diff --git a/App/app/(portal)/po/[id]/edit/actions.ts b/App/app/(portal)/po/[id]/edit/actions.ts
index d52958b..f37cb10 100644
--- a/App/app/(portal)/po/[id]/edit/actions.ts
+++ b/App/app/(portal)/po/[id]/edit/actions.ts
@@ -48,6 +48,7 @@ export async function updatePo(
vesselId: formData.get("vesselId"),
accountId: formData.get("accountId"),
companyId: (formData.get("companyId") as string) || undefined,
+ poDate: formData.get("poDate") || undefined,
projectCode: formData.get("projectCode") || undefined,
dateRequired: formData.get("dateRequired") || undefined,
vendorId: formData.get("vendorId") || undefined,
@@ -91,7 +92,7 @@ export async function updatePo(
vessel: string | null; vesselId: string;
account: string; accountId: string;
vendor: string | null; vendorId: string | null;
- projectCode: string | null; dateRequired: string | null; placeOfDelivery: string | null;
+ poDate: string | null; projectCode: string | null; dateRequired: string | null; placeOfDelivery: string | null;
};
} | null = null;
@@ -124,6 +125,7 @@ export async function updatePo(
accountId: currentPo.accountId,
vendor: currentPo.vendor?.name ?? null,
vendorId: currentPo.vendorId,
+ poDate: currentPo.poDate?.toISOString() ?? null,
projectCode: currentPo.projectCode,
dateRequired: currentPo.dateRequired?.toISOString() ?? null,
placeOfDelivery: currentPo.placeOfDelivery,
@@ -140,6 +142,7 @@ export async function updatePo(
accountId: data.accountId,
companyId: data.companyId ?? null,
vendorId: data.vendorId ?? null,
+ poDate: data.poDate ? new Date(data.poDate) : null,
projectCode: data.projectCode ?? null,
dateRequired: data.dateRequired ? new Date(data.dateRequired) : null,
piQuotationNo: data.piQuotationNo ?? null,
diff --git a/App/app/(portal)/po/[id]/edit/edit-po-form.tsx b/App/app/(portal)/po/[id]/edit/edit-po-form.tsx
index a2aea62..2135a77 100644
--- a/App/app/(portal)/po/[id]/edit/edit-po-form.tsx
+++ b/App/app/(portal)/po/[id]/edit/edit-po-form.tsx
@@ -92,6 +92,9 @@ export function EditPoForm({ po, vessels, accounts, vendors, companies, managerN
}
}
+ const poDateValue = po.poDate
+ ? new Date(po.poDate).toISOString().split("T")[0]
+ : "";
const dateValue = po.dateRequired
? new Date(po.dateRequired).toISOString().split("T")[0]
: "";
@@ -175,6 +178,11 @@ export function EditPoForm({ po, vessels, accounts, vendors, companies, managerN
required
/>
+
+
+
+
Optional — can be back-dated or forward-dated. Defaults to the approved date if left blank.
+
diff --git a/App/app/(portal)/po/new/actions.ts b/App/app/(portal)/po/new/actions.ts
index 38be188..a9114c0 100644
--- a/App/app/(portal)/po/new/actions.ts
+++ b/App/app/(portal)/po/new/actions.ts
@@ -54,6 +54,7 @@ export async function createPo(
vesselId: formData.get("vesselId"),
accountId: formData.get("accountId"),
companyId: (formData.get("companyId") as string) || undefined,
+ poDate: formData.get("poDate") || undefined,
projectCode: formData.get("projectCode") || undefined,
dateRequired: formData.get("dateRequired") || undefined,
vendorId: formData.get("vendorId") || undefined,
@@ -93,6 +94,7 @@ export async function createPo(
accountId: data.accountId,
companyId: data.companyId ?? null,
vendorId: data.vendorId ?? null,
+ poDate: data.poDate ? new Date(data.poDate) : null,
projectCode: data.projectCode ?? null,
dateRequired: data.dateRequired ? new Date(data.dateRequired) : null,
piQuotationNo: data.piQuotationNo ?? null,
diff --git a/App/app/(portal)/po/new/new-po-form.tsx b/App/app/(portal)/po/new/new-po-form.tsx
index 26cc335..ecf4be2 100644
--- a/App/app/(portal)/po/new/new-po-form.tsx
+++ b/App/app/(portal)/po/new/new-po-form.tsx
@@ -143,6 +143,11 @@ export function NewPoForm({ vessels, accounts, vendors, companies, initialLineIt
/>
+
+
+
+
Optional — can be back-dated or forward-dated. Defaults to the approved date if left blank.
+
diff --git a/App/app/api/po/[id]/export/route.ts b/App/app/api/po/[id]/export/route.ts
index 761fa8e..fa50b2f 100644
--- a/App/app/api/po/[id]/export/route.ts
+++ b/App/app/api/po/[id]/export/route.ts
@@ -102,6 +102,9 @@ export async function GET(request: NextRequest, { params }: Props) {
.find(a => a.actionType === "APPROVED" || a.actionType === "APPROVED_WITH_NOTE");
const approvedBy = approvalAction?.actor.name ?? "";
+ // PO date: submitter-set date → approved date → creation date
+ const poDisplayDate = po.poDate ?? po.approvedAt ?? po.createdAt;
+
// Fetch approver's signature for embedding in the document
let signatureBase64: string | null = null;
let signatureMime = "image/png";
@@ -257,7 +260,7 @@ export async function GET(request: NextRequest, { params }: Props) {
sc(5, 3, po.poNumber, { font: { ...fBold, color: { argb: "FF1A1A1A" } }, border: bordAll, align: alignL });
ws.mergeCells("C5:G5");
sc(5, 8, "Date:", { font: fBold, fill: fillLbl, border: bordAll, align: alignR });
- sc(5, 9, fmtDate(po.createdAt), { font: fBase, border: bordAll, align: alignL });
+ sc(5, 9, fmtDate(poDisplayDate), { font: fBase, border: bordAll, align: alignL });
// ══ ROW 6: PI / Quotation ════════════════════════════════════════════════
ws.getRow(6).height = 16;
@@ -601,7 +604,7 @@ export async function GET(request: NextRequest, { params }: Props) {
Purchase Order No: |
${po.poNumber} |
Date: |
-
${fmtDate(po.createdAt)} |
+
${fmtDate(poDisplayDate)} |
| Performa Invoice / Quotation No: |
diff --git a/App/components/po/po-detail.tsx b/App/components/po/po-detail.tsx
index 0f08562..7c74b32 100644
--- a/App/components/po/po-detail.tsx
+++ b/App/components/po/po-detail.tsx
@@ -16,6 +16,7 @@ type PoWithRelations = {
status: import("@prisma/client").POStatus;
totalAmount: import("@prisma/client").Prisma.Decimal;
currency: string;
+ poDate: Date | null;
projectCode: string | null;
dateRequired: Date | null;
managerNote: string | null;
@@ -124,7 +125,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
vessel: string | null; vesselId: string;
account: string; accountId: string;
vendor: string | null; vendorId: string | null;
- projectCode: string | null; dateRequired: string | null; placeOfDelivery: string | null;
+ poDate: string | null; projectCode: string | null; dateRequired: string | null; placeOfDelivery: string | null;
};
};
const resubmitAction = [...po.actions]
@@ -234,6 +235,8 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
const currentVendor = po.vendor?.name ?? null;
const currentDateRequired = po.dateRequired?.toISOString() ?? null;
+ const currentPoDate = po.poDate?.toISOString() ?? null;
+
const fieldChanges: { label: string; before: string | null; after: string | null }[] = [];
if (snap.title !== po.title)
fieldChanges.push({ label: "Title", before: snap.title, after: po.title });
@@ -243,6 +246,12 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
fieldChanges.push({ label: "Account", before: snap.account, after: currentAccount });
if (snap.vendorId !== (po.vendor?.id ?? null))
fieldChanges.push({ label: "Vendor", before: snap.vendor ?? "None", after: currentVendor ?? "None" });
+ if ((snap.poDate ?? null) !== currentPoDate)
+ fieldChanges.push({
+ label: "PO Date",
+ before: snap.poDate ? formatDate(snap.poDate) : "—",
+ after: po.poDate ? formatDate(po.poDate) : "—",
+ });
if (snap.projectCode !== po.projectCode)
fieldChanges.push({ label: "Project Code", before: snap.projectCode ?? "—", after: po.projectCode ?? "—" });
if (snap.dateRequired !== currentDateRequired)
@@ -293,6 +302,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
{approvalAction && (
Approved By{approvalAction.actor.name}
)}
+ {po.poDate && PO Date{formatDate(po.poDate)}}
{po.projectCode && Project Code{po.projectCode}}
{po.dateRequired && Delivery Date Required{formatDate(po.dateRequired)}}
{po.piQuotationNo && PI / Quotation No.{po.piQuotationNo}}
diff --git a/App/lib/validations/po.ts b/App/lib/validations/po.ts
index 3623cb0..bd57712 100644
--- a/App/lib/validations/po.ts
+++ b/App/lib/validations/po.ts
@@ -32,6 +32,7 @@ export const createPoSchema = z.object({
vesselId: z.string().min(1, "Cost Centre is required"),
accountId: z.string().min(1, "Accounting Code is required"),
companyId: z.string().optional(),
+ poDate: z.string().optional(),
projectCode: z.string().optional(),
dateRequired: z.string().optional(),
vendorId: z.string().optional(),
diff --git a/App/prisma/migrations/20260616000000_add_po_date/migration.sql b/App/prisma/migrations/20260616000000_add_po_date/migration.sql
new file mode 100644
index 0000000..563bc0f
--- /dev/null
+++ b/App/prisma/migrations/20260616000000_add_po_date/migration.sql
@@ -0,0 +1,2 @@
+-- Add optional submitter-entered PO date to PurchaseOrder
+ALTER TABLE "PurchaseOrder" ADD COLUMN "poDate" TIMESTAMP(3);
diff --git a/App/prisma/schema.prisma b/App/prisma/schema.prisma
index bc23465..4c63d23 100644
--- a/App/prisma/schema.prisma
+++ b/App/prisma/schema.prisma
@@ -263,6 +263,7 @@ model PurchaseOrder {
tcTransitInsurance String?
tcPaymentTerms String?
tcOthers String?
+ poDate DateTime?
submittedAt DateTime?
approvedAt DateTime?
paidAt DateTime?
diff --git a/App/tests/unit/validations.test.ts b/App/tests/unit/validations.test.ts
index f1a98a9..99bf6ad 100644
--- a/App/tests/unit/validations.test.ts
+++ b/App/tests/unit/validations.test.ts
@@ -71,7 +71,7 @@ describe("lineItemSchema", () => {
const baseValidPo = {
title: "Test Purchase Order",
- costCentreRef: "v:vessel-123",
+ vesselId: "vessel-123",
accountId: "account-456",
lineItems: [{ name: "Item A", description: "Item A", quantity: "5", unit: "pc", unitPrice: "200" }],
};
@@ -97,21 +97,11 @@ describe("createPoSchema", () => {
expect(result.success).toBe(false);
});
- it("rejects missing costCentreRef", () => {
- const result = createPoSchema.safeParse({ ...baseValidPo, costCentreRef: "" });
+ it("rejects missing vesselId", () => {
+ const result = createPoSchema.safeParse({ ...baseValidPo, vesselId: "" });
expect(result.success).toBe(false);
});
- it("rejects invalid costCentreRef format", () => {
- const result = createPoSchema.safeParse({ ...baseValidPo, costCentreRef: "invalid-id" });
- expect(result.success).toBe(false);
- });
-
- it("accepts site costCentreRef (s: prefix)", () => {
- const result = createPoSchema.safeParse({ ...baseValidPo, costCentreRef: "s:site-123" });
- expect(result.success).toBe(true);
- });
-
it("rejects empty lineItems array", () => {
const result = createPoSchema.safeParse({ ...baseValidPo, lineItems: [] });
expect(result.success).toBe(false);
@@ -152,6 +142,27 @@ describe("createPoSchema", () => {
});
expect(result.success).toBe(true);
});
+
+ it("accepts poDate as an optional date string", () => {
+ const result = createPoSchema.safeParse({ ...baseValidPo, poDate: "2026-01-15" });
+ expect(result.success).toBe(true);
+ expect(result.success && result.data.poDate).toBe("2026-01-15");
+ });
+
+ it("accepts back-dated poDate", () => {
+ const result = createPoSchema.safeParse({ ...baseValidPo, poDate: "2025-06-01" });
+ expect(result.success).toBe(true);
+ });
+
+ it("accepts forward-dated poDate", () => {
+ const result = createPoSchema.safeParse({ ...baseValidPo, poDate: "2027-12-31" });
+ expect(result.success).toBe(true);
+ });
+
+ it("leaves poDate undefined when omitted", () => {
+ const result = createPoSchema.safeParse(baseValidPo);
+ expect(result.success && result.data.poDate).toBeUndefined();
+ });
});
// ── Constants ─────────────────────────────────────────────────────────────────