All files / src/hooks useShoppingLists.tsx

100% Statements 53/53
100% Branches 20/20
100% Functions 10/10
100% Lines 46/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189                                                  1x 31x 31x     31x     31x 31x 31x 31x 31x     31x 21x             99x 21x                   31x   31x     31x   12x 5x   19x   1x               31x   3x   2x 2x     1x                   31x   3x   2x 2x     1x                         31x   4x   3x 3x     1x                   31x   3x   2x 2x     1x                   31x   3x   2x 2x     1x           31x                                        
// src/hooks/useShoppingLists.tsx
import { useState, useCallback, useEffect, useMemo } from 'react';
import { useAuth } from '../hooks/useAuth';
import { useUserData } from '../hooks/useUserData';
import {
  useCreateShoppingListMutation,
  useDeleteShoppingListMutation,
  useAddShoppingListItemMutation,
  useUpdateShoppingListItemMutation,
  useRemoveShoppingListItemMutation,
} from './mutations';
import { logger } from '../services/logger.client';
import type { ShoppingListItem } from '../types';
 
/**
 * A custom hook to manage all state and logic related to shopping lists.
 *
 * This hook has been refactored to use TanStack Query mutations (ADR-0005 Phase 4).
 * It provides a simplified interface for shopping list operations with:
 * - Automatic cache invalidation
 * - Success/error notifications
 * - No manual state management
 *
 * The interface remains backward compatible with the previous implementation.
 */
const useShoppingListsHook = () => {
  const { userProfile } = useAuth();
  const { shoppingLists } = useUserData();
 
  // Local state for tracking the active list (UI concern, not server state)
  const [activeListId, setActiveListId] = useState<number | null>(null);
 
  // TanStack Query mutation hooks
  const createListMutation = useCreateShoppingListMutation();
  const deleteListMutation = useDeleteShoppingListMutation();
  const addItemMutation = useAddShoppingListItemMutation();
  const updateItemMutation = useUpdateShoppingListItemMutation();
  const removeItemMutation = useRemoveShoppingListItemMutation();
 
  // Consolidate errors from all mutations
  const error = useMemo(() => {
    const errors = [
      createListMutation.error,
      deleteListMutation.error,
      addItemMutation.error,
      updateItemMutation.error,
      removeItemMutation.error,
    ];
    const firstError = errors.find((err) => err !== null);
    return firstError?.message || null;
  }, [
    createListMutation.error,
    deleteListMutation.error,
    addItemMutation.error,
    updateItemMutation.error,
    removeItemMutation.error,
  ]);
 
  // Effect to select the first list as active when lists are loaded or the user changes.
  useEffect(() => {
    // Check if the currently active list still exists in the shoppingLists array.
    const activeListExists = shoppingLists.some((l) => l.shopping_list_id === activeListId);
 
    // If the user is logged in and there are lists...
    if (userProfile && shoppingLists.length > 0) {
      // ...but no list is active, or the active one was deleted, select the first available list.
      if (!activeListExists) {
        setActiveListId(shoppingLists[0].shopping_list_id);
      }
    } else if (activeListId !== null) {
      // If there's no user or no lists, ensure no list is active.
      setActiveListId(null);
    }
  }, [shoppingLists, userProfile, activeListId]);
 
  /**
   * Create a new shopping list.
   * Uses TanStack Query mutation which automatically invalidates the cache.
   */
  const createList = useCallback(
    async (name: string) => {
      if (!userProfile) return;
 
      try {
        await createListMutation.mutateAsync({ name });
      } catch (error) {
        // Error is already handled by the mutation hook (notification shown)
        logger.error({ err: error }, '[useShoppingLists] Failed to create list');
      }
    },
    [userProfile, createListMutation],
  );
 
  /**
   * Delete a shopping list.
   * Uses TanStack Query mutation which automatically invalidates the cache.
   */
  const deleteList = useCallback(
    async (listId: number) => {
      if (!userProfile) return;
 
      try {
        await deleteListMutation.mutateAsync({ listId });
      } catch (error) {
        // Error is already handled by the mutation hook (notification shown)
        logger.error({ err: error }, '[useShoppingLists] Failed to delete list');
      }
    },
    [userProfile, deleteListMutation],
  );
 
  /**
   * Add an item to a shopping list.
   * Uses TanStack Query mutation which automatically invalidates the cache.
   *
   * Note: Duplicate checking has been moved to the server-side.
   * The API will handle duplicate detection and return appropriate errors.
   */
  const addItemToList = useCallback(
    async (listId: number, item: { masterItemId?: number; customItemName?: string }) => {
      if (!userProfile) return;
 
      try {
        await addItemMutation.mutateAsync({ listId, item });
      } catch (error) {
        // Error is already handled by the mutation hook (notification shown)
        logger.error({ err: error }, '[useShoppingLists] Failed to add item');
      }
    },
    [userProfile, addItemMutation],
  );
 
  /**
   * Update a shopping list item (quantity, purchased status, notes, etc).
   * Uses TanStack Query mutation which automatically invalidates the cache.
   */
  const updateItemInList = useCallback(
    async (itemId: number, updates: Partial<ShoppingListItem>) => {
      if (!userProfile) return;
 
      try {
        await updateItemMutation.mutateAsync({ itemId, updates });
      } catch (error) {
        // Error is already handled by the mutation hook (notification shown)
        logger.error({ err: error }, '[useShoppingLists] Failed to update item');
      }
    },
    [userProfile, updateItemMutation],
  );
 
  /**
   * Remove an item from a shopping list.
   * Uses TanStack Query mutation which automatically invalidates the cache.
   */
  const removeItemFromList = useCallback(
    async (itemId: number) => {
      if (!userProfile) return;
 
      try {
        await removeItemMutation.mutateAsync({ itemId });
      } catch (error) {
        // Error is already handled by the mutation hook (notification shown)
        logger.error({ err: error }, '[useShoppingLists] Failed to remove item');
      }
    },
    [userProfile, removeItemMutation],
  );
 
  return {
    shoppingLists,
    activeListId,
    setActiveListId,
    createList,
    deleteList,
    addItemToList,
    updateItemInList,
    removeItemFromList,
    // Loading states from mutations
    isCreatingList: createListMutation.isPending,
    isDeletingList: deleteListMutation.isPending,
    isAddingItem: addItemMutation.isPending,
    isUpdatingItem: updateItemMutation.isPending,
    isRemovingItem: removeItemMutation.isPending,
    error,
  };
};
 
export { useShoppingListsHook as useShoppingLists };