feat(reports): header actions, single export dialog, date-range presets

- ReportPage drops its own PageHeader (and the back arrow); ReportView
  now optionally renders the header itself (pageHeader prop) with
  export/refresh as its actions. Embedded ReportSection usage is
  unaffected (keeps the inline toolbar next to filters).
- Replace the two xlsx/pdf icon buttons with one Export button opening
  a dialog: format as large icon radio cards, fields as checkboxes
  (select-all toggle), record count (default all, capped per format).
  Export applies the report's current filters and sort.
- Backend: export route accepts fields (whitelisted against the
  report's own columns) and limit; ReportExportService takes an
  optional column subset instead of always dumping every column.
- Fixed a real bug found while wiring this up: runAll() ignored the
  caller's sortBy/sortOrder and always used the report's default sort,
  so exports silently didn't match whatever order was on screen.
- Report daterange filters now use DatePickerInput + the shared
  getDateRangePresets() (Today/Last 7 days/This month/...) instead of
  two bare DateInputs, matching every other date-range filter in the
  app.
- Removed the reports hub grid page. /dashboard/reports now redirects
  to the first report the caller has access to, or /dashboard if they
  have none.
This commit is contained in:
Nathnael
2026-08-13 08:53:03 +00:00
parent 58b47318e9
commit 6a102bf938
10 changed files with 293 additions and 187 deletions

View File

@@ -1,23 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { ReportView } from "@/components/reports/ReportView";
import { PageContainer, PageHeader } from "@/components/page";
import { api } from "@/services/api";
import { PageContainer } from "@/components/page";
export default function ReportPage() {
const { reportKey = "" } = useParams<{ reportKey: string }>();
const { data: catalog } = useQuery(api.reports.catalog.queryOptions());
const def = catalog?.find((r) => r.key === reportKey);
return (
<PageContainer>
<PageHeader
title={def?.title ?? "Report"}
subtitle={def?.description}
backTo="/dashboard/reports"
/>
<ReportView reportKey={reportKey} />
<ReportView reportKey={reportKey} pageHeader />
</PageContainer>
);
}

View File

@@ -1,64 +0,0 @@
import { Alert, Card, SimpleGrid, Skeleton, Stack, Text, Title } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { PageContainer, PageHeader } from "@/components/page";
import { api } from "@/services/api";
import type { ReportCatalogEntry } from "@/types/reports";
const GROUP_ORDER: ReportCatalogEntry["group"][] = ["Commercial", "Operations", "Finance"];
export default function ReportsHubPage() {
const navigate = useNavigate();
const { data: catalog, isLoading, isError } = useQuery(api.reports.catalog.queryOptions());
const groups = GROUP_ORDER.map((group) => ({
group,
reports: (catalog ?? []).filter((r) => r.group === group),
})).filter((g) => g.reports.length);
return (
<PageContainer>
<PageHeader title="Reports" subtitle="Every report you have access to, grouped by area." />
{isError ? <Alert color="red">Failed to load the report catalog.</Alert> : null}
{isLoading ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} height={96} radius="md" />
))}
</SimpleGrid>
) : null}
{!isLoading && !isError && !groups.length ? (
<Text c="dimmed">You don't have access to any reports yet.</Text>
) : null}
{groups.map(({ group, reports }) => (
<Stack key={group} gap="sm">
<Title order={4}>{group}</Title>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
{reports.map((report) => (
<Card
key={report.key}
withBorder
radius="md"
p="md"
className="cursor-pointer transition-colors hover:bg-gray-50"
onClick={() => navigate(`/dashboard/reports/${report.key}`)}
>
<Text fw={600} c="edr-text">
{report.title}
</Text>
<Text size="sm" c="dimmed" mt={4}>
{report.description}
</Text>
</Card>
))}
</SimpleGrid>
</Stack>
))}
</PageContainer>
);
}

View File

@@ -0,0 +1,17 @@
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 />;
}