anonym. reports

This commit is contained in:
2026-06-10 16:21:42 -05:00
parent b9ab025794
commit ba6019751f
3 changed files with 16 additions and 5 deletions

View File

@@ -504,6 +504,10 @@ export function injectReportsControls() {
<input type="date" id="cr-end" value="${crEndDate}" class="px-3 py-1.5 border border-gray-300 rounded-md text-sm"></div> <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.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> <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>
<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>
<div id="cr-result"></div> <div id="cr-result"></div>
</div> </div>
@@ -553,8 +557,10 @@ export async function loadTaxSummary() {
export async function loadCustomerRevenue() { export async function loadCustomerRevenue() {
crStartDate = document.getElementById('cr-start').value; crStartDate = document.getElementById('cr-start').value;
crEndDate = document.getElementById('cr-end').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.'); if (!crStartDate || !crEndDate) return showError('cr-result', 'Please select both start and end dates.');
showLoading('cr-result', 'Loading customer revenue...'); showLoading('cr-result', 'Loading customer revenue...');
const maskName = (name) => anonymize ? name.charAt(0) : name;
try { try {
const data = await window.API.accounting.getCustomerRevenue(crStartDate, crEndDate); const data = await window.API.accounting.getCustomerRevenue(crStartDate, crEndDate);
if (data.error) return showError('cr-result', data.error); if (data.error) return showError('cr-result', data.error);
@@ -571,7 +577,7 @@ export async function loadCustomerRevenue() {
const rev = parseFloat(r.total_revenue) || 0; const rev = parseFloat(r.total_revenue) || 0;
const pct = grandTotal > 0 ? ((rev / grandTotal) * 100).toFixed(1) : '0.0'; const pct = grandTotal > 0 ? ((rev / grandTotal) * 100).toFixed(1) : '0.0';
rowsHtml += `<tr class="border-t hover:bg-gray-50"> 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 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-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">${fmtMoney(rev)}</td>
<td class="px-3 py-2 text-sm text-right text-gray-500">${pct}%</td> <td class="px-3 py-2 text-sm text-right text-gray-500">${pct}%</td>
@@ -604,8 +610,10 @@ export async function loadCustomerRevenue() {
export function exportCustomerRevenuePdf() { export function exportCustomerRevenuePdf() {
const startEl = document.getElementById('cr-start'); const startEl = document.getElementById('cr-start');
const endEl = document.getElementById('cr-end'); 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.'); 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}`; let url = `/api/accounting/reports/customer-revenue/pdf?startDate=${startEl.value}&endDate=${endEl.value}`;
if (anonymize) url += '&anonymize=true';
window.open(url, '_blank'); window.open(url, '_blank');
} }

View File

@@ -164,7 +164,7 @@ router.get('/reports/customer-revenue', async (req, res) => {
router.get('/reports/customer-revenue/pdf', async (req, res) => { router.get('/reports/customer-revenue/pdf', async (req, res) => {
try { try {
const { startDate, endDate } = req.query; const { startDate, endDate, anonymize } = req.query;
if (!startDate || !endDate) { if (!startDate || !endDate) {
return res.status(400).json({ error: 'startDate and endDate are required' }); return res.status(400).json({ error: 'startDate and endDate are required' });
} }
@@ -189,6 +189,8 @@ router.get('/reports/customer-revenue/pdf', async (req, res) => {
const rows = result.rows; const rows = result.rows;
const grandTotal = rows.length > 0 ? parseFloat(rows[0].grand_total) || 0 : 0; const grandTotal = rows.length > 0 ? parseFloat(rows[0].grand_total) || 0 : 0;
const totalInvoices = rows.reduce((s, r) => s + parseInt(r.invoice_count), 0); const totalInvoices = rows.reduce((s, r) => s + parseInt(r.invoice_count), 0);
const doAnonymize = anonymize === 'true';
const maskName = (name) => doAnonymize ? name.charAt(0) : name;
let rowsHtml = ''; let rowsHtml = '';
let rank = 0; let rank = 0;
@@ -197,7 +199,7 @@ router.get('/reports/customer-revenue/pdf', async (req, res) => {
const rev = parseFloat(r.total_revenue) || 0; const rev = parseFloat(r.total_revenue) || 0;
const pct = grandTotal > 0 ? ((rev / grandTotal) * 100).toFixed(1) : '0.0'; const pct = grandTotal > 0 ? ((rev / grandTotal) * 100).toFixed(1) : '0.0';
rowsHtml += `<tr> rowsHtml += `<tr>
<td class="name">${rank}. ${r.customer_name}</td> <td class="name">${rank}. ${maskName(r.customer_name)}</td>
<td class="number">${r.invoice_count}</td> <td class="number">${r.invoice_count}</td>
<td class="number">$${formatMoney(rev)}</td> <td class="number">$${formatMoney(rev)}</td>
<td class="number">${pct}%</td> <td class="number">${pct}%</td>
@@ -223,6 +225,7 @@ router.get('/reports/customer-revenue/pdf', async (req, res) => {
.replace('{{SLOGAN}}', 'Providing IT Services and Support in South Texas Since 1996') .replace('{{SLOGAN}}', 'Providing IT Services and Support in South Texas Since 1996')
.replace('{{DATE_RANGE}}', dateRange) .replace('{{DATE_RANGE}}', dateRange)
.replace('{{GENERATED_DATE}}', generated) .replace('{{GENERATED_DATE}}', generated)
.replace('{{ANONYMIZED_NOTE}}', doAnonymize ? ' (anonymized)' : '')
.replace('{{ROWS}}', rowsHtml); .replace('{{ROWS}}', rowsHtml);
const pdf = await generatePdfFromHtml(html); const pdf = await generatePdfFromHtml(html);

View File

@@ -43,7 +43,7 @@
</div> </div>
</div> </div>
<div class="document-type">CUSTOMER REVENUE REPORT</div> <div class="document-type">CUSTOMER REVENUE REPORT{{ANONYMIZED_NOTE}}</div>
<div class="report-meta"> <div class="report-meta">
<p>Period: {{DATE_RANGE}} &middot; Generated: {{GENERATED_DATE}}</p> <p>Period: {{DATE_RANGE}} &middot; Generated: {{GENERATED_DATE}}</p>