Overview
The shell owns the Reports section. Plugins register one page element during register() — typically a small hub with nested Routes for each report. Heavy aggregation stays on the backend; the frontend loads data via withApiMetadataFetchFn + React Query.
- Register with AppRegistry.report.add({ pluginName, page })
- page is usually a hub + nested <Routes> for individual reports
- Sub-paths must be relative (no leading /)
- Gate access with the same APIs used by report queries
1. Register with AppRegistry.report.add
In frontend/src/index.tsx, call AppRegistry.report.add inside PluginModule.register. Shape: { pluginName, page } where page is a ReactElement.
- pluginName must equal module.metadata.json name
- page is JSX (<MyModuleReports />), not the component type
1import type { AppRegistryState, PluginModule } from "@quan-erp/shared-types";
2import metadata from "../module.metadata.json" with { type: "json" };
3import { MyModuleReports } from "./page/report";
4
5const Plugin: PluginModule = {
6 register(AppRegistry: AppRegistryState) {
7 AppRegistry.report.add({
8 pluginName: metadata.name,
9 page: <MyModuleReports />,
10 });
11 },
12};
13
14export default Plugin;2. Report hub + nested routes
When a plugin has multiple reports, build a landing hub (card grid) and nest Routes so deep links work. Sub-paths must be relative to the parent report route — use employees, not /employees.
- Use react-router-dom <Routes> / <Route> for deep-linking
- Index route = hub; child routes = individual reports
- Localize titles and descriptions — do not hardcode user-facing strings in production plugins
1import { Route, Routes, useNavigate } from "react-router-dom";
2import { Page, PageContent, PageTitle } from "@quan-erp/shared-ui";
3import metadata from "../../../module.metadata.json" with { type: "json" };
4import { EmployeesReport } from "./employees-report";
5
6const reportList = [
7 {
8 id: "employees",
9 name: "Employees",
10 description: "Headcount and status breakdown",
11 path: "employees",
12 element: <EmployeesReport />,
13 },
14];
15
16export function MyModuleReports() {
17 const navigate = useNavigate();
18
19 return (
20 <Routes>
21 <Route
22 index
23 element={
24 <Page pluginName={metadata.name}>
25 <PageTitle>
26 <span>Module reports</span>
27 </PageTitle>
28 <PageContent>
29 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-4">
30 {reportList.map((item) => (
31 <button
32 key={item.id}
33 type="button"
34 className="text-left border rounded-xl p-4 hover:shadow-md transition"
35 onClick={() => navigate(item.path)}
36 >
37 <div className="font-medium">{item.name}</div>
38 <div className="text-sm text-muted-foreground">
39 {item.description}
40 </div>
41 </button>
42 ))}
43 </div>
44 </PageContent>
45 </Page>
46 }
47 />
48 {reportList.map((item) => (
49 <Route key={item.path} path={item.path} element={item.element} />
50 ))}
51 </Routes>
52 );
53}3. Data layer
Load report data through src/api with withApiMetadataFetchFn and React Query hooks (same Admin Axios & Query pattern). Keep heavy grouping/filtering on the backend.
- Declare report endpoints in .api.ts; consume via .queries.ts
- Pass requiredApis / Protected using the same API objects
- Prefer server-side aggregates over shipping huge raw tables to the browser
4. Tables
Most reports use a table. DataTable is not exported from @quan-erp/shared-ui — use a local plugin DataTable (often under components/) built on shared-ui Table primitives, or copy the pattern from another plugin (e.g. hr).
import { DataTable } from "../../components/DataTable";
// columns + rows from your report query hook
<DataTable columns={columns} data={rows} />Critical rules
- Register only in register() via AppRegistry.report.add
- pluginName must match metadata.name
- page is a ReactElement (JSX)
- Nested report paths must be relative — no leading slash
- Localize hub and report UI strings
- Lazy-load heavy report pages when the hub lists many entries
Checklist
- Create page/report hub with nested Routes
- AppRegistry.report.add({ pluginName, page }) in register()
- Wire report APIs with withApiMetadataFetchFn + React Query
- Relative sub-paths; localize labels
- Verify under Reports after quan-erp watch and install
Common failures
- Report missing in hub — not registered, wrong pluginName, or plugin not installed
- Blank nested page — path used a leading / or does not match Route path
- Deep link 404 — missing nested <Routes> / index route
- Permission errors — report UI not gated with the same requiredApis as the query
- Huge payload / slow UI — aggregation done on the client instead of the backend