From 2e739638477e87a1b1df662740f191c86db60186 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 26 Jan 2026 08:10:27 -1000 Subject: Phase 5: Extract functions to reduce complexity - Create atoms.go with BuildUnifiedAtomList, SortAtomsByUrgency, PartitionAtomsByTime - Create helpers.go with parseFormOr400, requireFormValue - Refactor HandleTabTasks from 95 lines to 25 lines using extracted functions - Remove duplicate atomUrgencyTier function - Update handlers to use parseFormOr400 helper Co-Authored-By: Claude Opus 4.5 --- internal/handlers/atoms.go | 112 ++++++++++++++++++++++++++++++++++++++++++ internal/handlers/handlers.go | 97 ++---------------------------------- internal/handlers/helpers.go | 26 ++++++++++ 3 files changed, 143 insertions(+), 92 deletions(-) create mode 100644 internal/handlers/atoms.go create mode 100644 internal/handlers/helpers.go (limited to 'internal') diff --git a/internal/handlers/atoms.go b/internal/handlers/atoms.go new file mode 100644 index 0000000..7bc4465 --- /dev/null +++ b/internal/handlers/atoms.go @@ -0,0 +1,112 @@ +package handlers + +import ( + "sort" + + "task-dashboard/internal/models" + "task-dashboard/internal/store" +) + +// BuildUnifiedAtomList creates a list of atoms from tasks, cards, and bugs +func BuildUnifiedAtomList(s *store.Store) ([]models.Atom, []models.Board, error) { + tasks, err := s.GetTasks() + if err != nil { + return nil, nil, err + } + + boards, err := s.GetBoards() + if err != nil { + return nil, nil, err + } + + bugs, _ := s.GetUnresolvedBugs() // Ignore error, bugs are optional + + atoms := make([]models.Atom, 0, len(tasks)+len(bugs)) + + // Add incomplete tasks + for _, task := range tasks { + if !task.Completed { + atoms = append(atoms, models.TaskToAtom(task)) + } + } + + // Add cards with due dates or from actionable lists + for _, board := range boards { + for _, card := range board.Cards { + if card.DueDate != nil || isActionableList(card.ListName) { + atoms = append(atoms, models.CardToAtom(card)) + } + } + } + + // Add unresolved bugs + for _, bug := range bugs { + atoms = append(atoms, models.BugToAtom(models.Bug{ + ID: bug.ID, + Description: bug.Description, + CreatedAt: bug.CreatedAt, + })) + } + + // Compute UI fields for all atoms + for i := range atoms { + atoms[i].ComputeUIFields() + } + + return atoms, boards, nil +} + +// SortAtomsByUrgency sorts atoms by urgency tier, then due date, then priority +func SortAtomsByUrgency(atoms []models.Atom) { + sort.SliceStable(atoms, func(i, j int) bool { + tierI := atomUrgencyTier(atoms[i]) + tierJ := atomUrgencyTier(atoms[j]) + + if tierI != tierJ { + return tierI < tierJ + } + + 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) + } + } + + return atoms[i].Priority > atoms[j].Priority + }) +} + +// PartitionAtomsByTime separates atoms into current and future lists +// Recurring tasks that are future are excluded entirely +func PartitionAtomsByTime(atoms []models.Atom) (current, future []models.Atom) { + for _, a := range atoms { + // Don't show recurring tasks until the day they're due + if a.IsRecurring && a.IsFuture { + continue + } + if a.IsFuture { + future = append(future, a) + } else { + current = append(current, a) + } + } + return +} + +// atomUrgencyTier returns an urgency tier for sorting: +// 0 = overdue, 1 = today with specific time, 2 = today no time, 3 = future, 4 = no due date +func atomUrgencyTier(a models.Atom) int { + if a.DueDate == nil { + return 4 + } + if a.IsOverdue { + return 0 + } + if a.IsFuture { + return 3 + } + if a.HasSetTime { + return 1 + } + return 2 +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 635a69d..595ab67 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -511,8 +511,7 @@ func (h *Handler) HandleCreateCard(w http.ResponseWriter, r *http.Request) { // HandleCompleteCard marks a Trello card as complete func (h *Handler) HandleCompleteCard(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - JSONError(w, http.StatusBadRequest, "Failed to parse form", err) + if !parseFormOr400(w, r) { return } @@ -822,8 +821,7 @@ func (h *Handler) HandleGetBugs(w http.ResponseWriter, r *http.Request) { // HandleReportBug saves a new bug report func (h *Handler) HandleReportBug(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - JSONError(w, http.StatusBadRequest, "Invalid form data", err) + if !parseFormOr400(w, r) { return } @@ -962,82 +960,14 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) { // HandleTabTasks renders the unified Tasks tab (Todoist + Trello cards with due dates + Bugs) func (h *Handler) HandleTabTasks(w http.ResponseWriter, r *http.Request) { - tasks, err := h.store.GetTasks() + atoms, boards, err := BuildUnifiedAtomList(h.store) if err != nil { JSONError(w, http.StatusInternalServerError, "Failed to fetch tasks", err) return } - boards, err := h.store.GetBoards() - if err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to fetch boards", err) - return - } - - bugs, err := h.store.GetUnresolvedBugs() - if err != nil { - log.Printf("Warning: failed to fetch bugs: %v", err) - bugs = nil - } - - atoms := make([]models.Atom, 0) - - for _, task := range tasks { - if !task.Completed { - atoms = append(atoms, models.TaskToAtom(task)) - } - } - - for _, board := range boards { - for _, card := range board.Cards { - if card.DueDate != nil || isActionableList(card.ListName) { - atoms = append(atoms, models.CardToAtom(card)) - } - } - } - - // Add unresolved bugs as atoms - for _, bug := range bugs { - atoms = append(atoms, models.BugToAtom(models.Bug{ - ID: bug.ID, - Description: bug.Description, - CreatedAt: bug.CreatedAt, - })) - } - - for i := range atoms { - atoms[i].ComputeUIFields() - } - - sort.SliceStable(atoms, func(i, j int) bool { - tierI := atomUrgencyTier(atoms[i]) - tierJ := atomUrgencyTier(atoms[j]) - - if tierI != tierJ { - return tierI < tierJ - } - - 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) - } - } - - return atoms[i].Priority > atoms[j].Priority - }) - - var currentAtoms, futureAtoms []models.Atom - for _, a := range atoms { - // Don't show recurring tasks until the day they're due - if a.IsRecurring && a.IsFuture { - continue - } - if a.IsFuture { - futureAtoms = append(futureAtoms, a) - } else { - currentAtoms = append(currentAtoms, a) - } - } + SortAtomsByUrgency(atoms) + currentAtoms, futureAtoms := PartitionAtomsByTime(atoms) data := struct { Atoms []models.Atom @@ -1436,23 +1366,6 @@ func isActionableList(name string) bool { strings.Contains(lower, "today") } -// atomUrgencyTier returns the urgency tier for sorting -func atomUrgencyTier(a models.Atom) int { - if a.DueDate == nil { - return 4 - } - if a.IsOverdue { - return 0 - } - if a.IsFuture { - return 3 - } - if a.HasSetTime { - return 1 - } - return 2 -} - // ScheduledItem represents a scheduled event or task for the planning view type ScheduledItem struct { Type string diff --git a/internal/handlers/helpers.go b/internal/handlers/helpers.go new file mode 100644 index 0000000..e67eea7 --- /dev/null +++ b/internal/handlers/helpers.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "net/http" +) + +// parseFormOr400 parses the request form and returns false if parsing fails +// (after writing a 400 error response). Returns true if parsing succeeds. +func parseFormOr400(w http.ResponseWriter, r *http.Request) bool { + if err := r.ParseForm(); err != nil { + JSONError(w, http.StatusBadRequest, "Failed to parse form", err) + return false + } + return true +} + +// requireFormValue returns the form value for the given key, or writes a 400 error +// and returns empty string if the value is missing. +func requireFormValue(w http.ResponseWriter, r *http.Request, key string) (string, bool) { + value := r.FormValue(key) + if value == "" { + JSONError(w, http.StatusBadRequest, "Missing required field: "+key, nil) + return "", false + } + return value, true +} -- cgit v1.2.3