fix(ui): manage household store items in store settings
This commit is contained in:
parent
15c3ea279c
commit
36277a9e67
@ -276,12 +276,16 @@ exports.deleteAvailableItem = async (req, res) => {
|
||||
return sendError(res, 400, "Item ID must be a positive integer");
|
||||
}
|
||||
|
||||
const deleted = await AvailableItems.deleteAvailableItem(householdId, storeId, itemId);
|
||||
if (!deleted) {
|
||||
return sendError(res, 404, "Available item not found");
|
||||
const [deletedCatalogEntry, deletedClassification] = await Promise.all([
|
||||
AvailableItems.deleteAvailableItem(householdId, storeId, itemId),
|
||||
List.deleteClassification(householdId, storeId, itemId),
|
||||
]);
|
||||
|
||||
if (!deletedCatalogEntry && !deletedClassification) {
|
||||
return sendError(res, 404, "Managed item settings not found");
|
||||
}
|
||||
|
||||
res.json({ message: "Available item removed" });
|
||||
res.json({ message: "Store item settings cleared" });
|
||||
} catch (error) {
|
||||
if (isCatalogTableMissing(error)) {
|
||||
return sendError(
|
||||
|
||||
@ -31,25 +31,52 @@ async function findOrCreateItem(itemName) {
|
||||
|
||||
async function getAvailableItemRecord(householdId, storeId, itemId) {
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
hsai.item_id,
|
||||
i.name AS item_name,
|
||||
ENCODE(hsai.custom_image, 'base64') AS item_image,
|
||||
hsai.custom_image_mime_type AS image_mime_type,
|
||||
hic.item_type,
|
||||
hic.item_group,
|
||||
hic.zone,
|
||||
hsai.created_at,
|
||||
hsai.updated_at
|
||||
FROM household_store_available_items hsai
|
||||
JOIN items i ON i.id = hsai.item_id
|
||||
LEFT JOIN household_item_classifications hic
|
||||
ON hic.household_id = hsai.household_id
|
||||
AND hic.store_id = hsai.store_id
|
||||
AND hic.item_id = hsai.item_id
|
||||
WHERE hsai.household_id = $1
|
||||
AND hsai.store_id = $2
|
||||
AND hsai.item_id = $3`,
|
||||
`WITH manageable_items AS (
|
||||
SELECT DISTINCT hl.item_id
|
||||
FROM household_lists hl
|
||||
WHERE hl.household_id = $1
|
||||
AND hl.store_id = $2
|
||||
UNION
|
||||
SELECT hsai.item_id
|
||||
FROM household_store_available_items hsai
|
||||
WHERE hsai.household_id = $1
|
||||
AND hsai.store_id = $2
|
||||
),
|
||||
latest_list_items AS (
|
||||
SELECT DISTINCT ON (hl.item_id)
|
||||
hl.item_id,
|
||||
hl.custom_image,
|
||||
hl.custom_image_mime_type,
|
||||
hl.modified_on,
|
||||
hl.id
|
||||
FROM household_lists hl
|
||||
WHERE hl.household_id = $1
|
||||
AND hl.store_id = $2
|
||||
ORDER BY hl.item_id, hl.modified_on DESC NULLS LAST, hl.id DESC
|
||||
)
|
||||
SELECT
|
||||
mi.item_id,
|
||||
i.name AS item_name,
|
||||
ENCODE(COALESCE(hsai.custom_image, lli.custom_image), 'base64') AS item_image,
|
||||
COALESCE(hsai.custom_image_mime_type, lli.custom_image_mime_type) AS image_mime_type,
|
||||
hic.item_type,
|
||||
hic.item_group,
|
||||
hic.zone,
|
||||
hsai.created_at,
|
||||
hsai.updated_at,
|
||||
(hsai.item_id IS NOT NULL OR hic.item_id IS NOT NULL) AS has_managed_settings
|
||||
FROM manageable_items mi
|
||||
JOIN items i ON i.id = mi.item_id
|
||||
LEFT JOIN latest_list_items lli ON lli.item_id = mi.item_id
|
||||
LEFT JOIN household_store_available_items hsai
|
||||
ON hsai.household_id = $1
|
||||
AND hsai.store_id = $2
|
||||
AND hsai.item_id = mi.item_id
|
||||
LEFT JOIN household_item_classifications hic
|
||||
ON hic.household_id = $1
|
||||
AND hic.store_id = $2
|
||||
AND hic.item_id = mi.item_id
|
||||
WHERE mi.item_id = $3`,
|
||||
[householdId, storeId, itemId]
|
||||
);
|
||||
|
||||
@ -63,31 +90,58 @@ exports.listAvailableItems = async (householdId, storeId, query = "") => {
|
||||
|
||||
if (trimmedQuery) {
|
||||
values.push(`%${trimmedQuery}%`);
|
||||
filterClause = "AND i.name ILIKE $3";
|
||||
filterClause = "WHERE i.name ILIKE $3";
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
hsai.item_id,
|
||||
i.name AS item_name,
|
||||
ENCODE(hsai.custom_image, 'base64') AS item_image,
|
||||
hsai.custom_image_mime_type AS image_mime_type,
|
||||
hic.item_type,
|
||||
hic.item_group,
|
||||
hic.zone,
|
||||
hsai.created_at,
|
||||
hsai.updated_at
|
||||
FROM household_store_available_items hsai
|
||||
JOIN items i ON i.id = hsai.item_id
|
||||
LEFT JOIN household_item_classifications hic
|
||||
ON hic.household_id = hsai.household_id
|
||||
AND hic.store_id = hsai.store_id
|
||||
AND hic.item_id = hsai.item_id
|
||||
WHERE hsai.household_id = $1
|
||||
AND hsai.store_id = $2
|
||||
${filterClause}
|
||||
ORDER BY i.name ASC
|
||||
LIMIT 100`,
|
||||
`WITH manageable_items AS (
|
||||
SELECT DISTINCT hl.item_id
|
||||
FROM household_lists hl
|
||||
WHERE hl.household_id = $1
|
||||
AND hl.store_id = $2
|
||||
UNION
|
||||
SELECT hsai.item_id
|
||||
FROM household_store_available_items hsai
|
||||
WHERE hsai.household_id = $1
|
||||
AND hsai.store_id = $2
|
||||
),
|
||||
latest_list_items AS (
|
||||
SELECT DISTINCT ON (hl.item_id)
|
||||
hl.item_id,
|
||||
hl.custom_image,
|
||||
hl.custom_image_mime_type,
|
||||
hl.modified_on,
|
||||
hl.id
|
||||
FROM household_lists hl
|
||||
WHERE hl.household_id = $1
|
||||
AND hl.store_id = $2
|
||||
ORDER BY hl.item_id, hl.modified_on DESC NULLS LAST, hl.id DESC
|
||||
)
|
||||
SELECT
|
||||
mi.item_id,
|
||||
i.name AS item_name,
|
||||
ENCODE(COALESCE(hsai.custom_image, lli.custom_image), 'base64') AS item_image,
|
||||
COALESCE(hsai.custom_image_mime_type, lli.custom_image_mime_type) AS image_mime_type,
|
||||
hic.item_type,
|
||||
hic.item_group,
|
||||
hic.zone,
|
||||
hsai.created_at,
|
||||
hsai.updated_at,
|
||||
(hsai.item_id IS NOT NULL OR hic.item_id IS NOT NULL) AS has_managed_settings
|
||||
FROM manageable_items mi
|
||||
JOIN items i ON i.id = mi.item_id
|
||||
LEFT JOIN latest_list_items lli ON lli.item_id = mi.item_id
|
||||
LEFT JOIN household_store_available_items hsai
|
||||
ON hsai.household_id = $1
|
||||
AND hsai.store_id = $2
|
||||
AND hsai.item_id = mi.item_id
|
||||
LEFT JOIN household_item_classifications hic
|
||||
ON hic.household_id = $1
|
||||
AND hic.store_id = $2
|
||||
AND hic.item_id = mi.item_id
|
||||
${filterClause}
|
||||
ORDER BY i.name ASC
|
||||
LIMIT 100`,
|
||||
values
|
||||
);
|
||||
|
||||
@ -143,17 +197,55 @@ exports.updateAvailableItem = async (householdId, storeId, itemId, updates = {})
|
||||
removeImage = false,
|
||||
} = updates;
|
||||
|
||||
const existing = await pool.query(
|
||||
`SELECT item_id
|
||||
FROM household_store_available_items
|
||||
WHERE household_id = $1
|
||||
AND store_id = $2
|
||||
AND item_id = $3`,
|
||||
[householdId, storeId, itemId]
|
||||
);
|
||||
|
||||
const assignments = ["updated_at = NOW()"];
|
||||
const values = [householdId, storeId, itemId];
|
||||
let parameterIndex = values.length;
|
||||
let targetItemId = itemId;
|
||||
|
||||
if (itemName !== undefined && String(itemName).trim() !== "") {
|
||||
const { itemId: nextItemId } = await findOrCreateItem(itemName);
|
||||
targetItemId = nextItemId;
|
||||
parameterIndex += 1;
|
||||
assignments.push(`item_id = $${parameterIndex}`);
|
||||
values.push(nextItemId);
|
||||
}
|
||||
|
||||
if (existing.rowCount === 0) {
|
||||
if (imageBuffer && mimeType) {
|
||||
await pool.query(
|
||||
`INSERT INTO household_store_available_items
|
||||
(household_id, store_id, item_id, custom_image, custom_image_mime_type, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (household_id, store_id, item_id)
|
||||
DO UPDATE SET
|
||||
custom_image = EXCLUDED.custom_image,
|
||||
custom_image_mime_type = EXCLUDED.custom_image_mime_type,
|
||||
updated_at = NOW()`,
|
||||
[householdId, storeId, targetItemId, imageBuffer, mimeType]
|
||||
);
|
||||
} else if (targetItemId !== itemId) {
|
||||
await pool.query(
|
||||
`INSERT INTO household_store_available_items
|
||||
(household_id, store_id, item_id, updated_at)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
ON CONFLICT (household_id, store_id, item_id)
|
||||
DO UPDATE SET updated_at = NOW()`,
|
||||
[householdId, storeId, targetItemId]
|
||||
);
|
||||
}
|
||||
|
||||
return getAvailableItemRecord(householdId, storeId, targetItemId);
|
||||
}
|
||||
|
||||
if (removeImage) {
|
||||
assignments.push("custom_image = NULL", "custom_image_mime_type = NULL");
|
||||
} else if (imageBuffer && mimeType) {
|
||||
|
||||
@ -377,13 +377,14 @@ exports.upsertClassification = async (householdId, storeId, itemId, classificati
|
||||
* @param {number} itemId - Item ID
|
||||
*/
|
||||
exports.deleteClassification = async (householdId, storeId, itemId) => {
|
||||
await pool.query(
|
||||
const result = await pool.query(
|
||||
`DELETE FROM household_item_classifications
|
||||
WHERE household_id = $1
|
||||
AND store_id = $2
|
||||
AND item_id = $3`,
|
||||
[householdId, storeId, itemId]
|
||||
);
|
||||
return result.rowCount > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -10,6 +10,38 @@ describe("available-item.model", () => {
|
||||
pool.query.mockReset();
|
||||
});
|
||||
|
||||
test("lists manageable items from household/store history even without stored overrides", async () => {
|
||||
pool.query.mockResolvedValueOnce({
|
||||
rowCount: 1,
|
||||
rows: [
|
||||
{
|
||||
item_id: 55,
|
||||
item_name: "milk",
|
||||
item_image: null,
|
||||
image_mime_type: null,
|
||||
item_type: null,
|
||||
item_group: null,
|
||||
zone: null,
|
||||
has_managed_settings: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await AvailableItems.listAvailableItems(1, 2);
|
||||
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({
|
||||
item_id: 55,
|
||||
item_name: "milk",
|
||||
has_managed_settings: false,
|
||||
}),
|
||||
]);
|
||||
expect(pool.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining("WITH manageable_items AS"),
|
||||
[1, 2]
|
||||
);
|
||||
});
|
||||
|
||||
test("creates an available item using an existing catalog item", async () => {
|
||||
pool.query
|
||||
.mockResolvedValueOnce({ rowCount: 1, rows: [{ id: 55, name: "milk" }] })
|
||||
@ -72,10 +104,17 @@ describe("available-item.model", () => {
|
||||
test("updates available item images and returns refreshed data", async () => {
|
||||
const imageBuffer = Buffer.from("abc");
|
||||
pool.query
|
||||
.mockResolvedValueOnce({ rowCount: 1, rows: [{ item_id: 55 }] })
|
||||
.mockResolvedValueOnce({ rowCount: 1, rows: [{ item_id: 55 }] })
|
||||
.mockResolvedValueOnce({
|
||||
rowCount: 1,
|
||||
rows: [{ item_id: 55, item_name: "milk", item_image: "YWJj", image_mime_type: "image/jpeg" }],
|
||||
rows: [{
|
||||
item_id: 55,
|
||||
item_name: "milk",
|
||||
item_image: "YWJj",
|
||||
image_mime_type: "image/jpeg",
|
||||
has_managed_settings: true,
|
||||
}],
|
||||
});
|
||||
|
||||
const result = await AvailableItems.updateAvailableItem(1, 2, 55, {
|
||||
@ -85,7 +124,7 @@ describe("available-item.model", () => {
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ item_id: 55, image_mime_type: "image/jpeg" }));
|
||||
expect(pool.query).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
2,
|
||||
expect.stringContaining("UPDATE household_store_available_items"),
|
||||
[1, 2, 55, imageBuffer, "image/jpeg"]
|
||||
);
|
||||
|
||||
@ -43,7 +43,7 @@ describe("available-items.controller", () => {
|
||||
AvailableItems.importCurrentListItems.mockResolvedValue(2);
|
||||
AvailableItems.listAvailableItems.mockResolvedValue([]);
|
||||
List.upsertClassification.mockResolvedValue(undefined);
|
||||
List.deleteClassification.mockResolvedValue(undefined);
|
||||
List.deleteClassification.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
test("creates an available item and persists classification metadata", async () => {
|
||||
@ -135,6 +135,22 @@ describe("available-items.controller", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("clears managed settings without removing the underlying item", async () => {
|
||||
const req = {
|
||||
params: { householdId: "1", storeId: "2", itemId: "99" },
|
||||
};
|
||||
const res = createResponse();
|
||||
|
||||
AvailableItems.deleteAvailableItem.mockResolvedValueOnce(false);
|
||||
List.deleteClassification.mockResolvedValueOnce(true);
|
||||
|
||||
await controller.deleteAvailableItem(req, res);
|
||||
|
||||
expect(AvailableItems.deleteAvailableItem).toHaveBeenCalledWith("1", "2", 99);
|
||||
expect(List.deleteClassification).toHaveBeenCalledWith("1", "2", 99);
|
||||
expect(res.json).toHaveBeenCalledWith({ message: "Store item settings cleared" });
|
||||
});
|
||||
|
||||
test("returns an empty catalog payload when the available items table is missing", async () => {
|
||||
const req = {
|
||||
params: { householdId: "1", storeId: "2" },
|
||||
|
||||
@ -92,7 +92,7 @@ export default function ManageStores() {
|
||||
<section className="manage-section">
|
||||
<h2>Your Stores ({householdStores.length})</h2>
|
||||
<p className="manage-stores-help">
|
||||
Available item management lives inside each store card below.
|
||||
Item management lives inside each store card below for items already used in that household/store.
|
||||
</p>
|
||||
{!isAdmin && (
|
||||
<p className="manage-stores-note">
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
createAvailableItem,
|
||||
deleteAvailableItem,
|
||||
getAvailableItems,
|
||||
importCurrentAvailableItems,
|
||||
updateAvailableItem,
|
||||
} from "../../api/availableItems";
|
||||
import useActionToast from "../../hooks/useActionToast";
|
||||
@ -61,28 +59,6 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
||||
loadItems(query);
|
||||
}, [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}`);
|
||||
setShowEditor(false);
|
||||
setEditorItem(null);
|
||||
await loadItems(query);
|
||||
} catch (error) {
|
||||
const message = getApiErrorMessage(error, "Failed to add available item");
|
||||
toast.error("Add store item failed", `Add store item failed: ${message}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (payload) => {
|
||||
if (!catalogReady) {
|
||||
toast.info(
|
||||
@ -94,7 +70,7 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
||||
|
||||
try {
|
||||
await updateAvailableItem(householdId, store.id, editorItem.item_id, payload);
|
||||
toast.success("Updated store item", `Updated ${payload.itemName} for ${store.name}`);
|
||||
toast.success("Updated store item", `Updated ${editorItem.item_name} for ${store.name}`);
|
||||
setShowEditor(false);
|
||||
setEditorItem(null);
|
||||
await loadItems(query);
|
||||
@ -120,36 +96,11 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
||||
|
||||
try {
|
||||
await deleteAvailableItem(householdId, store.id, item.item_id);
|
||||
toast.success("Removed store item", `Removed ${item.item_name} from ${store.name}`);
|
||||
toast.success("Cleared store item settings", `Cleared settings for ${item.item_name} in ${store.name}`);
|
||||
await loadItems(query);
|
||||
} catch (error) {
|
||||
const message = getApiErrorMessage(error, "Failed to remove available item");
|
||||
toast.error("Remove store item failed", `Remove store item failed: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
toast.success(
|
||||
"Imported current list items",
|
||||
importedCount > 0
|
||||
? `Imported ${importedCount} current list items into ${store.name}`
|
||||
: `No current list items to import for ${store.name}`
|
||||
);
|
||||
await loadItems(query);
|
||||
} catch (error) {
|
||||
const message = getApiErrorMessage(error, "Failed to import current list items");
|
||||
toast.error("Import store items failed", `Import store items failed: ${message}`);
|
||||
const message = getApiErrorMessage(error, "Failed to clear store item settings");
|
||||
toast.error("Clear store item settings failed", `Clear store item settings failed: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -162,7 +113,7 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
||||
<div className="store-available-items-header">
|
||||
<div>
|
||||
<h4>Store Item Catalog</h4>
|
||||
<p>Manage the available item list for {store.name}.</p>
|
||||
<p>Manage settings for items already used in {store.name} for this household.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@ -185,30 +136,9 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
||||
className="store-available-items-search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search store items"
|
||||
placeholder="Search household/store items"
|
||||
disabled={!catalogReady}
|
||||
/>
|
||||
<div className="store-available-items-toolbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary btn-small"
|
||||
onClick={handleImport}
|
||||
disabled={!catalogReady}
|
||||
>
|
||||
Import Current List
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary btn-small"
|
||||
onClick={() => {
|
||||
setEditorItem(null);
|
||||
setShowEditor(true);
|
||||
}}
|
||||
disabled={!catalogReady}
|
||||
>
|
||||
Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!catalogReady ? (
|
||||
@ -216,7 +146,7 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
||||
) : loading ? (
|
||||
<p className="empty-message">Loading store items...</p>
|
||||
) : items.length === 0 ? (
|
||||
<p className="empty-message">No available items saved for this store yet.</p>
|
||||
<p className="empty-message">No household items found for this store yet.</p>
|
||||
) : (
|
||||
<div className="store-available-items-list">
|
||||
{items.map((item) => {
|
||||
@ -248,13 +178,15 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-danger btn-small"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
{item.has_managed_settings ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-danger btn-small"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
Clear Settings
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -271,7 +203,7 @@ export default function StoreAvailableItemsManager({ householdId, store, isAdmin
|
||||
setShowEditor(false);
|
||||
setEditorItem(null);
|
||||
}}
|
||||
onSave={editorItem ? handleUpdate : handleCreate}
|
||||
onSave={handleUpdate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -115,6 +115,7 @@ export default function AvailableItemEditorModal({ isOpen, item = null, onCancel
|
||||
value={itemName}
|
||||
onChange={(event) => setItemName(event.target.value)}
|
||||
placeholder="Enter item name"
|
||||
disabled={Boolean(item)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -156,7 +157,7 @@ export default function AvailableItemEditorModal({ isOpen, item = null, onCancel
|
||||
onClick={handleSubmit}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? "Saving..." : item ? "Save Changes" : "Add Item"}
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -45,7 +45,7 @@ async function mockHouseholdAndStoreShell(page: import("@playwright/test").Page)
|
||||
});
|
||||
}
|
||||
|
||||
test("manage stores lets admins import and curate available items", async ({ page }) => {
|
||||
test("manage stores lets admins edit settings for existing household/store items", async ({ page }) => {
|
||||
await seedAuthStorage(page);
|
||||
await mockConfig(page);
|
||||
await mockHouseholdAndStoreShell(page);
|
||||
@ -59,6 +59,17 @@ test("manage stores lets admins import and curate available items", async ({ pag
|
||||
item_type: "dairy",
|
||||
item_group: "Milk",
|
||||
zone: "Dairy & Refrigerated",
|
||||
has_managed_settings: true,
|
||||
},
|
||||
{
|
||||
item_id: 777,
|
||||
item_name: "apples",
|
||||
item_image: null,
|
||||
image_mime_type: null,
|
||||
item_type: null,
|
||||
item_group: null,
|
||||
zone: null,
|
||||
has_managed_settings: false,
|
||||
},
|
||||
];
|
||||
|
||||
@ -70,30 +81,6 @@ test("manage stores lets admins import and curate available items", async ({ pag
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/households/1/stores/10/available-items/import-current", async (route) => {
|
||||
availableItems = [
|
||||
...availableItems,
|
||||
{
|
||||
item_id: 777,
|
||||
item_name: "granola",
|
||||
item_image: null,
|
||||
image_mime_type: null,
|
||||
item_type: null,
|
||||
item_group: null,
|
||||
zone: null,
|
||||
},
|
||||
];
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
message: "Imported current list items",
|
||||
imported_count: 1,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/households/1/stores/10/available-items*", async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
@ -104,30 +91,33 @@ test("manage stores lets admins import and curate available items", async ({ pag
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ items: filteredItems }),
|
||||
body: JSON.stringify({ items: filteredItems, catalog_ready: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method() === "POST") {
|
||||
availableItems = [
|
||||
...availableItems,
|
||||
{
|
||||
item_id: 888,
|
||||
item_name: "trail mix",
|
||||
item_image: null,
|
||||
image_mime_type: null,
|
||||
item_type: "snack",
|
||||
item_group: "Trail Mix",
|
||||
zone: "Snacks & Candy",
|
||||
},
|
||||
];
|
||||
await route.fulfill({ status: 500 });
|
||||
});
|
||||
|
||||
await page.route("**/households/1/stores/10/available-items/777", async (route) => {
|
||||
if (route.request().method() === "PATCH") {
|
||||
availableItems = availableItems.map((item) =>
|
||||
item.item_id === 777
|
||||
? {
|
||||
...item,
|
||||
item_type: "produce",
|
||||
item_group: "Fruits",
|
||||
zone: "Produce & Fresh Vegetables",
|
||||
has_managed_settings: true,
|
||||
}
|
||||
: item
|
||||
);
|
||||
await route.fulfill({
|
||||
status: 201,
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
message: "Available item added",
|
||||
item: availableItems[availableItems.length - 1],
|
||||
message: "Available item updated",
|
||||
item: availableItems.find((item) => item.item_id === 777),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
@ -136,13 +126,23 @@ test("manage stores lets admins import and curate available items", async ({ pag
|
||||
await route.fulfill({ status: 500 });
|
||||
});
|
||||
|
||||
await page.route("**/households/1/stores/10/available-items/888", async (route) => {
|
||||
await page.route("**/households/1/stores/10/available-items/501", async (route) => {
|
||||
if (route.request().method() === "DELETE") {
|
||||
availableItems = availableItems.filter((item) => item.item_id !== 888);
|
||||
availableItems = availableItems.map((item) =>
|
||||
item.item_id === 501
|
||||
? {
|
||||
...item,
|
||||
item_type: null,
|
||||
item_group: null,
|
||||
zone: null,
|
||||
has_managed_settings: false,
|
||||
}
|
||||
: item
|
||||
);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ message: "Available item removed" }),
|
||||
body: JSON.stringify({ message: "Store item settings cleared" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -157,26 +157,27 @@ test("manage stores lets admins import and curate available items", async ({ pag
|
||||
await expect(storeCard.getByText("Store Item Catalog")).toBeVisible();
|
||||
|
||||
await expect(storeCard.getByText("milk")).toBeVisible();
|
||||
await expect(storeCard.getByText("apples")).toBeVisible();
|
||||
await expect(storeCard.getByRole("button", { name: "Add Item" })).toHaveCount(0);
|
||||
await expect(storeCard.getByRole("button", { name: "Import Current List" })).toHaveCount(0);
|
||||
|
||||
await storeCard.getByRole("button", { name: "Import Current List" }).click();
|
||||
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Imported current list items");
|
||||
await expect(storeCard.getByText("granola")).toBeVisible();
|
||||
|
||||
await storeCard.getByRole("button", { name: "Add Item" }).click();
|
||||
await storeCard.locator(".store-available-items-card").filter({ hasText: "apples" }).getByRole("button", { name: "Edit" }).click();
|
||||
const editorModal = page.locator(".available-item-editor-modal");
|
||||
await expect(editorModal).toBeVisible();
|
||||
await editorModal.getByLabel("Item Name").fill("trail mix");
|
||||
await editorModal.locator(".available-item-editor-select").nth(0).selectOption("snack");
|
||||
await editorModal.locator(".available-item-editor-select").nth(1).selectOption("Trail Mix");
|
||||
await editorModal.locator(".available-item-editor-select").nth(2).selectOption("Snacks & Candy");
|
||||
await editorModal.getByRole("button", { name: "Add Item" }).click();
|
||||
await expect(editorModal.getByLabel("Item Name")).toBeDisabled();
|
||||
await editorModal.locator(".available-item-editor-select").nth(0).selectOption("produce");
|
||||
await editorModal.locator(".available-item-editor-select").nth(1).selectOption("Fruits");
|
||||
await editorModal.locator(".available-item-editor-select").nth(2).selectOption("Produce & Fresh Vegetables");
|
||||
await editorModal.getByRole("button", { name: "Save Changes" }).click();
|
||||
|
||||
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Added store item");
|
||||
await expect(storeCard.getByText("trail mix")).toBeVisible();
|
||||
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Updated store item");
|
||||
await expect(storeCard.getByText("produce | Fruits | Produce & Fresh Vegetables")).toBeVisible();
|
||||
|
||||
page.once("dialog", (dialog) => dialog.accept());
|
||||
await storeCard.locator(".store-available-items-card").filter({ hasText: "trail mix" }).getByRole("button", { name: "Remove" }).click();
|
||||
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Removed store item");
|
||||
await storeCard.locator(".store-available-items-card").filter({ hasText: "milk" }).getByRole("button", { name: "Clear Settings" }).click();
|
||||
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Cleared store item settings");
|
||||
await expect(storeCard.locator(".store-available-items-card").filter({ hasText: "milk" }).getByText("No store defaults set")).toBeVisible();
|
||||
await expect(storeCard.locator(".store-available-items-card").filter({ hasText: "milk" }).getByRole("button", { name: "Clear Settings" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("grocery page remains unchanged and does not show a store items picker", async ({ page }) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user