summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/handlers/timeline.go21
-rw-r--r--internal/handlers/timeline_multiday_test.go46
2 files changed, 65 insertions, 2 deletions
diff --git a/internal/handlers/timeline.go b/internal/handlers/timeline.go
index e8eadde..9656d4c 100644
--- a/internal/handlers/timeline.go
+++ b/internal/handlers/timeline.go
@@ -24,6 +24,21 @@ func dateOnly(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
+// effectiveEndDay returns the last calendar day the item's occurrence
+// actually covers. Google Calendar's all-day End.Date is EXCLUSIVE -- a
+// single-day all-day event on July 13 has End = July 14 at midnight (see
+// TestParseEventTime_AllDayEvent in internal/api/google_calendar_test.go)
+// -- so for an all-day item this subtracts one day to get the real last
+// day. Timed events' End is already the real end instant, so no
+// adjustment is needed there.
+func effectiveEndDay(item models.TimelineItem, tz *time.Location) time.Time {
+ endDay := dateOnly(item.EndTime.In(tz))
+ if item.IsAllDay {
+ return endDay.AddDate(0, 0, -1)
+ }
+ return endDay
+}
+
// isMultiDayEvent reports whether item is a calendar event whose Start and
// End fall on different calendar days (in the display timezone). Only
// events are ever considered multi-day -- tasks, cards, meals, and Google
@@ -33,7 +48,9 @@ func isMultiDayEvent(item models.TimelineItem) bool {
return false
}
tz := config.GetDisplayTimezone()
- return !dateOnly(item.EndTime.In(tz)).Equal(dateOnly(item.Time.In(tz)))
+ startDay := dateOnly(item.Time.In(tz))
+ endDay := effectiveEndDay(item, tz)
+ return endDay.After(startDay)
}
// multiDayVariant returns how a multi-day item should render on renderDay:
@@ -44,7 +61,7 @@ func isMultiDayEvent(item models.TimelineItem) bool {
func multiDayVariant(item models.TimelineItem, renderDay time.Time) string {
tz := config.GetDisplayTimezone()
startDay := dateOnly(item.Time.In(tz))
- endDay := dateOnly(item.EndTime.In(tz))
+ endDay := effectiveEndDay(item, tz)
day := dateOnly(renderDay.In(tz))
switch {
diff --git a/internal/handlers/timeline_multiday_test.go b/internal/handlers/timeline_multiday_test.go
index 3dc2005..7af2cbe 100644
--- a/internal/handlers/timeline_multiday_test.go
+++ b/internal/handlers/timeline_multiday_test.go
@@ -81,3 +81,49 @@ func TestMultiDayVariant_DayOutsideSpan_ReturnsEmpty(t *testing.T) {
t.Errorf("variant = %q, want empty", got)
}
}
+
+func TestIsMultiDayEvent_SingleDayAllDayEvent_False(t *testing.T) {
+ // Exclusive end-date convention: a single-day all-day event on July 13
+ // has Start=Jul13 00:00, End=Jul14 00:00 (one day past its own day).
+ start, _ := time.Parse(time.RFC3339, "2026-07-13T00:00:00Z")
+ end, _ := time.Parse(time.RFC3339, "2026-07-14T00:00:00Z")
+ item := models.TimelineItem{
+ ID: "allday-1", Type: models.TimelineItemTypeEvent,
+ Time: start, EndTime: &end, IsAllDay: true,
+ }
+ if isMultiDayEvent(item) {
+ t.Error("expected a single-day all-day event (exclusive end-date) to not be multi-day")
+ }
+}
+
+func TestIsMultiDayEvent_MultiDayAllDayEvent_True(t *testing.T) {
+ // A 3-day all-day event (Jul 13, 14, 15) has End=Jul16 00:00 (exclusive).
+ start, _ := time.Parse(time.RFC3339, "2026-07-13T00:00:00Z")
+ end, _ := time.Parse(time.RFC3339, "2026-07-16T00:00:00Z")
+ item := models.TimelineItem{
+ ID: "allday-2", Type: models.TimelineItemTypeEvent,
+ Time: start, EndTime: &end, IsAllDay: true,
+ }
+ if !isMultiDayEvent(item) {
+ t.Error("expected a 3-day all-day event to be multi-day")
+ }
+}
+
+func TestMultiDayVariant_MultiDayAllDayEvent_LastRealDayIsEnds(t *testing.T) {
+ // The 3-day all-day event above (Jul 13-15 inclusive, End=Jul16 exclusive)
+ // must report "ends" on Jul 15 (its real last day), not Jul 16.
+ start, _ := time.Parse(time.RFC3339, "2026-07-13T00:00:00Z")
+ end, _ := time.Parse(time.RFC3339, "2026-07-16T00:00:00Z")
+ item := models.TimelineItem{
+ ID: "allday-2", Type: models.TimelineItemTypeEvent,
+ Time: start, EndTime: &end, IsAllDay: true,
+ }
+ renderDay, _ := time.Parse(time.RFC3339, "2026-07-15T00:00:00Z")
+ if got := multiDayVariant(item, renderDay); got != "ends" {
+ t.Errorf("variant on last real day = %q, want %q", got, "ends")
+ }
+ exclusiveEndDay, _ := time.Parse(time.RFC3339, "2026-07-16T00:00:00Z")
+ if got := multiDayVariant(item, exclusiveEndDay); got != "" {
+ t.Errorf("variant on the exclusive (non-real) end day = %q, want empty", got)
+ }
+}