2025-10-03 15:22:15 -04:00
|
|
|
|
// components/details/CO2GalvoDetail.tsx
|
2025-10-03 13:57:24 -04:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
2025-10-05 22:06:32 -04:00
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
2025-10-05 22:32:27 -04:00
|
|
|
|
import SettingsSubmit from "@/components/forms/SettingsSubmit";
|
2025-10-03 13:57:24 -04:00
|
|
|
|
|
|
|
|
|
|
type Rec = {
|
|
|
|
|
|
submission_id: string | number;
|
|
|
|
|
|
setting_title?: string | null;
|
|
|
|
|
|
setting_notes?: string | null;
|
2025-10-05 21:52:16 -04:00
|
|
|
|
|
2025-10-03 22:54:05 -04:00
|
|
|
|
photo?: { id?: string } | string | null;
|
|
|
|
|
|
screen?: { id?: string } | string | null;
|
2025-10-03 13:57:24 -04:00
|
|
|
|
|
2025-10-05 21:52:16 -04:00
|
|
|
|
// Material
|
2025-10-03 15:07:13 -04:00
|
|
|
|
mat?: { id?: string | number; name?: string | null } | null;
|
|
|
|
|
|
mat_coat?: { id?: string | number; name?: string | null } | null;
|
|
|
|
|
|
mat_color?: { id?: string | number; name?: string | null } | null;
|
|
|
|
|
|
mat_opacity?: { id?: string | number; opacity?: string | number | null } | null;
|
2025-10-03 13:57:24 -04:00
|
|
|
|
mat_thickness?: number | null;
|
|
|
|
|
|
|
2025-10-05 21:52:16 -04:00
|
|
|
|
// Rig & Optics
|
|
|
|
|
|
laser_soft?: { id?: string | number; name?: string | null } | string | number | null;
|
2025-10-03 19:11:14 -04:00
|
|
|
|
source?: { submission_id?: string | number; make?: string | null; model?: string | null; nm?: string | null } | null;
|
|
|
|
|
|
lens?: { id?: string | number; field_size?: string | number | null; focal_length?: string | number | null } | null;
|
2025-10-05 21:52:16 -04:00
|
|
|
|
focus?: number | null;
|
2025-10-03 13:57:24 -04:00
|
|
|
|
|
2025-10-05 21:52:16 -04:00
|
|
|
|
// CO₂ Galvo fixed lists
|
2025-10-05 17:09:39 -04:00
|
|
|
|
lens_conf?: { id?: string | number; name?: string | null } | null;
|
2025-10-05 22:38:40 -04:00
|
|
|
|
lens_apt?: { id?: string | number; name?: string | null } | string | number | null;
|
|
|
|
|
|
lens_exp?: { id?: string | number; name?: string | null } | string | number | null;
|
2025-10-05 17:09:39 -04:00
|
|
|
|
|
2025-10-05 17:45:09 -04:00
|
|
|
|
repeat_all?: number | null;
|
|
|
|
|
|
|
2025-10-05 21:52:16 -04:00
|
|
|
|
// Repeaters
|
2025-10-03 13:57:24 -04:00
|
|
|
|
fill_settings?: any[] | null;
|
|
|
|
|
|
line_settings?: any[] | null;
|
|
|
|
|
|
raster_settings?: any[] | null;
|
|
|
|
|
|
|
|
|
|
|
|
owner?: { id?: string | number; username?: string | null } | string | number | null;
|
|
|
|
|
|
uploader?: string | null;
|
2025-10-05 21:52:16 -04:00
|
|
|
|
|
2025-10-03 13:57:24 -04:00
|
|
|
|
last_modified_date?: string | null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-05 20:35:20 -04:00
|
|
|
|
export default function CO2GalvoDetail({ id, editable }: { id: string | number; editable?: boolean }) {
|
2025-10-05 22:06:32 -04:00
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const sp = useSearchParams();
|
2025-10-05 22:32:27 -04:00
|
|
|
|
const editMode = sp.get("edit") === "1";
|
2025-10-05 22:06:32 -04:00
|
|
|
|
|
2025-10-03 13:57:24 -04:00
|
|
|
|
const [rec, setRec] = useState<Rec | null>(null);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
|
|
|
2025-10-05 22:32:27 -04:00
|
|
|
|
// me id for owner-only edit
|
2025-10-05 22:06:32 -04:00
|
|
|
|
const [meId, setMeId] = useState<string | null>(null);
|
|
|
|
|
|
|
2025-10-05 21:52:16 -04:00
|
|
|
|
// Lightbox
|
|
|
|
|
|
const [viewerSrc, setViewerSrc] = useState<string | null>(null);
|
2025-10-03 17:59:26 -04:00
|
|
|
|
useEffect(() => {
|
2025-10-05 21:52:16 -04:00
|
|
|
|
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setViewerSrc(null);
|
|
|
|
|
|
if (viewerSrc) window.addEventListener("keydown", onKey);
|
|
|
|
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
|
|
|
|
}, [viewerSrc]);
|
|
|
|
|
|
|
|
|
|
|
|
const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
|
|
|
|
|
|
const fileUrl = (assetId?: string) => (assetId ? (API_BASE ? `${API_BASE}/assets/${assetId}` : `/api/dx/assets/${assetId}`) : "");
|
|
|
|
|
|
|
|
|
|
|
|
function ownerLabel(o: Rec["owner"]) {
|
|
|
|
|
|
if (!o) return "—";
|
|
|
|
|
|
if (typeof o === "string" || typeof o === "number") return String(o);
|
|
|
|
|
|
return o.username || String(o.id ?? "—");
|
|
|
|
|
|
}
|
|
|
|
|
|
const yesNo = (v: any) => (v ? "Yes" : "No");
|
2025-10-05 20:35:20 -04:00
|
|
|
|
|
2025-10-06 21:36:55 -04:00
|
|
|
|
// stringify possibly-object options for display
|
2025-10-05 22:38:40 -04:00
|
|
|
|
const optLabel = (v: any): string => {
|
|
|
|
|
|
if (v == null) return "—";
|
|
|
|
|
|
if (typeof v === "string" || typeof v === "number") return String(v);
|
|
|
|
|
|
if (typeof v === "object") return v.name ?? (v.id != null ? String(v.id) : "—");
|
|
|
|
|
|
return "—";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-05 22:06:32 -04:00
|
|
|
|
// fetch me id
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let alive = true;
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include" });
|
|
|
|
|
|
const t = await r.text();
|
|
|
|
|
|
const j = t ? JSON.parse(t) : null;
|
|
|
|
|
|
const idVal = j?.data?.id ?? j?.id ?? null;
|
|
|
|
|
|
if (alive) setMeId(idVal ? String(idVal) : null);
|
2025-10-05 22:32:27 -04:00
|
|
|
|
} catch { /* ignore */ }
|
2025-10-05 22:06:32 -04:00
|
|
|
|
})();
|
2025-10-05 22:32:27 -04:00
|
|
|
|
return () => { alive = false; };
|
2025-10-05 22:06:32 -04:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-10-05 20:35:20 -04:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
let dead = false;
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
setErr(null);
|
|
|
|
|
|
const fields = [
|
|
|
|
|
|
"submission_id",
|
|
|
|
|
|
"setting_title",
|
|
|
|
|
|
"setting_notes",
|
|
|
|
|
|
"photo.id",
|
|
|
|
|
|
"screen.id",
|
2025-10-05 21:52:16 -04:00
|
|
|
|
|
|
|
|
|
|
// Material
|
|
|
|
|
|
"mat.id",
|
|
|
|
|
|
"mat.name",
|
|
|
|
|
|
"mat_coat.id",
|
|
|
|
|
|
"mat_coat.name",
|
|
|
|
|
|
"mat_color.id",
|
|
|
|
|
|
"mat_color.name",
|
|
|
|
|
|
"mat_opacity.id",
|
|
|
|
|
|
"mat_opacity.opacity",
|
2025-10-05 20:35:20 -04:00
|
|
|
|
"mat_thickness",
|
2025-10-05 21:52:16 -04:00
|
|
|
|
|
|
|
|
|
|
// Rig & Optics
|
|
|
|
|
|
"laser_soft.id",
|
|
|
|
|
|
"laser_soft.name",
|
|
|
|
|
|
"source.submission_id",
|
|
|
|
|
|
"source.make",
|
|
|
|
|
|
"source.model",
|
|
|
|
|
|
"source.nm",
|
|
|
|
|
|
"lens.id",
|
|
|
|
|
|
"lens.field_size",
|
|
|
|
|
|
"lens.focal_length",
|
|
|
|
|
|
"lens_conf.id",
|
|
|
|
|
|
"lens_conf.name",
|
|
|
|
|
|
"lens_apt.id",
|
|
|
|
|
|
"lens_apt.name",
|
|
|
|
|
|
"lens_exp.id",
|
|
|
|
|
|
"lens_exp.name",
|
|
|
|
|
|
"focus",
|
|
|
|
|
|
"repeat_all",
|
|
|
|
|
|
|
|
|
|
|
|
// Repeaters
|
|
|
|
|
|
"fill_settings",
|
|
|
|
|
|
"line_settings",
|
|
|
|
|
|
"raster_settings",
|
|
|
|
|
|
|
|
|
|
|
|
// Meta
|
|
|
|
|
|
"owner.id",
|
|
|
|
|
|
"owner.username",
|
2025-10-05 20:35:20 -04:00
|
|
|
|
"uploader",
|
|
|
|
|
|
"last_modified_date",
|
|
|
|
|
|
].join(",");
|
|
|
|
|
|
|
2025-10-05 22:32:27 -04:00
|
|
|
|
const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&filter[submission_id][_eq]=${encodeURIComponent(
|
|
|
|
|
|
String(id)
|
|
|
|
|
|
)}&limit=1`;
|
2025-10-05 21:52:16 -04:00
|
|
|
|
|
2025-10-05 20:35:20 -04:00
|
|
|
|
const r = await fetch(url, { cache: "no-store", credentials: "include" });
|
2025-10-05 21:52:16 -04:00
|
|
|
|
const text = await r.text();
|
|
|
|
|
|
const j = text ? JSON.parse(text) : null;
|
|
|
|
|
|
if (!r.ok) throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`);
|
2025-10-05 20:35:20 -04:00
|
|
|
|
const row: Rec | null = Array.isArray(j?.data) ? j.data[0] || null : null;
|
|
|
|
|
|
if (!row) throw new Error("Setting not found.");
|
|
|
|
|
|
if (!dead) setRec(row);
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
if (!dead) setErr(e?.message || String(e));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (!dead) setLoading(false);
|
2025-10-05 17:09:39 -04:00
|
|
|
|
}
|
2025-10-05 20:35:20 -04:00
|
|
|
|
})();
|
2025-10-05 22:32:27 -04:00
|
|
|
|
return () => { dead = true; };
|
2025-10-05 21:52:16 -04:00
|
|
|
|
}, [id]);
|
2025-10-03 13:57:24 -04:00
|
|
|
|
|
2025-10-05 20:35:20 -04:00
|
|
|
|
if (loading) return <p className="p-6">Loading setting…</p>;
|
2025-10-05 21:52:16 -04:00
|
|
|
|
if (err)
|
2025-10-05 20:35:20 -04:00
|
|
|
|
return (
|
2025-10-05 21:52:16 -04:00
|
|
|
|
<div className="p-6">
|
|
|
|
|
|
<div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">{err}</div>
|
2025-10-05 20:35:20 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2025-10-05 21:52:16 -04:00
|
|
|
|
if (!rec) return <p className="p-6">Setting not found.</p>;
|
2025-10-03 22:54:05 -04:00
|
|
|
|
|
2025-10-05 20:35:20 -04:00
|
|
|
|
const photoId = typeof rec.photo === "object" ? rec.photo?.id : (rec.photo as any);
|
|
|
|
|
|
const screenId = typeof rec.screen === "object" ? rec.screen?.id : (rec.screen as any);
|
2025-10-05 21:52:16 -04:00
|
|
|
|
const photoSrc = fileUrl(photoId ? String(photoId) : "");
|
|
|
|
|
|
const screenSrc = fileUrl(screenId ? String(screenId) : "");
|
|
|
|
|
|
|
|
|
|
|
|
const softName = typeof rec.laser_soft === "object" ? rec.laser_soft?.name ?? "—" : "—";
|
|
|
|
|
|
const sourceText =
|
2025-10-05 22:32:27 -04:00
|
|
|
|
[rec.source?.make, rec.source?.model].filter(Boolean).join(" ") + (rec.source?.nm ? ` (${rec.source.nm})` : "");
|
2025-10-05 21:52:16 -04:00
|
|
|
|
|
2025-10-05 22:06:32 -04:00
|
|
|
|
const ownerId =
|
2025-10-05 22:32:27 -04:00
|
|
|
|
typeof rec.owner === "object" ? (rec.owner?.id != null ? String(rec.owner.id) : null) : rec.owner != null ? String(rec.owner) : null;
|
2025-10-05 22:06:32 -04:00
|
|
|
|
|
|
|
|
|
|
const isMine = meId && ownerId ? meId === ownerId : false;
|
|
|
|
|
|
|
2025-10-06 21:36:55 -04:00
|
|
|
|
// Small field renderer (label on top, value below)
|
2025-10-05 22:32:27 -04:00
|
|
|
|
const Field = ({ label, value, suffix }: { label: string; value: React.ReactNode | string | number | null | undefined; suffix?: string }) => {
|
|
|
|
|
|
const primitive =
|
|
|
|
|
|
typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
|
|
|
|
const isEmpty = value == null || value === "" || (typeof value === "number" && isNaN(value as number));
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
|
|
|
|
|
|
<div className="text-sm break-words">
|
|
|
|
|
|
{isEmpty ? (
|
|
|
|
|
|
"—"
|
|
|
|
|
|
) : primitive ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{String(value)}
|
|
|
|
|
|
{suffix ? <span className="opacity-70"> {suffix}</span> : null}
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
value
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-10-03 22:54:05 -04:00
|
|
|
|
|
2025-10-05 22:06:32 -04:00
|
|
|
|
const openEdit = () => {
|
|
|
|
|
|
const q = new URLSearchParams(sp.toString());
|
|
|
|
|
|
q.set("edit", "1");
|
|
|
|
|
|
router.replace(`?${q.toString()}`, { scroll: false });
|
|
|
|
|
|
};
|
2025-10-05 22:32:27 -04:00
|
|
|
|
const closeEdit = () => {
|
|
|
|
|
|
const q = new URLSearchParams(sp.toString());
|
|
|
|
|
|
q.delete("edit");
|
|
|
|
|
|
router.replace(`?${q.toString()}`, { scroll: false });
|
|
|
|
|
|
};
|
2025-10-05 22:06:32 -04:00
|
|
|
|
|
|
|
|
|
|
// Pretty labels
|
|
|
|
|
|
const TYPE_LABEL: Record<string, string> = {
|
|
|
|
|
|
uni: "UniDirectional",
|
|
|
|
|
|
bi: "BiDirectional",
|
|
|
|
|
|
offset: "Offset Fill",
|
|
|
|
|
|
};
|
|
|
|
|
|
const DITHER_LABEL = (v: string | undefined) => (v ? v.charAt(0).toUpperCase() + v.slice(1) : "—");
|
|
|
|
|
|
|
2025-10-05 22:38:40 -04:00
|
|
|
|
// ----- EDIT MODE -----
|
2025-10-05 22:32:27 -04:00
|
|
|
|
if (editMode && rec) {
|
|
|
|
|
|
const toId = (v: any) =>
|
|
|
|
|
|
v == null ? "" : typeof v === "object" ? (v.id ?? v.submission_id ?? "") : String(v);
|
|
|
|
|
|
|
|
|
|
|
|
const initialValues = {
|
|
|
|
|
|
submission_id: rec.submission_id,
|
|
|
|
|
|
setting_title: rec.setting_title ?? "",
|
|
|
|
|
|
setting_notes: rec.setting_notes ?? "",
|
|
|
|
|
|
photo: photoId ? String(photoId) : null,
|
|
|
|
|
|
screen: screenId ? String(screenId) : null,
|
|
|
|
|
|
// Material
|
|
|
|
|
|
mat: toId(rec.mat) || "",
|
|
|
|
|
|
mat_coat: toId(rec.mat_coat) || "",
|
|
|
|
|
|
mat_color: toId(rec.mat_color) || "",
|
|
|
|
|
|
mat_opacity: toId(rec.mat_opacity) || "",
|
|
|
|
|
|
mat_thickness: rec.mat_thickness ?? null,
|
|
|
|
|
|
// Rig & Optics
|
|
|
|
|
|
laser_soft: typeof rec.laser_soft === "object" ? String(rec.laser_soft?.id ?? "") : String(rec.laser_soft ?? "") || "",
|
|
|
|
|
|
source: rec.source && typeof rec.source === "object" ? String(rec.source.submission_id ?? "") : String(rec.source ?? "") || "",
|
|
|
|
|
|
lens: toId(rec.lens) || "",
|
|
|
|
|
|
focus: rec.focus ?? null,
|
|
|
|
|
|
// CO2 triplet
|
|
|
|
|
|
lens_conf: toId(rec.lens_conf) || "",
|
|
|
|
|
|
lens_apt: toId(rec.lens_apt) || "",
|
|
|
|
|
|
lens_exp: toId(rec.lens_exp) || "",
|
|
|
|
|
|
repeat_all: rec.repeat_all ?? null,
|
|
|
|
|
|
// Repeaters
|
|
|
|
|
|
fill_settings: rec.fill_settings ?? [],
|
|
|
|
|
|
line_settings: rec.line_settings ?? [],
|
|
|
|
|
|
raster_settings: rec.raster_settings ?? [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<main className="space-y-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<h1 className="text-xl lg:text-2xl font-semibold">Edit CO₂ Galvo Setting</h1>
|
|
|
|
|
|
<button className="px-2 py-1 border rounded" onClick={closeEdit}>
|
|
|
|
|
|
Cancel
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<SettingsSubmit mode="edit" submissionId={rec.submission_id} initialValues={initialValues} />
|
|
|
|
|
|
</main>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-03 13:57:24 -04:00
|
|
|
|
return (
|
2025-10-03 15:07:13 -04:00
|
|
|
|
<div className="space-y-6">
|
2025-10-05 21:52:16 -04:00
|
|
|
|
{/* Header */}
|
2025-10-03 15:07:13 -04:00
|
|
|
|
<header className="space-y-1">
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<div className="flex items-center justify-between">
|
2025-10-05 20:35:20 -04:00
|
|
|
|
<h1 className="text-2xl font-bold break-words">{rec.setting_title || "Untitled"}</h1>
|
2025-10-05 22:06:32 -04:00
|
|
|
|
{editable && isMine ? (
|
|
|
|
|
|
<button className="px-2 py-1 border rounded text-sm" onClick={openEdit}>
|
|
|
|
|
|
Edit
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
2025-10-03 19:11:14 -04:00
|
|
|
|
<div className="text-sm text-muted-foreground">Last modified: {rec.last_modified_date || "—"}</div>
|
2025-10-03 15:07:13 -04:00
|
|
|
|
</header>
|
|
|
|
|
|
|
2025-10-05 22:32:27 -04:00
|
|
|
|
{/* Top row: Info (left) + Images (right) */}
|
|
|
|
|
|
<section className="grid md:grid-cols-2 gap-6 items-start">
|
|
|
|
|
|
{/* Info */}
|
|
|
|
|
|
<div className="grid gap-3">
|
2025-10-05 21:52:16 -04:00
|
|
|
|
<Field label="Owner" value={ownerLabel(rec.owner)} />
|
|
|
|
|
|
<Field label="Uploader" value={rec.uploader || "—"} />
|
2025-10-05 22:32:27 -04:00
|
|
|
|
{rec.setting_notes ? (
|
|
|
|
|
|
<Field label="Notes" value={<p className="whitespace-pre-wrap">{rec.setting_notes}</p>} />
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
2025-10-05 20:35:20 -04:00
|
|
|
|
|
2025-10-05 22:32:27 -04:00
|
|
|
|
{/* Images (side-by-side thumbnails) */}
|
2025-10-05 20:35:20 -04:00
|
|
|
|
{(photoSrc || screenSrc) && (
|
2025-10-05 22:32:27 -04:00
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
2025-10-05 20:35:20 -04:00
|
|
|
|
{photoSrc ? (
|
2025-10-05 22:17:37 -04:00
|
|
|
|
<figure className="space-y-1 justify-self-start">
|
2025-10-05 21:52:16 -04:00
|
|
|
|
<div
|
2025-10-05 22:17:37 -04:00
|
|
|
|
className="border rounded overflow-hidden cursor-zoom-in mx-auto w-40 md:w-48"
|
2025-10-05 21:52:16 -04:00
|
|
|
|
style={{ aspectRatio: "1 / 1" }}
|
|
|
|
|
|
onClick={() => setViewerSrc(photoSrc)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<img src={photoSrc} alt="Result" className="w-full h-full object-cover" loading="lazy" />
|
|
|
|
|
|
</div>
|
2025-10-05 22:17:37 -04:00
|
|
|
|
<figcaption className="text-xs text-muted-foreground text-center">Result</figcaption>
|
2025-10-05 20:35:20 -04:00
|
|
|
|
</figure>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{screenSrc ? (
|
2025-10-05 22:17:37 -04:00
|
|
|
|
<figure className="space-y-1 justify-self-start">
|
2025-10-05 21:52:16 -04:00
|
|
|
|
<div
|
2025-10-05 22:17:37 -04:00
|
|
|
|
className="border rounded overflow-hidden cursor-zoom-in mx-auto w-40 md:w-48"
|
2025-10-05 21:52:16 -04:00
|
|
|
|
style={{ aspectRatio: "1 / 1" }}
|
|
|
|
|
|
onClick={() => setViewerSrc(screenSrc)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<img src={screenSrc} alt="Settings Screenshot" className="w-full h-full object-cover" loading="lazy" />
|
|
|
|
|
|
</div>
|
2025-10-05 22:17:37 -04:00
|
|
|
|
<figcaption className="text-xs text-muted-foreground text-center">Settings Screenshot</figcaption>
|
2025-10-05 20:35:20 -04:00
|
|
|
|
</figure>
|
|
|
|
|
|
) : null}
|
2025-10-05 22:32:27 -04:00
|
|
|
|
</div>
|
2025-10-05 20:35:20 -04:00
|
|
|
|
)}
|
2025-10-05 22:32:27 -04:00
|
|
|
|
</section>
|
2025-10-05 20:35:20 -04:00
|
|
|
|
|
2025-10-05 22:32:27 -04:00
|
|
|
|
{/* Two columns below: left Rig & Optics, right Material */}
|
2025-10-05 21:52:16 -04:00
|
|
|
|
<section className="grid md:grid-cols-2 gap-6">
|
|
|
|
|
|
{/* Rig & Optics */}
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<h2 className="text-lg font-semibold">Rig & Optics</h2>
|
|
|
|
|
|
<div className="grid gap-3">
|
|
|
|
|
|
<Field label="Software" value={softName} />
|
|
|
|
|
|
<Field label="Laser Source" value={sourceText || "—"} />
|
|
|
|
|
|
<Field label="Lens Configuration" value={rec.lens_conf?.name || "—"} />
|
2025-10-05 22:38:40 -04:00
|
|
|
|
<Field label="Scan Head Aperture" value={optLabel(rec.lens_apt)} suffix="mm" />
|
|
|
|
|
|
<Field label="Beam Expander" value={optLabel(rec.lens_exp)} suffix="x" />
|
2025-10-05 21:52:16 -04:00
|
|
|
|
<Field
|
|
|
|
|
|
label="Scan Lens"
|
|
|
|
|
|
value={
|
|
|
|
|
|
rec.lens
|
2025-10-05 22:06:32 -04:00
|
|
|
|
? `${rec.lens.field_size ?? "—"}${rec.lens.focal_length ? ` / ${rec.lens.focal_length}` : ""}`
|
2025-10-05 21:52:16 -04:00
|
|
|
|
: "—"
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<Field label="Focus" value={rec.focus ?? "—"} suffix="mm" />
|
2025-10-05 21:52:16 -04:00
|
|
|
|
<Field label="Repeat All" value={rec.repeat_all ?? "—"} />
|
2025-10-05 17:09:39 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-05 21:52:16 -04:00
|
|
|
|
|
|
|
|
|
|
{/* Material */}
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<h2 className="text-lg font-semibold">Material</h2>
|
|
|
|
|
|
<div className="grid gap-3">
|
|
|
|
|
|
<Field label="Material" value={rec.mat?.name || "—"} />
|
|
|
|
|
|
<Field label="Coating" value={rec.mat_coat?.name || "—"} />
|
|
|
|
|
|
<Field label="Color" value={rec.mat_color?.name || "—"} />
|
|
|
|
|
|
<Field label="Opacity" value={rec.mat_opacity?.opacity ?? "—"} />
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<Field label="Thickness" value={rec.mat_thickness ?? "—"} suffix="mm" />
|
2025-10-03 15:07:13 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
2025-10-05 22:06:32 -04:00
|
|
|
|
{/* Repeaters (cards, full width) */}
|
2025-10-05 20:35:20 -04:00
|
|
|
|
{(rec.fill_settings?.length ?? 0) > 0 && (
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<section className="space-y-3">
|
2025-10-05 20:35:20 -04:00
|
|
|
|
<h2 className="text-lg font-semibold">Fill Settings</h2>
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<div className="grid md:grid-cols-2 gap-3">
|
2025-10-05 22:17:37 -04:00
|
|
|
|
{rec.fill_settings!.map((r: any, i: number) => {
|
|
|
|
|
|
const showIncrement = !!r.auto;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={i} className="border rounded p-3 space-y-2">
|
|
|
|
|
|
<div className="font-medium">{r.name || `Fill ${i + 1}`}</div>
|
|
|
|
|
|
<div className="grid sm:grid-cols-2 gap-2">
|
|
|
|
|
|
<Field label="Type" value={TYPE_LABEL[r.type] || "—"} />
|
|
|
|
|
|
<Field label="Power" value={r.power ?? "—"} suffix="%" />
|
|
|
|
|
|
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
|
2025-10-05 22:32:27 -04:00
|
|
|
|
<Field label="Interval" value={r.interval ?? "—"} suffix="mm" />
|
2025-10-05 22:17:37 -04:00
|
|
|
|
<Field label="Angle" value={r.angle ?? "—"} suffix="°" />
|
|
|
|
|
|
<Field label="Pass" value={r.pass ?? "—"} />
|
|
|
|
|
|
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
|
|
|
|
|
|
<Field label="Pulse" value={r.pulse ?? "—"} suffix="ns" />
|
2025-10-06 21:36:55 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid sm:grid-cols-2 gap-2 items-center">
|
2025-10-05 22:17:37 -04:00
|
|
|
|
<Field label="Auto Rotate" value={yesNo(r.auto)} />
|
|
|
|
|
|
{showIncrement && <Field label="Auto Rotate Increment" value={r.increment ?? "—"} suffix="°" />}
|
2025-10-06 21:36:55 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap gap-6">
|
2025-10-05 22:17:37 -04:00
|
|
|
|
<Field label="Crosshatch" value={yesNo(r.cross)} />
|
|
|
|
|
|
<Field label="Flood Fill" value={yesNo(r.flood)} />
|
|
|
|
|
|
<Field label="Air Assist" value={yesNo(r.air)} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2025-10-05 20:35:20 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2025-10-03 15:07:13 -04:00
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-05 20:35:20 -04:00
|
|
|
|
{(rec.line_settings?.length ?? 0) > 0 && (
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<section className="space-y-3">
|
2025-10-05 20:35:20 -04:00
|
|
|
|
<h2 className="text-lg font-semibold">Line Settings</h2>
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<div className="grid md:grid-cols-2 gap-3">
|
2025-10-06 21:36:55 -04:00
|
|
|
|
{rec.line_settings!.map((r: any, i: number) => {
|
|
|
|
|
|
const perfEnabled = !!r.perf;
|
|
|
|
|
|
const wobbleEnabled = !!r.wobble;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={i} className="border rounded p-3 space-y-2">
|
|
|
|
|
|
<div className="font-medium">{r.name || `Line ${i + 1}`}</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Base fields – match form order */}
|
|
|
|
|
|
<div className="grid sm:grid-cols-2 gap-2">
|
|
|
|
|
|
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
|
|
|
|
|
|
<Field label="Pulse" value={r.pulse ?? "—"} suffix="ns" />
|
|
|
|
|
|
<Field label="Power" value={r.power ?? "—"} suffix="%" />
|
|
|
|
|
|
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
|
|
|
|
|
|
<Field label="Pass" value={r.pass ?? "—"} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Perforation row */}
|
|
|
|
|
|
<div className="grid sm:grid-cols-3 gap-2 items-center">
|
|
|
|
|
|
<Field label="Perforation Mode" value={yesNo(perfEnabled)} />
|
|
|
|
|
|
{perfEnabled && <Field label="Cut" value={r.cut ?? "—"} suffix="mm" />}
|
|
|
|
|
|
{perfEnabled && <Field label="Skip" value={r.skip ?? "—"} suffix="mm" />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Wobble row */}
|
|
|
|
|
|
<div className="grid sm:grid-cols-3 gap-2 items-center">
|
|
|
|
|
|
<Field label="Wobble" value={yesNo(wobbleEnabled)} />
|
|
|
|
|
|
{wobbleEnabled && <Field label="Step" value={r.step ?? "—"} suffix="mm" />}
|
|
|
|
|
|
{wobbleEnabled && <Field label="Size" value={r.size ?? "—"} suffix="mm" />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Simple toggle */}
|
|
|
|
|
|
<div className="flex flex-wrap gap-6">
|
|
|
|
|
|
<Field label="Air Assist" value={yesNo(r.air)} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2025-10-05 20:35:20 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2025-10-03 15:07:13 -04:00
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-05 20:35:20 -04:00
|
|
|
|
{(rec.raster_settings?.length ?? 0) > 0 && (
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<section className="space-y-3">
|
2025-10-05 20:35:20 -04:00
|
|
|
|
<h2 className="text-lg font-semibold">Raster Settings</h2>
|
2025-10-05 22:06:32 -04:00
|
|
|
|
<div className="grid md:grid-cols-2 gap-3">
|
2025-10-06 21:36:55 -04:00
|
|
|
|
{rec.raster_settings!.map((r: any, i: number) => {
|
|
|
|
|
|
const isHalftone = r?.dither === "halftone";
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={i} className="border rounded p-3 space-y-2">
|
|
|
|
|
|
<div className="font-medium">{r.name || `Raster ${i + 1}`}</div>
|
|
|
|
|
|
<div className="grid sm:grid-cols-2 gap-2">
|
|
|
|
|
|
<Field label="Type" value={TYPE_LABEL[r.type] || "—"} />
|
|
|
|
|
|
<Field label="Dither" value={DITHER_LABEL(r.dither)} />
|
|
|
|
|
|
<Field label="Power" value={r.power ?? "—"} suffix="%" />
|
|
|
|
|
|
<Field label="Speed" value={r.speed ?? "—"} suffix="mm/s" />
|
|
|
|
|
|
<Field label="Interval" value={r.interval ?? "—"} suffix="mm" />
|
|
|
|
|
|
<Field label="Pass" value={r.pass ?? "—"} />
|
|
|
|
|
|
<Field label="Frequency" value={r.frequency ?? "—"} suffix="kHz" />
|
|
|
|
|
|
<Field label="Pulse" value={r.pulse ?? "—"} suffix="ns" />
|
|
|
|
|
|
{isHalftone && <Field label="Halftone Cell" value={r.halftone_cell ?? "—"} />}
|
|
|
|
|
|
{isHalftone && <Field label="Halftone Angle" value={r.halftone_angle ?? "—"} />}
|
|
|
|
|
|
<Field label="Dot Width Adjustment" value={r.dot ?? "—"} suffix="mm" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap gap-6">
|
|
|
|
|
|
<Field label="Crosshatch" value={yesNo(r.cross)} />
|
|
|
|
|
|
<Field label="Inverted" value={yesNo(r.inversion)} />
|
|
|
|
|
|
<Field label="Air Assist" value={yesNo(r.air)} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2025-10-05 20:35:20 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2025-10-03 13:57:24 -04:00
|
|
|
|
)}
|
2025-10-05 21:52:16 -04:00
|
|
|
|
|
|
|
|
|
|
{/* Lightbox */}
|
|
|
|
|
|
{viewerSrc && (
|
2025-10-05 22:32:27 -04:00
|
|
|
|
<div className="fixed inset-0 z-50 bg-black/80 p-4 flex items-center justify-center" onClick={() => setViewerSrc(null)}>
|
|
|
|
|
|
<img src={viewerSrc} alt="" className="max-w-full max-h-full cursor-zoom-out" onClick={(e) => e.stopPropagation()} />
|
2025-10-05 21:52:16 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-10-05 17:45:09 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|