From 0252e8eab4e7286f851adb5c9e56186a3e5714d5 Mon Sep 17 00:00:00 2001 From: Hardik Date: Sat, 16 May 2026 04:17:51 +0530 Subject: [PATCH] feat(approvals): highlight submitter's edits to manager on resubmission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a submitter edits and resubmits a PO after the manager requested edits (EDITS_REQUESTED → MGR_REVIEW), the manager now sees exactly what changed. Changes: - edit/actions.ts: before mutating the PO, snapshot the current state (line items + header fields incl. vessel/account/vendor names) into the SUBMITTED action's metadata as { editSnapshot: { lineItems, fields } }. - po-line-items-editor: add `originalItemsLabel` prop so the diff banner message can be context-specific (manager edit vs. submitter resubmit). - po-detail: detect the resubmit snapshot from the most recent SUBMITTED action with editSnapshot metadata; show a "Changes from last review" amber table listing every header field the submitter changed (title, cost centre, account, vendor, project code, date required, place of delivery); pass the resubmit snapshot as originalItems to LineItemsEditor so changed line items are highlighted with strikethrough on prior values. Resubmit snapshot takes priority over manager-line-edit diff. Panel is only visible to MANAGER / SUPERUSER when status is MGR_REVIEW. Co-Authored-By: Claude Sonnet 4.6 --- .../app/(portal)/po/[id]/edit/actions.ts | 59 +++++++++- .../components/po/po-detail.tsx | 103 +++++++++++++++++- .../components/po/po-line-items-editor.tsx | 5 +- 3 files changed, 161 insertions(+), 6 deletions(-) diff --git a/App/pelagia-portal/app/(portal)/po/[id]/edit/actions.ts b/App/pelagia-portal/app/(portal)/po/[id]/edit/actions.ts index ac7dd8f..8d5d7e6 100644 --- a/App/pelagia-portal/app/(portal)/po/[id]/edit/actions.ts +++ b/App/pelagia-portal/app/(portal)/po/[id]/edit/actions.ts @@ -76,6 +76,59 @@ export async function updatePo( const isResubmit = intent === "resubmit" && po.status === "EDITS_REQUESTED"; + // Before mutating, snapshot the current PO state so the manager can see + // exactly what the submitter changed when they resubmit after edits requested. + let resubmitSnapshot: { + lineItems: Array<{ + name: string; description: string | null; quantity: number; + unit: string; size: string | null; unitPrice: number; gstRate: number; + }>; + fields: { + title: string; + vessel: string | null; vesselId: string; + account: string; accountId: string; + vendor: string | null; vendorId: string | null; + projectCode: string | null; dateRequired: string | null; placeOfDelivery: string | null; + }; + } | null = null; + + if (isResubmit) { + const currentPo = await db.purchaseOrder.findUnique({ + where: { id: poId }, + include: { + lineItems: { orderBy: { sortOrder: "asc" } }, + vessel: true, + account: true, + vendor: true, + }, + }); + if (currentPo) { + resubmitSnapshot = { + lineItems: currentPo.lineItems.map((li) => ({ + name: li.name, + description: li.description, + quantity: Number(li.quantity), + unit: li.unit, + size: li.size, + unitPrice: Number(li.unitPrice), + gstRate: Number(li.gstRate), + })), + fields: { + title: currentPo.title, + vessel: currentPo.vessel?.name ?? null, + vesselId: currentPo.vesselId, + account: `${currentPo.account.name} (${currentPo.account.code})`, + accountId: currentPo.accountId, + vendor: currentPo.vendor?.name ?? null, + vendorId: currentPo.vendorId, + projectCode: currentPo.projectCode, + dateRequired: currentPo.dateRequired?.toISOString() ?? null, + placeOfDelivery: currentPo.placeOfDelivery, + }, + }; + } + } + await db.purchaseOrder.update({ where: { id: poId }, data: { @@ -116,7 +169,11 @@ export async function updatePo( }, actions: { create: isResubmit - ? { actionType: "SUBMITTED", actorId: session.user.id } + ? { + actionType: "SUBMITTED", + actorId: session.user.id, + ...(resubmitSnapshot ? { metadata: { editSnapshot: resubmitSnapshot } } : {}), + } : undefined, }, }, diff --git a/App/pelagia-portal/components/po/po-detail.tsx b/App/pelagia-portal/components/po/po-detail.tsx index 7bc651a..6591904 100644 --- a/App/pelagia-portal/components/po/po-detail.tsx +++ b/App/pelagia-portal/components/po/po-detail.tsx @@ -5,6 +5,7 @@ import { DiscardDraftButton } from "@/components/po/discard-draft-button"; import { formatCurrency, formatDate, formatDateTime } from "@/lib/utils"; import { generateDownloadUrl } from "@/lib/storage"; import { TC_FIXED_LINE } from "@/lib/validations/po"; +import type { LineItemInput } from "@/lib/validations/po"; import type { Role } from "@prisma/client"; type PoWithRelations = { @@ -100,9 +101,39 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals const managerEditAction = [...po.actions] .reverse() .find((a) => a.actionType === "MANAGER_LINE_EDIT"); - const originalLineItems = managerEditAction - ? (managerEditAction.metadata as { original: typeof lineItemsForEditor } | null)?.original - : undefined; + + // Resubmit snapshot: stored in the most recent SUBMITTED action's metadata + // when the submitter resubmits after EDITS_REQUESTED. + type ResubmitSnapshot = { + lineItems: LineItemInput[]; + fields: { + title: string; + vessel: string | null; vesselId: string; + account: string; accountId: string; + vendor: string | null; vendorId: string | null; + projectCode: string | null; dateRequired: string | null; placeOfDelivery: string | null; + }; + }; + const resubmitAction = [...po.actions] + .reverse() + .find( + (a) => + a.actionType === "SUBMITTED" && + !!(a.metadata as { editSnapshot?: unknown } | null)?.editSnapshot + ); + const resubmitSnapshot = resubmitAction + ? (resubmitAction.metadata as { editSnapshot: ResubmitSnapshot }).editSnapshot + : null; + + // Resubmit snapshot takes priority over manager line edit diff. + const originalLineItems: LineItemInput[] | undefined = resubmitSnapshot?.lineItems + ?? (managerEditAction + ? (managerEditAction.metadata as { original: typeof lineItemsForEditor } | null)?.original + : undefined); + + const lineItemsDiffLabel = resubmitSnapshot + ? "Submitter updated these line items after edits were requested. Previous values shown with strikethrough." + : "Line items were amended by manager. Current values shown; original values shown with strikethrough."; const downloadUrls = await Promise.all( po.documents.map((doc) => generateDownloadUrl(doc.storageKey)) @@ -170,6 +201,65 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals )} + {/* Submitter changes banner — shown to managers when PO is resubmitted after edits */} + {resubmitSnapshot && + po.status === "MGR_REVIEW" && + (currentRole === "MANAGER" || currentRole === "SUPERUSER") && (() => { + const snap = resubmitSnapshot.fields; + const currentVessel = po.vessel?.name ?? null; + const currentAccount = `${po.account.name} (${po.account.code})`; + const currentVendor = po.vendor?.name ?? null; + const currentDateRequired = po.dateRequired?.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 }); + if (snap.vesselId !== po.vessel.id) + fieldChanges.push({ label: "Cost Centre", before: snap.vessel, after: currentVessel }); + if (snap.accountId !== po.account.id) + 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.projectCode !== po.projectCode) + fieldChanges.push({ label: "Project Code", before: snap.projectCode ?? "—", after: po.projectCode ?? "—" }); + if (snap.dateRequired !== currentDateRequired) + fieldChanges.push({ + label: "Date Required", + before: snap.dateRequired ? formatDate(snap.dateRequired) : "—", + after: po.dateRequired ? formatDate(po.dateRequired) : "—", + }); + if (snap.placeOfDelivery !== po.placeOfDelivery) + fieldChanges.push({ label: "Place of Delivery", before: snap.placeOfDelivery ?? "—", after: po.placeOfDelivery ?? "—" }); + + if (fieldChanges.length === 0) return null; + + return ( +
+

+ Submitter updated the following fields after edits were requested +

+ + + + + + + + + + {fieldChanges.map(({ label, before, after }) => ( + + + + + + ))} + +
FieldBeforeAfter
{label}{before}{after}
+
+ ); + })()} + {/* Order Details */}

