anonym. reports
This commit is contained in:
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}} · Generated: {{GENERATED_DATE}}</p>
|
<p>Period: {{DATE_RANGE}} · Generated: {{GENERATED_DATE}}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user