diff --git a/app/api/submit/settings/route.ts b/app/api/submit/settings/route.ts index b8f05ddf..c5956e5b 100644 --- a/app/api/submit/settings/route.ts +++ b/app/api/submit/settings/route.ts @@ -1,6 +1,6 @@ // app/api/submit/settings/route.ts import { NextResponse } from "next/server"; -import { uploadFile, bytesFromMB, dxGET, dxPATCH, dxPOST } from "@/lib/directus"; +import { uploadFile, createSettingsItem, bytesFromMB, dxGET, dxPATCH } from "@/lib/directus"; import { requireBearer } from "@/app/api/_lib/auth"; /** ───────────────────────────────────────────────────────────── @@ -18,7 +18,7 @@ import { requireBearer } from "@/app/api/_lib/auth"; * - settings_co2gal * - settings_uv * - * Edit: + * Also supports editing: * Body must include { mode: "edit", submission_id: string|number } * We PATCH via filter[submission_id][_eq] and owner = current user. * ──────────────────────────────────────────────────────────── */ @@ -53,7 +53,7 @@ function num(v: any, fallback: number | null = null) { } type ReadResult = { - mode: "json" | "multipart"; + mode: "json" | "multipart"; // transport mode, not create/edit body: any; photoFile: File | null; screenFile: File | null; @@ -140,8 +140,7 @@ export async function POST(req: Request) { } // Create vs Edit - const op: "create" | "edit" = - body?.mode === "edit" ? "edit" : "create"; + const op: "create" | "edit" = body?.mode === "edit" ? "edit" : "create"; // Required basics const setting_title = String(body?.setting_title || "").trim(); @@ -174,18 +173,17 @@ export async function POST(req: Request) { const mat_thickness = num(body?.mat_thickness, null); const source = body?.source ?? null; const lens = body?.lens ?? null; - - // CO₂ galvo extras (relations) - const lens_conf = body?.lens_conf ?? null; - const lens_apt = body?.lens_apt ?? null; - const lens_exp = body?.lens_exp ?? null; - const focus = num(body?.focus, null); const setting_notes = String(body?.setting_notes || "").trim(); // Shared string fields - const laser_soft = body?.laser_soft ?? null; - const repeat_all = num(body?.repeat_all, null); + const laser_soft = body?.laser_soft ?? null; // exact key: 'laser_soft' + const repeat_all = num(body?.repeat_all, null); // universally applicable + + // CO2 lens extras (may be null on non-co2) + const lens_conf = body?.lens_conf ?? null; + const lens_apt = body?.lens_apt ?? null; + const lens_exp = body?.lens_exp ?? null; // Upload / accept existing file ids let photo_id: string | null = body?.photo ?? null; @@ -241,14 +239,13 @@ export async function POST(req: Request) { setting_notes, // Ownership & attribution - owner: meId || null, // M2O to directus_users - uploader, // string mirror of username + owner: meId || null, // M2O to directus_users + uploader, // string mirror of username // exact keys laser_soft, repeat_all, - // material / optics mat, mat_coat, mat_color, @@ -256,15 +253,13 @@ export async function POST(req: Request) { mat_thickness, source, lens, + focus, - // CO₂ galvo extras + // CO2-specific lens extras lens_conf, lens_apt, lens_exp, - focus, - - // repeaters fill_settings: fills, line_settings: lines, raster_settings: rasters, @@ -273,37 +268,6 @@ export async function POST(req: Request) { last_modified_date: nowIso, }; - // ── Per-target requireds (server-side enforcement) ───────── - if (target === "settings_co2gal") { - const missing: string[] = []; - const req = { - setting_title, - uploader, - photo: op === "create" ? photo_id : true, // on edit, can be omitted - source, - lens, - lens_conf, - lens_apt, - lens_exp, - focus: Number.isFinite(focus as number), - mat, - mat_coat, - mat_color, - mat_opacity, - laser_soft, - repeat_all: Number.isFinite(repeat_all as number), - }; - for (const [k, v] of Object.entries(req)) { - if (!v) missing.push(k); - } - if (missing.length) { - return NextResponse.json( - { error: `Missing required: ${missing.join(", ")}` }, - { status: 400 } - ); - } - } - if (op === "create") { // Create-only fields basePayload.photo = photo_id; @@ -312,14 +276,9 @@ export async function POST(req: Request) { basePayload.submitted_via = "makearmy-app"; basePayload.submitted_at = nowIso; - // 🔑 Directus requires { data: {...} } - const res = await dxPOST<{ data: { id: string | number } }>( - `/items/${target}`, - bearer, - { data: basePayload } - ); - - return NextResponse.json({ ok: true, id: res?.data?.id }); + // Helper is expected to wrap as { data: … } internally + const { data } = await createSettingsItem(target, basePayload, bearer); + return NextResponse.json({ ok: true, id: data.id }); } // EDIT mode @@ -342,10 +301,11 @@ export async function POST(req: Request) { // enforce owner matches current user (works whether owner is id or M2O) qs.set("filter[_and][1][owner][_eq]", String(meId)); + // ⬇⬇⬇ Directus expects { data: {...} } here (this fixes the 400 "data is required") const res = await dxPATCH<{ data: any[] }>( `/items/${target}?${qs.toString()}`, bearer, - editPayload + { data: editPayload } ); const updatedCount = Array.isArray(res?.data) ? res.data.length : 0; diff --git a/components/details/CO2GalvoDetail.tsx b/components/details/CO2GalvoDetail.tsx index fba71549..b04f416c 100644 --- a/components/details/CO2GalvoDetail.tsx +++ b/components/details/CO2GalvoDetail.tsx @@ -27,6 +27,11 @@ type Rec = { laser_soft?: { id?: string | number; name?: string | null } | string | number | null; repeat_all?: number | null; + // CO₂ Galvo extras (M2O) + lens_conf?: { id?: string | number; name?: string | null } | null; + lens_apt?: { id?: string | number; name?: string | null } | null; + lens_exp?: { id?: string | number; name?: string | null } | null; + fill_settings?: any[] | null; line_settings?: any[] | null; raster_settings?: any[] | null; @@ -39,7 +44,11 @@ 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"]) { @@ -65,7 +74,9 @@ function ZoomableSquareImage(props: { src: string; alt: string; onOpen: () => vo className="w-full h-full object-cover cursor-zoom-in" loading="lazy" onClick={onOpen} - onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none"; }} + onError={(e) => { + (e.currentTarget as HTMLImageElement).style.display = "none"; + }} /> ); @@ -104,17 +115,23 @@ export default function CO2GalvoDetail({ const j = await readJson(r); const id = j?.data?.id ?? j?.id ?? null; if (!dead) setMeId(id ? String(id) : null); - } catch { /* ignore */ } + } catch { + /* ignore */ + } })(); - return () => { dead = true; }; + return () => { + dead = true; + }; }, []); // lightbox const [viewerSrc, setViewerSrc] = useState(null); useEffect(() => { - const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setViewerSrc(null); }; - if (viewerSrc) window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setViewerSrc(null); + }; + if (viewerSrc) window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); }, [viewerSrc]); // load record (with human-readable fields, +laser_soft.name) @@ -153,6 +170,14 @@ export default function CO2GalvoDetail({ "lens.field_size", "lens.focal_length", + // CO₂ Galvo extras + "lens_conf.id", + "lens_conf.name", + "lens_apt.id", + "lens_apt.name", + "lens_exp.id", + "lens_exp.name", + "focus", "laser_soft.id", "laser_soft.name", @@ -168,9 +193,9 @@ export default function CO2GalvoDetail({ "last_modified_date", ].join(","); - const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&filter[submission_id][_eq]=${encodeURIComponent( - String(id) - )}&limit=1`; + 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) { @@ -188,46 +213,60 @@ 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; + + // CO₂ Galvo M2O ids + const lensConfId = toId(rec.lens_conf); + const lensAptId = toId(rec.lens_apt); + const lensExpId = toId(rec.lens_exp); return { - submission_id: rec.submission_id, // ★ include for type parity + submission_id: rec.submission_id, // keep for edit mode parity setting_title: rec.setting_title ?? "", setting_notes: rec.setting_notes ?? "", - photo: photoId, + photo: photoId, screen: screenId, - mat: matId ? String(matId) : null, - mat_coat: coatId ? String(coatId) : null, - mat_color: colorId ? String(colorId) : null, - mat_opacity: opacityId ? String(opacityId) : null, + mat: matId ? String(matId) : null, + mat_coat: coatId ? String(coatId) : null, + mat_color: colorId ? String(colorId) : null, + mat_opacity: opacityId ? String(opacityId) : null, mat_thickness: rec.mat_thickness ?? null, source: sourceId != null ? String(sourceId) : null, - lens: lensId != null ? String(lensId) : null, - focus: rec.focus ?? null, + lens: lensId != null ? String(lensId) : null, + focus: rec.focus ?? null, laser_soft: (typeof rec.laser_soft === "object" ? rec.laser_soft?.id : (rec.laser_soft as any)) ?? null, repeat_all: rec.repeat_all ?? null, - fill_settings: rec.fill_settings ?? [], - line_settings: rec.line_settings ?? [], + // pass through for prefill + lens_conf: lensConfId != null ? String(lensConfId) : null, + lens_apt: lensAptId != null ? String(lensAptId) : null, + lens_exp: lensExpId != null ? String(lensExpId) : null, + + fill_settings: rec.fill_settings ?? [], + line_settings: rec.line_settings ?? [], raster_settings: rec.raster_settings ?? [], }; }, [rec]); @@ -239,11 +278,12 @@ 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 ─────────────────────────────────────────────── @@ -252,7 +292,9 @@ export default function CO2GalvoDetail({

Edit CO₂ Galvo Setting

- +
-
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: {softName}
-
Repeat All: {rec.repeat_all ?? "—"}
+
+ 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: {softName} +
+
+ Repeat All: {rec.repeat_all ?? "—"} +
{rec.setting_notes ? ( @@ -316,7 +392,9 @@ export default function CO2GalvoDetail({ {showOwnerEdit && isMine && (
- +
)}
@@ -444,8 +522,16 @@ export default function CO2GalvoDetail({ )} {viewerSrc && ( -
setViewerSrc(null)}> - e.stopPropagation()} /> +
setViewerSrc(null)} + > + e.stopPropagation()} + />
)}