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} + /> + + ); +}