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,
});
const toggleMode = () => {
setState(prev => {
const next = {
...prev,
mode: prev.mode === "expense" ? "income" as const : "expense" as const,
};
props.onModeChange?.(next);
return next;
});
const toggleMode = (
event: React.MouseEvent<HTMLElement>,
newMode: "expense" | "income" | null
) => {
if (newMode !== null && newMode !== state.mode) {
setState(prev => {
const next = {
...prev,
mode: newMode,
};
props.onModeChange?.(next);
return next;
});
}
};
const togglePeriodType = () => {

View File

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

View File

@@ -1,32 +1,9 @@
import { ReportData } from "../../features/report";
import {
getAmount,
DecoratedPeriod,
mergeBucketPeriods,
periodIdToKey,
} 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 {
tag: string;
amount: number;
@@ -39,24 +16,34 @@ export function extractTopTags(
): { items: TagItem[]; total: number } {
const tagMap = new Map<string, number>();
for (const bucket of reportData.buckets) {
const tags = bucket.group_key.tags;
if (!tags || tags.length === 0) continue;
let periodKey: ReturnType<typeof periodIdToKey> = "all";
if (selectedPeriodId) {
periodKey = periodIdToKey(selectedPeriodId);
}
// Prefer ALL if available
const allPeriods = (bucket.periods.all || []) as DecoratedPeriod[];
const periods = mergeBucketPeriods(reportData.buckets, periodKey);
const periodsToUse = selectedPeriodId
? (Object.values(bucket.periods).flat() as DecoratedPeriod[])
: allPeriods;
let period = periods[0];
if (selectedPeriodId) {
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) continue;
const amount = getAmount(period);
for (const tag of tags) {
tagMap.set(tag, (tagMap.get(tag) || 0) + amount);
if (period && period.metric && period.metric.transactions) {
for (const txn of period.metric.transactions) {
if (txn.tags && txn.tags.length > 0) {
for (const tagObj of txn.tags) {
const tagName = typeof tagObj === "string" ? tagObj : tagObj.name;
tagMap.set(tagName, (tagMap.get(tagName) || 0) + txn.amount);
}
} else {
tagMap.set("Untagged", (tagMap.get("Untagged") || 0) + txn.amount);
}
}
}