diff options
Diffstat (limited to 'internal/handlers/widget.go')
| -rw-r--r-- | internal/handlers/widget.go | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index a2a0837..de2e856 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/go-chi/chi/v5" + "task-dashboard/internal/config" "task-dashboard/internal/models" "task-dashboard/internal/store" @@ -40,6 +42,9 @@ func TimelineItemToWidgetItem(item models.TimelineItem) models.WidgetItem { IsOverdue: item.IsOverdue, URL: item.URL, RecurringEventID: item.RecurringEventID, + ChainPosition: item.ChainPosition, + ChainTotal: item.ChainTotal, + BucketState: item.BucketState, } switch item.Type { @@ -956,3 +961,197 @@ func (h *Handler) HandleWidgetLabelsBudgetTracked(w http.ResponseWriter, r *http } w.WriteHeader(http.StatusOK) } + +type chainCreateRequest struct { + Name string `json:"name"` + Tasks []string `json:"tasks"` +} + +type chainCreateResponse struct { + ID string `json:"id"` +} + +// HandleWidgetChainsCreate creates a linear task chain: a backing project +// plus one native_tasks row per title in req.Tasks, position 0 unlocked and +// due now, the rest locked with no due date. +func (h *Handler) HandleWidgetChainsCreate(w http.ResponseWriter, r *http.Request) { + var req chainCreateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if strings.TrimSpace(req.Name) == "" || len(req.Tasks) == 0 { + http.Error(w, "name and at least one task are required", http.StatusBadRequest) + return + } + chain, err := h.store.CreateChain(req.Name, req.Tasks) + if err != nil { + http.Error(w, "failed to create chain", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(chainCreateResponse{ID: chain.ID}) +} + +// handleWidgetChainSetStatus is the shared body for pause/resume/abandon -- +// each just sets a different status string on the chain named by the {id} +// URL param. +func (h *Handler) handleWidgetChainSetStatus(w http.ResponseWriter, r *http.Request, status string) { + id := chi.URLParam(r, "id") + if err := h.store.SetChainStatus(id, status); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "chain not found", http.StatusNotFound) + return + } + http.Error(w, "failed to update chain", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +// HandleWidgetChainsPause pauses a chain: the currently-unlocked task stays +// actionable, but completing it will not auto-advance until resumed. +func (h *Handler) HandleWidgetChainsPause(w http.ResponseWriter, r *http.Request) { + h.handleWidgetChainSetStatus(w, r, "paused") +} + +// HandleWidgetChainsResume reactivates a paused chain. +func (h *Handler) HandleWidgetChainsResume(w http.ResponseWriter, r *http.Request) { + h.handleWidgetChainSetStatus(w, r, "active") +} + +// HandleWidgetChainsAbandon marks a chain abandoned -- a terminal state +// distinguishable from "completed" in queries/reporting. +func (h *Handler) HandleWidgetChainsAbandon(w http.ResponseWriter, r *http.Request) { + h.handleWidgetChainSetStatus(w, r, "abandoned") +} + +type chainGetResponse struct { + Chain models.Chain `json:"chain"` + Tasks []models.Task `json:"tasks"` +} + +// HandleWidgetChainGet returns the full ordered checklist for a chain -- +// locked and unlocked tasks both, per the design's "visible in the tasks +// list" requirement met via this dedicated surface. +func (h *Handler) HandleWidgetChainGet(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + chain, err := h.store.GetChain(id) + if err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "chain not found", http.StatusNotFound) + return + } + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + tasks, err := h.store.GetChainTasks(id) + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(chainGetResponse{Chain: *chain, Tasks: tasks}) +} + +type bucketCreateRequest struct { + Name string `json:"name"` + CycleDays int `json:"cycle_days"` + PickN int `json:"pick_n"` +} + +// HandleWidgetBucketsGet returns every maintenance bucket. +func (h *Handler) HandleWidgetBucketsGet(w http.ResponseWriter, r *http.Request) { + buckets, err := h.store.GetBuckets() + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(buckets) +} + +// HandleWidgetBucketsCreate creates a new maintenance bucket. +func (h *Handler) HandleWidgetBucketsCreate(w http.ResponseWriter, r *http.Request) { + var req bucketCreateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if strings.TrimSpace(req.Name) == "" || req.CycleDays <= 0 || req.PickN <= 0 { + http.Error(w, "name, a positive cycle_days, and a positive pick_n are required", http.StatusBadRequest) + return + } + bucket, err := h.store.CreateBucket(req.Name, req.CycleDays, req.PickN) + if err != nil { + http.Error(w, "failed to create bucket", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(bucket) +} + +type bucketItemRequest struct { + TaskID string `json:"task_id"` +} + +// HandleWidgetBucketItemsAdd assigns an existing task to a bucket's pool. +func (h *Handler) HandleWidgetBucketItemsAdd(w http.ResponseWriter, r *http.Request) { + bucketID := chi.URLParam(r, "id") + var req bucketItemRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if err := h.store.AddBucketItem(bucketID, req.TaskID); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found", http.StatusNotFound) + return + } + http.Error(w, "failed to add bucket item", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +// HandleWidgetBucketItemsRemove clears a task's bucket membership. +func (h *Handler) HandleWidgetBucketItemsRemove(w http.ResponseWriter, r *http.Request) { + var req bucketItemRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if err := h.store.RemoveBucketItem(req.TaskID); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found", http.StatusNotFound) + return + } + http.Error(w, "failed to remove bucket item", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +type taskDeferRequest struct { + ID string `json:"id"` +} + +// HandleWidgetTaskDefer returns an active bucket item to its pool without +// crediting it as done -- distinct from Complete -- and triggers a fresh +// selection to backfill the freed slot. +func (h *Handler) HandleWidgetTaskDefer(w http.ResponseWriter, r *http.Request) { + var req taskDeferRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if err := h.store.DeferNativeTask(req.ID); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found or not an active bucket item", http.StatusNotFound) + return + } + http.Error(w, "failed to defer task", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} |
