refactor(reports): top-level /reports section in the header, not a Setup tab
CI / check (push) Failing after 30s
CI / check (push) Failing after 30s
Moves Reports out of the Setup tab bar into a standalone top-level route (/reports) with its own header nav link, alongside Booth/Shifts/ Subscriptions. Adds a /setup/reports → /reports legacy redirect. Same report:read gate. Wiki note updated. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
+21
-19
@@ -88,7 +88,6 @@ function SetupLayout() {
|
|||||||
{show("site:read") && <SetupTab to="/setup/site" label={t("nav.site")} />}
|
{show("site:read") && <SetupTab to="/setup/site" label={t("nav.site")} />}
|
||||||
{show("user:read") && <SetupTab to="/setup/users" label={t("nav.users")} />}
|
{show("user:read") && <SetupTab to="/setup/users" label={t("nav.users")} />}
|
||||||
{show("role:read") && <SetupTab to="/setup/roles" label={t("nav.roles")} />}
|
{show("role:read") && <SetupTab to="/setup/roles" label={t("nav.roles")} />}
|
||||||
{show("report:read") && <SetupTab to="/setup/reports" label={t("nav.reports")} />}
|
|
||||||
{show("log:read") && <SetupTab to="/setup/logs" label={t("nav.logs")} />}
|
{show("log:read") && <SetupTab to="/setup/logs" label={t("nav.logs")} />}
|
||||||
</nav>
|
</nav>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@@ -378,7 +377,9 @@ function RootLayout() {
|
|||||||
{(show("subscription:read") || show("subscription:plan") || show("tariff:read")) && (
|
{(show("subscription:read") || show("subscription:plan") || show("tariff:read")) && (
|
||||||
<NavLink to="/subscriptions" label={t("nav.subscriptions")} />
|
<NavLink to="/subscriptions" label={t("nav.subscriptions")} />
|
||||||
)}
|
)}
|
||||||
{/* One Setup entry — its tabs hold devices/tariff/site/users/roles/shifts/logs.
|
{/* Reports — a standalone admin section (own header entry, route /reports). */}
|
||||||
|
{show("report:read") && <NavLink to="/reports" label={t("nav.reports")} />}
|
||||||
|
{/* One Setup entry — its tabs hold devices/tariff/site/users/roles/logs.
|
||||||
Shown if the user can reach ANY of those screens (an operator with only
|
Shown if the user can reach ANY of those screens (an operator with only
|
||||||
shift:read still gets in, landing on Shifts). */}
|
shift:read still gets in, landing on Shifts). */}
|
||||||
{(show("site:update") ||
|
{(show("site:update") ||
|
||||||
@@ -386,7 +387,6 @@ function RootLayout() {
|
|||||||
show("site:read") ||
|
show("site:read") ||
|
||||||
show("user:read") ||
|
show("user:read") ||
|
||||||
show("role:read") ||
|
show("role:read") ||
|
||||||
show("report:read") ||
|
|
||||||
show("shift:read")) && <NavLink to="/setup" label={t("nav.setup")} />}
|
show("shift:read")) && <NavLink to="/setup" label={t("nav.setup")} />}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="ml-auto flex items-center gap-3">
|
<div className="ml-auto flex items-center gap-3">
|
||||||
@@ -447,6 +447,7 @@ const legacyRedirects = (
|
|||||||
["/setup/plans", "/subscriptions/plans"],
|
["/setup/plans", "/subscriptions/plans"],
|
||||||
["/setup/tariff-lab", "/subscriptions/tariff-lab"],
|
["/setup/tariff-lab", "/subscriptions/tariff-lab"],
|
||||||
["/setup/shifts", "/shifts"],
|
["/setup/shifts", "/shifts"],
|
||||||
|
["/setup/reports", "/reports"],
|
||||||
] as const
|
] as const
|
||||||
).map(([from, to]) =>
|
).map(([from, to]) =>
|
||||||
createRoute({
|
createRoute({
|
||||||
@@ -458,6 +459,22 @@ const legacyRedirects = (
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Admin reports/charts — a top-level section (own header nav entry), NOT a Setup tab.
|
||||||
|
// Gated by report:read. Lazy component (Recharts) in a Suspense so it stays out of the
|
||||||
|
// booth's initial bundle.
|
||||||
|
const reportsRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: "/reports",
|
||||||
|
beforeLoad: ({ context }) => requirePerm("report:read")(context),
|
||||||
|
component: function ReportsRoute() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="p-3 text-term-muted">…</div>}>
|
||||||
|
<Reports />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const shiftRoute = createRoute({
|
const shiftRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
path: "/shifts",
|
path: "/shifts",
|
||||||
@@ -496,7 +513,6 @@ const SETUP_TABS: { to: string; perm: Permission }[] = [
|
|||||||
{ to: "/setup/site", perm: "site:read" },
|
{ to: "/setup/site", perm: "site:read" },
|
||||||
{ to: "/setup/users", perm: "user:read" },
|
{ to: "/setup/users", perm: "user:read" },
|
||||||
{ to: "/setup/roles", perm: "role:read" },
|
{ to: "/setup/roles", perm: "role:read" },
|
||||||
{ to: "/setup/reports", perm: "report:read" },
|
|
||||||
{ to: "/shifts", perm: "shift:read" },
|
{ to: "/shifts", perm: "shift:read" },
|
||||||
{ to: "/setup/logs", perm: "log:read" },
|
{ to: "/setup/logs", perm: "log:read" },
|
||||||
];
|
];
|
||||||
@@ -594,20 +610,6 @@ const rolesRoute = createRoute({
|
|||||||
// (Shift history lives at the standalone /shifts route — see shiftRoute. It was
|
// (Shift history lives at the standalone /shifts route — see shiftRoute. It was
|
||||||
// removed as a Setup tab; /setup/shifts and the old /shift both redirect there.)
|
// removed as a Setup tab; /setup/shifts and the old /shift both redirect there.)
|
||||||
|
|
||||||
// Admin reports/charts. Gated by report:read. Lazy component (Recharts) in a Suspense.
|
|
||||||
const reportsRoute = createRoute({
|
|
||||||
getParentRoute: () => setupRoute,
|
|
||||||
path: "reports",
|
|
||||||
beforeLoad: ({ context }) => requirePerm("report:read")(context),
|
|
||||||
component: function ReportsRoute() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<div className="p-3 text-term-muted">…</div>}>
|
|
||||||
<Reports />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Diagnostic logs. Gated by log:read (an admin/diagnostic permission).
|
// Diagnostic logs. Gated by log:read (an admin/diagnostic permission).
|
||||||
const logsRoute = createRoute({
|
const logsRoute = createRoute({
|
||||||
getParentRoute: () => setupRoute,
|
getParentRoute: () => setupRoute,
|
||||||
@@ -621,6 +623,7 @@ const routeTree = rootRoute.addChildren([
|
|||||||
boothRoute,
|
boothRoute,
|
||||||
...legacyRedirects,
|
...legacyRedirects,
|
||||||
shiftRoute,
|
shiftRoute,
|
||||||
|
reportsRoute,
|
||||||
subscriptionsRoute.addChildren([
|
subscriptionsRoute.addChildren([
|
||||||
subscriptionsIndexRoute,
|
subscriptionsIndexRoute,
|
||||||
subscriptionPlansRoute,
|
subscriptionPlansRoute,
|
||||||
@@ -632,7 +635,6 @@ const routeTree = rootRoute.addChildren([
|
|||||||
siteRoute,
|
siteRoute,
|
||||||
usersRoute,
|
usersRoute,
|
||||||
rolesRoute,
|
rolesRoute,
|
||||||
reportsRoute,
|
|
||||||
logsRoute,
|
logsRoute,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ derived and rebuildable, never a separate ledger.
|
|||||||
|
|
||||||
## Built — admin Reports dashboard v1 (2026-06-22)
|
## Built — admin Reports dashboard v1 (2026-06-22)
|
||||||
|
|
||||||
A first cut shipped: an admin **Reports** screen (`/setup/reports`, gated on `report:read`), an
|
A first cut shipped: an admin **Reports** screen — a **top-level section** at **`/reports`** with its
|
||||||
on-demand **dashboard** (not a live feed). Server aggregates everything in **one call**
|
own header nav entry (not nested under Setup), gated on `report:read` — an on-demand **dashboard**
|
||||||
|
(not a live feed). Server aggregates everything in **one call**
|
||||||
(`GET /api/reports/summary?from&to&bucket`) so the SPA only renders; `…/summary.csv` exports the
|
(`GET /api/reports/summary?from&to&bucket`) so the SPA only renders; `…/summary.csv` exports the
|
||||||
per-bucket series. Code: `apps/server/src/reports.ts` (+ `routes/reports.ts`), `apps/web/src/Reports.tsx`.
|
per-bucket series. Code: `apps/server/src/reports.ts` (+ `routes/reports.ts`), `apps/web/src/Reports.tsx`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user