100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
import { getCurrentUser } from "@/lib/auth/session";
|
|
import { authorizeOperation } from "@/lib/auth/operation";
|
|
import { NextResponse } from "next/server";
|
|
import { pbAdmin } from "@/lib/pb";
|
|
import { parseCoordinate, type CoordinateKind } from "@/lib/site-coordinates";
|
|
|
|
/**
|
|
* POST /api/sites
|
|
*
|
|
* Creates a new site row. Required: project_code (unique). The rest
|
|
* is optional and can be backfilled later via PATCH /api/sites/[id].
|
|
*/
|
|
export async function POST(request: Request) {
|
|
const denied = await authorizeOperation(request);
|
|
if (denied) return denied;
|
|
const user = await getCurrentUser();
|
|
if (!user) return NextResponse.json({ error: "Not signed in" }, { status: 401 });
|
|
let body: Record<string, unknown>;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return NextResponse.json({ error: "Body must be JSON" }, { status: 400 });
|
|
}
|
|
|
|
const projectCode = typeof body.project_code === "string" ? body.project_code.trim() : "";
|
|
if (!projectCode) {
|
|
return NextResponse.json(
|
|
{ error: "project_code is required" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
if (projectCode.length > 64) {
|
|
return NextResponse.json(
|
|
{ error: "project_code must be 64 characters or fewer" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const payload: Record<string, unknown> = { project_code: projectCode };
|
|
for (const key of [
|
|
"name",
|
|
"duration",
|
|
"contract_value",
|
|
"work_hours",
|
|
"hours_per_week",
|
|
"wages_hours_per_day",
|
|
"job_start_date",
|
|
"work_month_start_day",
|
|
]) {
|
|
if (body[key] !== undefined && body[key] !== null && body[key] !== "") {
|
|
payload[key] = body[key];
|
|
}
|
|
}
|
|
|
|
for (const key of [
|
|
"contract_value",
|
|
"work_hours",
|
|
"hours_per_week",
|
|
"wages_hours_per_day",
|
|
"work_month_start_day",
|
|
]) {
|
|
if (typeof payload[key] === "string" && payload[key] === "") {
|
|
delete payload[key];
|
|
}
|
|
}
|
|
|
|
for (const k of ["latitude", "longitude"] as const satisfies readonly CoordinateKind[]) {
|
|
if (!(k in body)) continue;
|
|
const parsed = parseCoordinate(body[k], k);
|
|
if (!parsed.ok) {
|
|
return NextResponse.json({ error: parsed.error }, { status: 400 });
|
|
}
|
|
// A blank coordinate on create is simply "not known yet".
|
|
if (parsed.value !== null) payload[k] = parsed.value;
|
|
}
|
|
|
|
const pb = await pbAdmin();
|
|
try {
|
|
const created = await pb.collection("sites").create(payload);
|
|
if (user.role !== "admin") {
|
|
try {
|
|
// Append atomically so concurrent assignments are preserved.
|
|
await pb.collection("users").update(user.id, { "sites+": [created.id] });
|
|
} catch (assignmentError) {
|
|
await pb.collection("sites").delete(created.id);
|
|
throw assignmentError;
|
|
}
|
|
}
|
|
return NextResponse.json({ site: created }, { status: 201 });
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : "Insert failed";
|
|
if (/UNIQUE|duplicate|idx_sites_project_code/i.test(msg)) {
|
|
return NextResponse.json(
|
|
{ error: `project_code "${projectCode}" is already in use` },
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|