pelagia-portal/App/app/(portal)/po/new/actions.ts
Hardik 280966a369 refactor: revert cost centre to vessels only, remove vessel-site link
Cost Centre on PO forms now shows only Vessels (plain vesselId field).
Sites are a separate concept and not selectable as cost centres.

- PurchaseOrder.vesselId is required again (NOT NULL restored)
- Vessel.siteId and vessel->site relation removed from schema
- DB migration: drops Vessel.siteId column, restores PO.vesselId NOT NULL
- All PO forms (new/edit/import/manager-edit): plain vessel <select> with
  code-prefixed labels (e.g. "HNR1 — HNR 1")
- History, approvals, dashboard, my-orders, payments: back to vesselId
  filter params and po.vessel.name display
- Admin vessels: removed Site column and site-assignment dropdown
- Admin sites detail page: removed "Assigned Vessels" section
- Sites table: removed Vessels count column (no longer linked)
- seed-prod.ts and seed.ts: vessels created without siteId
- SearchableSelect accounting code picker retained from previous commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:14:24 +05:30

156 lines
5.4 KiB
TypeScript

"use server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { requirePermission } from "@/lib/permissions";
import { createPoSchema } from "@/lib/validations/po";
import { generatePoNumber } from "@/lib/utils";
import { notify } from "@/lib/notifier";
import { revalidatePath } from "next/cache";
export async function createPo(
formData: FormData
): Promise<{ id: string } | { error: string }> {
const session = await auth();
if (!session?.user) return { error: "Unauthorized" };
try {
requirePermission(session.user.role, "create_po");
} catch {
return { error: "You do not have permission to create purchase orders." };
}
const intent = formData.get("intent") as "draft" | "submit";
const lineItems: Array<{
name: string;
description?: string;
quantity: number;
unit: string;
size?: string;
unitPrice: number;
gstRate: number;
productId?: string;
accountId?: string;
}> = [];
let i = 0;
while (formData.has(`lineItems[${i}].name`)) {
lineItems.push({
name: formData.get(`lineItems[${i}].name`) as string,
description: (formData.get(`lineItems[${i}].description`) as string) || undefined,
quantity: Number(formData.get(`lineItems[${i}].quantity`)),
unit: formData.get(`lineItems[${i}].unit`) as string,
size: (formData.get(`lineItems[${i}].size`) as string) || undefined,
unitPrice: Number(formData.get(`lineItems[${i}].unitPrice`)),
gstRate: Number(formData.get(`lineItems[${i}].gstRate`) ?? 0.18),
productId: (formData.get(`lineItems[${i}].productId`) as string) || undefined,
accountId: (formData.get(`lineItems[${i}].accountId`) as string) || undefined,
});
i++;
}
const parsed = createPoSchema.safeParse({
title: formData.get("title"),
vesselId: formData.get("vesselId"),
accountId: formData.get("accountId"),
projectCode: formData.get("projectCode") || undefined,
dateRequired: formData.get("dateRequired") || undefined,
vendorId: formData.get("vendorId") || undefined,
piQuotationNo: formData.get("piQuotationNo") || undefined,
piQuotationDate: formData.get("piQuotationDate") || undefined,
requisitionNo: formData.get("requisitionNo") || undefined,
requisitionDate: formData.get("requisitionDate") || undefined,
placeOfDelivery: formData.get("placeOfDelivery") || undefined,
tcDelivery: formData.get("tcDelivery") || undefined,
tcDispatch: formData.get("tcDispatch") || undefined,
tcInspection: formData.get("tcInspection") || undefined,
tcTransitInsurance: formData.get("tcTransitInsurance") || undefined,
tcPaymentTerms: formData.get("tcPaymentTerms") || undefined,
tcOthers: formData.get("tcOthers") || undefined,
lineItems,
});
if (!parsed.success) {
return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
}
const data = parsed.data;
// totalAmount = grand total including GST
const total = data.lineItems.reduce(
(sum, item) => sum + item.quantity * item.unitPrice * (1 + item.gstRate),
0
);
const po = await db.purchaseOrder.create({
data: {
poNumber: generatePoNumber(),
title: data.title,
status: intent === "submit" ? "SUBMITTED" : "DRAFT",
totalAmount: total,
currency: data.currency,
vesselId: data.vesselId,
accountId: data.accountId,
vendorId: data.vendorId ?? null,
projectCode: data.projectCode ?? null,
dateRequired: data.dateRequired ? new Date(data.dateRequired) : null,
piQuotationNo: data.piQuotationNo ?? null,
piQuotationDate: data.piQuotationDate ? new Date(data.piQuotationDate) : null,
requisitionNo: data.requisitionNo ?? null,
requisitionDate: data.requisitionDate ? new Date(data.requisitionDate) : null,
placeOfDelivery: data.placeOfDelivery ?? null,
tcDelivery: data.tcDelivery ?? null,
tcDispatch: data.tcDispatch ?? null,
tcInspection: data.tcInspection ?? null,
tcTransitInsurance: data.tcTransitInsurance ?? null,
tcPaymentTerms: data.tcPaymentTerms ?? null,
tcOthers: data.tcOthers ?? null,
submitterId: session.user.id,
submittedAt: intent === "submit" ? new Date() : null,
lineItems: {
create: data.lineItems.map((item, idx) => ({
name: item.name,
description: item.description ?? null,
quantity: item.quantity,
unit: item.unit,
size: item.size ?? null,
unitPrice: item.unitPrice,
totalPrice: item.quantity * item.unitPrice,
gstRate: item.gstRate,
sortOrder: idx,
productId: item.productId ?? null,
accountId: item.accountId ?? null,
})),
},
actions: {
create: {
actionType: "CREATED",
actorId: session.user.id,
},
},
},
});
if (intent === "submit") {
await db.purchaseOrder.update({
where: { id: po.id },
data: {
status: "MGR_REVIEW",
actions: {
create: { actionType: "SUBMITTED", actorId: session.user.id },
},
},
});
const [fullPo, managers] = await Promise.all([
db.purchaseOrder.findUnique({ where: { id: po.id }, include: { submitter: true } }),
db.user.findMany({ where: { role: "MANAGER", isActive: true } }),
]);
if (fullPo) {
await notify({ event: "PO_SUBMITTED", po: fullPo, recipients: managers });
}
}
revalidatePath("/dashboard");
revalidatePath("/approvals");
return { id: po.id };
}