PDF Export Report
This commit is contained in:
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user