diff --git a/app/portal/my-settings/page.tsx b/app/portal/my-settings/page.tsx index bc0cb72c..12914277 100644 --- a/app/portal/my-settings/page.tsx +++ b/app/portal/my-settings/page.tsx @@ -1,3 +1,4 @@ +// app/portal/my-settings/page.tsx "use client"; import { useEffect, useMemo, useState } from "react"; @@ -33,7 +34,7 @@ function detailHref(coll: Coll, submissionId: string | number | null | undefined export default function MySettingsPage() { const [loading, setLoading] = useState(true); const [meId, setMeId] = useState(null); - const [meUsername, setMeUsername] = useState(null); + const [meUsername, setMeUsername] = useState(null); // used only for display const [q, setQ] = useState(""); const [byColl, setByColl] = useState>({ settings_co2gal: [], @@ -51,7 +52,11 @@ export default function MySettingsPage() { 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})`); + } } // 1) Load current user id + username @@ -79,9 +84,9 @@ export default function MySettingsPage() { return () => { dead = true; }; }, []); - // 2) Load my items per collection (no id/status fields requested) + // 2) Load my items per collection (STRICT owner-only) useEffect(() => { - if (!meId && !meUsername) return; + if (!meId) return; // we require the user id to filter by owner let dead = false; setLoading(true); setErrs((e) => ({ ...e, settings_co2gal: null, settings_co2gan: null, settings_fiber: null, settings_uv: null })); @@ -93,17 +98,13 @@ export default function MySettingsPage() { const qs = new URLSearchParams(); qs.set("limit", "-1"); qs.set("sort", "-last_modified_date"); - qs.set("fields", "submission_id,setting_title,last_modified_date"); // ← only fields we’re allowed + qs.set("fields", "submission_id,setting_title,last_modified_date"); // minimal, safe fields - // OR filter by owner and legacy uploader mirror - let orIdx = 0; - if (meId) { - qs.set(`filter[_or][${orIdx}][owner][_eq]`, meId); orIdx++; - qs.set(`filter[_or][${orIdx}][owner][id][_eq]`, meId); orIdx++; - } - if (meUsername) { - qs.set(`filter[_or][${orIdx}][uploader][_eq]`, meUsername); orIdx++; - } + // STRICT owner filter. We OR the two shapes: + // - owner stored as primitive id + // - owner stored as relation object { id: ... } + qs.set(`filter[_or][0][owner][_eq]`, meId); + qs.set(`filter[_or][1][owner][id][_eq]`, meId); const url = `/api/dx/items/${coll}?${qs.toString()}`; @@ -129,7 +130,7 @@ export default function MySettingsPage() { })(); return () => { dead = true; }; - }, [meId, meUsername]); + }, [meId]); // 3) Filter client-side const filtered = useMemo(() => { diff --git a/app/settings/co2-galvo/[id]/co2-galvo.tsx b/app/settings/co2-galvo/[id]/co2-galvo.tsx index 31fcefa4..7acc74ce 100644 --- a/app/settings/co2-galvo/[id]/co2-galvo.tsx +++ b/app/settings/co2-galvo/[id]/co2-galvo.tsx @@ -1,79 +1,207 @@ +// app/settings/co2-galvo/[id]/co2-galvo.tsx "use client"; -import { useEffect, useState } from "react"; -import { useParams } from "next/navigation"; -import Markdown from "react-markdown"; +import { useEffect, useMemo, useState } from "react"; +import { useParams, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import SettingsSubmit from "@/components/forms/SettingsSubmit"; + +type Rec = { + submission_id: string | number; + setting_title?: string | null; + setting_notes?: string | null; + + // files can be id or object with id + photo?: { id?: string } | string | null; + screen?: { id?: string } | string | null; + + // shapes pass-through for form + mat?: any; + mat_coat?: any; + mat_color?: any; + mat_opacity?: any; + mat_thickness?: number | null; + + source?: any; + lens?: any; + focus?: number | null; + + laser_soft?: any; + repeat_all?: number | null; + + 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; + + last_modified_date?: string | null; +}; + +function ownerLabel(o: Rec["owner"]) { + if (!o) return "—"; + if (typeof o === "string" || typeof o === "number") return String(o); + return o.username || String(o.id ?? "—"); +} + +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})`); } +} export default function CO2GalvoSettingDetailPage() { const { id } = useParams<{ id: string }>(); - const [setting, setSetting] = useState(null); - const [loading, setLoading] = useState(true); + const sp = useSearchParams(); + const editMode = sp.get("edit") === "1"; + const [rec, setRec] = useState(null); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(null); + + // Load record by submission_id (that's what the list links use) useEffect(() => { if (!id) return; + let dead = false; - const fields = [ - "submission_id", - "setting_title", - "uploader", - // ✅ parent + subfields for resilient owner - "owner", - "owner.id", - "owner.username", - "owner.first_name", - "owner.last_name", - "owner.email", - // ... (rest of your fields) - ].join(","); + (async () => { + try { + setLoading(true); + setErr(null); - const url = `/api/dx/items/settings_co2gal/${encodeURIComponent(String(id))}?fields=${encodeURIComponent(fields)}`; + const fields = [ + "submission_id", + "setting_title", + "setting_notes", + "photo.id", + "screen.id", + "mat", + "mat_coat", + "mat_color", + "mat_opacity", + "mat_thickness", + "source", + "lens", + "focus", + "laser_soft", + "repeat_all", + "fill_settings", + "line_settings", + "raster_settings", + "owner.id", + "owner.username", + "uploader", + "last_modified_date", + ].join(","); - fetch(url, { cache: "no-store", credentials: "include" }) - .then(async (res) => { - const txt = await res.text(); - const j = txt ? JSON.parse(txt) : null; - if (!res.ok) throw new Error(j?.errors?.[0]?.message || j?.message || `HTTP ${res.status}`); - return j; - }) - .then((json) => setSetting(json?.data ?? null)) - .catch((e) => { - console.error("CO2 Galvo detail fetch failed:", e); - setSetting(null); - }) - .finally(() => setLoading(false)); + const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent( + fields + )}&filter[submission_id][_eq]=${encodeURIComponent(String(id))}&limit=1`; + + const r = await fetch(url, { cache: "no-store", credentials: "include" }); + if (!r.ok) { + const j = await readJson(r).catch(() => null); + throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`); + } + const j = await readJson(r); + 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); + } + })(); + + return () => { dead = true; }; }, [id]); - if (loading) return

