package models import ( "time" "task-dashboard/internal/config" ) type TimelineItemType string const ( TimelineItemTypeTask TimelineItemType = "task" TimelineItemTypeMeal TimelineItemType = "meal" TimelineItemTypeCard TimelineItemType = "card" TimelineItemTypeEvent TimelineItemType = "event" TimelineItemTypeGTask TimelineItemType = "gtask" // Google Tasks ) type DaySection string const ( DaySectionToday DaySection = "today" DaySectionTomorrow DaySection = "tomorrow" DaySectionLater DaySection = "later" ) type TimelineItem struct { ID string `json:"id"` Type TimelineItemType `json:"type"` Title string `json:"title"` Time time.Time `json:"time"` EndTime *time.Time `json:"end_time,omitempty"` Description string `json:"description,omitempty"` URL string `json:"url,omitempty"` OriginalItem interface{} `json:"-"` // UI enhancement fields IsCompleted bool `json:"is_completed"` IsOverdue bool `json:"is_overdue"` IsAllDay bool `json:"is_all_day"` // True if time is midnight (no specific time) DaySection DaySection `json:"day_section"` Source string `json:"source"` // "trello", "plantoeat", "calendar", "gtasks" // Undated is true only for genuinely dateless Task/GTask items (the // caller uses Time=now as a layout placeholder for them -- see // timeline_logic.go's nativeUndated/gtask-no-due-date branches). // Deliberately NOT the same thing as IsAllDay: doot-native and Google // Tasks due dates are ALWAYS midnight-anchored even when a task has a // real due date (see reference_google_tasks_due_date_utc_quirk), so // IsAllDay ends up true for both "no due date" and "has a due date, but // it's date-only" -- a distinction the WEB UI doesn't need (it buckets // by DaySection, computed from Time regardless of IsAllDay, and only // uses IsAllDay for a same-day visual choice that's fine either way for // tasks) but the ANDROID WIDGET does: TimelineItemToWidgetItem uses // Undated, not IsAllDay, to decide whether to populate wi.Start, because // the widget's undated/floating pool has no per-day awareness at all -- // conflating "no date" with "dated but time-only-midnight" there means // a task due tomorrow renders as if due today (2026-08-10 report). Undated bool `json:"-"` // Source-specific metadata ListID string `json:"list_id,omitempty"` // For Google Tasks RecurringEventID string `json:"recurring_event_id,omitempty"` // For calendar events ProjectColor string `json:"project_color,omitempty"` // For doot-native tasks with a project assigned ChainPosition int `json:"chain_position,omitempty"` // For doot-native tasks that belong to a linear chain (1-indexed for display) ChainTotal int `json:"chain_total,omitempty"` // Total tasks in the chain, paired with ChainPosition BucketState string `json:"bucket_state,omitempty"` // "active" for doot-native tasks that are a maintenance-bucket item (dormant items never reach the timeline) } // ComputeDaySection sets the DaySection, IsOverdue, and IsAllDay based on the item's time func (item *TimelineItem) ComputeDaySection(now time.Time) { // Use configured display timezone for consistent comparisons tz := config.GetDisplayTimezone() localNow := now.In(tz) localItemTime := item.Time.In(tz) today := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), 0, 0, 0, 0, tz) tomorrow := today.AddDate(0, 0, 1) dayAfterTomorrow := today.AddDate(0, 0, 2) itemDay := time.Date(localItemTime.Year(), localItemTime.Month(), localItemTime.Day(), 0, 0, 0, 0, tz) // Check if item is overdue (before today) item.IsOverdue = itemDay.Before(today) // Check if item is all-day (midnight time means no specific time). // Only ever sets IsAllDay to true here -- never clears a true a caller // already set (e.g. nativeUndated/gtask "no due date" tasks, which use // IsAllDay as a floating-task marker with Time set to "now", not // midnight -- clobbering it back to false made them look like real // scheduled items to the widget client). if localItemTime.Hour() == 0 && localItemTime.Minute() == 0 { item.IsAllDay = true } if itemDay.Before(tomorrow) { item.DaySection = DaySectionToday } else if itemDay.Before(dayAfterTomorrow) { item.DaySection = DaySectionTomorrow } else { item.DaySection = DaySectionLater } }