diff options
Diffstat (limited to 'internal/handlers')
| -rw-r--r-- | internal/handlers/handlers.go | 58 | ||||
| -rw-r--r-- | internal/handlers/timeline_logic.go | 49 |
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) |
