Implemented data export

This commit is contained in:
2026-09-26 13:45:42 +02:00
parent 15a8d8f449
commit 2949d6218b
12 changed files with 10939 additions and 359 deletions
@@ -0,0 +1,247 @@
import * as XLSX from 'xlsx';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
export interface ExportColumn<T = any> {
header: string;
key: keyof T | string;
format?: (value: any, row: T) => string | number;
}
export interface ExportOptions<T = any> {
title: string;
subtitle?: string;
filename: string;
columns: ExportColumn<T>[];
data: T[];
tenantName?: string;
userEmail?: string;
}
/**
* Draws the official PFM Shield and Brand Header on jsPDF in landscape mode
*/
function drawPfmPdfHeader(
doc: jsPDF,
title: string,
subtitle?: string,
tenantName?: string,
userEmail?: string
): number {
const pageWidth = doc.internal.pageSize.getWidth();
// Dark header banner
doc.setFillColor(13, 18, 31); // #0d121f
doc.rect(0, 0, pageWidth, 68, 'F');
// Cyan-to-indigo accent bottom border line
doc.setFillColor(37, 99, 235); // #2563eb
doc.rect(0, 66, pageWidth, 2, 'F');
// Draw PFM Logo Shield Icon (Vector art)
const logoX = 24;
const logoY = 16;
// Background circle for logo
doc.setFillColor(15, 23, 42); // slate-900
doc.circle(logoX + 16, logoY + 16, 16, 'F');
doc.setDrawColor(56, 189, 248); // sky-400
doc.setLineWidth(1);
doc.circle(logoX + 16, logoY + 16, 16, 'S');
// Top facet (Violet/Indigo)
doc.setFillColor(99, 102, 241);
doc.triangle(
logoX + 16, logoY + 6,
logoX + 8, logoY + 14,
logoX + 24, logoY + 14,
'F'
);
// Left facet (Sky/Sapphire)
doc.setFillColor(14, 165, 233);
doc.triangle(
logoX + 8, logoY + 14,
logoX + 16, logoY + 26,
logoX + 16, logoY + 14,
'F'
);
// Right facet (Emerald/Cyan)
doc.setFillColor(16, 185, 129);
doc.triangle(
logoX + 24, logoY + 14,
logoX + 16, logoY + 26,
logoX + 16, logoY + 14,
'F'
);
// Core Diamond
doc.setFillColor(255, 255, 255);
doc.circle(logoX + 16, logoY + 14, 2.5, 'F');
// Brand Name & Subtitle
doc.setTextColor(255, 255, 255);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text('POST FUND MANAGEMENT (PFM)', logoX + 40, logoY + 13);
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.setTextColor(148, 163, 184); // slate-400
doc.text('National Grants & Institutional Governance Platform', logoX + 40, logoY + 24);
// Right-aligned Metadata (Date, Tenant, User)
doc.setFontSize(8);
doc.setTextColor(203, 213, 225);
const nowStr = new Date().toLocaleString();
doc.text(`Generated: ${nowStr}`, pageWidth - 24, logoY + 8, { align: 'right' });
if (tenantName) {
doc.text(`Tenant: ${tenantName}`, pageWidth - 24, logoY + 18, { align: 'right' });
}
if (userEmail) {
doc.text(`Exported by: ${userEmail}`, pageWidth - 24, logoY + 28, { align: 'right' });
}
// Section Title on page
doc.setFontSize(13);
doc.setFont('helvetica', 'bold');
doc.setTextColor(15, 23, 42); // slate-900
doc.text(title, 24, 90);
let currentY = 94;
if (subtitle) {
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.setTextColor(100, 116, 139); // slate-500
doc.text(subtitle, 24, 104);
currentY = 110;
} else {
currentY = 100;
}
return currentY;
}
/**
* Export tabular data to an Excel / Spreadsheet file (.xlsx)
*/
export function exportToSpreadsheet<T>(options: ExportOptions<T>): void {
const { title, filename, columns, data } = options;
if (!data || data.length === 0) {
console.warn('No data available to export to spreadsheet.');
return;
}
// Format table rows based on columns config
const rows = data.map((item) => {
const rowObj: Record<string, any> = {};
columns.forEach((col) => {
const rawVal = (item as any)[col.key];
const formatted = col.format ? col.format(rawVal, item) : (rawVal ?? '');
rowObj[col.header] = formatted;
});
return rowObj;
});
const worksheet = XLSX.utils.json_to_sheet(rows);
// Set column widths based on longest cell content
const colWidths = columns.map((col) => {
const headerLen = col.header.length;
const maxDataLen = data.reduce((max, item) => {
const rawVal = (item as any)[col.key];
const formatted = col.format ? String(col.format(rawVal, item)) : String(rawVal ?? '');
return Math.max(max, formatted.length);
}, headerLen);
return { wch: Math.min(Math.max(maxDataLen + 4, 12), 50) };
});
worksheet['!cols'] = colWidths;
const workbook = XLSX.utils.book_new();
const safeSheetName = title.replace(/[:\\/?*[\]]/g, ' ').substring(0, 31).trim() || 'Data';
XLSX.utils.book_append_sheet(workbook, worksheet, safeSheetName);
const cleanFilename = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`;
XLSX.writeFile(workbook, cleanFilename);
}
/**
* Export tabular data to a landscape PDF file with PFM Logo and branding
*/
export function exportToPdf<T>(options: ExportOptions<T>): void {
const { title, subtitle, filename, columns, data, tenantName, userEmail } = options;
if (!data || data.length === 0) {
console.warn('No data available to export to PDF.');
return;
}
// Landscape A4 doc: width: 841.89 pt, height: 595.28 pt
const doc = new jsPDF({
orientation: 'landscape',
unit: 'pt',
format: 'a4',
});
const startY = drawPfmPdfHeader(doc, title, subtitle, tenantName, userEmail);
// Build table headers and rows
const headers = [columns.map((c) => c.header)];
const body = data.map((item) => {
return columns.map((col) => {
const rawVal = (item as any)[col.key];
return col.format ? String(col.format(rawVal, item)) : String(rawVal ?? '');
});
});
autoTable(doc, {
head: headers,
body: body,
startY: startY + 6,
margin: { left: 24, right: 24, bottom: 32 },
styles: {
font: 'helvetica',
fontSize: 8,
cellPadding: 6,
textColor: [30, 41, 59], // slate-800
lineColor: [226, 232, 240], // slate-200
lineWidth: 0.5,
overflow: 'linebreak',
},
headStyles: {
fillColor: [15, 23, 42], // slate-900
textColor: [255, 255, 255],
fontStyle: 'bold',
fontSize: 8.5,
},
alternateRowStyles: {
fillColor: [248, 250, 252], // slate-50
},
didDrawPage: (dataArg) => {
// Footer on all pages
const pageCount = (doc.internal as any).getNumberOfPages();
const currentPage = dataArg.pageNumber;
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
doc.setFontSize(7.5);
doc.setTextColor(148, 163, 184); // slate-400
doc.text(
'CONFIDENTIAL & PROPRIETARY — POST FUND MANAGEMENT (PFM) PLATFORM',
24,
pageHeight - 14
);
doc.text(
`Page ${currentPage} of ${pageCount}`,
pageWidth - 24,
pageHeight - 14,
{ align: 'right' }
);
},
});
const cleanFilename = filename.endsWith('.pdf') ? filename : `${filename}.pdf`;
doc.save(cleanFilename);
}