diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-02-05 10:41:43 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-02-05 10:41:43 -1000 |
| commit | 5ddb419137b814481a208d1dd0d18ac36ed554ea (patch) | |
| tree | 1c4002b0a32718381ef4aa4c5b4776ac6255409f /internal/handlers/handlers.go | |
| parent | 223c94f52ebaa878f6951ebf7d08754e413bdca7 (diff) | |
Extract shopping handlers to dedicated file
Moved 8 shopping-related handlers from handlers.go to shopping.go:
- HandleTabShopping
- HandleShoppingQuickAdd
- HandleShoppingToggle
- HandleShoppingMode
- HandleShoppingModeToggle
- HandleShoppingModeComplete
- HandleGetShoppingLists
- aggregateShoppingLists
handlers.go: 1633 → 1232 lines (24% reduction)
shopping.go: 415 lines (new)
All tests passing. No behavioral changes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers/handlers.go')
| -rw-r--r-- | internal/handlers/handlers.go | 454 |
1 files changed, 0 insertions, 454 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index bd05cd0..650eeaa 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -6,15 +6,12 @@ import ( "html/template" "log" "net/http" - "net/url" "path/filepath" "sort" "strings" "sync" "time" - "github.com/go-chi/chi/v5" - "task-dashboard/internal/api" "task-dashboard/internal/auth" "task-dashboard/internal/config" @@ -926,41 +923,6 @@ func (h *Handler) HandleGetTaskDetail(w http.ResponseWriter, r *http.Request) { HTMLString(w, html) } -// HandleGetShoppingLists returns the lists from the Shopping board for quick-add -func (h *Handler) HandleGetShoppingLists(w http.ResponseWriter, r *http.Request) { - boards, err := h.store.GetBoards() - if err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to get boards", err) - return - } - - var shoppingBoardID string - for _, b := range boards { - if strings.EqualFold(b.Name, "Shopping") { - shoppingBoardID = b.ID - break - } - } - - if shoppingBoardID == "" { - JSONError(w, http.StatusNotFound, "Shopping board not found", nil) - return - } - - lists, err := h.trelloClient.GetLists(r.Context(), shoppingBoardID) - if err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to get lists", err) - return - } - - w.Header().Set("Content-Type", "text/html") - for _, list := range lists { - _, _ = fmt.Fprintf(w, `<option value="%s">%s</option>`, - template.HTMLEscapeString(list.ID), - template.HTMLEscapeString(list.Name)) - } -} - // HandleUpdateTask updates a task description func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { @@ -1231,422 +1193,6 @@ func mealTypeOrder(mealType string) int { } } -// HandleTabShopping renders the Shopping tab (Trello Shopping board + PlanToEat + User items) -func (h *Handler) HandleTabShopping(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - stores := h.aggregateShoppingLists(ctx) - grouped := r.URL.Query().Get("grouped") != "false" // Default to grouped - HTMLResponse(w, h.renderer, "shopping-tab", struct { - Stores []models.ShoppingStore - Grouped bool - }{stores, grouped}) -} - -// HandleShoppingQuickAdd adds a user shopping item -func (h *Handler) HandleShoppingQuickAdd(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - JSONError(w, http.StatusBadRequest, "Failed to parse form", err) - return - } - - name := strings.TrimSpace(r.FormValue("name")) - storeName := strings.TrimSpace(r.FormValue("store")) - mode := r.FormValue("mode") // "shopping-mode" if from shopping mode page - - if name == "" { - JSONError(w, http.StatusBadRequest, "Name is required", nil) - return - } - if storeName == "" { - JSONError(w, http.StatusBadRequest, "Store is required", nil) - return - } - - if err := h.store.SaveUserShoppingItem(name, storeName); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to save item", err) - return - } - - ctx := r.Context() - allStores := h.aggregateShoppingLists(ctx) - - // If called from shopping mode, return just the items for that store - if mode == "shopping-mode" { - var items []models.UnifiedShoppingItem - for _, store := range allStores { - if strings.EqualFold(store.Name, storeName) { - for _, cat := range store.Categories { - items = append(items, cat.Items...) - } - } - } - // Sort: unchecked first, then checked - sort.Slice(items, func(i, j int) bool { - if items[i].Checked != items[j].Checked { - return !items[i].Checked - } - return items[i].Name < items[j].Name - }) - HTMLResponse(w, h.renderer, "shopping-mode-items", struct { - StoreName string - Items []models.UnifiedShoppingItem - }{storeName, items}) - return - } - - // Return refreshed shopping tab - HTMLResponse(w, h.renderer, "shopping-tab", struct { - Stores []models.ShoppingStore - Grouped bool - }{allStores, true}) -} - -// HandleShoppingToggle toggles a shopping item's checked state -func (h *Handler) HandleShoppingToggle(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { - JSONError(w, http.StatusBadRequest, "Failed to parse form", err) - return - } - - id := r.FormValue("id") - source := r.FormValue("source") - checked := r.FormValue("checked") == "true" - - 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) - return - } - if err := h.store.ToggleUserShoppingItem(userID, checked); err != nil { - 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 - } - - // Return refreshed shopping tab - stores := h.aggregateShoppingLists(r.Context()) - HTMLResponse(w, h.renderer, "shopping-tab", struct { - Stores []models.ShoppingStore - Grouped bool - }{stores, true}) -} - -// HandleShoppingMode renders the focused shopping mode for a single store -func (h *Handler) HandleShoppingMode(w http.ResponseWriter, r *http.Request) { - storeName := chi.URLParam(r, "store") - if storeName == "" { - http.Redirect(w, r, "/?tab=shopping", http.StatusFound) - return - } - - // URL decode the store name - storeName, _ = url.QueryUnescape(storeName) - - ctx := r.Context() - allStores := h.aggregateShoppingLists(ctx) - - // Find the requested store - var items []models.UnifiedShoppingItem - var storeNames []string - for _, store := range allStores { - storeNames = append(storeNames, store.Name) - if strings.EqualFold(store.Name, storeName) { - // Flatten categories into single item list - for _, cat := range store.Categories { - items = append(items, cat.Items...) - } - } - } - - // Sort: unchecked first, then checked (both alphabetically within their group) - sort.Slice(items, func(i, j int) bool { - if items[i].Checked != items[j].Checked { - return !items[i].Checked // unchecked items first - } - return items[i].Name < items[j].Name - }) - - data := struct { - StoreName string - Items []models.UnifiedShoppingItem - StoreNames []string - CSRFToken string - }{ - StoreName: storeName, - Items: items, - StoreNames: storeNames, - CSRFToken: auth.GetCSRFTokenFromContext(ctx), - } - - HTMLResponse(w, h.renderer, "shopping-mode.html", data) -} - -// HandleShoppingModeToggle toggles an item in shopping mode and returns updated list -func (h *Handler) HandleShoppingModeToggle(w http.ResponseWriter, r *http.Request) { - storeName := chi.URLParam(r, "store") - if err := r.ParseForm(); err != nil { - JSONError(w, http.StatusBadRequest, "Failed to parse form", err) - return - } - - id := r.FormValue("id") - source := r.FormValue("source") - checked := r.FormValue("checked") == "true" - - // Toggle the item - 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) - return - } - if err := h.store.ToggleUserShoppingItem(userID, checked); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to toggle item", err) - return - } - case "trello", "plantoeat": - if err := h.store.SetShoppingItemChecked(source, id, checked); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to toggle item", err) - return - } - } - - // URL decode the store name - storeName, _ = url.QueryUnescape(storeName) - - // Return updated item list partial - ctx := r.Context() - allStores := h.aggregateShoppingLists(ctx) - - var items []models.UnifiedShoppingItem - for _, store := range allStores { - if strings.EqualFold(store.Name, storeName) { - for _, cat := range store.Categories { - items = append(items, cat.Items...) - } - } - } - - // Sort: unchecked first, then checked - sort.Slice(items, func(i, j int) bool { - if items[i].Checked != items[j].Checked { - return !items[i].Checked - } - return items[i].Name < items[j].Name - }) - - HTMLResponse(w, h.renderer, "shopping-mode-items", struct { - StoreName string - Items []models.UnifiedShoppingItem - }{storeName, items}) -} - -// HandleShoppingModeComplete removes an item from the shopping list -func (h *Handler) HandleShoppingModeComplete(w http.ResponseWriter, r *http.Request) { - storeName := chi.URLParam(r, "store") - if err := r.ParseForm(); err != nil { - JSONError(w, http.StatusBadRequest, "Failed to parse form", err) - return - } - - id := r.FormValue("id") - source := r.FormValue("source") - - // Complete (remove) the item - 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) - return - } - if err := h.store.DeleteUserShoppingItem(userID); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to delete item", err) - return - } - case "trello", "plantoeat": - // Mark as checked (will be filtered out of view) - if err := h.store.SetShoppingItemChecked(source, id, true); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to complete item", err) - return - } - } - - // URL decode the store name - storeName, _ = url.QueryUnescape(storeName) - - // Return updated item list partial - ctx := r.Context() - allStores := h.aggregateShoppingLists(ctx) - - var items []models.UnifiedShoppingItem - for _, store := range allStores { - if strings.EqualFold(store.Name, storeName) { - for _, cat := range store.Categories { - items = append(items, cat.Items...) - } - } - } - - // Sort alphabetically (checked items already filtered in template) - sort.Slice(items, func(i, j int) bool { - return items[i].Name < items[j].Name - }) - - HTMLResponse(w, h.renderer, "shopping-mode-items", struct { - StoreName string - Items []models.UnifiedShoppingItem - }{storeName, items}) -} - -// 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 { - log.Printf("ERROR [Shopping/Trello]: %v", err) - } else { - for _, board := range boards { - if strings.EqualFold(board.Name, "Shopping") { - log.Printf("DEBUG [Shopping]: Found Shopping board with %d cards", len(board.Cards)) - for _, card := range board.Cards { - storeName := card.ListName - if storeName == "" { - storeName = "Uncategorized" - } - - if storeMap[storeName] == nil { - storeMap[storeName] = make(map[string][]models.UnifiedShoppingItem) - } - - item := models.UnifiedShoppingItem{ - ID: card.ID, - Name: card.Name, - Store: storeName, - Source: "trello", - Checked: trelloChecks[card.ID], - } - storeMap[storeName][""] = append(storeMap[storeName][""], item) - } - } - } - } - - // 2. Fetch PlanToEat shopping list - if h.planToEatClient != nil { - items, err := h.planToEatClient.GetShoppingList(ctx) - if err != nil { - log.Printf("ERROR [Shopping/PlanToEat]: %v", err) - } else { - log.Printf("DEBUG [Shopping/PlanToEat]: Found %d items", len(items)) - for _, item := range items { - storeName := item.Store - if storeName == "" { - storeName = "PlanToEat" - } - - if storeMap[storeName] == nil { - 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, - Quantity: item.Quantity, - Category: item.Category, - Store: storeName, - Source: "plantoeat", - Checked: checked, - } - storeMap[storeName][item.Category] = append(storeMap[storeName][item.Category], unified) - } - } - } else { - log.Printf("DEBUG [Shopping/PlanToEat]: Client not configured") - } - - // 3. Fetch user-added shopping items - userItems, err := h.store.GetUserShoppingItems() - if err != nil { - log.Printf("ERROR [Shopping/User]: %v", err) - } else { - for _, item := range userItems { - storeName := item.Store - if storeName == "" { - continue // Skip items without a store - } - - if storeMap[storeName] == nil { - storeMap[storeName] = make(map[string][]models.UnifiedShoppingItem) - } - - unified := models.UnifiedShoppingItem{ - ID: fmt.Sprintf("user-%d", item.ID), - Name: item.Name, - Store: storeName, - Source: "user", - Checked: item.Checked, - } - storeMap[storeName][""] = append(storeMap[storeName][""], unified) - } - } - - // 4. Convert map to sorted slice - var stores []models.ShoppingStore - for storeName, categories := range storeMap { - store := models.ShoppingStore{Name: storeName} - - // Get sorted category names - var catNames []string - for catName := range categories { - catNames = append(catNames, catName) - } - sort.Strings(catNames) - - for _, catName := range catNames { - items := categories[catName] - sort.Slice(items, func(i, j int) bool { - return items[i].Name < items[j].Name - }) - store.Categories = append(store.Categories, models.ShoppingCategory{ - Name: catName, - Items: items, - }) - } - stores = append(stores, store) - } - - // Sort stores alphabetically - sort.Slice(stores, func(i, j int) bool { - return stores[i].Name < stores[j].Name - }) - - return stores -} - // HandleTabConditions renders the Conditions tab with live feeds func (h *Handler) HandleTabConditions(w http.ResponseWriter, r *http.Request) { HTMLResponse(w, h.renderer, "conditions-tab", nil) |
