Merge pull request 'fix: Submitter can set an optional PO date (back/forward-datable)' (#16) from claude/issue-4 into master
Reviewed-on: https://git.pelagiamarine.com/shad0w/pelagia-portal/pulls/16
This commit is contained in:
commit
feb6fb745a
10 changed files with 63 additions and 17 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-1.5">PO Date</label>
|
||||
<input name="poDate" type="date" defaultValue={poDateValue} className={INPUT_CLS} />
|
||||
<p className="mt-1 text-xs text-neutral-400">Optional — can be back-dated or forward-dated. Defaults to the approved date if left blank.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-1.5">Project Code</label>
|
||||
<input name="projectCode" defaultValue={po.projectCode ?? ""} className={INPUT_CLS} placeholder="Optional" />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -143,6 +143,11 @@ export function NewPoForm({ vessels, accounts, vendors, companies, initialLineIt
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-1.5">PO Date</label>
|
||||
<input name="poDate" type="date" className={INPUT_CLS} />
|
||||
<p className="mt-1 text-xs text-neutral-400">Optional — can be back-dated or forward-dated. Defaults to the approved date if left blank.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-1.5">Project Code</label>
|
||||
<input name="projectCode" className={INPUT_CLS} placeholder="Optional" />
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<td class="lbl" style="width:22%">Purchase Order No:</td>
|
||||
<td style="width:28%;font-weight:bold">${po.poNumber}</td>
|
||||
<td class="lbl" style="width:14%;text-align:right">Date:</td>
|
||||
<td style="width:36%">${fmtDate(po.createdAt)}</td>
|
||||
<td style="width:36%">${fmtDate(poDisplayDate)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="lbl">Performa Invoice / Quotation No:</td>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<div><dt className="text-neutral-500">Approved By</dt><dd className="font-medium text-neutral-900">{approvalAction.actor.name}</dd></div>
|
||||
)}
|
||||
{po.poDate && <div><dt className="text-neutral-500">PO Date</dt><dd className="font-medium text-neutral-900">{formatDate(po.poDate)}</dd></div>}
|
||||
{po.projectCode && <div><dt className="text-neutral-500">Project Code</dt><dd className="font-medium text-neutral-900">{po.projectCode}</dd></div>}
|
||||
{po.dateRequired && <div><dt className="text-neutral-500">Delivery Date Required</dt><dd className="font-medium text-neutral-900">{formatDate(po.dateRequired)}</dd></div>}
|
||||
{po.piQuotationNo && <div><dt className="text-neutral-500">PI / Quotation No.</dt><dd className="font-medium text-neutral-900">{po.piQuotationNo}</dd></div>}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
-- Add optional submitter-entered PO date to PurchaseOrder
|
||||
ALTER TABLE "PurchaseOrder" ADD COLUMN "poDate" TIMESTAMP(3);
|
||||
|
|
@ -263,6 +263,7 @@ model PurchaseOrder {
|
|||
tcTransitInsurance String?
|
||||
tcPaymentTerms String?
|
||||
tcOthers String?
|
||||
poDate DateTime?
|
||||
submittedAt DateTime?
|
||||
approvedAt DateTime?
|
||||
paidAt DateTime?
|
||||
|
|
|
|||
|
|
@ -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 ─────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue