From 0c6e7b72af9d1ebdbf2d84c1ac52fdbde3419ef1 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 May 2026 20:14:33 +0200 Subject: [PATCH] feat(forms): migrate remaining native date inputs to / MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the last ~17 native `` call sites onto the shared `` / `` primitives so date entry is uniform across the app (calendar popover on desktop, native OS picker on mobile via the primitive's viewport-aware fallback). Three patterns handled: 1. Controlled value/onChange — direct swap to : audit-log-list.tsx (audit-from / audit-to filters) reports/generate-report-form.tsx (date range) scan/scan-shell.tsx (expense date) reservations/reservation-detail.tsx (end-reservation dialog) shared/filter-bar.tsx ('date' filter variant) 2. RHF `register('field')` pattern — wrapped in with field.value/field.onChange bridge. The picker's '' → undefined normalisation kicks in via `field.onChange(v || undefined)`: berths/berth-form.tsx (tenureStartDate + tenureEndDate) reservations/berth-reserve-dialog.tsx (startDate) companies/add-membership-dialog.tsx (startDate) yachts/yacht-transfer-dialog.tsx (effectiveDate) invoices/invoice-detail.tsx (paymentDate) 3. RHF + Date-typed schema — same Controller wrap, plus a Date<->YYYY-MM-DD bridge in the render() since the zod schema coerces these to Date: expenses/expense-form-dialog.tsx (expenseDate) companies/company-form.tsx (incorporationDate) 4. Datetime variants — swapped onto : interests/interest-contact-log-tab.tsx (occurredAt + followUpAt) Skipped because they ARE picker primitives or internal date variants: - ui/date-picker.tsx, ui/date-time-picker.tsx (the primitives) - shared/inline-editable-field.tsx (the InlineEditableField date variant) - dashboard/date-range-picker.tsx (its own popover with min/max gating that doesn't map cleanly onto the shared primitive) Removed now-unused Input imports from four files. Verified: tsc clean, vitest 1448/1448. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/admin/audit/audit-log-list.tsx | 14 +++----- src/components/berths/berth-form.tsx | 26 ++++++++++++-- .../companies/add-membership-dialog.tsx | 13 +++++-- src/components/companies/company-form.tsx | 26 ++++++++++++-- .../expenses/expense-form-dialog.tsx | 36 ++++++++++++++----- .../interests/interest-contact-log-tab.tsx | 14 +++----- src/components/invoices/invoice-detail.tsx | 17 ++++++--- .../reports/generate-report-form.tsx | 14 +++----- .../reservations/berth-reserve-dialog.tsx | 14 ++++++-- .../reservations/reservation-detail.tsx | 10 ++---- src/components/scan/scan-shell.tsx | 9 ++--- src/components/shared/filter-bar.tsx | 6 ++-- .../yachts/yacht-transfer-dialog.tsx | 20 +++++++---- 13 files changed, 141 insertions(+), 78 deletions(-) diff --git a/src/components/admin/audit/audit-log-list.tsx b/src/components/admin/audit/audit-log-list.tsx index 6602f41a..4a316706 100644 --- a/src/components/admin/audit/audit-log-list.tsx +++ b/src/components/admin/audit/audit-log-list.tsx @@ -12,6 +12,7 @@ import { PageHeader } from '@/components/shared/page-header'; import { EmptyState } from '@/components/shared/empty-state'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; +import { DatePicker } from '@/components/ui/date-picker'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { @@ -530,12 +531,11 @@ export function AuditLogList() { - setDateFrom(e.target.value)} + onChange={setDateFrom} /> @@ -543,13 +543,7 @@ export function AuditLogList() { - setDateTo(e.target.value)} - /> + {/* M-AU03: CSV export inherits the current filter set. The diff --git a/src/components/berths/berth-form.tsx b/src/components/berths/berth-form.tsx index 5a240f33..fe878b50 100644 --- a/src/components/berths/berth-form.tsx +++ b/src/components/berths/berth-form.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; @@ -9,6 +9,7 @@ import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { DatePicker } from '@/components/ui/date-picker'; import { Select, SelectContent, @@ -120,6 +121,7 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) { handleSubmit, setValue, watch, + control, formState: { isSubmitting }, } = useForm, unknown, UpdateBerthInput>({ resolver: zodResolver(updateBerthSchema), @@ -449,11 +451,29 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
- + ( + field.onChange(v || undefined)} + /> + )} + />
- + ( + field.onChange(v || undefined)} + /> + )} + />
)} diff --git a/src/components/companies/add-membership-dialog.tsx b/src/components/companies/add-membership-dialog.tsx index dc3def88..ec960d15 100644 --- a/src/components/companies/add-membership-dialog.tsx +++ b/src/components/companies/add-membership-dialog.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2 } from 'lucide-react'; @@ -17,6 +17,7 @@ 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 { DatePicker } from '@/components/ui/date-picker'; import { Select, SelectContent, @@ -69,6 +70,7 @@ export function AddMembershipDialog({ open, onOpenChange, companyId }: AddMember watch, setValue, reset, + control, formState: { errors, isSubmitting }, } = useForm({ defaultValues: { @@ -180,7 +182,14 @@ export function AddMembershipDialog({ open, onOpenChange, companyId }: AddMember
- + ( + + )} + /> {errors.startDate &&

Required

}
diff --git a/src/components/companies/company-form.tsx b/src/components/companies/company-form.tsx index 71f9974c..7b20d2cd 100644 --- a/src/components/companies/company-form.tsx +++ b/src/components/companies/company-form.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useMemo, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; @@ -11,6 +11,7 @@ import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { DatePicker } from '@/components/ui/date-picker'; import { Textarea } from '@/components/ui/textarea'; import { Select, @@ -117,6 +118,7 @@ export function CompanyForm({ open, onOpenChange, company, prefill }: CompanyFor watch, setValue, reset, + control, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(createCompanySchema), @@ -327,7 +329,27 @@ export function CompanyForm({ open, onOpenChange, company, prefill }: CompanyFor
- + { + // Schema coerces incorporationDate to a Date; the picker + // speaks YYYY-MM-DD. Bridge both directions so validation + // + downstream API payload stay unchanged. + const isoValue = + field.value instanceof Date + ? field.value.toISOString().split('T')[0] + : typeof field.value === 'string' + ? (field.value as string).split('T')[0] + : ''; + return ( + field.onChange(v ? new Date(v) : undefined)} + /> + ); + }} + /> {errors.incorporationDate && (

{errors.incorporationDate.message}

)} diff --git a/src/components/expenses/expense-form-dialog.tsx b/src/components/expenses/expense-form-dialog.tsx index 9e801a93..adc744b2 100644 --- a/src/components/expenses/expense-form-dialog.tsx +++ b/src/components/expenses/expense-form-dialog.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { AlertTriangle, Loader2, Upload, X } from 'lucide-react'; @@ -10,6 +10,7 @@ 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 { DatePicker } from '@/components/ui/date-picker'; import { Textarea } from '@/components/ui/textarea'; import { FormErrorSummary } from '@/components/forms/form-error-summary'; import { useFormScrollToError } from '@/hooks/use-form-scroll-to-error'; @@ -69,6 +70,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi setValue, reset, watch, + control, formState: { errors, isSubmitting }, } = useForm, unknown, CreateExpenseInput>({ resolver: zodResolver(createExpenseSchema), @@ -230,17 +232,33 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi />
- (v ? new Date(v) : undefined), - })} + { + // Schema stores expenseDate as a Date; the picker speaks + // YYYY-MM-DD strings. Bridge both directions on the fly so + // upstream validation + downstream API payload stay + // unchanged. + const isoValue = + field.value instanceof Date + ? field.value.toISOString().split('T')[0] + : typeof field.value === 'string' + ? field.value.split('T')[0] + : ''; + return ( + field.onChange(v ? new Date(v) : undefined)} + /> + ); + }} /> {errors.expenseDate && (

{errors.expenseDate.message}

diff --git a/src/components/interests/interest-contact-log-tab.tsx b/src/components/interests/interest-contact-log-tab.tsx index 91908dcb..1f6134e2 100644 --- a/src/components/interests/interest-contact-log-tab.tsx +++ b/src/components/interests/interest-contact-log-tab.tsx @@ -42,7 +42,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { Input } from '@/components/ui/input'; +import { DateTimePicker } from '@/components/ui/date-time-picker'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { @@ -497,12 +497,7 @@ function ComposeDialogBody({
- setOccurredAt(e.target.value)} - /> +
@@ -588,11 +583,10 @@ function ComposeDialogBody({ - setFollowUpAt(e.target.value)} + onChange={setFollowUpAt} className="max-w-xs" />

diff --git a/src/components/invoices/invoice-detail.tsx b/src/components/invoices/invoice-detail.tsx index b8b5611e..dba5d4a3 100644 --- a/src/components/invoices/invoice-detail.tsx +++ b/src/components/invoices/invoice-detail.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2, Send, CreditCard } from 'lucide-react'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { format } from 'date-fns'; @@ -15,6 +15,7 @@ import { PermissionGate } from '@/components/shared/permission-gate'; import { toast } from 'sonner'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; +import { DatePicker } from '@/components/ui/date-picker'; import { Select, SelectContent, @@ -397,10 +398,16 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) { >

- ( + field.onChange(v || undefined)} + /> + )} /> {paymentForm.formState.errors.paymentDate && (

diff --git a/src/components/reports/generate-report-form.tsx b/src/components/reports/generate-report-form.tsx index d4673242..0c75d04c 100644 --- a/src/components/reports/generate-report-form.tsx +++ b/src/components/reports/generate-report-form.tsx @@ -6,6 +6,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { DatePicker } from '@/components/ui/date-picker'; import { Select, SelectContent, @@ -148,23 +149,16 @@ export function GenerateReportForm() {

- setDateFrom(e.target.value)} + onChange={setDateFrom} className="w-auto" />
- setDateTo(e.target.value)} - className="w-auto" - /> +
diff --git a/src/components/reservations/berth-reserve-dialog.tsx b/src/components/reservations/berth-reserve-dialog.tsx index 1fa0b7f0..7fea5f51 100644 --- a/src/components/reservations/berth-reserve-dialog.tsx +++ b/src/components/reservations/berth-reserve-dialog.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; @@ -16,7 +16,7 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; -import { Input } from '@/components/ui/input'; +import { DatePicker } from '@/components/ui/date-picker'; import { Textarea } from '@/components/ui/textarea'; import { Select, @@ -55,6 +55,7 @@ export function BerthReserveDialog({ open, onOpenChange, berthId }: BerthReserve watch, setValue, reset, + control, formState: { errors, isSubmitting }, } = useForm({ defaultValues: { @@ -186,7 +187,14 @@ export function BerthReserveDialog({ open, onOpenChange, berthId }: BerthReserve
- + ( + + )} + /> {errors.startDate &&

Required

}
diff --git a/src/components/reservations/reservation-detail.tsx b/src/components/reservations/reservation-detail.tsx index 06bb0f3c..3fdfcf77 100644 --- a/src/components/reservations/reservation-detail.tsx +++ b/src/components/reservations/reservation-detail.tsx @@ -15,8 +15,8 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { DatePicker } from '@/components/ui/date-picker'; import { PageHeader } from '@/components/shared/page-header'; import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { EmptyState } from '@/components/ui/empty-state'; @@ -96,13 +96,7 @@ function EndReservationDialog({ reservationId, open, onOpenChange }: EndReservat
- setEndDate(e.target.value)} - required - /> +
- setExpenseDate(e.target.value)} - required - /> +
diff --git a/src/components/shared/filter-bar.tsx b/src/components/shared/filter-bar.tsx index c3188b20..198b087e 100644 --- a/src/components/shared/filter-bar.tsx +++ b/src/components/shared/filter-bar.tsx @@ -4,6 +4,7 @@ import { X, Filter, ChevronDown } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { DatePicker } from '@/components/ui/date-picker'; import { Badge } from '@/components/ui/badge'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { @@ -272,10 +273,9 @@ function FilterField({ return (
- onChange(e.target.value || undefined)} + onChange={(v) => onChange(v || undefined)} className="h-8" />
diff --git a/src/components/yachts/yacht-transfer-dialog.tsx b/src/components/yachts/yacht-transfer-dialog.tsx index 3234d12b..242aec12 100644 --- a/src/components/yachts/yacht-transfer-dialog.tsx +++ b/src/components/yachts/yacht-transfer-dialog.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; @@ -16,8 +16,8 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; -import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; +import { DatePicker } from '@/components/ui/date-picker'; import { Select, SelectContent, @@ -62,6 +62,7 @@ export function YachtTransferDialog({ watch, setValue, reset, + control, formState: { errors, isSubmitting }, } = useForm({ defaultValues: { @@ -150,10 +151,17 @@ export function YachtTransferDialog({
- ( + + )} /> {errors.effectiveDate &&

Required

}