import { useContext, useEffect, 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 { ROLES } from "../constants/roles"; import { AuthContext } from "../context/AuthContext"; import "../styles/pages/GroceryList.css"; import { findSimilarItems } from "../utils/stringSimilarity"; export default function GroceryList() { const { role } = useContext(AuthContext); const [items, setItems] = useState([]); const [recentlyBoughtItems, setRecentlyBoughtItems] = useState([]); const [recentlyBoughtDisplayCount, setRecentlyBoughtDisplayCount] = useState(10); const [sortedItems, setSortedItems] = useState([]); const [sortMode, setSortMode] = useState("zone"); 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 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(); }, []); useEffect(() => { let 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.item_type && b.item_type) return 1; if (a.item_type && !b.item_type) return -1; if (!a.item_type && !b.item_type) return a.item_name.localeCompare(b.item_name); // Sort 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; // Then by zone const zoneCompare = (a.zone || "").localeCompare(b.zone || ""); if (zoneCompare !== 0) return zoneCompare; // Finally by name return a.item_name.localeCompare(b.item_name); }); } setSortedItems(sorted); }, [items, sortMode]); const handleSuggest = async (text) => { if (!text.trim()) { setSuggestions([]); setButtonText("Add Item"); return; } // Combine both unbought and recently bought items for similarity checking const allItems = [...items, ...recentlyBoughtItems]; // Check if exact match exists (case-insensitive) const lowerText = text.toLowerCase().trim(); const exactMatch = allItems.find(item => item.item_name.toLowerCase() === lowerText); if (exactMatch) { setButtonText("Add"); } else { setButtonText("Create + Add"); } try { let suggestions = await getSuggestions(text); suggestions = suggestions.data.map(s => s.item_name); setSuggestions(suggestions); } catch { setSuggestions([]); } }; const handleAdd = async (itemName, quantity) => { if (!itemName.trim()) return; const lowerItemName = itemName.toLowerCase().trim(); // First check if exact item exists in database (case-insensitive) let existingItem = null; try { const response = await getItemByName(itemName); existingItem = response.data; } catch { existingItem = null; } // If exact item exists, skip similarity check and process directly if (existingItem) { await processItemAddition(itemName, quantity); return; } // Only check for similar items if exact item doesn't exist const allItems = [...items, ...recentlyBoughtItems]; const similar = findSimilarItems(itemName, allItems, 80); if (similar.length > 0) { // Show modal and wait for user decision setSimilarItemSuggestion({ originalName: itemName, suggestedItem: similar[0], quantity }); setShowSimilarModal(true); return; } // Continue with normal flow for new items await processItemAddition(itemName, quantity); }; const processItemAddition = async (itemName, quantity) => { // Check if item exists in database (case-insensitive) let existingItem = null; try { const response = await getItemByName(itemName); existingItem = response.data; } catch { existingItem = null; } if (existingItem && existingItem.bought === false) { // Item exists and is unbought - update quantity 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; await addItem(itemName, newQuantity, null); setSuggestions([]); setButtonText("Add Item"); loadItems(); } else if (existingItem) { // Item exists in database (was previously bought) - just add quantity await addItem(itemName, quantity, null); setSuggestions([]); setButtonText("Add Item"); loadItems(); } else { // NEW ITEM - show combined add details modal setPendingItem({ itemName, quantity }); setShowAddDetailsModal(true); } }; const handleSimilarCancel = () => { setShowSimilarModal(false); setSimilarItemSuggestion(null); }; const handleSimilarNo = async () => { if (!similarItemSuggestion) return; setShowSimilarModal(false); // Create new item with original name await processItemAddition(similarItemSuggestion.originalName, similarItemSuggestion.quantity); setSimilarItemSuggestion(null); }; const handleSimilarYes = async () => { if (!similarItemSuggestion) return; setShowSimilarModal(false); // Use suggested item name await processItemAddition(similarItemSuggestion.suggestedItem.item_name, similarItemSuggestion.quantity); setSimilarItemSuggestion(null); }; const handleAddDetailsConfirm = async (imageFile, classification) => { if (!pendingItem) return; try { // Add item to grocery_list with image await addItem(pendingItem.itemName, pendingItem.quantity, imageFile); // If classification provided, add it if (classification) { const itemResponse = await getItemByName(pendingItem.itemName); const itemId = itemResponse.data.id; await updateItemWithClassification(itemId, undefined, undefined, classification); } setShowAddDetailsModal(false); setPendingItem(null); setSuggestions([]); setButtonText("Add Item"); loadItems(); } catch (error) { console.error("Failed to add item:", error); alert("Failed to add item. Please try again."); } }; const handleAddDetailsSkip = async () => { if (!pendingItem) return; try { // Add item without image or classification await addItem(pendingItem.itemName, pendingItem.quantity, null); setShowAddDetailsModal(false); setPendingItem(null); setSuggestions([]); setButtonText("Add Item"); loadItems(); } catch (error) { console.error("Failed to add item:", error); alert("Failed to add item. Please try again."); } }; const handleAddDetailsCancel = () => { setShowAddDetailsModal(false); setPendingItem(null); setSuggestions([]); setButtonText("Add Item"); }; const handleBought = async (id, quantity) => { await markBought(id); loadItems(); loadRecentlyBought(); }; const handleImageAdded = async (id, itemName, quantity, imageFile) => { try { await updateItemImage(id, itemName, quantity, imageFile); loadItems(); // Reload to show new image } catch (error) { console.error("Failed to add image:", error); alert("Failed to add image. Please try again."); } }; const handleLongPress = async (item) => { if (![ROLES.ADMIN, ROLES.EDITOR].includes(role)) return; try { // Fetch existing classification 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); } }; const handleEditSave = async (id, itemName, quantity, classification) => { try { await updateItemWithClassification(id, itemName, quantity, classification); setShowEditModal(false); setEditingItem(null); loadItems(); loadRecentlyBought(); } catch (error) { console.error("Failed to update item:", error); throw error; // Re-throw to let modal handle it } }; const handleEditCancel = () => { setShowEditModal(false); setEditingItem(null); }; // Group items by zone for classification view 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 (