282 lines
10 KiB
JavaScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|