"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"; export interface FastenerRow { id: string; partNumber: string; description: string; qty: number; supplier: string | null; unitCost: number | null; notes: string | null; onOrder: number; received: number; remaining: number; unresolved: number; } export default function FastenersClient({ project, initial, }: { project: { id: string; code: string; name: string }; initial: FastenerRow[]; }) { const router = useRouter(); const [newOpen, setNewOpen] = useState(false); const [edit, setEdit] = useState(null); return (
Fasteners — {project.code}} description={ Parts that get bought, not built. Lines roll up into purchase order drafts. } actions={
Purchase orders →
} /> {initial.map((f) => ( ))} {initial.length === 0 && ( )}
Part # Description Supplier Qty On order Received Unit cost
{f.partNumber}
{f.description}
{f.notes ? (
{f.notes}
) : null}
{f.supplier ?? "—"} {f.qty} {f.onOrder} {f.unresolved > 0 ? ( {f.unresolved} to PO ) : null} {f.received} {f.remaining === 0 && f.received > 0 ? ( full ) : null} {f.unitCost != null ? f.unitCost.toFixed(2) : "—"}
No fasteners yet. Add the bolts, rivets, inserts, etc. that need to be purchased for this project.
{newOpen && ( setNewOpen(false)} onSaved={() => { setNewOpen(false); router.refresh(); }} /> )} {edit && ( setEdit(null)} onSaved={() => { setEdit(null); router.refresh(); }} onDeleted={() => { setEdit(null); router.refresh(); }} /> )}
); } function FastenerModal({ projectId, fastener, onClose, onSaved, onDeleted, }: { projectId: string; fastener?: FastenerRow; onClose: () => void; onSaved: () => void; onDeleted?: () => void; }) { const editing = !!fastener; const [partNumber, setPartNumber] = useState(fastener?.partNumber ?? ""); const [description, setDescription] = useState(fastener?.description ?? ""); const [qty, setQty] = useState(String(fastener?.qty ?? 1)); const [supplier, setSupplier] = useState(fastener?.supplier ?? ""); const [unitCost, setUnitCost] = useState(fastener?.unitCost != null ? String(fastener.unitCost) : ""); const [notes, setNotes] = useState(fastener?.notes ?? ""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); async function submit(e: React.FormEvent) { e.preventDefault(); setBusy(true); setError(null); try { const payload = { partNumber, description, qty: Number(qty), supplier: supplier || null, unitCost: unitCost ? Number(unitCost) : null, notes: notes || null, }; if (editing) { await apiFetch(`/api/v1/fasteners/${fastener!.id}`, { method: "PATCH", body: JSON.stringify(payload), }); } else { await apiFetch(`/api/v1/projects/${projectId}/fasteners`, { method: "POST", body: JSON.stringify(payload), }); } onSaved(); } catch (err) { setError(err instanceof ApiClientError ? err.message : "Save failed"); setBusy(false); } } async function remove() { if (!fastener || !onDeleted) return; if (!confirm(`Delete fastener ${fastener.partNumber}?`)) return; setBusy(true); setError(null); try { await apiFetch(`/api/v1/fasteners/${fastener.id}`, { method: "DELETE" }); onDeleted(); } catch (err) { setError(err instanceof ApiClientError ? err.message : "Delete failed"); setBusy(false); } } return ( {editing && onDeleted ? ( ) : null}
} >
setPartNumber(e.target.value)} required autoFocus /> setDescription(e.target.value)} required />
setQty(e.target.value)} required /> setUnitCost(e.target.value)} />
setSupplier(e.target.value)} />