import { type NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api"; import { CreateFastenerSchema } from "@/lib/schemas"; import { audit } from "@/lib/audit"; import { clientIp } from "@/lib/request"; /** * List fasteners on a project + compute "remaining to order" per row so the * PO draft UI can suggest line qtys. remaining = qty - sum(receivedQty on * open lines). Closed / cancelled PO lines don't count against the need. */ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { await requireRole("admin"); const { id } = await ctx.params; const project = await prisma.project.findUnique({ where: { id }, select: { id: true } }); if (!project) throw new ApiError(404, "not_found", "Project not found"); const fasteners = await prisma.fastener.findMany({ where: { projectId: id }, orderBy: [{ supplier: "asc" }, { partNumber: "asc" }], include: { poLines: { select: { qty: true, receivedQty: true, po: { select: { status: true } }, }, }, }, }); const rows = fasteners.map((f) => { let onOrder = 0; let received = 0; for (const line of f.poLines) { if (line.po.status === "cancelled") continue; onOrder += line.qty; received += line.receivedQty; } const remaining = Math.max(0, f.qty - received); const unresolved = Math.max(0, f.qty - onOrder); return { ...f, poLines: undefined, onOrder, received, remaining, unresolved, // qty we haven't put on any PO yet }; }); return ok({ fasteners: rows }); } catch (err) { return errorResponse(err); } } export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("admin"); const { id } = await ctx.params; const body = await parseJson(req, CreateFastenerSchema); const project = await prisma.project.findUnique({ where: { id }, select: { id: true } }); if (!project) throw new ApiError(404, "not_found", "Project not found"); const created = await prisma.fastener.create({ data: { projectId: id, partNumber: body.partNumber, description: body.description, qty: body.qty, supplier: body.supplier ?? null, unitCost: body.unitCost ?? null, notes: body.notes ?? null, }, }); await audit({ actorId: actor.id, action: "create", entity: "Fastener", entityId: created.id, after: created, ipAddress: clientIp(req), }); return ok({ fastener: created }, { status: 201 }); } catch (err) { return errorResponse(err); } }