diff --git a/components/details/CO2GalvoDetail.tsx b/components/details/CO2GalvoDetail.tsx index ce244aeb..dc96ba3c 100644 --- a/components/details/CO2GalvoDetail.tsx +++ b/components/details/CO2GalvoDetail.tsx @@ -1,4 +1,3 @@ -// components/details/CO2GalvoDetail.tsx "use client"; import { useEffect, useMemo, useState } from "react"; @@ -14,14 +13,15 @@ type Rec = { photo?: { id?: string } | string | null; screen?: { id?: string } | string | null; - mat?: { id?: string | number } | string | number | null; - mat_coat?: { id?: string | number } | string | number | null; - mat_color?: { id?: string | number } | string | number | null; - mat_opacity?: { id?: string | number } | string | number | null; + // ids & readable fields + 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; mat_thickness?: number | null; - source?: { submission_id?: string | number } | string | number | null; - lens?: { id?: string | number } | string | number | null; + 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; focus?: number | null; laser_soft?: any; @@ -39,11 +39,7 @@ type Rec = { async function readJson(res: Response) { const text = await res.text(); - try { - return text ? JSON.parse(text) : null; - } catch { - throw new Error(`Unexpected response (HTTP ${res.status})`); - } + try { return text ? JSON.parse(text) : null; } catch { throw new Error(`Unexpected response (HTTP ${res.status})`); } } function ownerLabel(o: Rec["owner"]) { @@ -52,6 +48,10 @@ function ownerLabel(o: Rec["owner"]) { return o.username || String(o.id ?? "—"); } +function fileUrl(id?: string) { + return id ? `/api/dx/assets/${id}` : ""; +} + export default function CO2GalvoDetail({ id, mode, @@ -74,7 +74,7 @@ export default function CO2GalvoDetail({ const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); - // load record (by submission_id) + // load record (with human-readable fields) useEffect(() => { if (!id) return; let dead = false; @@ -90,19 +90,34 @@ export default function CO2GalvoDetail({ "setting_notes", "photo.id", "screen.id", + "mat.id", + "mat.name", "mat_coat.id", + "mat_coat.name", "mat_color.id", + "mat_color.name", "mat_opacity.id", + "mat_opacity.opacity", "mat_thickness", + "source.submission_id", + "source.make", + "source.model", + "source.nm", + "lens.id", + "lens.field_size", + "lens.focal_length", + "focus", "laser_soft", "repeat_all", + "fill_settings", "line_settings", "raster_settings", + "owner.id", "owner.username", "uploader", @@ -129,29 +144,22 @@ export default function CO2GalvoDetail({ } })(); - return () => { - dead = true; - }; + return () => { dead = true; }; }, [id]); const initialValues = useMemo(() => { if (!rec) return null; + const toId = (v: any) => (v == null ? null : typeof v === "object" ? v.id ?? v.submission_id ?? null : v); - const toId = (v: any) => - v == null ? null : typeof v === "object" ? v.id ?? v.submission_id ?? null : v; + const photoId = typeof rec.photo === "string" || typeof rec.photo === "number" ? String(rec.photo) : rec.photo?.id ?? null; + const screenId = typeof rec.screen === "string" || typeof rec.screen === "number" ? String(rec.screen) : rec.screen?.id ?? null; - const photoId = - typeof rec.photo === "string" || typeof rec.photo === "number" ? String(rec.photo) : rec.photo?.id ?? null; - const screenId = - typeof rec.screen === "string" || typeof rec.screen === "number" ? String(rec.screen) : rec.screen?.id ?? null; - - const matId = toId(rec.mat); - const coatId = toId(rec.mat_coat); - const colorId = toId(rec.mat_color); + const matId = toId(rec.mat); + const coatId = toId(rec.mat_coat); + const colorId = toId(rec.mat_color); const opacityId = toId(rec.mat_opacity); - const lensId = toId(rec.lens); - const sourceId = - rec.source && typeof rec.source === "object" ? rec.source.submission_id ?? null : (rec.source as any) ?? null; + const lensId = toId(rec.lens); + const sourceId = rec.source && typeof rec.source === "object" ? rec.source.submission_id ?? null : (rec.source as any) ?? null; return { setting_title: rec.setting_title ?? "", @@ -164,7 +172,7 @@ export default function CO2GalvoDetail({ mat_opacity: opacityId ? String(opacityId) : null, mat_thickness: rec.mat_thickness ?? null, source: sourceId != null ? String(sourceId) : null, - lens: lensId != null ? String(lensId) : null, + lens: lensId != null ? String(lensId) : null, focus: rec.focus ?? null, laser_soft: rec.laser_soft ?? null, repeat_all: rec.repeat_all ?? null, @@ -181,76 +189,187 @@ export default function CO2GalvoDetail({ } if (loading) return

