package handlers import ( "html/template" "log" "net/http" "path/filepath" "sort" "strings" "time" "task-dashboard/internal/api" "task-dashboard/internal/models" "task-dashboard/internal/store" ) // isActionableList returns true if the list name indicates an actionable list func isActionableList(name string) bool { lower := strings.ToLower(name) return strings.Contains(lower, "doing") || strings.Contains(lower, "in progress") || strings.Contains(lower, "to do") || strings.Contains(lower, "todo") || strings.Contains(lower, "tasks") || strings.Contains(lower, "next") || strings.Contains(lower, "today") } // atomUrgencyTier returns the urgency tier for sorting: // 0: Overdue, 1: Today with time, 2: Today all-day, 3: Future, 4: No due date func atomUrgencyTier(a models.Atom) int { if a.DueDate == nil { return 4 // No due date } if a.IsOverdue { return 0 // Overdue } if a.IsFuture { return 3 // Future } // Due today if a.HasSetTime { return 1 // Today with specific time } return 2 // Today all-day } // TabsHandler handles tab-specific rendering with Atom model type TabsHandler struct { store *store.Store googleCalendarClient api.GoogleCalendarAPI templates *template.Template } // NewTabsHandler creates a new TabsHandler instance func NewTabsHandler(store *store.Store, googleCalendarClient api.GoogleCalendarAPI, templateDir string) *TabsHandler { // Parse templates including partials tmpl, err := template.ParseGlob(filepath.Join(templateDir, "*.html")) if err != nil { log.Printf("Warning: failed to parse templates: %v", err) } // Also parse partials tmpl, err = tmpl.ParseGlob(filepath.Join(templateDir, "partials", "*.html")) if err != nil { log.Printf("Warning: failed to parse partial templates: %v", err) } return &TabsHandler{ store: store, googleCalendarClient: googleCalendarClient, templates: tmpl, } } // HandleTasks renders the unified Tasks tab (Todoist + Trello cards with due dates) func (h *TabsHandler) HandleTasks(w http.ResponseWriter, r *http.Request) { // Fetch Todoist tasks tasks, err := h.store.GetTasks() if err != nil { http.Error(w, "Failed to fetch tasks", http.StatusInternalServerError) log.Printf("Error fetching tasks: %v", err) return } // Fetch Trello boards boards, err := h.store.GetBoards() if err != nil { http.Error(w, "Failed to fetch boards", http.StatusInternalServerError) log.Printf("Error fetching boards: %v", err) return } // Convert to Atoms atoms := make([]models.Atom, 0) // Convert Todoist tasks for _, task := range tasks { if !task.Completed { atoms = append(atoms, models.TaskToAtom(task)) } } // Convert Trello cards with due dates or in actionable lists for _, board := range boards { for _, card := range board.Cards { if card.DueDate != nil || isActionableList(card.ListName) { atoms = append(atoms, models.CardToAtom(card)) } } } // Compute UI fields (IsOverdue, IsFuture, HasSetTime) for i := range atoms { atoms[i].ComputeUIFields() } // Sort atoms by urgency tiers: // 1. Overdue (before today) // 2. Today with specific time // 3. Today all-day (midnight) // 4. Future // 5. No due date // Within each tier: sort by due date/time, then by priority sort.SliceStable(atoms, func(i, j int) bool { // Compute urgency tier (lower = more urgent) tierI := atomUrgencyTier(atoms[i]) tierJ := atomUrgencyTier(atoms[j]) if tierI != tierJ { return tierI < tierJ } // Same tier: sort by due date/time if both have dates if atoms[i].DueDate != nil && atoms[j].DueDate != nil { if !atoms[i].DueDate.Equal(*atoms[j].DueDate) { return atoms[i].DueDate.Before(*atoms[j].DueDate) } } // Same due date/time (or both nil), sort by priority (descending) return atoms[i].Priority > atoms[j].Priority }) // Partition atoms into current (overdue + today) and future var currentAtoms, futureAtoms []models.Atom for _, a := range atoms { if a.IsFuture { futureAtoms = append(futureAtoms, a) } else { currentAtoms = append(currentAtoms, a) } } // Render template data := struct { Atoms []models.Atom // Current tasks (overdue + today) FutureAtoms []models.Atom // Future tasks (hidden by default) Boards []models.Board Today string }{ Atoms: currentAtoms, FutureAtoms: futureAtoms, Boards: boards, Today: time.Now().Format("2006-01-02"), } if err := h.templates.ExecuteTemplate(w, "tasks-tab", data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Error rendering tasks tab: %v", err) } } // ScheduledItem represents a scheduled event or task for the planning view type ScheduledItem struct { Type string // "event" or "task" ID string Title string Description string Start time.Time End time.Time URL string Source string // "todoist", "trello", "calendar" SourceIcon string Priority int } // HandlePlanning renders the Planning tab with structured sections func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) { now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) tomorrow := today.AddDate(0, 0, 1) in3Days := today.AddDate(0, 0, 4) // End of 3rd day // Fetch Trello boards boards, err := h.store.GetBoards() if err != nil { log.Printf("Error fetching boards: %v", err) boards = []models.Board{} } // Fetch Todoist tasks tasks, err := h.store.GetTasks() if err != nil { log.Printf("Error fetching tasks: %v", err) tasks = []models.Task{} } // Fetch Google Calendar events var events []models.CalendarEvent if h.googleCalendarClient != nil { events, err = h.googleCalendarClient.GetUpcomingEvents(r.Context(), 20) if err != nil { log.Printf("Error fetching calendar events: %v", err) } } // Categorize into sections var scheduled []ScheduledItem // Events and timed tasks for today var unscheduled []models.Atom // Tasks due today without specific time var upcoming []ScheduledItem // Events and tasks for next 3 days // Process calendar events for _, event := range events { item := ScheduledItem{ Type: "event", ID: event.ID, Title: event.Summary, Description: event.Description, Start: event.Start, End: event.End, URL: event.HTMLLink, Source: "calendar", SourceIcon: "📅", } if event.Start.Before(tomorrow) { scheduled = append(scheduled, item) } else if event.Start.Before(in3Days) { upcoming = append(upcoming, item) } } // Process Todoist tasks for _, task := range tasks { if task.Completed || task.DueDate == nil { continue } dueDate := *task.DueDate // Check if task has a specific time (not midnight) hasTime := dueDate.Hour() != 0 || dueDate.Minute() != 0 if dueDate.Before(tomorrow) { if hasTime { // Timed task for today -> scheduled scheduled = append(scheduled, ScheduledItem{ Type: "task", ID: task.ID, Title: task.Content, Start: dueDate, URL: task.URL, Source: "todoist", SourceIcon: "✓", Priority: task.Priority, }) } else { // All-day task for today -> unscheduled atom := models.TaskToAtom(task) atom.ComputeUIFields() unscheduled = append(unscheduled, atom) } } else if dueDate.Before(in3Days) { upcoming = append(upcoming, ScheduledItem{ Type: "task", ID: task.ID, Title: task.Content, Start: dueDate, URL: task.URL, Source: "todoist", SourceIcon: "✓", Priority: task.Priority, }) } } // Process Trello cards with due dates for _, board := range boards { for _, card := range board.Cards { if card.DueDate == nil { continue } dueDate := *card.DueDate hasTime := dueDate.Hour() != 0 || dueDate.Minute() != 0 if dueDate.Before(tomorrow) { if hasTime { scheduled = append(scheduled, ScheduledItem{ Type: "task", ID: card.ID, Title: card.Name, Start: dueDate, URL: card.URL, Source: "trello", SourceIcon: "📋", }) } else { atom := models.CardToAtom(card) atom.ComputeUIFields() unscheduled = append(unscheduled, atom) } } else if dueDate.Before(in3Days) { upcoming = append(upcoming, ScheduledItem{ Type: "task", ID: card.ID, Title: card.Name, Start: dueDate, URL: card.URL, Source: "trello", SourceIcon: "📋", }) } } } // Sort scheduled by start time sort.Slice(scheduled, func(i, j int) bool { return scheduled[i].Start.Before(scheduled[j].Start) }) // Sort unscheduled by priority (higher first) sort.Slice(unscheduled, func(i, j int) bool { return unscheduled[i].Priority > unscheduled[j].Priority }) // Sort upcoming by date sort.Slice(upcoming, func(i, j int) bool { return upcoming[i].Start.Before(upcoming[j].Start) }) data := struct { Scheduled []ScheduledItem Unscheduled []models.Atom Upcoming []ScheduledItem Boards []models.Board Today string }{ Scheduled: scheduled, Unscheduled: unscheduled, Upcoming: upcoming, Boards: boards, Today: today.Format("2006-01-02"), } if err := h.templates.ExecuteTemplate(w, "planning-tab", data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Error rendering planning tab: %v", err) } } // HandleMeals renders the Meals tab (PlanToEat) func (h *TabsHandler) HandleMeals(w http.ResponseWriter, r *http.Request) { // Fetch meals for next 7 days startDate := time.Now() endDate := startDate.AddDate(0, 0, 7) meals, err := h.store.GetMeals(startDate, endDate) if err != nil { http.Error(w, "Failed to fetch meals", http.StatusInternalServerError) log.Printf("Error fetching meals: %v", err) return } data := struct { Meals []models.Meal }{ Meals: meals, } if err := h.templates.ExecuteTemplate(w, "meals-tab", data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Error rendering meals tab: %v", err) } }