diff --git a/frontend/src/components/maps/LocationMapBottomSheetSections.jsx b/frontend/src/components/maps/LocationMapBottomSheetSections.jsx
index 354c902..7325668 100644
--- a/frontend/src/components/maps/LocationMapBottomSheetSections.jsx
+++ b/frontend/src/components/maps/LocationMapBottomSheetSections.jsx
@@ -139,15 +139,18 @@ export function SelectedZoneItemsPanel({
{selectedZoneItems.map((item) => (
- -
+
-
))}
diff --git a/frontend/src/styles/pages/LocationMapManager.css b/frontend/src/styles/pages/LocationMapManager.css
index aae1705..232bf46 100644
--- a/frontend/src/styles/pages/LocationMapManager.css
+++ b/frontend/src/styles/pages/LocationMapManager.css
@@ -787,6 +787,26 @@
background: rgba(96, 165, 250, 0.16);
}
+.location-map-zone-items li.is-bought {
+ background: rgba(20, 184, 166, 0.12);
+ border-color: rgba(20, 184, 166, 0.3);
+}
+
+.location-map-zone-items li.is-bought .location-map-zone-item-button {
+ color: #cbd5e1;
+ cursor: default;
+}
+
+.location-map-zone-items li.is-bought .location-map-zone-item-button span {
+ text-decoration: line-through;
+ text-decoration-thickness: 2px;
+ text-decoration-color: rgba(20, 184, 166, 0.85);
+}
+
+.location-map-zone-item-button:disabled {
+ opacity: 1;
+}
+
.location-map-zone-items small {
color: #cbd5e1;
font-weight: 800;
diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts
index 935efce..a704338 100644
--- a/frontend/tests/location-map-manager.spec.ts
+++ b/frontend/tests/location-map-manager.spec.ts
@@ -2264,6 +2264,70 @@ test("viewer can buy selected zone items from the map", async ({ page }) => {
await expect(page.getByText("No items assigned to this zone yet.")).toBeVisible();
});
+test("viewer shows completed zone items without rebuy actions", async ({ page }) => {
+ await mockMapShell(page);
+
+ const mapState = {
+ ...publishedMapState([
+ {
+ id: 1533,
+ 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: [
+ {
+ ...items[0],
+ bought: true,
+ },
+ ],
+ };
+ let boughtRequestCount = 0;
+
+ 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) => {
+ boughtRequestCount += 1;
+ 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: "Bought" }).click();
+ await page.locator('[data-object-key="1533"]').locator("rect").first().click();
+
+ const boughtRow = page.getByRole("button", { name: "french bread bought" });
+ await expect(boughtRow).toBeVisible();
+ await expect(boughtRow).toBeDisabled();
+ await expect(boughtRow.locator("small")).toHaveText("Bought");
+
+ await boughtRow.click({ force: true });
+
+ await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
+ expect(boughtRequestCount).toBe(0);
+});
+
test("viewer explains when unmapped items are hidden by filters", async ({ page }) => {
await mockMapShell(page);