/**
* 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 `
`;
}
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) + `
| Bucket |
Invoices |
Open |
${bucketCells}
| Total |
${t.invoice_count} |
${fmtMoney(t.open_amount)} |
| Customer |
Invoice # |
Date |
Due |
Original |
Open |
Bucket |
${rowsHtml}
`;
}
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 ``;
}
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) + `
| Customer |
Invoice # |
Date |
Subtotal |
Tax |
Total |
Paid |
Open |
Status |
${rowsHtml}
`;
}
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) + `
`;
}
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 = `
| Customer |
Invoices |
Revenue (net) |
% of Total |
${rowsHtml}
`;
} 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
};