package handlers import ( "net/http" "strconv" "time" "task-dashboard/internal/config" "task-dashboard/internal/models" ) // TimelineData holds grouped timeline items for the template type TimelineData struct { TodayItems []models.TimelineItem TomorrowItems []models.TimelineItem LaterItems []models.TimelineItem Start time.Time Days int // Section labels with day of week TodayLabel string // e.g., "Today - Monday" TomorrowLabel string // e.g., "Tomorrow - Tuesday" LaterLabel string // e.g., "Wednesday, Jan 29" // Calendar view bounds (1 hour before first event, 1 hour after last) TodayStartHour int TodayEndHour int TodayHours []int // Slice of hours to render TomorrowStartHour int TomorrowEndHour int TomorrowHours []int // Current time for "now" line NowHour int NowMinute int } // HandleTimeline renders the timeline view func (h *Handler) HandleTimeline(w http.ResponseWriter, r *http.Request) { // Parse query params startStr := r.URL.Query().Get("start") daysStr := r.URL.Query().Get("days") var start time.Time if startStr != "" { parsed, err := config.ParseDateInDisplayTZ(startStr) if err == nil { start = parsed } else { start = config.Today() } } else { start = config.Today() } days := 3 // Default if daysStr != "" { if d, err := strconv.Atoi(daysStr); err == nil && d > 0 { days = d } } end := start.AddDate(0, 0, days) // Call BuildTimeline items, err := BuildTimeline(r.Context(), h.store, h.googleCalendarClient, h.googleTasksClient, start, end) if err != nil { JSONError(w, http.StatusInternalServerError, "Failed to build timeline", err) return } // Compute section labels with day of week now := config.Now() today := config.Today() tomorrow := today.AddDate(0, 0, 1) dayAfterTomorrow := today.AddDate(0, 0, 2) // Group items by day section data := TimelineData{ Start: start, Days: days, TodayLabel: "Today - " + now.Format("Monday"), TomorrowLabel: "Tomorrow - " + tomorrow.Format("Monday"), LaterLabel: dayAfterTomorrow.Format("Monday, Jan 2") + "+", NowHour: now.Hour(), NowMinute: now.Minute(), } for _, item := range items { switch item.DaySection { case models.DaySectionToday: data.TodayItems = append(data.TodayItems, item) case models.DaySectionTomorrow: data.TomorrowItems = append(data.TomorrowItems, item) case models.DaySectionLater: data.LaterItems = append(data.LaterItems, item) } } // Calculate calendar bounds for Today (1 hour buffer before/after timed events) data.TodayStartHour, data.TodayEndHour = calcCalendarBounds(data.TodayItems, now.Hour()) for h := data.TodayStartHour; h <= data.TodayEndHour; h++ { data.TodayHours = append(data.TodayHours, h) } // Calculate calendar bounds for Tomorrow data.TomorrowStartHour, data.TomorrowEndHour = calcCalendarBounds(data.TomorrowItems, -1) for h := data.TomorrowStartHour; h <= data.TomorrowEndHour; h++ { data.TomorrowHours = append(data.TomorrowHours, h) } HTMLResponse(w, h.renderer, "timeline-tab", data) } // calcCalendarBounds returns start/end hours for calendar view based on timed events. // If currentHour >= 0, it's included in the range (for "now" line visibility). // Returns hours clamped to 0-23 with 1-hour buffer before/after events. func calcCalendarBounds(items []models.TimelineItem, currentHour int) (startHour, endHour int) { minHour := 23 maxHour := 0 hasTimedEvents := false for _, item := range items { // Skip all-day/overdue items (midnight with no real time) if item.IsAllDay || item.IsOverdue { continue } h := item.Time.Hour() // Skip midnight items unless they have an end time if h == 0 && item.Time.Minute() == 0 && item.EndTime == nil { continue } hasTimedEvents = true if h < minHour { minHour = h } endH := h if item.EndTime != nil { endH = item.EndTime.Hour() } if endH > maxHour { maxHour = endH } } // Include current hour if provided if currentHour >= 0 { hasTimedEvents = true if currentHour < minHour { minHour = currentHour } if currentHour > maxHour { maxHour = currentHour } } if !hasTimedEvents { // Default: show 8am-6pm return 8, 18 } // Add 1 hour buffer, clamp to valid range startHour = minHour - 1 if startHour < 0 { startHour = 0 } endHour = maxHour + 1 if endHour > 23 { endHour = 23 } return startHour, endHour }