P4 — landing + builder:
- /[portSlug]/reports — new landing page with 4 build-kind cards
(dashboard / clients / berths / interests), 3 library cards
(Templates / Runs / Schedules), and the pre-P4 reports list
preserved under "Legacy library" so historical PDFs stay accessible.
- /[portSlug]/reports/[kind] — kind-aware builder route.
- dashboard: refactored the existing export dialog body into
DashboardReportBuilder (page-mounted; same widget grouping +
date-range + SavedTemplatesPicker + preview). New "Queue + go to
Runs" CTA enqueues a report_runs row via /api/v1/reports/runs
(Reports P3 path); "Download PDF" keeps the synchronous /generate
fallback for ad-hoc one-shots.
- clients / berths / interests: SimpleReportBuilder — date-range +
enqueue to /api/v1/reports/runs. Kind-specific filters land
alongside dedicated renderers in P6+.
- Dashboard "Export as PDF" button rewired: no longer opens an
in-dashboard Dialog. Becomes a Link → /reports/dashboard?from=...&to=...
carrying the currently-active range through search params so the
builder pre-fills it. Removes the dialog body (~290 lines) from the
button file; the same UI lives in DashboardReportBuilder.
- ?from=YYYY-MM-DD&to=YYYY-MM-DD search params pass the range into the
builder page.
P5 — sub-pages (functional, backed by P2 CRUD endpoints):
- /reports/runs — paginated table of report_runs with status badges,
auto-polls every 5s while any row is pending/rendering, per-row
Download (file by storageKey) + Re-run actions.
- /reports/templates — saved template grid. Clicking the name links to
the builder with ?templateId=… so it pre-applies.
- /reports/schedules — schedule table with cadence labels (weekly /
monthly / quarterly), next-run timestamps, recipient counts, and a
per-row enable Switch (PATCH /api/v1/reports/schedules/[id]).
Verified: tsc clean, 1493/1493 vitest, dev-server compile clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
'use client';
|
|
|
|
import { useParams } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import type { Route } from 'next';
|
|
import { FileDown } from 'lucide-react';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { cn } from '@/lib/utils';
|
|
import { usePermissions } from '@/hooks/use-permissions';
|
|
import { rangeToBounds, type DateRange } from '@/lib/analytics/range';
|
|
|
|
function toIsoLocal(d: Date): string {
|
|
const y = d.getFullYear();
|
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${day}`;
|
|
}
|
|
|
|
/**
|
|
* Dashboard "Export as PDF" affordance. As of Reports P4 this navigates
|
|
* to the dedicated `/reports/dashboard` builder page (carrying the
|
|
* currently-active range through `?from=YYYY-MM-DD&to=YYYY-MM-DD`)
|
|
* instead of opening an in-dashboard dialog. The dialog body now lives
|
|
* in `DashboardReportBuilder`.
|
|
*
|
|
* Permission-gated client-side on `reports.export`; the server route
|
|
* re-checks via withPermission so a tampered client can't bypass.
|
|
*/
|
|
export function ExportDashboardPdfButton({
|
|
className,
|
|
initialRange,
|
|
}: {
|
|
className?: string;
|
|
/** Carried through to the builder so the rep doesn't re-pick a range
|
|
* they just chose on the dashboard. */
|
|
initialRange?: DateRange;
|
|
} = {}) {
|
|
const { can } = usePermissions();
|
|
const params = useParams<{ portSlug: string }>();
|
|
const portSlug = params?.portSlug ?? '';
|
|
|
|
if (!can('reports', 'export')) return null;
|
|
if (!portSlug) return null;
|
|
|
|
const search = (() => {
|
|
if (!initialRange) return '';
|
|
const { from, to } = rangeToBounds(initialRange);
|
|
return `?from=${toIsoLocal(from)}&to=${toIsoLocal(to)}`;
|
|
})();
|
|
const href = `/${portSlug}/reports/dashboard${search}` as Route;
|
|
|
|
return (
|
|
<Button
|
|
asChild
|
|
variant="ghost"
|
|
size="sm"
|
|
title="Export dashboard as PDF"
|
|
aria-label="Export dashboard as PDF"
|
|
className={cn('h-9 w-9 p-0 text-muted-foreground hover:text-foreground', className)}
|
|
>
|
|
<Link href={href}>
|
|
<FileDown className="h-4 w-4" aria-hidden />
|
|
</Link>
|
|
</Button>
|
|
);
|
|
}
|