Loading setting…

; - if (err) - return ( -
-
{err}
-
- ); + if (err) return
{err}
; if (!rec) return

Setting not found.

; - // EDIT MODE + // EDIT if (editMode && initialValues) { return ( -
+

Edit CO₂ Galvo Setting

- -
- - - -
- - ← Back to view - +
+
); } - // VIEW MODE + // VIEW (readable) const ownerDisplay = ownerLabel(rec.owner); + 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); + return ( -
-
-

{rec.setting_title || "Untitled"}

+
+
+

{rec.setting_title || "Untitled"}

+
Last modified: {rec.last_modified_date || "—"}
+
- {/* debug block left intact for now */} -
-        {JSON.stringify({ owner: rec?.owner }, null, 2)}
-        
+
+
+
+
Owner: {ownerDisplay}
+
Uploader: {rec.uploader || "—"}
+
Material: {rec.mat?.name || "—"}
+
Coating: {rec.mat_coat?.name || "—"}
+
Color: {rec.mat_color?.name || "—"}
+
Opacity: {rec.mat_opacity?.opacity ?? "—"}
+
Thickness (mm): {rec.mat_thickness ?? "—"}
+
Laser Source: {[rec.source?.make, rec.source?.model].filter(Boolean).join(" ") || "—"}{rec.source?.nm ? ` (${rec.source.nm})` : ""}
+
Scan Lens: {rec.lens?.field_size || "—"}{rec.lens?.focal_length ? ` / ${rec.lens.focal_length} mm` : ""}
+
Focus (mm): {rec.focus ?? "—"}
+
Software: {rec.laser_soft ?? "—"}
+
Repeat All: {rec.repeat_all ?? "—"}
+
-
-

- Owner: {ownerDisplay} -

-

- Uploader: {rec.uploader || "—"} -

