refactor: Align form validation UX across management dialogs

This commit is contained in:
2026-06-18 11:05:31 +05:30
parent d7c4027328
commit 9695c80e20
23 changed files with 1458 additions and 1017 deletions

View File

@@ -2,7 +2,7 @@
import type { ComponentProps } from 'react';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { Globe, Loader2, Package as PackageIcon } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { FormField } from '@/components/form';
import { Button } from '@/components/ui/button';
@@ -71,117 +71,126 @@ export function PackageDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-2xl"
className="flex max-h-[calc(100vh-2rem)] flex-col gap-0 p-0 sm:max-w-2xl"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<DialogHeader className="gap-2">
<DialogTitle className="flex items-center gap-3">
<div className="rounded-lg bg-primary p-2 text-primary-foreground shadow-sm">
<PackageIcon className="size-5" />
</div>
{packageId ? 'Edit Package Details' : 'Create New Package'}
<DialogHeader className="shrink-0 border-b px-6 py-4 pr-12">
<DialogTitle className="text-lg leading-none font-semibold tracking-tight">
{packageId ? 'Edit Package' : 'Create Package'}
</DialogTitle>
<DialogDescription>
{packageId
? 'Update the technical specifications for your road infrastructure package.'
: 'Select a project and provide the essential data to establish a new package.'}
<DialogDescription className="text-sm">
Manage package details and optional chainage range.
</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmit} className="space-y-6">
{!packageId ? (
<FormField label="Project" required error={projectErrorMessage}>
<Select value={projectId} onValueChange={onProjectChange}>
<SelectTrigger aria-invalid={!!projectErrorMessage}>
{isProjectsLoading ? (
<span className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
Loading...
</span>
) : (
<SelectValue placeholder="Choose a project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input
type="hidden"
{...register('project_id')}
value={projectId}
readOnly
/>
</FormField>
) : null}
<form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
<div className="max-h-[58vh] space-y-6 overflow-y-auto px-6 py-4">
<section className="space-y-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">Basic details</h3>
<p className="text-sm text-muted-foreground">
Project binding, package name, and region.
</p>
</div>
<div className="grid gap-5 md:grid-cols-2">
<FormField
id="package-name"
label="Package Name"
required
error={nameErrorMessage}
>
<Input
id="package-name"
placeholder="e.g. Package 01"
aria-invalid={!!nameErrorMessage}
{...register('name')}
/>
</FormField>
{!packageId ? (
<FormField label="Project" required error={projectErrorMessage}>
<Select value={projectId} onValueChange={onProjectChange}>
<SelectTrigger aria-invalid={!!projectErrorMessage}>
{isProjectsLoading ? (
<span className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
Loading...
</span>
) : (
<SelectValue placeholder="Choose a project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input
type="hidden"
{...register('project_id')}
value={projectId}
readOnly
/>
</FormField>
) : null}
<FormField
id="package-region"
label={
<span className="inline-flex items-center gap-2">
<Globe className="size-3.5 opacity-60" />
Region
</span>
}
>
<Input
id="package-region"
placeholder="e.g. North Zone"
{...register('region')}
/>
</FormField>
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="package-name"
label="Package Name"
required
error={nameErrorMessage}
>
<Input
id="package-name"
placeholder="Enter package name"
aria-invalid={!!nameErrorMessage}
{...register('name')}
/>
</FormField>
<FormField
id="package-start"
label="Segment Start (km)"
error={startErrorMessage}
>
<Input
id="package-start"
type="number"
step="0.01"
placeholder="0.00"
aria-invalid={!!startErrorMessage}
{...register('chainage_start_km')}
/>
</FormField>
<FormField id="package-region" label="Region">
<Input
id="package-region"
placeholder="North Zone"
{...register('region')}
/>
</FormField>
</div>
</section>
<FormField
id="package-end"
label="Segment End (km)"
error={endErrorMessage}
>
<Input
id="package-end"
type="number"
step="0.01"
placeholder="0.00"
aria-invalid={!!endErrorMessage}
{...register('chainage_end_km')}
/>
</FormField>
<section className="space-y-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">Chainage range</h3>
<p className="text-sm text-muted-foreground">
Optional start and end kilometre values.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="package-start"
label="Start (km)"
error={startErrorMessage}
>
<Input
id="package-start"
type="number"
step="0.01"
placeholder="0.00"
aria-invalid={!!startErrorMessage}
{...register('chainage_start_km')}
/>
</FormField>
<FormField
id="package-end"
label="End (km)"
error={endErrorMessage}
>
<Input
id="package-end"
type="number"
step="0.01"
placeholder="0.00"
aria-invalid={!!endErrorMessage}
{...register('chainage_end_km')}
/>
</FormField>
</div>
</section>
</div>
<DialogFooter>
<DialogFooter className="shrink-0 border-t px-6 py-4">
<Button
type="button"
variant="outline"
@@ -191,7 +200,7 @@ export function PackageDialog({
Cancel
</Button>
<Button type="submit" disabled={isSaving || !canSubmit}>
{isSaving ? <Loader2 className="size-4 animate-spin" /> : null}
{isSaving ? <Loader2 className="mr-2 size-4 animate-spin" /> : null}
{packageId ? 'Update Package' : 'Create Package'}
</Button>
</DialogFooter>