From 7e2386c0ebbc2d1cafc670ad4fd05b884f8b8fa7 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 15 Jul 2026 18:23:38 +0000 Subject: feat(tasks): add HTTP endpoints for projects and label colors Extends the task-detail response with project/labels so the popup can display them without a second round trip. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD --- internal/handlers/widget.go | 156 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) (limited to 'internal') diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index b8c3aa9..1bb706e 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -400,6 +400,154 @@ type recurrenceResponse struct { Weekdays []int `json:"weekdays,omitempty"` } +type projectResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Color string `json:"color"` + CreatedAt time.Time `json:"created_at"` +} + +func projectToResponse(p models.Project) projectResponse { + return projectResponse{ID: p.ID, Name: p.Name, Color: p.Color, CreatedAt: p.CreatedAt} +} + +// HandleWidgetProjectsGet returns all non-archived projects. +func (h *Handler) HandleWidgetProjectsGet(w http.ResponseWriter, r *http.Request) { + projects, err := h.store.GetProjects() + if err != nil { + http.Error(w, "failed to load projects", http.StatusInternalServerError) + return + } + resp := make([]projectResponse, 0, len(projects)) + for _, p := range projects { + resp = append(resp, projectToResponse(p)) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) +} + +type projectCreateRequest struct { + Name string `json:"name"` + Color string `json:"color"` +} + +// HandleWidgetProjectsCreate creates a new project. +func (h *Handler) HandleWidgetProjectsCreate(w http.ResponseWriter, r *http.Request) { + var req projectCreateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if req.Name == "" || req.Color == "" { + http.Error(w, "name and color are required", http.StatusBadRequest) + return + } + project, err := h.store.CreateProject(req.Name, req.Color) + if err != nil { + http.Error(w, "failed to create project", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(projectToResponse(*project)) +} + +type taskProjectRequest struct { + ID string `json:"id"` + ProjectID string `json:"project_id"` +} + +// HandleWidgetTaskProject sets or clears a task's project. +func (h *Handler) HandleWidgetTaskProject(w http.ResponseWriter, r *http.Request) { + var req taskProjectRequest + 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.SetTaskProject(req.ID, req.ProjectID); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found", http.StatusNotFound) + return + } + http.Error(w, "failed to set project", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +type taskLabelsRequest struct { + ID string `json:"id"` + Labels []string `json:"labels"` +} + +// HandleWidgetTaskLabels replaces a task's label set. +func (h *Handler) HandleWidgetTaskLabels(w http.ResponseWriter, r *http.Request) { + var req taskLabelsRequest + 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.SetTaskLabels(req.ID, req.Labels); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found", http.StatusNotFound) + return + } + http.Error(w, "failed to set labels", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +type labelColorResponse struct { + Name string `json:"name"` + Color string `json:"color"` +} + +// HandleWidgetLabelsGet returns every label that has an assigned color. +func (h *Handler) HandleWidgetLabelsGet(w http.ResponseWriter, r *http.Request) { + colors, err := h.store.GetLabelColors() + if err != nil { + http.Error(w, "failed to load label colors", http.StatusInternalServerError) + return + } + resp := make([]labelColorResponse, 0, len(colors)) + for _, c := range colors { + resp = append(resp, labelColorResponse{Name: c.Name, Color: c.Color}) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) +} + +type labelColorSetRequest struct { + Name string `json:"name"` + Color string `json:"color"` +} + +// HandleWidgetLabelsColorSet assigns a label's display color. +func (h *Handler) HandleWidgetLabelsColorSet(w http.ResponseWriter, r *http.Request) { + var req labelColorSetRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if req.Name == "" || req.Color == "" { + http.Error(w, "name and color are required", http.StatusBadRequest) + return + } + if err := h.store.SetLabelColor(req.Name, req.Color); err != nil { + http.Error(w, "failed to set label color", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + type taskDetailResponse struct { ID string `json:"id"` Title string `json:"title"` @@ -408,6 +556,8 @@ type taskDetailResponse struct { Completed bool `json:"completed"` Recurrence *recurrenceResponse `json:"recurrence,omitempty"` NextDate *time.Time `json:"next_date,omitempty"` + Project *projectResponse `json:"project,omitempty"` + Labels []string `json:"labels,omitempty"` } // HandleWidgetTaskDetail returns full detail for a single doot-native task, @@ -436,6 +586,12 @@ func (h *Handler) HandleWidgetTaskDetail(w http.ResponseWriter, r *http.Request) Description: task.Description, DueDate: task.DueDate, Completed: task.Completed, + Labels: task.Labels, + } + if task.ProjectID != "" { + if project, err := h.store.GetProjectByID(task.ProjectID); err == nil { + resp.Project = &projectResponse{ID: project.ID, Name: project.Name, Color: project.Color} + } } if task.RecurrenceSeriesID != "" { resp.Recurrence = &recurrenceResponse{ -- cgit v1.2.3