summaryrefslogtreecommitdiff
path: root/internal/handlers/atoms.go
blob: 6086a5b693961f95d69443a42e760b87292c2d1b (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
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)
	}

	atoms := make([]models.Atom, 0, len(tasks)+len(gTasks))

	// 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
}