From 214c0be44e25ed7c3b0d3c5c375cda5b9fc8a3e9 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sat, 4 Apr 2026 22:08:39 +0530 Subject: [PATCH] dashboard fixes --- src/Dashboard.tsx | 108 ++++++++------ src/utils/dashboardLoader.ts | 279 ++++++++++++++++++++++------------- 2 files changed, 232 insertions(+), 155 deletions(-) diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 2ead0a9..adcc508 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -1,26 +1,53 @@ import * as React from "react"; -import { Box, Container, Grid, Typography, Avatar, CircularProgress, Alert } from "@mui/material"; +import { + Box, + Container, + Grid, + Typography, + CircularProgress, + Alert, + ToggleButton, + ToggleButtonGroup +} from "@mui/material"; + import LatestItemsList, { LatestItem } from "./components/LatestItemsList"; import ProgressCard from "./components/ProgressCard"; import HistoryChart from "./components/HistoryChart"; -import { fetchLatestExpenses, fetchAggregatedExpenses, AggregatedDashboardData } from "./utils/dashboardLoader"; + +import { + fetchLatestExpenses, + fetchAggregatedExpenses, + fetchAggregatedIncome, + AggregatedDashboardData +} from "./utils/dashboardLoader"; export default function Dashboard() { const [latest, setLatest] = React.useState([]); - const [aggregated, setAggregated] = React.useState(null); + const [aggregated, setAggregated] = + React.useState(null); + + const [mode, setMode] = + React.useState<"expense" | "income">("expense"); + const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); + // -------- LOAD DATA -------- React.useEffect(() => { async function loadData() { try { setLoading(true); - const [latestData, aggData] = await Promise.all([ - fetchLatestExpenses(), - fetchAggregatedExpenses() - ]); + + const latestData = await fetchLatestExpenses(); + + const aggData = + mode === "expense" + ? await fetchAggregatedExpenses() + : await fetchAggregatedIncome(); + setLatest(latestData); setAggregated(aggData); + } catch (err: any) { console.error(err); setError(err.message || "Failed to load dashboard data"); @@ -28,12 +55,14 @@ export default function Dashboard() { setLoading(false); } } - loadData(); - }, []); + loadData(); + }, [mode]); + + // -------- UI STATES -------- if (loading) { return ( - + ); @@ -51,57 +80,38 @@ export default function Dashboard() { return ( + {/* -------- TOGGLE -------- */} + + val && setMode(val)} + > + Expenses + Income + + + - {/* Column 1: Latest Transactions */} + {/* Column 1 */} - {}} + {}} /> - {/* Column 2: Breakdown Graph */} - - - Spacer - + {/* Column 2 */} + - {/* Column 3: Top Payees Progress */} - - - Top Analytics - - - - - - - Top 5 Payees - {aggregated?.topPayees.map((payee, idx) => ( - - - - ))} - ); diff --git a/src/utils/dashboardLoader.ts b/src/utils/dashboardLoader.ts index 13c3890..af67fb6 100644 --- a/src/utils/dashboardLoader.ts +++ b/src/utils/dashboardLoader.ts @@ -4,151 +4,218 @@ import { ChartDataPoint } from "../components/HistoryChart"; import * as React from "react"; import MonetizationOnIcon from "@mui/icons-material/MonetizationOn"; -// Helper generic icon -const DEFAULT_ICON = React.createElement(MonetizationOnIcon, { sx: { color: "#388e3c" } }); +// ---------------- ICON ---------------- +const DEFAULT_ICON = React.createElement(MonetizationOnIcon, { + sx: { color: "#388e3c" } +}); +// ---------------- LATEST ---------------- export async function fetchLatestExpenses(): Promise { - const res = await api.get('/expenses', { params: { limit: 10, sort: '-occurred_at' } }); + const res = await api.get('/expenses', { + params: { limit: 10, sort: '-occurred_at' } + }); + const items = res.data?.items || res.data || []; return items.map((exp: any, index: number) => { - const time = new Date(exp.occurred_at || exp.created_at || Date.now()).getTime(); - const diffTime = Math.abs(Date.now() - time); - const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - const timeText = diffDays === 0 ? "Today" : `${diffDays} days ago`; + const time = new Date( + exp.occurred_at || exp.created_at || Date.now() + ).getTime(); + + const diffDays = Math.floor( + Math.abs(Date.now() - time) / (1000 * 60 * 60 * 24) + ); return { id: exp.id || index, icon: DEFAULT_ICON, - iconBgColor: "#e8f5e9", // soft green + iconBgColor: "#e8f5e9", title: exp.payee?.name || exp.payee || "Unknown Payee", - subtitle: exp.category?.name || exp.account?.name || "Expense", + subtitle: exp.category?.name || exp.account?.name || "Transaction", amount: `Rs ${exp.amount || 0}`, - timeAgo: timeText, + timeAgo: diffDays === 0 ? "Today" : `${diffDays} days ago` }; }); } +// ---------------- TYPES ---------------- export interface AggregatedDashboardData { chartData: Record; totalAmount: number; topPayees: Array<{ payeeName: string; amount: number }>; } -export async function fetchAggregatedExpenses(): Promise { +// ---------------- GENERIC AGGREGATOR ---------------- +export async function fetchAggregatedData( + type: "expense" | "income" +): Promise { const res = await api.get('/expenses', { params: { limit: 0 } }); - const allExpenses: any[] = res.data?.items || res.data || []; + const all: any[] = res.data?.items || res.data || []; + + const now = new Date(); - let maxTime = 0; let totalAmount = 0; const payeeMap: Record = {}; - // 1. Gather Total Amount, Max Time, and Payee accumulations - for (const exp of allExpenses) { + const isValid = (amt: number) => + type === "expense" ? amt < 0 : amt > 0; - const amt = Number(exp.amount) || 0; - if (amt >= 0) continue; // skip income / non-expense + const normalize = (amt: number) => Math.abs(amt); + + // ---------------- BUCKETS ---------------- + const todayBuckets: Record = { + "12am":0,"3am":0,"6am":0,"9am":0, + "12pm":0,"3pm":0,"6pm":0,"9pm":0 + }; + + const weekBuckets: Record = { + "Mon":0,"Tue":0,"Wed":0,"Thu":0, + "Fri":0,"Sat":0,"Sun":0 + }; + + const monthBuckets: Record = {}; + const yearBuckets: Record = {}; + + const getStartOfWeek = (d: Date) => { + const date = new Date(d); + const day = date.getDay() || 7; + if (day !== 1) date.setDate(date.getDate() - (day - 1)); + date.setHours(0,0,0,0); + return date; + }; + + const format = (d: Date) => + `${d.getDate()} ${d.toLocaleString('default', { month: 'short' })}`; + + // -------- MONTH (5 rolling weeks) -------- + for (let i = 0; i < 5; i++) { + const end = new Date(now); + end.setDate(end.getDate() - i * 7); + + const start = new Date(end); + start.setDate(start.getDate() - 6); + + const key = `${format(start)} - ${format(end)}`; + + monthBuckets[key] = { amount: 0, range: key }; + } + + // -------- YEAR (12 months rolling) -------- + for (let i = 0; i < 12; i++) { + const d = new Date(now); + d.setMonth(d.getMonth() - i); + + const start = new Date(d.getFullYear(), d.getMonth(), 1); + const end = new Date(d.getFullYear(), d.getMonth() + 1, 0); + + const label = d.toLocaleString('default', { month: 'short' }); + const range = `${format(start)} - ${format(end)}`; + + yearBuckets[label] = { amount: 0, range }; + } + + const weekStart = getStartOfWeek(now); + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekStart.getDate() + 6); + + // ---------------- LOOP ---------------- + 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 payeeName = exp.payee?.name || exp.payee || "Unknown"; - payeeMap[payeeName] = (payeeMap[payeeName] || 0) + amt; + const payee = item.payee?.name || item.payee || "Unknown"; + payeeMap[payee] = (payeeMap[payee] || 0) + amt; - const time = new Date(exp.occurred_at || exp.created_at || Date.now()).getTime(); - if (time > maxTime) maxTime = time; + // ---- TODAY + if ( + d.getDate() === now.getDate() && + d.getMonth() === now.getMonth() && + d.getFullYear() === now.getFullYear() + ) { + const hr = d.getHours(); + let label = "12am"; + if (hr >= 3 && hr < 6) label = "3am"; + else if (hr >= 6 && hr < 9) label = "6am"; + else if (hr >= 9 && hr < 12) label = "9am"; + else if (hr >= 12 && hr < 15) label = "12pm"; + else if (hr >= 15 && hr < 18) label = "3pm"; + else if (hr >= 18 && hr < 21) label = "6pm"; + else if (hr >= 21) label = "9pm"; + + todayBuckets[label] += amt; + } + + // ---- WEEK + if (d >= weekStart && d <= weekEnd) { + const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()]; + if (weekBuckets[day] !== undefined) { + weekBuckets[day] += amt; + } + } + + // ---- MONTH + Object.entries(monthBuckets).forEach(([_, obj]) => { + const [startStr, endStr] = obj.range.split(" - "); + const start = new Date(startStr); + const end = new Date(endStr); + + if (d >= start && d <= end) { + obj.amount += amt; + } + }); + + // ---- YEAR + Object.entries(yearBuckets).forEach(([_, obj]) => { + const [startStr, endStr] = obj.range.split(" - "); + const start = new Date(startStr); + const end = new Date(endStr); + + if (d >= start && d <= end) { + obj.amount += amt; + } + }); } - if (maxTime === 0) maxTime = Date.now(); - const baseDate = new Date(maxTime); + const toPoints = (b: any): ChartDataPoint[] => + Object.entries(b).map(([k, v]: any) => ({ + id: k, + amount: typeof v === "number" ? v : v.amount, + subLabel: typeof v === "number" ? undefined : v.range + })); - // 2. Chart Groups - const chartData: Record = { - today: [], - week: [], - month: [], - year: [] + const chartData = { + today: toPoints(todayBuckets), + week: toPoints(weekBuckets), + month: toPoints(monthBuckets), + year: toPoints(yearBuckets) }; - const todayBuckets: Record = { "12am":0, "3am":0, "6am":0, "9am":0, "12pm":0, "3pm":0, "6pm":0, "9pm":0 }; - const weekBuckets: Record = { "Mon":0, "Tue":0, "Wed":0, "Thu":0, "Fri":0, "Sat":0, "Sun":0 }; - const monthBuckets: Record = { "Week 1":0, "Week 2":0, "Week 3":0, "Week 4":0, "Week 5":0 }; - const yearBuckets: Record = { "Jan":0, "Feb":0, "Mar":0, "Apr":0, "May":0, "Jun":0, "Jul":0, "Aug":0, "Sep":0, "Oct":0, "Nov":0, "Dec":0 }; + // highlight max + Object.values(chartData).forEach(group => { + let max = group[0]; + for (const g of group) if (g.amount > max.amount) max = g; + if (max.amount > 0) max.highlighted = true; + }); - const getDayName = (dayIdx: number) => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][dayIdx]; - const getMonthName = (monthIdx: number) => ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][monthIdx]; - - for (const exp of allExpenses) { - const d = new Date(exp.occurred_at || exp.created_at || Date.now()); - const amt = Number(exp.amount) || 0; - - if (amt >= 0) continue; // skip income / non-expense - - // Check Today - if (d.getFullYear() === baseDate.getFullYear() && d.getMonth() === baseDate.getMonth() && d.getDate() === baseDate.getDate()) { - const hr = d.getHours(); - let label = "12am"; - if (hr >= 3 && hr < 6) label = "3am"; - else if (hr >= 6 && hr < 9) label = "6am"; - else if (hr >= 9 && hr < 12) label = "9am"; - else if (hr >= 12 && hr < 15) label = "12pm"; - else if (hr >= 15 && hr < 18) label = "3pm"; - else if (hr >= 18 && hr < 21) label = "6pm"; - else if (hr >= 21) label = "9pm"; - todayBuckets[label] += amt; - } - - // Check Week - const diffDays = (baseDate.getTime() - d.getTime()) / (1000 * 3600 * 24); - if (diffDays >= 0 && diffDays < 7) { - weekBuckets[getDayName(d.getDay())] += amt; - } - - // Check Month - if (d.getFullYear() === baseDate.getFullYear() && d.getMonth() === baseDate.getMonth()) { - const dNum = d.getDate(); - let wLabel = "Week 1"; - if (dNum > 7 && dNum <= 14) wLabel = "Week 2"; - else if (dNum > 14 && dNum <= 21) wLabel = "Week 3"; - else if (dNum > 21 && dNum <= 28) wLabel = "Week 4"; - else if (dNum > 28) wLabel = "Week 5"; - monthBuckets[wLabel] += amt; - } - - // Check Year - if (d.getFullYear() === baseDate.getFullYear()) { - yearBuckets[getMonthName(d.getMonth())] += amt; - } - } - - const convertBucket = (b: Record): ChartDataPoint[] => { - return Object.keys(b).map(k => ({ id: k, amount: b[k] })); - }; - - chartData.today = convertBucket(todayBuckets); - chartData.week = convertBucket(weekBuckets); - chartData.month = convertBucket(monthBuckets); - chartData.year = convertBucket(yearBuckets); - - for (const group of Object.values(chartData)) { - if(group.length === 0) continue; - let maxObj = group[0]; - for (const p of group) { - if (p.amount > maxObj.amount) maxObj = p; - } - if (maxObj.amount > 0) { - maxObj.highlighted = true; - } - } - - // 3. Top Payees const topPayees = Object.entries(payeeMap) .map(([name, amt]) => ({ payeeName: name, amount: amt })) .sort((a, b) => b.amount - a.amount) - .slice(0, 3); + .slice(0, 5); - return { - chartData, - totalAmount, - topPayees - }; + return { chartData, totalAmount, topPayees }; } + +// ---------------- EXPORTS ---------------- +export const fetchAggregatedExpenses = () => + fetchAggregatedData("expense"); + +export const fetchAggregatedIncome = () => + fetchAggregatedData("income"); \ No newline at end of file