diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index c32e87f..2ead0a9 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -1,138 +1,106 @@ import * as React from "react"; -import { Box, Container, Grid, Typography, Avatar } from "@mui/material"; +import { Box, Container, Grid, Typography, Avatar, CircularProgress, Alert } from "@mui/material"; import LatestItemsList, { LatestItem } from "./components/LatestItemsList"; import ProgressCard from "./components/ProgressCard"; import HistoryChart from "./components/HistoryChart"; -import ShoppingBagIcon from "@mui/icons-material/ShoppingBag"; -import SubscriptionsIcon from "@mui/icons-material/Subscriptions"; -import RestaurantIcon from "@mui/icons-material/Restaurant"; -import FoodBankIcon from "@mui/icons-material/FoodBank"; - -const mockLatestItems: LatestItem[] = [ - { - id: 1, - icon: , - iconBgColor: "#e8f5e9", - title: "Grocery Shopping", - subtitle: "Buy some grocery", - amount: "Rs 3000", - timeAgo: "3 days ago", - }, - { - id: 2, - icon: , - iconBgColor: "#f3e5f5", - title: "Subscription", - subtitle: "Netflix monthly", - amount: "Rs 800", - timeAgo: "5 days ago", - }, - { - id: 3, - icon: , - iconBgColor: "#ffebee", - title: "Food", - subtitle: "Buy a chinese noodles", - amount: "Rs 1000", - timeAgo: "6 days ago", - }, - { - id: 4, - icon: , - iconBgColor: "#fff8e1", - title: "Food Club", - subtitle: "Buy a chinese noodles", - amount: "Rs 1000", - timeAgo: "6 days ago", - }, -]; - -const mockChartData = { - today: [ - { id: "6am", amount: 100 }, - { id: "9am", amount: 500 }, - { id: "12pm", amount: 200 }, - { id: "3pm", amount: 1000, highlighted: true }, - { id: "6pm", amount: 600 }, - { id: "9pm", amount: 300 }, - ], - week: [ - { id: "Mon", amount: 1500 }, - { id: "Tue", amount: 1000 }, - { id: "Wed", amount: 2000 }, - { id: "Thu", amount: 500, highlighted: true }, - { id: "Fri", amount: 3000 }, - { id: "Sat", amount: 4500 }, - { id: "Sun", amount: 2000 }, - ], - month: [ - { id: "Week 1", amount: 10000 }, - { id: "Week 2", amount: 5000 }, - { id: "Week 3", amount: 12000, highlighted: true }, - { id: "Week 4", amount: 8000 }, - ], - year: [ - { id: "Q1", amount: 50000 }, - { id: "Q2", amount: 45000 }, - { id: "Q3", amount: 60000, highlighted: true }, - { id: "Q4", amount: 48000 }, - ], -}; +import { fetchLatestExpenses, fetchAggregatedExpenses, AggregatedDashboardData } from "./utils/dashboardLoader"; export default function Dashboard() { - return ( - - - {/* Left Column */} - - {/* User Greeting */} - - - - Hello Ananya - Good Morning - - + const [latest, setLatest] = React.useState([]); + const [aggregated, setAggregated] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); - { + async function loadData() { + try { + setLoading(true); + const [latestData, aggData] = await Promise.all([ + fetchLatestExpenses(), + fetchAggregatedExpenses() + ]); + setLatest(latestData); + setAggregated(aggData); + } catch (err: any) { + console.error(err); + setError(err.message || "Failed to load dashboard data"); + } finally { + setLoading(false); + } + } + loadData(); + }, []); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + const themes = ["primary", "secondary", "info", "success", "warning"] as const; + + return ( + + + {/* Column 1: Latest Transactions */} + + {}} /> - - - {}} - /> - - {/* Right Column */} - - - Statistics - - - + + Spacer + + + + + {/* 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 new file mode 100644 index 0000000..13c3890 --- /dev/null +++ b/src/utils/dashboardLoader.ts @@ -0,0 +1,154 @@ +import { api } from "../../react-openapi"; +import { LatestItem } from "../components/LatestItemsList"; +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" } }); + +export async function fetchLatestExpenses(): Promise { + 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`; + + return { + id: exp.id || index, + icon: DEFAULT_ICON, + iconBgColor: "#e8f5e9", // soft green + title: exp.payee?.name || exp.payee || "Unknown Payee", + subtitle: exp.category?.name || exp.account?.name || "Expense", + amount: `Rs ${exp.amount || 0}`, + timeAgo: timeText, + }; + }); +} + +export interface AggregatedDashboardData { + chartData: Record; + totalAmount: number; + topPayees: Array<{ payeeName: string; amount: number }>; +} + +export async function fetchAggregatedExpenses(): Promise { + const res = await api.get('/expenses', { params: { limit: 0 } }); + const allExpenses: any[] = res.data?.items || res.data || []; + + let maxTime = 0; + let totalAmount = 0; + const payeeMap: Record = {}; + + // 1. Gather Total Amount, Max Time, and Payee accumulations + for (const exp of allExpenses) { + + const amt = Number(exp.amount) || 0; + if (amt >= 0) continue; // skip income / non-expense + + totalAmount += amt; + + const payeeName = exp.payee?.name || exp.payee || "Unknown"; + payeeMap[payeeName] = (payeeMap[payeeName] || 0) + amt; + + const time = new Date(exp.occurred_at || exp.created_at || Date.now()).getTime(); + if (time > maxTime) maxTime = time; + } + + if (maxTime === 0) maxTime = Date.now(); + const baseDate = new Date(maxTime); + + // 2. Chart Groups + const chartData: Record = { + today: [], + week: [], + month: [], + year: [] + }; + + 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 }; + + 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); + + return { + chartData, + totalAmount, + topPayees + }; +}