Skip to content

Commit 25fb06d

Browse files
Top Customers Report
1 parent 177766c commit 25fb06d

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ <h5 class="modal-title text-warning fw-bold">Notice About Pricing Data</h5>
511511
<script src="/EasySearchTests/reports/productLapsedReport.js"></script>
512512
<script src="/EasySearchTests/reports/productStuckInventoryReport.js"></script>
513513
<script src="/EasySearchTests/reports/profitReport.js"></script>
514+
<script src="/EasySearchTests/reports/topCustomersRevenueReport.js"></script>
514515
<script src="reports.js"></script>
515516
<script src="uiRenderer.js"></script>
516517
<script src="app.js"></script>

reports.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ window.reportModules = [
3030
generatorFunctionName: 'buildStuckInventoryReport'
3131
},
3232
{
33+
id: 'top20cust',
34+
title: 'Top 20 Customers by Revenue (Last 12 mo)',
35+
generatorFunctionName: 'buildTopCustomersByRevenueReport'
36+
},
37+
{
3338
id: 'profit',
3439
title: 'Top Customers & Products by Profit',
3540
generatorFunctionName: 'buildProfitReport'

reports/topCustomersRevenueReport.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/* reports/topCustomersRevenueReport.js
2+
--------------------------------------------------------------- */
3+
4+
window.buildTopCustomersByRevenueReport = function buildTopCustomersByRevenueReport(
5+
modalEl,
6+
reportId // “top20cust”
7+
) {
8+
return new Promise((resolve, reject) => {
9+
10+
/* --- locate our <li> in the modal --- */
11+
const liId = `item-${reportId}`;
12+
const li = modalEl.querySelector(`#${liId}`);
13+
if (!li) {
14+
console.error(`[${reportId}] list-item not found`);
15+
return reject({ reportId, error : 'list-item not found' });
16+
}
17+
18+
/* --- little helpers --- */
19+
const parseDate = (str) => {
20+
if (!str) return null;
21+
const [m,d,y] = str.split('/').map(s => +s.trim());
22+
return (m && d && y) ? new Date(y, m - 1, d) : null;
23+
};
24+
const toCSV = (rows) => {
25+
if (!rows.length) return '';
26+
const cols = Object.keys(rows[0]);
27+
const esc = v => `"${String(v).replace(/"/g,'""')}"`;
28+
return [cols.join(',')]
29+
.concat(rows.map(r => cols.map(c => esc(r[c])).join(',')))
30+
.join('\n');
31+
};
32+
33+
/* --- crunch the numbers (async-friendly 0 ms slot) --- */
34+
setTimeout(() => {
35+
try {
36+
const salesDF = window.dataStore?.Sales?.dataframe || [];
37+
if (!salesDF.length) {
38+
li.querySelector('.spinner-border')?.remove();
39+
li.insertAdjacentHTML('beforeend',
40+
' <small class="text-muted">(no sales data)</small>');
41+
return resolve({ reportId, status : 'success', count : 0 });
42+
}
43+
44+
const today = new Date();
45+
const twelveMoAgo = new Date();
46+
twelveMoAgo.setMonth(today.getMonth() - 12);
47+
48+
/* --- aggregate revenue per customer --- */
49+
const totals = {}; // { customer → { revenue, orders } }
50+
for (const row of salesDF) {
51+
const saleDate = parseDate(row.Date);
52+
if (!saleDate || saleDate < twelveMoAgo) continue;
53+
54+
const cust = row.Customer?.trim();
55+
if (!cust) continue;
56+
57+
const amount = +String(row.Total_Amount).replace(/\s/g,'') || 0;
58+
if (!totals[cust]) totals[cust] = { revenue:0, orders:0 };
59+
totals[cust].revenue += amount;
60+
totals[cust].orders += 1;
61+
}
62+
63+
const top20 = Object.entries(totals)
64+
.sort((a,b) => b[1].revenue - a[1].revenue)
65+
.slice(0, 20)
66+
.map(([cust,stats], idx) => ({
67+
'#': idx+1,
68+
'Customer Name' : cust,
69+
'Total Revenue ($)' : stats.revenue.toFixed(2),
70+
'Orders (12 mo)' : stats.orders
71+
}));
72+
73+
li.querySelector('.spinner-border')?.remove();
74+
75+
if (!top20.length) {
76+
li.insertAdjacentHTML('beforeend',
77+
' <small class="text-muted">(no sales in last 12 mo)</small>');
78+
return resolve({ reportId, status:'success', count:0 });
79+
}
80+
81+
/* --- download-button --- */
82+
const csv = toCSV(top20);
83+
const btn = document.createElement('button');
84+
btn.className = 'report-download-btn';
85+
btn.title = 'Download Top-20 Customers CSV';
86+
btn.innerHTML = `
87+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"
88+
width="24" height="24" fill="#5f6368">
89+
<path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56
90+
58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480
91+
v-120h80v120q0 33-23.5 56.5T720-160H240Z"/>
92+
</svg>`;
93+
btn.onclick = () => saveAs(new Blob([csv],
94+
{type:'text/csv;charset=utf-8'}),
95+
'top20_customers_12mo.csv');
96+
li.appendChild(btn);
97+
98+
resolve({ reportId, status:'success', count:top20.length });
99+
100+
} catch (err) {
101+
console.error(`[${reportId}]`, err);
102+
li.querySelector('.spinner-border')?.remove();
103+
li.insertAdjacentHTML('beforeend',
104+
' <small class="text-danger">(error)</small>');
105+
reject({ reportId, error:err });
106+
}
107+
}, 0);
108+
});
109+
};

0 commit comments

Comments
 (0)