"use client"; import { useState, useMemo, useEffect } from "react"; import Image from "next/image"; import { logAuditEvent } from "@/actions/audit"; type Product = { id: string; name: string; type: string; price: number; image_url?: string | null; }; type AssignedProduct = { id: string; product_id: string; products: { name: string; type: string; price: number; image_url?: string | null } | null; }; type StopProductAssignmentProps = { stopId: string; allProducts: Product[]; assignedProducts: AssignedProduct[]; callerUid: string; }; type Filter = "all" | "available" | "assigned"; /* Helpers — monospace price + initial-letter avatar */ const formatPrice = (n: number) => `$${Number(n ?? 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; const initialOf = (name: string) => (name?.trim()?.charAt(0) ?? "·").toUpperCase(); const TYPE_LABEL: Record = { pickup: "Pickup", wholesale: "Wholesale", subscription: "Sub", }; export default function StopProductAssignment({ stopId, allProducts, assignedProducts, callerUid, }: StopProductAssignmentProps) { const [products, setProducts] = useState(assignedProducts); const [search, setSearch] = useState(""); const [filter, setFilter] = useState("all"); const [pendingId, setPendingId] = useState(null); const [removingId, setRemovingId] = useState(null); const [error, setError] = useState(null); const assignedIds = useMemo( () => new Set(products.map((p) => p.product_id)), [products] ); // Counters for the filter chips const counts = useMemo(() => { const assignedCount = products.length; const availableCount = allProducts.filter((p) => !assignedIds.has(p.id)).length; return { all: allProducts.length, available: availableCount, assigned: assignedCount, }; }, [allProducts, products, assignedIds]); // Apply search + filter const visibleProducts = useMemo(() => { const q = search.trim().toLowerCase(); return allProducts.filter((p) => { const isAssigned = assignedIds.has(p.id); if (filter === "available" && isAssigned) return false; if (filter === "assigned" && !isAssigned) return false; if (!q) return true; return p.name.toLowerCase().includes(q) || p.type.toLowerCase().includes(q); }); }, [allProducts, search, filter, assignedIds]); async function assign(productId: string) { if (!productId || assignedIds.has(productId) || pendingId) return; setPendingId(productId); setError(null); try { const res = await fetch( `${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/rpc/assign_product_to_stop`, { method: "POST", headers: { apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ p_stop_id: stopId, p_product_id: productId, p_caller_uid: callerUid, }), } ); const data = await res.json(); if (!res.ok || data.success === false) { const errMsg = data?.error ?? data?.message ?? `HTTP ${res.status}`; const errDetail = data?.details ?? data?.hint ?? data?.code ?? ""; setError(`Failed to assign: ${errMsg}${errDetail ? " — " + errDetail : ""}`); setPendingId(null); 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", }, body: JSON.stringify({ p_stop_id: stopId }), } ); const refreshData = await refreshRes.json(); setProducts(refreshData.products ?? []); setPendingId(null); logAuditEvent({ table_name: "product_stops", record_id: data.id, action: "INSERT", old_data: null, new_data: { stop_id: stopId, product_id: productId }, brand_id: null, }); } catch { setError("Network error while assigning product."); setPendingId(null); } } async function remove(productId: string) { if (removingId) return; const entry = products.find((p) => p.product_id === productId); if (!entry) return; setRemovingId(productId); setError(null); try { const res = await fetch( `${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/rpc/unassign_product_from_stop`, { method: "POST", headers: { apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ p_stop_id: stopId, p_product_id: productId, p_caller_uid: callerUid, }), } ); const data = await res.json(); if (!res.ok || data.success === false) { const errMsg = data?.error ?? data?.message ?? `HTTP ${res.status}`; const errDetail = data?.details ?? data?.hint ?? data?.code ?? ""; setError(`Failed to remove: ${errMsg}${errDetail ? " — " + errDetail : ""}`); setRemovingId(null); return; } setProducts(products.filter((p) => p.product_id !== productId)); setRemovingId(null); logAuditEvent({ table_name: "product_stops", record_id: entry.id, action: "DELETE", old_data: { stop_id: stopId, product_id: productId }, new_data: null, brand_id: null, }); } catch { setError("Network error while removing product."); setRemovingId(null); } } function toggle(productId: string) { if (assignedIds.has(productId)) { remove(productId); } else { assign(productId); } } function clearAll() { if (!products.length) return; // Remove sequentially to be safe (async () => { for (const p of [...products]) { await remove(p.product_id); } })(); } // Keyboard: pressing / focuses search useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === "/" && document.activeElement?.tagName !== "INPUT") { e.preventDefault(); const el = document.getElementById("stop-product-search") as HTMLInputElement | null; el?.focus(); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const hasAssigned = products.length > 0; const hasProducts = allProducts.length > 0; return (
{error && (
{error}
)} {/* Header: editorial title + search */}
{String(products.length).padStart(2, "0")} {products.length === 1 ? "Product on this stop" : "Products on this stop"}
setSearch(e.target.value)} placeholder="Search products…" className="ha-picker-search-input" autoComplete="off" />
{/* Filter chips */}
Press / to search
{/* Product grid */} {!hasProducts ? (

No products yet

Create products in the catalog first, then come back here to assign them to this stop.

) : visibleProducts.length === 0 ? (

{filter === "assigned" ? "Nothing assigned" : "No matches"}

{search.trim() ? `No products match "${search.trim()}". Try a different term.` : filter === "assigned" ? "This stop has no products assigned yet. Click a card in the All tab to add one." : "All products are already assigned to this stop."}

) : (
{visibleProducts.map((product) => { const isAssigned = assignedIds.has(product.id); const isBusy = pendingId === product.id || removingId === product.id; return ( ); })}
)} {/* Summary footer when assigned items exist */} {hasAssigned && (
{products.length}{" "} {products.length === 1 ? "product is" : "products are"} live at this stop
)}
); }