"use client"; import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; type Opt = { id: string | number; label: string }; const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); // helper: render multipliers like "06x", "8x", "1.5x"; avoid double "x" function formatMultiplier(raw: any) { const s = String(raw ?? "").trim(); if (!s) return "—"; if (/x$/i.test(s)) return s; // already has x // pure number? if (/^\d+(\.\d+)?$/.test(s)) { if (/^\d$/.test(s)) return `0${s}x`; return `${s}x`; } // otherwise, extract first number if present const m = s.match(/(\d+(?:\.\d+)?)/); if (m) { const n = m[1]; if (/^\d$/.test(n)) return `0${n}x`; return `${n}x`; } return `${s}x`; } // tiny helper to ensure we always iterate an array function toArray(v: any): T[] { return Array.isArray(v) ? (v as T[]) : []; } function useOptions( kind: "laser_software" | "laser_source" | "lens" | "scan_lens_apt" | "scan_lens_exp", targetKey?: string ) { const [opts, setOpts] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { let alive = true; setLoading(true); (async () => { try { let url = ""; // default normalize let normalize = (rows: any[]): Opt[] => rows.map((r) => ({ id: String(r.id ?? r.submission_id ?? ""), label: String(r.name ?? r.label ?? r.title ?? r.model ?? r.id ?? ""), })); if (kind === "laser_software") { url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`; } else if (kind === "laser_source") { // fetch all sources; client filter by nm band from targetKey url = `${API}/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model`; const parseNum = (v: any) => { if (v == null) return null; const m = String(v).match(/(\d+(\.\d+)?)/); return m ? Number(m[1]) : null; }; const nmRange = (t?: string | null): [number, number] | null => { if (!t) return null; const s = t.toLowerCase(); if (s.includes("fiber")) return [1000, 9000]; if (s.includes("uv")) return [300, 400]; if (s.includes("gantry") || s.includes("co2 gantry") || s.includes("co₂ gantry")) return [10000, 11000]; if (s.includes("galvo") || s.includes("co2 galvo") || s.includes("co₂ galvo")) return [10000, 11000]; return null; }; const range = nmRange(targetKey); normalize = (rows) => { const filtered = range ? rows.filter((r: any) => { const nm = parseNum(r.nm); return nm != null && nm >= range[0] && nm <= range[1]; }) : rows; return filtered.map((r: any) => ({ id: String(r.submission_id ?? ""), label: [r.make, r.model].filter(Boolean).join(" ") || String(r.submission_id ?? ""), })); }; } else if (kind === "lens") { if (targetKey && targetKey.toLowerCase().includes("gantry")) { url = `${API}/items/laser_focus_lens?fields=id,name&limit=1000&sort=name`; } else { url = `${API}/items/laser_scan_lens?fields=id,field_size,focal_length&limit=1000`; normalize = (rows) => { const toNum = (v: any) => { const m = String(v ?? "").match(/-?\d+(\.\d+)?/); return m ? parseFloat(m[0]) : Number.POSITIVE_INFINITY; }; return [...rows] .sort((a, b) => toNum(a.focal_length) - toNum(b.focal_length)) .map((r) => ({ id: String(r.id ?? ""), label: [r.field_size && `${r.field_size} mm`, r.focal_length && `${r.focal_length} mm`] .filter(Boolean) .join(" — ") || String(r.id ?? ""), })); }; } } else if (kind === "scan_lens_apt") { url = `${API}/items/laser_scan_lens_apt?fields=id,name&limit=1000&sort=name`; } else if (kind === "scan_lens_exp") { url = `${API}/items/laser_scan_lens_exp?fields=id,name&limit=1000&sort=name`; normalize = (rows) => rows.map((r: any) => ({ id: String(r.id ?? ""), label: formatMultiplier(r.name ?? r.label ?? r.id ?? ""), })); } const res = await fetch(url, { credentials: "include", cache: "no-store" }); if (!res.ok) throw new Error(`HTTP ${res.status} for ${kind}`); const json = await res.json().catch(() => ({} as any)); const rows = toArray(json?.data); let mapped: Opt[] = []; try { mapped = toArray(normalize(rows)); } catch { // Safe fallback: minimal id/label mapping to prevent render crashes mapped = rows.map((r: any) => ({ id: String(r?.id ?? r?.submission_id ?? ""), label: String(r?.name ?? r?.label ?? r?.title ?? r?.model ?? r?.id ?? ""), })); } if (alive) setOpts(mapped); } catch (_err) { if (alive) setOpts([]); // swallow errors → empty list; form still renders } finally { if (alive) setLoading(false); } })(); return () => { alive = false; }; }, [kind, targetKey]); return { opts, loading }; } export default function RigBuilderClient({ rigTypes }: { rigTypes: Opt[] }) { const router = useRouter(); const [name, setName] = useState(""); const [notes, setNotes] = useState(""); const [rigType, setRigType] = useState(""); const [laserSource, setLaserSource] = useState(""); const [scanLens, setScanLens] = useState(""); const [scanLensApt, setScanLensApt] = useState(""); // galvo-only const [scanLensExp, setScanLensExp] = useState(""); // galvo-only const [focusLens, setFocusLens] = useState(""); const [software, setSoftware] = useState(""); // rigTypes come from the SERVER as props. const targetKey = useMemo(() => { const rt = rigTypes.find((o) => String(o.id) === String(rigType))?.label || ""; return rt; }, [rigTypes, rigType]); const sources = useOptions("laser_source", targetKey); const lens = useOptions("lens", targetKey); const lensApt = useOptions("scan_lens_apt"); const lensExp = useOptions("scan_lens_exp"); const softwares = useOptions("laser_software"); const isGantry = (targetKey || "").toLowerCase().includes("gantry"); async function onSubmit(e: React.FormEvent) { e.preventDefault(); const body: any = { name, rig_type: rigType ? Number(rigType) : null, laser_source: laserSource ? Number(laserSource) : null, notes: notes || "", laser_software: software ? Number(software) : null, }; if (isGantry) { body.laser_focus_lens = focusLens ? Number(focusLens) : null; body.laser_scan_lens = null; body.laser_scan_lens_apt = null; body.laser_scan_lens_exp = null; } else { body.laser_scan_lens = scanLens ? Number(scanLens) : null; body.laser_focus_lens = null; body.laser_scan_lens_apt = scanLensApt ? Number(scanLensApt) : null; body.laser_scan_lens_exp = scanLensExp ? Number(scanLensExp) : null; } const res = await fetch("/api/rigs", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(body), }); const j = await res.json().catch(() => ({})); if (!res.ok) { alert(j?.error || "Failed to create rig"); return; } router.replace("/portal/rigs?t=my", { scroll: false }); router.refresh(); } return (
setName(e.target.value)} required />
{/* Lens (focus for gantry, scan + apt/exp for others) */} {isGantry ? (
) : ( <>
)}