Skip to content

Commit 1f77ccb

Browse files
authored
Merge pull request #588 from kleros/feat/add-monthly-totals
feat: add monthly totals to staking rewards
2 parents 21844e2 + b9b37ab commit 1f77ccb

1 file changed

Lines changed: 109 additions & 70 deletions

File tree

public/staking-rewards.html

Lines changed: 109 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -137,20 +137,86 @@
137137
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
138138
}
139139

140+
// Round a PNK amount to 2 decimal places.
141+
function round2(n) {
142+
return Math.round(n * 100) / 100;
143+
}
144+
145+
// Sort month labels newest-first, keeping the combined "2021-01 & 02" bucket last.
146+
function compareMonths(a, b) {
147+
if (a === "2021-01 & 02") return 1;
148+
if (b === "2021-01 & 02") return -1;
149+
return b.localeCompare(a);
150+
}
151+
152+
// Build a { Month, Mainnet PNK, Gnosis PNK, Total PNK } row summing all wallets for a month.
153+
function monthTotalRow(label, bucket) {
154+
let m = 0,
155+
g = 0;
156+
for (const v of Object.values(bucket.mainnet)) m += v;
157+
for (const v of Object.values(bucket.gnosis)) g += v;
158+
return {
159+
Month: label,
160+
"Mainnet PNK": round2(m),
161+
"Gnosis PNK": round2(g),
162+
"Total PNK": round2(m + g),
163+
};
164+
}
165+
166+
// Build sorted per-wallet { Address, Mainnet PNK, Gnosis PNK, Total PNK } rows for one month.
167+
function monthWalletRows(bucket) {
168+
const addrs = new Set([...Object.keys(bucket.mainnet), ...Object.keys(bucket.gnosis)]);
169+
return [...addrs]
170+
.map((addr) => {
171+
const m = bucket.mainnet[addr] || 0;
172+
const g = bucket.gnosis[addr] || 0;
173+
return {
174+
Address: addr,
175+
"Mainnet PNK": round2(m),
176+
"Gnosis PNK": round2(g),
177+
"Total PNK": round2(m + g),
178+
};
179+
})
180+
.sort((a, b) => b["Total PNK"] - a["Total PNK"]);
181+
}
182+
183+
// Build the 4-column spec shared by every tab: a left-aligned first column,
184+
// the two right-aligned PNK columns, and a right-aligned final total column.
185+
function pnkColumns(firstKey, lastKey) {
186+
return [
187+
{ key: firstKey, label: firstKey, align: "left" },
188+
{ key: "Mainnet PNK", label: "Mainnet PNK", align: "right" },
189+
{ key: "Gnosis PNK", label: "Gnosis PNK", align: "right" },
190+
{ key: lastKey, label: lastKey, align: "right" },
191+
];
192+
}
193+
194+
// Append a worksheet built from row objects, with the given column widths.
195+
function addSheet(wb, name, rows, widths) {
196+
const ws = XLSX.utils.json_to_sheet(rows);
197+
ws["!cols"] = widths.map((wch) => ({ wch }));
198+
XLSX.utils.book_append_sheet(wb, ws, name);
199+
}
200+
140201
// Spreadsheet-like table component
141202
const PAGE_SIZE = 100;
142203

143-
function SpreadsheetTable({ rows, columns }) {
204+
function SpreadsheetTable({ rows, columns, noun = "address", nounPlural = "addresses" }) {
144205
const [sortCol, setSortCol] = useState(null);
145206
const [sortAsc, setSortAsc] = useState(false);
146207
const [search, setSearch] = useState("");
147208
const [page, setPage] = useState(0);
148209

210+
const filterKey = columns[0].key;
149211
const filtered = useMemo(() => {
150212
if (!search) return rows;
151213
const q = search.toLowerCase();
152-
return rows.filter((r) => r.Address.toLowerCase().includes(q));
153-
}, [rows, search]);
214+
return rows.filter((r) =>
215+
String(r[filterKey] ?? "")
216+
.toLowerCase()
217+
.includes(q)
218+
);
219+
}, [rows, search, filterKey]);
154220

