major refactor of the dashboard and react-openapi integration #1

Merged
aetos merged 44 commits from period-selection into main 2026-05-07 11:00:54 +00:00
6 changed files with 96 additions and 121 deletions
Showing only changes of commit 922d05ae37 - Show all commits

View File

@@ -9,23 +9,16 @@ import {
ToggleButtonGroup
} from "@mui/material";
import LatestItemsList, { LatestItem } from "./components/LatestItems";
import HistoryChart, { AggregatedDashboardData } from "./components/HistoryChart";
import LatestItems from "./components/LatestItems";
import HistoryChart from "./components/HistoryChart";
import {
fetchLatestTransactions,
fetchAggregatedExpenses,
fetchAggregatedIncome,
} from "./utils/dashboardLoader";
import { useDashboardData } from "./features/dashboard";
export default function Dashboard() {
const [latest, setLatest] = React.useState<{
expense: LatestItem[];
income: LatestItem[];
}>({
expense: [],
income: []
});
const [mode, setMode] = React.useState<"expense" | "income">("expense");
const [period, setPeriod] = React.useState<"rolling" | "calendar">("rolling");
const [comparison, setComparison] = React.useState(false);
const palette = {
expense: {
primary: "#d32f2f",
@@ -41,73 +34,11 @@ export default function Dashboard() {
}
};
const [aggregated, setAggregated] = React.useState<{
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 { data, latest, isLoading, error } = useDashboardData(mode);
const colors = palette[mode];
// -------- LOAD ONCE --------
React.useEffect(() => {
async function loadData() {
try {
setLoading(true);
const [
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) {
if (isLoading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "60vh" }}>
<CircularProgress />
@@ -118,11 +49,15 @@ export default function Dashboard() {
if (error) {
return (
<Container sx={{ mt: 4 }}>
<Alert severity="error">{error}</Alert>
<Alert severity="error">{String(error)}</Alert>
</Container>
);
}
if (!data) {
return null;
}
return (
<Container
sx={{
@@ -133,7 +68,6 @@ export default function Dashboard() {
p: 2
}}
>
{/* -------- TOGGLE -------- */}
<Box sx={{ display: "flex", justifyContent: "center", mb: 3 }}>
<ToggleButtonGroup
value={mode}
@@ -160,13 +94,12 @@ export default function Dashboard() {
</Box>
<Grid container spacing={4} direction="row">
<Grid size={12}>
<HistoryChart
header={`${mode === "expense" ? "Expense" : "Income"} Breakdown`}
summary="Interactive chronological tracking"
tabs={["Daily", "Weekly", "Monthly"]}
data={currentData.chartData}
data={data.chartData}
period={period}
onPeriodChange={setPeriod}
comparison={comparison}
@@ -176,14 +109,13 @@ export default function Dashboard() {
</Grid>
<Grid size={12}>
<LatestItemsList
<LatestItems
title={`Recent ${mode === "expense" ? "Expenses" : "Income"}`}
items={currentLatest}
items={latest || []}
onViewAll={() => {}}
accentColor={colors.primary}
/>
</Grid>
</Grid>
</Container>
);

View File

@@ -1,10 +1,10 @@
import { api } from "../../react-openapi";
import { LatestItem } from "../components/LatestItems";
import { api } from "../../../react-openapi";
import { LatestItem } from "../../components/LatestItems";
import * as React from "react";
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
import { fetchReport } from "../features/report/report.api";
import { mapReportToDashboard } from "../features/report/report.mapper";
import { fetchReport } from "../report/report.api";
import { mapReportToDashboard } from "../report/report.mapper";
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
sx: { color: "#388e3c" }
@@ -17,7 +17,7 @@ export async function fetchLatestTransactions(
params: { limit: 100, sort: "-occurred_at" }
});
const items = res.data?.items || res.data || [];
const items = res.data || [];
const isValid = (amt: number) =>
type === "expense" ? amt < 0 : amt > 0;

View File

@@ -0,0 +1,3 @@
export {
useDashboardData
} from './useDashboardData'

View 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,
};
}

View File

@@ -0,0 +1,3 @@
export {
useReport
} from './useReport'

View File

@@ -21,18 +21,30 @@ import Header from './Header';
import Footer from './Footer';
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.process = process;
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
const API_BASE = import.meta.env.VITE_API_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
initializeApiClients(API_BASE, AUTH_BASE);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
retry: 1,
refetchOnWindowFocus: false,
},
},
});
const routerMapping = [
{ path: "/", component: Home, headerTitle: "Home" },
{ path: "/home", component: Home, headerTitle: "Home" },
@@ -41,6 +53,7 @@ const routerMapping = [
];
root.render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider authBaseUrl={AUTH_BASE}>
<AppTheme>
@@ -65,11 +78,11 @@ root.render(
/>
))}
</Routes>
</Box>
<Footer />
</AppTheme>
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
);