Demo

Report

How plugins contribute to the global reports hub with AppRegistry.report.add, a nested report router, and API-backed report pages.

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
TSXfrontend/src/index.tsx
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
TSXfrontend/src/page/report/index.tsx
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).

TSXlocal DataTable
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