diff options
Diffstat (limited to 'internal/handlers/handlers.go')
| -rw-r--r-- | internal/handlers/handlers.go | 152 |
1 files changed, 145 insertions, 7 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index a1a12e7..115d903 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -6,12 +6,15 @@ 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" @@ -1147,25 +1150,51 @@ func (h *Handler) HandleShoppingQuickAdd(w http.ResponseWriter, r *http.Request) } name := strings.TrimSpace(r.FormValue("name")) - store := strings.TrimSpace(r.FormValue("store")) + 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 store == "" { - store = "Quick Add" + if storeName == "" { + storeName = "Quick Add" } - if err := h.store.SaveUserShoppingItem(name, store); err != nil { + if err := h.store.SaveUserShoppingItem(name, storeName); err != nil { JSONError(w, http.StatusInternalServerError, "Failed to save item", err) return } - // Return refreshed shopping tab ctx := r.Context() - stores := h.aggregateShoppingLists(ctx) - HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{stores}) + 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.templates, "shopping-mode-items", struct { + StoreName string + Items []models.UnifiedShoppingItem + }{storeName, items}) + return + } + + // Return refreshed shopping tab + HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{allStores}) } // HandleShoppingToggle toggles a shopping item's checked state @@ -1206,6 +1235,115 @@ func (h *Handler) HandleShoppingToggle(w http.ResponseWriter, r *http.Request) { HTMLResponse(w, h.templates, "shopping-tab", struct{ Stores []models.ShoppingStore }{stores}) } +// 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 + }{ + StoreName: storeName, + Items: items, + StoreNames: storeNames, + } + + HTMLResponse(w, h.templates, "shopping-mode", 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.templates, "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) |
