From 146034b08446c4bf0099bb41ab5e689d418e6dce Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Tue, 14 Jul 2026 20:35:55 +0000 Subject: feat(tasks): add HTTP endpoints for task detail, update, and recurrence GET /api/widget/task, POST /api/widget/task/update, POST /api/widget/task/recurrence, POST /api/widget/task/next-date. Doot-only; the recurrence/next-date fields are null in the detail response for a non-recurring task. --- internal/handlers/widget.go | 161 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) (limited to 'internal/handlers/widget.go') diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index f0fd101..15ef3b2 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -393,3 +393,164 @@ func (h *Handler) HandleWidgetAdd(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + +type recurrenceResponse struct { + Freq string `json:"freq"` + Interval int `json:"interval"` + Weekdays []int `json:"weekdays,omitempty"` +} + +type taskDetailResponse struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + DueDate *time.Time `json:"due_date,omitempty"` + Completed bool `json:"completed"` + Recurrence *recurrenceResponse `json:"recurrence,omitempty"` + NextDate *time.Time `json:"next_date,omitempty"` +} + +// HandleWidgetTaskDetail returns full detail for a single doot-native task, +// including its recurrence pattern and computed next-occurrence date. +func (h *Handler) HandleWidgetTaskDetail(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + source := r.URL.Query().Get("source") + if id == "" || source != "doot" { + http.Error(w, "id required and source must be doot", http.StatusBadRequest) + return + } + + task, err := h.store.GetNativeTaskByID(id) + if err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found", http.StatusNotFound) + return + } + http.Error(w, "failed to load task", http.StatusInternalServerError) + return + } + + resp := taskDetailResponse{ + ID: task.ID, + Title: task.Content, + Description: task.Description, + DueDate: task.DueDate, + Completed: task.Completed, + } + if task.RecurrenceSeriesID != "" { + resp.Recurrence = &recurrenceResponse{ + Freq: task.RecurrenceFreq, + Interval: task.RecurrenceInterval, + Weekdays: task.RecurrenceWeekdays, + } + if task.NextOccurrenceOverride != nil { + resp.NextDate = task.NextOccurrenceOverride + } else if task.DueDate != nil { + next := models.ComputeNextOccurrence(*task.DueDate, task.RecurrenceFreq, task.RecurrenceInterval, task.RecurrenceWeekdays) + resp.NextDate = &next + } + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) +} + +type taskUpdateRequest struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` +} + +// HandleWidgetTaskUpdate updates a doot-native task's title and description. +func (h *Handler) HandleWidgetTaskUpdate(w http.ResponseWriter, r *http.Request) { + var req taskUpdateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if req.ID == "" || req.Title == "" { + http.Error(w, "id and title are required", http.StatusBadRequest) + return + } + if err := h.store.UpdateNativeTask(req.ID, req.Title, req.Description); err != nil { + http.Error(w, "failed to update task", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +type taskRecurrenceRequest struct { + ID string `json:"id"` + Freq string `json:"freq"` + Interval int `json:"interval"` + Weekdays []int `json:"weekdays"` +} + +// HandleWidgetTaskRecurrence sets or clears a doot-native task's recurrence +// pattern. freq == "" clears recurrence. +func (h *Handler) HandleWidgetTaskRecurrence(w http.ResponseWriter, r *http.Request) { + var req taskRecurrenceRequest + 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 req.Freq != "" && req.Freq != "daily" && req.Freq != "weekly" && req.Freq != "monthly" && req.Freq != "yearly" { + http.Error(w, "invalid freq", http.StatusBadRequest) + return + } + if err := h.store.SetTaskRecurrence(req.ID, req.Freq, req.Interval, req.Weekdays); err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found", http.StatusNotFound) + return + } + http.Error(w, "failed to update recurrence", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + +type taskNextDateRequest struct { + ID string `json:"id"` + Date string `json:"date"` // YYYY-MM-DD +} + +// HandleWidgetTaskNextDate sets a one-shot override for a recurring task's +// next occurrence. 400 if the task has no active recurrence. +func (h *Handler) HandleWidgetTaskNextDate(w http.ResponseWriter, r *http.Request) { + var req taskNextDateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + parsed, err := time.Parse("2006-01-02", req.Date) + if err != nil { + http.Error(w, "invalid date", http.StatusBadRequest) + return + } + + task, err := h.store.GetNativeTaskByID(req.ID) + if err != nil { + if errors.Is(err, store.ErrNativeTaskNotFound) { + http.Error(w, "task not found", http.StatusNotFound) + return + } + http.Error(w, "failed to load task", http.StatusInternalServerError) + return + } + if task.RecurrenceSeriesID == "" { + http.Error(w, "task has no active recurrence", http.StatusBadRequest) + return + } + + tz := config.GetDisplayTimezone() + dueDate := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, tz) + if err := h.store.SetNextOccurrenceOverride(req.ID, dueDate); err != nil { + http.Error(w, "failed to set next date", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} -- cgit v1.2.3