Dashboard Refactor: Flow-based Metrics + Unified Data Model #4
@@ -11,15 +11,20 @@ export default function Dashboard(props: DashboardProps) {
|
|||||||
comparison: false,
|
comparison: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleMode = () => {
|
const toggleMode = (
|
||||||
|
event: React.MouseEvent<HTMLElement>,
|
||||||
|
newMode: "expense" | "income" | null
|
||||||
|
) => {
|
||||||
|
if (newMode !== null && newMode !== state.mode) {
|
||||||
setState(prev => {
|
setState(prev => {
|
||||||
const next = {
|
const next = {
|
||||||
...prev,
|
...prev,
|
||||||
mode: prev.mode === "expense" ? "income" as const : "expense" as const,
|
mode: newMode,
|
||||||
};
|
};
|
||||||
props.onModeChange?.(next);
|
props.onModeChange?.(next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePeriodType = () => {
|
const togglePeriodType = () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user