diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-31 21:23:56 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-31 21:23:56 -1000 |
| commit | f9127d5272042f4980ece8b39a47613f95eeaf8e (patch) | |
| tree | e111cc6f85b0cd23dd7e705b0390d1154fbc13ee /internal | |
| parent | cbb0b53de1d06918c142171fd084f14f03798bc1 (diff) | |
Fix timeline calendar view and shopping UI bugs (#56, #65-73)
- #56: Add overflow-hidden to card/panel classes to prevent content overflow
- #65: Fix Google Tasks not showing by including tasks without due dates
- #66: Add no-cache headers to prevent stale template responses
- #67: Increase dropdown z-index to 100 for proper layering
- #69: Implement calendar-style Today section with hourly grid (6am-10pm),
duration-based event heights, and compact overdue/all-day section
- #70: Only reset shopping-mode form on successful submission
- #71: Remove checkboxes from shopping tab (only show in shopping mode)
- #72: Add inline add-item input at end of each store section
- #73: Add Grouped/Flat view toggle for shopping list
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/google_tasks.go | 13 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 16 | ||||
| -rw-r--r-- | internal/handlers/response.go | 10 | ||||
| -rw-r--r-- | internal/handlers/timeline_logic.go | 17 | ||||
| -rw-r--r-- | internal/models/timeline.go | 10 |
5 files changed, 53 insertions, 13 deletions
diff --git a/internal/api/google_tasks.go b/internal/api/google_tasks.go index 77a00ed..ecacb6d 100644 --- a/internal/api/google_tasks.go +++ b/internal/api/google_tasks.go @@ -123,23 +123,26 @@ func (c *GoogleTasksClient) getTasksFromList(ctx context.Context, listID string) return result, nil } -// GetTasksByDateRange fetches tasks with due dates in the specified range +// GetTasksByDateRange fetches tasks with due dates in the specified range. +// Tasks without due dates are included and treated as "today" tasks. func (c *GoogleTasksClient) GetTasksByDateRange(ctx context.Context, start, end time.Time) ([]models.GoogleTask, error) { allTasks, err := c.GetTasks(ctx) if err != nil { return nil, err } - // Filter by date range + startDay := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, c.displayTZ) + endDay := time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, c.displayTZ) + + // Filter by date range, include tasks without due dates var filtered []models.GoogleTask for _, task := range allTasks { if task.DueDate == nil { + // Include tasks without due dates (they'll be shown in "today" section) + filtered = append(filtered, task) continue } dueDay := time.Date(task.DueDate.Year(), task.DueDate.Month(), task.DueDate.Day(), 0, 0, 0, 0, c.displayTZ) - startDay := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, c.displayTZ) - endDay := time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, c.displayTZ) - if !dueDay.Before(startDay) && dueDay.Before(endDay) { filtered = append(filtered, task) } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 0e5edcc..bba12ad 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1230,7 +1230,11 @@ func mealTypeOrder(mealType string) int { func (h *Handler) HandleTabShopping(w http.ResponseWriter, r *http.Request) { ctx := r.Context() stores := h.aggregateShoppingLists(ctx) - HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{stores}) + grouped := r.URL.Query().Get("grouped") != "false" // Default to grouped + HTMLResponse(w, h.templates, "shopping-tab", struct { + Stores []models.ShoppingStore + Grouped bool + }{stores, grouped}) } // HandleShoppingQuickAdd adds a user shopping item @@ -1285,7 +1289,10 @@ func (h *Handler) HandleShoppingQuickAdd(w http.ResponseWriter, r *http.Request) } // Return refreshed shopping tab - HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{allStores}) + HTMLResponse(w, h.templates, "shopping-tab", struct { + Stores []models.ShoppingStore + Grouped bool + }{allStores, true}) } // HandleShoppingToggle toggles a shopping item's checked state @@ -1323,7 +1330,10 @@ func (h *Handler) HandleShoppingToggle(w http.ResponseWriter, r *http.Request) { // Return refreshed shopping tab stores := h.aggregateShoppingLists(r.Context()) - HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{stores}) + HTMLResponse(w, h.templates, "shopping-tab", struct { + Stores []models.ShoppingStore + Grouped bool + }{stores, true}) } // HandleShoppingMode renders the focused shopping mode for a single store diff --git a/internal/handlers/response.go b/internal/handlers/response.go index 9a7ab45..34d4491 100644 --- a/internal/handlers/response.go +++ b/internal/handlers/response.go @@ -7,9 +7,17 @@ import ( "net/http" ) +// noCacheHeaders sets headers to prevent browser caching +func noCacheHeaders(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") +} + // JSONResponse writes data as JSON with appropriate headers func JSONResponse(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") + noCacheHeaders(w) _ = json.NewEncoder(w).Encode(data) } @@ -23,6 +31,8 @@ func JSONError(w http.ResponseWriter, status int, msg string, err error) { // HTMLResponse renders an HTML template func HTMLResponse(w http.ResponseWriter, tmpl *template.Template, name string, data interface{}) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + noCacheHeaders(w) if err := tmpl.ExecuteTemplate(w, name, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Error rendering template %s: %v", name, err) diff --git a/internal/handlers/timeline_logic.go b/internal/handlers/timeline_logic.go index 5ea44b5..7a85393 100644 --- a/internal/handlers/timeline_logic.go +++ b/internal/handlers/timeline_logic.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "log" "sort" "strings" "time" @@ -130,7 +131,9 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl // 4. Fetch Events if calendarClient != nil { events, err := calendarClient.GetEventsByDateRange(ctx, start, end) - if err == nil { + if err != nil { + log.Printf("Warning: failed to fetch calendar events: %v", err) + } else { for _, event := range events { endTime := event.End item := models.TimelineItem{ @@ -142,7 +145,7 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl Description: event.Description, URL: event.HTMLLink, OriginalItem: event, - IsCompleted: false, // Events don't have completion status + IsCompleted: false, Source: "calendar", } item.ComputeDaySection(now) @@ -154,9 +157,13 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl // 5. Fetch Google Tasks if tasksClient != nil { gTasks, err := tasksClient.GetTasksByDateRange(ctx, start, end) - if err == nil { + if err != nil { + log.Printf("Warning: failed to fetch Google Tasks: %v", err) + } else { + log.Printf("Google Tasks: fetched %d tasks in date range", len(gTasks)) for _, gTask := range gTasks { - taskTime := start // Default to start of range if no due date + // Tasks without due date are placed in today section + taskTime := now if gTask.DueDate != nil { taskTime = *gTask.DueDate } @@ -176,6 +183,8 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl items = append(items, item) } } + } else { + log.Printf("Google Tasks client not configured") } // Sort items by Time diff --git a/internal/models/timeline.go b/internal/models/timeline.go index 0968d41..c4196bd 100644 --- a/internal/models/timeline.go +++ b/internal/models/timeline.go @@ -36,6 +36,8 @@ type TimelineItem struct { // UI enhancement fields IsCompleted bool `json:"is_completed"` + IsOverdue bool `json:"is_overdue"` + IsAllDay bool `json:"is_all_day"` // True if time is midnight (no specific time) DaySection DaySection `json:"day_section"` Source string `json:"source"` // "todoist", "trello", "plantoeat", "calendar", "gtasks" @@ -43,7 +45,7 @@ type TimelineItem struct { ListID string `json:"list_id,omitempty"` // For Google Tasks } -// ComputeDaySection sets the DaySection based on the item's time +// ComputeDaySection sets the DaySection, IsOverdue, and IsAllDay based on the item's time func (item *TimelineItem) ComputeDaySection(now time.Time) { // Use configured display timezone for consistent comparisons tz := config.GetDisplayTimezone() @@ -56,6 +58,12 @@ func (item *TimelineItem) ComputeDaySection(now time.Time) { itemDay := time.Date(localItemTime.Year(), localItemTime.Month(), localItemTime.Day(), 0, 0, 0, 0, tz) + // Check if item is overdue (before today) + item.IsOverdue = itemDay.Before(today) + + // Check if item is all-day (midnight time means no specific time) + item.IsAllDay = localItemTime.Hour() == 0 && localItemTime.Minute() == 0 + if itemDay.Before(tomorrow) { item.DaySection = DaySectionToday } else if itemDay.Before(dayAfterTomorrow) { |
