diff --git a/public/js/views/accounting-view.js b/public/js/views/accounting-view.js index 52409e8..aa03dad 100644 --- a/public/js/views/accounting-view.js +++ b/public/js/views/accounting-view.js @@ -458,6 +458,7 @@ export function injectReportsControls() { +
@@ -474,6 +475,7 @@ export function injectReportsControls() { +
@@ -490,6 +492,7 @@ export function injectReportsControls() { +
@@ -617,6 +620,31 @@ export function exportCustomerRevenuePdf() { window.open(url, '_blank'); } +export function exportProfitLossPdf() { + const startEl = document.getElementById('pl-start'); + const endEl = document.getElementById('pl-end'); + const method = document.getElementById('pl-method')?.value || 'Accrual'; + if (!startEl?.value || !endEl?.value) return alert('Please select start and end dates first.'); + window.open(`/api/accounting/reports/profit-loss/pdf?startDate=${startEl.value}&endDate=${endEl.value}&accountingMethod=${method}`, '_blank'); +} + +export function exportBalanceSheetPdf() { + const asOf = document.getElementById('bs-asof'); + const method = document.getElementById('bs-method')?.value || 'Accrual'; + if (!asOf?.value) return alert('Please select an as-of date first.'); + window.open(`/api/accounting/reports/balance-sheet/pdf?asOfDate=${asOf.value}&accountingMethod=${method}`, '_blank'); +} + +export function exportTaxSummaryPdf() { + const monthEl = document.getElementById('ts-month'); + const method = document.getElementById('ts-method')?.value || 'Accrual'; + if (!monthEl?.value) return alert('Please select a month first.'); + const [y, m] = monthEl.value.split('-').map(Number); + const startDate = firstOfMonthISO(y, m - 1); + const endDate = lastOfMonthISO(y, m - 1); + window.open(`/api/accounting/reports/tax-summary/pdf?startDate=${startDate}&endDate=${endDate}&accountingMethod=${method}`, '_blank'); +} + function renderQboReport(report) { if (!report || !report.Header) return `

No report data.

`; const cols = (report.Columns && report.Columns.Column) || []; @@ -1388,5 +1416,8 @@ window.accountingView = { recordTaxPayment, markTaxPaidExternal, loadCustomerRevenue, - exportCustomerRevenuePdf + exportCustomerRevenuePdf, + exportProfitLossPdf, + exportBalanceSheetPdf, + exportTaxSummaryPdf }; \ No newline at end of file diff --git a/src/routes/accounting.js b/src/routes/accounting.js index d37f75d..59a14e8 100644 --- a/src/routes/accounting.js +++ b/src/routes/accounting.js @@ -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 => + `${escapeHtmlReport(c.ColTitle || '')}` + ).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) => + `${i === 0 ? escapeHtmlReport(c.value || '') : ''}` + ).join(''); + html += `${cells}`; + } + + 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) => + `${escapeHtmlReport(c.value || '')}` + ).join(''); + html += `${cells}`; + } + + if (!isSection && row.ColData) { + const cells = row.ColData.map((c, i) => + `${escapeHtmlReport(c.value || '')}` + ).join(''); + html += `${cells}`; + } + } + 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, '''); +} + +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
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}}', `${headHtml}`) + .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 // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• diff --git a/templates/report-shell-template.html b/templates/report-shell-template.html new file mode 100644 index 0000000..a620a8f --- /dev/null +++ b/templates/report-shell-template.html @@ -0,0 +1,66 @@ + + + + + + + +
+
+
+ {{LOGO_HTML}} +
+

{{COMPANY_NAME}}

+

{{COMPANY_ADDRESS}}

+
+
+
+
+ {{SLOGAN}} +
+
+
+ +
{{REPORT_TITLE}}
+ +
+

{{REPORT_META}}

+
+ + + + {{TABLE_HEAD}} + + + {{REPORT_BODY}} + +
+ +

+ Generated: {{GENERATED_DATE}} +

+
+ +