diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index 7fbafb9..e21bc53 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -52,6 +52,7 @@ export default function LocationMapManager() { const [mapState, setMapState] = useState(null); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); const [saving, setSaving] = useState(false); const [savingAction, setSavingAction] = useState(null); const [mode, setMode] = useState("setup"); @@ -157,9 +158,13 @@ export default function LocationMapManager() { }, []); const loadMap = useCallback(async () => { - if (!activeHousehold?.id || !locationId) return; + if (!activeHousehold?.id || !locationId) { + setLoading(false); + return; + } setLoading(true); + setLoadError(null); try { const response = await getLocationMap(activeHousehold.id, locationId); const nextState = response.data; @@ -216,6 +221,12 @@ export default function LocationMapManager() { const message = getApiErrorMessage(error, "Failed to load map"); toastRef.current.error("Load map failed", message); setMapState(null); + setObjects([]); + setSelectedObjectKey(null); + setHistory([]); + setFuture([]); + setHasUnsavedChanges(false); + setLoadError(message); } finally { setLoading(false); } @@ -563,6 +574,25 @@ export default function LocationMapManager() { ); } + if (loadError) { + return ( +
+ +
+
+

Map Unavailable

+

{loadError}

+
+ +
+
+
+
+ ); + } + const hasAnyMap = Boolean(mapState?.draft_map || mapState?.published_map); const hasVisibleOrManageableMap = Boolean(mapState?.published_map || (mapState?.draft_map && canManage)); const status = getMapStatus(mapState, { mode, previewDraft, hasUnsavedChanges }); diff --git a/frontend/src/styles/pages/LocationMapManager.css b/frontend/src/styles/pages/LocationMapManager.css index 1f102f6..5170000 100644 --- a/frontend/src/styles/pages/LocationMapManager.css +++ b/frontend/src/styles/pages/LocationMapManager.css @@ -91,6 +91,11 @@ border-color: rgba(244, 114, 182, 0.42); } +.location-map-status-load-error { + background: rgba(248, 113, 113, 0.2); + border-color: rgba(248, 113, 113, 0.42); +} + .location-map-status-published { background: rgba(20, 184, 166, 0.18); border-color: rgba(20, 184, 166, 0.34); diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index 8e5ef12..0fa01d3 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -412,6 +412,64 @@ test("admin setup explains when no zones exist yet", async ({ page }) => { await expect(page.getByLabel("Map controls")).toHaveCount(0); }); +test("load failure shows retryable error instead of no-map setup", async ({ page }) => { + await mockMapShell(page); + + const mapState = publishedMapState([ + { + id: 391, + 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); + let attempts = 0; + + await page.route("**/households/1/locations/10/map", async (route) => { + attempts += 1; + if (attempts === 1) { + await route.fulfill({ + status: 503, + contentType: "application/json", + body: JSON.stringify({ error: { message: "Map service unavailable" } }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mapState), + }); + }); + + await page.goto("/stores/100/locations/10/map"); + + const errorCard = page.locator(".location-map-load-error"); + await expect(errorCard.getByRole("heading", { name: "Map Unavailable" })).toBeVisible(); + await expect(errorCard.getByText("Map service unavailable")).toBeVisible(); + await expect(page.getByText("Load Error")).toBeVisible(); + await expect(page.getByRole("heading", { name: "No Map" })).toHaveCount(0); + await expect(page.getByRole("button", { name: /Create From/ })).toHaveCount(0); + + await page.getByRole("button", { name: "Retry" }).click(); + + await expect(page.getByRole("heading", { name: "Map Unavailable" })).toHaveCount(0); + await expect(page.getByText("Published")).toBeVisible(); + await expect(page.getByRole("button", { name: "Map area Bakery" })).toBeVisible(); + expect(attempts).toBe(2); +}); + test("admin can add the first area from a blank map canvas", async ({ page }) => { await mockMapShell(page);