diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-26 17:00:30 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-26 17:00:30 -1000 |
| commit | bbf12fc441ca36c423e865107d34df178e3d26de (patch) | |
| tree | 44525ff1c846c1b4099f4c08c6ce88f6028ea5d2 /internal | |
| parent | fd2524eacd51f523998734f869b3343441e55b93 (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')
| -rw-r--r-- | internal/handlers/handlers.go | 58 | ||||
| -rw-r--r-- | internal/handlers/timeline_logic.go | 49 | ||||
| -rw-r--r-- | internal/store/sqlite.go | 34 |
3 files changed, 104 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) diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index c2f6e98..396ac54 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -701,6 +701,40 @@ func (s *Store) DeleteUserShoppingItem(id int64) error { return err } +// SetShoppingItemChecked sets the checked state for an external shopping item +func (s *Store) SetShoppingItemChecked(source, itemID string, checked bool) error { + checkedInt := 0 + if checked { + checkedInt = 1 + } + _, err := s.db.Exec(` + INSERT INTO shopping_item_checks (source, item_id, checked, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(source, item_id) DO UPDATE SET checked = ?, updated_at = CURRENT_TIMESTAMP + `, source, itemID, checkedInt, checkedInt) + return err +} + +// GetShoppingItemChecks returns a map of item_id -> checked for a given source +func (s *Store) GetShoppingItemChecks(source string) (map[string]bool, error) { + rows, err := s.db.Query(`SELECT item_id, checked FROM shopping_item_checks WHERE source = ?`, source) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + + checks := make(map[string]bool) + for rows.Next() { + var itemID string + var checked int + if err := rows.Scan(&itemID, &checked); err != nil { + return nil, err + } + checks[itemID] = checked == 1 + } + return checks, rows.Err() +} + // GetTasksByDateRange retrieves tasks due within a specific date range func (s *Store) GetTasksByDateRange(start, end time.Time) ([]models.Task, error) { rows, err := s.db.Query(` |
