();
+/**
+ * Convert raw TreasuryFlowBucket[] (per-token per-window rows from the API)
+ * into chart-ready TreasuryFlowPoint[] for the legacy export helper.
+ *
+ * Buckets with the same period label are merged by summing outflows.
+ * The running cumulative is computed in chronological order.
+ */
+export function toTreasuryFlowChart(
+ rows: TreasuryFlowBucket[],
+ granularity: AnalyticsGranularity = "month",
+): TreasuryFlowPoint[] {
+ const byPeriod = new Map<
+ string,
+ { inflow: number; outflow: number; sortKey: string }
+ >();
+
for (const row of rows) {
- const period = row.timestamp.slice(0, 7);
- byPeriod.set(period, (byPeriod.get(period) ?? 0) + (Number(row.outflow) || 0));
+ const label = formatTimeSeriesLabel(row.timestamp, granularity);
+ const existing = byPeriod.get(label);
+ const inflow = Number(row.inflow) || 0;
+ const outflow = Number(row.outflow) || 0;
+ if (existing) {
+ existing.inflow += inflow;
+ existing.outflow += outflow;
+ } else {
+ byPeriod.set(label, { inflow, outflow, sortKey: row.timestamp });
+ }
}
+
let cumulative = 0;
return [...byPeriod.entries()]
- .sort(([left], [right]) => left.localeCompare(right))
- .map(([period, outflow]) => {
+ .sort(([, a], [, b]) => a.sortKey.localeCompare(b.sortKey))
+ .map(([period, { inflow, outflow }]) => {
cumulative += outflow;
- return { period, outflow, cumulative };
+ return { period, inflow, outflow, cumulative };
});
}
@@ -122,43 +124,6 @@ export function AnalyticsPage() {
const query = useMemo(() => toQuery(filters), [filters]);
const { data, loading, error, refresh } = useTreasuryAnalytics(query);
- const fetchData = useCallback(() => {
- let active = true;
- setState((prev) => ({ ...prev, loading: true, error: null }));
-
- loadAnalyticsData()
- .then((data) => {
- if (active) setState({ ...data, loading: false, error: null });
- })
- .catch((err) => {
- if (active)
- setState((prev) => ({
- ...prev,
- loading: false,
- error:
- err instanceof Error
- ? err.message
- : "Failed to load analytics data",
- }));
- })
- .finally(() => {
- if (!active) return;
- });
-
- return () => {
- active = false;
- };
- }, []);
-
- useEffect(() => {
- return fetchData();
- }, [fetchData]);
-
- const filteredTransfers = useMemo(
- () => filterExecutedTransfers(state.proposals, filters),
- [state.proposals, filters],
- );
-
const spendByCategory = useMemo(
() => toSpendByCategory(data?.spendByCategory ?? []),
[data?.spendByCategory],
@@ -167,11 +132,24 @@ export function AnalyticsPage() {
() => toSpendByOwner(data?.spendByOwner ?? []),
[data?.spendByOwner],
);
+ // Flat TreasuryFlowPoint[] used by CSV export; TreasuryFlowChart consumes
+ // raw data?.flow directly so it can re-bucket on granularity change.
const treasuryFlow = useMemo(
- () => toTreasuryFlow(data?.flow ?? []),
+ () => toTreasuryFlowChart(data?.flow ?? []),
[data?.flow],
);
- const isEmpty = !loading && !error && !data;
+ // Balance time-series from the hook (may be undefined when not requested yet)
+ const balanceHistory = useMemo(() => {
+ const ts = data?.balance?.timeSeries;
+ if (!ts) return [];
+ return ts.map((point) => ({
+ timestamp: point.timestamp,
+ ...Object.fromEntries(
+ Object.entries(point.values).map(([k, v]) => [k.toLowerCase(), Number(v) || 0]),
+ ),
+ }));
+ }, [data?.balance?.timeSeries]);
+
const hasExportData = spendByCategory.length > 0 || treasuryFlow.length > 0;
const handleExportSpendCsv = () =>
@@ -205,60 +183,159 @@ export function AnalyticsPage() {
return (
<>
+ {/* ── Page header + export actions ─────────────────────────── */}
Analytics
- Export Spend CSV
- Export Treasury CSV
- Download Statement (PDF)
+
+ Export Spend CSV
+
+
+ Export Treasury CSV
+
+
+ Download Statement (PDF)
+
+ {/* ── Filters ───────────────────────────────────────────────── */}
- From setFilters((previous) => ({ ...previous, startDate: event.target.value }))} className="mt-1 w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-white text-sm focus:ring-2 focus:ring-zinc-400 focus:outline-none" />
- To setFilters((previous) => ({ ...previous, endDate: event.target.value }))} className="mt-1 w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-white text-sm focus:ring-2 focus:ring-zinc-400 focus:outline-none" />
- Category setFilters((previous) => ({ ...previous, category: event.target.value as ProposalCategory | "all" }))} className="mt-1 w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-white text-sm focus:ring-2 focus:ring-zinc-400 focus:outline-none">{CATEGORY_OPTIONS.map((option) => {option.label} )}
- Owner setFilters((previous) => ({ ...previous, owner: event.target.value }))} placeholder="Filter by proposer…" className="mt-1 w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-white text-sm placeholder-zinc-600 focus:ring-2 focus:ring-zinc-400 focus:outline-none" />
+
+ From
+
+ setFilters((previous) => ({
+ ...previous,
+ startDate: event.target.value,
+ }))
+ }
+ className="mt-1 w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-white text-sm focus:ring-2 focus:ring-zinc-400 focus:outline-none"
+ />
+
+
+ To
+
+ setFilters((previous) => ({
+ ...previous,
+ endDate: event.target.value,
+ }))
+ }
+ className="mt-1 w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-white text-sm focus:ring-2 focus:ring-zinc-400 focus:outline-none"
+ />
+
+
+ Category
+
+ setFilters((previous) => ({
+ ...previous,
+ category: event.target.value as ProposalCategory | "all",
+ }))
+ }
+ className="mt-1 w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-white text-sm focus:ring-2 focus:ring-zinc-400 focus:outline-none"
+ >
+ {CATEGORY_OPTIONS.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+ Owner
+
+ setFilters((previous) => ({
+ ...previous,
+ owner: event.target.value,
+ }))
+ }
+ placeholder="Filter by proposer…"
+ className="mt-1 w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-white text-sm placeholder-zinc-600 focus:ring-2 focus:ring-zinc-400 focus:outline-none"
+ />
+
+ {/* ── Summary stat cards ────────────────────────────────────── */}
-
-
-
+
+
+
+
+ {/* ── Charts ────────────────────────────────────────────────── */}
+
+
-
-
-
-
-
-
-
Treasury Outflow Over Time
-
Cumulative treasury outflow over the selected period.
-
-
-
-
- {treasuryFlow.length ? `Cumulative outflow ends at ${treasuryFlow[treasuryFlow.length - 1].cumulative.toFixed(2)}.` : "No treasury outflow data is available."}
-
-
>
);
}