diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-16 06:57:47 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-16 06:57:47 +0000 |
| commit | 8a200f7f56c2894349b3f2831cd82625655acc7f (patch) | |
| tree | 185ddc1e83e015b7186c8569a6a24caa7cd10004 /internal | |
| parent | de435fda250f93dc897fcb6414f418e7ed9b3595 (diff) | |
Suggest an estimate from same-project/label averages on task detail
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/handlers/widget.go | 37 | ||||
| -rw-r--r-- | internal/handlers/widget_test.go | 59 |
2 files changed, 87 insertions, 9 deletions
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index 5e515c3..5880314 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -612,15 +612,17 @@ func (h *Handler) HandleWidgetLabelsColorSet(w http.ResponseWriter, r *http.Requ } 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"` - Project *projectResponse `json:"project,omitempty"` - Labels []string `json:"labels,omitempty"` + 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"` + Project *projectResponse `json:"project,omitempty"` + Labels []string `json:"labels,omitempty"` + EstimatedMinutes int `json:"estimated_minutes,omitempty"` + SuggestedEstimateMinutes *int `json:"suggested_estimate_minutes,omitempty"` } // HandleWidgetTaskDetail returns full detail for a single doot-native task, @@ -670,6 +672,23 @@ func (h *Handler) HandleWidgetTaskDetail(w http.ResponseWriter, r *http.Request) } } + resp.EstimatedMinutes = task.EstimatedMinutes + if task.EstimatedMinutes == 0 { + if task.ProjectID != "" { + if avg, ok, err := h.store.AverageEstimateForProject(task.ProjectID); err == nil && ok { + resp.SuggestedEstimateMinutes = &avg + } + } + if resp.SuggestedEstimateMinutes == nil { + for _, label := range task.Labels { + if avg, ok, err := h.store.AverageEstimateForLabel(label); err == nil && ok { + resp.SuggestedEstimateMinutes = &avg + break + } + } + } + } + w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) } diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go index 10a5c02..02df111 100644 --- a/internal/handlers/widget_test.go +++ b/internal/handlers/widget_test.go @@ -1427,3 +1427,62 @@ func TestHandleWidgetLabelsBudgetTracked_SetsFlag(t *testing.T) { t.Error("expected 'errands' tracked") } } + +func TestHandleWidgetTaskDetail_SuggestsEstimateFromProjectAverage(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + h := &Handler{store: db} + project, err := h.store.CreateProject("Sailing prep", "#3B82F6") + if err != nil { + t.Fatal(err) + } + if err := h.store.CreateNativeTask(models.Task{ID: "t-past", Content: "past", ProjectID: project.ID, EstimatedMinutes: 40}); err != nil { + t.Fatal(err) + } + if err := h.store.CreateNativeTask(models.Task{ID: "t-new", Content: "new", ProjectID: project.ID}); err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("GET", "/api/widget/task?id=t-new&source=doot", nil) + w := httptest.NewRecorder() + h.HandleWidgetTaskDetail(w, req) + + var resp taskDetailResponse + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { + t.Fatal(err) + } + if resp.SuggestedEstimateMinutes == nil || *resp.SuggestedEstimateMinutes != 40 { + t.Errorf("SuggestedEstimateMinutes = %v, want pointer to 40", resp.SuggestedEstimateMinutes) + } +} + +func TestHandleWidgetTaskDetail_AlreadyEstimated_NoSuggestion(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) + } + if err := h.store.CreateNativeTask(models.Task{ID: "t-past", Content: "past", ProjectID: project.ID, EstimatedMinutes: 40}); err != nil { + t.Fatal(err) + } + if err := h.store.CreateNativeTask(models.Task{ID: "t-estimated", Content: "already estimated", ProjectID: project.ID, EstimatedMinutes: 15}); err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("GET", "/api/widget/task?id=t-estimated&source=doot", nil) + w := httptest.NewRecorder() + h.HandleWidgetTaskDetail(w, req) + + var resp taskDetailResponse + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { + t.Fatal(err) + } + if resp.SuggestedEstimateMinutes != nil { + t.Errorf("expected no suggestion for an already-estimated task, got %v", *resp.SuggestedEstimateMinutes) + } + if resp.EstimatedMinutes != 15 { + t.Errorf("EstimatedMinutes = %d, want 15", resp.EstimatedMinutes) + } +} |
