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")
}
// 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, HasSetTime)
for i := range atoms {
atoms[i].ComputeUIFields()
}
// Sort atoms: by DueDate (earliest first), then by HasSetTime, then by Priority
sort.SliceStable(atoms, func(i, j int) bool {
// Handle nil due dates (push to end)
if atoms[i].DueDate == nil && atoms[j].DueDate != nil {
return false
}
if atoms[i].DueDate != nil && atoms[j].DueDate == nil {
return true
}
// Both have due dates
if atoms[i].DueDate != nil && atoms[j].DueDate != nil {
// Compare by date only (ignore time)
dateI := atoms[i].DueDate.Truncate(24 * time.Hour)
dateJ := atoms[j].DueDate.Truncate(24 * time.Hour)
if !dateI.Equal(dateJ) {
return dateI.Before(dateJ)
}
// Same day: tasks with set times come before midnight tasks
if atoms[i].HasSetTime != atoms[j].HasSetTime {
return atoms[i].HasSetTime
}
// Both have set times or both are midnight, sort by actual time
if atoms[i].HasSetTime && atoms[j].HasSetTime {
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
})
// Render template
data := struct {
Atoms []models.Atom
Boards []models.Board
Today string
}{
Atoms: atoms,
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)
}
}