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 += ``;
+ }
+
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
{{REPORT_TITLE}}
+
+
+
+
+
+ {{TABLE_HEAD}}
+
+
+ {{REPORT_BODY}}
+
+
+
+
+ Generated: {{GENERATED_DATE}}
+
+
+
+