diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 4543b7b..6afd4fd 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -9,25 +9,18 @@ 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: { + expense: { primary: "#d32f2f", light: "#fdecea", dark: "#9a0007", @@ -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(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 ( - - - - ); - } - const currentLatest = latest[mode]; - - // -------- UI STATES -------- - if (loading) { + if (isLoading) { return ( @@ -118,11 +49,15 @@ export default function Dashboard() { if (error) { return ( - {error} + {String(error)} ); } + if (!data) { + return null; + } + return ( - {/* -------- TOGGLE -------- */} - - {}} accentColor={colors.primary} /> - ); -} \ No newline at end of file +} diff --git a/src/utils/dashboardLoader.ts b/src/features/dashboard/dashboard.service.ts similarity index 85% rename from src/utils/dashboardLoader.ts rename to src/features/dashboard/dashboard.service.ts index 654c961..f466cf6 100644 --- a/src/utils/dashboardLoader.ts +++ b/src/features/dashboard/dashboard.service.ts @@ -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; diff --git a/src/features/dashboard/index.ts b/src/features/dashboard/index.ts new file mode 100644 index 0000000..ddf8c37 --- /dev/null +++ b/src/features/dashboard/index.ts @@ -0,0 +1,3 @@ +export { + useDashboardData +} from './useDashboardData' diff --git a/src/features/dashboard/useDashboardData.ts b/src/features/dashboard/useDashboardData.ts new file mode 100644 index 0000000..028be86 --- /dev/null +++ b/src/features/dashboard/useDashboardData.ts @@ -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, + }; +} diff --git a/src/features/report/index.ts b/src/features/report/index.ts new file mode 100644 index 0000000..f037c04 --- /dev/null +++ b/src/features/report/index.ts @@ -0,0 +1,3 @@ +export { + useReport +} from './useReport' diff --git a/src/main.jsx b/src/main.jsx index c1c76f8..139efe0 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -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,35 +53,36 @@ const routerMapping = [ ]; root.render( - - - - -
+ + + + + +
- - + + - - {routerMapping.map(({ path, component: Component }) => ( - - ) : ( - - ) - } - /> - ))} - + + {routerMapping.map(({ path, component: Component }) => ( + + ) : ( + + ) + } + /> + ))} + + - - -