From 73cc7d1dcef9de47611aef6c0131c0f6f4cf90bb Mon Sep 17 00:00:00 2001 From: default Date: Thu, 4 Jun 2026 17:27:16 +0000 Subject: [PATCH] fix(admin/stops): product picker + add StopsHeaderActions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StopProductAssignment - remove() now uses functional setState — fixes a stale-closure bug where 'Remove all' only cleared the first product and rapid single removals could collide - assign() no longer re-fetches the full assigned list on every click; it now does an optimistic insert using the product already in allProducts, keyed by the row id returned from the RPC. Eliminates a network round-trip + brief UI flicker per click - clearAll() snapshots the ids at click time and stops on error so a failed remove doesn't silently continue - 'Press / to search' hint: moved `ha-modal-footer-hint` to the parent span (it was on the kbd), so the kbd styles from `.ha-modal-footer-hint kbd` actually apply; removed the redundant inline style and the duplicated className StopsHeaderActions (new) - Small client component that owns the Add Stop / Upload Schedule modals and renders the two header buttons. Wires into the PageHeader actions slot so the actions stay accessible from the StopsViewClient (where the StopTableClient filter bar is hidden by the shared search/status filter) --- .../admin/StopProductAssignment.tsx | 52 +++++++----- src/components/admin/StopsHeaderActions.tsx | 83 +++++++++++++++++++ 2 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 src/components/admin/StopsHeaderActions.tsx diff --git a/src/components/admin/StopProductAssignment.tsx b/src/components/admin/StopProductAssignment.tsx index 639ba78..3706b87 100644 --- a/src/components/admin/StopProductAssignment.tsx +++ b/src/components/admin/StopProductAssignment.tsx @@ -83,6 +83,9 @@ export default function StopProductAssignment({ async function assign(productId: string) { if (!productId || assignedIds.has(productId) || pendingId) return; + const product = allProducts.find((p) => p.id === productId); + if (!product) return; + setPendingId(productId); setError(null); @@ -112,20 +115,25 @@ export default function StopProductAssignment({ return; } - // Optimistic: re-fetch to keep assigned list in sync with server - const refreshRes = await fetch( - `${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/rpc/get_stop_products`, - { - method: "POST", - headers: { - apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - "Content-Type": "application/json", + // Optimistic insert: build the entry from the product we already have + // locally, keyed by the row id returned from the RPC. Use functional + // setState so concurrent calls compose correctly. + setProducts((prev) => { + if (prev.some((p) => p.product_id === productId)) return prev; + return [ + ...prev, + { + id: data.id, + product_id: productId, + products: { + name: product.name, + type: product.type, + price: product.price, + image_url: product.image_url ?? null, + }, }, - body: JSON.stringify({ p_stop_id: stopId }), - } - ); - const refreshData = await refreshRes.json(); - setProducts(refreshData.products ?? []); + ]; + }); setPendingId(null); logAuditEvent({ @@ -176,7 +184,7 @@ export default function StopProductAssignment({ return; } - setProducts(products.filter((p) => p.product_id !== productId)); + setProducts((prev) => prev.filter((p) => p.product_id !== productId)); setRemovingId(null); logAuditEvent({ @@ -202,11 +210,15 @@ export default function StopProductAssignment({ } function clearAll() { - if (!products.length) return; - // Remove sequentially to be safe + if (!products.length || removingId || pendingId) return; + // Snapshot ids at click time, then remove each one sequentially so the + // server sees one request at a time. + const idsToRemove = products.map((p) => p.product_id); (async () => { - for (const p of [...products]) { - await remove(p.product_id); + for (const id of idsToRemove) { + // Stop if a per-item failure already surfaced an error + if (error) break; + await remove(id); } })(); } @@ -301,8 +313,8 @@ export default function StopProductAssignment({ On this stop {counts.assigned} - - Press / to search + + Press / to search diff --git a/src/components/admin/StopsHeaderActions.tsx b/src/components/admin/StopsHeaderActions.tsx new file mode 100644 index 0000000..85a391c --- /dev/null +++ b/src/components/admin/StopsHeaderActions.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { AdminButton } from "@/components/admin/design-system"; +import ScheduleImportModal from "@/components/admin/ScheduleImportModal"; +import AddStopModal from "@/components/admin/AddStopModal"; + +type Props = { + brandId: string; +}; + +export default function StopsHeaderActions({ brandId }: Props) { + const router = useRouter(); + const [showAdd, setShowAdd] = useState(false); + const [showImport, setShowImport] = useState(false); + + const refresh = () => router.refresh(); + + return ( + <> + setShowImport(true)} + icon={ + + + + } + > + Upload Schedule + + setShowAdd(true)} + icon={ + + + + } + > + Add Stop + + + {showImport && ( + setShowImport(false)} + onComplete={refresh} + /> + )} + + setShowAdd(false)} + brandId={brandId} + onSuccess={refresh} + /> + + ); +}