"use client"; import Link from "next/link"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { Badge, Button, Card, ErrorBanner, Input, PageHeader, } from "@/components/ui"; import { apiFetch, ApiClientError } from "@/lib/client-api"; interface POInfo { id: string; vendor: string; status: string; createdAt: string; sentAt: string | null; receivedAt: string | null; notes: string | null; } interface LineRow { id: string; fastener: { id: string; partNumber: string; description: string; supplier: string | null; unitCost: number | null; }; qty: number; receivedQty: number; unitCost: number | null; } const STATUS_TONE: Record = { draft: "slate", sent: "blue", partial: "amber", received: "green", cancelled: "red", }; function formatDate(iso: string | null) { if (!iso) return "—"; return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } export default function POClient({ project, po, lines, }: { project: { id: string; code: string; name: string }; po: POInfo; lines: LineRow[]; }) { const router = useRouter(); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); // Per-line receive input. Keyed by line id so re-renders don't clobber state. const [receipts, setReceipts] = useState>({}); const total = lines.reduce((a, l) => { const cost = l.unitCost ?? l.fastener.unitCost ?? 0; return a + cost * l.qty; }, 0); const totalReceived = lines.reduce((a, l) => a + l.receivedQty, 0); const totalQty = lines.reduce((a, l) => a + l.qty, 0); async function changeStatus(next: string) { if (!confirm(`Move PO to "${next}"?`)) return; setBusy(true); setError(null); try { await apiFetch(`/api/v1/purchase-orders/${po.id}/status`, { method: "POST", body: JSON.stringify({ status: next }), }); router.refresh(); } catch (err) { setError(err instanceof ApiClientError ? err.message : "Status change failed"); } finally { setBusy(false); } } async function submitReceipts() { const payload = Object.entries(receipts) .map(([lineId, qty]) => ({ lineId, qty: Number(qty) })) .filter((r) => r.qty > 0); if (payload.length === 0) { setError("Enter at least one receipt quantity"); return; } setBusy(true); setError(null); try { await apiFetch(`/api/v1/purchase-orders/${po.id}/receive`, { method: "POST", body: JSON.stringify({ receipts: payload }), }); setReceipts({}); router.refresh(); } catch (err) { setError(err instanceof ApiClientError ? err.message : "Receipt failed"); } finally { setBusy(false); } } async function deleteDraft() { if (!confirm("Delete this draft PO? This can't be undone.")) return; setBusy(true); setError(null); try { await apiFetch(`/api/v1/purchase-orders/${po.id}`, { method: "DELETE" }); router.push(`/admin/projects/${project.id}/purchase-orders`); } catch (err) { setError(err instanceof ApiClientError ? err.message : "Delete failed"); setBusy(false); } } // Which state transitions are offered here — mirror of PO_TRANSITIONS in the API. const canSend = po.status === "draft"; const canCancel = po.status !== "received" && po.status !== "cancelled"; const canReceive = po.status === "sent" || po.status === "partial"; const canDeleteDraft = po.status === "draft"; return (
{po.vendor} {po.status} } description={ PO {po.id.slice(0, 8).toUpperCase()} · Created {formatDate(po.createdAt)} · Sent {formatDate(po.sentAt)} · Received {formatDate(po.receivedAt)} } actions={
Download PDF {canSend ? ( ) : null} {canCancel ? ( ) : null} {canDeleteDraft ? ( ) : null}
} /> {po.notes ? (
{po.notes}
) : null} {canReceive ? ( ) : null} {lines.map((l) => { const effectiveCost = l.unitCost ?? l.fastener.unitCost ?? null; const lineTotal = effectiveCost !== null ? effectiveCost * l.qty : null; const remaining = l.qty - l.receivedQty; const full = remaining === 0; return ( {canReceive ? ( ) : null} ); })} {canReceive ? : null}
Part # Description Qty Received Unit Line totalReceive +
{l.fastener.partNumber}
{l.fastener.description}
{l.fastener.supplier ? (
{l.fastener.supplier}
) : null}
{l.qty} {l.receivedQty} {full && l.receivedQty > 0 ? ( full ) : null} {effectiveCost !== null ? effectiveCost.toFixed(2) : "—"} {lineTotal !== null ? lineTotal.toFixed(2) : "—"} setReceipts((prev) => ({ ...prev, [l.id]: e.target.value })) } placeholder={remaining.toString()} className="w-20 text-right" disabled={full || busy} />
Totals {totalQty} {totalReceived} {total > 0 ? total.toFixed(2) : "—"}
{canReceive ? (
) : null}
); }