PDF Export Report

This commit is contained in:
2026-06-10 17:21:29 -05:00
parent ba6019751f
commit 8959064080
3 changed files with 246 additions and 1 deletions

View File

@@ -242,6 +242,154 @@ router.get('/reports/customer-revenue/pdf', async (req, res) => {
}
});
// ─── Shared Report PDF helper ──────────────────────────────────────
function reportToHtml(report) {
const cols = (report.Columns && report.Columns.Column) || [];
const headHtml = cols.map(c =>
`<th>${escapeHtmlReport(c.ColTitle || '')}</th>`
).join('');
function buildRows(rows, depth) {
if (!rows) return '';
const arr = Array.isArray(rows) ? rows : [rows];
let html = '';
for (const row of arr) {
const isSection = row.type === 'Section' || row.Rows || row.Summary;
const indent = depth * 16;
if (row.Header && row.Header.ColData) {
const cells = row.Header.ColData.map((c, i) =>
`<td style="padding-left:${i === 0 ? 12 + indent : 0}px;${i === 0 ? ' font-weight:bold;' : ''}">${i === 0 ? escapeHtmlReport(c.value || '') : ''}</td>`
).join('');
html += `<tr class="section-header">${cells}</tr>`;
}
if (isSection && row.Rows && row.Rows.Row)
html += buildRows(row.Rows.Row, depth + 1);
if (row.Summary && row.Summary.ColData) {
const cells = row.Summary.ColData.map((c, i) =>
`<td style="padding-left:${i === 0 ? 12 + indent : 0}px;" class="cell-bold">${escapeHtmlReport(c.value || '')}</td>`
).join('');
html += `<tr class="summary-row">${cells}</tr>`;
}
if (!isSection && row.ColData) {
const cells = row.ColData.map((c, i) =>
`<td style="padding-left:${i === 0 ? 12 + indent : 0}px;">${escapeHtmlReport(c.value || '')}</td>`
).join('');
html += `<tr>${cells}</tr>`;
}
}
return html;
}
const bodyHtml = buildRows(report.Rows && report.Rows.Row ? report.Rows.Row : null, 0);
return { headHtml, bodyHtml };
}
function escapeHtmlReport(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
async function renderReportPdf(res, title, meta, filename, reportObj) {
const templatePath = path.join(__dirname, '..', '..', 'templates', 'report-shell-template.html');
let html = await fs.readFile(templatePath, 'utf-8');
const logoHTML = await getLogoHtml();
const { headHtml, bodyHtml } = reportToHtml(reportObj);
const generated = new Date().toISOString().split('T')[0];
html = html
.replace('{{LOGO_HTML}}', logoHTML)
.replace('{{COMPANY_NAME}}', 'Bay Area Affiliates, Inc.')
.replace('{{COMPANY_ADDRESS}}', '1001 Blucher Street<br>Corpus Christi, Texas 78401')
.replace('{{SLOGAN}}', 'Providing IT Services and Support in South Texas Since 1996')
.replace('{{REPORT_TITLE}}', title)
.replace('{{REPORT_META}}', meta)
.replace('{{TABLE_HEAD}}', `<tr>${headHtml}</tr>`)
.replace('{{REPORT_BODY}}', bodyHtml)
.replace('{{GENERATED_DATE}}', generated);
const pdf = await generatePdfFromHtml(html);
const sanitized = filename.replace(/[^a-zA-Z0-9._-]/g, '-');
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdf.length,
'Content-Disposition': `attachment; filename="${sanitized}.pdf"`
});
res.end(pdf, 'binary');
}
// ─── P&L PDF ───────────────────────────────────────────────────────
router.get('/reports/profit-loss/pdf', async (req, res) => {
try {
const startDate = req.query.startDate;
const endDate = req.query.endDate;
const method = req.query.accountingMethod || 'Accrual';
if (!startDate || !endDate) return res.status(400).json({ error: 'startDate and endDate required' });
const report = await accountingService.getProfitAndLoss({ startDate, endDate, accountingMethod: method });
await renderReportPdf(res,
'PROFIT & LOSS REPORT',
`Period: ${startDate} to ${endDate} · ${method}`,
`Profit-Loss-${startDate}-to-${endDate}`,
report
);
} catch (err) {
console.error('p&l pdf error:', err.message);
res.status(500).json({ error: err.message });
}
});
// ─── Balance Sheet PDF ─────────────────────────────────────────────
router.get('/reports/balance-sheet/pdf', async (req, res) => {
try {
const asOfDate = req.query.asOfDate;
const method = req.query.accountingMethod || 'Accrual';
if (!asOfDate) return res.status(400).json({ error: 'asOfDate required' });
const report = await accountingService.getBalanceSheet({ asOfDate, accountingMethod: method });
await renderReportPdf(res,
'BALANCE SHEET',
`As of: ${asOfDate} · ${method}`,
`Balance-Sheet-${asOfDate}`,
report
);
} catch (err) {
console.error('balance-sheet pdf error:', err.message);
res.status(500).json({ error: err.message });
}
});
// ─── Sales Tax PDF ──────────────────────────────────────────────────
router.get('/reports/tax-summary/pdf', async (req, res) => {
try {
const startDate = req.query.startDate;
const endDate = req.query.endDate;
const method = req.query.accountingMethod || 'Accrual';
if (!startDate || !endDate) return res.status(400).json({ error: 'startDate and endDate required' });
const report = await accountingService.getTaxSummary({ startDate, endDate, accountingMethod: method });
await renderReportPdf(res,
'SALES TAX LIABILITY',
`Period: ${startDate} to ${endDate} · ${method}`,
`Sales-Tax-${startDate}-to-${endDate}`,
report
);
} catch (err) {
console.error('tax-summary pdf error:', err.message);
res.status(500).json({ error: err.message });
}
});
// ════════════════════════════════════════════════════════════════════
// Phase 2 Lieferung 1 — Sync + Cache-Reads
// ════════════════════════════════════════════════════════════════════