summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-26 08:10:27 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-26 08:10:27 -1000
commit2e739638477e87a1b1df662740f191c86db60186 (patch)
tree302916e1e0c99ae47213128fa79133752203a271 /internal/handlers
parentaff60af8ba24c8d5330c706ddf26927d81436d79 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/atoms.go112
-rw-r--r--internal/handlers/handlers.go97
-rw-r--r--internal/handlers/helpers.go26
3 files changed, 143 insertions, 92 deletions
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
+}