diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d9be7c4..07fd7ac 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,9 @@ "Bash(npm install *)", "Bash(DATABASE_URL=\"file:./dev.db\" npx prisma migrate dev --name init --skip-seed)", "Bash(DATABASE_URL=\"file:./dev.db\" APP_SECRET=\"dev-only-change-me-dev-only-change-me-dev-only-change-me\" npx tsc --noEmit)", - "Bash(DATABASE_URL=\"file:./dev.db\" APP_SECRET=\"dev-only-change-me-dev-only-change-me-dev-only-change-me\" npm run build)" + "Bash(DATABASE_URL=\"file:./dev.db\" APP_SECRET=\"dev-only-change-me-dev-only-change-me-dev-only-change-me\" npm run build)", + "Bash(npx tsc *)", + "Bash(npx next *)" ] } } diff --git a/app/admin/machines/MachinesClient.tsx b/app/admin/machines/MachinesClient.tsx new file mode 100644 index 0000000..52a0114 --- /dev/null +++ b/app/admin/machines/MachinesClient.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Badge, Button, Card, ErrorBanner, Field, Input, Modal, PageHeader, Select, Textarea } from "@/components/ui"; +import { apiFetch, ApiClientError } from "@/lib/client-api"; + +interface MachineRow { + id: string; + name: string; + kind: string; + location: string | null; + notes: string | null; + active: boolean; + createdAt: string; + updatedAt: string; +} + +const KINDS = ["NCT_PUNCH", "PRESS_BRAKE", "RIVET", "WELD", "LASER", "SHEAR", "ASSEMBLY", "OTHER"] as const; + +const KIND_LABEL: Record = { + NCT_PUNCH: "NCT punch", + PRESS_BRAKE: "Press brake", + RIVET: "Rivet", + WELD: "Weld", + LASER: "Laser", + SHEAR: "Shear", + ASSEMBLY: "Assembly", + OTHER: "Other", +}; + +export default function MachinesClient({ initial }: { initial: MachineRow[] }) { + const router = useRouter(); + const [edit, setEdit] = useState(null); + const [newOpen, setNewOpen] = useState(false); + + return ( +
+ setNewOpen(true)}>New machine} + /> + + + + + + + + + + + + + {initial.map((m) => ( + + + + + + + + ))} + {initial.length === 0 && ( + + + + )} + +
NameTypeLocationStatus
{m.name}{KIND_LABEL[m.kind] ?? m.kind}{m.location ?? "—"} + {m.active ? "active" : "inactive"} + + +
+ No machines yet. +
+
+ + {newOpen && setNewOpen(false)} onSaved={() => router.refresh()} />} + {edit && setEdit(null)} onSaved={() => router.refresh()} />} +
+ ); +} + +function MachineModal({ + machine, + onClose, + onSaved, +}: { + machine?: MachineRow; + onClose: () => void; + onSaved: () => void; +}) { + const editing = !!machine; + const [name, setName] = useState(machine?.name ?? ""); + const [kind, setKind] = useState(machine?.kind ?? "OTHER"); + const [location, setLocation] = useState(machine?.location ?? ""); + const [notes, setNotes] = useState(machine?.notes ?? ""); + const [active, setActive] = useState(machine?.active ?? true); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + async function submit(e: React.FormEvent) { + e.preventDefault(); + setBusy(true); + setError(null); + try { + if (editing) { + await apiFetch(`/api/v1/machines/${machine!.id}`, { + method: "PATCH", + body: JSON.stringify({ name, kind, location, notes, active }), + }); + } else { + await apiFetch("/api/v1/machines", { + method: "POST", + body: JSON.stringify({ name, kind, location, notes }), + }); + } + onSaved(); + onClose(); + } catch (err) { + setError(err instanceof ApiClientError ? err.message : "Save failed"); + } finally { + setBusy(false); + } + } + + async function deactivate() { + if (!machine || !confirm(`Deactivate ${machine.name}?`)) return; + setBusy(true); + setError(null); + try { + await apiFetch(`/api/v1/machines/${machine.id}`, { method: "DELETE" }); + onSaved(); + onClose(); + } catch (err) { + setError(err instanceof ApiClientError ? err.message : "Deactivate failed"); + setBusy(false); + } + } + + return ( + + {editing && machine!.active && ( + + )} +
+ + + + } + > +
+ + setName(e.target.value)} required maxLength={200} /> + + + + + + setLocation(e.target.value)} /> + + +