summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-26 17:00:30 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-26 17:00:30 -1000
commitbbf12fc441ca36c423e865107d34df178e3d26de (patch)
tree44525ff1c846c1b4099f4c08c6ce88f6028ea5d2 /internal/handlers
parentfd2524eacd51f523998734f869b3343441e55b93 (diff)
Fix multiple UI issues and shopping completion bug
- #54: Fix shopping item completion - now works for all sources (trello, plantoeat, user) with state stored in local DB - #48: Hide 12:00am times in timeline (all-day items) - #49: Remove "Task" type label from timeline items for cleaner UI - #51: Combine multiple PlanToEat meals for same date+mealType - #52: Change Conditions tab to standard link to standalone page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/handlers.go58
-rw-r--r--internal/handlers/timeline_logic.go49
2 files changed, 70 insertions, 37 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 1e376b5..a1a12e7 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -1179,7 +1179,8 @@ func (h *Handler) HandleShoppingToggle(w http.ResponseWriter, r *http.Request) {
source := r.FormValue("source")
checked := r.FormValue("checked") == "true"
- if source == "user" {
+ switch source {
+ case "user":
var userID int64
if _, err := fmt.Sscanf(id, "user-%d", &userID); err != nil {
JSONError(w, http.StatusBadRequest, "Invalid user item ID", err)
@@ -1189,35 +1190,30 @@ func (h *Handler) HandleShoppingToggle(w http.ResponseWriter, r *http.Request) {
JSONError(w, http.StatusInternalServerError, "Failed to toggle item", err)
return
}
+ case "trello", "plantoeat":
+ // Store checked state locally for external sources
+ if err := h.store.SetShoppingItemChecked(source, id, checked); err != nil {
+ JSONError(w, http.StatusInternalServerError, "Failed to toggle item", err)
+ return
+ }
+ default:
+ JSONError(w, http.StatusBadRequest, "Unknown source", nil)
+ return
}
- // Note: Trello and PlanToEat toggle would need their own API calls
-
- // Return updated item HTML
- checkedClass := ""
- checkedAttr := ""
- textClass := "text-white"
- if checked {
- checkedClass = "opacity-50"
- checkedAttr = "checked"
- textClass = "line-through text-white/40"
- }
- html := fmt.Sprintf(`<li class="flex items-center gap-3 p-3 bg-white/5 hover:bg-white/10 transition-colors rounded-lg border border-white/5 %s">
- <input type="checkbox" %s
- hx-post="/shopping/toggle"
- hx-vals='{"id":"%s","source":"%s","checked":%t}'
- hx-target="closest li"
- hx-swap="outerHTML"
- class="h-5 w-5 rounded bg-black/40 border-white/30 text-green-500 focus:ring-white/30 cursor-pointer flex-shrink-0">
- <span class="flex-1 %s">%s</span>
- <span class="text-xs px-2 py-0.5 rounded bg-purple-900/50 text-purple-300">user</span>
- </li>`, checkedClass, checkedAttr, template.HTMLEscapeString(id), template.HTMLEscapeString(source), !checked, textClass, template.HTMLEscapeString(r.FormValue("name")))
- HTMLString(w, html)
+
+ // Return refreshed shopping tab
+ stores := h.aggregateShoppingLists(r.Context())
+ HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{stores})
}
// aggregateShoppingLists combines Trello, PlanToEat, and user shopping items by store
func (h *Handler) aggregateShoppingLists(ctx context.Context) []models.ShoppingStore {
storeMap := make(map[string]map[string][]models.UnifiedShoppingItem)
+ // Fetch locally stored checked states for external sources
+ trelloChecks, _ := h.store.GetShoppingItemChecks("trello")
+ plantoeatChecks, _ := h.store.GetShoppingItemChecks("plantoeat")
+
// 1. Fetch Trello "Shopping" board cards
boards, err := h.store.GetBoards()
if err != nil {
@@ -1237,10 +1233,11 @@ func (h *Handler) aggregateShoppingLists(ctx context.Context) []models.ShoppingS
}
item := models.UnifiedShoppingItem{
- ID: card.ID,
- Name: card.Name,
- Store: storeName,
- Source: "trello",
+ ID: card.ID,
+ Name: card.Name,
+ Store: storeName,
+ Source: "trello",
+ Checked: trelloChecks[card.ID],
}
storeMap[storeName][""] = append(storeMap[storeName][""], item)
}
@@ -1265,6 +1262,11 @@ func (h *Handler) aggregateShoppingLists(ctx context.Context) []models.ShoppingS
storeMap[storeName] = make(map[string][]models.UnifiedShoppingItem)
}
+ // Use locally stored check state if available, otherwise use scraped state
+ checked := item.Checked
+ if localChecked, ok := plantoeatChecks[item.ID]; ok {
+ checked = localChecked
+ }
unified := models.UnifiedShoppingItem{
ID: item.ID,
Name: item.Name,
@@ -1272,7 +1274,7 @@ func (h *Handler) aggregateShoppingLists(ctx context.Context) []models.ShoppingS
Category: item.Category,
Store: storeName,
Source: "plantoeat",
- Checked: item.Checked,
+ Checked: checked,
}
storeMap[storeName][item.Category] = append(storeMap[storeName][item.Category], unified)
}
diff --git a/internal/handlers/timeline_logic.go b/internal/handlers/timeline_logic.go
index bead98f..553593d 100644
--- a/internal/handlers/timeline_logic.go
+++ b/internal/handlers/timeline_logic.go
@@ -3,6 +3,7 @@ package handlers
import (
"context"
"sort"
+ "strings"
"time"
"task-dashboard/internal/api"
@@ -40,15 +41,38 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl
items = append(items, item)
}
- // 2. Fetch Meals
+ // 2. Fetch Meals - combine multiple items for same date+mealType
meals, err := s.GetMealsByDateRange(start, end)
if err != nil {
return nil, err
}
+
+ // Group meals by date+mealType key
+ type mealKey struct {
+ date string
+ mealType string
+ }
+ mealGroups := make(map[mealKey][]models.Meal)
for _, meal := range meals {
- mealTime := meal.Date
- // Apply Meal Defaults
- switch meal.MealType {
+ key := mealKey{
+ date: meal.Date.Format("2006-01-02"),
+ mealType: meal.MealType,
+ }
+ mealGroups[key] = append(mealGroups[key], meal)
+ }
+
+ // Create combined timeline items for each group
+ for _, group := range mealGroups {
+ if len(group) == 0 {
+ continue
+ }
+
+ // Use first meal as base
+ firstMeal := group[0]
+ mealTime := firstMeal.Date
+
+ // Apply Meal time defaults
+ switch firstMeal.MealType {
case "breakfast":
mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), config.BreakfastHour, 0, 0, 0, mealTime.Location())
case "lunch":
@@ -59,14 +83,21 @@ func BuildTimeline(ctx context.Context, s *store.Store, calendarClient api.Googl
mealTime = time.Date(mealTime.Year(), mealTime.Month(), mealTime.Day(), config.LunchHour, 0, 0, 0, mealTime.Location())
}
+ // Combine recipe names with " + "
+ var names []string
+ for _, m := range group {
+ names = append(names, m.RecipeName)
+ }
+ combinedTitle := strings.Join(names, " + ")
+
item := models.TimelineItem{
- ID: meal.ID,
+ ID: firstMeal.ID,
Type: models.TimelineItemTypeMeal,
- Title: meal.RecipeName,
+ Title: combinedTitle,
Time: mealTime,
- URL: meal.RecipeURL,
- OriginalItem: meal,
- IsCompleted: false, // Meals don't have completion status
+ URL: firstMeal.RecipeURL, // Use first meal's URL
+ OriginalItem: group, // Store all meals
+ IsCompleted: false,
Source: "plantoeat",
}
item.ComputeDaySection(now)