// components/forms/SettingsSubmit.tsx "use client"; import { useEffect, useMemo, useState } from "react"; import { useForm, useFieldArray, useWatch, type UseFormRegister } from "react-hook-form"; import { useRouter } from "next/navigation"; type Target = "settings_co2gal"; type Opt = { id: string; label: string }; type Me = { id: string; username?: string; email?: string } | null; const API = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); type EditInitialValues = { submission_id: string | number; setting_title?: string; setting_notes?: string; photo?: string | null; screen?: string | null; // Material mat?: string | null; mat_coat?: string | null; mat_color?: string | null; mat_opacity?: string | null; mat_thickness?: number | null; // Rig & Optics laser_soft?: string | null; source?: string | null; // submission_id lens?: string | null; focus?: number | null; // CO2 Galvo triplet lens_conf?: string | null; lens_apt?: string | null; lens_exp?: string | null; repeat_all?: number | null; // Repeaters fill_settings?: any[] | null; line_settings?: any[] | null; raster_settings?: any[] | null; }; type BaseProps = { mode?: "create" | "edit"; submissionId?: string | number; initialValues?: EditInitialValues | null }; export default function SettingsSubmit({ mode = "create", submissionId, initialValues }: BaseProps) { const router = useRouter(); const isEdit = mode === "edit"; const [me, setMe] = useState(null); const [submitErr, setSubmitErr] = useState(null); // Robust current-user fetch useEffect(() => { let alive = true; (async () => { try { const r1 = await fetch(`/api/me`, { cache: "no-store", credentials: "include" }); if (r1.ok) { const j = await r1.json().catch(() => null); if (alive && j) { const id = j?.id ?? j?.data?.id ?? null; const username = j?.username ?? j?.data?.username ?? j?.name ?? null; const email = j?.email ?? j?.data?.email ?? null; setMe(id ? { id, username: username ?? undefined, email: email ?? undefined } : null); return; } } const r2 = await fetch(`/api/dx/users/me?fields=id,username,email`, { cache: "no-store", credentials: "include" }); if (alive && r2.ok) { const j2 = await r2.json().catch(() => null); const d = j2?.data ?? j2 ?? null; setMe(d?.id ? { id: d.id, username: d.username ?? undefined, email: d.email ?? undefined } : null); } } catch { if (alive) setMe(null); } })(); return () => { alive = false; }; }, []); // Options loaders (Directus reads) function useOptions(path: string, includeId?: string | null) { const [opts, setOpts] = useState([]); useEffect(() => { let live = true; (async () => { let url = ""; let map = (rows: any[]) => rows.map((r) => ({ id: String(r.id ?? r.submission_id), label: String(r.name ?? r.model ?? r.opacity ?? r.id) })); if (path === "material") url = `${API}/items/material?fields=id,name&limit=1000&sort=name`; else if (path === "material_coating") url = `${API}/items/material_coating?fields=id,name&limit=1000&sort=name`; else if (path === "material_color") url = `${API}/items/material_color?fields=id,name&limit=1000&sort=name`; else if (path === "material_opacity") { url = `${API}/items/material_opacity?fields=id,opacity&limit=1000&sort=opacity`; map = (rows) => rows.map((r) => ({ id: String(r.id), label: String(r.opacity ?? r.id) })); } else if (path === "laser_software") { url = `${API}/items/laser_software?fields=id,name&limit=1000&sort=name`; } else if (path === "laser_source_co2_galvo") { url = `${API}/items/laser_source?fields=submission_id,make,model,nm&limit=2000&sort=make,model`; type Row = { submission_id?: string | number; make?: string; model?: string; nm?: string | number | null }; const toNum = (v: unknown): number | null => { if (typeof v === "number") return globalThis.Number.isFinite(v) ? v : null; if (typeof v === "string") { const m = v.match(/-?\d+(\.\d+)?/); const n = m ? globalThis.Number(m[0]) : NaN; return globalThis.Number.isFinite(n) ? n : null; } return null; }; map = (rows: Row[]) => rows .filter((r) => { const nmVal = toNum(r.nm); return nmVal !== null && nmVal >= 10000 && nmVal <= 11000; }) .map((r) => ({ id: String(r.submission_id ?? ""), label: [r.make, r.model].filter(Boolean).join(" ") || String(r.submission_id ?? ""), })); } else if (path === "laser_scan_lens") { url = `${API}/items/laser_scan_lens?fields=id,field_size,focal_length&limit=1000`; map = (rows) => rows .slice() .sort((a, b) => (parseFloat(a.focal_length ?? "99999") || 99999) - (parseFloat(b.focal_length ?? "99999") || 99999)) .map((r) => { const fs = r.field_size ? `${r.field_size} mm` : ""; const fl = r.focal_length ? `${r.focal_length} mm` : ""; const label = [fs, fl].filter(Boolean).join(" — ") || String(r.id); return { id: String(r.id), label }; }); } else if (path === "laser_scan_lens_config") { url = `${API}/items/laser_scan_lens_config?fields=id,name&limit=1000&sort=name`; } else if (path === "laser_scan_lens_apt") { url = `${API}/items/laser_scan_lens_apt?fields=id,name&limit=1000&sort=name`; } else if (path === "laser_scan_lens_exp") { url = `${API}/items/laser_scan_lens_exp?fields=id,name&limit=1000&sort=name`; } else { setOpts([]); return; } const res = await fetch(url, { cache: "no-store", credentials: "include" }); const j = await res.json().catch(() => ({})); const rows = Array.isArray(j?.data) ? j.data : []; let list = map(rows); if (includeId && !list.some((o) => String(o.id) === String(includeId))) { list = [{ id: String(includeId), label: "(current selection)" }, ...list]; } if (live) setOpts(list); })().catch(() => live && setOpts([])); return () => { live = false; }; }, [path, includeId]); return { opts }; } // Enumerations const FILL_TYPES: Opt[] = [ { id: "uni", label: "UniDirectional" }, { id: "bi", label: "BiDirectional" }, { id: "offset", label: "Offset Fill" }, ]; const RASTER_TYPES = FILL_TYPES; const RASTER_DITHER: Opt[] = [ "threshold", "ordered", "atkinson", "dither", "stucki", "jarvis", "newsprint", "halftone", "sketch", "grayscale", ].map((x) => ({ id: x, label: x[0].toUpperCase() + x.slice(1) })); // react-hook-form const { register, handleSubmit, control, reset, setValue, // <-- add getValues, // <-- add formState: { isSubmitting }, } = useForm({ defaultValues: { setting_title: "", setting_notes: "", // Material mat: "", mat_coat: "", mat_color: "", mat_opacity: "", mat_thickness: "", // Rig & Optics laser_soft: "", source: "", // keep these blank so Select shows "—" lens_conf: "", lens_apt: "", lens_exp: "", lens: "", focus: "", repeat_all: "", // Repeaters fill_settings: [], line_settings: [], raster_settings: [], }, }); // Repeaters const fills = useFieldArray({ control, name: "fill_settings" }); const lines = useFieldArray({ control, name: "line_settings" }); const rasters = useFieldArray({ control, name: "raster_settings" }); // Prefill (edit) useEffect(() => { if (!isEdit || !initialValues) return; reset({ setting_title: initialValues.setting_title ?? "", setting_notes: initialValues.setting_notes ?? "", // Material mat: initialValues.mat ?? "", mat_coat: initialValues.mat_coat ?? "", mat_color: initialValues.mat_color ?? "", mat_opacity: initialValues.mat_opacity ?? "", mat_thickness: initialValues.mat_thickness ?? "", // Rig & Optics laser_soft: initialValues.laser_soft ?? "", source: initialValues.source ?? "", lens_conf: initialValues.lens_conf ?? "", lens_apt: initialValues.lens_apt ?? "", lens_exp: initialValues.lens_exp ?? "", lens: initialValues.lens ?? "", focus: initialValues.focus ?? "", repeat_all: initialValues.repeat_all ?? "", // Repeaters fill_settings: initialValues.fill_settings ?? [], line_settings: initialValues.line_settings ?? [], raster_settings: initialValues.raster_settings ?? [], }); }, [isEdit, initialValues, reset]); // Option lists (include current IDs to guarantee a visible option) const mats = useOptions("material", initialValues?.mat ?? null); const coats = useOptions("material_coating", initialValues?.mat_coat ?? null); const colors = useOptions("material_color", initialValues?.mat_color ?? null); const opacs = useOptions("material_opacity", initialValues?.mat_opacity ?? null); const soft = useOptions("laser_software", initialValues?.laser_soft ?? null); const srcs = useOptions("laser_source_co2_galvo", initialValues?.source ?? null); const lens = useOptions("laser_scan_lens", initialValues?.lens ?? null); const conf = useOptions("laser_scan_lens_config", initialValues?.lens_conf ?? null); const apt = useOptions("laser_scan_lens_apt", initialValues?.lens_apt ?? null); const exp = useOptions("laser_scan_lens_exp", initialValues?.lens_exp ?? null); // 🔧 Targeted fix: when options hydrate, re-apply current ids so selects adopt them. useEffect(() => { if (!initialValues) return; type Name = | "mat" | "mat_coat" | "mat_color" | "mat_opacity" | "laser_soft" | "source" | "lens_conf" | "lens_apt" | "lens_exp" | "lens"; const ensure = (name: Name, id?: string | null, opts?: Opt[]) => { if (!id) return; if (!opts || opts.length === 0) return; const curr = getValues(name as any); // Re-apply if blank OR already equal to the intended id (forces the select to adopt it) if (curr == null || curr === "" || String(curr) === String(id)) { setValue(name as any, String(id), { shouldDirty: false, shouldValidate: false }); } }; ensure("mat", initialValues.mat ?? null, mats.opts); ensure("mat_coat", initialValues.mat_coat ?? null, coats.opts); ensure("mat_color", initialValues.mat_color ?? null, colors.opts); ensure("mat_opacity",initialValues.mat_opacity ?? null, opacs.opts); ensure("laser_soft", initialValues.laser_soft ?? null, soft.opts); ensure("source", initialValues.source ?? null, srcs.opts); // Order: source → (conf/apt/exp) → lens ensure("lens_conf", initialValues.lens_conf ?? null, conf.opts); ensure("lens_apt", initialValues.lens_apt ?? null, apt.opts); ensure("lens_exp", initialValues.lens_exp ?? null, exp.opts); ensure("lens", initialValues.lens ?? null, lens.opts); }, [ initialValues, mats.opts, coats.opts, colors.opts, opacs.opts, soft.opts, srcs.opts, lens.opts, conf.opts, apt.opts, exp.opts, getValues, setValue, ]); // Image files const [photoFile, setPhotoFile] = useState(null); const [screenFile, setScreenFile] = useState(null); const onPick = (setter: (f: File | null) => void) => (e: React.ChangeEvent) => setter(e.target.files?.[0] ?? null); const onSubmit = async (values: any) => { setSubmitErr(null); const payload: any = { target: "settings_co2gal" as Target, ...(isEdit ? { mode: "edit" as const, submission_id: submissionId } : {}), setting_title: values.setting_title, setting_notes: values.setting_notes || "", // Material mat: values.mat || null, mat_coat: values.mat_coat || null, mat_color: values.mat_color || null, mat_opacity: values.mat_opacity || null, mat_thickness: values.mat_thickness === "" ? null : globalThis.Number(values.mat_thickness), // Rig & Optics laser_soft: values.laser_soft || null, source: values.source || null, lens_conf: values.lens_conf || null, lens_apt: values.lens_apt || null, lens_exp: values.lens_exp || null, lens: values.lens || null, focus: values.focus === "" ? null : globalThis.Number(values.focus), repeat_all: values.repeat_all === "" ? null : globalThis.Number(values.repeat_all), // Repeaters (raw pass-through; api will normalize nums/bools) fill_settings: values.fill_settings || [], line_settings: values.line_settings || [], raster_settings: values.raster_settings || [], // If editing with existing asset IDs, the API will accept them ...(initialValues?.photo ? { photo: initialValues.photo } : {}), ...(initialValues?.screen ? { screen: initialValues.screen } : {}), }; const form = new FormData(); form.set("payload", JSON.stringify(payload)); if (photoFile) form.set("photo", photoFile, photoFile.name || "photo"); if (screenFile) form.set("screen", screenFile, screenFile.name || "screen"); const res = await fetch("/api/settings", { method: "POST", body: form, credentials: "include" }); const j = await res.json().catch(() => ({})); if (!res.ok) { setSubmitErr(j?.error || `Submit failed (${res.status})`); return; } if (isEdit) { router.back(); } else { reset(); setPhotoFile(null); setScreenFile(null); router.push(`/submit/settings/success?id=${encodeURIComponent(String(j?.id ?? ""))}`); } }; const meLabel = me?.username || me?.email || ""; return (

{isEdit ? "Edit CO₂ Galvo Setting" : "Submit CO₂ Galvo Setting"}

{meLabel ?

Submitting as {meLabel}

: null} {submitErr ?
{submitErr}
: null}
{/* Info */}

Info