-
-
+ {rec.setting_notes ? ( +
+
Notes
+

{rec.setting_notes}

+
+ ) : null} {showOwnerEdit && (
- - Edit this setting - + Edit this setting
)}
+ +
+ {photoId ? ( +
+ Result +
Result
+
+ ) : null} + {screenId ? ( +
+ Settings screenshot +
Settings Screenshot
+
+ ) : null} +
+ + + {/* Repeaters */} + {(rec.fill_settings?.length ?? 0) > 0 && ( +
+

Fill Settings

+
+ + + + + + + + + + + + + + + + {rec.fill_settings!.map((r: any, i: number) => ( + + + + + + + + + + + + ))} + +
NameTypePower (%)Speed (mm/s)IntervalAnglePassFreq (kHz)Pulse (ns)
{r.name || "—"}{r.type || "—"}{r.power ?? "—"}{r.speed ?? "—"}{r.interval ?? "—"}{r.angle ?? "—"}{r.pass ?? "—"}{r.frequency ?? "—"}{r.pulse ?? "—"}
+
+
+ )} + + {(rec.line_settings?.length ?? 0) > 0 && ( +
+

Line Settings

+
+ + + + + + + + + + + + + + {rec.line_settings!.map((r: any, i: number) => ( + + + + + + + + + + ))} + +
NamePowerSpeedFreqPulsePassAir
{r.name || "—"}{r.power ?? "—"}{r.speed ?? "—"}{r.frequency ?? "—"}{r.pulse ?? "—"}{r.pass ?? "—"}{r.air ? "Yes" : "No"}
+
+
+ )} + + {(rec.raster_settings?.length ?? 0) > 0 && ( +
+

Raster Settings

+
+ + + + + + + + + + + + + + {rec.raster_settings!.map((r: any, i: number) => ( + + + + + + + + + + ))} + +
NameTypeDitherPowerSpeedIntervalPass
{r.name || "—"}{r.type || "—"}{r.dither || "—"}{r.power ?? "—"}{r.speed ?? "—"}{r.interval ?? "—"}{r.pass ?? "—"}
+
+
+ )} +
); } diff --git a/components/lists/CO2GalvoList.tsx b/components/lists/CO2GalvoList.tsx index 7710685a..72aeb5b5 100644 --- a/components/lists/CO2GalvoList.tsx +++ b/components/lists/CO2GalvoList.tsx @@ -1,286 +1,64 @@ -// components/lists/CO2GalvoList.tsx -"use client"; +return ( +
+
+ { + setLocalQuery(e.currentTarget.value); + onQueryChange?.(e.currentTarget.value); + })} + placeholder="Search by title, owner, material, model…" + className="w-full border rounded px-3 py-2" + /> +
-import { useEffect, useMemo, useState } from "react"; -import Link from "next/link"; - -type Owner = -| string -| number -| { - id?: string | number; - username?: string | null; - first_name?: string | null; - last_name?: string | null; - email?: string | null; -} -| null -| undefined; - -type SettingsRow = { - submission_id: string | number; - setting_title?: string | null; - uploader?: string | null; - owner?: Owner; - mat?: { name?: string | null } | null; - mat_coat?: { name?: string | null } | null; - source?: { model?: string | null } | null; - lens?: { field_size?: string | number | null } | null; -}; - -export type CO2GalvoListProps = { - /** Build the href for a record (portal vs standalone) */ - linkFor: (submission_id: string | number, opts?: { edit?: boolean }) => string; - /** Optional controlled search text (else internal input) */ - queryText?: string; - onQueryChange?: (q: string) => void; -}; - -async function readJson(res: Response) { - const text = await res.text(); - try { - return text ? JSON.parse(text) : null; - } catch { - throw new Error(`Unexpected response (status ${res.status})`); - } -} - -export default function CO2GalvoList({ linkFor, queryText, onQueryChange }: CO2GalvoListProps) { - const [settings, setSettings] = useState([]); - const [ownerMap, setOwnerMap] = useState>({}); - const [loading, setLoading] = useState(true); - const [resolvingOwners, setResolvingOwners] = useState(false); - const [meId, setMeId] = useState(null); - const [localQuery, setLocalQuery] = useState(queryText ?? ""); - - // keep local and external search text in sync - useEffect(() => { - if (queryText !== undefined) setLocalQuery(queryText); - }, [queryText]); - - const q = localQuery; - - // load current user id (for edit visibility) - useEffect(() => { - let dead = false; - (async () => { - try { - const r = await fetch(`/api/dx/users/me?fields=id`, { - cache: "no-store", - credentials: "include", - }); - if (!r.ok) return; - const j = await readJson(r); - const id = j?.data?.id ?? j?.id ?? null; - if (!dead) setMeId(id ? String(id) : null); - } catch { - /* ignore */ - } - })(); - return () => { - dead = true; - }; - }, []); - - // load list - useEffect(() => { - const fields = [ - "submission_id", - "setting_title", - "uploader", - "owner", - "owner.id", - "owner.username", - "photo.id", - "photo.title", - "mat.name", - "mat_coat.name", - "source.model", - "lens.field_size", - ].join(","); - - const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&limit=-1`; - - fetch(url, { cache: "no-store", credentials: "include" }) - .then(async (res) => { - if (!res.ok) { - const j = await readJson(res).catch(() => null); - const msg = (j as any)?.errors?.[0]?.message || `HTTP ${res.status}`; - throw new Error(msg); - } - return readJson(res); - }) - .then((json: any) => setSettings(Array.isArray(json?.data) ? json.data : [])) - .catch((e) => { - console.error("CO2 Galvo list fetch failed:", e); - setSettings([]); - }) - .finally(() => setLoading(false)); - }, []); - - // resolve owner usernames if needed - useEffect(() => { - if (!settings.length) return; - - const ids = new Set(); - for (const s of settings) { - const o = s.owner; - if (!o) continue; - - if (typeof o === "string" || typeof o === "number") { - const k = String(o); - if (!ownerMap[k]) ids.add(k); - } else { - const id = o.id != null ? String(o.id) : ""; - const hasUsername = !!o.username; - if (id && !hasUsername && !ownerMap[id]) ids.add(id); - } - } - if (!ids.size) return; - - const all = Array.from(ids); - const chunk = 100; - setResolvingOwners(true); - - (async () => { - const updates: Record = {}; - for (let i = 0; i < all.length; i += chunk) { - const slice = all.slice(i, i + chunk); - const qs = new URLSearchParams(); - qs.set("fields", "id,username"); - qs.set("limit", String(slice.length)); - qs.set("filter[id][_in]", slice.join(",")); - - try { - const r = await fetch(`/api/dx/users?${qs.toString()}`, { - credentials: "include", - cache: "no-store", - }); - if (!r.ok) { - const j = await readJson(r).catch(() => null); - const msg = (j as any)?.errors?.[0]?.message || `HTTP ${r.status}`; - console.warn("Owner lookup failed:", msg); - if (r.status === 401 || r.status === 403) break; - continue; - } - const j = await readJson(r); - const rows: Array<{ id: string; username?: string | null }> = j?.data || []; - for (const row of rows) { - updates[String(row.id)] = row.username || String(row.id); - } - } catch (e) { - console.warn("Owner lookup error:", e); - break; - } - } - - if (Object.keys(updates).length) { - setOwnerMap((prev) => ({ ...prev, ...updates })); - } - setResolvingOwners(false); - })(); - }, [settings, ownerMap]); - - const isMine = (o: Owner) => { - if (!meId || !o) return false; - if (typeof o === "string" || typeof o === "number") return String(o) === meId; - if (o.id != null) return String(o.id) === meId; - return false; - }; - - const ownerLabel = (o: Owner) => { - if (!o) return "—"; - if (typeof o === "string" || typeof o === "number") { - const id = String(o); - return ownerMap[id] || id; - } + {loading ? ( +

Loading…

+ ) : ( +
+ + + + + + + + + + + + + + {filtered.map((s) => { + const mine = isMine(s.owner); + const ownerText = ownerLabel(s.owner) + (mine ? " (you)" : ""); return ( - o.username || - [o.first_name, o.last_name].filter(Boolean).join(" ").trim() || - o.email || - (o.id != null ? String(o.id) : "—") - ); - }; - - const filtered = useMemo(() => { - const nq = (q || "").toLowerCase(); - if (!nq) return settings; - return settings.filter((entry) => { - const fields = [ - entry.setting_title, - ownerLabel(entry.owner), - entry.uploader, - entry.mat?.name, - entry.mat_coat?.name, - entry.source?.model, - entry.lens?.field_size as any, - ].filter(Boolean); - return fields.some((v: any) => String(v).toLowerCase().includes(nq)); - }); - }, [settings, q, ownerMap]); - - return ( -
-
- { - setLocalQuery(e.currentTarget.value); - onQueryChange?.(e.currentTarget.value); - }} - placeholder="Search by title, owner, material, model…" - className="w-full max-w-lg border rounded px-3 py-2" - /> -
- - {loading ? ( -

Loading…

- ) : ( -
-
TitleOwnerMaterialCoatingModelFieldEdit
- - - - - - - - - + + + + + + + + - - - {filtered.map((s) => { - const mine = isMine(s.owner); - const ownerText = ownerLabel(s.owner) + (mine ? " (you)" : ""); - return ( - - - - - - - - - - ); - })} - -
TitleOwner {resolvingOwners ? "…resolving" : ""}MaterialCoatingModelFieldActions
+ + {s.setting_title || "Untitled"} + + {ownerText}{s.mat?.name || "—"}{s.mat_coat?.name || "—"}{s.source?.model || "—"}{s.lens?.field_size || "—"} + {mine ? ( + + Edit + + ) : ( + + )} +
- - {s.setting_title || "Untitled"} - - {ownerText}{s.mat?.name || "—"}{s.mat_coat?.name || "—"}{s.source?.model || "—"}{s.lens?.field_size || "—"} - {mine ? ( - - Edit - - ) : ( - - )} -
-
- )} -
- ); -} + ); + })} + + +
+ )} + + ); diff --git a/components/portal/panels/CO2GalvoPanel.tsx b/components/portal/panels/CO2GalvoPanel.tsx index 9989db24..ad931905 100644 --- a/components/portal/panels/CO2GalvoPanel.tsx +++ b/components/portal/panels/CO2GalvoPanel.tsx @@ -1,4 +1,3 @@ -// components/portal/panels/CO2GalvoPanel.tsx "use client"; import { useRouter, useSearchParams } from "next/navigation"; @@ -10,47 +9,71 @@ export default function CO2GalvoPanel() { const router = useRouter(); const id = sp.get("id"); - const edit = sp.get("edit") === "1"; + const view = sp.get("view") === "detail" && id ? "detail" : "list"; - function linkFor(submission_id: string | number, opts?: { edit?: boolean }) { + function setView(next: "list" | "detail", nextId?: string | number) { const q = new URLSearchParams(sp.toString()); q.set("t", "co2-galvo"); - q.set("id", String(submission_id)); - if (opts?.edit) q.set("edit", "1"); - else q.delete("edit"); - return `/portal/laser-settings?${q.toString()}`; - } - - function clearIdAndEdit() { - const q = new URLSearchParams(sp.toString()); - q.delete("id"); - q.delete("edit"); + if (next === "detail" && nextId != null) { + q.set("view", "detail"); + q.set("id", String(nextId)); + } else { + q.set("view", "list"); + q.delete("id"); + q.delete("edit"); + } router.replace(`/portal/laser-settings?${q.toString()}`, { scroll: false }); } - return ( -
-
- -
+ const linkFor = (sid: string | number, opts?: { edit?: boolean }) => { + const q = new URLSearchParams(sp.toString()); + q.set("t", "co2-galvo"); + q.set("view", "detail"); + q.set("id", String(sid)); + if (opts?.edit) q.set("edit", "1"); else q.delete("edit"); + return `/portal/laser-settings?${q.toString()}`; + }; -
- {id ? ( - { - // Post-save: return to view mode and refetch by dropping edit=1 in place. - const q = new URLSearchParams(sp.toString()); - q.delete("edit"); - router.replace(`/portal/laser-settings?${q.toString()}`, { scroll: false }); - }} - /> - ) : ( -
Select a setting to view details
- )} + return ( +
+ {/* App-style header (no big boxes) */} +
+ + +
+ CO₂ Galvo Settings +
+
+ + {/* Body */} + {view === "list" ? ( +
+
+ ) : ( +
+ {id ? ( + setView("list")} + showOwnerEdit={true} + /> + ) : ( +
No record selected.
+ )} +
+ )}
); }