move reports
This commit is contained in:
@@ -12,7 +12,13 @@
|
||||
* 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];
|
||||
@@ -28,6 +34,20 @@ let revAnonymize = false;
|
||||
let revGroupByMonth = false;
|
||||
let revData = 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' },
|
||||
@@ -37,47 +57,12 @@ const AR_BUCKETS = [
|
||||
];
|
||||
const BUCKET_LABEL = Object.fromEntries(AR_BUCKETS.map(b => [b.key, b.label]));
|
||||
|
||||
// ── Local helpers (Stil wie accounting-view.js) ─────────────────────
|
||||
// ── Local helpers ───────────────────────────────────────────────────
|
||||
// fmtMoney/escapeHtml/showError/showLoading kommen aus report-helpers.js.
|
||||
|
||||
/** numeric-String → "$1,234.56". Kein Float-Zwischenschritt außer zur Anzeige. */
|
||||
function fmtMoney(v) {
|
||||
if (v == null || v === '') return '—';
|
||||
const n = parseFloat(v);
|
||||
if (isNaN(n)) return '—';
|
||||
const fixed = n.toFixed(2);
|
||||
const [intPart, decPart] = fixed.replace('-', '').split('.');
|
||||
const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return `${n < 0 ? '-' : ''}$${grouped}.${decPart}`;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
if (s == null) return '';
|
||||
return String(s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function showError(slotId, message) {
|
||||
const el = document.getElementById(slotId);
|
||||
if (!el) return;
|
||||
el.innerHTML = `
|
||||
<div class="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p class="font-semibold text-red-800">Report Error</p>
|
||||
<p class="text-sm text-red-600 mt-1">${escapeHtml(message)}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function showLoading(slotId, message = 'Loading…') {
|
||||
const el = document.getElementById(slotId);
|
||||
if (!el) return;
|
||||
el.innerHTML = `
|
||||
<div class="flex items-center gap-3 p-4 text-gray-500">
|
||||
<svg class="animate-spin h-5 w-5 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
<span>${escapeHtml(message)}</span>
|
||||
</div>`;
|
||||
/** showError mit dem Reports-spezifischen Titel. */
|
||||
function reportError(slotId, message) {
|
||||
showError(slotId, message, 'Report Error');
|
||||
}
|
||||
|
||||
function methodologyBox(text, anonymized) {
|
||||
@@ -121,75 +106,158 @@ function downloadCsv(filename, lines) {
|
||||
|
||||
// ── 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 `
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200" x-data="{ open: false }">
|
||||
<div class="px-4 py-3 border-b bg-gray-50 flex items-center gap-2 cursor-pointer select-none"
|
||||
@click="open = !open">
|
||||
<svg class="w-4 h-4 text-gray-500 transition-transform" :class="open ? 'rotate-90' : ''"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<h3 class="font-semibold text-gray-800">${title}</h3>
|
||||
</div>
|
||||
<div class="p-4" x-show="open" style="display:none">
|
||||
${bodyHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="flex flex-wrap items-end gap-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">As of</label>
|
||||
<input type="date" id="ar-asof" value="${arAsOf}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<button onclick="window.reportsView.loadArAging()"
|
||||
class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Run</button>
|
||||
<button onclick="window.reportsView.exportArAgingCsv()"
|
||||
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">⬇ Export CSV</button>
|
||||
<label class="flex items-center gap-1 pt-5 text-xs text-gray-600 cursor-pointer">
|
||||
<input type="checkbox" id="ar-anonymize" ${arAnonymize ? 'checked' : ''}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
Anonymize customers
|
||||
</label>
|
||||
</div>
|
||||
<div id="ar-result"></div>`;
|
||||
|
||||
const revenueBody = `
|
||||
<div class="flex flex-wrap items-end gap-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">From</label>
|
||||
<input type="date" id="rev-from" value="${revFrom}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">To</label>
|
||||
<input type="date" id="rev-to" value="${revTo}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<button onclick="window.reportsView.loadRevenue()"
|
||||
class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Run</button>
|
||||
<button onclick="window.reportsView.exportRevenueCsv()"
|
||||
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">⬇ Export CSV</button>
|
||||
<label class="flex items-center gap-1 pt-5 text-xs text-gray-600 cursor-pointer">
|
||||
<input type="checkbox" id="rev-group-month" ${revGroupByMonth ? 'checked' : ''}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
Group by month
|
||||
</label>
|
||||
<label class="flex items-center gap-1 pt-5 text-xs text-gray-600 cursor-pointer">
|
||||
<input type="checkbox" id="rev-anonymize" ${revAnonymize ? 'checked' : ''}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
Anonymize customers
|
||||
</label>
|
||||
</div>
|
||||
<div id="rev-result"></div>`;
|
||||
|
||||
// ── Ab hier 1:1 aus accounting-view.js injectReportsControls() ──
|
||||
const profitLossBody = `
|
||||
<div class="flex flex-wrap items-end gap-3 mb-3">
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">Start</label>
|
||||
<input type="date" id="pl-start" value="${plStartDate}" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm"></div>
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">End</label>
|
||||
<input type="date" id="pl-end" value="${plEndDate}" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm"></div>
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">Method</label>
|
||||
<select id="pl-method" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
<option value="Accrual" ${plAccountingMethod === 'Accrual' ? 'selected' : ''}>Accrual</option>
|
||||
<option value="Cash" ${plAccountingMethod === 'Cash' ? 'selected' : ''}>Cash</option>
|
||||
</select></div>
|
||||
<button onclick="window.reportsView.loadProfitLoss()" class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Run</button>
|
||||
<button onclick="window.reportsView.exportProfitLossPdf()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">📄 PDF</button>
|
||||
</div>
|
||||
<div id="pl-result"></div>`;
|
||||
|
||||
const balanceSheetBody = `
|
||||
<div class="flex flex-wrap items-end gap-3 mb-3">
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">As of</label>
|
||||
<input type="date" id="bs-asof" value="${bsAsOfDate}" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm"></div>
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">Method</label>
|
||||
<select id="bs-method" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
<option value="Accrual" ${bsAccountingMethod === 'Accrual' ? 'selected' : ''}>Accrual</option>
|
||||
<option value="Cash" ${bsAccountingMethod === 'Cash' ? 'selected' : ''}>Cash</option>
|
||||
</select></div>
|
||||
<button onclick="window.reportsView.loadBalanceSheet()" class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Run</button>
|
||||
<button onclick="window.reportsView.exportBalanceSheetPdf()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">📄 PDF</button>
|
||||
</div>
|
||||
<div id="bs-result"></div>`;
|
||||
|
||||
const taxSummaryBody = `
|
||||
<div class="flex flex-wrap items-end gap-3 mb-3">
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">Month</label>
|
||||
<input type="month" id="ts-month" value="${tsMonth}" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm"></div>
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">Method</label>
|
||||
<select id="ts-method" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
<option value="Accrual" ${tsAccountingMethod === 'Accrual' ? 'selected' : ''}>Accrual</option>
|
||||
<option value="Cash" ${tsAccountingMethod === 'Cash' ? 'selected' : ''}>Cash</option>
|
||||
</select></div>
|
||||
<button onclick="window.reportsView.loadTaxSummary()" class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Run</button>
|
||||
<button onclick="window.reportsView.exportTaxSummaryPdf()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">📄 PDF</button>
|
||||
</div>
|
||||
<div id="ts-result"></div>`;
|
||||
|
||||
const customerRevenueBody = `
|
||||
<div class="flex flex-wrap items-end gap-3 mb-3">
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">Start</label>
|
||||
<input type="date" id="cr-start" value="${crStartDate}" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm"></div>
|
||||
<div><label class="block text-xs font-medium text-gray-700 mb-1">End</label>
|
||||
<input type="date" id="cr-end" value="${crEndDate}" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm"></div>
|
||||
<button onclick="window.reportsView.loadCustomerRevenue()" class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Run</button>
|
||||
<button onclick="window.reportsView.exportCustomerRevenuePdf()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">📄 Export PDF</button>
|
||||
<label class="flex items-center gap-1 pt-5 text-xs text-gray-600 cursor-pointer">
|
||||
<input type="checkbox" id="cr-anonymize" class="h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
Anonymize
|
||||
</label>
|
||||
</div>
|
||||
<div id="cr-result"></div>`;
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Report 1: AR Aging -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
|
||||
<div class="px-4 py-3 border-b bg-gray-50">
|
||||
<h3 class="font-semibold text-gray-800">Accounts Receivable Aging</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap items-end gap-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">As of</label>
|
||||
<input type="date" id="ar-asof" value="${arAsOf}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<button onclick="window.reportsView.loadArAging()"
|
||||
class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Run</button>
|
||||
<button onclick="window.reportsView.exportArAgingCsv()"
|
||||
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">⬇ Export CSV</button>
|
||||
<label class="flex items-center gap-1 pt-5 text-xs text-gray-600 cursor-pointer">
|
||||
<input type="checkbox" id="ar-anonymize" ${arAnonymize ? 'checked' : ''}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
Anonymize customers
|
||||
</label>
|
||||
</div>
|
||||
<div id="ar-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report 2: Invoice-Level Revenue -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
|
||||
<div class="px-4 py-3 border-b bg-gray-50">
|
||||
<h3 class="font-semibold text-gray-800">Invoice-Level Revenue</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap items-end gap-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">From</label>
|
||||
<input type="date" id="rev-from" value="${revFrom}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">To</label>
|
||||
<input type="date" id="rev-to" value="${revTo}"
|
||||
class="px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<button onclick="window.reportsView.loadRevenue()"
|
||||
class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Run</button>
|
||||
<button onclick="window.reportsView.exportRevenueCsv()"
|
||||
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">⬇ Export CSV</button>
|
||||
<label class="flex items-center gap-1 pt-5 text-xs text-gray-600 cursor-pointer">
|
||||
<input type="checkbox" id="rev-group-month" ${revGroupByMonth ? 'checked' : ''}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
Group by month
|
||||
</label>
|
||||
<label class="flex items-center gap-1 pt-5 text-xs text-gray-600 cursor-pointer">
|
||||
<input type="checkbox" id="rev-anonymize" ${revAnonymize ? 'checked' : ''}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
Anonymize customers
|
||||
</label>
|
||||
</div>
|
||||
<div id="rev-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
${reportCard('Accounts Receivable Aging', arAgingBody)}
|
||||
${reportCard('Invoice-Level Revenue', revenueBody)}
|
||||
${reportCard('Customer Revenue (Summary)', customerRevenueBody)}
|
||||
${reportCard('Profit & Loss', profitLossBody)}
|
||||
${reportCard('Balance Sheet', balanceSheetBody)}
|
||||
${reportCard('Sales Tax (QBO)', taxSummaryBody)}
|
||||
</div>`;
|
||||
|
||||
// Alpine scannt das DOM nur beim Start — nachgereichtes Markup selbst anmelden.
|
||||
initAlpineTree(el);
|
||||
}
|
||||
|
||||
// ── Report 1: AR Aging ──────────────────────────────────────────────
|
||||
@@ -200,11 +268,11 @@ export async function loadArAging() {
|
||||
showLoading('ar-result', 'Loading receivables…');
|
||||
try {
|
||||
const data = await window.API.reports.getArAging(arAsOf, arAnonymize);
|
||||
if (data.error) return showError('ar-result', data.error);
|
||||
if (data.error) return reportError('ar-result', data.error);
|
||||
arData = data;
|
||||
renderArAging(data);
|
||||
} catch (err) {
|
||||
showError('ar-result', err.message || 'Failed to load AR aging');
|
||||
reportError('ar-result', err.message || 'Failed to load AR aging');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +361,7 @@ function renderArAging(data) {
|
||||
|
||||
export function exportArAgingCsv() {
|
||||
if (!arData || !arData.rows.length) {
|
||||
return showError('ar-result', 'Run the report first — there is nothing to export.');
|
||||
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}`]));
|
||||
@@ -334,17 +402,17 @@ export async function loadRevenue() {
|
||||
revAnonymize = document.getElementById('rev-anonymize')?.checked || false;
|
||||
revGroupByMonth = document.getElementById('rev-group-month')?.checked || false;
|
||||
|
||||
if (!revFrom || !revTo) return showError('rev-result', 'Please select both a from and a to date.');
|
||||
if (revFrom > revTo) return showError('rev-result', 'The from date must not be after the to date.');
|
||||
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 showError('rev-result', data.error);
|
||||
if (data.error) return reportError('rev-result', data.error);
|
||||
revData = data;
|
||||
renderRevenue(data);
|
||||
} catch (err) {
|
||||
showError('rev-result', err.message || 'Failed to load revenue');
|
||||
reportError('rev-result', err.message || 'Failed to load revenue');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +500,7 @@ function renderRevenue(data) {
|
||||
|
||||
export function exportRevenueCsv() {
|
||||
if (!revData || !revData.rows.length) {
|
||||
return showError('rev-result', 'Run the report first — there is nothing to export.');
|
||||
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}`]));
|
||||
@@ -472,11 +540,152 @@ export function exportRevenueCsv() {
|
||||
downloadCsv(`revenue-${revData.from}_to_${revData.to}${revData.anonymized ? '-anonymized' : ''}.csv`, lines);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// 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 = '<p class="text-sm text-gray-500">No invoices found in this period.</p>';
|
||||
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 += `<tr class="border-t hover:bg-gray-50">
|
||||
<td class="px-3 py-2 text-sm font-medium">${rank}. ${escapeHtml(maskName(r.customer_name))}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">${r.invoice_count}</td>
|
||||
<td class="px-3 py-2 text-sm text-right">${fmtMoney(rev)}</td>
|
||||
<td class="px-3 py-2 text-sm text-right text-gray-500">${pct}%</td>
|
||||
</tr>`;
|
||||
}
|
||||
rowsHtml += `<tr class="border-t bg-gray-50 font-semibold">
|
||||
<td class="px-3 py-2 text-sm">TOTAL (${data.length} customers)</td>
|
||||
<td class="px-3 py-2 text-sm text-center">${totalInvoices}</td>
|
||||
<td class="px-3 py-2 text-sm text-right">${fmtMoney(grandTotal)}</td>
|
||||
<td class="px-3 py-2 text-sm text-right">100.0%</td>
|
||||
</tr>`;
|
||||
|
||||
document.getElementById('cr-result').innerHTML = `
|
||||
<div class="overflow-x-auto border border-gray-200 rounded">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-medium text-gray-700">Customer</th>
|
||||
<th class="px-3 py-2 text-center font-medium text-gray-700">Invoices</th>
|
||||
<th class="px-3 py-2 text-right font-medium text-gray-700">Revenue (net)</th>
|
||||
<th class="px-3 py-2 text-right font-medium text-gray-700">% of Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rowsHtml}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} 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,
|
||||
loadRevenue,
|
||||
exportRevenueCsv
|
||||
exportRevenueCsv,
|
||||
loadProfitLoss,
|
||||
loadBalanceSheet,
|
||||
loadTaxSummary,
|
||||
loadCustomerRevenue,
|
||||
exportProfitLossPdf,
|
||||
exportBalanceSheetPdf,
|
||||
exportTaxSummaryPdf,
|
||||
exportCustomerRevenuePdf
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user