Order Details

@@ -238,7 +328,12 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals {/* Line Items */}

Line Items

- +
{/* Terms & Conditions */} diff --git a/App/pelagia-portal/components/po/po-line-items-editor.tsx b/App/pelagia-portal/components/po/po-line-items-editor.tsx index a33f36f..f37cccf 100644 --- a/App/pelagia-portal/components/po/po-line-items-editor.tsx +++ b/App/pelagia-portal/components/po/po-line-items-editor.tsx @@ -40,6 +40,8 @@ interface Props { onChange?: (items: LineItemInput[]) => void; readOnly?: boolean; originalItems?: LineItemInput[]; + /** Label shown in the diff banner when originalItems is provided */ + originalItemsLabel?: string; /** When true, show per-row account selector */ multiAccount?: boolean; accounts?: AccountOption[]; @@ -202,6 +204,7 @@ export function LineItemsEditor({ onChange, readOnly = false, originalItems, + originalItemsLabel, multiAccount = false, accounts = [], defaultAccountId, @@ -256,7 +259,7 @@ export function LineItemsEditor({
{hasDiff && (

- Line items were amended by manager. Current values shown; original values shown with strikethrough. + {originalItemsLabel ?? "Line items were amended by manager. Current values shown; original values shown with strikethrough."}

)}