customer report

This commit is contained in:
2026-06-10 15:51:43 -05:00
parent e4ffe085ff
commit fb12d66759
4 changed files with 264 additions and 1 deletions

View File

@@ -156,6 +156,12 @@ const API = {
method: 'POST'
}).then(r => r.json()),
// Customer Revenue Report
getCustomerRevenue: (startDate, endDate) => {
const params = new URLSearchParams({ startDate, endDate });
return fetch('/api/accounting/reports/customer-revenue?' + params.toString()).then(r => r.json());
},
// Phase 2 Lieferung 1 — Sync + Cache-Reads
syncAccounts: () => fetch('/api/accounting/sync-accounts', { method: 'POST' }).then(r => r.json()),
syncVendors: () => fetch('/api/accounting/sync-vendors', { method: 'POST' }).then(r => r.json()),

View File

@@ -30,6 +30,9 @@ let bsAccountingMethod = 'Accrual';
let tsMonth = null; // 'YYYY-MM' — selected month for tax summary
let tsAccountingMethod = 'Accrual';
let crStartDate = null;
let crEndDate = null;
let stPeriods = [];
let stEditingPeriodId = null; // current period in detail dialog, null = new
let stAccounts = []; // cached account list for pickers
@@ -434,6 +437,8 @@ export function injectReportsControls() {
if (!plEndDate) plEndDate = todayISO();
if (!bsAsOfDate) bsAsOfDate = todayISO();
if (!tsMonth) tsMonth = prevMonthISO();
if (!crStartDate) crStartDate = firstOfYearISO();
if (!crEndDate) crEndDate = todayISO();
c.innerHTML = `
${makeCollapsible('Reports', 'reports-section-body')}
@@ -489,6 +494,20 @@ export function injectReportsControls() {
<div id="ts-result"></div>
</div>
</div>
<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">Customer 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">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.accountingView.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.accountingView.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>
</div>
<div id="cr-result"></div>
</div>
</div>
</div>
</div>`;
}
@@ -531,6 +550,65 @@ export async function loadTaxSummary() {
} 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;
if (!crStartDate || !crEndDate) return showError('cr-result', 'Please select both start and end dates.');
showLoading('cr-result', 'Loading customer revenue...');
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(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');
if (!startEl?.value || !endEl?.value) return alert('Please select start and end dates first.');
const url = `/api/accounting/reports/customer-revenue/pdf?startDate=${startEl.value}&endDate=${endEl.value}`;
window.open(url, '_blank');
}
function renderQboReport(report) {
if (!report || !report.Header) return `<p class="text-sm text-gray-500">No report data.</p>`;
const cols = (report.Columns && report.Columns.Column) || [];
@@ -1300,5 +1378,7 @@ window.accountingView = {
updateTaxPreview,
saveTaxPeriodDraft,
recordTaxPayment,
markTaxPaidExternal
markTaxPaidExternal,
loadCustomerRevenue,
exportCustomerRevenuePdf
};