155221
const sorted = useMemo(() => {
156222
if (sortCol === null) return filtered;
@@ -185,7 +251,7 @@
185251
<input
186252
className="search-input"
187253
type="text"
188-
placeholder="Search address..."
254+
placeholder={"Search " + noun + "..."}
189255
value={search}
190256
onChange={(e) => setSearch(e.target.value)}
191257
style={{
@@ -201,7 +267,7 @@
201267
}}
202268
/>
203269
<span style={{ fontSize: 11, color: "#6c6c8a", whiteSpace: "nowrap" }}>
204-
{filtered.length.toLocaleString()} address{filtered.length !== 1 ? "es" : ""}
270+
{filtered.length.toLocaleString()} {filtered.length !== 1 ? nounPlural : noun}
205271
</span>
206272
</div>
207273
<div style={{ overflowX: "auto", borderRadius: 8, border: "1px solid rgba(108,92,231,0.15)" }}>
@@ -466,63 +532,45 @@
466532
setProgress((p) => ({ ...p, done: Math.min(i + CONCURRENCY, total) }));
467533
}
468534

469-
const sorted = Object.keys(md).sort((a, b) => {
470-
if (a === "2021-01 & 02") return 1;
471-
if (b === "2021-01 & 02") return -1;
472-
return b.localeCompare(a);
473-
});
535+
const sorted = Object.keys(md).sort(compareMonths);
474536

475537
setMonthData(md);
476538
setGrandTotals(gt);
477-
setTabList(["Summary", ...sorted]);
539+
setTabList(["Summary", "Monthly Totals", ...sorted]);
478540
setActiveTab("Summary");
479541
setErrors(errs);
480542
setPhase("done");
481543
}
482544

545+
// Months sorted newest-first (matches the tab order)
546+
const sortedMonths = useMemo(() => {
547+
if (!monthData) return [];
548+
return Object.keys(monthData).sort(compareMonths);
549+
}, [monthData]);
550+
483551
// Build rows for current tab
484552
const currentRows = useMemo(() => {
485553
if (!grandTotals || !monthData) return [];
486554
if (activeTab === "Summary") {
487555
return Object.entries(grandTotals)
488556
.map(([addr, t]) => ({
489557
Address: addr,
490-
"Mainnet PNK": Math.round(t.mainnet * 100) / 100,
491-
"Gnosis PNK": Math.round(t.gnosis * 100) / 100,
492-
"Grand Total": Math.round((t.mainnet + t.gnosis) * 100) / 100,
558+
"Mainnet PNK": round2(t.mainnet),
559+
"Gnosis PNK": round2(t.gnosis),
560+
"Grand Total": round2(t.mainnet + t.gnosis),
493561
}))
494562
.sort((a, b) => b["Grand Total"] - a["Grand Total"]);
495563
}
496-
const { mainnet, gnosis } = monthData[activeTab] || { mainnet: {}, gnosis: {} };
497-
const allAddrs = new Set([...Object.keys(mainnet), ...Object.keys(gnosis)]);
498-
return [...allAddrs]
499-
.map((addr) => {
500-
const m = mainnet[addr] || 0;
501-
const g = gnosis[addr] || 0;
502-
return {
503-
Address: addr,
504-
"Mainnet PNK": Math.round(m * 100) / 100,
505-
"Gnosis PNK": Math.round(g * 100) / 100,
506-
"Total PNK": Math.round((m + g) * 100) / 100,
507-
};
508-
})
509-
.sort((a, b) => (b["Total PNK"] || 0) - (a["Total PNK"] || 0));
510-
}, [activeTab, grandTotals, monthData]);
564+
if (activeTab === "Monthly Totals") {
565+
return sortedMonths.map((label) => monthTotalRow(label, monthData[label]));
566+
}
567+
return monthWalletRows(monthData[activeTab] || { mainnet: {}, gnosis: {} });
568+
}, [activeTab, grandTotals, monthData, sortedMonths]);
511569

512570
const currentColumns = useMemo(() => {
513-
if (activeTab === "Summary")
514-
return [
515-
{ key: "Address", label: "Address", align: "left" },
516-
{ key: "Mainnet PNK", label: "Mainnet PNK", align: "right" },
517-
{ key: "Gnosis PNK", label: "Gnosis PNK", align: "right" },
518-
{ key: "Grand Total", label: "Grand Total", align: "right" },
519-
];
520-
return [
521-
{ key: "Address", label: "Address", align: "left" },
522-
{ key: "Mainnet PNK", label: "Mainnet PNK", align: "right" },
523-
{ key: "Gnosis PNK", label: "Gnosis PNK", align: "right" },
524-
{ key: "Total PNK", label: "Total PNK", align: "right" },
525-
];
571+
if (activeTab === "Summary") return pnkColumns("Address", "Grand Total");
572+
if (activeTab === "Monthly Totals") return pnkColumns("Month", "Total PNK");
573+
return pnkColumns("Address", "Total PNK");
526574
}, [activeTab]);
527575

