Add a /reports page built on react-openapi's runtime configs: - inline generate panel (GroupSpec enum, prefetched payee picker, strict DD-MM-YYYY range, 400 surfacing) - metadata report list with view/regenerate/delete - viewer that slices the cached per-entity payload via metadata.group_options, aggregates metrics + txns, pivots period x payee, and renders month accordions - route + nav entry; no hand-rolled schema duplication
167 lines
5.0 KiB
JavaScript
167 lines
5.0 KiB
JavaScript
import * as React from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import {
|
|
BrowserRouter,
|
|
Routes,
|
|
Route,
|
|
useNavigate,
|
|
useLocation,
|
|
matchPath
|
|
} from "react-router-dom";
|
|
import {
|
|
Box,
|
|
CssBaseline,
|
|
CircularProgress
|
|
} from "@mui/material";
|
|
import Home from './Home';
|
|
import FetchRequests from './FetchRequest/FetchRequestCreate';
|
|
import FetchRequestDetail from './FetchRequest/FetchRequestDetail';
|
|
import Expense from './Expense/Expense';
|
|
import Reports from './Reports/Reports';
|
|
import { AppProvider, useAppContext, Admin, ProfileRoutes } from '../react-openapi';
|
|
import { AuthProvider, AuthPage, useAuth, ProfileCreate, ProfileEdit, ProfileView } from "../react-auth";
|
|
import Header from './Header';
|
|
import Footer from './Footer';
|
|
import AppTheme from './shared-theme/AppTheme';
|
|
import { ToastProvider } from './ui/Toast';
|
|
import { specConfiguration } from './openapi-config';
|
|
|
|
const queryClient = new QueryClient();
|
|
|
|
const rootElement = document.getElementById('root');
|
|
const root = createRoot(rootElement);
|
|
|
|
// Wire profile components from react-auth into the spec-driven admin
|
|
specConfiguration.profileComponents = {
|
|
create: ProfileCreate,
|
|
edit: ProfileEdit,
|
|
view: ProfileView,
|
|
};
|
|
|
|
function LoginPage() {
|
|
const { login, register, loading, error, currentUser } = useAuth();
|
|
const navigate = useNavigate();
|
|
return (
|
|
<AuthPage
|
|
mode="login"
|
|
onBack={() => navigate("/")}
|
|
onSwitchMode={() => navigate("/register")}
|
|
login={login}
|
|
register={register}
|
|
loading={loading}
|
|
error={error}
|
|
currentUser={currentUser}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function RegisterPage() {
|
|
const { login, register, loading, error, currentUser } = useAuth();
|
|
const navigate = useNavigate();
|
|
return (
|
|
<AuthPage
|
|
mode="register"
|
|
onBack={() => navigate("/")}
|
|
onSwitchMode={() => navigate("/login")}
|
|
login={login}
|
|
register={register}
|
|
loading={loading}
|
|
error={error}
|
|
currentUser={currentUser}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const routerMapping = [
|
|
{ path: "/", component: Home, headerTitle: "Home" },
|
|
{ path: "/home", component: Home, headerTitle: "Home" },
|
|
{ path: "/login", component: LoginPage, headerTitle: "Login" },
|
|
{ path: "/register", component: RegisterPage, headerTitle: "Register" },
|
|
{ path: "/fetch-requests", component: FetchRequests, headerTitle: "Fetch Requests" },
|
|
{ path: "/fetch-requests/:id", component: FetchRequestDetail, headerTitle: "Fetch Request" },
|
|
{ path: "/expenses", component: Expense, headerTitle: "Expenses" },
|
|
{ path: "/reports", component: Reports, headerTitle: "Reports" },
|
|
{ path: "/admin/*", component: Admin, headerTitle: "Admin" },
|
|
{ path: "/profile/*", component: ProfileRoutes, headerTitle: "Profile" },
|
|
];
|
|
|
|
/** Reads authConfig from AppProvider context and passes it to AuthProvider. */
|
|
function AppContent() {
|
|
const { authConfig, loading } = useAppContext();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
React.useEffect(() => {
|
|
const matched = routerMapping.find((route) =>
|
|
matchPath({ path: route.path, end: false }, location.pathname),
|
|
);
|
|
document.title = matched
|
|
? `${matched.headerTitle} · Khata`
|
|
: "Khata — Financial Ledger";
|
|
}, [location.pathname]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "100vh" }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthProvider authConfig={authConfig} onUnauthorized={() => navigate("/login")}>
|
|
<AppTheme>
|
|
<CssBaseline enableColorScheme />
|
|
<ToastProvider>
|
|
<Box sx={{ display: "flex", flexDirection: "column", minHeight: "100vh" }}>
|
|
<Header routerMapping={routerMapping} />
|
|
|
|
<Box
|
|
component="main"
|
|
key={location.pathname}
|
|
sx={{
|
|
flexGrow: 1,
|
|
width: "100%",
|
|
animation: "fadeIn 220ms ease-out",
|
|
"@keyframes fadeIn": {
|
|
from: { opacity: 0, transform: "translateY(4px)" },
|
|
to: { opacity: 1, transform: "translateY(0)" },
|
|
},
|
|
}}
|
|
>
|
|
<Routes>
|
|
{routerMapping.map(({ path, component: Component }) => (
|
|
<Route
|
|
key={path}
|
|
path={path}
|
|
element={<Component basePath={path.replace(/\/\*$/, "")} />}
|
|
/>
|
|
))}
|
|
</Routes>
|
|
</Box>
|
|
|
|
<Footer />
|
|
</Box>
|
|
</ToastProvider>
|
|
</AppTheme>
|
|
</AuthProvider>
|
|
);
|
|
}
|
|
|
|
function AppWithAuth() {
|
|
const navigate = useNavigate();
|
|
return (
|
|
<AppProvider specConfiguration={specConfiguration} onUnauthorized={() => navigate("/login")}>
|
|
<AppContent />
|
|
</AppProvider>
|
|
);
|
|
}
|
|
|
|
root.render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<BrowserRouter>
|
|
<AppWithAuth />
|
|
</BrowserRouter>
|
|
</QueryClientProvider>
|
|
); |