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"
"sort"
"time"
"task-dashboard/internal/api"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
)
// BuildTimeline aggregates and normalizes data into a timeline structure
func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.GoogleCalendarAPI, start, end time.Time) ([]models.TimelineItem, error) {
var items []models.TimelineItem
now := time.Now()
// 1. Fetch Tasks
tasks, err := s.GetTasksByDateRange(start, end)
if err != nil {
return nil, err
}
for _, task := range tasks {
if task.DueDate == nil {
continue
}
item := models.TimelineItem{
ID: task.ID,
Type: models.TimelineItemTypeTask,
Title: task.Content,
Time: *task.DueDate,
Description: task.Description,
URL: task.URL,
OriginalItem: task,
IsCompleted: task.Completed,
Source: "todoist",
}
item.ComputeDaySection(now)
items = append(items, item)
}
// 2. Fetch Meals
meals, err := s.GetMealsByDateRange(start, end)
if err != nil {
return nil, err
}
for _, meal := range meals {
mealTime := meal.Date
// Apply Meal Defaults
switch meal.MealType {
case "breakfast":
mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), 8, 0, 0, 0, mealTime.Location())
case "lunch":
mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), 12, 0, 0, 0, mealTime.Location())
case "dinner":
mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), 19, 0, 0, 0, mealTime.Location())
default:
mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), 12, 0, 0, 0, mealTime.Location())
}
item := models.TimelineItem{
ID: meal.ID,
Type: models.TimelineItemTypeMeal,
Title: meal.RecipeName,
Time: mealTime,
URL: meal.RecipeURL,
OriginalItem: meal,
IsCompleted: false, // Meals don't have completion status
Source: "plantoeat",
}
item.ComputeDaySection(now)
items = append(items, item)
}
// 3. Fetch Cards
cards, err := s.GetCardsByDateRange(start, end)
if err != nil {
return nil, err
}
for _, card := range cards {
if card.DueDate == nil {
continue
}
item := models.TimelineItem{
ID: card.ID,
Type: models.TimelineItemTypeCard,
Title: card.Name,
Time: *card.DueDate,
URL: card.URL,
OriginalItem: card,
IsCompleted: false, // Cards in timeline are not completed (closed cards filtered out)
Source: "trello",
}
item.ComputeDaySection(now)
items = append(items, item)
}
// 4. Fetch Events
if calendarClient != nil {
events, err := calendarClient.GetEventsByDateRange(ctx, start, end)
if err == nil {
for _, event := range events {
endTime := event.End
item := models.TimelineItem{
ID: event.ID,
Type: models.TimelineItemTypeEvent,
Title: event.Summary,
Time: event.Start,
EndTime: &endTime,
Description: event.Description,
URL: event.HTMLLink,
OriginalItem: event,
IsCompleted: false, // Events don't have completion status
Source: "calendar",
}
item.ComputeDaySection(now)
items = append(items, item)
}
}
}
// Sort items by Time
sort.Slice(items, func(i, j int) bool {
return items[i].Time.Before(items[j].Time)
})
return items, nil
}
|