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 EditItemModal from "../components/modals/EditItemModal"; import SimilarItemModal from "../components/modals/SimilarItemModal"; import { ZONE_FLOW } from "../constants/classifications"; import { ROLES } from "../constants/roles"; import { AuthContext } from "../context/AuthContext"; import { SettingsContext } from "../context/SettingsContext"; import "../styles/pages/GroceryList.css"; import { findSimilarItems } from "../utils/stringSimilarity"; export default function GroceryList() { const { role } = useContext(AuthContext); 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); // === Data Loading === const loadItems = async () => { setLoading(true); const res = await getList(); console.log(res.data); setItems(res.data); setLoading(false); }; const loadRecentlyBought = async () => { try { const res = await getRecentlyBought(); setRecentlyBoughtItems(res.data); } catch (error) { console.error("Failed to load recently bought items:", error); setRecentlyBoughtItems([]); } }; useEffect(() => { loadItems(); loadRecentlyBought(); }, []); // === 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; } const allItems = [...items, ...recentlyBoughtItems]; const lowerText = text.toLowerCase().trim(); const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText); setButtonText(exactMatch ? "Add" : "Create + Add"); try { const suggestions = await getSuggestions(text); setSuggestions(suggestions.data.map(s => s.item_name)); } catch { setSuggestions([]); } }; // === Item Addition Handlers === const handleAdd = useCallback(async (itemName, quantity) => { if (!itemName.trim()) return; let existingItem = null; try { const response = await getItemByName(itemName); existingItem = response.data; } catch { existingItem = null; } 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; }); }, [recentlyBoughtItems]); const processItemAddition = useCallback(async (itemName, quantity) => { let existingItem = null; try { const response = await getItemByName(itemName); existingItem = response.data; } catch { existingItem = null; } if (existingItem?.bought === false) { const currentQuantity = existingItem.quantity; const newQuantity = currentQuantity + quantity; const yes = window.confirm( `Item "${itemName}" already exists in the list. Do you want to update its quantity from ${currentQuantity} to ${newQuantity}?` ); if (!yes) return; const response = await addItem(itemName, newQuantity, null); setSuggestions([]); setButtonText("Add Item"); if (response.data) { setItems(prevItems => prevItems.map(item => item.id === existingItem.id ? { ...item, ...response.data } : item ) ); } } else if (existingItem) { const response = await addItem(itemName, quantity, null); setSuggestions([]); setButtonText("Add Item"); if (response.data) { setItems(prevItems => [...prevItems, response.data]); setRecentlyBoughtItems(prevItems => prevItems.filter(item => item.id !== existingItem.id)); } } else { setPendingItem({ itemName, quantity }); setShowAddDetailsModal(true); } }, []); // === 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]); // === Add Details Modal Handlers === const handleAddDetailsConfirm = useCallback(async (imageFile, classification) => { if (!pendingItem) return; try { const addResponse = await addItem(pendingItem.itemName, pendingItem.quantity, imageFile); let newItem = addResponse.data; if (classification) { const itemResponse = await getItemByName(pendingItem.itemName); const itemId = itemResponse.data.id; const updateResponse = await updateItemWithClassification(itemId, undefined, undefined, classification); newItem = { ...newItem, ...updateResponse.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."); } }, [pendingItem]); const handleAddDetailsSkip = useCallback(async () => { if (!pendingItem) return; try { const response = await addItem(pendingItem.itemName, pendingItem.quantity, null); setShowAddDetailsModal(false); setPendingItem(null); setSuggestions([]); setButtonText("Add Item"); if (response.data) { setItems(prevItems => [...prevItems, response.data]); } } catch (error) { console.error("Failed to add item:", error); alert("Failed to add item. Please try again."); } }, [pendingItem]); const handleAddDetailsCancel = useCallback(() => { setShowAddDetailsModal(false); setPendingItem(null); setSuggestions([]); setButtonText("Add Item"); }, []); // === Item Action Handlers === const handleBought = useCallback(async (id, quantity) => { await markBought(id); setItems(prevItems => prevItems.filter(item => item.id !== id)); loadRecentlyBought(); }, []); const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile) => { try { const response = await updateItemImage(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."); } }, []); const handleLongPress = useCallback(async (item) => { if (![ROLES.ADMIN, ROLES.EDITOR].includes(role)) return; try { const classificationResponse = await getClassification(item.id); setEditingItem({ ...item, classification: classificationResponse.data }); setShowEditModal(true); } catch (error) { console.error("Failed to load classification:", error); setEditingItem({ ...item, classification: null }); setShowEditModal(true); } }, [role]); // === Edit Modal Handlers === const handleEditSave = useCallback(async (id, itemName, quantity, classification) => { try { const response = await updateItemWithClassification(id, itemName, quantity, classification); setShowEditModal(false); setEditingItem(null); const updatedItem = response.data; setItems(prevItems => prevItems.map(item => item.id === id ? { ...item, ...updatedItem } : item ) ); setRecentlyBoughtItems(prevItems => prevItems.map(item => item.id === id ? { ...item, ...updatedItem } : item ) ); } catch (error) { console.error("Failed to update item:", error); throw error; } }, []); 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 (loading) return

Loading...

; return (

Costco Grocery List

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

{zone === 'unclassified' ? 'Unclassified' : zone}

    {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 && ( <>

Recently Bought (24HR)

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