build failure fixes
This commit is contained in:
parent
384f0a4958
commit
a1d14fb6ba
2 changed files with 374 additions and 367 deletions
369
app/my/rigs/RigBuilderClient.tsx
Normal file
369
app/my/rigs/RigBuilderClient.tsx
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
// 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,27 +1,16 @@
|
|||
// app/my/rigs/page.tsx
|
||||
import { cookies } from "next/headers";
|
||||
import SignOutButton from "@/components/SignOutButton";
|
||||
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";
|
||||
import { z } from "zod";
|
||||
import RigBuilderClient from "./RigBuilderClient";
|
||||
|
||||
// Server-only: load rig types with the user's bearer
|
||||
const API_BASE = process.env.DIRECTUS_URL!;
|
||||
|
||||
type RigType = { id: number | string; name: "fiber" | "uv" | "co2_galvo" | "co2_gantry" | string };
|
||||
type RigType = { id: number | string; name: string };
|
||||
|
||||
async function loadRigTypes(): Promise<RigType[]> {
|
||||
const ck = await cookies();
|
||||
const at = ck.get("ma_at")?.value;
|
||||
|
||||
const headers: Record<string, string> = { Accept: "application/json" };
|
||||
if (at) headers.Authorization = `Bearer ${at}`;
|
||||
|
||||
|
|
@ -29,6 +18,7 @@ async function loadRigTypes(): Promise<RigType[]> {
|
|||
`${API_BASE}/items/user_rig_type?fields=id,name&sort=sort`,
|
||||
{ cache: "no-store", headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.warn("[my/rigs] failed to load rig types:", await res.text());
|
||||
return [];
|
||||
|
|
@ -37,16 +27,6 @@ async function loadRigTypes(): Promise<RigType[]> {
|
|||
return (json?.data ?? []) as RigType[];
|
||||
}
|
||||
|
||||
// (optional) helper while converting string -> number if numeric
|
||||
function coerceId(v: unknown) {
|
||||
if (typeof v === "number") return v;
|
||||
if (typeof v === "string") {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : v;
|
||||
}
|
||||
return v ?? null;
|
||||
}
|
||||
|
||||
export default async function MyRigsPage() {
|
||||
const rigTypes = await loadRigTypes();
|
||||
|
||||
|
|
@ -57,349 +37,7 @@ export default async function MyRigsPage() {
|
|||
<SignOutButton redirectTo="/auth/sign-in" />
|
||||
</header>
|
||||
|
||||
<RigBuilder rigTypes={rigTypes} />
|
||||
<RigList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────
|
||||
* Client: Form + interactions
|
||||
* ────────────────────────────────────────────────────────── */
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string().min(2, "Please enter a name"),
|
||||
rig_type: z.string().min(1, "Choose a rig type"), // we’ll coerce to number/uuid on submit
|
||||
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>;
|
||||
|
||||
function RigBuilder({ rigTypes }: { rigTypes: RigType[] }) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
rig_type: "",
|
||||
notes: "",
|
||||
},
|
||||
});
|
||||
|
||||
const selectedType = useMemo(() => {
|
||||
const v = form.watch("rig_type");
|
||||
return rigTypes.find((r) => String(r.id) === String(v))?.name ?? "";
|
||||
}, [form, rigTypes]);
|
||||
|
||||
// Options (client-side) – you already have these /api/options 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 }[]>([]);
|
||||
|
||||
// Fetch option lists (simple, debounced-less; tweak as desired)
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
// laser sources – all
|
||||
const ls = await fetch("/api/options/laser_source").then((r) => r.json());
|
||||
setLaserSources(ls?.data ?? []);
|
||||
} catch {}
|
||||
try {
|
||||
// scan lenses – list for all scan types
|
||||
const sl = await fetch("/api/options/lens?target=settings_fiber").then((r) => r.json());
|
||||
setScanLenses(sl?.data ?? []);
|
||||
} catch {}
|
||||
try {
|
||||
// focus lenses – gantry focuses
|
||||
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 {}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function onSubmit(values: FormValues) {
|
||||
try {
|
||||
const payload = {
|
||||
name: values.name,
|
||||
rig_type: coerceId(values.rig_type),
|
||||
laser_source: values.laser_source ? coerceId(values.laser_source) : null,
|
||||
laser_focus_lens:
|
||||
selectedType === "co2_gantry" && values.laser_focus_lens
|
||||
? coerceId(values.laser_focus_lens)
|
||||
: null,
|
||||
laser_scan_lens:
|
||||
selectedType !== "co2_gantry" && values.laser_scan_lens
|
||||
? coerceId(values.laser_scan_lens)
|
||||
: null,
|
||||
laser_scan_lens_apt:
|
||||
selectedType !== "co2_gantry" && values.laser_scan_lens_apt
|
||||
? coerceId(values.laser_scan_lens_apt)
|
||||
: null,
|
||||
laser_scan_lens_exp:
|
||||
selectedType !== "co2_gantry" && values.laser_scan_lens_exp
|
||||
? coerceId(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: "" });
|
||||
// You might also trigger a refetch on the RigList (simple hacky way below)
|
||||
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>
|
||||
{selectedType ? <Badge variant="secondary">{selectedType}</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 (IDs) */}
|
||||
<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>
|
||||
|
||||
{/* Laser 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 */}
|
||||
{selectedType === "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 */}
|
||||
{selectedType && selectedType !== "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>
|
||||
);
|
||||
}
|
||||
|
||||
// Very small client list (refreshes when a rig is created)
|
||||
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">
|
||||
<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>
|
||||
<RigBuilderClient rigTypes={rigTypes} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue