summaryrefslogtreecommitdiff
path: root/internal/handlers/atoms.go
blob: 16cd0030e180251cb1fd340ce5fff3aca1b0617b (plain)
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
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) {
	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(gTasks)+len(nativeTasks))

	// Add native doot tasks (GetNativeTasks already filters completed=0).
	// Chain tasks (locked or unlocked) and dormant bucket-pool items are
	// deliberately excluded here. Chains get their own "Chains" section
	// (see BuildChainSummaries) that owns the whole lifecycle -- viewing
	// every step, completing the current one, pause/resume/abandon -- in
	// one place, rather than the current step also duplicating into this
	// flat list and the locked steps cluttering it with non-actionable
	// cards. Dormant bucket items aren't actionable until picked, per the
	// bucket design.
	for _, task := range nativeTasks {
		if task.ChainID != "" {
			continue
		}
		if task.BucketState == "dormant" {
			continue
		}
		atoms = append(atoms, models.NativeTaskToAtom(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
func PartitionAtomsByTime(atoms []models.Atom) (current, future []models.Atom) {
	for _, a := range atoms {
		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
}