major refactor of the dashboard and react-openapi integration #1
@@ -9,23 +9,16 @@ import {
|
|||||||
ToggleButtonGroup
|
ToggleButtonGroup
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import LatestItemsList, { LatestItem } from "./components/LatestItems";
|
import LatestItems from "./components/LatestItems";
|
||||||
import HistoryChart, { AggregatedDashboardData } from "./components/HistoryChart";
|
import HistoryChart from "./components/HistoryChart";
|
||||||
|
|
||||||
import {
|
import { useDashboardData } from "./features/dashboard";
|
||||||
fetchLatestTransactions,
|
|
||||||
fetchAggregatedExpenses,
|
|
||||||
fetchAggregatedIncome,
|
|
||||||
} from "./utils/dashboardLoader";
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [latest, setLatest] = React.useState<{
|
const [mode, setMode] = React.useState<"expense" | "income">("expense");
|
||||||
expense: LatestItem[];
|
const [period, setPeriod] = React.useState<"rolling" | "calendar">("rolling");
|
||||||
income: LatestItem[];
|
const [comparison, setComparison] = React.useState(false);
|
||||||
}>({
|
|
||||||
expense: [],
|
|
||||||
income: []
|
|
||||||
});
|
|
||||||
const palette = {
|
const palette = {
|
||||||
expense: {
|
expense: {
|
||||||
primary: "#d32f2f",
|
primary: "#d32f2f",
|
||||||
@@ -41,73 +34,11 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [aggregated, setAggregated] = React.useState<{
|
const { data, latest, isLoading, error } = useDashboardData(mode);
|
||||||
expense: AggregatedDashboardData | null;
|
|
||||||
income: AggregatedDashboardData | null;
|
|
||||||
}>({
|
|
||||||
expense: null,
|
|
||||||
income: null
|
|
||||||
});
|
|
||||||
|
|
||||||
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<string | null>(null);
|
|
||||||
|
|
||||||
const colors = palette[mode];
|
const colors = palette[mode];
|
||||||
// -------- LOAD ONCE --------
|
|
||||||
React.useEffect(() => {
|
|
||||||
async function loadData() {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const [
|
if (isLoading) {
|
||||||
latestExpense,
|
|
||||||
latestIncome,
|
|
||||||
expenseData,
|
|
||||||
incomeData
|
|
||||||
] = await Promise.all([
|
|
||||||
fetchLatestTransactions("expense"),
|
|
||||||
fetchLatestTransactions("income"),
|
|
||||||
fetchAggregatedExpenses(),
|
|
||||||
fetchAggregatedIncome()
|
|
||||||
]);
|
|
||||||
|
|
||||||
setLatest({
|
|
||||||
expense: latestExpense,
|
|
||||||
income: latestIncome
|
|
||||||
});
|
|
||||||
|
|
||||||
setAggregated({
|
|
||||||
expense: expenseData,
|
|
||||||
income: incomeData
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err);
|
|
||||||
setError(err.message || "Failed to load dashboard data");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const currentData = aggregated[mode];
|
|
||||||
if (!currentData) {
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}>
|
|
||||||
<CircularProgress />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const currentLatest = latest[mode];
|
|
||||||
|
|
||||||
// -------- UI STATES --------
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}>
|
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
@@ -118,11 +49,15 @@ export default function Dashboard() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Container sx={{ mt: 4 }}>
|
<Container sx={{ mt: 4 }}>
|
||||||
<Alert severity="error">{error}</Alert>
|
<Alert severity="error">{String(error)}</Alert>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
sx={{
|
sx={{
|
||||||
@@ -133,7 +68,6 @@ export default function Dashboard() {
|
|||||||
p: 2
|
p: 2
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* -------- TOGGLE -------- */}
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "center", mb: 3 }}>
|
<Box sx={{ display: "flex", justifyContent: "center", mb: 3 }}>
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
value={mode}
|
value={mode}
|
||||||
@@ -160,13 +94,12 @@ export default function Dashboard() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Grid container spacing={4} direction="row">
|
<Grid container spacing={4} direction="row">
|
||||||
|
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
<HistoryChart
|
<HistoryChart
|
||||||
header={`${mode === "expense" ? "Expense" : "Income"} Breakdown`}
|
header={`${mode === "expense" ? "Expense" : "Income"} Breakdown`}
|
||||||
summary="Interactive chronological tracking"
|
summary="Interactive chronological tracking"
|
||||||
tabs={["Daily", "Weekly", "Monthly"]}
|
tabs={["Daily", "Weekly", "Monthly"]}
|
||||||
data={currentData.chartData}
|
data={data.chartData}
|
||||||
period={period}
|
period={period}
|
||||||
onPeriodChange={setPeriod}
|
onPeriodChange={setPeriod}
|
||||||
comparison={comparison}
|
comparison={comparison}
|
||||||
@@ -176,14 +109,13 @@ export default function Dashboard() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
<LatestItemsList
|
<LatestItems
|
||||||
title={`Recent ${mode === "expense" ? "Expenses" : "Income"}`}
|
title={`Recent ${mode === "expense" ? "Expenses" : "Income"}`}
|
||||||
items={currentLatest}
|
items={latest || []}
|
||||||
onViewAll={() => {}}
|
onViewAll={() => {}}
|
||||||
accentColor={colors.primary}
|
accentColor={colors.primary}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { api } from "../../react-openapi";
|
import { api } from "../../../react-openapi";
|
||||||
import { LatestItem } from "../components/LatestItems";
|
import { LatestItem } from "../../components/LatestItems";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
|
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
|
||||||
|
|
||||||
import { fetchReport } from "../features/report/report.api";
|
import { fetchReport } from "../report/report.api";
|
||||||
import { mapReportToDashboard } from "../features/report/report.mapper";
|
import { mapReportToDashboard } from "../report/report.mapper";
|
||||||
|
|
||||||
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
||||||
sx: { color: "#388e3c" }
|
sx: { color: "#388e3c" }
|
||||||
@@ -17,7 +17,7 @@ export async function fetchLatestTransactions(
|
|||||||
params: { limit: 100, sort: "-occurred_at" }
|
params: { limit: 100, sort: "-occurred_at" }
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = res.data?.items || res.data || [];
|
const items = res.data || [];
|
||||||
|
|
||||||
const isValid = (amt: number) =>
|
const isValid = (amt: number) =>
|
||||||
type === "expense" ? amt < 0 : amt > 0;
|
type === "expense" ? amt < 0 : amt > 0;
|
||||||
3
src/features/dashboard/index.ts
Normal file
3
src/features/dashboard/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export {
|
||||||
|
useDashboardData
|
||||||
|
} from './useDashboardData'
|
||||||
24
src/features/dashboard/useDashboardData.ts
Normal file
24
src/features/dashboard/useDashboardData.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
fetchAggregatedData,
|
||||||
|
fetchLatestTransactions,
|
||||||
|
} from "./dashboard.service";
|
||||||
|
|
||||||
|
export function useDashboardData(type: "expense" | "income") {
|
||||||
|
const aggregated = useQuery({
|
||||||
|
queryKey: ["dashboard", type],
|
||||||
|
queryFn: () => fetchAggregatedData(type),
|
||||||
|
});
|
||||||
|
|
||||||
|
const latest = useQuery({
|
||||||
|
queryKey: ["latest", type],
|
||||||
|
queryFn: () => fetchLatestTransactions(type),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: aggregated.data,
|
||||||
|
latest: latest.data,
|
||||||
|
isLoading: aggregated.isLoading || latest.isLoading,
|
||||||
|
error: aggregated.error || latest.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
3
src/features/report/index.ts
Normal file
3
src/features/report/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export {
|
||||||
|
useReport
|
||||||
|
} from './useReport'
|
||||||
17
src/main.jsx
17
src/main.jsx
@@ -21,18 +21,30 @@ import Header from './Header';
|
|||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import AppTheme from './AppTheme';
|
import AppTheme from './AppTheme';
|
||||||
|
|
||||||
// Polyfill Node.js globals for browser environment (needed by SwaggerParser)
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
window.Buffer = Buffer;
|
window.Buffer = Buffer;
|
||||||
window.process = process;
|
window.process = process;
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
||||||
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL;
|
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL;
|
||||||
|
|
||||||
// Initialize global API clients so all components across khata-ui have generic API access
|
// Initialize global API clients so all components across khata-ui have generic API access
|
||||||
initializeApiClients(API_BASE, AUTH_BASE);
|
initializeApiClients(API_BASE, AUTH_BASE);
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
retry: 1,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const routerMapping = [
|
const routerMapping = [
|
||||||
{ path: "/", component: Home, headerTitle: "Home" },
|
{ path: "/", component: Home, headerTitle: "Home" },
|
||||||
{ path: "/home", component: Home, headerTitle: "Home" },
|
{ path: "/home", component: Home, headerTitle: "Home" },
|
||||||
@@ -41,6 +53,7 @@ const routerMapping = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider authBaseUrl={AUTH_BASE}>
|
<AuthProvider authBaseUrl={AUTH_BASE}>
|
||||||
<AppTheme>
|
<AppTheme>
|
||||||
@@ -65,11 +78,11 @@ root.render(
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</AppTheme>
|
</AppTheme>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user