summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-31 21:23:56 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-31 21:23:56 -1000
commitf9127d5272042f4980ece8b39a47613f95eeaf8e (patch)
treee111cc6f85b0cd23dd7e705b0390d1154fbc13ee /internal
parentcbb0b53de1d06918c142171fd084f14f03798bc1 (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.go13
-rw-r--r--internal/handlers/handlers.go16
-rw-r--r--internal/handlers/response.go10
-rw-r--r--internal/handlers/timeline_logic.go17
-rw-r--r--internal/models/timeline.go10
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) {