"use client" /** * ExportDrawer — floating right-side drawer with export form. * * Built on {@link FloatingSheetPanel} — see `components/ui/floating-sheet-panel.tsx` * for the shared hub-adjacent sheet shell. * * Form fields (shadcn Form + react-hook-form + zod): * • File format CSV · Excel · PDF (SelectionTileGrid radio) * • Date range From / To (FilterTextValueInput dateMDY — same as Settings / table filters) * • Columns All · Visible only (radio) * • Apply active filters (checkbox) * * WCAG 2.1 AA: * ✓ All inputs labelled via FormLabel linked to control id (1.3.1) * ✓ Error messages linked via aria-describedby (3.3.1) * ✓ Focus returns to trigger on close (2.4.3) */ import * as React from "react" import { useForm } from "react-hook-form" import { z } from "zod" import { zodResolver } from "@hookform/resolvers/zod" import { devLog } from "../../lib/dev-log" import { Checkbox } from "./checkbox" import { Label } from "./label" import { RadioGroup, RadioGroupItem, RadioGroupLabel } from "./radio-group" import { SelectionTileGrid } from "./selection-tile-grid" import { FilterTextValueInput } from "../data-table/filter-text-value-input" import type { SelectionTileOption } from "./selection-tile-grid" import { FloatingSheetPanel, FloatingSheetPanelBody, FloatingSheetPanelContent, FloatingSheetPanelHeader, FloatingSheetPanelToolbar, FloatingSheetPanelWorkflowFooter, } from "./floating-sheet-panel" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "./form" /** Parse full MM/DD/YYYY from masked input; partial/invalid → undefined. */ function parseMdyToDate(raw: string | undefined): Date | undefined { if (!raw?.trim()) return undefined const m = raw.trim().match(/^(\d{2})\/(\d{2})\/(\d{4})$/) if (!m) return undefined const month = Number(m[1]) const day = Number(m[2]) const year = Number(m[3]) if (month < 1 || month > 12 || day < 1 || day > 31) return undefined const d = new Date(year, month - 1, day, 12, 0, 0, 0) if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day) return undefined return d } // ── Validation schema ───────────────────────────────────────────────────────── const EXPORT_FORMAT_OPTIONS: SelectionTileOption<"csv" | "excel" | "pdf">[] = [ { value: "csv", label: "CSV", icon: "fa-file-csv" }, { value: "excel", label: "Excel", icon: "fa-file-excel" }, { value: "pdf", label: "PDF", icon: "fa-file-pdf" }, ] const exportSchema = z .object({ format: z.enum(["csv", "excel", "pdf"]), columns: z.enum(["all", "visible"]), dateFrom: z.string().optional(), dateTo: z.string().optional(), includeFilters: z.boolean(), }) .superRefine((data, ctx) => { const from = parseMdyToDate(data.dateFrom) const to = parseMdyToDate(data.dateTo) if (data.dateFrom?.trim() && !from) { ctx.addIssue({ code: "custom", message: "Enter a valid from date", path: ["dateFrom"] }) } if (data.dateTo?.trim() && !to) { ctx.addIssue({ code: "custom", message: "Enter a valid to date", path: ["dateTo"] }) } if (from && to && to < from) { ctx.addIssue({ code: "custom", message: "End date must be after start date", path: ["dateTo"] }) } }) type ExportForm = z.infer // ── Component ───────────────────────────────────────────────────────────────── export interface ExportDrawerProps { open: boolean onOpenChange: (open: boolean) => void totalRows?: number visibleColumns?: number } export function ExportDrawer({ open, onOpenChange, totalRows = 0, visibleColumns, }: ExportDrawerProps) { const form = useForm({ resolver: zodResolver(exportSchema), defaultValues: { format: "csv", columns: "visible", dateFrom: "", dateTo: "", includeFilters: true, }, }) const [isExporting, setIsExporting] = React.useState(false) async function onSubmit(values: ExportForm) { setIsExporting(true) await new Promise(r => setTimeout(r, 1200)) devLog("Export:", { ...values, dateFrom: parseMdyToDate(values.dateFrom)?.toISOString(), dateTo: parseMdyToDate(values.dateTo)?.toISOString(), }) setIsExporting(false) onOpenChange(false) form.reset() } return (
{/* File format */} ( )} /> {/* Date range */}
Date range{" "} (optional)
( From MM/DD/YYYY )} /> ( To MM/DD/YYYY )} />
{/* Columns */} ( Columns {([ { value: "all", label: "All columns", sub: null }, { value: "visible", label: "Visible columns only", sub: visibleColumns !== undefined ? `${visibleColumns} columns` : null }, ] as const).map(opt => (
{opt.label} {opt.sub && ( ({opt.sub}) )}
))}
)} /> {/* Include filters */} (

Export only rows matching current filters

)} />
onOpenChange(false)} primaryForm="export-form" primaryLabel="Export" onPrimary={() => form.handleSubmit(onSubmit)()} primaryDisabled={isExporting} primaryLoading={isExporting} primaryLoadingLabel="Exporting…" />
) }