package handlers import ( "html/template" "log" "net/http" "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) *TabsHandler { // Parse templates including partials tmpl, err := template.ParseGlob("web/templates/*.html") if err != nil { log.Printf("Warning: failed to parse templates: %v", err) } // Also parse partials tmpl, err = tmpl.ParseGlob("web/templates/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)) } } } // Sort atoms: by DueDate (earliest first), then by Priority (descending) 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, sort by date 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 (or both nil), sort by priority (descending) return atoms[i].Priority > atoms[j].Priority }) // Render template data := struct { Atoms []models.Atom Boards []models.Board }{ Atoms: atoms, Boards: boards, } 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 + Todoist tasks) 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 } // Fetch Todoist tasks tasks, err := h.store.GetTasks() if err != nil { log.Printf("Error fetching tasks: %v", err) tasks = []models.Task{} } data := struct { Boards []models.Board Tasks []models.Task Projects []models.Project }{ Boards: boards, Tasks: tasks, Projects: []models.Project{}, // Empty for now - form won't display but checkboxes will work } 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) } } // HandleNotes renders the Notes tab (Obsidian notes) func (h *TabsHandler) HandleNotes(w http.ResponseWriter, r *http.Request) { // Check for search query parameter query := r.URL.Query().Get("q") var notes []models.Note var err error // If search query is present, search notes; otherwise, get all notes if query != "" { notes, err = h.store.SearchNotes(query) } else { notes, err = h.store.GetNotes(20) } if err != nil { http.Error(w, "Failed to fetch notes", http.StatusInternalServerError) log.Printf("Error fetching notes: %v", err) return } data := struct { Notes []models.Note Errors []string }{ Notes: notes, Errors: nil, } // Check HX-Target header for partial update hxTarget := r.Header.Get("HX-Target") templateName := "notes-tab" if hxTarget == "notes-results" { // Render only the notes list for HTMX partial update templateName = "obsidian-notes" } if err := h.templates.ExecuteTemplate(w, templateName, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Error rendering notes 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) } }