proper use of react-openapi for resource api calls
This commit is contained in:
@@ -1,24 +1,15 @@
|
||||
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 "../report/report.api";
|
||||
import { mapReportToDashboard } from "../report/report.mapper";
|
||||
import { LatestItem } from "../../components/LatestItems";
|
||||
|
||||
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
||||
sx: { color: "#388e3c" }
|
||||
});
|
||||
|
||||
export async function fetchLatestTransactions(
|
||||
export function mapToLatestItems(
|
||||
items: any[],
|
||||
type: "expense" | "income"
|
||||
): Promise<LatestItem[]> {
|
||||
const res = await api.get("/expenses", {
|
||||
params: { limit: 100, sort: "-occurred_at" }
|
||||
});
|
||||
|
||||
const items = res.data || [];
|
||||
|
||||
): LatestItem[] {
|
||||
const isValid = (amt: number) =>
|
||||
type === "expense" ? amt < 0 : amt > 0;
|
||||
|
||||
@@ -47,20 +38,3 @@ export async function fetchLatestTransactions(
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAggregatedData(
|
||||
type: "expense" | "income"
|
||||
) {
|
||||
const [weekly, monthly] = await Promise.all([
|
||||
fetchReport({ period: "weekly", rolling: true }),
|
||||
fetchReport({ period: "monthly", rolling: true }),
|
||||
]);
|
||||
|
||||
return mapReportToDashboard(weekly.buckets, monthly.buckets, type);
|
||||
}
|
||||
|
||||
export const fetchAggregatedExpenses = () =>
|
||||
fetchAggregatedData("expense");
|
||||
|
||||
export const fetchAggregatedIncome = () =>
|
||||
fetchAggregatedData("income");
|
||||
@@ -1,24 +1,48 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
fetchAggregatedData,
|
||||
fetchLatestTransactions,
|
||||
} from "./dashboard.service";
|
||||
import { useResourceByName } from "../../../react-openapi";
|
||||
import { mapToLatestItems } from "./dashboard.mapper";
|
||||
import { mapReportToDashboard } from "../report/report.mapper";
|
||||
|
||||
export function useDashboardData(type: "expense" | "income") {
|
||||
const aggregated = useQuery({
|
||||
queryKey: ["dashboard", type],
|
||||
queryFn: () => fetchAggregatedData(type),
|
||||
const { useList: useExpenseList } = useResourceByName("expenses");
|
||||
const { useList: useReportList } = useResourceByName("reports");
|
||||
|
||||
// Fetch latest transactions
|
||||
const latestQuery = useExpenseList({
|
||||
limit: 100,
|
||||
sort: "-occurred_at"
|
||||
});
|
||||
|
||||
const latest = useQuery({
|
||||
queryKey: ["latest", type],
|
||||
queryFn: () => fetchLatestTransactions(type),
|
||||
});
|
||||
// Fetch reports for aggregation
|
||||
const weeklyReport = useReportList({ period: "weekly", rolling: true });
|
||||
const monthlyReport = useReportList({ period: "monthly", rolling: true });
|
||||
|
||||
const isLoading =
|
||||
latestQuery.isLoading ||
|
||||
weeklyReport.isLoading ||
|
||||
monthlyReport.isLoading;
|
||||
|
||||
const error =
|
||||
latestQuery.error ||
|
||||
weeklyReport.error ||
|
||||
monthlyReport.error;
|
||||
|
||||
const latest = latestQuery.data?.data
|
||||
? mapToLatestItems(latestQuery.data.data, type)
|
||||
: [];
|
||||
|
||||
const aggregatedData =
|
||||
weeklyReport.data?.data && monthlyReport.data?.data
|
||||
? mapReportToDashboard(
|
||||
(weeklyReport.data.data as any).buckets,
|
||||
(monthlyReport.data.data as any).buckets,
|
||||
type
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
data: aggregated.data,
|
||||
latest: latest.data,
|
||||
isLoading: aggregated.isLoading || latest.isLoading,
|
||||
error: aggregated.error || latest.error,
|
||||
data: aggregatedData,
|
||||
latest: latest,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { api } from "../../../react-openapi";
|
||||
|
||||
export async function fetchReport(params: {
|
||||
period: "weekly" | "monthly" | "yearly" | "fyly";
|
||||
rolling?: boolean;
|
||||
}) {
|
||||
const res = await api.get("/reports", { params });
|
||||
return res.data;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { fetchReport } from "./report.api";
|
||||
import {
|
||||
AggregatedDashboardData,
|
||||
ChartData,
|
||||
ChartDataPoint,
|
||||
} from "../components/HistoryChart";
|
||||
|
||||
type ReportBucket = any; // replace with generated type if available
|
||||
|
||||
function sumBucket(bucket: ReportBucket, flow: "expenses" | "incomes") {
|
||||
return bucket.groups.reduce(
|
||||
(acc: number, g: any) => acc + (g?.[flow]?.sum || 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
function toLabel(start: string, end: string, type: "weekly" | "monthly") {
|
||||
const s = new Date(start);
|
||||
const e = new Date(end);
|
||||
|
||||
if (type === "monthly") {
|
||||
return s.toLocaleString("default", { month: "short" });
|
||||
}
|
||||
|
||||
const sd = s.getDate();
|
||||
const ed = e.getDate();
|
||||
const m = e.toLocaleString("default", { month: "short" });
|
||||
return `${sd}–${ed} ${m}`;
|
||||
}
|
||||
|
||||
function toChartPoints(
|
||||
buckets: ReportBucket[],
|
||||
type: "weekly" | "monthly",
|
||||
flow: "expenses" | "incomes"
|
||||
): ChartDataPoint[] {
|
||||
return buckets.map((b, i) => {
|
||||
const amount = sumBucket(b, flow);
|
||||
|
||||
const prev = buckets[i - 1];
|
||||
const compareAmount = prev ? sumBucket(prev, flow) : 0;
|
||||
|
||||
return {
|
||||
id: toLabel(b.start, b.end, type),
|
||||
amount,
|
||||
compare: prev
|
||||
? {
|
||||
id: toLabel(prev.start, prev.end, type),
|
||||
amount: compareAmount,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildChartData(
|
||||
weekly: ReportBucket[],
|
||||
monthly: ReportBucket[],
|
||||
flow: "expenses" | "incomes"
|
||||
): ChartData {
|
||||
return {
|
||||
daily: [], // not supported by /reports → keep empty or drop
|
||||
weekly: {
|
||||
rolling: toChartPoints(weekly, "weekly", flow),
|
||||
calendar: toChartPoints(weekly, "weekly", flow), // same unless backend differentiates
|
||||
},
|
||||
monthly: {
|
||||
rolling: toChartPoints(monthly, "monthly", flow),
|
||||
calendar: toChartPoints(monthly, "monthly", flow),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getTopPayees(buckets: ReportBucket[], flow: "expenses" | "incomes") {
|
||||
const map: Record<string, number> = {};
|
||||
|
||||
for (const b of buckets) {
|
||||
for (const g of b.groups) {
|
||||
const key = g.group_key || "Unknown";
|
||||
const amt = g?.[flow]?.sum || 0;
|
||||
map[key] = (map[key] || 0) + amt;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(map)
|
||||
.map(([payeeName, amount]) => ({ payeeName, amount }))
|
||||
.sort((a, b) => b.amount - a.amount)
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
export async function getDashboardData(
|
||||
type: "expense" | "income"
|
||||
): Promise<AggregatedDashboardData> {
|
||||
const flow = type === "expense" ? "expenses" : "incomes";
|
||||
|
||||
const [weeklyBuckets, monthlyBuckets] = await Promise.all([
|
||||
fetchReport({ period: "weekly", rolling: true }),
|
||||
fetchReport({ period: "monthly", rolling: true }),
|
||||
]);
|
||||
|
||||
const chartData = buildChartData(weeklyBuckets, monthlyBuckets, flow);
|
||||
|
||||
const totalAmount = weeklyBuckets.reduce(
|
||||
(acc: number, b: any) => acc + sumBucket(b, flow),
|
||||
0
|
||||
);
|
||||
|
||||
const topPayees = getTopPayees(weeklyBuckets, flow);
|
||||
|
||||
return {
|
||||
chartData,
|
||||
totalAmount,
|
||||
topPayees,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchReport } from "./report.api";
|
||||
import { useResourceByName } from "../../../react-openapi";
|
||||
|
||||
export interface ReportParams {
|
||||
period: "weekly" | "monthly" | "yearly" | "fyly";
|
||||
@@ -10,8 +9,6 @@ export interface ReportParams {
|
||||
}
|
||||
|
||||
export function useReport(params: ReportParams) {
|
||||
return useQuery({
|
||||
queryKey: ["report", params],
|
||||
queryFn: () => fetchReport(params),
|
||||
});
|
||||
const { useList } = useResourceByName("reports");
|
||||
return useList(params);
|
||||
}
|
||||
24
src/main.jsx
24
src/main.jsx
@@ -12,7 +12,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import Home from './Home';
|
||||
import Dashboard from './Dashboard';
|
||||
import { Admin, initializeApiClients } from '../react-openapi';
|
||||
import { Admin, AppProvider } from '../react-openapi';
|
||||
import { configuration, profileConfiguration } from './openapi-config';
|
||||
import { Buffer } from 'buffer';
|
||||
import process from 'process';
|
||||
@@ -21,30 +21,14 @@ import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import AppTheme from './AppTheme';
|
||||
|
||||
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" },
|
||||
@@ -53,7 +37,7 @@ const routerMapping = [
|
||||
];
|
||||
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppProvider resourceOverrides={configuration} profileConfig={profileConfiguration}>
|
||||
<BrowserRouter>
|
||||
<AuthProvider authBaseUrl={AUTH_BASE}>
|
||||
<AppTheme>
|
||||
@@ -70,7 +54,7 @@ root.render(
|
||||
path={path}
|
||||
element={
|
||||
path.startsWith("/admin") ? (
|
||||
<Component basePath="/admin" resourceOverrides={configuration} profileConfig={profileConfiguration} />
|
||||
<Component basePath="/admin" />
|
||||
) : (
|
||||
<Component />
|
||||
)
|
||||
@@ -84,5 +68,5 @@ root.render(
|
||||
</AppTheme>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user