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 ─────────────────────────────────────────────────────────────────