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);