mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
chore: more reporting
This commit is contained in:
@@ -44,7 +44,7 @@ import MyProfilePage from "./pages/dashboard/MyProfilePage";
|
||||
import OverviewPage from "./pages/dashboard/OverviewPage";
|
||||
import OverviewDomainPage from "./pages/dashboard/OverviewDomainPage";
|
||||
import { OVERVIEW_DOMAINS } from "./components/overview/overview-domains.config";
|
||||
import ReportsIndexRedirect from "./pages/reports/ReportsIndexRedirect";
|
||||
import ReportsLandingPage from "./pages/reports/ReportsLandingPage";
|
||||
import ReportPage from "./pages/reports/ReportPage";
|
||||
import AuditLogsPage from "./pages/AuditLogsPage";
|
||||
import AiBookingMockTestPage from "./pages/ai/AiBookingMockTestPage";
|
||||
@@ -245,7 +245,7 @@ const App = () => {
|
||||
path="reports"
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.reports.view}>
|
||||
<ReportsIndexRedirect />
|
||||
<ReportsLandingPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,8 @@ interface ReportSectionProps {
|
||||
reportKey: string;
|
||||
/** Scopes the report to one entity, e.g. the contract this page is showing. */
|
||||
idKeyValue?: string;
|
||||
/** Opens on the chart instead of the table — for dashboard tiles. */
|
||||
defaultView?: "table" | "chart";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,7 +19,7 @@ interface ReportSectionProps {
|
||||
* loading or if the caller lacks the report's permission, so pages can embed
|
||||
* it unconditionally without their own permission check.
|
||||
*/
|
||||
export function ReportSection({ reportKey, idKeyValue }: ReportSectionProps) {
|
||||
export function ReportSection({ reportKey, idKeyValue, defaultView }: ReportSectionProps) {
|
||||
const { data: catalog } = useQuery(api.reports.catalog.queryOptions());
|
||||
const def = catalog?.find((r) => r.key === reportKey);
|
||||
|
||||
@@ -31,7 +33,7 @@ export function ReportSection({ reportKey, idKeyValue }: ReportSectionProps) {
|
||||
{def.description}
|
||||
</Text>
|
||||
</div>
|
||||
<ReportView reportKey={reportKey} idKeyValue={idKeyValue} />
|
||||
<ReportView reportKey={reportKey} idKeyValue={idKeyValue} defaultView={defaultView} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
import { api } from "@/services/api";
|
||||
|
||||
/**
|
||||
* `/dashboard/reports` has no page of its own — it forwards to the first
|
||||
* report the caller has access to (catalog order = registration order,
|
||||
* already permission-filtered server-side), or home if they have none.
|
||||
*/
|
||||
export default function ReportsIndexRedirect() {
|
||||
const { data: catalog, isLoading } = useQuery(api.reports.catalog.queryOptions());
|
||||
|
||||
if (isLoading) return null;
|
||||
const first = catalog?.[0];
|
||||
return <Navigate to={first ? `/dashboard/reports/${first.key}` : "/dashboard"} replace />;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { SimpleGrid, Stack } from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
import { PageContainer, PageHeader } from "@/components/page";
|
||||
import { ReportSection } from "@/components/reports/ReportSection";
|
||||
import { api } from "@/services/api";
|
||||
|
||||
/**
|
||||
* The revenue dashboard the reporting spec asks for, assembled from reports
|
||||
* that already exist rather than a second aggregation API: each tile is a
|
||||
* `ReportSection` opened on its chart, and each one permission-gates itself by
|
||||
* rendering nothing when the caller's catalog lacks that report.
|
||||
*/
|
||||
const TILES = [
|
||||
"revenue-by-period",
|
||||
"revenue-by-category",
|
||||
"revenue-by-route",
|
||||
"revenue-top-customers",
|
||||
];
|
||||
|
||||
export default function ReportsLandingPage() {
|
||||
const { data: catalog, isLoading } = useQuery(api.reports.catalog.queryOptions());
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
const visible = TILES.filter((key) => catalog?.some((r) => r.key === key));
|
||||
|
||||
// No revenue reports for this user — fall back to the old behaviour and send
|
||||
// them to the first report they can actually open.
|
||||
if (!visible.length) {
|
||||
const first = catalog?.[0];
|
||||
return <Navigate to={first ? `/dashboard/reports/${first.key}` : "/dashboard"} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Stack gap="lg">
|
||||
<PageHeader
|
||||
title="Revenue dashboard"
|
||||
subtitle="Billed rail revenue by period, category, corridor and customer. Pick any report in the sidebar for the full table, filters and export."
|
||||
/>
|
||||
<SimpleGrid cols={{ base: 1, xl: 2 }} spacing="lg">
|
||||
{visible.map((key) => (
|
||||
<ReportSection key={key} reportKey={key} defaultView="chart" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,16 @@ export interface ReportChartDef {
|
||||
y: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a summary row clickable: the row's values are carried into another
|
||||
* report as filter params. Keys are this report's column keys; values are the
|
||||
* target report's filter keys.
|
||||
*/
|
||||
export interface ReportDrillDef {
|
||||
to: string;
|
||||
carry: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Mirrors the backend's ReportCatalogEntry — one entry per GET /reports item. */
|
||||
export interface ReportCatalogEntry {
|
||||
key: string;
|
||||
@@ -58,6 +68,7 @@ export interface ReportCatalogEntry {
|
||||
defaultSort?: { key: string; dir: "ASC" | "DESC" };
|
||||
hasSummary: boolean;
|
||||
chart?: ReportChartDef;
|
||||
drill?: ReportDrillDef;
|
||||
}
|
||||
|
||||
export interface ReportPageMeta {
|
||||
|
||||
Reference in New Issue
Block a user