Dashboard Refactor: Flow-based Metrics + Unified Data Model #4

Merged
aetos merged 11 commits from cached-reports into main 2026-05-18 05:37:52 +00:00
3 changed files with 42 additions and 50 deletions
Showing only changes of commit 32303f7067 - Show all commits

View File

@@ -11,15 +11,20 @@ export default function Dashboard(props: DashboardProps) {
comparison: false, comparison: false,
}); });
const toggleMode = () => { const toggleMode = (
setState(prev => { event: React.MouseEvent<HTMLElement>,
const next = { newMode: "expense" | "income" | null
...prev, ) => {
mode: prev.mode === "expense" ? "income" as const : "expense" as const, if (newMode !== null && newMode !== state.mode) {
}; setState(prev => {
props.onModeChange?.(next); const next = {
return next; ...prev,
}); mode: newMode,
};
props.onModeChange?.(next);
return next;
});
}
}; };
const togglePeriodType = () => { const togglePeriodType = () => {

View File

@@ -14,7 +14,7 @@ import { DashboardProps, DashboardState } from "./Dashboard.models";
interface ViewProps extends DashboardProps { interface ViewProps extends DashboardProps {
state: DashboardState; state: DashboardState;
setState: React.Dispatch<React.SetStateAction<DashboardState>>; setState: React.Dispatch<React.SetStateAction<DashboardState>>;
toggleMode: () => void; toggleMode: (event: React.MouseEvent<HTMLElement>, newMode: "expense" | "income" | null) => void;
togglePeriodType: () => void; togglePeriodType: () => void;
setSelectedPeriodId: (id: string | null) => void; setSelectedPeriodId: (id: string | null) => void;
setSelectedGroupKey: (groupKey: GroupKey | null) => void; setSelectedGroupKey: (groupKey: GroupKey | null) => void;

View File

@@ -1,32 +1,9 @@
import { ReportData } from "../../features/report"; import { ReportData } from "../../features/report";
import { import {
getAmount, mergeBucketPeriods,
DecoratedPeriod, periodIdToKey,
} from "../report.helpers"; } from "../report.helpers";
// ─── Helpers ─────────────────────────────────────────────────
function findPeriod(
periods: DecoratedPeriod[],
selectedPeriodId?: string | null
) {
if (!periods.length) return null;
if (selectedPeriodId) {
const match = periods.find((p) => p.id === selectedPeriodId);
if (match) return match;
}
// fallback → latest
return periods.reduce((latest, p) =>
new Date(p.start).getTime() > new Date(latest.start).getTime()
? p
: latest
);
}
// ─── Main adapter ────────────────────────────────────────────
export interface TagItem { export interface TagItem {
tag: string; tag: string;
amount: number; amount: number;
@@ -39,24 +16,34 @@ export function extractTopTags(
): { items: TagItem[]; total: number } { ): { items: TagItem[]; total: number } {
const tagMap = new Map<string, number>(); const tagMap = new Map<string, number>();
for (const bucket of reportData.buckets) { let periodKey: ReturnType<typeof periodIdToKey> = "all";
const tags = bucket.group_key.tags; if (selectedPeriodId) {
if (!tags || tags.length === 0) continue; periodKey = periodIdToKey(selectedPeriodId);
}
// Prefer ALL if available const periods = mergeBucketPeriods(reportData.buckets, periodKey);
const allPeriods = (bucket.periods.all || []) as DecoratedPeriod[];
const periodsToUse = selectedPeriodId let period = periods[0];
? (Object.values(bucket.periods).flat() as DecoratedPeriod[]) if (selectedPeriodId) {
: allPeriods; period = periods.find(p => p.id === selectedPeriodId) || period;
} else if (periods.length > 0) {
period = periods.reduce((latest, p) =>
new Date(p.start).getTime() > new Date(latest.start).getTime()
? p
: latest
);
}
const period = findPeriod(periodsToUse, selectedPeriodId); if (period && period.metric && period.metric.transactions) {
if (!period) continue; for (const txn of period.metric.transactions) {
if (txn.tags && txn.tags.length > 0) {
const amount = getAmount(period); for (const tagObj of txn.tags) {
const tagName = typeof tagObj === "string" ? tagObj : tagObj.name;
for (const tag of tags) { tagMap.set(tagName, (tagMap.get(tagName) || 0) + txn.amount);
tagMap.set(tag, (tagMap.get(tag) || 0) + amount); }
} else {
tagMap.set("Untagged", (tagMap.get("Untagged") || 0) + txn.amount);
}
} }
} }