'use client'; import { useMemo, useState } from 'react'; import { Eye, FileDown, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { triggerBlobDownload } from '@/lib/utils/download'; import { usePermissions } from '@/hooks/use-permissions'; import { resolvePortIdFromSlug } from '@/lib/api/client'; import { SavedTemplatesPicker, type SavedTemplate } from './saved-templates-picker'; import { PdfPreviewModal } from './pdf-preview-modal'; type ListKind = 'clients' | 'berths' | 'interests'; interface Props { kind: ListKind; /** Label shown on the trigger button (e.g. "Export PDF"). */ buttonLabel?: string; /** Default title pre-populated in the dialog. */ defaultTitle?: string; } const KIND_LABEL: Record = { clients: 'clients', berths: 'berths', interests: 'interests', }; /** * Generic list-report export button. Renders a small dialog with * a title input + "include archived" toggle, then POSTs to the * report-generate endpoint. The kind discriminator picks the * matching server-side data resolver and React-PDF template. * * Permission-gated client-side on `reports.export`; the server * route enforces the same. */ export function ExportListPdfButton({ kind, buttonLabel = 'Export PDF', defaultTitle }: Props) { const { can } = usePermissions(); const [open, setOpen] = useState(false); const [title, setTitle] = useState( defaultTitle ?? `${KIND_LABEL[kind].charAt(0).toUpperCase() + KIND_LABEL[kind].slice(1)} report - ${new Date().toLocaleDateString(undefined)}`, ); const [includeArchived, setIncludeArchived] = useState(false); const [loading, setLoading] = useState(false); const [previewOpen, setPreviewOpen] = useState(false); const previewPayload = useMemo( () => ({ title: title.trim() || `${kind} report`, config: { kind, filters: { includeArchived } }, }), [title, kind, includeArchived], ); if (!can('reports', 'export')) return null; async function handleExport() { setLoading(true); try { const headers = new Headers({ 'Content-Type': 'application/json' }); if (typeof window !== 'undefined') { const slug = window.location.pathname.split('/').filter(Boolean)[0]; if (slug && slug !== 'login' && slug !== 'portal' && slug !== 'api') { const portId = await resolvePortIdFromSlug(slug); if (portId) headers.set('X-Port-Id', portId); } } const res = await fetch('/api/v1/reports/generate', { method: 'POST', headers, body: JSON.stringify({ title: title.trim() || `${kind} report`, config: { kind, filters: { includeArchived }, }, }), }); if (!res.ok) { const text = await res.text(); throw new Error(text || `Export failed (${res.status})`); } const blob = await res.blob(); triggerBlobDownload(blob, `${title.trim().replace(/[\\/]/g, '_')}.pdf`); toast.success('Report downloaded'); setOpen(false); } catch (err) { toast.error(err instanceof Error ? err.message : 'Export failed'); } finally { setLoading(false); } } return ( <> Export {KIND_LABEL[kind]} as PDF The PDF inherits the active port's logo and primary color. Up to 1 000 rows are exported; for larger exports use CSV.
{ const cfg = t.config as { filters?: { includeArchived?: boolean } }; if (cfg.filters?.includeArchived !== undefined) { setIncludeArchived(Boolean(cfg.filters.includeArchived)); } if (t.name) setTitle(t.name); }} />
setTitle(e.target.value)} />
{previewOpen ? ( ) : null} ); }