Updated A LOT
This commit is contained in:
123
server/services/reportExport.js
Normal file
123
server/services/reportExport.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const PDFDocument = require('pdfkit');
|
||||
const Papa = require('papaparse');
|
||||
const writeXlsxFile = require('write-excel-file/node');
|
||||
|
||||
function formatValue(value, format) {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
if (format === 'currency') return Number(value).toLocaleString('en-US', { style: 'currency', currency: 'USD' });
|
||||
if (format === 'number') return Number(value).toLocaleString('en-US');
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function tableMatrix(report) {
|
||||
const header = report.columns.map((column) => column.label);
|
||||
const body = report.rows.map((row) => report.columns.map((column) => {
|
||||
const value = row[column.key];
|
||||
return column.format === 'currency' || column.format === 'number' ? Number(value || 0) : (value ?? '');
|
||||
}));
|
||||
return { header, body };
|
||||
}
|
||||
|
||||
function totalsRow(report) {
|
||||
if (!report.totals || !Object.keys(report.totals).length) return null;
|
||||
return report.columns.map((column, index) => {
|
||||
if (index === 0) return 'Totals';
|
||||
if (Object.prototype.hasOwnProperty.call(report.totals, column.key)) return report.totals[column.key];
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
function exportCsv(report) {
|
||||
const { header, body } = tableMatrix(report);
|
||||
const totals = totalsRow(report);
|
||||
const rows = totals ? [...body, totals] : body;
|
||||
return {
|
||||
contentType: 'text/csv',
|
||||
body: Papa.unparse([header, ...rows])
|
||||
};
|
||||
}
|
||||
|
||||
async function exportXlsx(report) {
|
||||
const { header } = tableMatrix(report);
|
||||
const totals = totalsRow(report);
|
||||
const data = [
|
||||
header.map((label) => ({ value: label, fontWeight: 'bold' })),
|
||||
...report.rows.map((row) => report.columns.map((column) => {
|
||||
const value = row[column.key];
|
||||
if (column.format === 'currency' || column.format === 'number') {
|
||||
return { type: Number, value: Number(value || 0), format: column.format === 'currency' ? '#,##0.00' : '#,##0' };
|
||||
}
|
||||
return { value: value === null || value === undefined ? '' : String(value) };
|
||||
}))
|
||||
];
|
||||
if (totals) {
|
||||
data.push(totals.map((value, index) => {
|
||||
if (index === 0) return { value: 'Totals', fontWeight: 'bold' };
|
||||
return typeof value === 'number'
|
||||
? { type: Number, value, format: '#,##0.00', fontWeight: 'bold' }
|
||||
: { value: String(value || ''), fontWeight: 'bold' };
|
||||
}));
|
||||
}
|
||||
return {
|
||||
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
body: await writeXlsxFile(data).toBuffer()
|
||||
};
|
||||
}
|
||||
|
||||
function exportPdf(report) {
|
||||
const doc = new PDFDocument({ margin: 36, size: 'LETTER', layout: 'landscape' });
|
||||
const chunks = [];
|
||||
doc.on('data', (chunk) => chunks.push(chunk));
|
||||
|
||||
doc.fontSize(16).text(report.title || 'MixedAssets report');
|
||||
doc.fontSize(9).fillColor('#555').text(
|
||||
`Generated ${new Date(report.generatedAt || Date.now()).toLocaleString()}` +
|
||||
(report.options ? ` · ${Object.entries(report.options).filter(([, v]) => v !== null && v !== undefined && v !== '').map(([k, v]) => `${k}: ${v}`).join(' · ')}` : '')
|
||||
);
|
||||
if (report.meta && report.meta.note) doc.moveDown(0.3).fontSize(8).fillColor('#777').text(report.meta.note);
|
||||
doc.fillColor('#000').moveDown(0.6);
|
||||
|
||||
const pageWidth = doc.page.width - 72;
|
||||
const columns = report.columns;
|
||||
const colWidth = pageWidth / columns.length;
|
||||
const drawRow = (cells, options = {}) => {
|
||||
const y = doc.y;
|
||||
doc.fontSize(options.bold ? 8.5 : 8);
|
||||
cells.forEach((cell, index) => {
|
||||
const column = columns[index];
|
||||
const align = column.format === 'currency' || column.format === 'number' ? 'right' : 'left';
|
||||
doc.text(String(cell ?? ''), 36 + index * colWidth, y, { width: colWidth - 6, align, ellipsis: true });
|
||||
});
|
||||
doc.moveDown(0.2);
|
||||
if (doc.y > doc.page.height - 48) doc.addPage();
|
||||
};
|
||||
|
||||
drawRow(columns.map((column) => column.label), { bold: true });
|
||||
doc.moveTo(36, doc.y).lineTo(36 + pageWidth, doc.y).strokeColor('#ccc').stroke();
|
||||
doc.moveDown(0.2);
|
||||
|
||||
for (const row of report.rows) {
|
||||
drawRow(columns.map((column) => formatValue(row[column.key], column.format)));
|
||||
}
|
||||
|
||||
const totals = totalsRow(report);
|
||||
if (totals) {
|
||||
doc.moveTo(36, doc.y).lineTo(36 + pageWidth, doc.y).strokeColor('#ccc').stroke();
|
||||
doc.moveDown(0.2);
|
||||
drawRow(totals.map((value, index) => (typeof value === 'number' ? formatValue(value, columns[index].format || 'currency') : value)), { bold: true });
|
||||
}
|
||||
|
||||
doc.end();
|
||||
return new Promise((resolve) => {
|
||||
doc.on('end', () => resolve({ contentType: 'application/pdf', body: Buffer.concat(chunks) }));
|
||||
});
|
||||
}
|
||||
|
||||
async function exportReport(report, format) {
|
||||
if (format === 'csv') return exportCsv(report);
|
||||
if (format === 'xlsx') return exportXlsx(report);
|
||||
if (format === 'pdf') return exportPdf(report);
|
||||
return { contentType: 'application/json', body: JSON.stringify(report, null, 2) };
|
||||
}
|
||||
|
||||
module.exports = { exportReport };
|
||||
Reference in New Issue
Block a user