From 437ee6b10462909fb2db677a64be264d811a2e1c Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 12 Jul 2026 10:21:36 +0000 Subject: feat(widget): add DueDate field for doot tasks to the widget API --- internal/handlers/widget.go | 9 +++++ internal/handlers/widget_test.go | 71 ++++++++++++++++++++++++++++++++++++++++ internal/models/widget.go | 1 + 3 files changed, 81 insertions(+) diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index f8a63a0..3a30fcf 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -76,6 +76,15 @@ func TimelineItemToWidgetItem(item models.TimelineItem) models.WidgetItem { } } + // DueDate is independent of Start/IsAllDay -- doot tasks deliberately + // keep Start nil (see the "floating task" doc comment above) so the + // client's SlotPacker positions them, but the Android detail popup + // still needs to know the real due date to display and reschedule it. + if item.Type == models.TimelineItemTypeTask && item.Source == "doot" && !item.Time.IsZero() { + due := item.Time + wi.DueDate = &due + } + return wi } diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go index 06e1749..14dd396 100644 --- a/internal/handlers/widget_test.go +++ b/internal/handlers/widget_test.go @@ -210,6 +210,77 @@ func TestTimelineItemToWidgetItem_NotOverdueByDefault(t *testing.T) { } } +// TestTimelineItemToWidgetItem_DootTaskGetsDueDate proves the 2026-07-12 +// clickable-reschedule fix: a doot task's raw due date must reach the +// client via a NEW field (DueDate) that is independent of Start/IsAllDay -- +// Start is deliberately left nil for doot tasks (see +// TestTimelineItemToWidgetItem_AllDayTask_KeepsFloatingBehavior) so the +// client's floating-task SlotPacker can position it, and that must keep +// working unchanged. Before this fix there was no way for the Android +// detail popup to know a doot task's current due date at all. +func TestTimelineItemToWidgetItem_DootTaskGetsDueDate(t *testing.T) { + due := time.Date(2026, 7, 15, 0, 0, 0, 0, time.Local) + item := models.TimelineItem{ + ID: "doot-1", + Title: "Pay the water bill", + Source: "doot", + Type: models.TimelineItemTypeTask, + Time: due, + IsAllDay: true, + } + + wi := TimelineItemToWidgetItem(item) + + if wi.DueDate == nil { + t.Fatal("expected DueDate to be set for a doot task with a real due date") + } + if !wi.DueDate.Equal(due) { + t.Errorf("DueDate = %v, want %v", *wi.DueDate, due) + } + // Start must stay nil -- this is the pre-existing floating-task + // behavior and this feature must not change it. + if wi.Start != nil { + t.Error("Start must remain nil for a doot task -- DueDate is a separate field, not a replacement") + } +} + +func TestTimelineItemToWidgetItem_UndatedDootTask_NilDueDate(t *testing.T) { + item := models.TimelineItem{ + ID: "doot-undated", + Title: "Someday task", + Source: "doot", + Type: models.TimelineItemTypeTask, + Time: time.Now(), + IsAllDay: true, + } + // Zero Time simulates the "no real due date" case at the field level; + // TimelineItemToWidgetItem's DueDate logic must guard on !Time.IsZero(). + item.Time = time.Time{} + + wi := TimelineItemToWidgetItem(item) + + if wi.DueDate != nil { + t.Error("expected DueDate to be nil when the source item has a zero Time") + } +} + +func TestTimelineItemToWidgetItem_CalendarEvent_NilDueDate(t *testing.T) { + start := time.Now() + item := models.TimelineItem{ + ID: "cal-1", + Title: "Team sync", + Source: "calendar", + Type: models.TimelineItemTypeEvent, + Time: start, + } + + wi := TimelineItemToWidgetItem(item) + + if wi.DueDate != nil { + t.Error("expected DueDate to stay nil for a non-doot item (calendar event)") + } +} + func TestHandleWidgetComplete_NonCompletable(t *testing.T) { h := &Handler{} body := `{"id":"x","source":"calendar"}` diff --git a/internal/models/widget.go b/internal/models/widget.go index e2876b0..54a6eee 100644 --- a/internal/models/widget.go +++ b/internal/models/widget.go @@ -14,6 +14,7 @@ type WidgetItem struct { End *time.Time `json:"end,omitempty"` IsAllDay bool `json:"is_all_day"` IsOverdue bool `json:"is_overdue"` + DueDate *time.Time `json:"due_date,omitempty"` // doot tasks only -- see TimelineItemToWidgetItem URL string `json:"url,omitempty"` Completable bool `json:"completable"` // true = doot task (checkbox shown) } -- cgit v1.2.3