diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/google_calendar.go | 62 | ||||
| -rw-r--r-- | internal/api/interfaces.go | 12 | ||||
| -rw-r--r-- | internal/config/config.go | 13 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 44 | ||||
| -rw-r--r-- | internal/handlers/heuristic_test.go | 2 | ||||
| -rw-r--r-- | internal/handlers/tab_state_test.go | 2 | ||||
| -rw-r--r-- | internal/handlers/tabs.go | 26 | ||||
| -rw-r--r-- | internal/models/types.go | 25 |
8 files changed, 156 insertions, 30 deletions
diff --git a/internal/api/google_calendar.go b/internal/api/google_calendar.go new file mode 100644 index 0000000..836d98c --- /dev/null +++ b/internal/api/google_calendar.go @@ -0,0 +1,62 @@ +package api + +import ( + "context" + "fmt" + "time" + + "task-dashboard/internal/models" + + "google.golang.org/api/calendar/v3" + "google.golang.org/api/option" +) + +type GoogleCalendarClient struct { + srv *calendar.Service + calendarID string +} + +func NewGoogleCalendarClient(ctx context.Context, credentialsFile, calendarID string) (*GoogleCalendarClient, error) { + srv, err := calendar.NewService(ctx, option.WithCredentialsFile(credentialsFile)) + if err != nil { + return nil, fmt.Errorf("unable to retrieve Calendar client: %v", err) + } + + return &GoogleCalendarClient{ + srv: srv, + calendarID: calendarID, + }, nil +} + +func (c *GoogleCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) { + t := time.Now().Format(time.RFC3339) + events, err := c.srv.Events.List(c.calendarID).ShowDeleted(false). + SingleEvents(true).TimeMin(t).MaxResults(int64(maxResults)).OrderBy("startTime").Do() + if err != nil { + return nil, fmt.Errorf("unable to retrieve events: %v", err) + } + + var calendarEvents []models.CalendarEvent + for _, item := range events.Items { + var start, end time.Time + if item.Start.DateTime == "" { + // All-day event + start, _ = time.Parse("2006-01-02", item.Start.Date) + end, _ = time.Parse("2006-01-02", item.End.Date) + } else { + start, _ = time.Parse(time.RFC3339, item.Start.DateTime) + end, _ = time.Parse(time.RFC3339, item.End.DateTime) + } + + calendarEvents = append(calendarEvents, models.CalendarEvent{ + ID: item.Id, + Summary: item.Summary, + Description: item.Description, + Start: start, + End: end, + HTMLLink: item.HtmlLink, + }) + } + + return calendarEvents, nil +} diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go index 32d0120..e2521f4 100644 --- a/internal/api/interfaces.go +++ b/internal/api/interfaces.go @@ -34,9 +34,15 @@ type PlanToEatAPI interface { AddMealToPlanner(ctx context.Context, recipeID string, date time.Time, mealType string) error } +// GoogleCalendarAPI defines the interface for Google Calendar operations +type GoogleCalendarAPI interface { + GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) +} + // Ensure concrete types implement interfaces var ( - _ TodoistAPI = (*TodoistClient)(nil) - _ TrelloAPI = (*TrelloClient)(nil) - _ PlanToEatAPI = (*PlanToEatClient)(nil) + _ TodoistAPI = (*TodoistClient)(nil) + _ TrelloAPI = (*TrelloClient)(nil) + _ PlanToEatAPI = (*PlanToEatClient)(nil) + _ GoogleCalendarAPI = (*GoogleCalendarClient)(nil) ) diff --git a/internal/config/config.go b/internal/config/config.go index 662159e..ba2719d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,6 +14,10 @@ type Config struct { TrelloAPIKey string TrelloToken string + // Google Calendar + GoogleCredentialsFile string + GoogleCalendarID string + // Paths DatabasePath string TemplateDir string @@ -34,6 +38,10 @@ func Load() (*Config, error) { TrelloAPIKey: os.Getenv("TRELLO_API_KEY"), TrelloToken: os.Getenv("TRELLO_TOKEN"), + // Google Calendar + GoogleCredentialsFile: os.Getenv("GOOGLE_CREDENTIALS_FILE"), + GoogleCalendarID: getEnvWithDefault("GOOGLE_CALENDAR_ID", "primary"), + // Paths DatabasePath: getEnvWithDefault("DATABASE_PATH", "./dashboard.db"), TemplateDir: getEnvWithDefault("TEMPLATE_DIR", "web/templates"), @@ -81,6 +89,11 @@ func (c *Config) HasTrello() bool { return c.TrelloAPIKey != "" && c.TrelloToken != "" } +// HasGoogleCalendar checks if Google Calendar is configured +func (c *Config) HasGoogleCalendar() bool { + return c.GoogleCredentialsFile != "" +} + // getEnvWithDefault returns environment variable value or default if not set func getEnvWithDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 19415c7..1cb978d 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -22,16 +22,17 @@ import ( // Handler holds dependencies for HTTP handlers type Handler struct { - store *store.Store - todoistClient api.TodoistAPI - trelloClient api.TrelloAPI - planToEatClient api.PlanToEatAPI - config *config.Config - templates *template.Template + store *store.Store + todoistClient api.TodoistAPI + trelloClient api.TrelloAPI + planToEatClient api.PlanToEatAPI + googleCalendarClient api.GoogleCalendarAPI + config *config.Config + templates *template.Template } // New creates a new Handler instance -func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, cfg *config.Config) *Handler { +func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, cfg *config.Config) *Handler { // Parse templates including partials tmpl, err := template.ParseGlob(filepath.Join(cfg.TemplateDir, "*.html")) if err != nil { @@ -45,12 +46,13 @@ func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat } return &Handler{ - store: s, - todoistClient: todoist, - trelloClient: trello, - planToEatClient: planToEat, - config: cfg, - templates: tmpl, + store: s, + todoistClient: todoist, + trelloClient: trello, + planToEatClient: planToEat, + googleCalendarClient: googleCalendar, + config: cfg, + templates: tmpl, } } @@ -279,6 +281,22 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models }() } + // Fetch Google Calendar events (if configured) + if h.googleCalendarClient != nil { + wg.Add(1) + go func() { + defer wg.Done() + events, err := h.googleCalendarClient.GetUpcomingEvents(ctx, 10) + mu.Lock() + defer mu.Unlock() + if err != nil { + data.Errors = append(data.Errors, "Google Calendar: "+err.Error()) + } else { + data.Events = events + } + }() + } + wg.Wait() // Filter Trello cards into tasks based on heuristic diff --git a/internal/handlers/heuristic_test.go b/internal/handlers/heuristic_test.go index dc8620a..2b70218 100644 --- a/internal/handlers/heuristic_test.go +++ b/internal/handlers/heuristic_test.go @@ -63,7 +63,7 @@ func TestHandleTasks_Heuristic(t *testing.T) { } // Create Handler - h := NewTabsHandler(db, "../../web/templates") + h := NewTabsHandler(db, nil, "../../web/templates") // Skip if templates are not loaded if h.templates == nil { diff --git a/internal/handlers/tab_state_test.go b/internal/handlers/tab_state_test.go index a4f6d23..d7bb8dd 100644 --- a/internal/handlers/tab_state_test.go +++ b/internal/handlers/tab_state_test.go @@ -30,7 +30,7 @@ func TestHandleDashboard_TabState(t *testing.T) { } // Create handler - h := New(db, todoistClient, trelloClient, nil, cfg) + h := New(db, todoistClient, trelloClient, nil, nil, cfg) // Skip if templates are not loaded (test environment issue) if h.templates == nil { diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go index 2f22c44..b651dac 100644 --- a/internal/handlers/tabs.go +++ b/internal/handlers/tabs.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "task-dashboard/internal/api" "task-dashboard/internal/models" "task-dashboard/internal/store" ) @@ -46,12 +47,13 @@ func atomUrgencyTier(a models.Atom) int { // TabsHandler handles tab-specific rendering with Atom model type TabsHandler struct { - store *store.Store - templates *template.Template + store *store.Store + googleCalendarClient api.GoogleCalendarAPI + templates *template.Template } // NewTabsHandler creates a new TabsHandler instance -func NewTabsHandler(store *store.Store, templateDir string) *TabsHandler { +func NewTabsHandler(store *store.Store, googleCalendarClient api.GoogleCalendarAPI, templateDir string) *TabsHandler { // Parse templates including partials tmpl, err := template.ParseGlob(filepath.Join(templateDir, "*.html")) if err != nil { @@ -65,8 +67,9 @@ func NewTabsHandler(store *store.Store, templateDir string) *TabsHandler { } return &TabsHandler{ - store: store, - templates: tmpl, + store: store, + googleCalendarClient: googleCalendarClient, + templates: tmpl, } } @@ -178,12 +181,25 @@ func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) { return } + // Fetch Google Calendar events + var events []models.CalendarEvent + if h.googleCalendarClient != nil { + var err error + events, err = h.googleCalendarClient.GetUpcomingEvents(r.Context(), 10) + if err != nil { + log.Printf("Error fetching calendar events: %v", err) + // Don't fail the whole request, just show empty events + } + } + data := struct { Boards []models.Board Projects []models.Project + Events []models.CalendarEvent }{ Boards: boards, Projects: []models.Project{}, // Empty for now + Events: events, } if err := h.templates.ExecuteTemplate(w, "planning-tab", data); err != nil { diff --git a/internal/models/types.go b/internal/models/types.go index d9e955b..a604b28 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -57,6 +57,16 @@ type Project struct { Name string `json:"name"` } +// CalendarEvent represents a Google Calendar event +type CalendarEvent struct { + ID string `json:"id"` + Summary string `json:"summary"` + Description string `json:"description"` + Start time.Time `json:"start"` + End time.Time `json:"end"` + HTMLLink string `json:"html_link"` +} + // CacheMetadata tracks when data was last fetched type CacheMetadata struct { Key string `json:"key"` @@ -72,11 +82,12 @@ func (cm *CacheMetadata) IsCacheValid() bool { // DashboardData aggregates all data for the main view type DashboardData struct { - Tasks []Task `json:"tasks"` - Meals []Meal `json:"meals"` - Boards []Board `json:"boards,omitempty"` - TrelloTasks []Card `json:"trello_tasks,omitempty"` - Projects []Project `json:"projects,omitempty"` - LastUpdated time.Time `json:"last_updated"` - Errors []string `json:"errors,omitempty"` + Tasks []Task `json:"tasks"` + Meals []Meal `json:"meals"` + Boards []Board `json:"boards,omitempty"` + TrelloTasks []Card `json:"trello_tasks,omitempty"` + Projects []Project `json:"projects,omitempty"` + Events []CalendarEvent `json:"events,omitempty"` + LastUpdated time.Time `json:"last_updated"` + Errors []string `json:"errors,omitempty"` } |
