From fc88703a3803edbdf44112f8e717fdc1333afd66 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Mon, 6 Apr 2026 18:27:05 +0530 Subject: [PATCH] rolling and calender toggle --- src/Dashboard.tsx | 6 +- src/components/HistoryChart.tsx | 9 +- src/utils/dashboardLoader.ts | 226 +++++++++++++++++--------------- 3 files changed, 133 insertions(+), 108 deletions(-) diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index e69d272..1fdbec0 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -56,8 +56,8 @@ export default function Dashboard() { ] = await Promise.all([ fetchLatestTransactions("expense"), fetchLatestTransactions("income"), - fetchAggregatedExpenses(period), - fetchAggregatedIncome(period) + fetchAggregatedExpenses(), + fetchAggregatedIncome() ]); setLatest({ @@ -79,7 +79,7 @@ export default function Dashboard() { } loadData(); - }, [period]); + }, []); const currentData = aggregated[mode]; const currentLatest = latest[mode]; diff --git a/src/components/HistoryChart.tsx b/src/components/HistoryChart.tsx index 0d5c65b..395592f 100644 --- a/src/components/HistoryChart.tsx +++ b/src/components/HistoryChart.tsx @@ -4,6 +4,7 @@ import { Box, Typography, ToggleButtonGroup, ToggleButton, Paper } from "@mui/ma export interface ChartDataPoint { id: string; amount: number; + compareAmount?: number; count?: number; highlighted?: boolean; } @@ -34,7 +35,13 @@ export default function HistoryChart({ }; const activeDataKey = activeTab.toLowerCase(); - const rawData = data[activeDataKey] || data[activeTab] || []; + let rawData; + if (activeDataKey === "daily") { + rawData = data.daily; + } else { + // @ts-ignore + rawData = data[activeDataKey]?.[period] || []; + } const currentData = [...rawData].reverse(); const maxAmount = diff --git a/src/utils/dashboardLoader.ts b/src/utils/dashboardLoader.ts index c99f6e1..78a1864 100644 --- a/src/utils/dashboardLoader.ts +++ b/src/utils/dashboardLoader.ts @@ -71,17 +71,26 @@ export async function fetchLatestTransactions( } // ---------------- TYPES ---------------- +export interface ChartSeries { + rolling: ChartDataPoint[]; + calendar: ChartDataPoint[]; +} + +export interface ChartData { + daily: ChartDataPoint[]; + weekly: ChartSeries; + monthly: ChartSeries; +} + export interface AggregatedDashboardData { - chartData: Record; + chartData: ChartData; totalAmount: number; topPayees: Array<{ payeeName: string; amount: number }>; } -// ---------------- AGGREGATION ---------------- // ---------------- AGGREGATION ---------------- export async function fetchAggregatedData( - type: "expense" | "income", - mode: "rolling" | "calendar" = "rolling" + type: "expense" | "income" ): Promise { const res = await api.get("/expenses", { params: { limit: 0 } }); const all: any[] = res.data?.items || res.data || []; @@ -96,7 +105,7 @@ export async function fetchAggregatedData( const normalize = (amt: number) => Math.abs(amt); - // ---------------- WEEK ---------------- + // ---------------- DAILY ---------------- const dailyBuckets: Record = { Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0, Sat: 0, Sun: 0 @@ -105,77 +114,81 @@ export async function fetchAggregatedData( const weekStart = getStartOfWeek(now); const weekEnd = endOfDay(new Date(weekStart.getTime() + 6 * 86400000)); - // ---------------- MONTH (rolling 5 weeks, Mon–Sun aligned) ---------------- - const weeklyBuckets = []; + // ---------------- WEEKLY ---------------- + const weeklyRolling: any[] = []; + const weeklyCalendar: any[] = []; - if (mode === "rolling") { - const currentWeekStart = getStartOfWeek(now); + const currentWeekStart = getStartOfWeek(now); - for (let i = 0; i < 5; i++) { - const start = new Date(currentWeekStart.getTime() - i * 7 * 86400000); - const end = endOfDay(new Date(start.getTime() + 6 * 86400000)); + // rolling (last 5 weeks, oldest → newest) + 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)); - weeklyBuckets.push({ - label: `${format(start)} - ${format(end)}`, - start, - end, - amount: 0 - }); - } - } else { - // calendar weeks within current month - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - let cursor = getStartOfWeek(startOfMonth); - - while (cursor <= now) { - const start = new Date(cursor); - const end = endOfDay(new Date(start.getTime() + 6 * 86400000)); - - weeklyBuckets.push({ - label: `${format(start)} - ${format(end)}`, - start, - end, - amount: 0 - }); - - cursor = new Date(cursor.getTime() + 7 * 86400000); - } + weeklyRolling.push({ + label: `${format(start)} - ${format(end)}`, + start, + end, + amount: 0 + }); } - // ---------------- YEAR (rolling 12 months) ---------------- - const monthlyBuckets = []; + // calendar (full weeks covering current month) + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); - if (mode === "rolling") { - for (let i = 0; i < 12; i++) { - const d = new Date(now); - d.setMonth(d.getMonth() - i); + const firstWeekStart = getStartOfWeek(startOfMonth); - const start = new Date(d.getFullYear(), d.getMonth(), 1); + const totalWeeks = Math.ceil( + (endOfMonth.getTime() - firstWeekStart.getTime()) / (7 * 86400000) + ) + 1; - const end = - i === 0 - ? endOfDay(now) // current month → till now - : endOfDay(new Date(d.getFullYear(), d.getMonth() + 1, 0)); - monthlyBuckets.push({ - label: `${d.toLocaleString("default", { month: "short" })}-${String(d.getFullYear()).slice(2)}`, - start, - end, - amount: 0 - }); - } - } else { - // calendar year (Jan → current month) - for (let i = 0; i <= now.getMonth(); i++) { - const start = new Date(now.getFullYear(), i, 1); - const end = endOfDay(new Date(now.getFullYear(), i + 1, 0)); + 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)); - monthlyBuckets.push({ - label: `${start.toLocaleString("default", { month: "short" })}-${String(start.getFullYear()).slice(2)}`, - start, - end, - amount: 0 - }); - } + weeklyCalendar.push({ + label: `${format(start)} - ${format(end)}`, + start, + end, + amount: 0 + }); + } + + // ---------------- MONTHLY ---------------- + const monthlyRolling: any[] = []; + const monthlyCalendar: any[] = []; + + // rolling (last 12 months, oldest → newest) + 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)); + + monthlyRolling.push({ + label: `${d.toLocaleString("default", { month: "short" })}-${String(d.getFullYear()).slice(2)}`, + start, + end, + amount: 0 + }); + } + + // calendar (full year 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)); + + monthlyCalendar.push({ + label: `${start.toLocaleString("default", { month: "short" })}-${String(start.getFullYear()).slice(2)}`, + start, + end, + amount: 0 + }); } // ---------------- LOOP ---------------- @@ -194,7 +207,7 @@ export async function fetchAggregatedData( const payee = item.payee?.name || item.payee || "Unknown"; payeeMap[payee] = (payeeMap[payee] || 0) + amt; - // WEEK + // DAILY if (d >= weekStart && d <= weekEnd) { const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()]; if (dailyBuckets[day] !== undefined) { @@ -202,42 +215,51 @@ export async function fetchAggregatedData( } } - // MONTH (rolling weeks) - for (const b of weeklyBuckets) { - if (d >= b.start && d <= b.end) { - b.amount += amt; - } + // WEEKLY + for (const b of weeklyRolling) { + if (d >= b.start && d <= b.end) b.amount += amt; + } + for (const b of weeklyCalendar) { + if (d >= b.start && d <= b.end) b.amount += amt; } - // YEAR (rolling months) - for (const b of monthlyBuckets) { - if (d >= b.start && d <= b.end) { - b.amount += amt; - } + // MONTHLY + for (const b of monthlyRolling) { + if (d >= b.start && d <= b.end) b.amount += amt; + } + for (const b of monthlyCalendar) { + if (d >= b.start && d <= b.end) b.amount += amt; } } - const toPoints = (b: any): ChartDataPoint[] => - Array.isArray(b) - ? b.map((x) => ({ - id: x.label, - amount: x.amount - })) - : Object.entries(b).map(([k, v]: any) => ({ - id: k, - amount: v - })); + const toPoints = (arr: any[]): ChartDataPoint[] => + arr.map((x) => ({ + id: x.label, + amount: x.amount + })); - const chartData = { - daily: toPoints(dailyBuckets), - weekly: toPoints(weeklyBuckets), - monthly: toPoints(monthlyBuckets) + const chartData: ChartData = { + daily: Object.entries(dailyBuckets).map(([k, v]) => ({ + id: k, + amount: v + })), + weekly: { + rolling: toPoints(weeklyRolling), + calendar: toPoints(weeklyCalendar) + }, + monthly: { + rolling: toPoints(monthlyRolling), + calendar: toPoints(monthlyCalendar) + } }; - // highlight max - Object.values(chartData).forEach(group => { - let max = group[0]; - for (const g of group) { + // highlight max (per visible set default to rolling) + 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; @@ -252,12 +274,8 @@ export async function fetchAggregatedData( } // ---------------- EXPORTS ---------------- -export const fetchAggregatedExpenses = ( - mode: "rolling" | "calendar" -) => - fetchAggregatedData("expense", mode); +export const fetchAggregatedExpenses = () => + fetchAggregatedData("expense"); -export const fetchAggregatedIncome = ( - mode: "rolling" | "calendar" -) => - fetchAggregatedData("income", mode); \ No newline at end of file +export const fetchAggregatedIncome = () => + fetchAggregatedData("income");