import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { addItem, getClassification, getItemByName, getList, getRecentlyBought, getSuggestions, markBought, updateItemImage, updateItemWithClassification } from "../api/list"; import FloatingActionButton from "../components/common/FloatingActionButton"; import SortDropdown from "../components/common/SortDropdown"; import AddItemForm from "../components/forms/AddItemForm"; import GroceryListItem from "../components/items/GroceryListItem"; import AddItemWithDetailsModal from "../components/modals/AddItemWithDetailsModal"; import ConfirmAddExistingModal from "../components/modals/ConfirmAddExistingModal"; import EditItemModal from "../components/modals/EditItemModal"; import SimilarItemModal from "../components/modals/SimilarItemModal"; import StoreTabs from "../components/store/StoreTabs"; import { ZONE_FLOW } from "../constants/classifications"; import { ROLES } from "../constants/roles"; import { AuthContext } from "../context/AuthContext"; import { HouseholdContext } from "../context/HouseholdContext"; import { SettingsContext } from "../context/SettingsContext"; import { StoreContext } from "../context/StoreContext"; import "../styles/pages/GroceryList.css"; import { findSimilarItems } from "../utils/stringSimilarity"; export default function GroceryList() { const { role } = useContext(AuthContext); const { activeHousehold } = useContext(HouseholdContext); const { activeStore } = useContext(StoreContext); const { settings } = useContext(SettingsContext); // === State === // const [items, setItems] = useState([]); const [recentlyBoughtItems, setRecentlyBoughtItems] = useState([]); const [recentlyBoughtDisplayCount, setRecentlyBoughtDisplayCount] = useState(settings.recentlyBoughtCount); const [sortMode, setSortMode] = useState(settings.defaultSortMode); const [suggestions, setSuggestions] = useState([]); const [showAddForm, setShowAddForm] = useState(true); const [loading, setLoading] = useState(true); const [buttonText, setButtonText] = useState("Add Item"); const [pendingItem, setPendingItem] = useState(null); const [showAddDetailsModal, setShowAddDetailsModal] = useState(false); const [showSimilarModal, setShowSimilarModal] = useState(false); const [similarItemSuggestion, setSimilarItemSuggestion] = useState(null); const [showEditModal, setShowEditModal] = useState(false); const [editingItem, setEditingItem] = useState(null); const [recentlyBoughtCollapsed, setRecentlyBoughtCollapsed] = useState(settings.recentlyBoughtCollapsed); const [collapsedZones, setCollapsedZones] = useState({}); const [showConfirmAddExisting, setShowConfirmAddExisting] = useState(false); const [confirmAddExistingData, setConfirmAddExistingData] = useState(null); // === Data Loading === const loadItems = async () => { if (!activeHousehold?.id || !activeStore?.id) { setLoading(false); return; } setLoading(true); try { const res = await getList(activeHousehold.id, activeStore.id); console.log('[GroceryList] Items loaded:', res.data); setItems(res.data.items || res.data || []); } catch (error) { console.error('[GroceryList] Failed to load items:', error); setItems([]); } finally { setLoading(false); } }; const loadRecentlyBought = async () => { if (!activeHousehold?.id || !activeStore?.id) return; try { const res = await getRecentlyBought(activeHousehold.id, activeStore.id); setRecentlyBoughtItems(res.data); } catch (error) { console.error("Failed to load recently bought items:", error); setRecentlyBoughtItems([]); } }; useEffect(() => { loadItems(); loadRecentlyBought(); }, [activeHousehold?.id, activeStore?.id]); // === Zone Collapse Handler === const toggleZoneCollapse = (zone) => { setCollapsedZones(prev => ({ ...prev, [zone]: !prev[zone] })); }; // === Sorted Items Computation === const sortedItems = useMemo(() => { const sorted = [...items]; if (sortMode === "az") sorted.sort((a, b) => a.item_name.localeCompare(b.item_name)); if (sortMode === "za") sorted.sort((a, b) => b.item_name.localeCompare(a.item_name)); if (sortMode === "qty-high") sorted.sort((a, b) => b.quantity - a.quantity); if (sortMode === "qty-low") sorted.sort((a, b) => a.quantity - b.quantity); if (sortMode === "zone") { sorted.sort((a, b) => { // Items without classification go to the end if (!a.zone && b.zone) return 1; if (a.zone && !b.zone) return -1; if (!a.zone && !b.zone) return a.item_name.localeCompare(b.item_name); // Sort by ZONE_FLOW order const aZoneIndex = ZONE_FLOW.indexOf(a.zone); const bZoneIndex = ZONE_FLOW.indexOf(b.zone); // If zone not in ZONE_FLOW, put at end const aIndex = aZoneIndex === -1 ? ZONE_FLOW.length : aZoneIndex; const bIndex = bZoneIndex === -1 ? ZONE_FLOW.length : bZoneIndex; const zoneCompare = aIndex - bIndex; if (zoneCompare !== 0) return zoneCompare; // Then by item_type const typeCompare = (a.item_type || "").localeCompare(b.item_type || ""); if (typeCompare !== 0) return typeCompare; // Then by item_group const groupCompare = (a.item_group || "").localeCompare(b.item_group || ""); if (groupCompare !== 0) return groupCompare; // Finally by name return a.item_name.localeCompare(b.item_name); }); } return sorted; }, [items, sortMode]); // === Suggestion Handler === const handleSuggest = async (text) => { if (!text.trim()) { setSuggestions([]); setButtonText("Add Item"); return; } if (!activeHousehold?.id || !activeStore?.id) { setSuggestions([]); setButtonText("Create + Add"); return; } const lowerText = text.toLowerCase().trim(); try { const response = await getSuggestions(activeHousehold.id, activeStore.id, text); const suggestionList = response.data.map(s => s.item_name); setSuggestions(suggestionList); // All suggestions are now lowercase from DB, direct comparison const exactMatch = suggestionList.includes(lowerText); setButtonText(exactMatch ? "Add" : "Create + Add"); } catch { setSuggestions([]); setButtonText("Create + Add"); } }; // === Item Addition Handlers === const handleAdd = useCallback(async (itemName, quantity) => { if (!itemName.trim()) return; if (!activeHousehold?.id || !activeStore?.id) return; // Check if item already exists let existingItem = null; try { const response = await getItemByName(activeHousehold.id, activeStore.id, itemName); existingItem = response.data; } catch { // Item doesn't exist, continue } if (existingItem) { await processItemAddition(itemName, quantity); return; } setItems(prevItems => { const allItems = [...prevItems, ...recentlyBoughtItems]; const similar = findSimilarItems(itemName, allItems, 70); if (similar.length > 0) { setSimilarItemSuggestion({ originalName: itemName, suggestedItem: similar[0], quantity }); setShowSimilarModal(true); return prevItems; } processItemAddition(itemName, quantity); return prevItems; }); }, [activeHousehold?.id, activeStore?.id, recentlyBoughtItems]); const processItemAddition = useCallback(async (itemName, quantity) => { if (!activeHousehold?.id || !activeStore?.id) return; // Fetch current item state from backend let existingItem = null; try { const response = await getItemByName(activeHousehold.id, activeStore.id, itemName); existingItem = response.data; } catch { // Item doesn't exist, continue with add } if (existingItem?.bought === false) { const currentQuantity = existingItem.quantity; const newQuantity = currentQuantity + quantity; // Show modal instead of window.confirm setConfirmAddExistingData({ itemName, currentQuantity, addingQuantity: quantity, newQuantity, existingItem }); setShowConfirmAddExisting(true); } else if (existingItem) { await addItem(activeHousehold.id, activeStore.id, itemName, quantity, null); setSuggestions([]); setButtonText("Add Item"); // Reload lists to reflect the changes await loadItems(); await loadRecentlyBought(); } else { setPendingItem({ itemName, quantity }); setShowAddDetailsModal(true); } }, [activeHousehold?.id, activeStore?.id, items, loadItems]); // === Similar Item Modal Handlers === const handleSimilarCancel = useCallback(() => { setShowSimilarModal(false); setSimilarItemSuggestion(null); }, []); const handleSimilarNo = useCallback(async () => { if (!similarItemSuggestion) return; setShowSimilarModal(false); await processItemAddition(similarItemSuggestion.originalName, similarItemSuggestion.quantity); setSimilarItemSuggestion(null); }, [similarItemSuggestion, processItemAddition]); const handleSimilarYes = useCallback(async () => { if (!similarItemSuggestion) return; setShowSimilarModal(false); await processItemAddition(similarItemSuggestion.suggestedItem.item_name, similarItemSuggestion.quantity); setSimilarItemSuggestion(null); }, [similarItemSuggestion, processItemAddition]); // === Confirm Add Existing Modal Handlers === const handleConfirmAddExisting = useCallback(async () => { if (!confirmAddExistingData) return; if (!activeHousehold?.id || !activeStore?.id) return; const { itemName, newQuantity, existingItem } = confirmAddExistingData; setShowConfirmAddExisting(false); setConfirmAddExistingData(null); try { await addItem(activeHousehold.id, activeStore.id, itemName, newQuantity, null); const response = await getItemByName(activeHousehold.id, activeStore.id, itemName); const updatedItem = response.data; setItems(prevItems => prevItems.map(item => item.id === existingItem.id ? updatedItem : item ) ); setSuggestions([]); setButtonText("Add Item"); } catch (error) { console.error("Failed to update item:", error); await loadItems(); } }, []); // === Add Details Modal Handlers === const handleAddWithDetails = useCallback(async (imageFile, classification) => { if (!pendingItem) return; if (!activeHousehold?.id || !activeStore?.id) return; try { await addItem(activeHousehold.id, activeStore.id, pendingItem.itemName, pendingItem.quantity, imageFile); if (classification) { // Apply classification if provided await updateItemWithClassification(activeHousehold.id, activeStore.id, pendingItem.itemName, pendingItem.quantity, classification); } // Fetch the newly added item const itemResponse = await getItemByName(activeHousehold.id, activeStore.id, pendingItem.itemName); const newItem = itemResponse.data; setShowAddDetailsModal(false); setPendingItem(null); setSuggestions([]); setButtonText("Add Item"); // Add to state if (newItem) { setItems(prevItems => [...prevItems, newItem]); } } catch (error) { console.error("Failed to add item:", error); alert("Failed to add item. Please try again."); } }, [activeHousehold?.id, activeStore?.id, pendingItem]); const handleAddDetailsSkip = useCallback(async () => { if (!pendingItem) return; if (!activeHousehold?.id || !activeStore?.id) return; try { await addItem(activeHousehold.id, activeStore.id, pendingItem.itemName, pendingItem.quantity, null); // Fetch the newly added item const itemResponse = await getItemByName(activeHousehold.id, activeStore.id, pendingItem.itemName); const newItem = itemResponse.data; setShowAddDetailsModal(false); setPendingItem(null); setSuggestions([]); setButtonText("Add Item"); if (newItem) { setItems(prevItems => [...prevItems, newItem]); } } catch (error) { console.error("Failed to add item:", error); alert("Failed to add item. Please try again."); } }, [activeHousehold?.id, activeStore?.id, pendingItem]); const handleAddDetailsCancel = useCallback(() => { setShowAddDetailsModal(false); setPendingItem(null); setSuggestions([]); setButtonText("Add Item"); }, []); // === Item Action Handlers === const handleBought = useCallback(async (id, quantity) => { if (!activeHousehold?.id || !activeStore?.id) return; const item = items.find(i => i.id === id); if (!item) return; await markBought(activeHousehold.id, activeStore.id, item.item_name, quantity, true); // If buying full quantity, remove from list if (quantity >= item.quantity) { setItems(prevItems => prevItems.filter(item => item.id !== id)); } else { // If partial, fetch updated item const response = await getItemByName(activeHousehold.id, activeStore.id, item.item_name); const updatedItem = response.data; setItems(prevItems => prevItems.map(i => i.id === id ? updatedItem : i) ); } loadRecentlyBought(); }, [activeHousehold?.id, activeStore?.id, items]); const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => { if (!activeHousehold?.id || !activeStore?.id) return; try { const response = await updateItemImage(activeHousehold.id, activeStore.id, id, itemName, quantity, imageFile); setItems(prevItems => prevItems.map(item => item.id === id ? { ...item, ...response.data } : item ) ); setRecentlyBoughtItems(prevItems => prevItems.map(item => item.id === id ? { ...item, ...response.data } : item ) ); } catch (error) { console.error("Failed to add image:", error); alert("Failed to add image. Please try again."); } }, [activeHousehold?.id, activeStore?.id]); const handleLongPress = useCallback(async (item) => { if (![ROLES.ADMIN, ROLES.EDITOR].includes(role)) return; if (!activeHousehold?.id || !activeStore?.id) return; try { const classificationResponse = await getClassification(activeHousehold.id, activeStore.id, item.id); setEditingItem({ ...item, classification: classificationResponse.data }); setShowEditModal(true); } catch (error) { console.error("Failed to load classification:", error); setEditingItem({ ...item, classification: null }); setShowEditModal(true); } }, [activeHousehold?.id, activeStore?.id, role]); // === Edit Modal Handlers === const handleEditSave = useCallback(async (id, itemName, quantity, classification) => { if (!activeHousehold?.id || !activeStore?.id) return; try { await updateItemWithClassification(activeHousehold.id, activeStore.id, itemName, quantity, classification); // Fetch the updated item const response = await getItemByName(activeHousehold.id, activeStore.id, itemName); const updatedItem = response.data; setShowEditModal(false); setEditingItem(null); setItems(prevItems => prevItems.map(item => item.id === id ? updatedItem : item ) ); setRecentlyBoughtItems(prevItems => prevItems.map(item => item.id === id ? { ...item, ...updatedItem } : item ) ); } catch (error) { console.error("Failed to update item:", error); throw error; } }, [activeHousehold?.id, activeStore?.id]); const handleEditCancel = useCallback(() => { setShowEditModal(false); setEditingItem(null); }, []); // === Helper Functions === const groupItemsByZone = (items) => { const groups = {}; items.forEach(item => { const zone = item.zone || 'unclassified'; if (!groups[zone]) { groups[zone] = []; } groups[zone].push(item); }); return groups; }; if (!activeHousehold || !activeStore) { return (

Costco Grocery List

{!activeHousehold ? 'Loading households...' : 'Loading stores...'}

); } if (loading) { return (

Costco Grocery List

Loading grocery list...

); } return (

Costco Grocery List

{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && showAddForm && ( )} {sortMode === "zone" ? ( (() => { const grouped = groupItemsByZone(sortedItems); return Object.keys(grouped).map(zone => { const isCollapsed = collapsedZones[zone]; const itemCount = grouped[zone].length; return (

toggleZoneCollapse(zone)} > {zone === 'unclassified' ? 'Unclassified' : zone} ({itemCount}) {isCollapsed ? "▼" : "▲"}

{!isCollapsed && (
    {grouped[zone].map((item) => ( [ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity) } onImageAdded={ [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null } onLongPress={ [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleLongPress : null } /> ))}
)}
); }); })() ) : (
    {sortedItems.map((item) => ( [ROLES.ADMIN, ROLES.EDITOR].includes(role) && handleBought(id, quantity) } onImageAdded={ [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleImageAdded : null } onLongPress={ [ROLES.ADMIN, ROLES.EDITOR].includes(role) ? handleLongPress : null } /> ))}
)} {recentlyBoughtItems.length > 0 && settings.showRecentlyBought && ( <>

setRecentlyBoughtCollapsed(!recentlyBoughtCollapsed)} > Recently Bought (24HR) {recentlyBoughtCollapsed ? "▼" : "▲"}

{!recentlyBoughtCollapsed && ( <>
    {recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount).map((item) => ( ))}
{recentlyBoughtDisplayCount < recentlyBoughtItems.length && (
)} )} )}
{[ROLES.ADMIN, ROLES.EDITOR].includes(role) && ( setShowAddForm(!showAddForm)} /> )} {showAddDetailsModal && pendingItem && ( )} {showSimilarModal && similarItemSuggestion && ( )} {showEditModal && editingItem && ( )} {showConfirmAddExisting && confirmAddExistingData && ( { setShowConfirmAddExisting(false); setConfirmAddExistingData(null); }} /> )}
); }