1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
package handlers
import (
"context"
"log"
"sort"
"task-dashboard/internal/api"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
)
// BuildUnifiedAtomList creates a list of atoms from tasks, cards, and google tasks
func BuildUnifiedAtomList(s *store.Store, claudomator api.ClaudomatorClient) ([]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
}
gTasks, err := s.GetGoogleTasks()
if err != nil {
// Log but don't fail if gtasks fails (might be new/not configured)
log.Printf("Warning: failed to fetch cached google tasks: %v", err)
}
nativeTasks, err := s.GetNativeTasks()
if err != nil {
log.Printf("Warning: failed to fetch native tasks: %v", err)
}
atoms := make([]models.Atom, 0, len(tasks)+len(gTasks)+len(nativeTasks))
// Add native doot tasks (GetNativeTasks already filters completed=0)
for _, task := range nativeTasks {
atoms = append(atoms, models.NativeTaskToAtom(task))
}
// Add incomplete tasks
for _, task := range tasks {
if !task.Completed {
atoms = append(atoms, models.TaskToAtom(task))
}
}
// Add incomplete google tasks
for _, gTask := range gTasks {
if !gTask.Completed {
atoms = append(atoms, models.GoogleTaskToAtom(gTask))
}
}
// 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))
}
}
}
if claudomator != nil {
stories, err := claudomator.GetActiveStories(context.Background())
if err != nil {
log.Printf("Warning: failed to fetch Claudomator stories: %v", err)
} else {
for _, s := range stories {
atoms = append(atoms, models.StoryToAtom(s))
}
}
}
// 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
}
|