feat(forms): migrate remaining native date inputs to <DatePicker> / <DateTimePicker>

Sweeps the last ~17 native `<Input type="date"|"datetime-local">`
call sites onto the shared `<DatePicker>` / `<DateTimePicker>`
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 <DatePicker
     value/onChange>:
       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 <Controller> 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 <DateTimePicker>:
       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) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 20:14:33 +02:00
parent f0dbefcac2
commit 0c6e7b72af
13 changed files with 141 additions and 78 deletions

View File

@@ -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() {
<Label htmlFor="audit-from" className="text-xs">
From
</Label>
<Input
<DatePicker
id="audit-from"
type="date"
className="w-44 h-9"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
onChange={setDateFrom}
/>
</div>
@@ -543,13 +543,7 @@ export function AuditLogList() {
<Label htmlFor="audit-to" className="text-xs">
To
</Label>
<Input
id="audit-to"
type="date"
className="w-44 h-9"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
/>
<DatePicker id="audit-to" className="w-44 h-9" value={dateTo} onChange={setDateTo} />
</div>
{/* M-AU03: CSV export inherits the current filter set. The

View File

@@ -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<z.input<typeof updateBerthSchema>, unknown, UpdateBerthInput>({
resolver: zodResolver(updateBerthSchema),
@@ -449,11 +451,29 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
</div>
<div className="space-y-2">
<Label>Start Date</Label>
<Input type="date" {...register('tenureStartDate')} />
<Controller
control={control}
name="tenureStartDate"
render={({ field }) => (
<DatePicker
value={field.value ?? ''}
onChange={(v) => field.onChange(v || undefined)}
/>
)}
/>
</div>
<div className="space-y-2">
<Label>End Date</Label>
<Input type="date" {...register('tenureEndDate')} />
<Controller
control={control}
name="tenureEndDate"
render={({ field }) => (
<DatePicker
value={field.value ?? ''}
onChange={(v) => field.onChange(v || undefined)}
/>
)}
/>
</div>
</div>
)}

View File

@@ -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<FormValues>({
defaultValues: {
@@ -180,7 +182,14 @@ export function AddMembershipDialog({ open, onOpenChange, companyId }: AddMember
<div className="space-y-2">
<Label htmlFor="startDate">Start date</Label>
<Input id="startDate" type="date" {...register('startDate', { required: true })} />
<Controller
control={control}
name="startDate"
rules={{ required: true }}
render={({ field }) => (
<DatePicker id="startDate" value={field.value ?? ''} onChange={field.onChange} />
)}
/>
{errors.startDate && <p className="text-xs text-destructive">Required</p>}
</div>

View File

@@ -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<CompanyFormValues>({
resolver: zodResolver(createCompanySchema),
@@ -327,7 +329,27 @@ export function CompanyForm({ open, onOpenChange, company, prefill }: CompanyFor
</div>
<div className="space-y-1">
<Label>Incorporation Date</Label>
<Input type="date" {...register('incorporationDate')} />
<Controller
control={control}
name="incorporationDate"
render={({ field }) => {
// 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 (
<DatePicker
value={isoValue}
onChange={(v) => field.onChange(v ? new Date(v) : undefined)}
/>
);
}}
/>
{errors.incorporationDate && (
<p className="text-xs text-destructive">{errors.incorporationDate.message}</p>
)}

View File

@@ -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<z.input<typeof createExpenseSchema>, unknown, CreateExpenseInput>({
resolver: zodResolver(createExpenseSchema),
@@ -230,17 +232,33 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
/>
<div className="space-y-2">
<Label htmlFor="expenseDate">Date *</Label>
<Input
id="expenseDate"
type="date"
{...register('expenseDate', {
setValueAs: (v) => (v ? new Date(v) : undefined),
})}
<Controller
control={control}
name="expenseDate"
defaultValue={
expense?.expenseDate
? new Date(expense.expenseDate).toISOString().split('T')[0]
: new Date().toISOString().split('T')[0]
? (new Date(expense.expenseDate) as unknown as Date)
: (new Date() as unknown as Date)
}
render={({ field }) => {
// 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 (
<DatePicker
id="expenseDate"
value={isoValue}
onChange={(v) => field.onChange(v ? new Date(v) : undefined)}
/>
);
}}
/>
{errors.expenseDate && (
<p className="text-xs text-destructive">{errors.expenseDate.message}</p>

View File

@@ -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({
<div className="space-y-1">
<Label htmlFor="cl-occurred">When did the conversation happen?</Label>
<Input
id="cl-occurred"
type="datetime-local"
value={occurredAt}
onChange={(e) => setOccurredAt(e.target.value)}
/>
<DateTimePicker id="cl-occurred" value={occurredAt} onChange={setOccurredAt} />
</div>
<div className="space-y-1">
@@ -588,11 +583,10 @@ function ComposeDialogBody({
<Label htmlFor="cl-followup" className="text-xs text-muted-foreground">
Remind me on
</Label>
<Input
<DateTimePicker
id="cl-followup"
type="datetime-local"
value={followUpAt}
onChange={(e) => setFollowUpAt(e.target.value)}
onChange={setFollowUpAt}
className="max-w-xs"
/>
<p className="text-[11px] text-muted-foreground">

View File

@@ -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) {
>
<div className="space-y-1">
<Label htmlFor="paymentDate">Payment Date</Label>
<Input
<Controller
control={paymentForm.control}
name="paymentDate"
render={({ field }) => (
<DatePicker
id="paymentDate"
type="date"
{...paymentForm.register('paymentDate')}
value={field.value ?? ''}
onChange={(v) => field.onChange(v || undefined)}
/>
)}
/>
{paymentForm.formState.errors.paymentDate && (
<p className="text-xs text-destructive">

View File

@@ -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() {
<div className="flex flex-wrap gap-4">
<div className="grid gap-2">
<Label htmlFor="dateFrom">Date From (optional)</Label>
<Input
<DatePicker
id="dateFrom"
type="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
onChange={setDateFrom}
className="w-auto"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dateTo">Date To (optional)</Label>
<Input
id="dateTo"
type="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="w-auto"
/>
<DatePicker id="dateTo" value={dateTo} onChange={setDateTo} className="w-auto" />
</div>
</div>

View File

@@ -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<FormValues>({
defaultValues: {
@@ -186,7 +187,14 @@ export function BerthReserveDialog({ open, onOpenChange, berthId }: BerthReserve
<div className="space-y-2">
<Label htmlFor="startDate">Start date</Label>
<Input id="startDate" type="date" {...register('startDate', { required: true })} />
<Controller
control={control}
name="startDate"
rules={{ required: true }}
render={({ field }) => (
<DatePicker id="startDate" value={field.value ?? ''} onChange={field.onChange} />
)}
/>
{errors.startDate && <p className="text-xs text-destructive">Required</p>}
</div>

View File

@@ -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
<form onSubmit={handleSubmit} className="space-y-4 pt-2">
<div className="space-y-1.5">
<Label htmlFor="end-date">End date</Label>
<Input
id="end-date"
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
required
/>
<DatePicker id="end-date" value={endDate} onChange={setEndDate} />
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>

View File

@@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { DatePicker } from '@/components/ui/date-picker';
import {
Select,
SelectContent,
@@ -239,13 +240,7 @@ function VerifyForm({
</div>
<div className="space-y-1.5">
<Label htmlFor="expenseDate">Date</Label>
<Input
id="expenseDate"
type="date"
value={expenseDate}
onChange={(e) => setExpenseDate(e.target.value)}
required
/>
<DatePicker id="expenseDate" value={expenseDate} onChange={setExpenseDate} />
</div>
<div className="space-y-1.5">
<Label htmlFor="category">Category</Label>

View File

@@ -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 (
<div className="space-y-1">
<Label className="text-xs">{definition.label}</Label>
<Input
type="date"
<DatePicker
value={(value as string) ?? ''}
onChange={(e) => onChange(e.target.value || undefined)}
onChange={(v) => onChange(v || undefined)}
className="h-8"
/>
</div>

View File

@@ -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<FormValues>({
defaultValues: {
@@ -150,10 +151,17 @@ export function YachtTransferDialog({
<div className="space-y-2">
<Label htmlFor="effectiveDate">Effective date</Label>
<Input
<Controller
control={control}
name="effectiveDate"
rules={{ required: true }}
render={({ field }) => (
<DatePicker
id="effectiveDate"
type="date"
{...register('effectiveDate', { required: true })}
value={field.value ?? ''}
onChange={field.onChange}
/>
)}
/>
{errors.effectiveDate && <p className="text-xs text-destructive">Required</p>}
</div>