package handlers
import (
"html/template"
"log"
"net/http"
"path/filepath"
"sort"
"strings"
"time"
"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
templates *template.Template
}
// NewTabsHandler creates a new TabsHandler instance
func NewTabsHandler(store *store.Store, 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,
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)
}
}
// HandlePlanning renders the Planning tab (Trello boards)
func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) {
// 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
}
data := struct {
Boards []models.Board
Projects []models.Project
}{
Boards: boards,
Projects: []models.Project{}, // Empty for now
}
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)
}
}