customer report
This commit is contained in:
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user