makearmy-app/app/my/rigs/RigBuilderClient.tsx
2025-09-26 18:30:40 -04:00

369 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/my/rigs/RigBuilderClient.tsx
"use client";
import { useEffect, useMemo, useState } from "react";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useToast } from "@/hooks/use-toast";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
type RigType = { id: number | string; name: string };
export default function RigBuilderClient({ rigTypes }: { rigTypes: RigType[] }) {
const { toast } = useToast();
const FormSchema = z.object({
name: z.string().min(2, "Please enter a name"),
rig_type: z.string().min(1, "Choose a rig type"),
laser_source: z.string().optional().nullable(),
laser_focus_lens: z.string().optional().nullable(),
laser_scan_lens: z.string().optional().nullable(),
laser_scan_lens_apt: z.string().optional().nullable(),
laser_scan_lens_exp: z.string().optional().nullable(),
laser_software: z.string().optional().nullable(),
notes: z.string().optional().nullable(),
});
type FormValues = z.infer<typeof FormSchema>;
const form = useForm<FormValues>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: "",
rig_type: "",
notes: "",
},
});
const selectedTypeName = useMemo(() => {
const v = form.watch("rig_type");
return rigTypes.find((r) => String(r.id) === String(v))?.name ?? "";
}, [form, rigTypes]);
// Option lists (pulled from your existing endpoints)
const [laserSources, setLaserSources] = useState<{ id: string; label: string }[]>([]);
const [scanLenses, setScanLenses] = useState<{ id: string; label: string }[]>([]);
const [focusLenses, setFocusLenses] = useState<{ id: string; label: string }[]>([]);
const [softwares, setSoftwares] = useState<{ id: string; label: string }[]>([]);
useEffect(() => {
(async () => {
try {
const ls = await fetch("/api/options/laser_source").then((r) => r.json());
setLaserSources(ls?.data ?? []);
} catch {}
try {
const sl = await fetch("/api/options/lens?target=settings_fiber").then((r) => r.json());
setScanLenses(sl?.data ?? []);
} catch {}
try {
const fl = await fetch("/api/options/repeater-choices?key=laser_focus_lens").then((r) =>
r.json()
);
setFocusLenses(fl?.data ?? []);
} catch {}
try {
const sw = await fetch("/api/options/repeater-choices?key=laser_software").then((r) =>
r.json()
);
setSoftwares(sw?.data ?? []);
} catch {}
})();
}, []);
const coerceId = (v: unknown) => {
if (typeof v === "number") return v;
if (typeof v === "string" && v !== "") {
const n = Number(v);
return Number.isFinite(n) ? n : v;
}
return v ?? null;
};
async function onSubmit(values: FormValues) {
try {
const payload = {
name: values.name,
rig_type: coerceId(values.rig_type), // IMPORTANT: send ID
laser_source: values.laser_source ? coerceId(values.laser_source) : null,
laser_focus_lens:
selectedTypeName === "co2_gantry" && values.laser_focus_lens
? coerceId(values.laser_focus_lens)
: null,
laser_scan_lens:
selectedTypeName !== "co2_gantry" && values.laser_scan_lens
? coerceId(values.laser_scan_lens)
: null,
laser_scan_lens_apt:
selectedTypeName !== "co2_gantry" && values.laser_scan_lens_apt
? values.laser_scan_lens_apt
: null,
laser_scan_lens_exp:
selectedTypeName !== "co2_gantry" && values.laser_scan_lens_exp
? values.laser_scan_lens_exp
: null,
laser_software: values.laser_software ? coerceId(values.laser_software) : null,
notes: values.notes ?? null,
};
const res = await fetch("/api/my/rigs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const json = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(json?.error || json?.errors?.[0]?.message || res.statusText);
}
toast({ title: "Saved!", description: `Rig created (id: ${json?.data?.id ?? "?"}).` });
form.reset({ name: "", rig_type: "", notes: "" });
document.dispatchEvent(new CustomEvent("rigs:refresh"));
} catch (err: any) {
toast({
title: "Failed to save rig",
description: String(err?.message || err),
variant: "destructive",
});
}
}
return (
<>
<div className="rounded-xl border bg-card p-4 md:p-6 space-y-4">
<div className="flex items-center gap-2">
<h2 className="text-lg font-medium">New Rig</h2>
{selectedTypeName ? <Badge variant="secondary">{selectedTypeName}</Badge> : null}
</div>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-cols-1 md:grid-cols-2 gap-4"
>
{/* Name */}
<div className="space-y-2">
<label className="text-sm font-medium">Name</label>
<Input placeholder="My Fiber #1" {...form.register("name")} />
{form.formState.errors.name ? (
<p className="text-xs text-red-500">{form.formState.errors.name.message}</p>
) : null}
</div>
{/* Rig Type (ID values) */}
<div className="space-y-2">
<label className="text-sm font-medium">Rig Type</label>
<Select
value={form.watch("rig_type")}
onValueChange={(v) => form.setValue("rig_type", v, { shouldValidate: true })}
>
<SelectTrigger>
<SelectValue placeholder="Choose a rig type" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{rigTypes.map((rt) => (
<SelectItem key={rt.id} value={String(rt.id)}>
{rt.name}
</SelectItem>
))}
</SelectContent>
</Select>
{form.formState.errors.rig_type ? (
<p className="text-xs text-red-500">{form.formState.errors.rig_type.message}</p>
) : null}
</div>
{/* Source */}
<div className="space-y-2">
<label className="text-sm font-medium">LASER Source</label>
<Select
value={form.watch("laser_source") ?? ""}
onValueChange={(v) => form.setValue("laser_source", v)}
>
<SelectTrigger>
<SelectValue placeholder="Optional" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{laserSources.map((o) => (
<SelectItem key={o.id} value={String(o.id)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Software */}
<div className="space-y-2">
<label className="text-sm font-medium">LASER Software</label>
<Select
value={form.watch("laser_software") ?? ""}
onValueChange={(v) => form.setValue("laser_software", v)}
>
<SelectTrigger>
<SelectValue placeholder="Optional" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{softwares.map((o) => (
<SelectItem key={o.id} value={String(o.id)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Focus lens only for co2_gantry */}
{selectedTypeName === "co2_gantry" && (
<div className="space-y-2">
<label className="text-sm font-medium">LASER Focus Lens</label>
<Select
value={form.watch("laser_focus_lens") ?? ""}
onValueChange={(v) => form.setValue("laser_focus_lens", v)}
>
<SelectTrigger>
<SelectValue placeholder="Select a focus lens" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{focusLenses.map((o) => (
<SelectItem key={o.id} value={String(o.id)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* Scan lens fields for fiber/uv/co2_galvo */}
{selectedTypeName && selectedTypeName !== "co2_gantry" && (
<>
<div className="space-y-2">
<label className="text-sm font-medium">LASER Scan Lens</label>
<Select
value={form.watch("laser_scan_lens") ?? ""}
onValueChange={(v) => form.setValue("laser_scan_lens", v)}
>
<SelectTrigger>
<SelectValue placeholder="Select a scan lens" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{scanLenses.map((o) => (
<SelectItem key={o.id} value={String(o.id)}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Scan Head Aperture (optional)</label>
<Input placeholder="e.g. 10 mm" {...form.register("laser_scan_lens_apt")} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Beam Expander (optional)</label>
<Input placeholder="e.g. 2×" {...form.register("laser_scan_lens_exp")} />
</div>
</>
)}
<div className="md:col-span-2 space-y-2">
<label className="text-sm font-medium">Notes</label>
<Textarea rows={4} placeholder="Optional notes…" {...form.register("notes")} />
</div>
<div className="md:col-span-2">
<Button type="submit" className="w-full md:w-auto">
Save Rig
</Button>
</div>
</form>
</div>
<RigList />
</>
);
}
/* Simple list that refreshes after create */
function RigList() {
const { toast } = useToast();
const [items, setItems] = useState<any[]>([]);
async function fetchRigs() {
try {
const res = await fetch("/api/my/rigs", { cache: "no-store" });
const json = await res.json();
setItems(json?.data ?? []);
} catch (e: any) {
toast({
title: "Failed to load rigs",
description: String(e?.message || e),
variant: "destructive",
});
}
}
useEffect(() => {
fetchRigs();
const onRefresh = () => fetchRigs();
document.addEventListener("rigs:refresh", onRefresh);
return () => document.removeEventListener("rigs:refresh", onRefresh);
}, []);
if (!items.length) {
return <p className="text-sm text-muted-foreground">No rigs yet.</p>;
}
return (
<div className="space-y-3 mt-6">
<h3 className="text-base font-medium">Your Rigs</h3>
<ul className="space-y-2">
{items.map((r) => (
<li
key={r.id}
className="flex items-center justify-between rounded-md border px-3 py-2"
>
<div className="flex items-center gap-2">
<span className="font-medium">{r.name}</span>
{r?.rig_type_name ? <Badge variant="secondary">{r.rig_type_name}</Badge> : null}
</div>
<form
onSubmit={async (e) => {
e.preventDefault();
try {
const res = await fetch(`/api/my/rigs/${r.id}`, { method: "DELETE" });
const j = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(j?.error || res.statusText);
document.dispatchEvent(new CustomEvent("rigs:refresh"));
} catch (err: any) {
toast({
title: "Delete failed",
description: String(err?.message || err),
variant: "destructive",
});
}
}}
>
<Button type="submit" variant="outline" size="sm">
Delete
</Button>
</form>
</li>
))}
</ul>
</div>
);
}