Password Policy/App rename/PM Scheduling Improvements
This commit is contained in:
@@ -69,7 +69,7 @@ function exportPdf(report) {
|
||||
const chunks = [];
|
||||
doc.on('data', (chunk) => chunks.push(chunk));
|
||||
|
||||
doc.fontSize(16).text(report.title || 'MixedAssets report');
|
||||
doc.fontSize(16).text(report.title || 'DepreCore 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(' · ')}` : '')
|
||||
@@ -113,10 +113,111 @@ function exportPdf(report) {
|
||||
});
|
||||
}
|
||||
|
||||
// Draw a simple bordered table for a {columns, rows} block into an existing PDF document.
|
||||
function drawTable(doc, columns, rows, totals) {
|
||||
const left = doc.page.margins.left;
|
||||
const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||
const colWidth = width / columns.length;
|
||||
const line = (cells, bold) => {
|
||||
if (doc.y > doc.page.height - 60) doc.addPage();
|
||||
const y = doc.y;
|
||||
doc.fontSize(bold ? 8.5 : 8).fillColor('#000');
|
||||
cells.forEach((cell, index) => {
|
||||
const column = columns[index];
|
||||
const align = column.format === 'currency' || column.format === 'number' ? 'right' : 'left';
|
||||
doc.text(String(cell ?? ''), left + index * colWidth, y, { width: colWidth - 6, align, ellipsis: true });
|
||||
});
|
||||
doc.moveDown(0.25);
|
||||
};
|
||||
line(columns.map((column) => column.label), true);
|
||||
doc.moveTo(left, doc.y).lineTo(left + width, doc.y).strokeColor('#ccc').stroke();
|
||||
doc.moveDown(0.15);
|
||||
if (!rows.length) {
|
||||
doc.fontSize(8).fillColor('#777').text('None recorded.', left).moveDown(0.2);
|
||||
return;
|
||||
}
|
||||
for (const row of rows) line(columns.map((column) => formatValue(row[column.key], column.format)));
|
||||
if (totals && Object.keys(totals).length) {
|
||||
doc.moveTo(left, doc.y).lineTo(left + width, doc.y).strokeColor('#ccc').stroke();
|
||||
doc.moveDown(0.15);
|
||||
line(columns.map((column, index) => {
|
||||
if (index === 0) return 'Totals';
|
||||
return Object.prototype.hasOwnProperty.call(totals, column.key) ? formatValue(totals[column.key], column.format || 'currency') : '';
|
||||
}), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Per-asset dossier PDF: identity, per-book financials, each meta section, then photo thumbnails.
|
||||
function exportDossierPdf(report) {
|
||||
const doc = new PDFDocument({ margin: 40, size: 'LETTER' });
|
||||
const chunks = [];
|
||||
doc.on('data', (chunk) => chunks.push(chunk));
|
||||
const meta = report.meta || {};
|
||||
const asset = meta.asset || {};
|
||||
|
||||
doc.fontSize(17).fillColor('#000').text(`Asset journal — ${asset.asset_id || ''}`);
|
||||
if (asset.description) doc.fontSize(11).fillColor('#333').text(asset.description);
|
||||
doc.fontSize(8).fillColor('#777').text(`Generated ${new Date(report.generatedAt || Date.now()).toLocaleString()}`);
|
||||
doc.moveDown(0.5);
|
||||
|
||||
// Identity / financial key-values (two columns).
|
||||
doc.fontSize(11).fillColor('#000').text('Asset details').moveDown(0.2);
|
||||
const entries = Object.entries(asset).filter(([key]) => key !== 'asset_id' && key !== 'description');
|
||||
const half = Math.ceil(entries.length / 2);
|
||||
const colW = (doc.page.width - doc.page.margins.left - doc.page.margins.right) / 2;
|
||||
const startY = doc.y;
|
||||
entries.forEach(([key, value], index) => {
|
||||
const column = index < half ? 0 : 1;
|
||||
const y = startY + (index % half) * 14;
|
||||
const label = key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
doc.fontSize(8).fillColor('#777').text(`${label}: `, doc.page.margins.left + column * colW, y, { continued: true });
|
||||
doc.fillColor('#000').text(value === null || value === undefined || value === '' ? '—' : String(value));
|
||||
});
|
||||
doc.y = startY + half * 14;
|
||||
doc.moveDown(0.6);
|
||||
|
||||
doc.fontSize(11).fillColor('#000').text('Depreciation by book').moveDown(0.2);
|
||||
drawTable(doc, report.columns || [], report.rows || [], report.totals);
|
||||
doc.moveDown(0.4);
|
||||
|
||||
for (const section of meta.sections || []) {
|
||||
doc.fontSize(11).fillColor('#000').text(section.title).moveDown(0.2);
|
||||
drawTable(doc, section.columns || [], section.rows || []);
|
||||
doc.moveDown(0.4);
|
||||
}
|
||||
|
||||
const photos = meta.photos || [];
|
||||
if (meta.photo_count) {
|
||||
doc.fontSize(11).fillColor('#000').text(`Photos (${meta.photo_count})`).moveDown(0.2);
|
||||
let x = doc.page.margins.left;
|
||||
let rowY = doc.y;
|
||||
const thumbW = 160;
|
||||
const thumbH = 120;
|
||||
for (const photo of photos) {
|
||||
if (x + thumbW > doc.page.width - doc.page.margins.right) { x = doc.page.margins.left; rowY += thumbH + 20; }
|
||||
if (rowY + thumbH > doc.page.height - 50) { doc.addPage(); rowY = doc.y; x = doc.page.margins.left; }
|
||||
try {
|
||||
const base64 = String(photo.data || '').replace(/^data:[^;]+;base64,/, '');
|
||||
if (base64) doc.image(Buffer.from(base64, 'base64'), x, rowY, { fit: [thumbW, thumbH] });
|
||||
} catch {
|
||||
doc.fontSize(7).fillColor('#999').text('[image unavailable]', x, rowY + 50, { width: thumbW, align: 'center' });
|
||||
}
|
||||
doc.fontSize(7).fillColor('#555').text(photo.caption || '', x, rowY + thumbH + 2, { width: thumbW, ellipsis: true });
|
||||
x += thumbW + 20;
|
||||
}
|
||||
doc.y = rowY + thumbH + 18;
|
||||
}
|
||||
|
||||
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);
|
||||
if (format === 'pdf') return report.meta && report.meta.layout === 'dossier' ? exportDossierPdf(report) : exportPdf(report);
|
||||
return { contentType: 'application/json', body: JSON.stringify(report, null, 2) };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user