From f80ea472ce7156c4d7239d143352dd9419195c02 Mon Sep 17 00:00:00 2001 From: Nico Date: Sun, 31 May 2026 21:06:41 -0700 Subject: [PATCH] fix: compact add store control --- .../src/components/manage/ManageStores.jsx | 64 +++++-------- .../styles/components/manage/ManageStores.css | 75 +++++---------- .../tests/available-items-catalog.spec.ts | 5 + frontend/tests/toast-notifications.spec.ts | 91 +++++++++++-------- 4 files changed, 102 insertions(+), 133 deletions(-) diff --git a/frontend/src/components/manage/ManageStores.jsx b/frontend/src/components/manage/ManageStores.jsx index d477783..bf278b4 100644 --- a/frontend/src/components/manage/ManageStores.jsx +++ b/frontend/src/components/manage/ManageStores.jsx @@ -42,11 +42,7 @@ export default function ManageStores() { refreshZones, } = useContext(StoreContext); const toast = useActionToast(); - const [createForm, setCreateForm] = useState({ - name: "", - location_name: "", - address: "", - }); + const [newStoreName, setNewStoreName] = useState(""); const [saving, setSaving] = useState(false); const isAdmin = ["owner", "admin"].includes(activeHousehold?.role); @@ -62,18 +58,17 @@ export default function ManageStores() { const handleCreateStore = async (event) => { event.preventDefault(); - if (!createForm.name.trim()) return; + const storeName = newStoreName.trim(); + if (!storeName) return; setSaving(true); try { await createHouseholdStore(activeHousehold.id, { - name: createForm.name.trim(), - location_name: createForm.location_name.trim() || "Default Location", - address: createForm.address.trim() || null, + name: storeName, }); - setCreateForm({ name: "", location_name: "", address: "" }); + setNewStoreName(""); await refreshAfterStoreChange(); - toast.success("Created store", `Created store ${createForm.name.trim()}`); + toast.success("Created store", `Created store ${storeName}`); } catch (error) { const message = getApiErrorMessage(error, "Failed to create store"); toast.error("Create store failed", `Create store failed: ${message}`); @@ -85,7 +80,23 @@ export default function ManageStores() { return (
-

Store Locations ({householdStores.length})

+
+

Store Locations ({householdStores.length})

+ {isAdmin ? ( +
+ setNewStoreName(event.target.value)} + placeholder="Store name" + aria-label="Store name" + required + /> + +
+ ) : null} +

Stores are private to this household. Locations define map-specific zones, item placement, and shopping order. @@ -152,34 +163,7 @@ export default function ManageStores() { )}

- {isAdmin ? ( -
-

Add Store

-
- setCreateForm((current) => ({ ...current, name: event.target.value }))} - placeholder="Store name, e.g. Costco" - required - /> - - setCreateForm((current) => ({ ...current, location_name: event.target.value })) - } - placeholder="Location name, e.g. Fontana" - /> - setCreateForm((current) => ({ ...current, address: event.target.value }))} - placeholder="Address or notes" - /> - -
-
- ) : activeStore ? ( + {!isAdmin && activeStore ? (

Household members can manage item defaults. Only owners and admins can manage stores, locations, zones, and item deletion. diff --git a/frontend/src/styles/components/manage/ManageStores.css b/frontend/src/styles/components/manage/ManageStores.css index 1bc7c34..6092b76 100644 --- a/frontend/src/styles/components/manage/ManageStores.css +++ b/frontend/src/styles/components/manage/ManageStores.css @@ -22,6 +22,14 @@ font-size: 1.3rem; font-weight: 600; color: var(--text-primary); + margin: 0; +} + +.manage-stores-topline { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(240px, 360px); + gap: 1rem; + align-items: center; margin-bottom: 1rem; } @@ -150,7 +158,7 @@ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; } -.add-store-panel input, +.add-store-inline input, .store-management-create-row input, .store-settings-form input { width: 100%; @@ -237,52 +245,15 @@ justify-content: flex-end; } -/* Add Store Panel */ -.add-store-panel { - display: flex; - flex-direction: column; - gap: 1rem; - margin-top: 1rem; -} - -.available-stores { +.add-store-inline { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; -} - -.available-store-card { - background: var(--background); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1.25rem; - display: flex; - justify-content: space-between; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.5rem; align-items: center; - gap: 1rem; - transition: all 0.2s; } -.available-store-card:hover { - border-color: var(--primary); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.available-store-card .store-info { - flex: 1; -} - -.available-store-card h3 { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - margin: 0 0 0.25rem 0; -} - -.available-store-card .store-location { - color: var(--text-secondary); - font-size: 0.85rem; - margin: 0; +.add-store-inline .btn-primary { + min-width: 4.5rem; } /* Empty State */ @@ -295,22 +266,14 @@ /* Responsive */ @media (max-width: 600px) { - .stores-list, - .available-stores { + .stores-list { grid-template-columns: 1fr; } - .available-store-card { - flex-direction: column; - align-items: flex-start; - } - - .available-store-card button { - width: 100%; - } - .store-card-header, .store-location-controls, + .manage-stores-topline, + .add-store-inline, .store-items-modal-toolbar.store-management-create-row, .store-items-modal-toolbar.store-location-create-row, .store-management-row { @@ -321,6 +284,10 @@ width: 100%; } + .add-store-inline .btn-primary { + width: 100%; + } + .store-management-order { text-align: left; } diff --git a/frontend/tests/available-items-catalog.spec.ts b/frontend/tests/available-items-catalog.spec.ts index 719083f..ad5b460 100644 --- a/frontend/tests/available-items-catalog.spec.ts +++ b/frontend/tests/available-items-catalog.spec.ts @@ -105,6 +105,11 @@ test("manage stores opens a modal to edit and delete household store items", asy await page.goto("/manage?tab=stores"); + await expect(page.getByRole("heading", { name: "Add Store" })).toHaveCount(0); + await expect(page.getByPlaceholder("Store name")).toBeVisible(); + await expect(page.getByPlaceholder("Location name")).toHaveCount(0); + await expect(page.getByPlaceholder("Address or notes")).toHaveCount(0); + const storeCard = page.locator(".store-card").filter({ hasText: "Costco" }); await expect(storeCard).toBeVisible(); await expect(storeCard.getByText("Costco", { exact: true })).toHaveCount(1); diff --git a/frontend/tests/toast-notifications.spec.ts b/frontend/tests/toast-notifications.spec.ts index ba89553..8303c3b 100644 --- a/frontend/tests/toast-notifications.spec.ts +++ b/frontend/tests/toast-notifications.spec.ts @@ -47,10 +47,15 @@ test("manage stores add success shows success toast", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); - let linkedStoreIds = [10]; - const allStores = [ - { id: 10, name: "Costco North", location: "North", is_default: true }, - { id: 11, name: "Costco South", location: "South", is_default: false }, + let stores = [ + { + id: 10, + household_store_id: 100, + name: "Costco", + location_name: "Default Location", + display_name: "Costco", + is_default: true, + }, ]; await page.route("**/households", async (route) => { @@ -61,65 +66,63 @@ test("manage stores add success shows success toast", async ({ page }) => { }); }); - await page.route("**/stores/household/1", async (route) => { + await page.route("**/households/1/stores", async (route) => { const request = route.request(); if (request.method() === "GET") { - const payload = linkedStoreIds.map((id, index) => { - const store = allStores.find((candidate) => candidate.id === id); - return { - ...store, - is_default: index === 0, - }; - }); await route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify(payload), + body: JSON.stringify(stores), }); return; } if (request.method() === "POST") { - const body = request.postDataJSON() as { storeId?: number }; - if (body.storeId && !linkedStoreIds.includes(body.storeId)) { - linkedStoreIds = [...linkedStoreIds, body.storeId]; - } + const body = request.postDataJSON() as { name?: string }; + const name = body.name || "New Store"; + stores = [ + ...stores, + { + id: 11, + household_store_id: 101, + name, + location_name: "Default Location", + display_name: name, + is_default: false, + }, + ]; await route.fulfill({ - status: 200, + status: 201, contentType: "application/json", - body: JSON.stringify({ ok: true }), + body: JSON.stringify({ store: stores[stores.length - 1] }), }); return; } - await route.fallback(); + await route.fulfill({ status: 405 }); }); - await page.route("**/stores", async (route) => { + await page.route("**/households/1/locations/10/zones", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify(allStores), + body: JSON.stringify({ zones: [] }), }); }); await page.goto("/manage?tab=stores"); - await page.getByRole("button", { name: "+ Add Store" }).click(); - await page.locator(".available-store-card").filter({ hasText: "Costco South" }).getByRole("button", { name: "Add" }).click(); + const addStoreForm = page.locator(".add-store-inline"); + await addStoreForm.getByLabel("Store name").fill("Stater Bros"); + await addStoreForm.getByRole("button", { name: "Add" }).click(); - await expect(page.locator(".action-toast.action-toast-success")).toContainText("Added store"); - await expect(page.locator(".action-toast.action-toast-success")).toContainText("Costco South"); + await expect(page.locator(".action-toast.action-toast-success")).toContainText("Created store"); + await expect(page.locator(".action-toast.action-toast-success")).toContainText("Stater Bros"); }); test("manage stores add failure shows normalized error toast", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); - const allStores = [ - { id: 10, name: "Costco North", location: "North", is_default: true }, - { id: 11, name: "Costco South", location: "South", is_default: false }, - ]; - await page.route("**/households", async (route) => { await route.fulfill({ status: 200, @@ -128,13 +131,22 @@ test("manage stores add failure shows normalized error toast", async ({ page }) }); }); - await page.route("**/stores/household/1", async (route) => { + await page.route("**/households/1/stores", async (route) => { const request = route.request(); if (request.method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify([{ id: 10, name: "Costco North", location: "North", is_default: true }]), + body: JSON.stringify([ + { + id: 10, + household_store_id: 100, + name: "Costco", + location_name: "Default Location", + display_name: "Costco", + is_default: true, + }, + ]), }); return; } @@ -150,22 +162,23 @@ test("manage stores add failure shows normalized error toast", async ({ page }) return; } - await route.fallback(); + await route.fulfill({ status: 405 }); }); - await page.route("**/stores", async (route) => { + await page.route("**/households/1/locations/10/zones", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify(allStores), + body: JSON.stringify({ zones: [] }), }); }); await page.goto("/manage?tab=stores"); - await page.getByRole("button", { name: "+ Add Store" }).click(); - await page.locator(".available-store-card").filter({ hasText: "Costco South" }).getByRole("button", { name: "Add" }).click(); + const addStoreForm = page.locator(".add-store-inline"); + await addStoreForm.getByLabel("Store name").fill("Costco"); + await addStoreForm.getByRole("button", { name: "Add" }).click(); - await expect(page.locator(".action-toast.action-toast-error")).toContainText("Add store failed"); + await expect(page.locator(".action-toast.action-toast-error")).toContainText("Create store failed"); await expect(page.locator(".action-toast.action-toast-error")).toContainText("Store already linked to household"); }); -- 2.39.5