summaryrefslogtreecommitdiff
path: root/internal/handlers/widget.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-14 20:35:55 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 02:44:52 +0000
commit146034b08446c4bf0099bb41ab5e689d418e6dce (patch)
tree46db3cca04eb3d57b58a5195bbe42d0c193a9043 /internal/handlers/widget.go
parentba5205213f6f1995ca4e64b08925bcc6c48b8211 (diff)
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.
Diffstat (limited to 'internal/handlers/widget.go')
-rw-r--r--internal/handlers/widget.go161
1 files changed, 161 insertions, 0 deletions
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)
+}