From abda001208bf7aba87fe0394e92a96f612cd74e3 Mon Sep 17 00:00:00 2001 From: Nico Date: Thu, 4 Jun 2026 11:54:57 -0700 Subject: [PATCH] fix: keep map buy flow moving after refresh failure --- frontend/src/pages/LocationMapManager.jsx | 22 +++-- frontend/tests/location-map-manager.spec.ts | 94 +++++++++++++++++++++ 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index 30e6404..532e542 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -42,6 +42,15 @@ function getNextMapBuyItem(items, currentIndex, excludedItemId) { return remainingItems[Math.min(Math.max(currentIndex, 0), remainingItems.length - 1)]; } +function getLocallyReducedMapItem(item, quantity) { + const currentQuantity = Number(item.quantity || 1); + const boughtQuantity = Math.max(1, Number(quantity || 1)); + return { + ...item, + quantity: Math.max(1, currentQuantity - boughtQuantity), + }; +} + export default function LocationMapManager() { const { storeId, locationId } = useParams(); const navigate = useNavigate(); @@ -183,11 +192,14 @@ export default function LocationMapManager() { nextBuyModalItems = buyModalItems.filter((candidate) => candidate.id !== item.id); updateMapItems((currentItems) => currentItems.filter((candidate) => candidate.id !== item.id)); } else { - const response = await getItemByName(activeHousehold.id, locationId, item.item_name); - const updatedItem = response.data || { - ...item, - quantity: Math.max(1, Number(item.quantity || 1) - Number(quantity || 1)), - }; + let updatedItem; + try { + const response = await getItemByName(activeHousehold.id, locationId, item.item_name); + updatedItem = response.data || getLocallyReducedMapItem(item, quantity); + } catch { + updatedItem = getLocallyReducedMapItem(item, quantity); + toast.info("Updated item locally", "Latest quantity will sync when the map reloads."); + } nextBuyModalItems = buyModalItems.map((candidate) => (candidate.id === item.id ? updatedItem : candidate)); updateMapItems((currentItems) => diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index 50984dd..dbe1929 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -2445,6 +2445,100 @@ test("viewer advances map buy modal after partial quantity buys", async ({ page await expect(page.getByRole("button", { name: "Mark flour bought" }).locator("small")).toHaveText("x2"); }); +test("viewer keeps map buy flow moving when quantity refresh fails", async ({ page }) => { + await mockMapShell(page); + + const partialItems = [ + { + id: 4311, + item_name: "flour", + quantity: 3, + bought: false, + zone_id: 501, + zone: "Bakery", + added_by_users: ["map-user"], + }, + { + id: 4312, + item_name: "yeast", + quantity: 1, + bought: false, + zone_id: 501, + zone: "Bakery", + added_by_users: ["map-user"], + }, + ]; + const mapState = { + ...publishedMapState([ + { + id: 1535, + location_map_id: 901, + zone_id: 501, + zone_name: "Bakery", + type: "zone", + label: "Bakery", + x: 40, + y: 40, + width: 260, + height: 160, + rotation: 0, + locked: false, + visible: true, + sort_order: 1, + }, + ], true), + items: partialItems, + }; + const boughtPayloads: Array> = []; + + await page.route("**/households/1/locations/10/map", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mapState), + }); + }); + + await page.route("**/households/1/locations/10/list/item**", async (route) => { + if (route.request().method() === "GET") { + await route.fulfill({ + status: 503, + contentType: "application/json", + body: JSON.stringify({ error: "refresh unavailable" }), + }); + return; + } + + boughtPayloads.push(await route.request().postDataJSON()); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ success: true }), + }); + }); + + await page.goto("/stores/100/locations/10/map"); + await page.locator('[data-object-key="1535"]').locator("rect").first().click(); + await page.getByRole("button", { name: "Mark flour bought" }).click(); + await expect(page.locator(".confirm-buy-item-name")).toHaveText("flour"); + + await page.locator(".confirm-buy-counter-btn").first().click(); + await page.locator(".confirm-buy-counter-btn").first().click(); + await expect(page.locator(".confirm-buy-counter-display")).toHaveValue("1"); + await page.getByRole("button", { name: "Mark as Bought" }).click(); + + await expect.poll(() => boughtPayloads.length).toBe(1); + expect(boughtPayloads[0]).toMatchObject({ + item_name: "flour", + bought: true, + quantity_bought: 1, + }); + await expect(page.getByText("Updated item locally")).toBeVisible(); + await expect(page.locator(".confirm-buy-item-name")).toHaveText("yeast"); + await expect(page.getByRole("button", { name: "Mark flour bought" }).locator("small")).toHaveText("x2"); + await expect(page.getByText("Mark item bought failed")).toHaveCount(0); +}); + test("viewer shows completed zone items without rebuy actions", async ({ page }) => { await mockMapShell(page);