summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/widget.go153
-rw-r--r--internal/handlers/widget_test.go159
2 files changed, 312 insertions, 0 deletions
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go
index bff7dd7..5e515c3 100644
--- a/internal/handlers/widget.go
+++ b/internal/handlers/widget.go
@@ -777,3 +777,156 @@ func (h *Handler) HandleWidgetTaskNextDate(w http.ResponseWriter, r *http.Reques
}
w.WriteHeader(http.StatusOK)
}
+
+type availabilityBlockResponse struct {
+ ID string `json:"id"`
+ Weekday int `json:"weekday"`
+ StartTime string `json:"start_time"`
+ EndTime string `json:"end_time"`
+ Label string `json:"label"`
+}
+
+func availabilityBlockToResponse(b models.AvailabilityBlock) availabilityBlockResponse {
+ return availabilityBlockResponse{ID: b.ID, Weekday: b.Weekday, StartTime: b.StartTime, EndTime: b.EndTime, Label: b.Label}
+}
+
+// HandleWidgetAvailabilityGet returns every configured availability block.
+func (h *Handler) HandleWidgetAvailabilityGet(w http.ResponseWriter, r *http.Request) {
+ blocks, err := h.store.GetAvailabilityBlocks()
+ if err != nil {
+ http.Error(w, "failed to load availability", http.StatusInternalServerError)
+ return
+ }
+ resp := make([]availabilityBlockResponse, 0, len(blocks))
+ for _, b := range blocks {
+ resp = append(resp, availabilityBlockToResponse(b))
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(resp)
+}
+
+type availabilityCreateRequest struct {
+ Weekday int `json:"weekday"`
+ StartTime string `json:"start_time"`
+ EndTime string `json:"end_time"`
+ Label string `json:"label"`
+}
+
+// HandleWidgetAvailabilityCreate creates a new weekly availability block.
+func (h *Handler) HandleWidgetAvailabilityCreate(w http.ResponseWriter, r *http.Request) {
+ var req availabilityCreateRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "bad request", http.StatusBadRequest)
+ return
+ }
+ if req.Weekday < 0 || req.Weekday > 6 || req.StartTime == "" || req.EndTime == "" {
+ http.Error(w, "weekday (0-6), start_time, and end_time are required", http.StatusBadRequest)
+ return
+ }
+ block, err := h.store.CreateAvailabilityBlock(req.Weekday, req.StartTime, req.EndTime, req.Label)
+ if err != nil {
+ http.Error(w, "failed to create availability block", http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(availabilityBlockToResponse(*block))
+}
+
+type availabilityDeleteRequest struct {
+ ID string `json:"id"`
+}
+
+// HandleWidgetAvailabilityDelete deletes an availability block.
+func (h *Handler) HandleWidgetAvailabilityDelete(w http.ResponseWriter, r *http.Request) {
+ var req availabilityDeleteRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "bad request", http.StatusBadRequest)
+ return
+ }
+ if err := h.store.DeleteAvailabilityBlock(req.ID); err != nil {
+ if errors.Is(err, store.ErrNativeTaskNotFound) {
+ http.Error(w, "availability block not found", http.StatusNotFound)
+ return
+ }
+ http.Error(w, "failed to delete availability block", http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+type taskEstimateRequest struct {
+ ID string `json:"id"`
+ EstimatedMinutes int `json:"estimated_minutes"`
+}
+
+// HandleWidgetTaskEstimate sets a task's estimated duration in minutes.
+func (h *Handler) HandleWidgetTaskEstimate(w http.ResponseWriter, r *http.Request) {
+ var req taskEstimateRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "bad request", http.StatusBadRequest)
+ return
+ }
+ if req.ID == "" {
+ http.Error(w, "id is required", http.StatusBadRequest)
+ return
+ }
+ if err := h.store.SetTaskEstimate(req.ID, req.EstimatedMinutes); err != nil {
+ if errors.Is(err, store.ErrNativeTaskNotFound) {
+ http.Error(w, "task not found", http.StatusNotFound)
+ return
+ }
+ http.Error(w, "failed to set estimate", http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+type projectBudgetTrackedRequest struct {
+ ID string `json:"id"`
+ Tracked bool `json:"tracked"`
+}
+
+// HandleWidgetProjectsBudgetTracked opts a project in or out of budget tracking.
+func (h *Handler) HandleWidgetProjectsBudgetTracked(w http.ResponseWriter, r *http.Request) {
+ var req projectBudgetTrackedRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "bad request", http.StatusBadRequest)
+ return
+ }
+ if req.ID == "" {
+ http.Error(w, "id is required", http.StatusBadRequest)
+ return
+ }
+ if err := h.store.SetProjectBudgetTracked(req.ID, req.Tracked); err != nil {
+ if errors.Is(err, store.ErrNativeTaskNotFound) {
+ http.Error(w, "project not found", http.StatusNotFound)
+ return
+ }
+ http.Error(w, "failed to update project", http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+type labelBudgetTrackedRequest struct {
+ Name string `json:"name"`
+ Tracked bool `json:"tracked"`
+}
+
+// HandleWidgetLabelsBudgetTracked opts a label in or out of budget tracking.
+func (h *Handler) HandleWidgetLabelsBudgetTracked(w http.ResponseWriter, r *http.Request) {
+ var req labelBudgetTrackedRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "bad request", http.StatusBadRequest)
+ return
+ }
+ if req.Name == "" {
+ http.Error(w, "name is required", http.StatusBadRequest)
+ return
+ }
+ if err := h.store.SetLabelBudgetTracked(req.Name, req.Tracked); err != nil {
+ http.Error(w, "failed to update label", http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go
index a638eca..10a5c02 100644
--- a/internal/handlers/widget_test.go
+++ b/internal/handlers/widget_test.go
@@ -1268,3 +1268,162 @@ func TestHandleWidgetGet_BudgetTrackedTaskDueToday_IncludesBudgetStatus(t *testi
}
}
+
+func TestHandleWidgetAvailabilityGet_ReturnsBlocks(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: db}
+ if _, err := h.store.CreateAvailabilityBlock(1, "18:00", "20:00", "evening"); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("GET", "/api/widget/availability", nil)
+ w := httptest.NewRecorder()
+ h.HandleWidgetAvailabilityGet(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", w.Code)
+ }
+ var blocks []models.AvailabilityBlock
+ if err := json.NewDecoder(w.Body).Decode(&blocks); err != nil {
+ t.Fatal(err)
+ }
+ if len(blocks) != 1 || blocks[0].Label != "evening" {
+ t.Errorf("blocks = %+v", blocks)
+ }
+}
+
+func TestHandleWidgetAvailabilityCreate_CreatesBlock(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: db}
+
+ body := `{"weekday":2,"start_time":"07:00","end_time":"08:00","label":"morning walk"}`
+ req := httptest.NewRequest("POST", "/api/widget/availability", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ h.HandleWidgetAvailabilityCreate(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", w.Code)
+ }
+ blocks, err := h.store.GetAvailabilityBlocks()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(blocks) != 1 || blocks[0].Weekday != 2 {
+ t.Errorf("blocks = %+v", blocks)
+ }
+}
+
+func TestHandleWidgetAvailabilityDelete_RemovesBlock(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: db}
+ block, err := h.store.CreateAvailabilityBlock(3, "09:00", "10:00", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ body := `{"id":"` + block.ID + `"}`
+ req := httptest.NewRequest("POST", "/api/widget/availability/delete", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ h.HandleWidgetAvailabilityDelete(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", w.Code)
+ }
+ blocks, err := h.store.GetAvailabilityBlocks()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(blocks) != 0 {
+ t.Errorf("expected block deleted, got %+v", blocks)
+ }
+}
+
+func TestHandleWidgetAvailabilityDelete_UnknownID_Returns404(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: db}
+
+ req := httptest.NewRequest("POST", "/api/widget/availability/delete", strings.NewReader(`{"id":"nope"}`))
+ w := httptest.NewRecorder()
+ h.HandleWidgetAvailabilityDelete(w, req)
+
+ if w.Code != http.StatusNotFound {
+ t.Errorf("status = %d, want 404", w.Code)
+ }
+}
+
+func TestHandleWidgetTaskEstimate_SetsEstimate(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: db}
+ if err := h.store.CreateNativeTask(models.Task{ID: "t-1", Content: "task"}); err != nil {
+ t.Fatal(err)
+ }
+
+ body := `{"id":"t-1","estimated_minutes":25}`
+ req := httptest.NewRequest("POST", "/api/widget/task/estimate", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ h.HandleWidgetTaskEstimate(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", w.Code)
+ }
+ task, err := h.store.GetNativeTaskByID("t-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if task.EstimatedMinutes != 25 {
+ t.Errorf("EstimatedMinutes = %d, want 25", task.EstimatedMinutes)
+ }
+}
+
+func TestHandleWidgetProjectsBudgetTracked_SetsFlag(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: db}
+ project, err := h.store.CreateProject("P", "#111111")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ body := `{"id":"` + project.ID + `","tracked":true}`
+ req := httptest.NewRequest("POST", "/api/widget/projects/budget-tracked", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ h.HandleWidgetProjectsBudgetTracked(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", w.Code)
+ }
+ got, err := h.store.GetProjectByID(project.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !got.BudgetTracked {
+ t.Error("expected BudgetTracked = true")
+ }
+}
+
+func TestHandleWidgetLabelsBudgetTracked_SetsFlag(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: db}
+
+ body := `{"name":"errands","tracked":true}`
+ req := httptest.NewRequest("POST", "/api/widget/labels/budget-tracked", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ h.HandleWidgetLabelsBudgetTracked(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", w.Code)
+ }
+ names, err := h.store.GetBudgetTrackedLabelNames()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !names["errands"] {
+ t.Error("expected 'errands' tracked")
+ }
+}