From 1658b7f9793059ac637130d7db4067ae1a352fb1 Mon Sep 17 00:00:00 2001 From: Nico Date: Thu, 4 Jun 2026 11:11:01 -0700 Subject: [PATCH] fix: buy unmapped map items in place --- .../maps/LocationMapBottomSheet.jsx | 2 + .../maps/LocationMapBottomSheetSections.jsx | 39 +++++---- frontend/src/pages/LocationMapManager.jsx | 15 ++-- .../src/styles/pages/LocationMapManager.css | 25 +++--- frontend/tests/location-map-manager.spec.ts | 80 ++++++++++++++++++- 5 files changed, 130 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/maps/LocationMapBottomSheet.jsx b/frontend/src/components/maps/LocationMapBottomSheet.jsx index 0d5c095..7e05662 100644 --- a/frontend/src/components/maps/LocationMapBottomSheet.jsx +++ b/frontend/src/components/maps/LocationMapBottomSheet.jsx @@ -44,6 +44,7 @@ export default function LocationMapBottomSheet({ onDeleteObject, onShowHiddenAreas, onSelectZoneItem, + onSelectUnmappedItem, }) { const selectedTitle = getMapObjectDisplayLabel(selectedObject); const selectedZoneId = objectZoneId(selectedObject); @@ -292,6 +293,7 @@ export default function LocationMapBottomSheet({ hiddenUnmappedItemCount={hiddenUnmappedItemCount} hasHiddenUnmappedItems={hasHiddenUnmappedItems} onShowUnmappedItems={showUnmappedItems} + onSelectUnmappedItem={onSelectUnmappedItem} /> ) : null} diff --git a/frontend/src/components/maps/LocationMapBottomSheetSections.jsx b/frontend/src/components/maps/LocationMapBottomSheetSections.jsx index 7325668..1576c1b 100644 --- a/frontend/src/components/maps/LocationMapBottomSheetSections.jsx +++ b/frontend/src/components/maps/LocationMapBottomSheetSections.jsx @@ -13,6 +13,27 @@ export const MAP_DISPLAY_CONTROLS = [ const UNMAPPED_PREVIEW_LIMIT = 8; +function MapItemActionRow({ item, onSelectItem }) { + const isBought = Boolean(item.bought); + + return ( +
  • + +
  • + ); +} + export function LocationMapLayerPanel({ filters, setFilters }) { return (
    @@ -139,20 +160,7 @@ export function SelectedZoneItemsPanel({
      {selectedZoneItems.map((item) => ( -
    • - -
    • + ))}
    {hasHiddenSelectedItems ? ( @@ -174,6 +182,7 @@ export function UnmappedItemsPanel({ hiddenUnmappedItemCount, hasHiddenUnmappedItems, onShowUnmappedItems, + onSelectUnmappedItem, }) { if (visibleUnmappedItems.length === 0 && hasHiddenUnmappedItems) { return ( @@ -203,7 +212,7 @@ export function UnmappedItemsPanel({ Unmapped Items
      {unmappedItemPreview.map((item) => ( -
    • {item.item_name}
    • + ))} {overflowUnmappedItemCount > 0 ? (
    • unmappedItems(mapItems, filters, username), [filters, mapItems, username] ); + const buyModalItems = useMemo( + () => (selectedObject ? selectedZoneItems : visibleUnmappedItems), + [selectedObject, selectedZoneItems, visibleUnmappedItems] + ); const { visibleAssignedItemCount, unmappedItemCount } = useMemo(() => { let nextVisibleAssignedItemCount = 0; let nextUnmappedItemCount = 0; @@ -166,17 +170,17 @@ export default function LocationMapManager() { return; } - const item = selectedZoneItems.find((candidate) => candidate.id === buyModalItem.id) || buyModalItem; + const item = buyModalItems.find((candidate) => candidate.id === buyModalItem.id) || buyModalItem; try { - const currentIndex = selectedZoneItems.findIndex((candidate) => candidate.id === item.id); + const currentIndex = buyModalItems.findIndex((candidate) => candidate.id === item.id); const resolvedIndex = currentIndex >= 0 ? currentIndex : 0; await markBought(activeHousehold.id, locationId, item.item_name, quantity, true); if (quantity >= item.quantity) { updateMapItems((currentItems) => currentItems.filter((candidate) => candidate.id !== item.id)); - setBuyModalItem(getNextMapBuyItem(selectedZoneItems, resolvedIndex, item.id)); + setBuyModalItem(getNextMapBuyItem(buyModalItems, resolvedIndex, item.id)); } else { const response = await getItemByName(activeHousehold.id, locationId, item.item_name); const updatedItem = response.data || { @@ -195,7 +199,7 @@ export default function LocationMapManager() { const message = getApiErrorMessage(error, "Failed to mark item as bought"); toast.error("Mark item bought failed", `Mark item bought failed: ${message}`); } - }, [activeHousehold?.id, buyModalItem, locationId, selectedZoneItems, toast, updateMapItems]); + }, [activeHousehold?.id, buyModalItem, buyModalItems, locationId, toast, updateMapItems]); const mapSize = useMemo( () => ({ @@ -435,6 +439,7 @@ export default function LocationMapManager() { onDeleteObject={requestDeleteObject} onShowHiddenAreas={handleShowHiddenAreas} onSelectZoneItem={setBuyModalItem} + onSelectUnmappedItem={setBuyModalItem} /> )} @@ -443,7 +448,7 @@ export default function LocationMapManager() { {buyModalItem ? ( { + await mockMapShell(page); + + const unmappedMapItems = [ + { + id: 4201, + item_name: "loose batteries", + quantity: 1, + bought: false, + zone_id: null, + zone: null, + added_by_users: ["map-user"], + }, + { + id: 4202, + item_name: "paper plates", + quantity: 2, + bought: false, + zone_id: null, + zone: null, + added_by_users: ["map-user"], + }, + ]; + const mapState = { + ...publishedMapState([], true), + items: unmappedMapItems, + unmapped_count: unmappedMapItems.length, + }; + 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) => { + 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.getByRole("button", { name: "Layers" }).click(); + await page.getByRole("button", { name: "Unmapped" }).click(); + + await page.getByRole("button", { name: "Mark loose batteries bought" }).click(); + await expect(page.locator(".confirm-buy-item-name")).toHaveText("loose batteries"); + await page.getByRole("button", { name: "Mark as Bought" }).click(); + + await expect.poll(() => boughtPayloads.length).toBe(1); + expect(boughtPayloads[0]).toMatchObject({ + item_name: "loose batteries", + bought: true, + quantity_bought: 1, + }); + await expect(page.locator(".confirm-buy-item-name")).toHaveText("paper plates"); + await expect(page.getByText("loose batteries")).toHaveCount(0); + await expect(page.getByLabel("Map summary").locator(".location-map-overview-row", { hasText: "Unmapped" })).toContainText("1"); + + await page.getByRole("button", { name: "Mark as Bought" }).click(); + + await expect.poll(() => boughtPayloads.length).toBe(2); + expect(boughtPayloads[1]).toMatchObject({ + item_name: "paper plates", + bought: true, + quantity_bought: 2, + }); + await expect(page.locator(".confirm-buy-modal")).toHaveCount(0); + await expect(page.getByText("paper plates")).toHaveCount(0); + await expect(page.getByLabel("Map summary").locator(".location-map-overview-row", { hasText: "Unmapped" })).toContainText("0"); +}); + test("viewer hides unrelated unmapped items while a zone is selected", async ({ page }) => { await mockMapShell(page);