diff --git a/backend/controllers/available-items.controller.js b/backend/controllers/available-items.controller.js index c23734e..8242519 100644 --- a/backend/controllers/available-items.controller.js +++ b/backend/controllers/available-items.controller.js @@ -13,6 +13,10 @@ function parseBoolean(value) { return value === true || value === "true" || value === "1"; } +function isCatalogTableMissing(error) { + return error?.code === "42P01" && /household_store_available_items/i.test(error?.message || ""); +} + function parseClassificationInput(value) { if (value === undefined) { return undefined; @@ -119,8 +123,15 @@ exports.getAvailableItems = async (req, res) => { try { const { householdId, storeId } = req.params; const items = await AvailableItems.listAvailableItems(householdId, storeId, req.query.query || ""); - res.json({ items }); + res.json({ items, catalog_ready: true }); } catch (error) { + if (isCatalogTableMissing(error)) { + return res.json({ + items: [], + catalog_ready: false, + message: "Store item catalog is unavailable until the latest database migration is applied.", + }); + } logError(req, "availableItems.getAvailableItems", error); sendError(res, 500, "Failed to load available items"); } @@ -171,6 +182,13 @@ exports.createAvailableItem = async (req, res) => { item: refreshedItem, }); } catch (error) { + if (isCatalogTableMissing(error)) { + return sendError( + res, + 503, + "Store item catalog is unavailable until the latest database migration is applied" + ); + } logError(req, "availableItems.createAvailableItem", error); if (error.code === "23505") { return sendError(res, 400, "Available item already exists for this store"); @@ -234,6 +252,13 @@ exports.updateAvailableItem = async (req, res) => { item: refreshedItem, }); } catch (error) { + if (isCatalogTableMissing(error)) { + return sendError( + res, + 503, + "Store item catalog is unavailable until the latest database migration is applied" + ); + } logError(req, "availableItems.updateAvailableItem", error); if (error.code === "23505") { return sendError(res, 400, "Available item already exists for this store"); @@ -258,6 +283,13 @@ exports.deleteAvailableItem = async (req, res) => { res.json({ message: "Available item removed" }); } catch (error) { + if (isCatalogTableMissing(error)) { + return sendError( + res, + 503, + "Store item catalog is unavailable until the latest database migration is applied" + ); + } logError(req, "availableItems.deleteAvailableItem", error); sendError(res, 500, "Failed to remove available item"); } @@ -273,6 +305,13 @@ exports.importCurrentItems = async (req, res) => { imported_count: importedCount, }); } catch (error) { + if (isCatalogTableMissing(error)) { + return sendError( + res, + 503, + "Store item catalog is unavailable until the latest database migration is applied" + ); + } logError(req, "availableItems.importCurrentItems", error); sendError(res, 500, "Failed to import current list items"); } diff --git a/backend/tests/available-items.controller.test.js b/backend/tests/available-items.controller.test.js index 1fc0252..16828ce 100644 --- a/backend/tests/available-items.controller.test.js +++ b/backend/tests/available-items.controller.test.js @@ -134,4 +134,53 @@ describe("available-items.controller", () => { }) ); }); + + test("returns an empty catalog payload when the available items table is missing", async () => { + const req = { + params: { householdId: "1", storeId: "2" }, + query: {}, + }; + const res = createResponse(); + + AvailableItems.listAvailableItems.mockRejectedValueOnce({ + code: "42P01", + message: 'relation "household_store_available_items" does not exist', + }); + + await controller.getAvailableItems(req, res); + + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + catalog_ready: false, + }) + ); + }); + + test("returns a setup error when creating while the available items table is missing", async () => { + const req = { + params: { householdId: "1", storeId: "2" }, + body: { + item_name: "milk", + }, + processedImage: null, + }; + const res = createResponse(); + + AvailableItems.createAvailableItem.mockRejectedValueOnce({ + code: "42P01", + message: 'relation "household_store_available_items" does not exist', + }); + + await controller.createAvailableItem(req, res); + + expect(res.status).toHaveBeenCalledWith(503); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.objectContaining({ + message: expect.stringContaining("latest database migration"), + }), + }) + ); + }); }); diff --git a/frontend/src/components/manage/StoreAvailableItemsManager.jsx b/frontend/src/components/manage/StoreAvailableItemsManager.jsx index dec357c..aece269 100644 --- a/frontend/src/components/manage/StoreAvailableItemsManager.jsx +++ b/frontend/src/components/manage/StoreAvailableItemsManager.jsx @@ -23,6 +23,8 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin const toast = useActionToast(); const [expanded, setExpanded] = useState(true); const [items, setItems] = useState([]); + const [catalogReady, setCatalogReady] = useState(true); + const [catalogMessage, setCatalogMessage] = useState(""); const [query, setQuery] = useState(""); const [loading, setLoading] = useState(false); const [editorItem, setEditorItem] = useState(null); @@ -38,8 +40,12 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin 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 available items:", error); + setCatalogReady(false); + setCatalogMessage("Store item catalog is unavailable right now."); const message = getApiErrorMessage(error, "Failed to load available items"); toast.error("Load store items failed", `Load store items failed: ${message}`); } finally { @@ -56,6 +62,14 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin }, [expanded, query, loadItems]); const handleCreate = async (payload) => { + if (!catalogReady) { + toast.info( + "Store item catalog unavailable", + catalogMessage || "Store item catalog is unavailable until the latest database migration is applied." + ); + return; + } + try { await createAvailableItem(householdId, store.id, payload); toast.success("Added store item", `Added ${payload.itemName} to ${store.name}`); @@ -70,6 +84,14 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin }; const handleUpdate = async (payload) => { + if (!catalogReady) { + toast.info( + "Store item catalog unavailable", + catalogMessage || "Store item catalog is unavailable until the latest database migration is applied." + ); + return; + } + try { await updateAvailableItem(householdId, store.id, editorItem.item_id, payload); toast.success("Updated store item", `Updated ${payload.itemName} for ${store.name}`); @@ -84,6 +106,14 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin }; const handleDelete = async (item) => { + if (!catalogReady) { + toast.info( + "Store item catalog unavailable", + catalogMessage || "Store item catalog is unavailable until the latest database migration is applied." + ); + return; + } + if (!confirm(`Remove ${item.item_name} from ${store.name}'s available items?`)) { return; } @@ -99,6 +129,14 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin }; const handleImport = async () => { + if (!catalogReady) { + toast.info( + "Store item catalog unavailable", + catalogMessage || "Store item catalog is unavailable until the latest database migration is applied." + ); + return; + } + try { const response = await importCurrentAvailableItems(householdId, store.id); const importedCount = response.data.imported_count || 0; @@ -137,18 +175,25 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin {expanded ? (
+ {catalogMessage || "Store item catalog is unavailable until the latest database migration is applied."} +
+ ) : null}Run the latest database migrations to enable this catalog.
+ ) : loading ? (Loading store items...
) : items.length === 0 ? (No available items saved for this store yet.
diff --git a/frontend/src/styles/components/manage/StoreAvailableItemsManager.css b/frontend/src/styles/components/manage/StoreAvailableItemsManager.css index a85dd2c..a8321b2 100644 --- a/frontend/src/styles/components/manage/StoreAvailableItemsManager.css +++ b/frontend/src/styles/components/manage/StoreAvailableItemsManager.css @@ -29,6 +29,15 @@ gap: var(--spacing-md); } +.store-available-items-notice { + margin: 0; + padding: var(--spacing-sm) var(--spacing-md); + border: var(--border-width-thin) solid var(--color-border-light); + border-radius: var(--border-radius-md); + background: var(--color-bg-surface); + color: var(--color-text-secondary); +} + .store-available-items-toolbar { display: flex; gap: var(--spacing-sm);