From e6c7778c08bf40a0780f1a4e584f532c64d708b8 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Mon, 6 Apr 2026 18:39:19 +0530 Subject: [PATCH] comparison --- src/Dashboard.tsx | 5 +- src/components/HistoryChart.tsx | 182 +++++++++++++++++++++----------- src/utils/dashboardLoader.ts | 87 +++++++++++---- 3 files changed, 194 insertions(+), 80 deletions(-) diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 1fdbec0..bc0ace9 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -38,6 +38,7 @@ export default function Dashboard() { 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); @@ -131,9 +132,11 @@ export default function Dashboard() { header={`${mode === "expense" ? "Expense" : "Income"} Breakdown`} summary="Interactive chronological tracking" tabs={["Daily", "Weekly", "Monthly"]} - data={currentData?.chartData || {}} + data={currentData.chartData} period={period} onPeriodChange={setPeriod} + comparison={comparison} + setComparison={setComparison} /> diff --git a/src/components/HistoryChart.tsx b/src/components/HistoryChart.tsx index 473d9cf..5759c78 100644 --- a/src/components/HistoryChart.tsx +++ b/src/components/HistoryChart.tsx @@ -9,13 +9,26 @@ export interface ChartDataPoint { highlighted?: boolean; } +export interface ChartSeries { + rolling: ChartDataPoint[]; + calendar: ChartDataPoint[]; +} + +export interface ChartData { + daily: ChartDataPoint[]; + weekly: ChartSeries; + monthly: ChartSeries; +} + export interface HistoryChartProps { header: string; summary?: string; tabs: string[]; - data: Record; - period: "rolling" | "calendar", + data: ChartData; + period: "rolling" | "calendar"; onPeriodChange: (mode: "rolling" | "calendar") => void; + comparison: boolean; + setComparison: (mode: boolean) => void; } export default function HistoryChart({ @@ -25,6 +38,8 @@ export default function HistoryChart({ data, period, onPeriodChange, + comparison, + setComparison, }: HistoryChartProps) { const [activeTab, setActiveTab] = React.useState(tabs[0] || ""); @@ -34,54 +49,69 @@ export default function HistoryChart({ } }; - const activeDataKey = activeTab.toLowerCase(); - let rawData; + const activeDataKey = activeTab.toLowerCase() as keyof ChartData; + + let rawData: ChartDataPoint[] = []; + if (activeDataKey === "daily") { - rawData = data.daily; + rawData = data.daily || []; } else { - // @ts-ignore - rawData = data[activeDataKey]?.[period] || []; + const section = data[activeDataKey]; + rawData = section?.[period] || []; } + const currentData = rawData; const maxAmount = currentData.length > 0 - ? Math.max(...currentData.map((d: { amount: any; }) => d.amount), 1) + ? Math.max( + ...currentData.flatMap((d) => + comparison ? [d.amount, d.compareAmount || 0] : [d.amount] + ), + 1 + ) : 1; - // ✅ Formatter (₹ + adaptive units) const formatAmount = (amount: number) => { const tab = activeTab.toLowerCase(); if (amount === 0) return ""; if (tab === "monthly") { - if (amount >= 100000) { - return `₹ ${(amount / 100000).toFixed(2)} L`; - } + if (amount >= 100000) return `₹ ${(amount / 100000).toFixed(2)} L`; return `₹ ${amount.toLocaleString("en-IN")}`; } if (tab === "weekly") { - if (amount >= 1000) { - return `₹ ${(amount / 1000).toFixed(1)} K`; - } + if (amount >= 1000) return `₹ ${(amount / 1000).toFixed(1)} K`; return `₹ ${amount.toLocaleString("en-IN")}`; } return `₹ ${amount.toLocaleString("en-IN")}`; }; + return ( - + {header} + {summary && ( {summary} )} + {/* Tabs */} theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)', + bgcolor: (theme) => + theme.palette.mode === "dark" + ? "rgba(255,255,255,0.05)" + : "rgba(0,0,0,0.02)", borderRadius: 8, p: 0.5, "& .MuiToggleButton-root": { @@ -99,11 +132,16 @@ export default function HistoryChart({ fontWeight: 600, color: "text.secondary", "&.Mui-selected": { - bgcolor: (theme) => theme.palette.mode === 'dark' ? 'primary.dark' : 'primary.light', - color: (theme) => theme.palette.mode === 'dark' ? 'primary.contrastText' : 'primary.main', - boxShadow: '0 2px 8px rgba(0,0,0,0.05)', - }, - }, + bgcolor: (theme) => + theme.palette.mode === "dark" + ? "primary.dark" + : "primary.light", + color: (theme) => + theme.palette.mode === "dark" + ? "primary.contrastText" + : "primary.main" + } + } }} > {tabs.map((tab) => ( @@ -113,6 +151,7 @@ export default function HistoryChart({ ))} + {/* Period Toggle */} Rolling - + Calendar - {/* Chart Area */} + setComparison(v === "on")} + size="small" + sx={{ mb: 2 }} + > + Single + Compare + + + {/* Chart */} {currentData.length > 0 ? ( - - {currentData.map((point: { amount: number; id: string, highlighted: boolean }) => { - const heightPerc = (point.amount / maxAmount) * 100; + + {currentData.map((point) => { + const currentHeight = (point.amount / maxAmount) * 100; + const compareHeight = ((point.compareAmount || 0) / maxAmount) * 100; + return ( + {/* Values */} + + {formatAmount(point.amount)} + + + {/* Bars */} + + {/* Compare */} + {comparison && ( + + )} + + {/* Current */} + + + + {/* Label */} - {point.amount > 0 ? formatAmount(point.amount) : ""} - - - `0 4px 12px ${theme.palette.error.main}40`, - }), - }} - /> - - {point.id} diff --git a/src/utils/dashboardLoader.ts b/src/utils/dashboardLoader.ts index 78a1864..9c4cc14 100644 --- a/src/utils/dashboardLoader.ts +++ b/src/utils/dashboardLoader.ts @@ -32,6 +32,9 @@ const getStartOfWeek = (d: Date) => { return startOfDay(date); }; +const shiftDate = (d: Date, days: number) => + new Date(d.getTime() + days * 86400000); + // ---------------- LATEST ---------------- export async function fetchLatestTransactions( type: "expense" | "income" @@ -106,13 +109,20 @@ export async function fetchAggregatedData( const normalize = (amt: number) => Math.abs(amt); // ---------------- DAILY ---------------- - const dailyBuckets: Record = { - Mon: 0, Tue: 0, Wed: 0, Thu: 0, - Fri: 0, Sat: 0, Sun: 0 + const dailyBuckets: 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); // ---------------- WEEKLY ---------------- const weeklyRolling: any[] = []; @@ -120,7 +130,7 @@ export async function fetchAggregatedData( const currentWeekStart = getStartOfWeek(now); - // rolling (last 5 weeks, oldest → newest) + // rolling (last 5 weeks) 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)); @@ -129,11 +139,14 @@ export async function fetchAggregatedData( label: `${format(start)} - ${format(end)}`, start, end, - amount: 0 + amount: 0, + compare: 0, + prevStart: shiftDate(start, -7), + prevEnd: shiftDate(end, -7) }); } - // calendar (full weeks covering current month) + // calendar weeks const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); @@ -151,7 +164,10 @@ export async function fetchAggregatedData( label: `${format(start)} - ${format(end)}`, start, end, - amount: 0 + amount: 0, + compare: 0, + prevStart: shiftDate(start, -7), + prevEnd: shiftDate(end, -7) }); } @@ -159,7 +175,7 @@ export async function fetchAggregatedData( const monthlyRolling: any[] = []; const monthlyCalendar: any[] = []; - // rolling (last 12 months, oldest → newest) + // rolling (last 12 months) for (let i = 11; i >= 0; i--) { const d = new Date(now); d.setMonth(d.getMonth() - i); @@ -170,24 +186,44 @@ export async function fetchAggregatedData( ? endOfDay(now) : endOfDay(new Date(d.getFullYear(), d.getMonth() + 1, 0)); + const prevStart = new Date(start); + prevStart.setMonth(prevStart.getMonth() - 1); + const prevEnd = new Date(end); + prevEnd.setMonth(prevEnd.getMonth() - 1); + monthlyRolling.push({ - label: `${d.toLocaleString("default", { month: "short" })}-${String(d.getFullYear()).slice(2)}`, + label: `${d.toLocaleString("default", { month: "short" })}-${String( + d.getFullYear() + ).slice(2)}`, start, end, - amount: 0 + amount: 0, + compare: 0, + prevStart, + prevEnd }); } - // calendar (full year Jan → Dec) + // calendar (Jan–Dec) 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); + monthlyCalendar.push({ - label: `${start.toLocaleString("default", { month: "short" })}-${String(start.getFullYear()).slice(2)}`, + label: `${start.toLocaleString("default", { month: "short" })}-${String( + start.getFullYear() + ).slice(2)}`, start, end, - amount: 0 + amount: 0, + compare: 0, + prevStart, + prevEnd }); } @@ -210,38 +246,51 @@ export async function fetchAggregatedData( // DAILY if (d >= weekStart && d <= weekEnd) { const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()]; - if (dailyBuckets[day] !== undefined) { - dailyBuckets[day] += amt; + if (dailyBuckets[day]) { + dailyBuckets[day].amount += amt; + } + } + + if (d >= prevWeekStart && d <= prevWeekEnd) { + const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()]; + if (dailyBuckets[day]) { + dailyBuckets[day].compare += amt; } } // WEEKLY for (const b of weeklyRolling) { if (d >= b.start && d <= b.end) b.amount += amt; + if (d >= b.prevStart && d <= b.prevEnd) b.compare += amt; } for (const b of weeklyCalendar) { if (d >= b.start && d <= b.end) b.amount += amt; + if (d >= b.prevStart && d <= b.prevEnd) b.compare += amt; } // MONTHLY for (const b of monthlyRolling) { if (d >= b.start && d <= b.end) b.amount += amt; + if (d >= b.prevStart && d <= b.prevEnd) b.compare += amt; } for (const b of monthlyCalendar) { if (d >= b.start && d <= b.end) b.amount += amt; + if (d >= b.prevStart && d <= b.prevEnd) b.compare += amt; } } const toPoints = (arr: any[]): ChartDataPoint[] => arr.map((x) => ({ id: x.label, - amount: x.amount + amount: x.amount, + compareAmount: x.compare })); const chartData: ChartData = { - daily: Object.entries(dailyBuckets).map(([k, v]) => ({ + daily: Object.entries(dailyBuckets).map(([k, v]: any) => ({ id: k, - amount: v + amount: v.amount, + compareAmount: v.compare })), weekly: { rolling: toPoints(weeklyRolling), @@ -253,7 +302,7 @@ export async function fetchAggregatedData( } }; - // highlight max (per visible set default to rolling) + // highlight max (current only) Object.values(chartData).forEach((group: any) => { const arr = Array.isArray(group) ? group : group.rolling; if (!arr?.length) return;