"use client"; import Link from "next/link"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { Badge, Button, Card, ErrorBanner, Field, Input, Modal, PageHeader, Textarea, } from "@/components/ui"; import { apiFetch, ApiClientError } from "@/lib/client-api"; import { EditProjectModal } from "../ProjectsClient"; interface ProjectInfo { id: string; code: string; name: string; customerCode: string | null; status: string; dueDate: string | null; notes: string | null; fastenerCount: number; poCount: number; } interface AssemblyRow { id: string; code: string; name: string; qty: number; notes: string | null; partCount: number; } const STATUS_TONE: Record = { planning: "slate", in_progress: "blue", completed: "green", cancelled: "red", }; const STATUS_LABEL: Record = { planning: "Planning", in_progress: "In progress", completed: "Completed", cancelled: "Cancelled", }; function formatDate(iso: string | null) { if (!iso) return "—"; return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } export default function ProjectDetailClient({ project, assemblies, }: { project: ProjectInfo; assemblies: AssemblyRow[]; }) { const router = useRouter(); const [editOpen, setEditOpen] = useState(false); const [newAssemblyOpen, setNewAssemblyOpen] = useState(false); return (
{project.code} {project.name} } description={ {STATUS_LABEL[project.status] ?? project.status} Customer: {project.customerCode ?? "—"} · Due: {formatDate(project.dueDate)} } actions={ <> Print outstanding travelers Print all travelers } /> {project.notes ? (
{project.notes}
) : null}

Assemblies

{assemblies.map((a) => ( ))} {assemblies.length === 0 && ( )}
Code Name Qty Parts
{a.code} {a.name} {a.qty} {a.partCount} Open →
No assemblies yet. Add one to start building parts and operations.

Fasteners

Open →

{project.fastenerCount} item{project.fastenerCount === 1 ? "" : "s"} tracked

Add BOM items, suppliers, unit costs, and unresolved-need suggestions for POs.

Purchase orders

Open →

{project.poCount} PO{project.poCount === 1 ? "" : "s"}

Draft → sent → partial → received. Download vendor PDFs, record receipts.

{editOpen && ( setEditOpen(false)} onSaved={() => router.refresh()} /> )} {newAssemblyOpen && ( setNewAssemblyOpen(false)} onSaved={(assemblyId) => { setNewAssemblyOpen(false); router.push(`/admin/projects/${project.id}/assemblies/${assemblyId}`); }} /> )}
); } function NewAssemblyModal({ projectId, onClose, onSaved, }: { projectId: string; onClose: () => void; onSaved: (id: string) => void; }) { const [code, setCode] = useState(""); const [name, setName] = useState(""); const [qty, setQty] = useState("1"); const [notes, setNotes] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); async function submit(e: React.FormEvent) { e.preventDefault(); setBusy(true); setError(null); try { const res = await apiFetch<{ assembly: { id: string } }>( `/api/v1/projects/${projectId}/assemblies`, { method: "POST", body: JSON.stringify({ code, name, qty: Number(qty), notes: notes || null }), }, ); onSaved(res.assembly.id); } catch (err) { setError(err instanceof ApiClientError ? err.message : "Save failed"); setBusy(false); } } return (
} >
setCode(e.target.value)} required autoFocus /> setName(e.target.value)} required /> setQty(e.target.value)} required />