pelagia-portal/App/app/(portal)/po/new/actions.ts
Hardik cc7251e6b7 feat: Cost Centre covers vessels and sites, vessel codes, Accounting Code rename, vessel-site assignment
- Undo Vessel→Cost Centre rename in admin (admin shows "Vessel Management" again)
- Sidebar: "Cost Centres"→"Vessels", "Accounts"→"Accounting Codes"
- PO forms (new/edit/import/manager-edit) now show both Vessels (with code) and Sites in the
  Cost Centre dropdown, encoded as v:<id> / s:<id> via a costCentreRef field
- vesselId on PurchaseOrder is now nullable; siteId is set when a site is the cost centre
- History, approvals, dashboard, my-orders, payments display vessel.name ?? site.name as Cost Centre
- History and approvals cost centre filters use costCentreRef URL param supporting both types
- Admin vessel form: adds Site assignment dropdown
- Admin accounts: renamed to "Accounting Code" throughout (pages, forms, sidebar)
- PO detail and exports: "Account" label renamed to "Accounting Code"
- Site detail: "Assigned Vessels (Cost Centres)" heading; vessel detail breadcrumb fixed
- Create PO links from vessel/site detail use ?costCentreRef= param
- Export routes handle costCentreRef filter param (with legacy vesselId fallback)
- DB migration: ALTER TABLE PurchaseOrder ALTER COLUMN vesselId DROP NOT NULL
- CLAUDE.md updated with Cost Centre Model documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 03:04:29 +05:30

160 lines
5.7 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"),
costCentreRef: formData.get("costCentreRef"),
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;
const vesselId = data.costCentreRef.startsWith("v:") ? data.costCentreRef.slice(2) : null;
const costCentreSiteId = data.costCentreRef.startsWith("s:") ? data.costCentreRef.slice(2) : null;
// 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,
siteId: costCentreSiteId,
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 };
}