528576
function downloadXLSX() {
@@ -532,34 +580,20 @@
532580
const summaryRows = Object.entries(grandTotals)
533581
.map(([addr, t]) => ({
534582
Address: addr,
535-
"Total Mainnet PNK": Math.round(t.mainnet * 100) / 100,
536-
"Total Gnosis PNK": Math.round(t.gnosis * 100) / 100,
537-
"Grand Total PNK": Math.round((t.mainnet + t.gnosis) * 100) / 100,
583+
"Total Mainnet PNK": round2(t.mainnet),
584+
"Total Gnosis PNK": round2(t.gnosis),
585+
"Grand Total PNK": round2(t.mainnet + t.gnosis),
538586
}))
539587
.sort((a, b) => b["Grand Total PNK"] - a["Grand Total PNK"]);
540-
const sws = XLSX.utils.json_to_sheet(summaryRows);
541-
sws["!cols"] = [{ wch: 44 }, { wch: 20 }, { wch: 20 }, { wch: 20 }];
542-
XLSX.utils.book_append_sheet(wb, sws, "Summary");
543-
544-
for (const label of tabList.slice(1)) {
545-
const { mainnet, gnosis } = monthData[label];
546-
const allAddrs = new Set([...Object.keys(mainnet), ...Object.keys(gnosis)]);
547-
if (allAddrs.size === 0) continue;
548-
const rows = [...allAddrs]
549-
.map((addr) => {
550-
const m = mainnet[addr] || 0;
551-
const g = gnosis[addr] || 0;
552-
return {
553-
Address: addr,
554-
"Mainnet PNK": Math.round(m * 100) / 100,
555-
"Gnosis PNK": Math.round(g * 100) / 100,
556-
"Total PNK": Math.round((m + g) * 100) / 100,
557-
};
558-
})
559-
.sort((a, b) => b["Total PNK"] - a["Total PNK"]);
560-
const ws = XLSX.utils.json_to_sheet(rows);
561-
ws["!cols"] = [{ wch: 44 }, { wch: 18 }, { wch: 18 }, { wch: 18 }];
562-
XLSX.utils.book_append_sheet(wb, ws, label.slice(0, 31));
588+
addSheet(wb, "Summary", summaryRows, [44, 20, 20, 20]);
589+
590+
const monthlyTotalRows = sortedMonths.map((label) => monthTotalRow(label, monthData[label]));
591+
addSheet(wb, "Monthly Totals", monthlyTotalRows, [14, 18, 18, 18]);
592+
593+
for (const label of sortedMonths) {
594+
const rows = monthWalletRows(monthData[label]);
595+
if (rows.length === 0) continue;
596+
addSheet(wb, label.slice(0, 31), rows, [44, 18, 18, 18]);
563597
}
564598

565599
const buf = XLSX.write(wb, { bookType: "xlsx", type: "array" });
@@ -634,7 +668,7 @@
634668
)}
635669
{phase === "done" && (
636670
<span style={{ fontSize: 10, color: "#6c6c8a" }}>
637-
{Object.keys(grandTotals).length.toLocaleString()} addresses &middot; {tabList.length - 1} months
671+
{Object.keys(grandTotals).length.toLocaleString()} addresses &middot; {sortedMonths.length} months
638672
</span>
639673
)}
640674
</div>
@@ -776,7 +810,12 @@
776810

777811
{/* Table */}
778812
<div className="table-wrap" style={{ flex: 1, overflow: "auto", padding: "16px 20px" }}>
779-
<SpreadsheetTable rows={currentRows} columns={currentColumns} />
813+
<SpreadsheetTable
814+
rows={currentRows}
815+
columns={currentColumns}
816+
noun={activeTab === "Monthly Totals" ? "month" : "address"}
817+
nounPlural={activeTab === "Monthly Totals" ? "months" : "addresses"}
818+
/>
780819
</div>
781820

782821
{/* Fetch errors */}

0 commit comments

Comments
 (0)