settings overhaul and reset
This commit is contained in:
parent
4ef3160515
commit
3195ff5d74
6 changed files with 904 additions and 1651 deletions
306
app/api/settings/route.ts
Normal file
306
app/api/settings/route.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
// app/api/settings/route.ts
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
* Fresh, minimal Directus client (no external helpers).
|
||||
* - Upload assets to /files with multipart/form-data.
|
||||
* - Create and update records via /items/{collection}.
|
||||
* - Auth is via user cookie (ma_at) or a submit token (DIRECTUS_TOKEN_SUBMIT).
|
||||
*/
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Env
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const DX = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
|
||||
const SUBMIT_TOKEN = process.env.DIRECTUS_TOKEN_SUBMIT || "";
|
||||
|
||||
// Folder IDs from env (data sheet says fixed, not browsable)
|
||||
const FOLDERS = {
|
||||
settings_co2gal: {
|
||||
photo: process.env.DX_FOLDER_GALVO_PHOTOS || "",
|
||||
screen: process.env.DX_FOLDER_GALVO_SCREENS || "",
|
||||
},
|
||||
settings_co2gan: {
|
||||
photo: process.env.DX_FOLDER_GANTRY_PHOTOS || "",
|
||||
screen: process.env.DX_FOLDER_GANTRY_SCREENS || "",
|
||||
},
|
||||
settings_fiber: {
|
||||
photo: process.env.DX_FOLDER_FIBER_PHOTOS || "",
|
||||
screen: process.env.DX_FOLDER_FIBER_SCREENS || "",
|
||||
},
|
||||
settings_uv: {
|
||||
photo: process.env.DX_FOLDER_UV_PHOTOS || "",
|
||||
screen: process.env.DX_FOLDER_UV_SCREENS || "",
|
||||
},
|
||||
} as const;
|
||||
|
||||
type Target = "settings_co2gal" | "settings_co2gan" | "settings_fiber" | "settings_uv";
|
||||
|
||||
function bearerFrom(req: Request) {
|
||||
// Prefer user cookie (session) else fall back to submit token for server ops.
|
||||
const cookie = req.headers.get("cookie") || "";
|
||||
const m = cookie.match(/(?:^|;\s*)ma_at=([^;]+)/);
|
||||
const at = m?.[1];
|
||||
return at ? `Bearer ${at}` : SUBMIT_TOKEN ? `Bearer ${SUBMIT_TOKEN}` : "";
|
||||
}
|
||||
|
||||
async function dxUpload(file: File, folderId: string, bearer: string) {
|
||||
const form = new FormData();
|
||||
form.set("file", file, file.name || "upload");
|
||||
if (folderId) form.set("folder", folderId);
|
||||
|
||||
const res = await fetch(`${DX}/files`, {
|
||||
method: "POST",
|
||||
headers: bearer ? { authorization: bearer } : undefined,
|
||||
body: form,
|
||||
});
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const msg = j?.errors?.[0]?.message || `Directus /files failed (HTTP ${res.status})`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return j?.data?.id as string;
|
||||
}
|
||||
|
||||
async function dxCreate(target: Target, data: any, bearer: string) {
|
||||
const res = await fetch(`${DX}/items/${target}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(bearer ? { authorization: bearer } : {}),
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const msg = j?.errors?.[0]?.message || `Directus create failed (HTTP ${res.status})`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return j?.data;
|
||||
}
|
||||
|
||||
async function dxUpdate(target: Target, pk: string | number, data: any, bearer: string) {
|
||||
const res = await fetch(`${DX}/items/${target}/${encodeURIComponent(String(pk))}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(bearer ? { authorization: bearer } : {}),
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const msg = j?.errors?.[0]?.message || `Directus update failed (HTTP ${res.status})`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return j?.data;
|
||||
}
|
||||
|
||||
// Guard numeric
|
||||
const num = (v: any) => (v === "" || v == null || Number.isNaN(Number(v)) ? null : Number(v));
|
||||
// Guard bool
|
||||
const bool = (v: any) => !!v;
|
||||
// Guard id string
|
||||
const idOrNull = (v: any) => (v === "" || v == null ? null : String(v));
|
||||
|
||||
type ReadResult = {
|
||||
mode: "json" | "multipart";
|
||||
body: any;
|
||||
photoFile: File | null;
|
||||
screenFile: File | null;
|
||||
};
|
||||
|
||||
async function readJsonOrMultipart(req: Request): Promise<ReadResult> {
|
||||
const ct = (req.headers.get("content-type") || "").toLowerCase();
|
||||
if (ct.includes("multipart/form-data")) {
|
||||
const form = await (req as any).formData();
|
||||
const payloadRaw = String(form.get("payload") ?? "{}");
|
||||
let body: any = {};
|
||||
try {
|
||||
body = JSON.parse(payloadRaw);
|
||||
} catch {
|
||||
throw new Error("Invalid JSON in 'payload'");
|
||||
}
|
||||
const p = form.get("photo");
|
||||
const s = form.get("screen");
|
||||
return {
|
||||
mode: "multipart",
|
||||
body,
|
||||
photoFile: p instanceof File && p.size > 0 ? (p as File) : null,
|
||||
screenFile: s instanceof File && s.size > 0 ? (s as File) : null,
|
||||
};
|
||||
}
|
||||
const body = await (req as any).json().catch(() => ({}));
|
||||
return { mode: "json", body, photoFile: null, screenFile: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: create or update a settings_* record
|
||||
* Body (JSON or multipart with { payload }):
|
||||
* {
|
||||
* target: "settings_co2gal" | ...,
|
||||
* mode?: "edit",
|
||||
* submission_id?: string|number,
|
||||
* // fields per data sheet (CO2 Galvo shown)
|
||||
* setting_title: string (required),
|
||||
* setting_notes?: string,
|
||||
* photo?: string (asset id) // if not provided in create, require file in multipart
|
||||
* screen?: string (asset id) // optional
|
||||
* // Material & Rig / Optics
|
||||
* mat: string (id),
|
||||
* mat_coat: string (id),
|
||||
* mat_color: string (id),
|
||||
* mat_opacity: string (id),
|
||||
* mat_thickness?: number,
|
||||
* laser_soft: string (id),
|
||||
* source: string (submission_id of laser_source),
|
||||
* lens: string (id),
|
||||
* focus?: number,
|
||||
* // CO2 Galvo Options (part of Rig & Optics per sheet)
|
||||
* lens_conf: string (id),
|
||||
* lens_apt: string (id),
|
||||
* lens_exp: string (id),
|
||||
* repeat_all?: number,
|
||||
* // Repeaters
|
||||
* fill_settings?: Array<...>,
|
||||
* line_settings?: Array<...>,
|
||||
* raster_settings?: Array<...>
|
||||
* }
|
||||
*/
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { body, photoFile, screenFile } = await readJsonOrMultipart(req);
|
||||
|
||||
const target = String(body?.target || "") as Target;
|
||||
if (!target || !FOLDERS[target]) {
|
||||
return NextResponse.json({ error: "Invalid or missing target." }, { status: 400 });
|
||||
}
|
||||
|
||||
const isEdit = body?.mode === "edit";
|
||||
const pk = isEdit ? body?.submission_id : null;
|
||||
|
||||
// Upload assets if files are present
|
||||
const bearer = bearerFrom(req);
|
||||
|
||||
const folderCfg = FOLDERS[target];
|
||||
let photoId = idOrNull(body.photo);
|
||||
let screenId = idOrNull(body.screen);
|
||||
|
||||
if (photoFile) {
|
||||
if (!folderCfg.photo) throw new Error("Photo folder not configured.");
|
||||
photoId = await dxUpload(photoFile, folderCfg.photo, bearer);
|
||||
}
|
||||
if (screenFile) {
|
||||
if (!folderCfg.screen) throw new Error("Screen folder not configured.");
|
||||
screenId = await dxUpload(screenFile, folderCfg.screen, bearer);
|
||||
}
|
||||
|
||||
// Enforce requireds (data sheet: title + result photo on create)
|
||||
if (!body.setting_title || String(body.setting_title).trim() === "") {
|
||||
return NextResponse.json({ error: "Missing required: setting_title" }, { status: 400 });
|
||||
}
|
||||
if (!isEdit && !photoId) {
|
||||
return NextResponse.json({ error: "Result photo is required." }, { status: 400 });
|
||||
}
|
||||
|
||||
// Build Directus payload strictly as the collection expects (ids + arrays)
|
||||
const payload: any = {
|
||||
setting_title: String(body.setting_title),
|
||||
setting_notes: String(body.setting_notes || ""),
|
||||
// Assets
|
||||
...(photoId ? { photo: photoId } : {}),
|
||||
...(screenId ? { screen: screenId } : {}),
|
||||
// Material/Rig & Optics (M2O ids)
|
||||
mat: idOrNull(body.mat),
|
||||
mat_coat: idOrNull(body.mat_coat),
|
||||
mat_color: idOrNull(body.mat_color),
|
||||
mat_opacity: idOrNull(body.mat_opacity),
|
||||
mat_thickness: num(body.mat_thickness),
|
||||
laser_soft: idOrNull(body.laser_soft),
|
||||
source: idOrNull(body.source), // note: this is submission_id for laser_source; schema should be configured accordingly
|
||||
lens: idOrNull(body.lens),
|
||||
focus: num(body.focus),
|
||||
// CO2 Galvo option triplet (Rig & Optics)
|
||||
lens_conf: idOrNull(body.lens_conf),
|
||||
lens_apt: idOrNull(body.lens_apt),
|
||||
lens_exp: idOrNull(body.lens_exp),
|
||||
repeat_all: num(body.repeat_all),
|
||||
// Repeaters (arrays of plain objects)
|
||||
fill_settings: Array.isArray(body.fill_settings) ? body.fill_settings.map(mapFill) : [],
|
||||
line_settings: Array.isArray(body.line_settings) ? body.line_settings.map(mapLine) : [],
|
||||
raster_settings: Array.isArray(body.raster_settings) ? body.raster_settings.map(mapRaster) : [],
|
||||
};
|
||||
|
||||
let saved;
|
||||
if (isEdit) {
|
||||
if (!pk) return NextResponse.json({ error: "Missing submission_id for edit mode." }, { status: 400 });
|
||||
saved = await dxUpdate(target, pk, payload, bearer);
|
||||
} else {
|
||||
saved = await dxCreate(target, payload, bearer);
|
||||
}
|
||||
|
||||
return NextResponse.json({ id: saved?.submission_id ?? saved?.id ?? null, data: saved }, { status: 200 });
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e?.message || "Failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Mappers (ensure numeric/bool normalization per sheet)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function mapFill(r: any) {
|
||||
return {
|
||||
name: r?.name || "",
|
||||
type: (r?.type || "").toString(), // uni|bi|offset
|
||||
power: num(r?.power),
|
||||
speed: num(r?.speed),
|
||||
interval: num(r?.interval),
|
||||
pass: num(r?.pass),
|
||||
frequency: num(r?.frequency),
|
||||
pulse: num(r?.pulse),
|
||||
angle: num(r?.angle),
|
||||
auto: bool(r?.auto),
|
||||
increment: num(r?.increment),
|
||||
cross: bool(r?.cross),
|
||||
flood: bool(r?.flood),
|
||||
air: bool(r?.air),
|
||||
};
|
||||
}
|
||||
function mapLine(r: any) {
|
||||
return {
|
||||
name: r?.name || "",
|
||||
power: num(r?.power),
|
||||
speed: num(r?.speed),
|
||||
perf: bool(r?.perf),
|
||||
cut: bool(r?.cut),
|
||||
skip: bool(r?.skip),
|
||||
pass: num(r?.pass),
|
||||
air: bool(r?.air),
|
||||
frequency: num(r?.frequency),
|
||||
pulse: num(r?.pulse),
|
||||
wobble: bool(r?.wobble),
|
||||
step: num(r?.step),
|
||||
size: num(r?.size),
|
||||
};
|
||||
}
|
||||
function mapRaster(r: any) {
|
||||
return {
|
||||
name: r?.name || "",
|
||||
type: (r?.type || "").toString(), // uni|bi|offset
|
||||
dither: (r?.dither || "").toString(), // threshold|ordered|...
|
||||
halftone_cell: num(r?.halftone_cell),
|
||||
halftone_angle: num(r?.halftone_angle),
|
||||
inversion: bool(r?.inversion),
|
||||
interval: num(r?.interval),
|
||||
dot: num(r?.dot),
|
||||
power: num(r?.power),
|
||||
speed: num(r?.speed),
|
||||
pass: num(r?.pass),
|
||||
air: bool(r?.air),
|
||||
frequency: num(r?.frequency),
|
||||
pulse: num(r?.pulse),
|
||||
cross: bool(r?.cross),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue