From 82264a5c3441990309e67aaf08fde04ef1f25eb0 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Tue, 7 Apr 2026 13:47:55 +0530 Subject: [PATCH 01/44] color pallete --- src/Dashboard.tsx | 41 +++++++++++++++++++++++++++++- src/components/HistoryChart.tsx | 12 ++++++--- src/components/LatestItemsList.tsx | 4 ++- src/types/historyChart.ts | 1 + 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 740b691..3dc2116 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -29,6 +29,20 @@ export default function Dashboard() { expense: [], income: [] }); + const palette = { + expense: { + primary: "#d32f2f", + light: "#fdecea", + dark: "#9a0007", + text: "#b71c1c" + }, + income: { + primary: "#2e7d32", + light: "#e8f5e9", + dark: "#1b5e20", + text: "#1b5e20" + } + }; const [aggregated, setAggregated] = React.useState<{ expense: AggregatedDashboardData | null; @@ -45,6 +59,7 @@ export default function Dashboard() { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); + const colors = palette[mode]; // -------- LOAD ONCE -------- React.useEffect(() => { async function loadData() { @@ -112,13 +127,35 @@ export default function Dashboard() { } return ( - + {/* -------- TOGGLE -------- */} val && setMode(val)} + sx={{ + borderRadius: 3, + overflow: "hidden", + "& .MuiToggleButton-root": { + px: 3, + textTransform: "none", + color: "text.secondary" + }, + "&.Mui-selected": { + bgcolor: colors.primary, + color: "white", + borderColor: colors.primary + }, + }} > Expenses Income @@ -137,6 +174,7 @@ export default function Dashboard() { onPeriodChange={setPeriod} comparison={comparison} setComparison={setComparison} + colorScheme={colors} /> @@ -145,6 +183,7 @@ export default function Dashboard() { title={`Recent ${mode === "expense" ? "Expenses" : "Income"}`} items={currentLatest} onViewAll={() => {}} + accentColor={colors.primary} /> diff --git a/src/components/HistoryChart.tsx b/src/components/HistoryChart.tsx index 5540d9e..97a5c9a 100644 --- a/src/components/HistoryChart.tsx +++ b/src/components/HistoryChart.tsx @@ -72,6 +72,7 @@ export default function HistoryChart({ onPeriodChange, comparison, setComparison, + colorScheme, }: HistoryChartProps) { const [activeTab, setActiveTab] = React.useState(tabs[0] || ""); @@ -141,10 +142,11 @@ export default function HistoryChart({ width: "100%", boxShadow: "none", border: "1px solid", - borderColor: "divider" + borderColor: "divider", + bgcolor: colorScheme.light, }} > - + {header} @@ -296,7 +298,7 @@ export default function HistoryChart({ sx={{ width: 6, height: `${compareHeight}%`, - bgcolor: "grey.400", + bgcolor: `${colorScheme.primary}55`, borderRadius: 2 }} /> @@ -310,7 +312,9 @@ export default function HistoryChart({ sx={{ width: 10, height: `${currentHeight}%`, - bgcolor: point.highlighted ? "error.main" : "primary.main", + bgcolor: point.highlighted + ? colorScheme.primary + : `${colorScheme.primary}99`, borderRadius: 2 }} /> diff --git a/src/components/LatestItemsList.tsx b/src/components/LatestItemsList.tsx index b3f55e3..eae5cfe 100644 --- a/src/components/LatestItemsList.tsx +++ b/src/components/LatestItemsList.tsx @@ -24,12 +24,14 @@ export interface LatestItemsListProps { title?: string; items: LatestItem[]; onViewAll?: () => void; + accentColor: any; } export default function LatestItemsList({ title = "Recent Transactions", items, onViewAll, + accentColor, }: LatestItemsListProps) { return ( @@ -69,7 +71,7 @@ export default function LatestItemsList({ void; comparison: boolean; setComparison: (mode: boolean) => void; + colorScheme: any; } -- 2.49.1 From c9e609fee68c42e8ae5fbc3538620d66876908cc Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sat, 11 Apr 2026 11:23:06 +0530 Subject: [PATCH 02/44] header fixes --- src/Header.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Header.tsx b/src/Header.tsx index cd9cd0a..58ad72b 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -83,12 +83,14 @@ export default function Header({ navigate("/")} > {headerTitle} + + {/* AUTH SECTION */} {isAuthenticated ? ( <> -- 2.49.1 From 175ca64d1f20caa0c096c9dd2c9b6db54a7355fe Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 24 Apr 2026 13:58:12 +0530 Subject: [PATCH 03/44] refactored HistoryChart to component --- src/Dashboard.tsx | 2 +- src/components/HistoryChart.tsx | 391 ------------------ .../HistoryChart/HistoryChart.models.ts} | 26 +- src/components/HistoryChart/HistoryChart.tsx | 60 +++ .../HistoryChart/HistoryChart.utils.ts | 43 ++ .../HistoryChart/HistoryChart.view.tsx | 238 +++++++++++ src/components/HistoryChart/index.ts | 2 + 7 files changed, 357 insertions(+), 405 deletions(-) delete mode 100644 src/components/HistoryChart.tsx rename src/{types/historyChart.ts => components/HistoryChart/HistoryChart.models.ts} (50%) create mode 100644 src/components/HistoryChart/HistoryChart.tsx create mode 100644 src/components/HistoryChart/HistoryChart.utils.ts create mode 100644 src/components/HistoryChart/HistoryChart.view.tsx create mode 100644 src/components/HistoryChart/index.ts diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 3dc2116..5a9d5dd 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -13,7 +13,7 @@ import LatestItemsList, { LatestItem } from "./components/LatestItemsList"; import HistoryChart from "./components/HistoryChart"; import { AggregatedDashboardData -} from "./types/historyChart"; +} from "./components/HistoryChart"; import { fetchLatestTransactions, diff --git a/src/components/HistoryChart.tsx b/src/components/HistoryChart.tsx deleted file mode 100644 index 97a5c9a..0000000 --- a/src/components/HistoryChart.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import * as React from "react"; -import { - Box, - Typography, - ToggleButtonGroup, - ToggleButton, - Paper -} from "@mui/material"; -import { - ChartDataPoint, - HistoryChartProps, - ChartData, -} from "../types/historyChart"; -import IconButton from "@mui/material/IconButton"; -import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; - -const formatDisplay = ( - point: ChartDataPoint, - tab: string, - comparison: boolean -) => { - const base = point.amount; - const cmp = point.compareAmount ?? 0; - - const formatShort = (val: number) => { - if (tab === "monthly") { - if (val >= 100000) return `${(val / 100000).toFixed(2)}L`; - } - if (tab === "weekly") { - if (val >= 1000) return `${(val / 1000).toFixed(1)}K`; - } - return val.toLocaleString("en-IN"); - }; - - // Only hide diff when comparison OFF or compare is undefined - if (!comparison) { - return `₹ ${formatShort(base)}`; - } - - const diff = base - cmp; - const sign = diff >= 0 ? "+" : "-"; - const absDiff = Math.abs(diff); - - return `₹ ${formatShort(base)} (${sign}${formatShort(absDiff)})`; -}; - -const formatLabel = (label: string, type: string) => { - if (type === "monthly") return label; - - if (type === "weekly") { - const parts = label.split(" - "); - if (parts.length === 2) { - const [start, end] = parts; - const startDay = start.split(" ")[0]; - const endParts = end.split(" "); - const endDay = endParts[0]; - const month = endParts[1]; - return `${startDay}–${endDay} ${month}`; - } - } - - return label; -}; - -export default function HistoryChart({ - header, - summary, - tabs, - data, - period, - onPeriodChange, - comparison, - setComparison, - colorScheme, -}: HistoryChartProps) { - const [activeTab, setActiveTab] = React.useState(tabs[0] || ""); - - const handleTabChange = (_: React.MouseEvent, newTab: string | null) => { - if (newTab !== null) setActiveTab(newTab); - }; - - const activeDataKey = activeTab.toLowerCase() as keyof ChartData; - - let rawData: ChartDataPoint[] = []; - - if (activeDataKey === "daily") { - rawData = data.daily || []; - } else { - const section = data[activeDataKey]; - rawData = section?.[period] || []; - } - - const currentData = rawData; - - const maxAmount = - currentData.length > 0 - ? Math.max( - ...currentData.flatMap((d) => - comparison ? [d.amount, d.compareAmount || 0] : [d.amount] - ), - 1 - ) - : 1; - - const [startIndex, setStartIndex] = React.useState(0); - const visibleCountDataTabMapping = { - daily: 7, - weekly: 6, - monthly: 4, - } - const visibleCount = visibleCountDataTabMapping[activeDataKey]; - const total = currentData.length; - - // clamp startIndex so we always show full 5 (when possible) - const clampedStartIndex = Math.min( - startIndex, - Math.max(total - visibleCount, 0) - ); - - const visibleData = currentData.slice( - clampedStartIndex, - clampedStartIndex + visibleCount - ); - - const canGoLeft = startIndex > 0; - const canGoRight = startIndex + visibleCount < currentData.length; - - const handlePrev = () => { - if (canGoLeft) setStartIndex((prev) => prev - visibleCount); - }; - - const handleNext = () => { - if (canGoRight) setStartIndex((prev) => prev + visibleCount); - }; - - return ( - - - {header} - - - {summary && ( - - {summary} - - )} - - - {tabs.map((tab) => ( - - {tab} - - ))} - - - - {/* Rolling / Calendar */} - v && onPeriodChange(v)} - size="small" - > - Rolling - - Calendar - - - - {/* Compare toggle */} - setComparison(!comparison)} - size="small" - sx={{ - textTransform: "none", - borderRadius: 2, - px: 2, - - // OFF - color: "text.secondary", - border: "1px solid", - borderColor: "divider", - - // ON - "&.Mui-selected": { - color: "white", - bgcolor: "success.main", - borderColor: "success.main" - }, - "&.Mui-selected:hover": { - bgcolor: "success.dark" - } - }} - > - Compare - - - - {currentData.length > 0 ? ( - - - {/* LEFT ARROW */} - {canGoLeft && ( - - - - )} - - {/* CHART */} - - {visibleData.map((point) => { - const currentHeight = (point.amount / maxAmount) * 100; - const compareHeight = comparison - ? ((point.compareAmount || 0) / maxAmount) * 100 - : 0; - const labelHeight = Math.max(currentHeight, compareHeight); - - return ( - - - - {formatDisplay(point, activeTab.toLowerCase(), comparison)} - - - {/* Compare */} - {comparison && ( - - )} - - {/* Spacer */} - - - {/* Current */} - - - - - - {formatLabel(point.id, activeDataKey)} - - - - {point.compareLabel - ? formatLabel(point.compareLabel, activeDataKey) - : "placeholder"} - - - - ); - })} - - - {/* RIGHT ARROW */} - {canGoRight && ( - - - - )} - - ) : ( - - No Data Available - - )} - - ); -} diff --git a/src/types/historyChart.ts b/src/components/HistoryChart/HistoryChart.models.ts similarity index 50% rename from src/types/historyChart.ts rename to src/components/HistoryChart/HistoryChart.models.ts index 3ce2855..abda369 100644 --- a/src/types/historyChart.ts +++ b/src/components/HistoryChart/HistoryChart.models.ts @@ -1,21 +1,17 @@ - -export interface ChartDataPoint { +export interface _ChartDataPoint { id: string; amount: number; - compareAmount?: number; - compareLabel?: string; highlighted?: boolean; } -export interface ChartSeries { - rolling: ChartDataPoint[]; - calendar: ChartDataPoint[]; +export interface ChartDataPoint extends _ChartDataPoint { + compare?: _ChartDataPoint; } export interface ChartData { - daily: ChartDataPoint[]; - weekly: ChartSeries; - monthly: ChartSeries; + daily?: ChartDataPoint[]; + weekly?: Record; + monthly?: Record; } export interface AggregatedDashboardData { @@ -30,8 +26,12 @@ export interface HistoryChartProps { tabs: string[]; data: ChartData; period: "rolling" | "calendar"; - onPeriodChange: (mode: "rolling" | "calendar") => void; + onPeriodChange: (p: "rolling" | "calendar") => void; comparison: boolean; - setComparison: (mode: boolean) => void; - colorScheme: any; + setComparison: (v: boolean) => void; + colorScheme: { + primary: string; + light: string; + text: string; + }; } diff --git a/src/components/HistoryChart/HistoryChart.tsx b/src/components/HistoryChart/HistoryChart.tsx new file mode 100644 index 0000000..c1b72c3 --- /dev/null +++ b/src/components/HistoryChart/HistoryChart.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { ChartDataPoint, HistoryChartProps, ChartData } from "./HistoryChart.models"; +import HistoryChartView from "./HistoryChart.view"; + +export default function HistoryChart(props: HistoryChartProps) { + const { tabs, data, period, comparison } = props; + + const [activeTab, setActiveTab] = React.useState(tabs[0] || ""); + const [startIndex, setStartIndex] = React.useState(0); + + const activeDataKey = activeTab.toLowerCase() as keyof ChartData; + + let rawData: ChartDataPoint[] = []; + + if (activeDataKey === "daily") { + rawData = data.daily || []; + } else { + const section = data[activeDataKey]; + rawData = section?.[period] || []; + } + + const currentData = rawData; + + const maxAmount = + currentData.length > 0 + ? Math.max( + ...currentData.flatMap((d) => + comparison ? [d.amount, d.compare?.amount ?? 0] : [d.amount] + ), + 1 + ) + : 1; + + const visibleCountMap = { daily: 7, weekly: 6, monthly: 4 }; + const visibleCount = visibleCountMap[activeDataKey]; + + const total = currentData.length; + + const clampedStartIndex = Math.min(startIndex, Math.max(total - visibleCount, 0)); + + const visibleData = currentData.slice( + clampedStartIndex, + clampedStartIndex + visibleCount + ); + + return ( + + ); +} diff --git a/src/components/HistoryChart/HistoryChart.utils.ts b/src/components/HistoryChart/HistoryChart.utils.ts new file mode 100644 index 0000000..b4bea05 --- /dev/null +++ b/src/components/HistoryChart/HistoryChart.utils.ts @@ -0,0 +1,43 @@ +import { ChartDataPoint } from "./HistoryChart.models"; + +export const formatDisplay = ( + point: ChartDataPoint, + tab: string, + comparison: boolean +) => { + const base = point.amount; + const cmp = point.compare?.amount ?? 0; + + const formatShort = (val: number) => { + if (tab === "monthly" && val >= 100000) { + return `${(val / 100000).toFixed(2)}L`; + } + if (tab === "weekly" && val >= 1000) { + return `${(val / 1000).toFixed(1)}K`; + } + return val.toLocaleString("en-IN"); + }; + + if (!comparison) return `₹ ${formatShort(base)}`; + + const diff = base - cmp; + const sign = diff >= 0 ? "+" : "-"; + + return `₹ ${formatShort(base)} (${sign}${formatShort(Math.abs(diff))})`; +}; + +export const formatLabel = (label: string, type: string) => { + if (type === "monthly") return label; + + if (type === "weekly") { + const parts = label.split(" - "); + if (parts.length === 2) { + const [start, end] = parts; + const startDay = start.split(" ")[0]; + const [endDay, month] = end.split(" "); + return `${startDay}–${endDay} ${month}`; + } + } + + return label; +}; diff --git a/src/components/HistoryChart/HistoryChart.view.tsx b/src/components/HistoryChart/HistoryChart.view.tsx new file mode 100644 index 0000000..35d325f --- /dev/null +++ b/src/components/HistoryChart/HistoryChart.view.tsx @@ -0,0 +1,238 @@ +import * as React from "react"; +import { + Box, + Typography, + ToggleButtonGroup, + ToggleButton, + Paper +} from "@mui/material"; +import IconButton from "@mui/material/IconButton"; +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { + ChartDataPoint, + HistoryChartProps, +} from "./HistoryChart.models"; +import { formatDisplay, formatLabel } from "./HistoryChart.utils"; + +interface ViewProps extends HistoryChartProps { + activeTab: string; + setActiveTab: (v: string) => void; + currentData: ChartDataPoint[]; + visibleData: ChartDataPoint[]; + maxAmount: number; + visibleCount: number; + startIndex: number; + setStartIndex: React.Dispatch>; + activeDataKey: string; +} + +export default function HistoryChartView(props: ViewProps) { + const { + header, + summary, + tabs, + period, + onPeriodChange, + comparison, + setComparison, + colorScheme, + activeTab, + setActiveTab, + currentData, + visibleData, + maxAmount, + visibleCount, + startIndex, + setStartIndex, + activeDataKey, + } = props; + + const handleTabChange = (_: React.MouseEvent, newTab: string | null) => { + if (newTab !== null) setActiveTab(newTab); + }; + + const canGoLeft = startIndex > 0; + const canGoRight = startIndex + visibleCount < currentData.length; + + const handlePrev = () => { + if (canGoLeft) setStartIndex((prev) => prev - visibleCount); + }; + + const handleNext = () => { + if (canGoRight) setStartIndex((prev) => prev + visibleCount); + }; + + return ( + + + {header} + + + {summary && ( + + {summary} + + )} + + + {tabs.map((tab) => ( + + {tab} + + ))} + + + + v && onPeriodChange(v)} size="small"> + Rolling + + Calendar + + + + setComparison(!comparison)} + size="small" + sx={{ + textTransform: "none", + borderRadius: 2, + px: 2, + color: "text.secondary", + border: "1px solid", + borderColor: "divider", + "&.Mui-selected": { + color: "white", + bgcolor: "success.main", + borderColor: "success.main" + }, + "&.Mui-selected:hover": { + bgcolor: "success.dark" + } + }} + > + Compare + + + + {currentData.length > 0 ? ( + + {canGoLeft && ( + + + + )} + + + {visibleData.map((point) => { + const currentHeight = (point.amount / maxAmount) * 100; + const compareHeight = comparison + ? ((point.compare?.amount ?? 0) / maxAmount) * 100 + : 0; + const labelHeight = Math.max(currentHeight, compareHeight); + + return ( + + + + {formatDisplay(point, activeTab.toLowerCase(), comparison)} + + + {comparison && ( + + )} + + + + + + + + + {formatLabel(point.id, activeDataKey)} + + + + {point.compare ? formatLabel(point.compare.id, activeDataKey) : "placeholder"} + + + + ); + })} + + + {canGoRight && ( + + + + )} + + ) : ( + + No Data Available + + )} + + ); +} diff --git a/src/components/HistoryChart/index.ts b/src/components/HistoryChart/index.ts new file mode 100644 index 0000000..28b6303 --- /dev/null +++ b/src/components/HistoryChart/index.ts @@ -0,0 +1,2 @@ +export { default } from "./HistoryChart"; +export * from "./HistoryChart.models"; -- 2.49.1 From b1509fd5ab7bf3554cc89db94ba750ffc89c0442 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 24 Apr 2026 14:01:49 +0530 Subject: [PATCH 04/44] refactored LatestItems to component --- src/Dashboard.tsx | 7 ++----- .../LatestItems/LatestItems.models.ts | 18 ++++++++++++++++++ .../LatestItems.tsx} | 2 +- .../LatestItems/LatestItems.view.tsx | 6 ++++++ src/components/LatestItems/index.ts | 2 ++ 5 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/components/LatestItems/LatestItems.models.ts rename src/components/{LatestItemsList.tsx => LatestItems/LatestItems.tsx} (98%) create mode 100644 src/components/LatestItems/LatestItems.view.tsx create mode 100644 src/components/LatestItems/index.ts diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 5a9d5dd..4543b7b 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -9,11 +9,8 @@ import { ToggleButtonGroup } from "@mui/material"; -import LatestItemsList, { LatestItem } from "./components/LatestItemsList"; -import HistoryChart from "./components/HistoryChart"; -import { - AggregatedDashboardData -} from "./components/HistoryChart"; +import LatestItemsList, { LatestItem } from "./components/LatestItems"; +import HistoryChart, { AggregatedDashboardData } from "./components/HistoryChart"; import { fetchLatestTransactions, diff --git a/src/components/LatestItems/LatestItems.models.ts b/src/components/LatestItems/LatestItems.models.ts new file mode 100644 index 0000000..bd15502 --- /dev/null +++ b/src/components/LatestItems/LatestItems.models.ts @@ -0,0 +1,18 @@ +import * as React from "react"; + +export interface LatestItem { + id: string | number; + icon: React.ReactNode; + iconBgColor?: string; + title: string; + subtitle: string; + amount: string; + timeAgo: string; +} + +export interface LatestItemsListProps { + title?: string; + items: LatestItem[]; + onViewAll?: () => void; + accentColor: string; +} diff --git a/src/components/LatestItemsList.tsx b/src/components/LatestItems/LatestItems.tsx similarity index 98% rename from src/components/LatestItemsList.tsx rename to src/components/LatestItems/LatestItems.tsx index eae5cfe..9c9f247 100644 --- a/src/components/LatestItemsList.tsx +++ b/src/components/LatestItems/LatestItems.tsx @@ -27,7 +27,7 @@ export interface LatestItemsListProps { accentColor: any; } -export default function LatestItemsList({ +export default function LatestItems({ title = "Recent Transactions", items, onViewAll, diff --git a/src/components/LatestItems/LatestItems.view.tsx b/src/components/LatestItems/LatestItems.view.tsx new file mode 100644 index 0000000..71a7983 --- /dev/null +++ b/src/components/LatestItems/LatestItems.view.tsx @@ -0,0 +1,6 @@ +import LatestItemsListView from "./LatestItems.view"; +import { LatestItemsListProps } from "./LatestItems.models"; + +export default function LatestItemsList(props: LatestItemsListProps) { + return ; +} diff --git a/src/components/LatestItems/index.ts b/src/components/LatestItems/index.ts new file mode 100644 index 0000000..2847eeb --- /dev/null +++ b/src/components/LatestItems/index.ts @@ -0,0 +1,2 @@ +export { default } from "./LatestItems"; +export * from "./LatestItems.models"; -- 2.49.1 From 49bdb85088774e6f9adc851d5605b092f4c77925 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 24 Apr 2026 14:04:24 +0530 Subject: [PATCH 05/44] refactored ProgressCard to component --- .../ProgressCard/ProgressCard.models.ts | 7 +++ src/components/ProgressCard/ProgressCard.tsx | 23 ++++++++ .../ProgressCard/ProgressCard.utils.ts | 19 +++++++ .../ProgressCard.view.tsx} | 56 ++++++++++--------- src/components/ProgressCard/index.ts | 2 + 5 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 src/components/ProgressCard/ProgressCard.models.ts create mode 100644 src/components/ProgressCard/ProgressCard.tsx create mode 100644 src/components/ProgressCard/ProgressCard.utils.ts rename src/components/{ProgressCard.tsx => ProgressCard/ProgressCard.view.tsx} (56%) create mode 100644 src/components/ProgressCard/index.ts diff --git a/src/components/ProgressCard/ProgressCard.models.ts b/src/components/ProgressCard/ProgressCard.models.ts new file mode 100644 index 0000000..0d37939 --- /dev/null +++ b/src/components/ProgressCard/ProgressCard.models.ts @@ -0,0 +1,7 @@ +export interface ProgressCardProps { + header: string; + summary?: string; + progressAmount: number; + totalAmount: number; + colorTheme?: "primary" | "secondary" | "error" | "info" | "success" | "warning"; +} diff --git a/src/components/ProgressCard/ProgressCard.tsx b/src/components/ProgressCard/ProgressCard.tsx new file mode 100644 index 0000000..ac82e41 --- /dev/null +++ b/src/components/ProgressCard/ProgressCard.tsx @@ -0,0 +1,23 @@ +import ProgressCardView from "./ProgressCard.view"; +import { ProgressCardProps } from "./ProgressCard.models"; +import { getPercentage, parseSummary } from "./ProgressCard.utils"; + +export default function ProgressCard(props: ProgressCardProps) { + const { progressAmount, totalAmount, summary } = props; + + const percentage = getPercentage(progressAmount, totalAmount); + const { prefixAmount, suffixString } = parseSummary( + summary, + progressAmount, + totalAmount + ); + + return ( + + ); +} diff --git a/src/components/ProgressCard/ProgressCard.utils.ts b/src/components/ProgressCard/ProgressCard.utils.ts new file mode 100644 index 0000000..6f126cd --- /dev/null +++ b/src/components/ProgressCard/ProgressCard.utils.ts @@ -0,0 +1,19 @@ +export const getPercentage = (progressAmount: number, totalAmount: number) => { + if (!totalAmount) return 0; + return Math.min(100, Math.max(0, (progressAmount / totalAmount) * 100)); +}; + +export const parseSummary = ( + summary: string | undefined, + progressAmount: number, + totalAmount: number +) => { + const displaySummary = summary ?? `Rs ${progressAmount} / Rs ${totalAmount}`; + + const parts = displaySummary.split("/"); + const prefixAmount = parts[0]?.trim() || ""; + const suffixString = + parts.length > 1 ? `/ ${parts.slice(1).join("/").trim()}` : ""; + + return { prefixAmount, suffixString }; +}; diff --git a/src/components/ProgressCard.tsx b/src/components/ProgressCard/ProgressCard.view.tsx similarity index 56% rename from src/components/ProgressCard.tsx rename to src/components/ProgressCard/ProgressCard.view.tsx index 74c66d3..94454e8 100644 --- a/src/components/ProgressCard.tsx +++ b/src/components/ProgressCard/ProgressCard.view.tsx @@ -1,29 +1,26 @@ import * as React from "react"; -import { Box, Typography, Paper, LinearProgress, linearProgressClasses } from "@mui/material"; +import { + Box, + Typography, + Paper, + LinearProgress, + linearProgressClasses +} from "@mui/material"; +import { ProgressCardProps } from "./ProgressCard.models"; -export interface ProgressCardProps { - header: string; - summary?: string; - progressAmount: number; - totalAmount: number; - colorTheme?: "primary" | "secondary" | "error" | "info" | "success" | "warning"; +interface ViewProps extends ProgressCardProps { + percentage: number; + prefixAmount: string; + suffixString: string; } -export default function ProgressCard({ +export default function ProgressCardView({ header, - summary, - progressAmount, - totalAmount, colorTheme = "info", -}: ProgressCardProps) { - const percentage = Math.min(100, Math.max(0, (progressAmount / totalAmount) * 100)) || 0; - - const displaySummary = summary ?? `Rs ${progressAmount} / Rs ${totalAmount}`; - - const parts = displaySummary.split('/'); - const prefixAmount = parts[0]?.trim() || ''; - const suffixString = parts.length > 1 ? `/ ${parts.slice(1).join('/').trim()}` : ''; - + percentage, + prefixAmount, + suffixString, +}: ViewProps) { return ( `0 12px 24px -10px ${theme.palette.mode === 'dark' ? '#000' : theme.palette[colorTheme].main}`, + position: "relative", + overflow: "hidden", + boxShadow: (theme) => + `0 12px 24px -10px ${ + theme.palette.mode === "dark" + ? "#000" + : theme.palette[colorTheme].main + }`, }} > @@ -52,7 +54,11 @@ export default function ProgressCard({ {prefixAmount}{" "} {suffixString && ( - + {suffixString} )} @@ -70,7 +76,7 @@ export default function ProgressCard({ }, [`& .${linearProgressClasses.bar}`]: { borderRadius: 5, - backgroundColor: "#fff", + backgroundColor: "#fff", }, }} /> diff --git a/src/components/ProgressCard/index.ts b/src/components/ProgressCard/index.ts new file mode 100644 index 0000000..2847eeb --- /dev/null +++ b/src/components/ProgressCard/index.ts @@ -0,0 +1,2 @@ +export { default } from "./LatestItems"; +export * from "./LatestItems.models"; -- 2.49.1 From 1fe44abfde339cc536277bf4b3fe1ae6a0593ebd Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 24 Apr 2026 14:27:20 +0530 Subject: [PATCH 06/44] removed client data massaging with backend report using feature/report --- src/features/report/report.api.ts | 9 ++ src/features/report/report.mapper.ts | 96 ++++++++++++++ src/features/report/report.service.ts | 114 +++++++++++++++++ src/features/report/useReport.ts | 17 +++ src/utils/dashboardLoader.ts | 142 ++------------------ src/utils/dateUtils.ts | 33 ----- src/utils/periodBuilders.ts | 178 -------------------------- 7 files changed, 244 insertions(+), 345 deletions(-) create mode 100644 src/features/report/report.api.ts create mode 100644 src/features/report/report.mapper.ts create mode 100644 src/features/report/report.service.ts create mode 100644 src/features/report/useReport.ts delete mode 100644 src/utils/dateUtils.ts delete mode 100644 src/utils/periodBuilders.ts diff --git a/src/features/report/report.api.ts b/src/features/report/report.api.ts new file mode 100644 index 0000000..528b844 --- /dev/null +++ b/src/features/report/report.api.ts @@ -0,0 +1,9 @@ +import { api } from "../../../react-openapi"; + +export async function fetchReport(params: { + period: "weekly" | "monthly" | "yearly" | "fyly"; + rolling?: boolean; +}) { + const res = await api.get("/reports", { params }); + return res.data; +} \ No newline at end of file diff --git a/src/features/report/report.mapper.ts b/src/features/report/report.mapper.ts new file mode 100644 index 0000000..ddd469a --- /dev/null +++ b/src/features/report/report.mapper.ts @@ -0,0 +1,96 @@ +import { + AggregatedDashboardData, + ChartData, + ChartDataPoint, +} from "../../components/HistoryChart"; + +type ReportBucket = any; + +const sumBucket = (bucket: ReportBucket, flow: "expenses" | "incomes") => + bucket.groups.reduce( + (acc: number, g: any) => acc + (g?.[flow]?.sum || 0), + 0 + ); + +const toLabel = (start: string, end: string, type: "weekly" | "monthly") => { + const s = new Date(start); + const e = new Date(end); + + if (type === "monthly") { + return s.toLocaleString("default", { month: "short" }); + } + + return `${s.getDate()}–${e.getDate()} ${e.toLocaleString("default", { + month: "short", + })}`; +}; + +const toPoints = ( + buckets: ReportBucket[], + type: "weekly" | "monthly", + flow: "expenses" | "incomes" +): ChartDataPoint[] => { + return buckets.map((b, i) => { + const amount = sumBucket(b, flow); + const prev = buckets[i - 1]; + + return { + id: toLabel(b.start, b.end, type), + amount, + compare: prev + ? { + id: toLabel(prev.start, prev.end, type), + amount: sumBucket(prev, flow), + } + : undefined, + }; + }); +}; + +export function mapReportToDashboard( + weekly: ReportBucket[], + monthly: ReportBucket[], + type: "expense" | "income" +): AggregatedDashboardData { + const flow = type === "expense" ? "expenses" : "incomes"; + + const chartData: ChartData = { + daily: [], + + weekly: { + rolling: toPoints(weekly, "weekly", flow), + calendar: toPoints(weekly, "weekly", flow), + }, + + monthly: { + rolling: toPoints(monthly, "monthly", flow), + calendar: toPoints(monthly, "monthly", flow), + }, + }; + + const totalAmount = weekly.reduce( + (acc, b) => acc + sumBucket(b, flow), + 0 + ); + + const payeeMap: Record = {}; + + for (const b of weekly) { + for (const g of b.groups) { + const key = g.group_key || "Unknown"; + const amt = g?.[flow]?.sum || 0; + payeeMap[key] = (payeeMap[key] || 0) + amt; + } + } + + const topPayees = Object.entries(payeeMap) + .map(([payeeName, amount]) => ({ payeeName, amount })) + .sort((a, b) => b.amount - a.amount) + .slice(0, 5); + + return { + chartData, + totalAmount, + topPayees, + }; +} diff --git a/src/features/report/report.service.ts b/src/features/report/report.service.ts new file mode 100644 index 0000000..b5726ad --- /dev/null +++ b/src/features/report/report.service.ts @@ -0,0 +1,114 @@ +import { fetchReport } from "./report.api"; +import { + AggregatedDashboardData, + ChartData, + ChartDataPoint, +} from "../components/HistoryChart"; + +type ReportBucket = any; // replace with generated type if available + +function sumBucket(bucket: ReportBucket, flow: "expenses" | "incomes") { + return bucket.groups.reduce( + (acc: number, g: any) => acc + (g?.[flow]?.sum || 0), + 0 + ); +} + +function toLabel(start: string, end: string, type: "weekly" | "monthly") { + const s = new Date(start); + const e = new Date(end); + + if (type === "monthly") { + return s.toLocaleString("default", { month: "short" }); + } + + const sd = s.getDate(); + const ed = e.getDate(); + const m = e.toLocaleString("default", { month: "short" }); + return `${sd}–${ed} ${m}`; +} + +function toChartPoints( + buckets: ReportBucket[], + type: "weekly" | "monthly", + flow: "expenses" | "incomes" +): ChartDataPoint[] { + return buckets.map((b, i) => { + const amount = sumBucket(b, flow); + + const prev = buckets[i - 1]; + const compareAmount = prev ? sumBucket(prev, flow) : 0; + + return { + id: toLabel(b.start, b.end, type), + amount, + compare: prev + ? { + id: toLabel(prev.start, prev.end, type), + amount: compareAmount, + } + : undefined, + }; + }); +} + +function buildChartData( + weekly: ReportBucket[], + monthly: ReportBucket[], + flow: "expenses" | "incomes" +): ChartData { + return { + daily: [], // not supported by /reports → keep empty or drop + weekly: { + rolling: toChartPoints(weekly, "weekly", flow), + calendar: toChartPoints(weekly, "weekly", flow), // same unless backend differentiates + }, + monthly: { + rolling: toChartPoints(monthly, "monthly", flow), + calendar: toChartPoints(monthly, "monthly", flow), + }, + }; +} + +function getTopPayees(buckets: ReportBucket[], flow: "expenses" | "incomes") { + const map: Record = {}; + + for (const b of buckets) { + for (const g of b.groups) { + const key = g.group_key || "Unknown"; + const amt = g?.[flow]?.sum || 0; + map[key] = (map[key] || 0) + amt; + } + } + + return Object.entries(map) + .map(([payeeName, amount]) => ({ payeeName, amount })) + .sort((a, b) => b.amount - a.amount) + .slice(0, 5); +} + +export async function getDashboardData( + type: "expense" | "income" +): Promise { + const flow = type === "expense" ? "expenses" : "incomes"; + + const [weeklyBuckets, monthlyBuckets] = await Promise.all([ + fetchReport({ period: "weekly", rolling: true }), + fetchReport({ period: "monthly", rolling: true }), + ]); + + const chartData = buildChartData(weeklyBuckets, monthlyBuckets, flow); + + const totalAmount = weeklyBuckets.reduce( + (acc: number, b: any) => acc + sumBucket(b, flow), + 0 + ); + + const topPayees = getTopPayees(weeklyBuckets, flow); + + return { + chartData, + totalAmount, + topPayees, + }; +} \ No newline at end of file diff --git a/src/features/report/useReport.ts b/src/features/report/useReport.ts new file mode 100644 index 0000000..bd2e039 --- /dev/null +++ b/src/features/report/useReport.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchReport } from "./report.api"; + +export interface ReportParams { + period: "weekly" | "monthly" | "yearly" | "fyly"; + rolling?: boolean; + report_date?: string; + group_by?: ("flow" | "payee" | "tags")[]; + ignore_self?: boolean; +} + +export function useReport(params: ReportParams) { + return useQuery({ + queryKey: ["report", params], + queryFn: () => fetchReport(params), + }); +} \ No newline at end of file diff --git a/src/utils/dashboardLoader.ts b/src/utils/dashboardLoader.ts index 1630b98..654c961 100644 --- a/src/utils/dashboardLoader.ts +++ b/src/utils/dashboardLoader.ts @@ -1,17 +1,10 @@ import { api } from "../../react-openapi"; -import { LatestItem } from "../components/LatestItemsList"; -import { ChartDataPoint } from "../types/historyChart"; +import { LatestItem } from "../components/LatestItems"; import * as React from "react"; -import { format } from "./dateUtils"; import MonetizationOnIcon from "@mui/icons-material/MonetizationOn"; -import { - buildDailyBuckets, - buildWeeklyRolling, - buildWeeklyCalendar, - buildMonthlyRolling, - buildMonthlyCalendar -} from "./periodBuilders"; +import { fetchReport } from "../features/report/report.api"; +import { mapReportToDashboard } from "../features/report/report.mapper"; const DEFAULT_ICON = React.createElement(MonetizationOnIcon, { sx: { color: "#388e3c" } @@ -58,131 +51,12 @@ export async function fetchLatestTransactions( export async function fetchAggregatedData( type: "expense" | "income" ) { - const res = await api.get("/expenses", { params: { limit: 0 } }); - const all: any[] = res.data?.items || res.data || []; + const [weekly, monthly] = await Promise.all([ + fetchReport({ period: "weekly", rolling: true }), + fetchReport({ period: "monthly", rolling: true }), + ]); - const now = new Date(); - - let totalAmount = 0; - const payeeMap: Record = {}; - - const isValid = (amt: number) => - type === "expense" ? amt < 0 : amt > 0; - - const normalize = (amt: number) => Math.abs(amt); - - const { - buckets: dailyBuckets, - weekStart, - weekEnd, - prevWeekStart, - prevWeekEnd - } = buildDailyBuckets(now); - - const weeklyRolling = buildWeeklyRolling(now); - const weeklyCalendar = buildWeeklyCalendar(now); - const monthlyRolling = buildMonthlyRolling(now); - const monthlyCalendar = buildMonthlyCalendar(now); - - for (const item of all) { - const d = new Date( - item.occurred_at || item.created_at || Date.now() - ); - - const amtRaw = Number(item.amount) || 0; - if (!isValid(amtRaw)) continue; - - const amt = normalize(amtRaw); - totalAmount += amt; - - const payee = item.payee?.name || item.payee || "Unknown"; - payeeMap[payee] = (payeeMap[payee] || 0) + amt; - - const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()]; - - if (d >= weekStart && d <= weekEnd) { - if (dailyBuckets[day]) { - dailyBuckets[day].amount += amt; - } - } - - if (d >= prevWeekStart && d <= prevWeekEnd) { - if (dailyBuckets[day]) { - dailyBuckets[day].compare += amt; - } - } - - const apply = (arr: any[]) => { - for (const b of arr) { - if (d >= b.start && d <= b.end) b.amount += amt; - if (d >= b.prevStart && d <= b.prevEnd) - b.compare += amt; - } - }; - - apply(weeklyRolling); - apply(weeklyCalendar); - apply(monthlyRolling); - apply(monthlyCalendar); - } - - const toPoints = (arr: any[], type: "weekly" | "monthly"): ChartDataPoint[] => - arr.map((x) => { - let compareLabel: string | undefined; - - if (x.prevStart && x.prevEnd) { - if (type === "monthly") { - const year = String(x.prevStart.getFullYear()).slice(2); - compareLabel = `${x.prevStart.toLocaleString("default", { - month: "short" - })}-${year}`; - } else { - const year = String(x.prevEnd.getFullYear()).slice(2); - compareLabel = `${format(x.prevStart)} - ${format(x.prevEnd)} ${year}`; - } - } - - return { - id: x.label, - amount: x.amount, - compareAmount: x.compare, - compareLabel - }; - }); - - const chartData = { - daily: Object.entries(dailyBuckets).map(([k, v]: any) => ({ - id: k, - amount: v.amount, - compareAmount: v.compare - })), - weekly: { - rolling: toPoints(weeklyRolling, "weekly"), - calendar: toPoints(weeklyCalendar, "weekly") - }, - monthly: { - rolling: toPoints(monthlyRolling, "monthly"), - calendar: toPoints(monthlyCalendar, "monthly") - } - }; - - Object.values(chartData).forEach((group: any) => { - const arr = Array.isArray(group) ? group : group.rolling; - if (!arr?.length) return; - - let max = arr[0]; - for (const g of arr) { - if (g.amount > max.amount) max = g; - } - if (max.amount > 0) max.highlighted = true; - }); - - const topPayees = Object.entries(payeeMap) - .map(([name, amt]) => ({ payeeName: name, amount: amt })) - .sort((a, b) => b.amount - a.amount) - .slice(0, 5); - - return { chartData, totalAmount, topPayees }; + return mapReportToDashboard(weekly.buckets, monthly.buckets, type); } export const fetchAggregatedExpenses = () => diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts deleted file mode 100644 index 0652304..0000000 --- a/src/utils/dateUtils.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const format = (d: Date) => - `${d.getDate()} ${d.toLocaleString("default", { month: "short" })}`; - -export const startOfDay = (d: Date) => { - const x = new Date(d); - x.setHours(0, 0, 0, 0); - return x; -}; - -export const endOfDay = (d: Date) => { - const x = new Date(d); - x.setHours(23, 59, 59, 999); - return x; -}; - -export const getStartOfWeek = (d: Date) => { - const date = new Date(d); - const day = date.getDay() || 7; - if (day !== 1) date.setDate(date.getDate() - (day - 1)); - return startOfDay(date); -}; - -export const shiftDate = (d: Date, days: number) => - new Date(d.getTime() + days * 86400000); - -export const getWeekIndex = (date: Date) => { - const firstDay = new Date(date.getFullYear(), date.getMonth(), 1); - const firstWeekStart = getStartOfWeek(firstDay); - return Math.floor( - (startOfDay(date).getTime() - firstWeekStart.getTime()) / - (7 * 86400000) - ); -}; diff --git a/src/utils/periodBuilders.ts b/src/utils/periodBuilders.ts deleted file mode 100644 index daa444e..0000000 --- a/src/utils/periodBuilders.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - format, - endOfDay, - getStartOfWeek, - shiftDate, - getWeekIndex -} from "./dateUtils"; - -export const buildDailyBuckets = (now: Date) => { - const buckets: Record = { - Mon: { amount: 0, compare: 0 }, - Tue: { amount: 0, compare: 0 }, - Wed: { amount: 0, compare: 0 }, - Thu: { amount: 0, compare: 0 }, - Fri: { amount: 0, compare: 0 }, - Sat: { amount: 0, compare: 0 }, - Sun: { amount: 0, compare: 0 } - }; - - const weekStart = getStartOfWeek(now); - const weekEnd = endOfDay(new Date(weekStart.getTime() + 6 * 86400000)); - const prevWeekStart = shiftDate(weekStart, -7); - const prevWeekEnd = shiftDate(weekEnd, -7); - - return { buckets, weekStart, weekEnd, prevWeekStart, prevWeekEnd }; -}; - -const getPrevMonthWeek = (start: Date) => { - const prevMonthDate = new Date(start); - prevMonthDate.setMonth(prevMonthDate.getMonth() - 1); - - const prevMonthFirst = new Date( - prevMonthDate.getFullYear(), - prevMonthDate.getMonth(), - 1 - ); - - const prevFirstWeekStart = getStartOfWeek(prevMonthFirst); - const weekIndex = getWeekIndex(start); - - const prevStart = new Date( - prevFirstWeekStart.getTime() + weekIndex * 7 * 86400000 - ); - const prevEnd = endOfDay(new Date(prevStart.getTime() + 6 * 86400000)); - - return { prevStart, prevEnd }; -}; - -export const buildWeeklyRolling = (now: Date) => { - const arr: any[] = []; - const currentWeekStart = getStartOfWeek(now); - - for (let i = 4; i >= 0; i--) { - const start = new Date( - currentWeekStart.getTime() - i * 7 * 86400000 - ); - const end = endOfDay(new Date(start.getTime() + 6 * 86400000)); - - const { prevStart, prevEnd } = getPrevMonthWeek(start); - - arr.push({ - label: `${format(start)} - ${format(end)}`, - start, - end, - amount: 0, - compare: 0, - prevStart, - prevEnd - }); - } - - return arr; -}; - -export const buildWeeklyCalendar = (now: Date) => { - const arr: any[] = []; - - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); - const firstWeekStart = getStartOfWeek(startOfMonth); - - const totalWeeks = - Math.ceil( - (endOfMonth.getTime() - firstWeekStart.getTime()) / - (7 * 86400000) - ) + 1; - - for (let i = 0; i < totalWeeks; i++) { - const start = new Date( - firstWeekStart.getTime() + i * 7 * 86400000 - ); - const end = endOfDay(new Date(start.getTime() + 6 * 86400000)); - - const { prevStart, prevEnd } = getPrevMonthWeek(start); - - arr.push({ - label: `${format(start)} - ${format(end)}`, - start, - end, - amount: 0, - compare: 0, - prevStart, - prevEnd - }); - } - - return arr; -}; - -export const buildMonthlyRolling = (now: Date) => { - const arr: any[] = []; - - for (let i = 11; i >= 0; i--) { - const d = new Date(now); - d.setMonth(d.getMonth() - i); - - const start = new Date(d.getFullYear(), d.getMonth(), 1); - const end = - i === 0 - ? endOfDay(now) - : endOfDay(new Date(d.getFullYear(), d.getMonth() + 1, 0)); - - const prevStart = new Date(start); - prevStart.setFullYear(prevStart.getFullYear() - 1); - - let prevEnd = new Date(end); - prevEnd.setFullYear(prevEnd.getFullYear() - 1); - - if (i === 0) { - prevEnd = new Date(prevStart); - prevEnd.setDate(now.getDate()); - prevEnd = endOfDay(prevEnd); - } - - arr.push({ - label: `${d.toLocaleString("default", { - month: "short" - })}-${String(d.getFullYear()).slice(2)}`, - start, - end, - amount: 0, - compare: 0, - prevStart, - prevEnd - }); - } - - return arr; -}; - -export const buildMonthlyCalendar = (now: Date) => { - const arr: any[] = []; - - for (let i = 0; i < 12; i++) { - const start = new Date(now.getFullYear(), i, 1); - const end = endOfDay(new Date(now.getFullYear(), i + 1, 0)); - - const prevStart = new Date(start); - prevStart.setFullYear(prevStart.getFullYear() - 1); - - const prevEnd = new Date(end); - prevEnd.setFullYear(prevEnd.getFullYear() - 1); - - arr.push({ - label: `${start.toLocaleString("default", { - month: "short" - })}-${String(start.getFullYear()).slice(2)}`, - start, - end, - amount: 0, - compare: 0, - prevStart, - prevEnd - }); - } - - return arr; -}; -- 2.49.1 From 922d05ae3740a89f3fceb92b709b496618de16b0 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 24 Apr 2026 14:41:19 +0530 Subject: [PATCH 07/44] dashboard feature to use dashboard data --- src/Dashboard.tsx | 106 ++++-------------- .../dashboard/dashboard.service.ts} | 10 +- src/features/dashboard/index.ts | 3 + src/features/dashboard/useDashboardData.ts | 24 ++++ src/features/report/index.ts | 3 + src/main.jsx | 71 +++++++----- 6 files changed, 96 insertions(+), 121 deletions(-) rename src/{utils/dashboardLoader.ts => features/dashboard/dashboard.service.ts} (85%) create mode 100644 src/features/dashboard/index.ts create mode 100644 src/features/dashboard/useDashboardData.ts create mode 100644 src/features/report/index.ts diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 4543b7b..6afd4fd 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -9,25 +9,18 @@ import { ToggleButtonGroup } from "@mui/material"; -import LatestItemsList, { LatestItem } from "./components/LatestItems"; -import HistoryChart, { AggregatedDashboardData } from "./components/HistoryChart"; +import LatestItems from "./components/LatestItems"; +import HistoryChart from "./components/HistoryChart"; -import { - fetchLatestTransactions, - fetchAggregatedExpenses, - fetchAggregatedIncome, -} from "./utils/dashboardLoader"; +import { useDashboardData } from "./features/dashboard"; export default function Dashboard() { - const [latest, setLatest] = React.useState<{ - expense: LatestItem[]; - income: LatestItem[]; - }>({ - expense: [], - income: [] - }); + const [mode, setMode] = React.useState<"expense" | "income">("expense"); + const [period, setPeriod] = React.useState<"rolling" | "calendar">("rolling"); + const [comparison, setComparison] = React.useState(false); + const palette = { - expense: { + expense: { primary: "#d32f2f", light: "#fdecea", dark: "#9a0007", @@ -41,73 +34,11 @@ export default function Dashboard() { } }; - const [aggregated, setAggregated] = React.useState<{ - expense: AggregatedDashboardData | null; - income: AggregatedDashboardData | null; - }>({ - expense: null, - income: null - }); - - const [mode, setMode] = React.useState<"expense" | "income">("expense"); - const [period, setPeriod] = React.useState<"rolling" | "calendar">("rolling"); - const [comparison, setComparison] = React.useState(false); - - const [loading, setLoading] = React.useState(true); - const [error, setError] = React.useState(null); + const { data, latest, isLoading, error } = useDashboardData(mode); const colors = palette[mode]; - // -------- LOAD ONCE -------- - React.useEffect(() => { - async function loadData() { - try { - setLoading(true); - const [ - latestExpense, - latestIncome, - expenseData, - incomeData - ] = await Promise.all([ - fetchLatestTransactions("expense"), - fetchLatestTransactions("income"), - fetchAggregatedExpenses(), - fetchAggregatedIncome() - ]); - - setLatest({ - expense: latestExpense, - income: latestIncome - }); - - setAggregated({ - expense: expenseData, - income: incomeData - }); - - } catch (err: any) { - console.error(err); - setError(err.message || "Failed to load dashboard data"); - } finally { - setLoading(false); - } - } - - loadData(); - }, []); - - const currentData = aggregated[mode]; - if (!currentData) { - return ( - - - - ); - } - const currentLatest = latest[mode]; - - // -------- UI STATES -------- - if (loading) { + if (isLoading) { return ( @@ -118,11 +49,15 @@ export default function Dashboard() { if (error) { return ( - {error} + {String(error)} ); } + if (!data) { + return null; + } + return ( - {/* -------- TOGGLE -------- */} - - {}} accentColor={colors.primary} /> - ); -} \ No newline at end of file +} diff --git a/src/utils/dashboardLoader.ts b/src/features/dashboard/dashboard.service.ts similarity index 85% rename from src/utils/dashboardLoader.ts rename to src/features/dashboard/dashboard.service.ts index 654c961..f466cf6 100644 --- a/src/utils/dashboardLoader.ts +++ b/src/features/dashboard/dashboard.service.ts @@ -1,10 +1,10 @@ -import { api } from "../../react-openapi"; -import { LatestItem } from "../components/LatestItems"; +import { api } from "../../../react-openapi"; +import { LatestItem } from "../../components/LatestItems"; import * as React from "react"; import MonetizationOnIcon from "@mui/icons-material/MonetizationOn"; -import { fetchReport } from "../features/report/report.api"; -import { mapReportToDashboard } from "../features/report/report.mapper"; +import { fetchReport } from "../report/report.api"; +import { mapReportToDashboard } from "../report/report.mapper"; const DEFAULT_ICON = React.createElement(MonetizationOnIcon, { sx: { color: "#388e3c" } @@ -17,7 +17,7 @@ export async function fetchLatestTransactions( params: { limit: 100, sort: "-occurred_at" } }); - const items = res.data?.items || res.data || []; + const items = res.data || []; const isValid = (amt: number) => type === "expense" ? amt < 0 : amt > 0; diff --git a/src/features/dashboard/index.ts b/src/features/dashboard/index.ts new file mode 100644 index 0000000..ddf8c37 --- /dev/null +++ b/src/features/dashboard/index.ts @@ -0,0 +1,3 @@ +export { + useDashboardData +} from './useDashboardData' diff --git a/src/features/dashboard/useDashboardData.ts b/src/features/dashboard/useDashboardData.ts new file mode 100644 index 0000000..028be86 --- /dev/null +++ b/src/features/dashboard/useDashboardData.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; +import { + fetchAggregatedData, + fetchLatestTransactions, +} from "./dashboard.service"; + +export function useDashboardData(type: "expense" | "income") { + const aggregated = useQuery({ + queryKey: ["dashboard", type], + queryFn: () => fetchAggregatedData(type), + }); + + const latest = useQuery({ + queryKey: ["latest", type], + queryFn: () => fetchLatestTransactions(type), + }); + + return { + data: aggregated.data, + latest: latest.data, + isLoading: aggregated.isLoading || latest.isLoading, + error: aggregated.error || latest.error, + }; +} diff --git a/src/features/report/index.ts b/src/features/report/index.ts new file mode 100644 index 0000000..f037c04 --- /dev/null +++ b/src/features/report/index.ts @@ -0,0 +1,3 @@ +export { + useReport +} from './useReport' diff --git a/src/main.jsx b/src/main.jsx index c1c76f8..139efe0 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -21,18 +21,30 @@ import Header from './Header'; import Footer from './Footer'; import AppTheme from './AppTheme'; -// Polyfill Node.js globals for browser environment (needed by SwaggerParser) +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + window.Buffer = Buffer; window.process = process; const rootElement = document.getElementById('root'); const root = createRoot(rootElement); + const API_BASE = import.meta.env.VITE_API_BASE_URL; const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL; // Initialize global API clients so all components across khata-ui have generic API access initializeApiClients(API_BASE, AUTH_BASE); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); + const routerMapping = [ { path: "/", component: Home, headerTitle: "Home" }, { path: "/home", component: Home, headerTitle: "Home" }, @@ -41,35 +53,36 @@ const routerMapping = [ ]; root.render( - - - - -
+ + + + + +
- - + + - - {routerMapping.map(({ path, component: Component }) => ( - - ) : ( - - ) - } - /> - ))} - + + {routerMapping.map(({ path, component: Component }) => ( + + ) : ( + + ) + } + /> + ))} + + - - -