Implemented data export
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user