user_rigs collection update
This commit is contained in:
parent
344e0fcafb
commit
ae1486636e
2 changed files with 231 additions and 396 deletions
|
|
@ -1,76 +1,48 @@
|
|||
// app/api/my/rigs/[id]/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { directusFetch } from "@/lib/directus";
|
||||
|
||||
const BASE = process.env.DIRECTUS_URL!;
|
||||
if (!BASE) console.warn("[my/rigs/:id] Missing DIRECTUS_URL");
|
||||
const BASE_COLLECTION = "user_rigs";
|
||||
|
||||
function bearerFromCookies() {
|
||||
const at = cookies().get("ma_at")?.value;
|
||||
async function bearerFromCookies() {
|
||||
// In Next 15, types may represent `cookies()` as async—await it to satisfy TS.
|
||||
const store = await cookies();
|
||||
const at = store.get("ma_at")?.value;
|
||||
if (!at) throw new Error("Not authenticated");
|
||||
return `Bearer ${at}`;
|
||||
}
|
||||
|
||||
async function df(path: string, init?: RequestInit) {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: bearerFromCookies(),
|
||||
"Content-Type": "application/json",
|
||||
...(init?.headers || {}),
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
const text = await res.text();
|
||||
let json: any = null;
|
||||
try { json = text ? JSON.parse(text) : null; } catch {}
|
||||
if (!res.ok) throw new Error(`Directus error ${res.status}: ${text || res.statusText}`);
|
||||
return json ?? {};
|
||||
}
|
||||
|
||||
export async function GET(_req: NextRequest, ctx: any) {
|
||||
export async function PATCH(req: NextRequest, ctx: { params: { id: string } }) {
|
||||
try {
|
||||
const id = ctx?.params?.id;
|
||||
const fields = [
|
||||
"id","name","rig_type","notes","meta",
|
||||
"laser_source.id","laser_source.make","laser_source.model",
|
||||
"laser_scan_lens.id","laser_scan_lens.field_size","laser_scan_lens.f_number",
|
||||
"laser_focus_lens.id","laser_focus_lens.name",
|
||||
"laser_scan_lens_apt.id","laser_scan_lens_apt.name",
|
||||
"laser_scan_lens_exp.id","laser_scan_lens_exp.multiplier",
|
||||
"laser_software.id","laser_software.name",
|
||||
"date_created","date_updated"
|
||||
].join(",");
|
||||
|
||||
const { data } = await df(`/items/rigs/${id}?fields=${encodeURIComponent(fields)}`);
|
||||
return NextResponse.json({ ok: true, data });
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || "Load failed";
|
||||
const code = msg.includes("Not authenticated") ? 401 : (msg.includes("404") ? 404 : 500);
|
||||
return NextResponse.json({ error: msg }, { status: code });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(req: NextRequest, ctx: any) {
|
||||
try {
|
||||
const id = ctx?.params?.id;
|
||||
const auth = await bearerFromCookies();
|
||||
const body = await req.json();
|
||||
const { data } = await df(`/items/rigs/${id}`, { method: "PATCH", body: JSON.stringify(body) });
|
||||
return NextResponse.json({ ok: true, id: data?.id ?? id });
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || "Update failed";
|
||||
return NextResponse.json({ error: msg }, { status: msg.includes("Not authenticated") ? 401 : 500 });
|
||||
|
||||
const data = await directusFetch<{ data: any }>(`/items/${BASE_COLLECTION}/${ctx.params.id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: auth, // overrides submit token in helper
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, data: data.data });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err?.message || "Update failed" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, ctx: any) {
|
||||
export async function DELETE(_req: NextRequest, ctx: { params: { id: string } }) {
|
||||
try {
|
||||
const id = ctx?.params?.id;
|
||||
await df(`/items/rigs/${id}`, { method: "DELETE" });
|
||||
const auth = await bearerFromCookies();
|
||||
|
||||
await directusFetch(`/items/${BASE_COLLECTION}/${ctx.params.id}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: auth },
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || "Delete failed";
|
||||
return NextResponse.json({ error: msg }, { status: msg.includes("Not authenticated") ? 401 : 500 });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err?.message || "Delete failed" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,35 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
// UI bits (shadcn-style)
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import SignOutButton from "@/components/SignOutButton"; // ⟵ default import
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Loader2, Plus, Save, Settings2, X } from "lucide-react";
|
||||
import { SignOutButton } from "@/components/SignOutButton";
|
||||
|
||||
// simple fetch helper
|
||||
async function jfetch<T = any>(url: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init?.headers || {}),
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || res.statusText);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
/** ---------- Types for option endpoints ---------- */
|
||||
type Opt = { id: string; label: string };
|
||||
|
||||
// Finder for options with free-text filter
|
||||
function useOptions(endpoint: string) {
|
||||
// --- schema (minimal; expand later) ---
|
||||
const RigSchema = z.object({
|
||||
name: z.string().min(2, "Give your rig a short name"),
|
||||
notes: z.string().optional(),
|
||||
laser_source: z.string().min(1, "Laser source is required"),
|
||||
laser_software: z.string().min(1, "Software is required"),
|
||||
laser_scan_lens: z.string().optional().nullable(),
|
||||
laser_focus_lens: z.string().optional().nullable(),
|
||||
laser_scan_lens_apt: z.string().optional().nullable(),
|
||||
laser_scan_lens_exp: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
type RigForm = z.infer<typeof RigSchema>;
|
||||
|
||||
function useOptions(path: string) {
|
||||
const [opts, setOpts] = useState<Opt[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [q, setQ] = useState("");
|
||||
|
|
@ -49,359 +37,234 @@ function useOptions(endpoint: string) {
|
|||
useEffect(() => {
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
const url = `/api/options/${endpoint}${endpoint.includes("?") ? "&" : "?"}q=${encodeURIComponent(q)}`;
|
||||
const url = `/api/options/${path}${path.includes("?") ? "&" : "?"}q=${encodeURIComponent(q)}`;
|
||||
fetch(url, { cache: "no-store" })
|
||||
.then((r) => r.json())
|
||||
.then((j) => {
|
||||
if (!alive) return;
|
||||
setOpts((j?.data as Opt[]) ?? []);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!alive) return;
|
||||
setOpts([]);
|
||||
})
|
||||
.finally(() => alive && setLoading(false));
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [endpoint, q]);
|
||||
.then((j) => { if (alive) setOpts((j?.data as Opt[]) ?? []); })
|
||||
.finally(() => { if (alive) setLoading(false); });
|
||||
return () => { alive = false; };
|
||||
}, [path, q]);
|
||||
|
||||
return { opts, loading, setQ };
|
||||
}
|
||||
|
||||
/** ---------- Form schema ---------- */
|
||||
const RigSchema = z.object({
|
||||
name: z.string().min(1, "Rig name required"),
|
||||
notes: z.string().optional(),
|
||||
// Relations (IDs)
|
||||
laser_source: z.string().min(1, "Laser source required"),
|
||||
laser_software: z.string().min(1, "Software required"),
|
||||
// One of scan lens (with optional aperture & expander) OR focus lens
|
||||
laser_scan_lens: z.string().optional(),
|
||||
laser_scan_lens_apt: z.string().optional(),
|
||||
laser_scan_lens_exp: z.string().optional(),
|
||||
laser_focus_lens: z.string().optional(),
|
||||
});
|
||||
|
||||
type RigForm = z.infer<typeof RigSchema>;
|
||||
|
||||
/** ---------- Little select with filter ---------- */
|
||||
function FilterableSelect({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
placeholder = "Select…",
|
||||
endpoint, // e.g. "laser_source?target=settings_fiber"
|
||||
function Select({
|
||||
label, value, onChange, options, placeholder = "—", loading,
|
||||
}: {
|
||||
label: string;
|
||||
value?: string;
|
||||
onValueChange: (v: string) => void;
|
||||
value?: string | null;
|
||||
onChange: (v: string) => void;
|
||||
options: Opt[];
|
||||
placeholder?: string;
|
||||
endpoint: string;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const { opts, loading, setQ } = useOptions(endpoint);
|
||||
const [filter, setFilter] = useState("");
|
||||
const filtered = useMemo(() => {
|
||||
if (!filter) return options;
|
||||
const f = filter.toLowerCase();
|
||||
return options.filter((o) => o.label.toLowerCase().includes(f));
|
||||
}, [options, filter]);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label>{label}</Label>
|
||||
<Input
|
||||
<div>
|
||||
<label className="block text-sm mb-1">{label}</label>
|
||||
<input
|
||||
className="w-full border rounded px-2 py-1 mb-1"
|
||||
placeholder="Type to filter…"
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
className="mb-1"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
<Select value={value} onValueChange={onValueChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loading ? "Loading…" : placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{opts.length === 0 && (
|
||||
<div className="px-2 py-1 text-sm text-muted-foreground">
|
||||
{loading ? "Loading…" : "No matches"}
|
||||
</div>
|
||||
)}
|
||||
{opts.map((o) => (
|
||||
<SelectItem key={o.id} value={o.id}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<select
|
||||
className="w-full border rounded px-2 py-1"
|
||||
value={value ?? ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
<option value="">{placeholder}{loading ? " (loading…)" : ""}</option>
|
||||
{filtered.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** ---------- Page ---------- */
|
||||
export default function MyRigsPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
const [loadingList, setLoadingList] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Which lens type UI is active: "scan" vs "focus"
|
||||
const [lensMode, setLensMode] = useState<"scan" | "focus">("scan");
|
||||
// options
|
||||
const srcs = useOptions("laser_source?target=settings_fiber"); // any; user can mix later
|
||||
const soft = useOptions("laser_software");
|
||||
const scan = useOptions("lens?target=settings_fiber"); // scan lens list
|
||||
const focus = useOptions("lens?target=settings_co2gan"); // focus lens list (gantry)
|
||||
|
||||
const form = useForm<RigForm>({
|
||||
const apt = useOptions("laser_scan_lens_apt"); // aperture list (if exists)
|
||||
const exp = useOptions("laser_scan_lens_exp"); // expander list (if exists)
|
||||
|
||||
const { handleSubmit, control, reset, formState: { isSubmitting } } = useForm<RigForm>({
|
||||
resolver: zodResolver(RigSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
notes: "",
|
||||
laser_source: "",
|
||||
laser_software: "",
|
||||
laser_scan_lens: "",
|
||||
laser_scan_lens_apt: "",
|
||||
laser_scan_lens_exp: "",
|
||||
laser_focus_lens: "",
|
||||
},
|
||||
defaultValues: {
|
||||
name: "",
|
||||
notes: "",
|
||||
laser_source: "",
|
||||
laser_software: "",
|
||||
laser_scan_lens: "",
|
||||
laser_focus_lens: "",
|
||||
laser_scan_lens_apt: "",
|
||||
laser_scan_lens_exp: "",
|
||||
},
|
||||
});
|
||||
|
||||
const sourceTarget = "settings_fiber"; // for now default fiber; could be dynamic later
|
||||
|
||||
// Options hooks
|
||||
const srcs = useOptions(`laser_source?target=${sourceTarget}`);
|
||||
const soft = useOptions("laser_software");
|
||||
const scanLens = useOptions(`lens?target=${sourceTarget}`); // F-theta
|
||||
const apertures = useOptions("laser_scan_lens_apt");
|
||||
const expanders = useOptions("laser_scan_lens_exp");
|
||||
const focusLens = useOptions("lens?target=settings_co2gan"); // focus lens for gantry
|
||||
|
||||
// Load my rigs
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
(async () => {
|
||||
try {
|
||||
setLoadingList(true);
|
||||
const res = await jfetch<{ data: any[] }>("/api/rigs/list");
|
||||
if (!alive) return;
|
||||
setList(res.data || []);
|
||||
} catch (e: any) {
|
||||
console.warn("[my/rigs] list error", e?.message);
|
||||
} finally {
|
||||
alive = false ? undefined : setLoadingList(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function onSave(data: RigForm) {
|
||||
async function onSubmit(values: RigForm) {
|
||||
try {
|
||||
setSaving(true);
|
||||
|
||||
// If in focus mode, ensure scan-lens fields are cleared and vice versa.
|
||||
const payload: any = { ...data };
|
||||
if (lensMode === "focus") {
|
||||
payload.laser_scan_lens = null;
|
||||
payload.laser_scan_lens_apt = null;
|
||||
payload.laser_scan_lens_exp = null;
|
||||
} else {
|
||||
payload.laser_focus_lens = null;
|
||||
}
|
||||
|
||||
const res = await jfetch<{ ok: boolean; id: string }>("/api/rigs/save", {
|
||||
const res = await fetch("/api/my/rigs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!res.ok) throw new Error(j?.error || "Failed to save rig");
|
||||
|
||||
toast({
|
||||
title: "Rig saved",
|
||||
description: "Your rig has been saved to your account.",
|
||||
});
|
||||
|
||||
// refresh list
|
||||
const updated = await jfetch<{ data: any[] }>("/api/rigs/list");
|
||||
setList(updated.data || []);
|
||||
|
||||
// clear form (optional)
|
||||
form.reset({
|
||||
name: "",
|
||||
notes: "",
|
||||
laser_source: "",
|
||||
laser_software: "",
|
||||
laser_scan_lens: "",
|
||||
laser_scan_lens_apt: "",
|
||||
laser_scan_lens_exp: "",
|
||||
laser_focus_lens: lensMode === "focus" ? "" : "",
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
title: "Save failed",
|
||||
description: e?.message || "Unknown error",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
toast({ title: "Rig saved", description: "Your rig has been saved to your account." });
|
||||
reset();
|
||||
} catch (err: any) {
|
||||
toast({ title: "Save failed", description: err?.message || "Unknown error", variant: "destructive" });
|
||||
}
|
||||
}
|
||||
|
||||
function LensModeToggle() {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={lensMode === "scan" ? "default" : "secondary"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setLensMode("scan")}
|
||||
>
|
||||
Scan Lens
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={lensMode === "focus" ? "default" : "secondary"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setLensMode("focus")}
|
||||
>
|
||||
Focus Lens
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto p-4 md:p-6 space-y-6">
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<h1 className="text-2xl font-bold">My Rigs</h1>
|
||||
<SignOutButton />
|
||||
<div className="max-w-4xl mx-auto p-4 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">My Rigs</h1>
|
||||
<p className="text-sm text-muted-foreground">Create and manage your laser rigs.</p>
|
||||
</div>
|
||||
<SignOutButton /> {/* top-right, unobtrusive */}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings2 className="w-5 h-5" />
|
||||
Build a Rig
|
||||
</CardTitle>
|
||||
<LensModeToggle />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSave)}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label>Rig Name *</Label>
|
||||
<Input placeholder="e.g., Shop Fiber #1 (20W)" {...form.register("name")} />
|
||||
{form.formState.errors.name && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.name.message}
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 border rounded p-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Rig Name *</label>
|
||||
<Input {...field} placeholder="e.g., Shop Fiber #1" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="laser_software"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label="Software *"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
options={soft.opts}
|
||||
loading={soft.loading}
|
||||
placeholder="Select software"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FilterableSelect
|
||||
label="Laser Source *"
|
||||
endpoint={`laser_source?target=${sourceTarget}`}
|
||||
value={form.watch("laser_source")}
|
||||
onValueChange={(v) => form.setValue("laser_source", v)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Controller
|
||||
name="laser_source"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label="Laser Source *"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
options={srcs.opts}
|
||||
loading={srcs.loading}
|
||||
placeholder="Select source"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FilterableSelect
|
||||
label="Software *"
|
||||
endpoint="laser_software"
|
||||
value={form.watch("laser_software")}
|
||||
onValueChange={(v) => form.setValue("laser_software", v)}
|
||||
<Controller
|
||||
name="laser_scan_lens"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label="Scan Lens"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
options={scan.opts}
|
||||
loading={scan.loading}
|
||||
placeholder="Select scan lens (galvo)"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{lensMode === "scan" ? (
|
||||
<>
|
||||
<FilterableSelect
|
||||
label="Scan Lens (F-theta)"
|
||||
endpoint={`lens?target=${sourceTarget}`}
|
||||
value={form.watch("laser_scan_lens")}
|
||||
onValueChange={(v) => form.setValue("laser_scan_lens", v)}
|
||||
/>
|
||||
<FilterableSelect
|
||||
label="Galvo Head Aperture"
|
||||
endpoint="laser_scan_lens_apt"
|
||||
value={form.watch("laser_scan_lens_apt")}
|
||||
onValueChange={(v) => form.setValue("laser_scan_lens_apt", v)}
|
||||
/>
|
||||
<FilterableSelect
|
||||
label="Beam Expander (multiplier)"
|
||||
endpoint="laser_scan_lens_exp"
|
||||
value={form.watch("laser_scan_lens_exp")}
|
||||
onValueChange={(v) => form.setValue("laser_scan_lens_exp", v)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FilterableSelect
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Controller
|
||||
name="laser_focus_lens"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label="Focus Lens"
|
||||
endpoint="lens?target=settings_co2gan"
|
||||
value={form.watch("laser_focus_lens")}
|
||||
onValueChange={(v) => form.setValue("laser_focus_lens", v)}
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
options={focus.opts}
|
||||
loading={focus.loading}
|
||||
placeholder="Select focus lens (gantry)"
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Focus lenses don’t use F-numbers; just pick the lens by name.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Label>Notes</Label>
|
||||
<Textarea rows={3} placeholder="Optional notes about this rig…" {...form.register("notes")} />
|
||||
/>
|
||||
<Controller
|
||||
name="laser_scan_lens_apt"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label="Scan Head Aperture"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
options={apt.opts}
|
||||
loading={apt.loading}
|
||||
placeholder="Select aperture (optional)"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 flex items-center justify-end gap-2">
|
||||
<Button type="submit" disabled={saving}>
|
||||
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />}
|
||||
Save Rig
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Controller
|
||||
name="laser_scan_lens_exp"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label="Beam Expander"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
options={exp.opts}
|
||||
loading={exp.loading}
|
||||
placeholder="Select expander (optional)"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="notes"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Notes</label>
|
||||
<Textarea rows={4} placeholder="Any rig notes…" {...field} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">Owner-only</Badge>
|
||||
<span className="text-sm text-muted-foreground">Rigs are private to your account.</span>
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Saving…" : "Save Rig"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Saved Rigs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loadingList ? (
|
||||
<div className="text-sm text-muted-foreground">Loading…</div>
|
||||
) : list.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">No rigs saved yet.</div>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
{list.map((r) => (
|
||||
<div key={r.id} className="border rounded p-3 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-medium">{r.name}</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Source: {r.laser_source_label || r.laser_source}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Software: {r.laser_software_label || r.laser_software}
|
||||
</div>
|
||||
{r.lens_mode === "scan" ? (
|
||||
<>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Scan Lens: {r.laser_scan_lens_label || r.laser_scan_lens}
|
||||
</div>
|
||||
{r.laser_scan_lens_apt_label && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Aperture: {r.laser_scan_lens_apt_label}
|
||||
</div>
|
||||
)}
|
||||
{r.laser_scan_lens_exp_label && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Expander: {r.laser_scan_lens_exp_label}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : r.laser_focus_lens ? (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Focus Lens: {r.laser_focus_lens_label || r.laser_focus_lens}
|
||||
</div>
|
||||
) : null}
|
||||
{r.notes && <div className="text-xs">{r.notes}</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue