grocery-app/frontend/src/pages/GroceryList.jsx

927 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
addItem,
getClassification,
getItemByName,
getList,
getRecentlyBought,
getSuggestions,
markBought,
updateItemWithClassification
} from "../api/list";
import { getHouseholdMembers } from "../api/households";
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 { IMAGE_UPLOAD_SUCCESS_EVENT } from "../context/UploadQueueContext";
import { SettingsContext } from "../context/SettingsContext";
import { StoreContext } from "../context/StoreContext";
import useActionToast from "../hooks/useActionToast";
import useUploadQueue from "../hooks/useUploadQueue";
import getApiErrorMessage from "../lib/getApiErrorMessage";
import "../styles/pages/GroceryList.css";
import { findSimilarItems } from "../utils/stringSimilarity";
export default function GroceryList() {
const pageTitle = "Grocery List";
const { userId } = useContext(AuthContext);
const { activeHousehold } = useContext(HouseholdContext);
const { activeStore, stores, loading: storeLoading } = useContext(StoreContext);
const { settings } = useContext(SettingsContext);
const toast = useActionToast();
const { enqueueImageUpload } = useUploadQueue();
const navigate = useNavigate();
// Get household role for permissions
const householdRole = activeHousehold?.role;
const isHouseholdAdmin = ["owner", "admin"].includes(householdRole);
// === State === //
const [items, setItems] = useState([]);
const [recentlyBoughtItems, setRecentlyBoughtItems] = useState([]);
const [householdMembers, setHouseholdMembers] = useState([]);
const [recentlyBoughtDisplayCount, setRecentlyBoughtDisplayCount] = useState(settings.recentlyBoughtCount);
const [sortMode, setSortMode] = useState(settings.defaultSortMode);
const [suggestions, setSuggestions] = useState([]);
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]);
useEffect(() => {
const loadHouseholdMembers = async () => {
if (!activeHousehold?.id) {
setHouseholdMembers([]);
return;
}
try {
const response = await getHouseholdMembers(activeHousehold.id);
setHouseholdMembers(response.data || []);
} catch (error) {
console.error("Failed to load household members:", error);
setHouseholdMembers([]);
}
};
loadHouseholdMembers();
}, [activeHousehold?.id]);
useEffect(() => {
const handleUploadSuccess = async (event) => {
const detail = event?.detail || {};
if (!activeHousehold?.id || !activeStore?.id) return;
if (String(detail.householdId) !== String(activeHousehold.id)) return;
if (String(detail.storeId) !== String(activeStore.id)) return;
if (!detail.itemName) return;
try {
const response = await getItemByName(activeHousehold.id, activeStore.id, detail.itemName);
const refreshedItem = response.data;
setItems((prev) =>
prev.map((item) => {
const byId =
detail.localItemId !== null &&
detail.localItemId !== undefined &&
item.id === detail.localItemId;
const byName =
String(item.item_name || "").toLowerCase() ===
String(detail.itemName || "").toLowerCase();
return byId || byName ? { ...item, ...refreshedItem } : item;
})
);
setRecentlyBoughtItems((prev) =>
prev.map((item) => {
const byId =
detail.localItemId !== null &&
detail.localItemId !== undefined &&
item.id === detail.localItemId;
const byName =
String(item.item_name || "").toLowerCase() ===
String(detail.itemName || "").toLowerCase();
return byId || byName ? { ...item, ...refreshedItem } : item;
})
);
} catch (error) {
console.error("Failed to refresh item after upload success:", error);
}
};
window.addEventListener(IMAGE_UPLOAD_SUCCESS_EVENT, handleUploadSuccess);
return () => {
window.removeEventListener(IMAGE_UPLOAD_SUCCESS_EVENT, handleUploadSuccess);
};
}, [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, addedForUserId = null) => {
try {
const normalizedItemName = itemName.trim().toLowerCase();
if (!normalizedItemName) return;
if (!activeHousehold?.id || !activeStore?.id) return;
const allItems = [...items, ...recentlyBoughtItems];
const existingLocalItem = allItems.find(
(item) => String(item.item_name || "").toLowerCase() === normalizedItemName
);
if (existingLocalItem) {
await processItemAddition(itemName, quantity, {
existingItem: existingLocalItem,
addedForUserId
});
return;
}
const similar = findSimilarItems(itemName, allItems, 70);
if (similar.length > 0) {
setSimilarItemSuggestion({
originalName: itemName,
suggestedItem: similar[0],
quantity,
addedForUserId
});
setShowSimilarModal(true);
return;
}
const shouldSkipLookup = buttonText === "Create + Add";
await processItemAddition(itemName, quantity, {
skipLookup: shouldSkipLookup,
addedForUserId
});
} catch (error) {
console.error("Failed to process add item flow:", error);
}
}, [activeHousehold?.id, activeStore?.id, items, recentlyBoughtItems, buttonText]);
const processItemAddition = useCallback(async (itemName, quantity, options = {}) => {
if (!activeHousehold?.id || !activeStore?.id) return;
const {
existingItem: providedItem = null,
skipLookup = false,
addedForUserId = null
} = options;
let existingItem = providedItem;
if (!existingItem && !skipLookup) {
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,
addedForUserId
});
setShowConfirmAddExisting(true);
} else if (existingItem) {
try {
await addItem(
activeHousehold.id,
activeStore.id,
itemName,
quantity,
null,
null,
addedForUserId
);
setSuggestions([]);
setButtonText("Add Item");
toast.success("Added item", `Added item ${itemName}`);
// Reload lists to reflect the changes
await loadItems();
await loadRecentlyBought();
} catch (error) {
const message = getApiErrorMessage(error, "Failed to add item");
toast.error("Add item failed", `Add item failed: ${message}`);
throw error;
}
} else {
setPendingItem({ itemName, quantity, addedForUserId });
setShowAddDetailsModal(true);
}
}, [activeHousehold?.id, activeStore?.id, loadItems, loadRecentlyBought, toast]);
// === 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, {
skipLookup: true,
addedForUserId: similarItemSuggestion.addedForUserId || null
});
setSimilarItemSuggestion(null);
}, [similarItemSuggestion, processItemAddition]);
const handleSimilarYes = useCallback(async () => {
if (!similarItemSuggestion) return;
setShowSimilarModal(false);
await processItemAddition(similarItemSuggestion.suggestedItem.item_name, similarItemSuggestion.quantity, {
addedForUserId: similarItemSuggestion.addedForUserId || null
});
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, addedForUserId } = confirmAddExistingData;
setShowConfirmAddExisting(false);
setConfirmAddExistingData(null);
try {
await addItem(
activeHousehold.id,
activeStore.id,
itemName,
newQuantity,
null,
null,
addedForUserId || 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");
toast.success("Updated item quantity", `Updated item ${itemName}`);
} catch (error) {
console.error("Failed to update item:", error);
const message = getApiErrorMessage(error, "Failed to update item");
toast.error("Update item failed", `Update item failed: ${message}`);
await loadItems();
}
}, [activeHousehold?.id, activeStore?.id, confirmAddExistingData, loadItems, toast]);
// === Add Details Modal Handlers ===
const handleAddWithDetails = useCallback(async (imageFile, classification) => {
if (!pendingItem) return;
if (!activeHousehold?.id || !activeStore?.id) return;
try {
// Create the list item first, upload image separately in background.
await addItem(
activeHousehold.id,
activeStore.id,
pendingItem.itemName,
pendingItem.quantity,
null,
null,
pendingItem.addedForUserId || null
);
if (classification) {
// Apply classification if provided
await updateItemWithClassification(activeHousehold.id, activeStore.id, pendingItem.itemName, pendingItem.quantity, classification);
toast.success("Updated item classification", `Updated classification for ${pendingItem.itemName}`);
}
// 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]);
toast.success("Added item", `Added item ${newItem.item_name || pendingItem.itemName}`);
if (imageFile) {
enqueueImageUpload({
householdId: activeHousehold.id,
storeId: activeStore.id,
itemName: newItem.item_name || pendingItem.itemName,
quantity: newItem.quantity || pendingItem.quantity,
fileBlob: imageFile,
fileName: imageFile.name || "upload.jpg",
fileType: imageFile.type || "image/jpeg",
fileSize: imageFile.size || 0,
source: "add_details",
localItemId: newItem.id,
});
toast.info("Queued image upload", `Queued image upload for ${newItem.item_name || pendingItem.itemName}`);
}
}
} catch (error) {
console.error("Failed to add item:", error);
const message = getApiErrorMessage(error, "Failed to add item");
toast.error("Add item failed", `Add item failed: ${message}`);
}
}, [activeHousehold?.id, activeStore?.id, pendingItem, enqueueImageUpload, toast]);
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,
null,
pendingItem.addedForUserId || 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]);
toast.success("Added item", `Added item ${newItem.item_name || pendingItem.itemName}`);
}
} catch (error) {
console.error("Failed to add item:", error);
const message = getApiErrorMessage(error, "Failed to add item");
toast.error("Add item failed", `Add item failed: ${message}`);
}
}, [activeHousehold?.id, activeStore?.id, pendingItem, toast]);
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;
try {
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((existingItem) => existingItem.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((existingItem) => (existingItem.id === id ? updatedItem : existingItem))
);
}
toast.success("Marked item bought", `Marked item ${item.item_name} as bought`);
loadRecentlyBought();
} catch (error) {
const message = getApiErrorMessage(error, "Failed to mark item as bought");
toast.error("Mark item bought failed", `Mark item bought failed: ${message}`);
}
}, [activeHousehold?.id, activeStore?.id, items, toast]);
const handleImageAdded = useCallback(async (id, itemName, quantity, imageFile, source = "add_image_modal") => {
if (!activeHousehold?.id || !activeStore?.id) return;
if (!imageFile) return;
try {
enqueueImageUpload({
householdId: activeHousehold.id,
storeId: activeStore.id,
itemName,
quantity,
fileBlob: imageFile,
fileName: imageFile.name || "upload.jpg",
fileType: imageFile.type || "image/jpeg",
fileSize: imageFile.size || 0,
source,
localItemId: id,
});
toast.info("Queued image upload", `Queued image upload for ${itemName}`);
} catch (error) {
console.error("Failed to add image:", error);
const message = getApiErrorMessage(error, "Failed to add image");
toast.error("Add image failed", `Add image failed: ${message}`);
}
}, [activeHousehold?.id, activeStore?.id, enqueueImageUpload, toast]);
const handleLongPress = useCallback(async (item) => {
if (!householdRole || householdRole === 'viewer') return;
if (!activeHousehold?.id || !activeStore?.id) return;
try {
const classificationResponse = await getClassification(activeHousehold.id, activeStore.id, item.item_name);
setEditingItem({
...item,
classification: classificationResponse.data?.classification || null
});
setShowEditModal(true);
} catch (error) {
console.error("Failed to load classification:", error);
setEditingItem({ ...item, classification: null });
setShowEditModal(true);
}
}, [activeHousehold?.id, activeStore?.id, householdRole]);
// === 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
)
);
toast.success("Updated item", `Updated item ${itemName}`);
} catch (error) {
console.error("Failed to update item:", error);
const message = getApiErrorMessage(error, "Failed to update item");
toast.error("Update item failed", `Update item failed: ${message}`);
throw error;
}
}, [activeHousehold?.id, activeStore?.id, toast]);
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) {
return (
<div className="glist-body">
<div className="glist-container">
<h1 className="glist-title">{pageTitle}</h1>
<p style={{ textAlign: 'center', marginTop: '2rem', color: 'var(--text-secondary)' }}>
Loading households...
</p>
</div>
</div>
);
}
if (storeLoading) {
return (
<div className="glist-body">
<div className="glist-container">
<h1 className="glist-title">{pageTitle}</h1>
<p style={{ textAlign: 'center', marginTop: '2rem', color: 'var(--text-secondary)' }}>
Loading stores...
</p>
</div>
</div>
);
}
if (!storeLoading && stores.length === 0) {
return (
<div className="glist-body">
<div className="glist-container">
<h1 className="glist-title">{pageTitle}</h1>
<div className="glist-empty-state">
<h2 className="glist-empty-title">No stores found</h2>
<p className="glist-empty-text">
This household doesnt have any stores yet.
</p>
{isHouseholdAdmin ? (
<button
className="btn-primary"
onClick={() => navigate("/manage?tab=stores")}
>
Go to Manage Stores
</button>
) : (
<p className="glist-empty-text">
Please notify a household admin to add a store.
</p>
)}
</div>
</div>
</div>
);
}
if (!activeStore) {
return (
<div className="glist-body">
<div className="glist-container">
<h1 className="glist-title">{pageTitle}</h1>
<StoreTabs />
<p style={{ textAlign: 'center', marginTop: '2rem', color: 'var(--text-secondary)' }}>
Loading stores...
</p>
</div>
</div>
);
}
if (loading) {
return (
<div className="glist-body">
<div className="glist-container">
<h1 className="glist-title">{pageTitle}</h1>
<StoreTabs />
<p style={{ textAlign: 'center', marginTop: '2rem' }}>Loading grocery list...</p>
</div>
</div>
);
}
return (
<div className="glist-body">
<div className="glist-container">
<h1 className="glist-title">{pageTitle}</h1>
<StoreTabs />
{householdRole && householdRole !== 'viewer' && (
<AddItemForm
onAdd={handleAdd}
onSuggest={handleSuggest}
suggestions={suggestions}
buttonText={buttonText}
householdMembers={householdMembers}
currentUserId={userId}
/>
)}
<SortDropdown value={sortMode} onChange={setSortMode} />
{sortMode === "zone" ? (
(() => {
const grouped = groupItemsByZone(sortedItems);
return Object.keys(grouped).map(zone => {
const isCollapsed = collapsedZones[zone];
const itemCount = grouped[zone].length;
return (
<div key={zone} className="glist-classification-group">
<h3
className="glist-classification-header clickable"
onClick={() => toggleZoneCollapse(zone)}
>
<span>
{zone === 'unclassified' ? 'Unclassified' : zone}
<span className="glist-zone-count"> ({itemCount})</span>
</span>
<span className="glist-zone-indicator">
{isCollapsed ? "▼" : "▲"}
</span>
</h3>
{!isCollapsed && (
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
{grouped[zone].map((item) => (
<GroceryListItem
key={item.id}
item={item}
allItems={sortedItems}
compact={settings.compactView}
onClick={(id, quantity) =>
householdRole && householdRole !== 'viewer' && handleBought(id, quantity)
}
onImageAdded={
householdRole && householdRole !== 'viewer' ? handleImageAdded : null
}
onLongPress={
householdRole && householdRole !== 'viewer' ? handleLongPress : null
}
/>
))}
</ul>
)}
</div>
);
});
})()
) : (
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
{sortedItems.map((item) => (
<GroceryListItem
key={item.id}
item={item}
allItems={sortedItems}
compact={settings.compactView}
onClick={(id, quantity) =>
[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
}
/>
))}
</ul>
)}
{recentlyBoughtItems.length > 0 && settings.showRecentlyBought && (
<>
<h2
className="glist-section-title clickable"
onClick={() => setRecentlyBoughtCollapsed(!recentlyBoughtCollapsed)}
>
<span>Recently Bought (24HR)</span>
<span className="glist-section-indicator">
{recentlyBoughtCollapsed ? "▼" : "▲"}
</span>
</h2>
{!recentlyBoughtCollapsed && (
<>
<ul className={`glist-ul ${settings.compactView ? 'compact' : ''}`}>
{recentlyBoughtItems.slice(0, recentlyBoughtDisplayCount).map((item) => (
<GroceryListItem
key={item.id}
item={item}
allItems={recentlyBoughtItems}
compact={settings.compactView}
onClick={null}
onImageAdded={
householdRole && householdRole !== 'viewer' ? handleImageAdded : null
}
onLongPress={
householdRole && householdRole !== 'viewer' ? handleLongPress : null
}
/>
))}
</ul>
{recentlyBoughtDisplayCount < recentlyBoughtItems.length && (
<div style={{ textAlign: 'center', marginTop: '1rem' }}>
<button
className="glist-show-more-btn"
onClick={() => setRecentlyBoughtDisplayCount(prev => prev + 10)}
>
Show More ({recentlyBoughtItems.length - recentlyBoughtDisplayCount} remaining)
</button>
</div>
)}
</>
)}
</>
)}
</div>
{showAddDetailsModal && pendingItem && (
<AddItemWithDetailsModal
itemName={pendingItem.itemName}
onConfirm={handleAddWithDetails}
onSkip={handleAddDetailsSkip}
onCancel={handleAddDetailsCancel}
/>
)}
{showSimilarModal && similarItemSuggestion && (
<SimilarItemModal
originalName={similarItemSuggestion.originalName}
suggestedName={similarItemSuggestion.suggestedItem.item_name}
onCancel={handleSimilarCancel}
onNo={handleSimilarNo}
onYes={handleSimilarYes}
/>
)}
{showEditModal && editingItem && (
<EditItemModal
item={editingItem}
onSave={handleEditSave}
onCancel={handleEditCancel}
onImageUpdate={handleImageAdded}
/>
)}
{showConfirmAddExisting && confirmAddExistingData && (
<ConfirmAddExistingModal
itemName={confirmAddExistingData.itemName}
currentQuantity={confirmAddExistingData.currentQuantity}
addingQuantity={confirmAddExistingData.addingQuantity}
onConfirm={handleConfirmAddExisting}
onCancel={() => {
setShowConfirmAddExisting(false);
setConfirmAddExistingData(null);
}}
/>
)}
</div>
);
}