summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-12 10:21:36 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-12 10:21:36 +0000
commit437ee6b10462909fb2db677a64be264d811a2e1c (patch)
tree29291d12c3590e5611eaf11e0a8182324fa7c42e /internal
parentc0454371a044ef4f20bfe44ee9f183fb336656dd (diff)
feat(widget): add DueDate field for doot tasks to the widget API
Diffstat (limited to 'internal')
-rw-r--r--internal/handlers/widget.go9
-rw-r--r--internal/handlers/widget_test.go71
-rw-r--r--internal/models/widget.go1
3 files changed, 81 insertions, 0 deletions
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)
}