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)
}
}