fix(export): gate PDF/XLSX on manager-approved status; drop submitter-name fallback

- Export API returns 403 for any PO not yet approved (DRAFT, SUBMITTED,
  MGR_REVIEW, EDITS_REQUESTED, VENDOR_ID_PENDING, REJECTED) — only
  MGR_APPROVED, SENT_FOR_PAYMENT, PAID_DELIVERED, PARTIALLY_CLOSED and
  CLOSED are exportable.
- The submitter's name is no longer used as a signatory fallback; since
  export is blocked until after manager approval an approver always exists.
- Export PDF / Export XLSX buttons are hidden in po-detail for pre-approval
  statuses, so users never encounter the 403 through normal UI flows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-05-16 20:50:26 +05:30
parent 340a3dcce0
commit 8322f33880
2 changed files with 31 additions and 18 deletions

View file

@ -47,6 +47,16 @@ export async function GET(request: NextRequest, { params }: Props) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Exports are only available for approved POs — manager approval is a prerequisite for a valid PO document.
// The submitter's signature is never embedded; only the approving manager's signature is used.
const EXPORTABLE_STATUSES = ["MGR_APPROVED", "SENT_FOR_PAYMENT", "PAID_DELIVERED", "PARTIALLY_CLOSED", "CLOSED"];
if (!EXPORTABLE_STATUSES.includes(po.status)) {
return NextResponse.json(
{ error: "Export is only available for approved purchase orders." },
{ status: 403 }
);
}
const format = request.nextUrl.searchParams.get("format") ?? "pdf";
// ── Computed data ─────────────────────────────────────────────────────────
@ -386,10 +396,10 @@ export async function GET(request: NextRequest, { params }: Props) {
});
sc(SIG_ROW, 1, "", { border: { top: thin(), left: thin(), right: thin() } });
} else {
sc(SIG_ROW, 1, approvedBy || po.submitter.name, { font: fBold, border: { top: thin(), left: thin(), right: thin() }, align: alignC });
sc(SIG_ROW, 1, approvedBy, { font: fBold, border: { top: thin(), left: thin(), right: thin() }, align: alignC });
}
ws.mergeCells(`A${SIG_ROW}:D${SIG_ROW}`);
sc(SIG_ROW + 1, 1, approvedBy || po.submitter.name, { font: fBold, border: { left: thin(), right: thin() }, align: alignC });
sc(SIG_ROW + 1, 1, approvedBy, { font: fBold, border: { left: thin(), right: thin() }, align: alignC });
ws.mergeCells(`A${SIG_ROW + 1}:D${SIG_ROW + 1}`);
sc(SIG_ROW + 2, 1, "Authorized Signatory & Stamp", { font: fSmall, border: { left: thin(), right: thin() }, align: alignC });
ws.mergeCells(`A${SIG_ROW + 2}:D${SIG_ROW + 2}`);
@ -675,10 +685,10 @@ export async function GET(request: NextRequest, { params }: Props) {
<div class="sig-box">
${signatureBase64
? `<img src="data:${signatureMime};base64,${signatureBase64}" alt="Signature" style="max-height:48px;max-width:180px;object-fit:contain;display:block;margin:0 auto 4px;" />`
: `<div class="sig-name">${approvedBy || po.submitter.name}</div>`
: `<div class="sig-name">${approvedBy}</div>`
}
<div>
<div class="sig-sub" style="font-weight:bold">${approvedBy || po.submitter.name}</div>
<div class="sig-sub" style="font-weight:bold">${approvedBy}</div>
<div class="sig-sub">Authorized Signatory &amp; Stamp</div>
<div class="sig-sub">For, Pelagia Marine Services Pvt. Ltd.</div>
</div>

View file

@ -183,20 +183,23 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
!readOnly && (
<DiscardDraftButton poId={po.id} />
)}
<a
href={`/api/po/${po.id}/export?format=pdf`}
target="_blank"
rel="noopener noreferrer"
className="rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
>
Export PDF
</a>
<a
href={`/api/po/${po.id}/export?format=xlsx`}
className="rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
>
Export XLSX
</a>
{/* Export buttons — only available once the PO has been approved by a manager */}
{["MGR_APPROVED", "SENT_FOR_PAYMENT", "PAID_DELIVERED", "PARTIALLY_CLOSED", "CLOSED"].includes(po.status) && (<>
<a
href={`/api/po/${po.id}/export?format=pdf`}
target="_blank"
rel="noopener noreferrer"
className="rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
>
Export PDF
</a>
<a
href={`/api/po/${po.id}/export?format=xlsx`}
className="rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
>
Export XLSX
</a>
</>)}
</div>
</div>