Loading setting...

; - if (!setting) return

Setting not found.

; + const initialValues = useMemo(() => { + if (!rec) return null; - const ownerDisplay = - typeof setting?.owner === "object" - ? (setting.owner?.username || - [setting.owner?.first_name, setting.owner?.last_name].filter(Boolean).join(" ").trim() || - setting.owner?.email || - setting.owner?.id || - "—") - : typeof setting?.owner === "string" || typeof setting?.owner === "number" - ? String(setting.owner) - : "—"; + // normalize existing file refs to ids for the form + const photoId = typeof rec.photo === "string" ? rec.photo : rec.photo?.id ?? null; + const screenId = typeof rec.screen === "string" ? rec.screen : rec.screen?.id ?? null; + + return { + setting_title: rec.setting_title ?? "", + setting_notes: rec.setting_notes ?? "", + + photo: photoId, + screen: screenId, + + mat: rec.mat ?? null, + mat_coat: rec.mat_coat ?? null, + mat_color: rec.mat_color ?? null, + mat_opacity: rec.mat_opacity ?? null, + mat_thickness: rec.mat_thickness ?? null, + + source: rec.source ?? null, + lens: rec.lens ?? null, + focus: rec.focus ?? null, + + laser_soft: rec.laser_soft ?? null, + repeat_all: rec.repeat_all ?? null, + + fill_settings: rec.fill_settings ?? [], + line_settings: rec.line_settings ?? [], + raster_settings: rec.raster_settings ?? [], + }; + }, [rec]); + + if (loading) return

Loading setting…

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

Setting not found.

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

Edit CO₂ Galvo Setting

+ +
+ ← Back to view +
+
+ ); + } + + // ── VIEW MODE (read-only) ─────────────────────────────────── + const ownerDisplay = ownerLabel(rec.owner); return (
-

{setting.setting_title || "Untitled"}

+

{rec.setting_title || "Untitled"}

- {/* TEMP DEBUG — remove once owner shows */} + {/* Keep the debug block for now */}
-    {JSON.stringify({ owner: setting?.owner }, null, 2)}
+    {JSON.stringify({ owner: rec?.owner }, null, 2)}
     

Owner: {ownerDisplay}

-

Uploader: {setting.uploader || "—"}

+

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

- {/* …rest of the page… */} +
+ + Edit this setting + +
); }