From 57d5177c70128dba32908def73737d27178bb5bb Mon Sep 17 00:00:00 2001 From: makearmy Date: Sat, 27 Sep 2025 15:12:08 -0400 Subject: [PATCH] added rig viewer to portal --- app/my/rigs/RigBuilderClient.tsx | 539 ------------------------------- app/my/rigs/page.tsx | 22 -- app/portal/rigs/page.tsx | 13 +- app/rigs/RigBuilderClient.tsx | 140 ++++++++ app/rigs/page.tsx | 14 + 5 files changed, 161 insertions(+), 567 deletions(-) delete mode 100644 app/my/rigs/RigBuilderClient.tsx delete mode 100644 app/my/rigs/page.tsx create mode 100644 app/rigs/RigBuilderClient.tsx create mode 100644 app/rigs/page.tsx diff --git a/app/my/rigs/RigBuilderClient.tsx b/app/my/rigs/RigBuilderClient.tsx deleted file mode 100644 index 03f074ad..00000000 --- a/app/my/rigs/RigBuilderClient.tsx +++ /dev/null @@ -1,539 +0,0 @@ -// app/my/rigs/RigBuilderClient.tsx -"use client"; - -import { useEffect, useMemo, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; - -let _redirectingDueToAuth = false; -function handleAuthError(err: any): boolean { - const status = (err as any)?.status; - const code = (err as any)?.code; - if (status === 401 || code === "TOKEN_EXPIRED") { - if (_redirectingDueToAuth) return true; - _redirectingDueToAuth = true; - - const here = window.location.pathname + window.location.search; - const onSignIn = window.location.pathname.startsWith("/auth"); - const next = encodeURIComponent(here); - window.location.replace(onSignIn ? `/auth/sign-in` : `/auth/sign-in?next=${next}`); - return true; - } - return false; -} - -import { useToast } from "@/hooks/use-toast"; - -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Button } from "@/components/ui/button"; -import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, -} from "@/components/ui/select"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; - -// ───────────────────────────────────────────────────────────── -// Types -// ───────────────────────────────────────────────────────────── - -type Option = { id: string | number; name: string }; -type RigType = { id: number | string; name: "fiber" | "uv" | "co2_galvo" | "co2_gantry" | string }; - -type RigRow = { - id: number; - name: string; - rig_type: number | string | null; - rig_type_name?: string; -}; - -// ───────────────────────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────────────────────── - -const SETTINGS_TARGET_MAP: Record = { - fiber: "settings_fiber", - co2_gantry: "settings_co2gan", - co2_galvo: "settings_co2gal", - uv: "settings_uv", -}; - -async function apiJson(url: string, init?: RequestInit): Promise { - const res = await fetch(url, { - ...init, - headers: { "Content-Type": "application/json", ...(init?.headers || {}) }, - cache: "no-store", - credentials: "include", - }); - const txt = await res.text().catch(() => ""); - if (res.ok) { - try { return JSON.parse(txt) as T; } catch { return undefined as T; } - } - let body: any = undefined; - try { body = JSON.parse(txt); } catch {} - if (body && typeof body.error === "string") { - try { body = JSON.parse(body.error); } catch {} - } - const err: any = new Error(`HTTP ${res.status} for ${url}`); - err.status = res.status; - err.code = body?.errors?.[0]?.extensions?.code || body?.code; - err.body = body ?? txt; - throw err; -} - -const schema = z.object({ - name: z.string().min(1, "Name is required"), - rig_type: z.string().min(1, "Pick a rig type"), - laser_source: z.string().optional().nullable(), - laser_software: 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(), - notes: z.string().optional().nullable(), -}); - -type FormValues = z.infer; - -// ───────────────────────────────────────────────────────────── -// Component -// ───────────────────────────────────────────────────────────── - -export default function RigBuilderClient() { - const { toast } = useToast(); - - // Lists - const [rigTypes, setRigTypes] = useState([]); - const [rigs, setRigs] = useState([]); - - // Options that depend on rig type - const [sourceOpts, setSourceOpts] = useState([]); - const [softwareOpts, setSoftwareOpts] = useState([]); - const [scanLensOpts, setScanLensOpts] = useState([]); - const [focusLensOpts, setFocusLensOpts] = useState([]); - - // Load laser software list once (independent of rig type) - useEffect(() => { - (async () => { - try { - const swJson = await apiJson<{ data: Option[] }>(`/api/options/laser_software`); - const sw = Array.isArray(swJson?.data) ? swJson.data : []; - setSoftwareOpts(sw); - } catch (e: any) { - if (!handleAuthError(e)) { - console.error("[laser_software] load failed:", e); - setSoftwareOpts([]); - } - } - })(); - }, []); - - // Form - const { - register, - handleSubmit, - reset, - setValue, - watch, - formState: { isSubmitting }, - } = useForm({ - resolver: zodResolver(schema), - defaultValues: { - name: "My Fiber #1", - rig_type: "", - laser_source: null, - laser_software: null, - laser_focus_lens: null, - laser_scan_lens: null, - laser_scan_lens_apt: null, - laser_scan_lens_exp: null, - notes: "", - }, - }); - - const rigTypeVal = watch("rig_type"); - const settingsTarget = SETTINGS_TARGET_MAP[rigTypeVal ?? ""] ?? ""; - const isGantry = rigTypeVal === "co2_gantry"; - const isScan = rigTypeVal === "fiber" || rigTypeVal === "uv" || rigTypeVal === "co2_galvo"; - - // Initial loads (rig types + existing rigs) - useEffect(() => { - (async () => { - try { - const [typesRes, rigsRes] = await Promise.all([ - apiJson<{ data: { id: number | string; name: string }[] }>(`/api/options/user_rig_type`), - apiJson<{ data: RigRow[] }>(`/api/my/rigs`), - ]); - - const mappedTypes: RigType[] = (typesRes?.data ?? []).map((t) => ({ - id: t.id, - name: t.name as any, - })); - setRigTypes(mappedTypes); - setRigs(rigsRes.data ?? []); - } catch (e: any) { - if (!handleAuthError(e)) { - console.warn("[rigs] initial load failed:", e?.message || e); - toast({ - title: "Failed to load", - description: "Could not load your rigs or rig types.", - variant: "destructive", - }); - } - } - })(); - }, [toast]); - - // Load options that depend on rig type - useEffect(() => { - // when rig type changes, clear type-specific fields - setValue("laser_focus_lens", null); - setValue("laser_scan_lens", null); - setValue("laser_scan_lens_apt", null); - setValue("laser_scan_lens_exp", null); - - if (!settingsTarget) { - setSourceOpts([]); - setScanLensOpts([]); - setFocusLensOpts([]); - return; - } - - (async () => { - // LASER sources by target (matches settings_* targets), fallback to no target - try { - const withTarget = await apiJson<{ data: Option[] }>( - `/api/options/laser_source?target=${encodeURIComponent(settingsTarget)}` - ); - let list = withTarget?.data ?? []; - if (!list.length) { - const fallback = await apiJson<{ data: Option[] }>(`/api/options/laser_source`); - list = fallback?.data ?? []; - } - setSourceOpts(list); - } catch (e: any) { - if (!handleAuthError(e)) console.error("[laser_source] load failed:", e); - setSourceOpts([]); - } - - if (isScan) { - try { - const lensesJson = await apiJson<{ data: Option[] }>( - `/api/options/lens?target=${encodeURIComponent(settingsTarget)}` - ); - setScanLensOpts(lensesJson.data ?? []); - } catch (e: any) { - if (!handleAuthError(e)) console.error("[scan_lens] load failed:", e); - setScanLensOpts([]); - } - } else { - setScanLensOpts([]); - } - - if (isGantry) { - try { - const focusJson = await apiJson<{ data: Option[] }>(`/api/options/laser_focus_lens`); - setFocusLensOpts(focusJson.data ?? []); - } catch (e: any) { - if (!handleAuthError(e)) console.error("[focus_lens] load failed:", e); - setFocusLensOpts([]); - } - } else { - setFocusLensOpts([]); - } - })(); - }, [settingsTarget, isScan, isGantry, setValue]); - - async function onSubmit(values: FormValues) { - try { - const payload = { - name: values.name, - // Your select uses the slug (name) as value; map it back to id for save: - rig_type: - rigTypes.find((t) => String(t.name) === String(values.rig_type))?.id ?? - values.rig_type, - laser_source: values.laser_source || null, - laser_software: values.laser_software || null, - laser_focus_lens: isGantry ? values.laser_focus_lens || null : null, - laser_scan_lens: isScan ? values.laser_scan_lens || null : null, - laser_scan_lens_apt: isScan ? values.laser_scan_lens_apt || null : null, - laser_scan_lens_exp: isScan ? values.laser_scan_lens_exp || null : null, - notes: values.notes || null, - }; - - await apiJson(`/api/my/rigs`, { - method: "POST", - body: JSON.stringify(payload), - }); - - toast({ title: "Rig saved", description: "Your rig was added." }); - - const rigsRes = await apiJson<{ data: RigRow[] }>(`/api/my/rigs`); - setRigs(rigsRes.data ?? []); - - reset({ - name: "", - rig_type: values.rig_type, - laser_source: null, - laser_software: null, - laser_focus_lens: null, - laser_scan_lens: null, - laser_scan_lens_apt: null, - laser_scan_lens_exp: null, - notes: "", - }); - } catch (e: any) { - if (handleAuthError(e)) return; - const message = (() => { - try { - const j = typeof e?.body === "object" ? e.body : JSON.parse(e?.message || "{}"); - if (j?.errors) { - const first = j.errors[0]; - return `${first?.extensions?.code || "API"}: ${first?.message || "Failed"}`; - } - } catch {} - return e?.message || "Failed to save rig"; - })(); - - toast({ - title: "Failed to save rig", - description: message, - variant: "destructive", - }); - } - } - - async function deleteRig(id: number) { - if (!confirm("Delete this rig?")) return; - try { - await apiJson(`/api/my/rigs/${id}`, { method: "DELETE" }); - setRigs((prev) => prev.filter((r) => r.id !== id)); - } catch (e: any) { - if (handleAuthError(e)) return; - toast({ - title: "Delete failed", - description: e?.message || "Could not delete rig.", - variant: "destructive", - }); - } - } - - const rigTypeItems = useMemo( - () => - rigTypes.map((t) => ({ - value: String(t.name), // using slug as value per your current pattern - label: String(t.name).replaceAll("_", " "), - })), - [rigTypes] - ); - - return ( -
- - - New Rig - - -
- {/* Name */} -
- - -
- - {/* Rig type */} -
- - -
- - {/* LASER Source (optional) */} -
- - -
- - {/* LASER Software (optional) */} -
- - -
- - {/* Notes */} -
- -