grocery-app/frontend/src/components/manage/StoreAvailableItemsManager.jsx
2026-05-31 18:42:25 -07:00

282 lines
10 KiB
JavaScript

import { useCallback, useEffect, useState } from "react";
import {
createAvailableItem,
deleteAvailableItem,
getAvailableItems,
updateAvailableItem,
} from "../../api/availableItems";
import { getLocationZones } from "../../api/stores";
import useActionToast from "../../hooks/useActionToast";
import getApiErrorMessage from "../../lib/getApiErrorMessage";
import AvailableItemEditorModal from "../modals/AvailableItemEditorModal";
import ConfirmSlideModal from "../modals/ConfirmSlideModal";
function itemImageSource(item) {
if (!item?.item_image) {
return null;
}
const mimeType = item.image_mime_type || "image/jpeg";
return `data:${mimeType};base64,${item.item_image}`;
}
export default function StoreAvailableItemsManager({ householdId, store, isAdmin }) {
const toast = useActionToast();
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);
const [zones, setZones] = useState([]);
const [catalogReady, setCatalogReady] = useState(true);
const [catalogMessage, setCatalogMessage] = useState("");
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [editorItem, setEditorItem] = useState(null);
const [showEditor, setShowEditor] = useState(false);
const [pendingDeleteItem, setPendingDeleteItem] = useState(null);
const loadItems = useCallback(async (search = query) => {
if (!householdId || !store?.id) {
setItems([]);
return;
}
setLoading(true);
try {
const response = await getAvailableItems(householdId, store.id, search);
setItems(response.data.items || []);
setCatalogReady(response.data.catalog_ready !== false);
setCatalogMessage(response.data.message || "");
} catch (error) {
console.error("Failed to load store items:", error);
setCatalogReady(false);
setCatalogMessage("Store item management is unavailable right now.");
const message = getApiErrorMessage(error, "Failed to load store items");
toast.error("Load store items failed", `Load store items failed: ${message}`);
} finally {
setLoading(false);
}
}, [householdId, query, store?.id, toast]);
const loadZones = useCallback(async () => {
if (!householdId || !store?.id) {
setZones([]);
return;
}
try {
const response = await getLocationZones(householdId, store.id);
setZones(response.data?.zones || []);
} catch (error) {
console.error("Failed to load location zones:", error);
setZones([]);
}
}, [householdId, store?.id]);
useEffect(() => {
if (!isOpen) {
return;
}
loadItems(query);
loadZones();
}, [isOpen, query, loadItems, loadZones]);
const closeManager = () => {
setIsOpen(false);
setPendingDeleteItem(null);
};
const handleUpdate = async (payload) => {
if (!catalogReady) {
toast.info(
"Store item management unavailable",
catalogMessage || "Store item management is unavailable until the latest database migration is applied."
);
return;
}
try {
if (editorItem?.item_id) {
await updateAvailableItem(householdId, store.id, editorItem.item_id, payload);
toast.success("Updated store item", `Updated ${editorItem.item_name} for ${store.display_name || store.name}`);
} else {
const response = await createAvailableItem(householdId, store.id, payload);
toast.success(
"Created store item",
`Created ${response.data?.item?.item_name || payload.itemName} for ${store.display_name || store.name}`
);
}
setShowEditor(false);
setEditorItem(null);
await loadItems(query);
} catch (error) {
const message = getApiErrorMessage(error, "Failed to update store item");
toast.error("Update store item failed", `Update store item failed: ${message}`);
throw error;
}
};
const handleDeleteConfirm = async () => {
if (!pendingDeleteItem) {
return;
}
try {
await deleteAvailableItem(householdId, store.id, pendingDeleteItem.item_id);
toast.success("Deleted store item", `Deleted ${pendingDeleteItem.item_name} from ${store.display_name || store.name}`);
setPendingDeleteItem(null);
await loadItems(query);
} catch (error) {
const message = getApiErrorMessage(error, "Failed to delete store item");
toast.error("Delete store item failed", `Delete store item failed: ${message}`);
}
};
return (
<>
<button
type="button"
className="btn-secondary btn-small store-available-items-trigger"
onClick={() => setIsOpen(true)}
>
Manage Items
</button>
{isOpen ? (
<div className="store-items-modal-overlay" onClick={closeManager}>
<div className="store-items-modal" onClick={(event) => event.stopPropagation()}>
<div className="store-items-modal-header">
<div>
<h3>{store.display_name || store.name} Items</h3>
<p>Manage location-specific items used for suggestions and defaults.</p>
</div>
<button
type="button"
className="store-items-modal-close"
onClick={closeManager}
aria-label="Close manage items modal"
>
x
</button>
</div>
{!catalogReady ? (
<p className="store-available-items-notice">
{catalogMessage || "Store item management is unavailable until the latest database migration is applied."}
</p>
) : null}
<div className="store-items-modal-toolbar">
<input
className="store-available-items-search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search household/store items"
disabled={!catalogReady}
/>
<button
type="button"
className="btn-primary btn-small"
disabled={!catalogReady}
onClick={() => {
setEditorItem(null);
setShowEditor(true);
}}
>
Add Item
</button>
</div>
<div className="store-items-modal-body">
{!catalogReady ? (
<p className="empty-message">Run the latest database migrations to enable store item management.</p>
) : loading ? (
<p className="empty-message">Loading store items...</p>
) : items.length === 0 ? (
<p className="empty-message">No household items found for this store yet.</p>
) : (
<div className="store-items-table">
<div className="store-items-table-body">
{items.map((item) => {
const imageSrc = itemImageSource(item);
return (
<div key={item.item_id} className="store-items-table-row">
<div className="store-items-table-cell store-items-table-item">
<div className="store-available-items-summary">
{imageSrc ? (
<img src={imageSrc} alt="" className="store-available-items-thumb" />
) : (
<span
className="store-available-items-thumb store-available-items-thumb-placeholder"
aria-hidden="true"
>
{"\uD83D\uDCE6"}
</span>
)}
<div className="store-available-items-copy">
<strong>{item.item_name}</strong>
</div>
</div>
</div>
<div className="store-items-table-cell store-items-table-actions">
<div className="store-available-items-actions">
<button
type="button"
className="btn-secondary btn-small store-available-items-action"
onClick={() => {
setEditorItem(item);
setShowEditor(true);
}}
>
Edit Settings
</button>
{isAdmin ? (
<button
type="button"
className="btn-danger btn-small store-available-items-action"
onClick={() => setPendingDeleteItem(item)}
>
Delete Item
</button>
) : null}
</div>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
) : null}
<AvailableItemEditorModal
isOpen={showEditor}
item={editorItem}
zones={zones}
onCancel={() => {
setShowEditor(false);
setEditorItem(null);
}}
onSave={handleUpdate}
/>
<ConfirmSlideModal
isOpen={Boolean(pendingDeleteItem)}
title={pendingDeleteItem ? `Delete ${pendingDeleteItem.item_name}?` : "Delete item?"}
description={
pendingDeleteItem
? `Slide to confirm. This permanently deletes ${pendingDeleteItem.item_name} from ${store.display_name || store.name} for this household, including current list entries and history.`
: ""
}
confirmLabel="Delete Item"
onClose={() => setPendingDeleteItem(null)}
onConfirm={handleDeleteConfirm}
/>
</>
);
}