/** * reports-view.js — Due-Diligence Reports * * 1. Accounts Receivable Aging (Stichtag frei wählbar) * 2. Invoice-Level Revenue (Zeitraum frei wählbar, optional nach Monat gruppiert) * 3. Revenue by Category (Parts/Labor/Subscription, optional Monate als Spalten) * * Die ersten beiden Reports können Kundennamen pseudonymisieren. Die Anonymisierung * passiert ausschließlich serverseitig — dieses Modul bekommt bei aktiver Option gar * keine Klarnamen zu sehen und kann sie folglich auch nicht in ein CSV schreiben. * Report 3 aggregiert über Kategorien und enthält gar keine Kundennamen. * * Beträge kommen als numeric-Strings vom Server und werden nur zur Anzeige * formatiert. Ins CSV gehen die Rohwerte, damit Excel sauber rechnen kann. */ import '../utils/api.js'; import { formatDate } from '../utils/helpers.js'; import { fmtMoney, escapeHtml, showError, showLoading, todayISO, firstOfMonthISO, lastOfMonthISO, prevMonthISO, firstOfYearISO, renderQboReport, initAlpineTree } from '../utils/report-helpers.js'; // ── State ─────────────────────────────────────────────────────────── const today = new Date().toISOString().split('T')[0]; const startOfYear = `${new Date().getFullYear()}-01-01`; let arAsOf = today; let arAnonymize = false; let arData = null; let revFrom = startOfYear; let revTo = today; let revAnonymize = false; let revGroupByMonth = false; let revData = null; let catFrom = startOfYear; let catTo = today; let catGroupByMonth = false; let catData = null; // State der aus accounting-view.js übernommenen Reports (1:1) let plStartDate = null; let plEndDate = null; let plAccountingMethod = 'Accrual'; let bsAsOfDate = null; let bsAccountingMethod = 'Accrual'; let tsMonth = null; // 'YYYY-MM' — selected month for tax summary let tsAccountingMethod = 'Accrual'; let crStartDate = null; let crEndDate = null; const AR_BUCKETS = [ { key: 'current', label: 'Current' }, { key: 'd1_30', label: '1–30 days' }, { key: 'd31_60', label: '31–60 days' }, { key: 'd61_90', label: '61–90 days' }, { key: 'd90_plus', label: '90+ days' } ]; const BUCKET_LABEL = Object.fromEntries(AR_BUCKETS.map(b => [b.key, b.label])); // ── Local helpers ─────────────────────────────────────────────────── // fmtMoney/escapeHtml/showError/showLoading kommen aus report-helpers.js. /** showError mit dem Reports-spezifischen Titel. */ function reportError(slotId, message) { showError(slotId, message, 'Report Error'); } function methodologyBox(text, anonymized) { return `
Methodology: ${escapeHtml(text)} ${anonymized ? 'anonymized' : ''}
`; } function monthLabel(monthKey) { const [y, m] = monthKey.split('-').map(Number); return new Date(Date.UTC(y, m - 1, 1)) .toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' }); } // ── CSV ───────────────────────────────────────────────────────────── function csvCell(v) { if (v == null) return ''; const s = String(v); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; } function csvRow(cells) { return cells.map(csvCell).join(','); } function downloadCsv(filename, lines) { // BOM, damit Excel UTF-8 korrekt erkennt const blob = new Blob(['' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // ── Shell ─────────────────────────────────────────────────────────── function reportCard(title, bodyHtml) { // x-show statt x-if: der Teilbaum bleibt im DOM, damit Datumsfelder und // Checkboxen ihren Zustand über das Zu-/Aufklappen hinweg behalten. // Das inline display:none verhindert ein Aufblitzen, bis Alpine initialisiert. return `

${title}

