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";