From ceaeca70cc39617fbb88f47616fcb4f59c92ae23 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Mon, 18 May 2026 14:18:36 +0530 Subject: [PATCH] common component props --- src/components/Dashboard/Dashboard.models.ts | 7 ++ src/components/Dashboard/Dashboard.tsx | 14 +-- src/components/Dashboard/Dashboard.view.tsx | 28 ++---- .../HistoryChart/HistoryChart.models.ts | 30 +------ src/components/HistoryChart/HistoryChart.tsx | 9 +- .../HistoryChart/HistoryChart.view.tsx | 13 ++- .../LatestItems/LatestItems.adapter.ts | 58 ++---------- src/components/LatestItems/LatestItems.tsx | 20 ++--- .../ProgressCard/TopPayees.adapter.ts | 56 +++--------- src/components/ProgressCard/TopPayees.tsx | 22 ++--- .../ProgressCard/TopTags.adapter.ts | 66 +++----------- src/components/ProgressCard/TopTags.tsx | 22 ++--- src/components/report.helpers.ts | 88 +++++++++++++++++++ src/components/report.props.ts | 17 ++++ 14 files changed, 191 insertions(+), 259 deletions(-) create mode 100644 src/components/report.props.ts diff --git a/src/components/Dashboard/Dashboard.models.ts b/src/components/Dashboard/Dashboard.models.ts index e22879e..87f9224 100644 --- a/src/components/Dashboard/Dashboard.models.ts +++ b/src/components/Dashboard/Dashboard.models.ts @@ -16,6 +16,13 @@ export interface DashboardState { comparison: boolean; } +export interface DashboardStateSetters { + setSelectedPeriodId: (id: DashboardSelectedPeriodId) => void; + setSelectedGroupKey: (groupKey: GroupKey | null) => void; + togglePeriodType: () => void; + toggleComparison: () => void; +} + export interface DashboardSection { id: string; title?: string; diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx index 4ecd870..50fb4b9 100644 --- a/src/components/Dashboard/Dashboard.tsx +++ b/src/components/Dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import DashboardView from "./Dashboard.view"; -import { DashboardProps, DashboardState } from "./Dashboard.models"; +import { DashboardProps, DashboardState, DashboardStateSetters } from "./Dashboard.models"; export default function Dashboard(props: DashboardProps) { const [state, setState] = React.useState({ @@ -49,16 +49,20 @@ export default function Dashboard(props: DashboardProps) { setState(prev => ({ ...prev, selectedGroupKey: groupKey })); }; + const stateSetters: DashboardStateSetters = { + togglePeriodType, + toggleComparison, + setSelectedPeriodId, + setSelectedGroupKey, + }; + return ( ); } diff --git a/src/components/Dashboard/Dashboard.view.tsx b/src/components/Dashboard/Dashboard.view.tsx index f3837fe..dba16b8 100644 --- a/src/components/Dashboard/Dashboard.view.tsx +++ b/src/components/Dashboard/Dashboard.view.tsx @@ -10,16 +10,13 @@ import { } from "@mui/material"; import { useTheme, alpha } from "@mui/material/styles"; import { GroupKey } from "../../features/report"; -import { DashboardProps, DashboardState } from "./Dashboard.models"; +import { DashboardProps, DashboardState, DashboardStateSetters } from "./Dashboard.models"; interface ViewProps extends DashboardProps { state: DashboardState; setState: React.Dispatch>; toggleFlow: (event: React.MouseEvent, newFlow: "outflows" | "inflows" | null) => void; - togglePeriodType: () => void; - setSelectedPeriodId: (id: string | null) => void; - setSelectedGroupKey: (groupKey: GroupKey | null) => void; - toggleComparison: () => void; + stateSetters: DashboardStateSetters; } export default function DashboardView({ @@ -28,14 +25,12 @@ export default function DashboardView({ state, setState, toggleFlow, - togglePeriodType, - toggleComparison, - setSelectedPeriodId, - setSelectedGroupKey, + stateSetters, }: ViewProps) { const theme = useTheme(); const themeMode = theme.palette.mode; - const { flow, periodType, comparison, selectedPeriodId, selectedGroupKey } = state; + const { flow, selectedGroupKey } = state; + const { setSelectedGroupKey } = stateSetters; // Resolve colors with fallbacks const colors = React.useMemo(() => { @@ -121,17 +116,8 @@ export default function DashboardView({ colorScheme={colors} // State management - flow={flow} - - periodType={periodType} - comparison={comparison} - selectedPeriodId={selectedPeriodId} - selectedGroupKey={selectedGroupKey} - - togglePeriodType={togglePeriodType} - toggleComparison={toggleComparison} - setSelectedPeriodId={setSelectedPeriodId} - setSelectedGroupKey={setSelectedGroupKey} + state={state} + stateSetters={stateSetters} isFetching={arguments[0].isFetching} /> diff --git a/src/components/HistoryChart/HistoryChart.models.ts b/src/components/HistoryChart/HistoryChart.models.ts index 0c63cc0..79b5561 100644 --- a/src/components/HistoryChart/HistoryChart.models.ts +++ b/src/components/HistoryChart/HistoryChart.models.ts @@ -1,9 +1,4 @@ -import { - DashboardFlow, - DashboardPeriodType, - DashboardSelectedPeriodId -} from "../Dashboard"; -import { ReportData } from "../../features/report"; +import { ComponentProps } from "../report.props"; export interface _ChartDataPoint { id: string; @@ -16,27 +11,6 @@ export interface ChartDataPoint extends _ChartDataPoint { compare?: _ChartDataPoint; } -export interface HistoryChartProps { - header: string; - summary?: string; +export interface HistoryChartProps extends ComponentProps { tabs: string[]; - - reportData: ReportData; - - colorScheme: { - primary: string; - light: string; - text: string; - }; - - flow: DashboardFlow; - periodType: DashboardPeriodType; - selectedPeriodId: DashboardSelectedPeriodId; - comparison: boolean; - - togglePeriodType: () => void; - setSelectedPeriodId: (id: string | null) => void; - toggleComparison: () => void; - - isFetching?: boolean; } diff --git a/src/components/HistoryChart/HistoryChart.tsx b/src/components/HistoryChart/HistoryChart.tsx index 98ba837..5dca757 100644 --- a/src/components/HistoryChart/HistoryChart.tsx +++ b/src/components/HistoryChart/HistoryChart.tsx @@ -7,12 +7,13 @@ export default function HistoryChart(props: HistoryChartProps) { const { tabs, reportData, - flow, - comparison, - selectedPeriodId, - setSelectedPeriodId + state, + stateSetters, } = props; + const { flow, comparison, selectedPeriodId } = state; + const { setSelectedPeriodId } = stateSetters; + const [activeTab, setActiveTab] = React.useState(tabs[0] || ""); const [startIndex, setStartIndex] = React.useState(0); diff --git a/src/components/HistoryChart/HistoryChart.view.tsx b/src/components/HistoryChart/HistoryChart.view.tsx index 3b72287..5622a78 100644 --- a/src/components/HistoryChart/HistoryChart.view.tsx +++ b/src/components/HistoryChart/HistoryChart.view.tsx @@ -35,14 +35,8 @@ export default function HistoryChartView(props: ViewProps) { tabs, colorScheme, - flow, - periodType, - selectedPeriodId, - comparison, - - togglePeriodType, - setSelectedPeriodId, - toggleComparison, + state, + stateSetters, activeTab, setActiveTab, @@ -55,6 +49,9 @@ export default function HistoryChartView(props: ViewProps) { activeDataKey, } = props; + const { flow, periodType, selectedPeriodId, comparison } = state; + const { togglePeriodType, setSelectedPeriodId, toggleComparison } = stateSetters; + const theme = useTheme(); const isDark = theme.palette.mode === "dark"; diff --git a/src/components/LatestItems/LatestItems.adapter.ts b/src/components/LatestItems/LatestItems.adapter.ts index d35e8ec..5720ab6 100644 --- a/src/components/LatestItems/LatestItems.adapter.ts +++ b/src/components/LatestItems/LatestItems.adapter.ts @@ -1,67 +1,19 @@ -import { ReportData, Transaction, GroupKey } from "../../features/report"; +import { ReportData, GroupKey } from "../../features/report"; import { - mergeBucketPeriods, - periodIdToKey, formatCurrency, - filterBuckets, + extractFilteredTransactions, } from "../report.helpers"; import { LatestItem } from "./LatestItems.models"; -// ─── Transaction extraction ───────────────────────────────── - -function extractTransactions( - reportData: ReportData, - selectedPeriodId: string | null, - selectedGroupKey: GroupKey | null, -): Transaction[] { - // 1. Get raw transactions - let rawTxns: Transaction[] = []; - - if (selectedPeriodId) { - const key = periodIdToKey(selectedPeriodId); - const periods = mergeBucketPeriods(reportData.buckets, key); - const selected = periods.find((p) => p.id === selectedPeriodId); - rawTxns = selected?.metric.transactions || []; - } else { - const periods = mergeBucketPeriods(reportData.buckets, "all"); - if (periods.length > 0) { - rawTxns = periods[0].metric.transactions || []; - } - } - - // 2. Filter by group key - if (selectedGroupKey) { - rawTxns = rawTxns.filter(txn => { - let match = true; - if (selectedGroupKey.tags && selectedGroupKey.tags.length > 0) { - if (!txn.tags) match = false; - else { - const txnTags = txn.tags.map(t => typeof t === "string" ? t : t.name); - if (!selectedGroupKey.tags.every(selectedTag => txnTags.includes(selectedTag))) match = false; - } - } - if (match && selectedGroupKey.payee && selectedGroupKey.payee.length > 0) { - if (!txn.payee || !txn.payee.name) match = false; - else { - if (!selectedGroupKey.payee.includes(txn.payee.name)) match = false; - } - } - return match; - }); - } - - return rawTxns; -} - // ─── Main adapter ──────────────────────────────────────────── export function buildLatestItems( reportData: ReportData, - selectedPeriodId: string | null, - selectedGroupKey: GroupKey | null, + selectedPeriodId: string | null | undefined, + selectedGroupKey: GroupKey | null | undefined, flow: "outflows" | "inflows" ): LatestItem[] { - const txns = extractTransactions(reportData, selectedPeriodId, selectedGroupKey); + const txns = extractFilteredTransactions(reportData, selectedPeriodId, selectedGroupKey); return txns .sort( diff --git a/src/components/LatestItems/LatestItems.tsx b/src/components/LatestItems/LatestItems.tsx index badc744..ac2d356 100644 --- a/src/components/LatestItems/LatestItems.tsx +++ b/src/components/LatestItems/LatestItems.tsx @@ -1,27 +1,19 @@ import * as React from "react"; -import { ReportData, GroupKey } from "../../features/report"; +import { ComponentProps } from "../report.props"; import { buildLatestItems } from "./LatestItems.adapter"; import LatestItemsView from "./LatestItems.view"; -type Props = { - reportData: ReportData; - flow: "outflows" | "inflows"; - header: string; - selectedPeriodId: string | null; - selectedGroupKey?: GroupKey | null; - accentColor: string; - isFetching?: boolean; -}; +type Props = ComponentProps; export default function LatestItems({ reportData, - flow, + state, + stateSetters, header, - selectedPeriodId, - selectedGroupKey = null, - accentColor, + accentColor = "", isFetching, }: Props) { + const { flow, selectedPeriodId, selectedGroupKey } = state; const [visibleCount, setVisibleCount] = React.useState(5); const allItems = React.useMemo(() => { diff --git a/src/components/ProgressCard/TopPayees.adapter.ts b/src/components/ProgressCard/TopPayees.adapter.ts index af71bea..dffbdc9 100644 --- a/src/components/ProgressCard/TopPayees.adapter.ts +++ b/src/components/ProgressCard/TopPayees.adapter.ts @@ -1,5 +1,8 @@ -import { mergeBucketPeriods, periodIdToKey } from "../report.helpers"; import { GroupKey, ReportData } from "../../features/report"; +import { + extractFilteredTransactions, + aggregateTransactions, +} from "../report.helpers"; export interface PayeeItem { name: string; @@ -12,54 +15,17 @@ export function extractTopPayees( selectedPeriodId?: string | null, selectedGroupKey?: GroupKey | null ): { items: PayeeItem[]; total: number } { - const payeeMap = new Map(); + const txns = extractFilteredTransactions(reportData, selectedPeriodId, selectedGroupKey); - 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); + const { items, total } = aggregateTransactions(txns, (txn) => { + if (txn.payee && txn.payee.name) { + return [txn.payee.name]; } - } 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 []; + }); return { - items: items.slice(0, 4), // Top 4 + items, total, }; } diff --git a/src/components/ProgressCard/TopPayees.tsx b/src/components/ProgressCard/TopPayees.tsx index ea161da..3103496 100644 --- a/src/components/ProgressCard/TopPayees.tsx +++ b/src/components/ProgressCard/TopPayees.tsx @@ -1,30 +1,24 @@ import * as React from "react"; import { Box, Paper, Typography } from "@mui/material"; -import { ReportData, GroupKey } from "../../features/report"; +import { ComponentProps } from "../report.props"; 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; +interface Props extends ComponentProps { compact?: boolean; - isFetching?: boolean; -}; +} export default function TopPayees({ reportData, - flow, + state, + stateSetters, header, - selectedPeriodId, - selectedGroupKey, - setSelectedGroupKey, compact = true, isFetching, }: Props) { + const { flow, selectedPeriodId, selectedGroupKey } = state; + const { setSelectedGroupKey } = stateSetters; + const { items, total } = React.useMemo(() => { return extractTopPayees(reportData, flow, selectedPeriodId, selectedGroupKey); }, [reportData, flow, selectedPeriodId, selectedGroupKey]); diff --git a/src/components/ProgressCard/TopTags.adapter.ts b/src/components/ProgressCard/TopTags.adapter.ts index eb4cf1f..871fb94 100644 --- a/src/components/ProgressCard/TopTags.adapter.ts +++ b/src/components/ProgressCard/TopTags.adapter.ts @@ -1,11 +1,9 @@ -import { ReportData } from "../../features/report"; +import { ReportData, GroupKey } from "../../features/report"; import { - mergeBucketPeriods, - periodIdToKey, + extractFilteredTransactions, + aggregateTransactions, } from "../report.helpers"; -import { GroupKey } from "../../features/report"; - export interface TagItem { tag: string; amount: number; @@ -17,55 +15,17 @@ export function extractTopTags( selectedPeriodId?: string | null, selectedGroupKey?: GroupKey | null ): { items: TagItem[]; total: number } { - const tagMap = new Map(); + const txns = extractFilteredTransactions(reportData, selectedPeriodId, selectedGroupKey); - let periodKey: ReturnType = "all"; - if (selectedPeriodId) { - periodKey = periodIdToKey(selectedPeriodId); - } - - const periods = mergeBucketPeriods(reportData.buckets, periodKey); - - 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 - ); - } - - if (period && period.metric && 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) - ); + const { items, total } = aggregateTransactions(txns, (txn) => { + if (txn.tags && txn.tags.length > 0) { + return txn.tags.map((t) => (typeof t === "string" ? t : t.name)); } + return ["Untagged"]; + }); - 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; - tagMap.set(tagName, (tagMap.get(tagName) || 0) + txn.amount); - } - } else { - tagMap.set("Untagged", (tagMap.get("Untagged") || 0) + txn.amount); - } - } - } - - const arr = Array.from(tagMap.entries()).map(([tag, amount]) => ({ - tag, - amount, - })); - - arr.sort((a, b) => b.amount - a.amount); - - const top = arr.slice(0, 4); - const total = top.reduce((sum, t) => sum + t.amount, 0); - - return { items: top, total }; + return { + items: items.map((item) => ({ tag: item.name, amount: item.amount })), + total, + }; } diff --git a/src/components/ProgressCard/TopTags.tsx b/src/components/ProgressCard/TopTags.tsx index 08b2af3..c3aa263 100644 --- a/src/components/ProgressCard/TopTags.tsx +++ b/src/components/ProgressCard/TopTags.tsx @@ -1,30 +1,24 @@ import * as React from "react"; import { Box, Paper, Typography } from "@mui/material"; -import { ReportData, GroupKey } from "../../features/report"; +import { ComponentProps } from "../report.props"; import ProgressCard from "./ProgressCard"; import { extractTopTags } from "./TopTags.adapter"; -type Props = { - reportData: ReportData; - flow: "outflows" | "inflows"; - header: string; - selectedPeriodId?: string | null; - selectedGroupKey?: GroupKey | null; - setSelectedGroupKey?: (key: GroupKey | null) => void; +interface Props extends ComponentProps { compact?: boolean; - isFetching?: boolean; -}; +} export default function TopTags({ reportData, - flow, + state, + stateSetters, header, - selectedPeriodId, - selectedGroupKey, - setSelectedGroupKey, compact = true, isFetching, }: Props) { + const { flow, selectedPeriodId, selectedGroupKey } = state; + const { setSelectedGroupKey } = stateSetters; + const { items, total } = React.useMemo(() => { return extractTopTags(reportData, flow, selectedPeriodId, selectedGroupKey); }, [reportData, flow, selectedPeriodId, selectedGroupKey]); diff --git a/src/components/report.helpers.ts b/src/components/report.helpers.ts index 0b21725..df441a2 100644 --- a/src/components/report.helpers.ts +++ b/src/components/report.helpers.ts @@ -3,6 +3,8 @@ import { ReportBucket, GroupKey, PeriodType, + ReportData, + Transaction, } from "../features/report"; // ─── Types ──────────────────────────────────────────────────── @@ -140,3 +142,89 @@ export function filterBuckets( if (!selectedGroupKey) return buckets; return buckets.filter((b) => matchesGroupKey(b, selectedGroupKey)); } + +export function extractFilteredTransactions( + reportData: ReportData, + selectedPeriodId: string | null | undefined, + selectedGroupKey: GroupKey | null | undefined +): Transaction[] { + let txns: Transaction[] = []; + + if (selectedPeriodId) { + const key = periodIdToKey(selectedPeriodId); + const periods = mergeBucketPeriods(reportData.buckets, key); + const selected = periods.find((p) => p.id === selectedPeriodId); + txns = selected?.metric.transactions || []; + } else { + const periods = mergeBucketPeriods(reportData.buckets, "all"); + if (periods.length > 0) { + const period = periods.reduce((latest, p) => + new Date(p.start).getTime() > new Date(latest.start).getTime() + ? p + : latest + , periods[0]); + txns = period?.metric.transactions || []; + } + } + + if (selectedGroupKey) { + txns = txns.filter((txn) => { + let match = true; + if (selectedGroupKey.tags && selectedGroupKey.tags.length > 0) { + if (!txn.tags) { + match = false; + } else { + const txnTags = txn.tags.map((t: any) => + typeof t === "string" ? t : t.name + ); + if ( + !selectedGroupKey.tags.every((selectedTag) => + txnTags.includes(selectedTag) + ) + ) { + match = false; + } + } + } + if (match && selectedGroupKey.payee && selectedGroupKey.payee.length > 0) { + if (!txn.payee || !txn.payee.name) { + match = false; + } else { + if (!selectedGroupKey.payee.includes(txn.payee.name)) { + match = false; + } + } + } + return match; + }); + } + + return txns; +} + +export function aggregateTransactions( + transactions: Transaction[], + keyExtractor: (txn: Transaction) => string[], + limit = 4 +): { items: { name: string; amount: number }[]; total: number } { + const map = new Map(); + + for (const txn of transactions) { + const keys = keyExtractor(txn); + for (const key of keys) { + map.set(key, (map.get(key) || 0) + txn.amount); + } + } + + const items = Array.from(map.entries()).map(([name, amount]) => ({ + name, + amount, + })); + + items.sort((a, b) => b.amount - a.amount); + + const top = items.slice(0, limit); + const total = top.reduce((sum, item) => sum + item.amount, 0); + + return { items: top, total }; +} diff --git a/src/components/report.props.ts b/src/components/report.props.ts new file mode 100644 index 0000000..1f7e04e --- /dev/null +++ b/src/components/report.props.ts @@ -0,0 +1,17 @@ +import { ReportData } from "../features/report"; +import { DashboardState, DashboardStateSetters } from "./Dashboard"; + +export interface ComponentProps { + reportData: ReportData; + state: DashboardState; + stateSetters: DashboardStateSetters; + isFetching?: boolean; + header: string; + summary?: string; + accentColor?: string; + colorScheme?: { + primary: string; + light: string; + text: string; + }; +}