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 { 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); const [collapsedZones, setCollapsedZones] = useState({}); const [showConfirmAddExisting, setShowConfirmAddExisting] = useState(false); const [confirmAddExistingData, setConfirmAddExistingData] = useState(null); // === 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(); }, []); // === 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; } const lowerText = text.toLowerCase().trim(); try { const response = await getSuggestions(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; 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; // Show modal instead of window.confirm setConfirmAddExistingData({ itemName, currentQuantity, addingQuantity: quantity, newQuantity, existingItem }); setShowConfirmAddExisting(true); } else if (existingItem) { await addItem(itemName, quantity, null); setSuggestions([]); setButtonText("Add Item"); // Reload lists to reflect the changes await loadItems(); await loadRecentlyBought(); } 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]); // === Confirm Add Existing Modal Handlers === const handleConfirmAddExisting = useCallback(async () => { if (!confirmAddExistingData) return; const { itemName, newQuantity, existingItem } = confirmAddExistingData; setShowConfirmAddExisting(false); setConfirmAddExistingData(null); try { // Update the item await addItem(itemName, newQuantity, null); // Fetch the updated item with properly formatted data const response = await getItemByName(itemName); const updatedItem = response.data; // Update state with the full item 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); // Fallback to full reload on error await loadItems(); } }, [confirmAddExistingData, loadItems]); const handleCancelAddExisting = useCallback(() => { setShowConfirmAddExisting(false); setConfirmAddExistingData(null); }, []); // === 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) => { const item = items.find(i => i.id === id); if (!item) return; await markBought(id, quantity); // If buying full quantity, remove from list if (quantity >= item.quantity) { setItems(prevItems => prevItems.filter(item => item.id !== id)); } else { // If partial, update quantity const response = await getItemByName(item.item_name); if (response.data) { setItems(prevItems => prevItems.map(item => item.id === id ? response.data : item) ); } } loadRecentlyBought(); }, [items]); 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 (