summaryrefslogtreecommitdiff
path: root/internal/handlers/handlers.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/handlers.go')
-rw-r--r--internal/handlers/handlers.go454
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)