mega-plant/app/api/operators/route.ts
David c269cd00b4 Add Job Cards (vehicle-QR start/finish) and retire Operators/Mechanics pages
- Plant Clock "Job cards" tab: scan a vehicle QR to start a job, scan again to finish
  with odometer (or no meter), kind of work and a note
- /running/job-cards table with period, work, vehicle and mechanic filters
- Remove /running/operators and /running/mechanics; mechanic badge PDF moves to Employees

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 12:54:59 +02:00

81 lines
No EOL
3.1 KiB
TypeScript

import { revalidatePath } from "next/cache";
import { serverAuth } from "@/lib/pb";
export const dynamic = "force-dynamic";
// POST /api/operators — add a person to the operator register from the app.
//
// This exists because people join and leave, and the rate sheet is only re-issued
// occasionally. A row written here is NOT in data/source/operators.json, which is why
// the seed reconciles that collection by employee code instead of clearing it — this
// row survives every future re-seed and the log names it.
//
// An operator with NO employee code is rejected: the code is what a later allocation
// is preserved against, so a row without one could never keep its machine across a
// re-seed.
export async function POST(req: Request) {
let body: { name?: string; employeeCode?: string; rate?: number | string; assetId?: string } = {};
try {
body = await req.json();
} catch {
return Response.json({ ok: false, error: "Invalid JSON body" }, { status: 400 });
}
const name = (body.name || "").trim();
const employeeCode = (body.employeeCode || "").trim();
const rate = typeof body.rate === "string" ? Number(body.rate) : body.rate;
if (!name) return Response.json({ ok: false, error: "Name is required" }, { status: 400 });
if (!employeeCode) {
return Response.json(
{ ok: false, error: "Employee number is required — it is what keeps this person's machine across a re-seed" },
{ status: 400 },
);
}
if (typeof rate !== "number" || !Number.isFinite(rate) || rate <= 0) {
return Response.json({ ok: false, error: "Rate must be a number greater than zero" }, { status: 400 });
}
try {
const pb = await serverAuth();
// Names are not unique, employee codes are. Two rows sharing a code would also
// break the allocation capture, so this is enforced, not advisory.
const clash = (await pb.collection("operators").getFullList())
.find((o) => (o.employee_code || "").trim().toUpperCase() === employeeCode.toUpperCase());
if (clash) {
return Response.json(
{ ok: false, error: `${employeeCode} is already used by ${clash.name}` },
{ status: 409 },
);
}
let assetId = "";
if (body.assetId) {
const asset = await pb.collection("assets").getOne(body.assetId).catch(() => null);
if (!asset) {
return Response.json({ ok: false, error: "That plant item no longer exists" }, { status: 400 });
}
assetId = asset.id;
}
const created = await pb.collection("operators").create({
name,
employee_code: employeeCode,
rate: Math.round(rate * 100) / 100,
asset: assetId,
// Marked as app-added so the seed log can name it, and so it is obvious where
// the row came from when someone audits the register.
source: "Added in app",
});
revalidatePath("/running");
revalidatePath("/employees", "layout");
return Response.json({
ok: true,
operator: { id: created.id, name: created.name, employee_code: created.employee_code, rate: created.rate },
});
} catch (e: any) {
return Response.json({ ok: false, error: e?.message || "Could not add the operator" }, { status: 500 });
}
}