`; } export function renderReportsView() { const el = document.getElementById('reports-content'); if (!el) return; // Defaults der übernommenen Reports — 1:1 aus injectReportsControls() if (!plStartDate) plStartDate = firstOfYearISO(); if (!plEndDate) plEndDate = todayISO(); if (!bsAsOfDate) bsAsOfDate = todayISO(); if (!tsMonth) tsMonth = prevMonthISO(); if (!crStartDate) crStartDate = firstOfYearISO(); if (!crEndDate) crEndDate = todayISO(); const arAgingBody = `
`; const revenueBody = `
`; const categoryRevenueBody = `
`; // ── Ab hier 1:1 aus accounting-view.js injectReportsControls() ── const profitLossBody = `
`; const balanceSheetBody = `
`; const taxSummaryBody = `
`; const customerRevenueBody = `
`; el.innerHTML = `
${reportCard('Accounts Receivable Aging', arAgingBody)} ${reportCard('Invoice-Level Revenue', revenueBody)} ${reportCard('Revenue by Category', categoryRevenueBody)} ${reportCard('Customer Revenue (Summary)', customerRevenueBody)} ${reportCard('Profit & Loss', profitLossBody)} ${reportCard('Balance Sheet', balanceSheetBody)} ${reportCard('Sales Tax (QBO)', taxSummaryBody)}
`; // Alpine scannt das DOM nur beim Start — nachgereichtes Markup selbst anmelden. initAlpineTree(el); } // ── Report 1: AR Aging ────────────────────────────────────────────── export async function loadArAging() { arAsOf = document.getElementById('ar-asof').value || today; arAnonymize = document.getElementById('ar-anonymize')?.checked || false; showLoading('ar-result', 'Loading receivables…'); try { const data = await window.API.reports.getArAging(arAsOf, arAnonymize); if (data.error) return reportError('ar-result', data.error); arData = data; renderArAging(data); } catch (err) { reportError('ar-result', err.message || 'Failed to load AR aging'); } } function renderArAging(data) { if (!data.rows.length) { document.getElementById('ar-result').innerHTML = methodologyBox(data.methodology, data.anonymized) + '

No open receivables as of this date.

'; return; } let rowsHtml = ''; for (const r of data.rows) { const overdue = r.bucket !== 'current'; rowsHtml += ` ${escapeHtml(r.customer_name || '—')} ${escapeHtml(r.invoice_number || '—')} ${formatDate(r.invoice_date)} ${formatDate(r.due_date)} ${fmtMoney(r.original_amount)} ${fmtMoney(r.open_amount)} ${BUCKET_LABEL[r.bucket] || r.bucket} `; } const t = data.totals; rowsHtml += ` TOTAL (${t.invoice_count} invoices) ${fmtMoney(t.original_amount)} ${fmtMoney(t.open_amount)} `; let bucketCells = ''; for (const b of AR_BUCKETS) { const entry = data.buckets[b.key]; bucketCells += ` ${b.label} ${entry.invoice_count} ${fmtMoney(entry.open_amount)} `; } document.getElementById('ar-result').innerHTML = methodologyBox(data.methodology, data.anonymized) + `
${bucketCells}
Bucket Invoices Open
Total ${t.invoice_count} ${fmtMoney(t.open_amount)}
${rowsHtml}
Customer Invoice # Date Due Original Open Bucket
`; } export function exportArAgingCsv() { if (!arData || !arData.rows.length) { return reportError('ar-result', 'Run the report first — there is nothing to export.'); } const lines = []; lines.push(csvRow([`Accounts Receivable Aging as of ${arData.asOf}`])); lines.push(csvRow([`Methodology: ${arData.methodology}`])); lines.push(''); lines.push(csvRow(['Customer', 'Invoice #', 'Invoice Date', 'Due Date', 'Original Amount', 'Open Amount', 'Bucket'])); for (const r of arData.rows) { lines.push(csvRow([ r.customer_name || '', r.invoice_number || '', String(r.invoice_date).split('T')[0], String(r.due_date).split('T')[0], r.original_amount, r.open_amount, BUCKET_LABEL[r.bucket] || r.bucket ])); } const t = arData.totals; lines.push(csvRow(['TOTAL', '', '', '', t.original_amount, t.open_amount, `${t.invoice_count} invoices`])); lines.push(''); lines.push(csvRow(['Bucket Summary', 'Invoices', 'Open Amount'])); for (const b of AR_BUCKETS) { const entry = arData.buckets[b.key]; lines.push(csvRow([b.label, entry.invoice_count, entry.open_amount])); } lines.push(csvRow(['Total', t.invoice_count, t.open_amount])); downloadCsv(`ar-aging-${arData.asOf}${arData.anonymized ? '-anonymized' : ''}.csv`, lines); } /** * PDF-Export nach dem Muster von exportCustomerRevenuePdf: Parameter direkt * aus den Eingabefeldern, der Server holt die Daten neu. Der Report muss * dafür nicht vorher ausgeführt worden sein. */ export function exportArAgingPdf() { const asOfEl = document.getElementById('ar-asof'); if (!asOfEl?.value) return alert('Please select an as-of date first.'); const anonymize = document.getElementById('ar-anonymize')?.checked || false; let url = `/api/reports/ar-aging/pdf?asOf=${asOfEl.value}`; if (anonymize) url += '&anonymize=true'; window.open(url, '_blank'); } // ── Report 2: Invoice-Level Revenue ───────────────────────────────── export async function loadRevenue() { revFrom = document.getElementById('rev-from').value; revTo = document.getElementById('rev-to').value; revAnonymize = document.getElementById('rev-anonymize')?.checked || false; revGroupByMonth = document.getElementById('rev-group-month')?.checked || false; if (!revFrom || !revTo) return reportError('rev-result', 'Please select both a from and a to date.'); if (revFrom > revTo) return reportError('rev-result', 'The from date must not be after the to date.'); showLoading('rev-result', 'Loading revenue…'); try { const data = await window.API.reports.getRevenue(revFrom, revTo, revAnonymize, revGroupByMonth); if (data.error) return reportError('rev-result', data.error); revData = data; renderRevenue(data); } catch (err) { reportError('rev-result', err.message || 'Failed to load revenue'); } } function revenueRowHtml(r) { return ` ${escapeHtml(r.customer_name || '—')} ${escapeHtml(r.invoice_number || '—')} ${formatDate(r.invoice_date)} ${fmtMoney(r.subtotal)} ${fmtMoney(r.tax_amount)} ${fmtMoney(r.total)} ${fmtMoney(r.paid_amount)} ${fmtMoney(r.open_amount)} ${escapeHtml(r.payment_status || '—')} `; } function revenueSummaryRow(label, s, extraClass) { return ` ${escapeHtml(label)} (${s.invoice_count} invoices) ${fmtMoney(s.subtotal)} ${fmtMoney(s.tax_amount)} ${fmtMoney(s.total)} ${fmtMoney(s.paid_amount)} ${fmtMoney(s.open_amount)} `; } function renderRevenue(data) { if (!data.rows.length) { document.getElementById('rev-result').innerHTML = methodologyBox(data.methodology, data.anonymized) + '

No invoices found in this period.

'; return; } let rowsHtml = ''; if (data.groupByMonth) { // Monats-Zwischensummen kommen aus SQL, nicht aus JS-Addition const sumsByMonth = new Map(data.months.map(m => [m.month_key, m])); const groupClass = 'bg-gray-50 border-t-2 border-gray-300 font-semibold text-gray-700'; let currentMonth = null; for (const r of data.rows) { if (r.month_key !== currentMonth) { if (currentMonth !== null) { rowsHtml += revenueSummaryRow('Group Total', sumsByMonth.get(currentMonth), groupClass); } currentMonth = r.month_key; rowsHtml += `📅 ${escapeHtml(monthLabel(r.month_key))}`; } rowsHtml += revenueRowHtml(r); } if (currentMonth !== null) { rowsHtml += revenueSummaryRow('Group Total', sumsByMonth.get(currentMonth), groupClass); } } else { for (const r of data.rows) rowsHtml += revenueRowHtml(r); } rowsHtml += revenueSummaryRow('TOTAL', data.totals, 'border-t-2 border-gray-400 bg-gray-100 font-bold'); document.getElementById('rev-result').innerHTML = methodologyBox(data.methodology, data.anonymized) + `
${rowsHtml}
Customer Invoice # Date Subtotal Tax Total Paid Open Status
`; } export function exportRevenueCsv() { if (!revData || !revData.rows.length) { return reportError('rev-result', 'Run the report first — there is nothing to export.'); } const lines = []; lines.push(csvRow([`Invoice-Level Revenue ${revData.from} to ${revData.to}`])); lines.push(csvRow([`Methodology: ${revData.methodology}`])); lines.push(''); const header = ['Customer', 'Invoice #', 'Invoice Date', 'Subtotal', 'Tax', 'Total', 'Paid', 'Open', 'Payment Status']; if (revData.groupByMonth) header.unshift('Month'); lines.push(csvRow(header)); for (const r of revData.rows) { const cells = [ r.customer_name || '', r.invoice_number || '', String(r.invoice_date).split('T')[0], r.subtotal, r.tax_amount, r.total, r.paid_amount, r.open_amount, r.payment_status || '' ]; if (revData.groupByMonth) cells.unshift(r.month_key); lines.push(csvRow(cells)); } const t = revData.totals; const totalCells = [`TOTAL (${t.invoice_count} invoices)`, '', '', t.subtotal, t.tax_amount, t.total, t.paid_amount, t.open_amount, '']; if (revData.groupByMonth) totalCells.unshift(''); lines.push(csvRow(totalCells)); if (revData.groupByMonth && revData.months.length) { lines.push(''); lines.push(csvRow(['Month', 'Invoices', 'Subtotal', 'Tax', 'Total', 'Paid', 'Open'])); for (const m of revData.months) { lines.push(csvRow([m.month_key, m.invoice_count, m.subtotal, m.tax_amount, m.total, m.paid_amount, m.open_amount])); } } downloadCsv(`revenue-${revData.from}_to_${revData.to}${revData.anonymized ? '-anonymized' : ''}.csv`, lines); } export function exportRevenuePdf() { const fromEl = document.getElementById('rev-from'); const toEl = document.getElementById('rev-to'); if (!fromEl?.value || !toEl?.value) return alert('Please select both a from and a to date.'); if (fromEl.value > toEl.value) return alert('The from date must not be after the to date.'); const anonymize = document.getElementById('rev-anonymize')?.checked || false; const groupByMonth = document.getElementById('rev-group-month')?.checked || false; let url = `/api/reports/revenue/pdf?from=${fromEl.value}&to=${toEl.value}`; if (anonymize) url += '&anonymize=true'; if (groupByMonth) url += '&groupByMonth=true'; window.open(url, '_blank'); } // ── Report 3: Revenue by Category ─────────────────────────────────── const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; /** 'YYYY-MM' → { m: 'Mar', y: '2026' } — reine String-Zerlegung, kein Date. */ function monthParts(monthKey) { return { m: MONTH_ABBR[Number(monthKey.slice(5, 7)) - 1], y: monthKey.slice(0, 4) }; } /** * Spaltenmodell der Matrix: Monate in Jahresblöcken, je Block optional eine * Jahressumme, dahinter Gesamt und Prozentanteil. Identisch zu * categoryColumns() in report-pdf-service.js — Bildschirm, CSV und PDF zeigen * damit dieselben Spalten in derselben Reihenfolge. */ function categoryColumns(data) { const cols = []; if (data.groupByMonth) { for (const y of data.years) { for (const m of y.months) cols.push({ type: 'month', key: m }); if (data.showYearColumns) cols.push({ type: 'year', key: y.year }); } } cols.push({ type: 'total' }); cols.push({ type: 'pct' }); return cols; } /** Wert einer Kategoriezeile in einer Spalte; null = Prozentspalte. */ function categoryCell(data, catKey, col) { if (col.type === 'month') return data.cells[catKey][col.key]; if (col.type === 'year') return data.yearCells[catKey][col.key]; if (col.type === 'total') return data.categoryTotals[catKey].revenue; return null; } /** Wert der TOTAL-Zeile in einer Spalte. */ function categoryTotalCell(data, col) { if (col.type === 'month') return data.monthTotals[col.key]; if (col.type === 'year') return data.yearTotals[col.key]; if (col.type === 'total') return data.grandTotal; return null; } /** Spaltenüberschrift für Bildschirm und CSV (dort einzeilig). */ function categoryColumnLabel(col, oneLine) { if (col.type === 'month') { const p = monthParts(col.key); return oneLine ? `${p.m} ${p.y}` : `${p.m}
${p.y}`; } if (col.type === 'year') { return oneLine ? `${col.key} Total` : `${col.key}
Total`; } if (col.type === 'total') return 'Total'; return oneLine ? '% of Total' : '% of
Total'; } export async function loadCategoryRevenue() { catFrom = document.getElementById('cat-from').value; catTo = document.getElementById('cat-to').value; catGroupByMonth = document.getElementById('cat-group-month')?.checked || false; if (!catFrom || !catTo) return reportError('cat-result', 'Please select both a from and a to date.'); if (catFrom > catTo) return reportError('cat-result', 'The from date must not be after the to date.'); showLoading('cat-result', 'Loading category revenue…'); try { const data = await window.API.reports.getCategoryRevenue(catFrom, catTo, catGroupByMonth); if (data.error) return reportError('cat-result', data.error); catData = data; renderCategoryRevenue(data); } catch (err) { reportError('cat-result', err.message || 'Failed to load category revenue'); } } function renderCategoryRevenue(data) { if (!data.hasData) { document.getElementById('cat-result').innerHTML = methodologyBox(data.methodology, false) + '

No invoices found in this period.

'; return; } const cols = categoryColumns(data); const colClass = c => c.type === 'year' ? ' bg-blue-50 font-semibold' : c.type === 'total' ? ' bg-gray-100 font-semibold' : ''; const headHtml = `Category` + cols.map(c => `${categoryColumnLabel(c, false)}`).join(''); let rowsHtml = ''; for (const cat of data.categories) { const cells = cols.map(c => c.type === 'pct' ? `${data.categoryTotals[cat.key].pct}%` : `${fmtMoney(categoryCell(data, cat.key, c))}` ).join(''); rowsHtml += ` ${escapeHtml(cat.label)}${cells}`; } // 100.0 % per Definition (Gesamt/Gesamt), nicht als Summe der gerundeten // Kategorieanteile. const totalCells = cols.map(c => c.type === 'pct' ? `100.0%` : `${fmtMoney(categoryTotalCell(data, c))}` ).join(''); rowsHtml += ` TOTAL (${data.invoiceCount} invoices)${totalCells}`; document.getElementById('cat-result').innerHTML = methodologyBox(data.methodology, false) + `
${headHtml}${rowsHtml}
`; } export function exportCategoryRevenueCsv() { if (!catData || !catData.hasData) { return reportError('cat-result', 'Run the report first — there is nothing to export.'); } const data = catData; const cols = categoryColumns(data); const lines = []; lines.push(csvRow([`Revenue by Category ${data.from} to ${data.to}`])); lines.push(csvRow([`Methodology: ${data.methodology}`])); lines.push(''); lines.push(csvRow(['Category', ...cols.map(c => categoryColumnLabel(c, true))])); for (const cat of data.categories) { lines.push(csvRow([cat.label, ...cols.map(c => c.type === 'pct' ? data.categoryTotals[cat.key].pct : categoryCell(data, cat.key, c))])); } lines.push(csvRow([`TOTAL (${data.invoiceCount} invoices)`, ...cols.map(c => c.type === 'pct' ? '100.0' : categoryTotalCell(data, c))])); downloadCsv(`category-revenue-${data.from}_to_${data.to}.csv`, lines); } export function exportCategoryRevenuePdf() { const fromEl = document.getElementById('cat-from'); const toEl = document.getElementById('cat-to'); if (!fromEl?.value || !toEl?.value) return alert('Please select both a from and a to date.'); if (fromEl.value > toEl.value) return alert('The from date must not be after the to date.'); const groupByMonth = document.getElementById('cat-group-month')?.checked || false; let url = `/api/reports/category-revenue/pdf?from=${fromEl.value}&to=${toEl.value}`; if (groupByMonth) url += '&groupByMonth=true'; window.open(url, '_blank'); } // ──────────────────────────────────────────────────────────────────── // Aus accounting-view.js übernommene Reports // Funktionskörper 1:1; geändert wurden nur die onclick-Namespaces im // zugehörigen Markup und der Titel-Parameter von showError. // ──────────────────────────────────────────────────────────────────── export async function loadProfitLoss() { plStartDate = document.getElementById('pl-start').value; plEndDate = document.getElementById('pl-end').value; plAccountingMethod = document.getElementById('pl-method').value; showLoading('pl-result', 'Loading P&L from QBO…'); try { const data = await window.API.accounting.getProfitAndLoss(plStartDate, plEndDate, plAccountingMethod); if (data.error) return showError('pl-result', data.error); document.getElementById('pl-result').innerHTML = renderQboReport(data); } catch (err) { showError('pl-result', err.message || 'Failed to load P&L'); } } export async function loadBalanceSheet() { bsAsOfDate = document.getElementById('bs-asof').value; bsAccountingMethod = document.getElementById('bs-method').value; showLoading('bs-result', 'Loading Balance Sheet from QBO…'); try { const data = await window.API.accounting.getBalanceSheet(bsAsOfDate, bsAccountingMethod); if (data.error) return showError('bs-result', data.error); document.getElementById('bs-result').innerHTML = renderQboReport(data); } catch (err) { showError('bs-result', err.message || 'Failed to load Balance Sheet'); } } export async function loadTaxSummary() { tsMonth = document.getElementById('ts-month').value; tsAccountingMethod = document.getElementById('ts-method').value; if (!tsMonth) return showError('ts-result', 'Please select a month.'); const [y, m] = tsMonth.split('-').map(Number); const startDate = firstOfMonthISO(y, m - 1); const endDate = lastOfMonthISO(y, m - 1); showLoading('ts-result', 'Loading Sales Tax Liability from QBO…'); try { const data = await window.API.accounting.getTaxSummary(startDate, endDate, tsAccountingMethod); if (data.error) return showError('ts-result', data.error); document.getElementById('ts-result').innerHTML = renderQboReport(data); } catch (err) { showError('ts-result', err.message || 'Failed to load Tax Summary'); } } export async function loadCustomerRevenue() { crStartDate = document.getElementById('cr-start').value; crEndDate = document.getElementById('cr-end').value; const anonymize = document.getElementById('cr-anonymize')?.checked || false; if (!crStartDate || !crEndDate) return showError('cr-result', 'Please select both start and end dates.'); showLoading('cr-result', 'Loading customer revenue...'); const maskName = (name) => anonymize ? name.charAt(0) : name; try { const data = await window.API.accounting.getCustomerRevenue(crStartDate, crEndDate); if (data.error) return showError('cr-result', data.error); if (!data.length) { document.getElementById('cr-result').innerHTML = '

No invoices found in this period.

'; return; } const grandTotal = parseFloat(data[0].grand_total) || 0; const totalInvoices = data.reduce((s, r) => s + parseInt(r.invoice_count), 0); let rowsHtml = ''; let rank = 0; for (const r of data) { rank++; const rev = parseFloat(r.total_revenue) || 0; const pct = grandTotal > 0 ? ((rev / grandTotal) * 100).toFixed(1) : '0.0'; rowsHtml += ` ${rank}. ${escapeHtml(maskName(r.customer_name))} ${r.invoice_count} ${fmtMoney(rev)} ${pct}% `; } rowsHtml += ` TOTAL (${data.length} customers) ${totalInvoices} ${fmtMoney(grandTotal)} 100.0% `; document.getElementById('cr-result').innerHTML = `
${rowsHtml}
Customer Invoices Revenue (net) % of Total
`; } catch (err) { showError('cr-result', err.message || 'Failed to load revenue report'); } } export function exportCustomerRevenuePdf() { const startEl = document.getElementById('cr-start'); const endEl = document.getElementById('cr-end'); const anonymize = document.getElementById('cr-anonymize')?.checked || false; if (!startEl?.value || !endEl?.value) return alert('Please select start and end dates first.'); let url = `/api/accounting/reports/customer-revenue/pdf?startDate=${startEl.value}&endDate=${endEl.value}`; if (anonymize) url += '&anonymize=true'; 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'); } // ── Expose for onclick handlers ───────────────────────────────────── window.reportsView = { renderReportsView, loadArAging, exportArAgingCsv, exportArAgingPdf, loadRevenue, exportRevenueCsv, exportRevenuePdf, loadCategoryRevenue, exportCategoryRevenueCsv, exportCategoryRevenuePdf, loadProfitLoss, loadBalanceSheet, loadTaxSummary, loadCustomerRevenue, exportProfitLossPdf, exportBalanceSheetPdf, exportTaxSummaryPdf, exportCustomerRevenuePdf };