303 lines
7.9 KiB
TypeScript
303 lines
7.9 KiB
TypeScript
import { jsPDF } from 'jspdf';
|
|
import autoTable from 'jspdf-autotable';
|
|
|
|
export interface ExportColumn {
|
|
header: string;
|
|
key: string;
|
|
format?: (value: any, row: any) => string;
|
|
}
|
|
|
|
export interface ExportDataOptions {
|
|
title: string;
|
|
subtitle?: string;
|
|
filename: string;
|
|
columns: ExportColumn[];
|
|
data: any[];
|
|
}
|
|
|
|
/**
|
|
* Converts dataset into standard CSV string and triggers browser download
|
|
*/
|
|
export function exportToSpreadsheet({ filename, columns, data }: ExportDataOptions): void {
|
|
if (!data || data.length === 0) return;
|
|
|
|
const headers = columns.map((col) => `"${(col.header || '').replace(/"/g, '""')}"`).join(',');
|
|
const rows = data.map((row) =>
|
|
columns
|
|
.map((col) => {
|
|
let val: any;
|
|
if (col.format) {
|
|
val = col.format(row[col.key], row);
|
|
} else {
|
|
val = row[col.key];
|
|
}
|
|
if (val === null || val === undefined) val = '';
|
|
const strVal = String(val).replace(/"/g, '""');
|
|
return `"${strVal}"`;
|
|
})
|
|
.join(',')
|
|
);
|
|
|
|
const csvContent = '\uFEFF' + [headers, ...rows].join('\r\n');
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', `${filename.replace(/\.csv$/i, '')}.csv`);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/**
|
|
* Draws the high-visibility official PFM vector brand insignia onto the jsPDF document.
|
|
*/
|
|
function drawPfmLogo(doc: jsPDF, x: number, y: number, size: number): void {
|
|
const scale = size / 64;
|
|
doc.saveGraphicsState();
|
|
|
|
// Dark foundation disc
|
|
doc.setFillColor(9, 13, 22);
|
|
doc.circle(x + 32 * scale, y + 32 * scale, 31 * scale, 'F');
|
|
doc.setDrawColor(30, 41, 59);
|
|
doc.setLineWidth(1 * scale);
|
|
doc.circle(x + 32 * scale, y + 32 * scale, 30 * scale, 'S');
|
|
|
|
// Cyan/Blue Outer hexagonal shield ring
|
|
doc.setDrawColor(59, 130, 246);
|
|
doc.setLineWidth(1.2 * scale);
|
|
(doc as any).polygon(
|
|
[
|
|
{ x: x + 32 * scale, y: y + 6.5 * scale },
|
|
{ x: x + 53.5 * scale, y: y + 19 * scale },
|
|
{ x: x + 56 * scale, y: y + 23.2 * scale },
|
|
{ x: x + 56 * scale, y: y + 40.8 * scale },
|
|
{ x: x + 53.5 * scale, y: y + 45 * scale },
|
|
{ x: x + 32 * scale, y: y + 57.5 * scale },
|
|
{ x: x + 27 * scale, y: y + 57.5 * scale },
|
|
{ x: x + 5.5 * scale, y: y + 45 * scale },
|
|
{ x: x + 3 * scale, y: y + 40.8 * scale },
|
|
{ x: x + 3 * scale, y: y + 23.2 * scale },
|
|
{ x: x + 5.5 * scale, y: y + 19 * scale },
|
|
{ x: x + 27 * scale, y: y + 6.5 * scale },
|
|
],
|
|
'S'
|
|
);
|
|
|
|
// Top Facet - Indigo/Violet Governance Canopy
|
|
doc.setFillColor(99, 102, 241);
|
|
doc.triangle(
|
|
x + 32 * scale,
|
|
y + 9 * scale,
|
|
x + 51.5 * scale,
|
|
y + 20.2 * scale,
|
|
x + 43 * scale,
|
|
y + 25.2 * scale,
|
|
'F'
|
|
);
|
|
doc.triangle(
|
|
x + 32 * scale,
|
|
y + 9 * scale,
|
|
x + 43 * scale,
|
|
y + 25.2 * scale,
|
|
x + 21 * scale,
|
|
y + 25.2 * scale,
|
|
'F'
|
|
);
|
|
doc.triangle(
|
|
x + 32 * scale,
|
|
y + 9 * scale,
|
|
x + 21 * scale,
|
|
y + 25.2 * scale,
|
|
x + 12.5 * scale,
|
|
y + 20.2 * scale,
|
|
'F'
|
|
);
|
|
|
|
// Left Facet - Electric Sapphire Capital Vault
|
|
doc.setFillColor(37, 99, 235);
|
|
doc.triangle(
|
|
x + 10 * scale,
|
|
y + 23 * scale,
|
|
x + 19.2 * scale,
|
|
y + 28.4 * scale,
|
|
x + 19.2 * scale,
|
|
y + 44.5 * scale,
|
|
'F'
|
|
);
|
|
doc.triangle(
|
|
x + 10 * scale,
|
|
y + 23 * scale,
|
|
x + 19.2 * scale,
|
|
y + 44.5 * scale,
|
|
x + 30 * scale,
|
|
y + 50.8 * scale,
|
|
'F'
|
|
);
|
|
|
|
// Right Facet - Cyan/Emerald Performance Wing
|
|
doc.setFillColor(6, 182, 212);
|
|
doc.triangle(
|
|
x + 54 * scale,
|
|
y + 23 * scale,
|
|
x + 44.8 * scale,
|
|
y + 28.4 * scale,
|
|
x + 44.8 * scale,
|
|
y + 44.5 * scale,
|
|
'F'
|
|
);
|
|
doc.triangle(
|
|
x + 54 * scale,
|
|
y + 23 * scale,
|
|
x + 44.8 * scale,
|
|
y + 44.5 * scale,
|
|
x + 34 * scale,
|
|
y + 50.8 * scale,
|
|
'F'
|
|
);
|
|
|
|
// Center Plate
|
|
doc.setFillColor(15, 23, 42);
|
|
doc.setDrawColor(51, 65, 85);
|
|
(doc as any).polygon(
|
|
[
|
|
{ x: x + 22 * scale, y: y + 28.5 * scale },
|
|
{ x: x + 32 * scale, y: y + 22.8 * scale },
|
|
{ x: x + 42 * scale, y: y + 28.5 * scale },
|
|
{ x: x + 42 * scale, y: y + 35.5 * scale },
|
|
{ x: x + 32 * scale, y: y + 41.2 * scale },
|
|
{ x: x + 22 * scale, y: y + 35.5 * scale },
|
|
],
|
|
'FD'
|
|
);
|
|
|
|
// Center Diamond Spark
|
|
doc.setFillColor(255, 255, 255);
|
|
doc.circle(x + 32 * scale, y + 32 * scale, 2.5 * scale, 'F');
|
|
|
|
doc.restoreGraphicsState();
|
|
}
|
|
|
|
/**
|
|
* Generates an executive-grade PDF document in LANDSCAPE mode with clear PFM logo visibility,
|
|
* metadata header, and structured autoTable data.
|
|
*/
|
|
export function exportToPdf({ title, subtitle, filename, columns, data }: ExportDataOptions): void {
|
|
if (!data || data.length === 0) return;
|
|
|
|
// Enforce Landscape orientation
|
|
const doc = new jsPDF({
|
|
orientation: 'landscape',
|
|
unit: 'pt',
|
|
format: 'a4',
|
|
});
|
|
|
|
const pageWidth = doc.internal.pageSize.getWidth();
|
|
const pageHeight = doc.internal.pageSize.getHeight();
|
|
const margin = 40;
|
|
|
|
// Header background banner
|
|
doc.setFillColor(13, 18, 31);
|
|
doc.rect(0, 0, pageWidth, 72, 'F');
|
|
doc.setDrawColor(30, 41, 59);
|
|
doc.setLineWidth(1);
|
|
doc.line(0, 72, pageWidth, 72);
|
|
|
|
// Draw PFM Logo with clear visibility
|
|
const logoSize = 44;
|
|
drawPfmLogo(doc, margin, 14, logoSize);
|
|
|
|
// PFM Branding Text next to Logo
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.setFont('helvetica', 'bold');
|
|
doc.setFontSize(14);
|
|
doc.text('PFM Console', margin + logoSize + 12, 33);
|
|
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(56, 189, 248); // Cyan
|
|
doc.text('POST-FUND MANAGEMENT SYSTEM • OFFICIAL REPORT', margin + logoSize + 12, 46);
|
|
|
|
// Document Title & Subtitle in Header (Right aligned or center)
|
|
doc.setFont('helvetica', 'bold');
|
|
doc.setFontSize(13);
|
|
doc.setTextColor(248, 250, 252);
|
|
doc.text(title, pageWidth - margin, 32, { align: 'right' });
|
|
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(148, 163, 184);
|
|
const nowStr = new Date().toLocaleString('en-GB', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
});
|
|
doc.text(
|
|
`${subtitle || 'Authoritative Register Export'} • Generated: ${nowStr}`,
|
|
pageWidth - margin,
|
|
46,
|
|
{ align: 'right' }
|
|
);
|
|
|
|
// Prepare table headers and rows
|
|
const tableHeaders = [columns.map((c) => c.header)];
|
|
const tableRows = data.map((row) =>
|
|
columns.map((col) => {
|
|
let val: any;
|
|
if (col.format) {
|
|
val = col.format(row[col.key], row);
|
|
} else {
|
|
val = row[col.key];
|
|
}
|
|
if (val === null || val === undefined) return '—';
|
|
return String(val);
|
|
})
|
|
);
|
|
|
|
// Render autoTable in landscape
|
|
(autoTable as any)(doc, {
|
|
head: tableHeaders,
|
|
body: tableRows,
|
|
startY: 86,
|
|
margin: { left: margin, right: margin, bottom: 45 },
|
|
theme: 'grid',
|
|
styles: {
|
|
fontSize: 8,
|
|
cellPadding: 6,
|
|
textColor: [30, 41, 59],
|
|
lineColor: [226, 232, 240],
|
|
lineWidth: 0.5,
|
|
overflow: 'linebreak',
|
|
},
|
|
headStyles: {
|
|
fillColor: [15, 23, 42],
|
|
textColor: [255, 255, 255],
|
|
fontStyle: 'bold',
|
|
fontSize: 8,
|
|
},
|
|
alternateRowStyles: {
|
|
fillColor: [248, 250, 252],
|
|
},
|
|
didDrawPage: (hookData: any) => {
|
|
// Footer on every page
|
|
const currentPg = hookData.pageNumber;
|
|
doc.setFont('helvetica', 'normal');
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(100, 116, 139);
|
|
doc.text(
|
|
`Post Fund Management (PFM) • Confidential & Authoritative • Page ${currentPg}`,
|
|
margin,
|
|
pageHeight - 20
|
|
);
|
|
doc.text(
|
|
`Total Records: ${data.length} • Landscape Master Report`,
|
|
pageWidth - margin,
|
|
pageHeight - 20,
|
|
{ align: 'right' }
|
|
);
|
|
},
|
|
});
|
|
|
|
doc.save(`${filename.replace(/\.pdf$/i, '')}.pdf`);
|
|
}
|