dashboard loader
This commit is contained in:
@@ -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: <ShoppingBagIcon sx={{ color: "#388e3c" }} />,
|
||||
iconBgColor: "#e8f5e9",
|
||||
title: "Grocery Shopping",
|
||||
subtitle: "Buy some grocery",
|
||||
amount: "Rs 3000",
|
||||
timeAgo: "3 days ago",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: <SubscriptionsIcon sx={{ color: "#7b1fa2" }} />,
|
||||
iconBgColor: "#f3e5f5",
|
||||
title: "Subscription",
|
||||
subtitle: "Netflix monthly",
|
||||
amount: "Rs 800",
|
||||
timeAgo: "5 days ago",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: <RestaurantIcon sx={{ color: "#d32f2f" }} />,
|
||||
iconBgColor: "#ffebee",
|
||||
title: "Food",
|
||||
subtitle: "Buy a chinese noodles",
|
||||
amount: "Rs 1000",
|
||||
timeAgo: "6 days ago",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: <FoodBankIcon sx={{ color: "#fbc02d" }} />,
|
||||
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() {
|
||||
const [latest, setLatest] = React.useState<LatestItem[]>([]);
|
||||
const [aggregated, setAggregated] = React.useState<AggregatedDashboardData | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
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 (
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '60vh' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Container sx={{ mt: 4 }}>
|
||||
<Alert severity="error">{error}</Alert>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const themes = ["primary", "secondary", "info", "success", "warning"] as const;
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ mt: 4, mb: 4 }}>
|
||||
<Grid container spacing={4}>
|
||||
{/* Left Column */}
|
||||
<Grid item xs={12} md={6}>
|
||||
{/* User Greeting */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4, px: 2 }}>
|
||||
<Avatar sx={{ width: 56, height: 56, mr: 2 }} src="https://i.pravatar.cc/150?img=5" />
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight={700}>Hello Ananya</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary">Good Morning</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ProgressCard
|
||||
header="Account Balance"
|
||||
summary="Rs 2,700,00"
|
||||
progressAmount={270000}
|
||||
totalAmount={270000}
|
||||
colorTheme="info"
|
||||
/>
|
||||
|
||||
<Box mt={4}>
|
||||
{/* Column 1: Latest Transactions */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<LatestItemsList
|
||||
title="Recent Transactions"
|
||||
items={mockLatestItems}
|
||||
items={latest}
|
||||
onViewAll={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Right Column */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography variant="h6" fontWeight={700}>Statistics</Typography>
|
||||
{/* Column 2: Breakdown Graph */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'center', visibility: 'hidden' }}>
|
||||
<Typography variant="h6">Spacer</Typography>
|
||||
</Box>
|
||||
|
||||
<ProgressCard
|
||||
header="Total Expense"
|
||||
progressAmount={27000}
|
||||
totalAmount={30000}
|
||||
summary="Rs 27000 / Rs 30000 per month"
|
||||
colorTheme="info"
|
||||
/>
|
||||
|
||||
<Box mt={4}>
|
||||
<HistoryChart
|
||||
header="Expense Breakdown"
|
||||
summary="Limit each day Rs 5000/-"
|
||||
summary="Interactive chronological tracking"
|
||||
tabs={["Today", "Week", "Month", "Year"]}
|
||||
data={mockChartData}
|
||||
data={aggregated?.chartData || {}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Column 3: Top Payees Progress */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography variant="h6" fontWeight={700}>Top Analytics</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<ProgressCard
|
||||
header="Total Expenses"
|
||||
progressAmount={aggregated?.totalAmount || 0}
|
||||
totalAmount={aggregated?.totalAmount || 0}
|
||||
summary={`Rs ${aggregated?.totalAmount || 0}`}
|
||||
colorTheme="error" // Highlight total in red/error or use info
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" fontWeight={600} mb={2} px={1}>Top 5 Payees</Typography>
|
||||
{aggregated?.topPayees.map((payee, idx) => (
|
||||
<Box sx={{ mb: 2 }} key={payee.payeeName}>
|
||||
<ProgressCard
|
||||
header={payee.payeeName}
|
||||
progressAmount={payee.amount}
|
||||
totalAmount={aggregated?.totalAmount || 1}
|
||||
colorTheme={themes[idx % themes.length]}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
|
||||
154
src/utils/dashboardLoader.ts
Normal file
154
src/utils/dashboardLoader.ts
Normal file
@@ -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<LatestItem[]> {
|
||||
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<string, ChartDataPoint[]>;
|
||||
totalAmount: number;
|
||||
topPayees: Array<{ payeeName: string; amount: number }>;
|
||||
}
|
||||
|
||||
export async function fetchAggregatedExpenses(): Promise<AggregatedDashboardData> {
|
||||
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<string, number> = {};
|
||||
|
||||
// 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<string, ChartDataPoint[]> = {
|
||||
today: [],
|
||||
week: [],
|
||||
month: [],
|
||||
year: []
|
||||
};
|
||||
|
||||
const todayBuckets: Record<string, number> = { "12am":0, "3am":0, "6am":0, "9am":0, "12pm":0, "3pm":0, "6pm":0, "9pm":0 };
|
||||
const weekBuckets: Record<string, number> = { "Mon":0, "Tue":0, "Wed":0, "Thu":0, "Fri":0, "Sat":0, "Sun":0 };
|
||||
const monthBuckets: Record<string, number> = { "Week 1":0, "Week 2":0, "Week 3":0, "Week 4":0, "Week 5":0 };
|
||||
const yearBuckets: Record<string, number> = { "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<string, number>): 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user