diff --git a/src/components/Dashboard/Dashboard.view.tsx b/src/components/Dashboard/Dashboard.view.tsx index 1b7c232..f3837fe 100644 --- a/src/components/Dashboard/Dashboard.view.tsx +++ b/src/components/Dashboard/Dashboard.view.tsx @@ -5,7 +5,8 @@ import { Grid, Typography, ToggleButton, - ToggleButtonGroup + ToggleButtonGroup, + Button } from "@mui/material"; import { useTheme, alpha } from "@mui/material/styles"; import { GroupKey } from "../../features/report"; @@ -69,7 +70,7 @@ export default function DashboardView({ transition: 'background 0.3s ease' }} > - + Outflows Inflows + + {selectedGroupKey && Object.keys(selectedGroupKey).length > 0 && ( + + )} diff --git a/src/components/ProgressCard/TopPayees.adapter.ts b/src/components/ProgressCard/TopPayees.adapter.ts new file mode 100644 index 0000000..af71bea --- /dev/null +++ b/src/components/ProgressCard/TopPayees.adapter.ts @@ -0,0 +1,65 @@ +import { mergeBucketPeriods, periodIdToKey } from "../report.helpers"; +import { GroupKey, ReportData } from "../../features/report"; + +export interface PayeeItem { + name: string; + amount: number; +} + +export function extractTopPayees( + reportData: ReportData, + flow: "outflows" | "inflows", + selectedPeriodId?: string | null, + selectedGroupKey?: GroupKey | null +): { items: PayeeItem[]; total: number } { + const payeeMap = new Map(); + + let targetPeriods = []; + + if (selectedPeriodId) { + const key = periodIdToKey(selectedPeriodId); + const periods = mergeBucketPeriods(reportData.buckets, key); + const selected = periods.find((p) => p.id === selectedPeriodId); + if (selected) { + targetPeriods.push(selected); + } + } else { + // If no specific period is selected, aggregate over the "all" period bucket + targetPeriods = mergeBucketPeriods(reportData.buckets, "all"); + } + + for (const p of targetPeriods) { + let txns = p.metric.transactions || []; + + if (selectedGroupKey?.tags && selectedGroupKey.tags.length > 0) { + txns = txns.filter(txn => { + if (!txn.tags) return false; + const txnTags = txn.tags.map(t => typeof t === "string" ? t : t.name); + return selectedGroupKey.tags!.every(selectedTag => txnTags.includes(selectedTag)); + }); + } + + for (const txn of txns) { + if (txn.payee && txn.payee.name) { + const current = payeeMap.get(txn.payee.name) || 0; + payeeMap.set(txn.payee.name, current + txn.amount); + } + } + } + + let items: PayeeItem[] = []; + let total = 0; + + for (const [name, amount] of payeeMap.entries()) { + items.push({ name, amount }); + total += amount; + } + + // Sort descending by amount + items.sort((a, b) => b.amount - a.amount); + + return { + items: items.slice(0, 4), // Top 4 + total, + }; +} diff --git a/src/components/ProgressCard/TopPayees.tsx b/src/components/ProgressCard/TopPayees.tsx new file mode 100644 index 0000000..b05acd5 --- /dev/null +++ b/src/components/ProgressCard/TopPayees.tsx @@ -0,0 +1,93 @@ +import * as React from "react"; +import { Box, Paper, Typography } from "@mui/material"; +import { ReportData, GroupKey } from "../../features/report"; +import ProgressCard from "./ProgressCard"; +import { extractTopPayees } from "./TopPayees.adapter"; + +type Props = { + reportData: ReportData; + flow: "outflows" | "inflows"; + header: string; + selectedPeriodId?: string | null; + selectedGroupKey?: GroupKey | null; + setSelectedGroupKey?: (key: GroupKey | null) => void; + compact?: boolean; + isFetching?: boolean; +}; + +export default function TopPayees({ + reportData, + flow, + header, + selectedPeriodId, + selectedGroupKey, + setSelectedGroupKey, + compact = true, + isFetching, +}: Props) { + const { items, total } = React.useMemo(() => { + return extractTopPayees(reportData, flow, selectedPeriodId, selectedGroupKey); + }, [reportData, flow, selectedPeriodId, selectedGroupKey]); + + return ( + + + {header} + + + + {items.map((item) => { + const isSelected = selectedGroupKey?.payee?.includes(item.name); + return ( + { + if (setSelectedGroupKey) { + let newKey = selectedGroupKey ? { ...selectedGroupKey } : {}; + + if (isSelected) { + delete newKey.payee; + } else { + newKey.payee = [item.name]; + } + + setSelectedGroupKey(Object.keys(newKey).length ? newKey : null); + } + }} + /> + ); + })} + + + ); +} diff --git a/src/components/ProgressCard/TopTags.adapter.ts b/src/components/ProgressCard/TopTags.adapter.ts index 451b703..eb4cf1f 100644 --- a/src/components/ProgressCard/TopTags.adapter.ts +++ b/src/components/ProgressCard/TopTags.adapter.ts @@ -4,6 +4,8 @@ import { periodIdToKey, } from "../report.helpers"; +import { GroupKey } from "../../features/report"; + export interface TagItem { tag: string; amount: number; @@ -12,7 +14,8 @@ export interface TagItem { export function extractTopTags( reportData: ReportData, flow: "outflows" | "inflows", - selectedPeriodId?: string | null + selectedPeriodId?: string | null, + selectedGroupKey?: GroupKey | null ): { items: TagItem[]; total: number } { const tagMap = new Map(); @@ -35,7 +38,14 @@ export function extractTopTags( } if (period && period.metric && period.metric.transactions) { - for (const txn of period.metric.transactions) { + let txns = period.metric.transactions; + if (selectedGroupKey?.payee && selectedGroupKey.payee.length > 0) { + txns = txns.filter(txn => + txn.payee?.name && selectedGroupKey.payee!.includes(txn.payee.name) + ); + } + + for (const txn of txns) { if (txn.tags && txn.tags.length > 0) { for (const tagObj of txn.tags) { const tagName = typeof tagObj === "string" ? tagObj : tagObj.name; diff --git a/src/components/ProgressCard/TopTags.tsx b/src/components/ProgressCard/TopTags.tsx index f88997c..08b2af3 100644 --- a/src/components/ProgressCard/TopTags.tsx +++ b/src/components/ProgressCard/TopTags.tsx @@ -26,8 +26,8 @@ export default function TopTags({ isFetching, }: Props) { const { items, total } = React.useMemo(() => { - return extractTopTags(reportData, flow, selectedPeriodId); - }, [reportData, flow, selectedPeriodId]); + return extractTopTags(reportData, flow, selectedPeriodId, selectedGroupKey); + }, [reportData, flow, selectedPeriodId, selectedGroupKey]); return (