import { auth } from "@/auth"; import { db } from "@/lib/db"; import { NextRequest, NextResponse } from "next/server"; import ExcelJS from "exceljs"; import { TC_FIXED_LINE, TC_DEFAULTS } from "@/lib/validations/po"; import { downloadBuffer } from "@/lib/storage"; // ── Company constants ───────────────────────────────────────────────────────── const CO_NAME = "PELAGIA MARINE SERVICES PVT. LTD"; const CO_ADDR = "Office address: 409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210"; const CO_TEL = "Tel: +91-22-6909 9028 / Email: technical@pelagiamarine.com / Mob: +91 74000 60772"; const INV_ADDR = "Pelagia Marine Services Pvt Ltd, 409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210 (MH)"; const INV_GST = "Email: accounts@pelagiamarine.com GST NO: 27AAHCP5787B1Z6"; // ── Helpers ─────────────────────────────────────────────────────────────────── function fmtDate(d: Date | null | undefined): string { if (!d) return ""; return new Date(d).toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" }); } function fmtNum(n: number, dec = 2): string { return n.toLocaleString("en-IN", { minimumFractionDigits: dec, maximumFractionDigits: dec }); } // ── Route ───────────────────────────────────────────────────────────────────── interface Props { params: Promise<{ id: string }> } export async function GET(request: NextRequest, { params }: Props) { const session = await auth(); if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); const { id } = await params; const po = await db.purchaseOrder.findUnique({ where: { id }, include: { submitter: true, vessel: true, account: true, vendor: true, lineItems: { orderBy: { sortOrder: "asc" } }, actions: { include: { actor: true }, orderBy: { createdAt: "asc" } }, }, }); if (!po) return NextResponse.json({ error: "Not found" }, { status: 404 }); const canViewAll = ["ACCOUNTS", "MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"].includes(session.user.role); if (!canViewAll && po.submitterId !== session.user.id) { 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 ───────────────────────────────────────────────────────── const items = po.lineItems.map((li, i) => { const qty = Number(li.quantity); const unitPrice = Number(li.unitPrice); const gstRate = Number((li as { gstRate?: unknown }).gstRate ?? 0.18); const taxable = Number(li.totalPrice); const gstAmt = taxable * gstRate; const li_ = li as typeof li & { name?: string }; const desc = li_.name ?? li.description ?? ""; return { sn: i + 1, desc, unit: li.unit, qty, unitPrice, gstRate, taxable, gstAmt, total: taxable + gstAmt }; }); const totalTaxable = items.reduce((s, i) => s + i.taxable, 0); const totalGst = items.reduce((s, i) => s + i.gstAmt, 0); const grandTotal = totalTaxable + totalGst; const approvalAction = [...po.actions].reverse() .find(a => a.actionType === "APPROVED" || a.actionType === "APPROVED_WITH_NOTE"); const approvedBy = approvalAction?.actor.name ?? ""; // Fetch approver's signature for embedding in the document let signatureBase64: string | null = null; let signatureMime = "image/png"; if (approvalAction) { const approver = await db.user.findUnique({ where: { id: approvalAction.actorId }, select: { signatureKey: true }, }); if (approver?.signatureKey) { const buf = await downloadBuffer(approver.signatureKey); if (buf) { signatureBase64 = buf.toString("base64"); const ext = approver.signatureKey.split(".").pop()?.toLowerCase(); signatureMime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png"; } } } const ext = po as { piQuotationNo?: string | null; piQuotationDate?: Date | null; requisitionNo?: string | null; requisitionDate?: Date | null; placeOfDelivery?: string | null; tcDelivery?: string | null; tcDispatch?: string | null; tcInspection?: string | null; tcTransitInsurance?: string | null; tcPaymentTerms?: string | null; tcOthers?: string | null; }; const piNo = ext.piQuotationNo ?? ""; const piDate = fmtDate(ext.piQuotationDate); const reqNo = ext.requisitionNo ?? ""; const reqDate = fmtDate(ext.requisitionDate); const delivery = ext.placeOfDelivery ?? ""; const tcLines: [number, string, string][] = [ [1, "", TC_FIXED_LINE], [2, "DELIVERY", ext.tcDelivery ?? TC_DEFAULTS.tcDelivery], [3, "DISPATCH INSTRUCTIONS", ext.tcDispatch ?? TC_DEFAULTS.tcDispatch], [4, "INSPECTION", ext.tcInspection ?? TC_DEFAULTS.tcInspection], [5, "TRANSIT INSURANCE", ext.tcTransitInsurance ?? TC_DEFAULTS.tcTransitInsurance], [6, "PAYMENT TERMS", ext.tcPaymentTerms ?? TC_DEFAULTS.tcPaymentTerms], ...(ext.tcOthers ? [[7, "OTHERS", ext.tcOthers] as [number, string, string]] : []), ]; const vendorAddr = [ po.vendor?.address, po.vendor?.gstin ? `GSTIN: ${po.vendor.gstin}` : null, ].filter(Boolean).join(" "); const vendorContact = [po.vendor?.contactName, po.vendor?.contactMobile, po.vendor?.contactEmail] .filter(Boolean).join(" "); // ── GST summary row label ───────────────────────────────────────────────── // If all items share the same GST rate, show it; otherwise show "GST" const gstRates = [...new Set(items.map(i => Math.round(i.gstRate * 100)))]; const gstLabel = gstRates.length === 1 ? `GST (${gstRates[0]}%)` : "GST"; // ═══════════════════════════════════════════════════════════════════════════ // XLSX — ExcelJS with full formatting // ═══════════════════════════════════════════════════════════════════════════ if (format === "xlsx") { const wb = new ExcelJS.Workbook(); wb.creator = "PPMS — Pelagia Payment Management System"; const ws = wb.addWorksheet(po.poNumber.replace(/\//g, "-"), { pageSetup: { paperSize: 9, orientation: "portrait", fitToPage: true, fitToWidth: 1, fitToHeight: 0 }, }); // ── Column widths (A-I) ───────────────────────────────────────────────── ws.columns = [ { width: 20 }, // A { width: 4 }, // B { width: 22 }, // C { width: 10 }, // D { width: 7 }, // E { width: 16 }, // F { width: 7 }, // G { width: 7 }, // H { width: 16 }, // I ]; // ── Style constants ───────────────────────────────────────────────────── const thin = (argb = "FF999999") => ({ style: "thin" as const, color: { argb } }); const med = (argb = "FF555555") => ({ style: "medium" as const, color: { argb } }); const bordAll = { top: thin(), left: thin(), bottom: thin(), right: thin() }; const bordMed = { top: med(), left: med(), bottom: med(), right: med() }; const ARIAL = "Arial"; const fBase = { name: ARIAL, size: 9 }; const fBold = { name: ARIAL, size: 9, bold: true }; const fTitle = { name: ARIAL, size: 13, bold: true }; const fH2 = { name: ARIAL, size: 11, bold: true }; const fSmall = { name: ARIAL, size: 8 }; const fillHdr = { type: "pattern" as const, pattern: "solid" as const, fgColor: { argb: "FFD8D8D8" } }; const fillLbl = { type: "pattern" as const, pattern: "solid" as const, fgColor: { argb: "FFF2F2F2" } }; const fillTot = { type: "pattern" as const, pattern: "solid" as const, fgColor: { argb: "FFF0F0F0" } }; const fillGT = { type: "pattern" as const, pattern: "solid" as const, fgColor: { argb: "FFD8D8D8" } }; const fillInst = { type: "pattern" as const, pattern: "solid" as const, fgColor: { argb: "FFEAEAEA" } }; const alignC: Partial = { horizontal: "center", vertical: "middle", wrapText: true }; const alignL: Partial = { horizontal: "left", vertical: "middle", wrapText: true }; const alignR: Partial = { horizontal: "right", vertical: "middle" }; // Helper: set cell value + style function sc(row: number, col: number, value: ExcelJS.CellValue, opts: { font?: Partial; fill?: ExcelJS.Fill; border?: Partial; align?: Partial; numFmt?: string; } = {}) { const cell = ws.getCell(row, col); cell.value = value; if (opts.font) cell.font = opts.font as ExcelJS.Font; if (opts.fill) cell.fill = opts.fill; if (opts.border) cell.border = opts.border; if (opts.align) cell.alignment = opts.align; if (opts.numFmt) cell.numFmt = opts.numFmt; } // Helper: apply border to a whole row range function rowBorder(row: number, c1: number, c2: number, border: Partial) { for (let c = c1; c <= c2; c++) ws.getCell(row, c).border = border; } // ── Row heights ───────────────────────────────────────────────────────── ws.getRow(1).height = 22; ws.getRow(2).height = 14; ws.getRow(3).height = 14; ws.getRow(4).height = 18; // ══ ROW 1: Company name ═════════════════════════════════════════════════ sc(1, 1, CO_NAME, { font: fTitle, align: alignC }); ws.mergeCells("A1:I1"); ws.getRow(1).getCell(1).border = { bottom: thin() }; // ══ ROW 2: Address ══════════════════════════════════════════════════════ sc(2, 1, CO_ADDR, { font: fBase, align: alignC }); ws.mergeCells("A2:I2"); // ══ ROW 3: Tel / Email ══════════════════════════════════════════════════ sc(3, 1, CO_TEL, { font: fSmall, align: alignC }); ws.mergeCells("A3:I3"); // ══ ROW 4: PURCHASE ORDER ════════════════════════════════════════════════ sc(4, 1, "PURCHASE ORDER", { font: { ...fH2, underline: true }, align: alignC }); ws.mergeCells("A4:I4"); ws.getRow(4).border = { top: thin(), bottom: thin() }; // ══ ROW 5: PO Number & Date ══════════════════════════════════════════════ ws.getRow(5).height = 16; sc(5, 1, "Purchase Order No:", { font: fBold, fill: fillLbl, border: bordAll, align: alignL }); ws.mergeCells("A5:B5"); 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 }); // ══ ROW 6: PI / Quotation ════════════════════════════════════════════════ ws.getRow(6).height = 14; sc(6, 1, "Performa Invoice / Quotation No:", { font: fBold, fill: fillLbl, border: bordAll, align: alignL }); ws.mergeCells("A6:B6"); sc(6, 3, piNo, { font: fBase, border: bordAll, align: alignL }); ws.mergeCells("C6:F6"); sc(6, 7, "P I / Quotation Date:", { font: fBold, fill: fillLbl, border: bordAll, align: alignR }); ws.mergeCells("G6:H6"); sc(6, 9, piDate, { font: fBase, border: bordAll, align: alignL }); // ══ ROW 7: Cost Centre / Account / Requested By ═══════════════════════ ws.getRow(7).height = 14; sc(7, 1, "Cost Centre", { font: fBold, fill: fillLbl, border: bordAll, align: alignL }); ws.mergeCells("A7:B7"); sc(7, 3, "Pelagia Marine Services Pvt. Ltd.", { font: fBase, border: bordAll, align: alignL }); sc(7, 4, "Account", { font: fBold, fill: fillLbl, border: bordAll, align: alignC }); ws.mergeCells("D7:E7"); sc(7, 6, po.account.code, { font: fBase, border: bordAll, align: alignC }); sc(7, 7, "Requested By", { font: fBold, fill: fillLbl, border: bordAll, align: alignC }); sc(7, 8, po.submitter.name, { font: fBase, border: bordAll, align: alignL }); ws.mergeCells("H7:I7"); // ══ ROW 8: Requisition / Approved By ═════════════════════════════════ ws.getRow(8).height = 14; sc(8, 1, "Cost Centre/Office Requisition Number", { font: fBold, fill: fillLbl, border: bordAll, align: alignL }); ws.mergeCells("A8:B8"); sc(8, 3, reqNo, { font: fBase, border: bordAll, align: alignL }); sc(8, 4, "Reqn. Date", { font: fBold, fill: fillLbl, border: bordAll, align: alignC }); ws.mergeCells("D8:E8"); sc(8, 6, reqDate, { font: fBase, border: bordAll, align: alignC }); sc(8, 7, "Approved By", { font: fBold, fill: fillLbl, border: bordAll, align: alignC }); sc(8, 8, approvedBy, { font: fBase, border: bordAll, align: alignL }); ws.mergeCells("H8:I8"); // ══ ROWS 9-10: Place of Delivery (2-row span) ═══════════════════════ ws.getRow(9).height = 14; ws.getRow(10).height = 14; sc(9, 1, "Place of Delivery", { font: fBold, fill: fillLbl, border: bordAll, align: alignC }); ws.mergeCells("A9:B10"); sc(9, 3, delivery, { font: fBase, border: bordAll, align: alignL }); ws.mergeCells("C9:I10"); // ══ ROWS 11-12: Invoice Details (2-row span) ════════════════════════ ws.getRow(11).height = 14; ws.getRow(12).height = 13; sc(11, 1, "Invoice Details", { font: fBold, fill: fillLbl, border: bordAll, align: alignC }); ws.mergeCells("A11:B12"); sc(11, 3, INV_ADDR, { font: fSmall, border: { top: thin(), left: thin(), right: thin() }, align: alignL }); ws.mergeCells("C11:I11"); sc(12, 3, INV_GST, { font: fSmall, border: { bottom: thin(), left: thin(), right: thin() }, align: alignL }); ws.mergeCells("C12:I12"); // ══ ROW 13: Vendor Name & Address ═══════════════════════════════════ ws.getRow(13).height = 28; sc(13, 1, "Vendor Name & Address", { font: fBold, fill: fillLbl, border: bordAll, align: alignC }); ws.mergeCells("A13:B13"); sc(13, 3, po.vendor?.name ?? "—", { font: fBold, border: bordAll, align: alignL }); sc(13, 4, vendorAddr, { font: fSmall, border: bordAll, align: alignL }); ws.mergeCells("D13:I13"); // ══ ROW 14: Contact ═════════════════════════════════════════════════ ws.getRow(14).height = 14; sc(14, 1, "Contact Person name / mobile no.", { font: fBold, fill: fillLbl, border: bordAll, align: alignC }); ws.mergeCells("A14:B14"); sc(14, 3, vendorContact, { font: fSmall, border: bordAll, align: alignL }); ws.mergeCells("C14:I14"); // ══ ROW 15: Line item header ═════════════════════════════════════════ const HDR_ROW = 15; ws.getRow(HDR_ROW).height = 16; const hdrStyle = { font: fBold, fill: fillHdr, border: bordAll }; sc(HDR_ROW, 1, "S.N.", { ...hdrStyle, align: alignC }); sc(HDR_ROW, 2, "Description", { ...hdrStyle, align: alignC }); ws.mergeCells(`B${HDR_ROW}:C${HDR_ROW}`); sc(HDR_ROW, 4, "Unit", { ...hdrStyle, align: alignC }); sc(HDR_ROW, 5, "Qnty", { ...hdrStyle, align: alignC }); sc(HDR_ROW, 6, "Unit price", { ...hdrStyle, align: alignC }); sc(HDR_ROW, 7, "Taxable cost",{ ...hdrStyle, align: alignC }); sc(HDR_ROW, 8, "GST%", { ...hdrStyle, align: alignC }); sc(HDR_ROW, 9, "Total cost", { ...hdrStyle, align: alignC }); // ══ Line items ═══════════════════════════════════════════════════════ const NUM_FMT = '#,##0.00'; const BODY_ROWS = Math.max(items.length, 7); // reserve at least 7 rows for (let idx = 0; idx < BODY_ROWS; idx++) { const r = HDR_ROW + 1 + idx; ws.getRow(r).height = 15; const item = items[idx]; const fillAlt = idx % 2 === 1 ? { type: "pattern" as const, pattern: "solid" as const, fgColor: { argb: "FFFAFAFA" } } : undefined; sc(r, 1, item?.sn ?? null, { font: fBase, fill: fillAlt, border: bordAll, align: alignC }); sc(r, 2, item?.desc ?? "", { font: fBase, fill: fillAlt, border: bordAll, align: alignL }); ws.mergeCells(`B${r}:C${r}`); sc(r, 4, item?.unit ?? "", { font: fBase, fill: fillAlt, border: bordAll, align: alignC }); sc(r, 5, item ? item.qty : null, { font: fBase, fill: fillAlt, border: bordAll, align: alignR, numFmt: NUM_FMT }); sc(r, 6, item ? item.unitPrice : null, { font: fBase, fill: fillAlt, border: bordAll, align: alignR, numFmt: NUM_FMT }); sc(r, 7, item ? item.taxable : null, { font: fBase, fill: fillAlt, border: bordAll, align: alignR, numFmt: NUM_FMT }); sc(r, 8, item ? item.gstRate : null, { font: fBase, fill: fillAlt, border: bordAll, align: alignC, numFmt: '0%' }); sc(r, 9, item ? item.total : null, { font: { ...fBase, bold: !!item }, fill: fillAlt, border: bordAll, align: alignR, numFmt: NUM_FMT }); } // ══ Totals ════════════════════════════════════════════════════════════ const TOT_ROW = HDR_ROW + 1 + BODY_ROWS + 1; ws.getRow(TOT_ROW).height = 14; ws.getRow(TOT_ROW + 1).height = 14; ws.getRow(TOT_ROW + 2).height = 16; // "Total taxable value" sc(TOT_ROW, 6, "Total taxable value", { font: fBold, fill: fillTot, border: bordAll, align: alignR }); ws.mergeCells(`F${TOT_ROW}:G${TOT_ROW}`); sc(TOT_ROW, 8, totalTaxable, { font: fBold, fill: fillTot, border: bordAll, align: alignR, numFmt: NUM_FMT }); ws.mergeCells(`H${TOT_ROW}:I${TOT_ROW}`); // "GST" sc(TOT_ROW + 1, 6, gstLabel, { font: fBold, fill: fillTot, border: bordAll, align: alignR }); ws.mergeCells(`F${TOT_ROW + 1}:G${TOT_ROW + 1}`); sc(TOT_ROW + 1, 8, totalGst, { font: fBold, fill: fillTot, border: bordAll, align: alignR, numFmt: NUM_FMT }); ws.mergeCells(`H${TOT_ROW + 1}:I${TOT_ROW + 1}`); // "GRAND TOTAL" sc(TOT_ROW + 2, 6, "GRAND TOTAL", { font: { ...fBold, size: 10 }, fill: fillGT, border: bordAll, align: alignR }); ws.mergeCells(`F${TOT_ROW + 2}:G${TOT_ROW + 2}`); sc(TOT_ROW + 2, 8, grandTotal, { font: { ...fBold, size: 10 }, fill: fillGT, border: bordAll, align: alignR, numFmt: NUM_FMT }); ws.mergeCells(`H${TOT_ROW + 2}:I${TOT_ROW + 2}`); // ══ Instructions ═════════════════════════════════════════════════════ const INST_ROW = TOT_ROW + 4; ws.getRow(INST_ROW).height = 16; sc(INST_ROW, 1, "INSTRUCTIONS TO VENDORS", { font: { ...fBold, size: 10 }, fill: fillInst, border: bordAll, align: alignC }); ws.mergeCells(`A${INST_ROW}:I${INST_ROW}`); tcLines.forEach(([num, label, value], i) => { const r = INST_ROW + 1 + i; ws.getRow(r).height = 14; sc(r, 1, num, { font: fBase, border: bordAll, align: alignC }); const text = label ? `${label}: ${value}` : value; sc(r, 2, text, { font: fBase, border: bordAll, align: alignL }); ws.mergeCells(`B${r}:I${r}`); }); // ══ Signatures ═══════════════════════════════════════════════════════ const SIG_ROW = INST_ROW + tcLines.length + 3; ws.getRow(SIG_ROW).height = 40; ws.getRow(SIG_ROW + 1).height = 14; ws.getRow(SIG_ROW + 2).height = 14; // Left sig block (approver — the manager who authorized the PO) if (signatureBase64) { const imgType = signatureMime === "image/jpeg" ? "jpeg" : "png"; const imgId = wb.addImage({ base64: signatureBase64, extension: imgType }); // Span the image across columns A-D in the sig row ws.addImage(imgId, { tl: { col: 0, row: SIG_ROW - 1 }, br: { col: 4, row: SIG_ROW }, editAs: "oneCell", }); sc(SIG_ROW, 1, "", { border: { top: thin(), left: thin(), right: thin() } }); } else { 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, { 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}`); sc(SIG_ROW + 3, 1, "For, Pelagia Marine Services Pvt. Ltd.", { font: fSmall, border: { left: thin(), bottom: thin(), right: thin() }, align: alignC }); ws.mergeCells(`A${SIG_ROW + 3}:D${SIG_ROW + 3}`); // Adjust row heights when signature present ws.getRow(SIG_ROW + 1).height = 14; ws.getRow(SIG_ROW + 2).height = 14; ws.getRow(SIG_ROW + 3).height = 14; // Right sig block (vendor) const vName = po.vendor?.name ?? ""; sc(SIG_ROW, 6, vName, { font: fBold, border: { top: thin(), left: thin(), right: thin() }, align: alignC }); ws.mergeCells(`F${SIG_ROW}:I${SIG_ROW}`); sc(SIG_ROW + 1, 6, "Authorized Signatory & Stamp", { font: fSmall, border: { left: thin(), right: thin() }, align: alignC }); ws.mergeCells(`F${SIG_ROW + 1}:I${SIG_ROW + 1}`); sc(SIG_ROW + 2, 6, `For, ${vName}`, { font: fSmall, border: { left: thin(), bottom: thin(), right: thin() }, align: alignC }); ws.mergeCells(`F${SIG_ROW + 2}:I${SIG_ROW + 2}`); // ── Serialise ───────────────────────────────────────────────────────── const buf = await wb.xlsx.writeBuffer(); const slug = po.poNumber.replace(/\//g, "-"); return new NextResponse(Buffer.from(buf), { headers: { "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Content-Disposition": `attachment; filename="${slug}.xlsx"`, }, }); } // ═══════════════════════════════════════════════════════════════════════════ // PDF — HTML print page matching sample layout // ═══════════════════════════════════════════════════════════════════════════ const itemRows = items.map((item, i) => ` ${item.sn} ${item.desc} ${item.unit} ${fmtNum(item.qty, item.qty % 1 === 0 ? 0 : 3)} ${fmtNum(item.unitPrice)} ${fmtNum(item.taxable)} ${Math.round(item.gstRate * 100)}% ${fmtNum(item.total)} `).join(""); // Pad to at least 7 rows so the table doesn't collapse const blankRows = Math.max(0, 7 - items.length); const padRows = Array(blankRows).fill( '' ).join(""); const termRows = tcLines.map(([num, label, value]) => ` ${num}. ${label ? `${label}: ` : ""}${value} `).join(""); const html = ` ${po.poNumber} — Purchase Order
${CO_NAME}
${CO_ADDR}
${CO_TEL}
PURCHASE ORDER
Purchase Order No: ${po.poNumber} Date: ${fmtDate(po.createdAt)}
Performa Invoice / Quotation No: ${piNo} P I / Quotation Date: ${piDate}
Cost Centre Pelagia Marine Services Pvt. Ltd. Account ${po.account.code} Requested By ${po.submitter.name}
Cost Centre/Office Requisition Number ${reqNo} Reqn. Date ${reqDate} Approved By ${approvedBy}
Place of Delivery ${delivery}
Invoice Details ${INV_ADDR}
${INV_GST}
Vendor Name & Address ${po.vendor?.name ?? "—"} ${vendorAddr}
Contact Person name / mobile no. ${vendorContact}
${itemRows} ${padRows}
S.N. Description Unit Qnty Unit price Taxable cost GST% Total cost
Total taxable value ${fmtNum(totalTaxable)}
${gstLabel} ${fmtNum(totalGst)}
GRAND TOTAL ${fmtNum(grandTotal)}
${termRows}
INSTRUCTIONS TO VENDORS
${signatureBase64 ? `Signature` : `
${approvedBy}
` }
${approvedBy}
Authorized Signatory & Stamp
For, Pelagia Marine Services Pvt. Ltd.
${po.vendor?.name ?? ""}
Authorized Signatory & Stamp
For, ${po.vendor?.name ?? ""}
`; return new NextResponse(html, { headers: { "Content-Type": "text/html; charset=utf-8" } }); }