package handlers import ( "context" "sort" "time" "task-dashboard/internal/api" "task-dashboard/internal/config" "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 := config.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(), config.BreakfastHour, 0, 0, 0, mealTime.Location()) case "lunch": mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), config.LunchHour, 0, 0, 0, mealTime.Location()) case "dinner": mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), config.DinnerHour, 0, 0, 0, mealTime.Location()) default: mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), config.LunchHour, 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 }