Files
road-monitoring-ui/src/app/(modules)/segment/components/SegmentDialog.tsx

329 lines
11 KiB
TypeScript

'use client';
import type { ComponentProps } from 'react';
import { useRef } from 'react';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { Loader2 } from 'lucide-react';
import { FormField } from '@/components/form';
import { PackageSelect } from '@/components/lookups/PackageSelect';
import { ProjectSelect } from '@/components/lookups/ProjectSelect';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { SegmentFormValues } from '../hooks/useSegmentForm';
interface SegmentDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
segmentId?: string;
projectId: string;
onProjectChange: (projectId: string) => void;
packageId: string;
onPackageChange: (packageId: string) => void;
direction: 'UP' | 'DOWN';
onDirectionChange: (direction: 'UP' | 'DOWN') => void;
register: UseFormRegister<SegmentFormValues>;
errors: FieldErrors<SegmentFormValues>;
isSubmitted: boolean;
onSubmit: ComponentProps<'form'>['onSubmit'];
canSubmit: boolean;
isSaving: boolean;
}
export function SegmentDialog({
open,
onOpenChange,
segmentId,
projectId,
onProjectChange,
packageId,
onPackageChange,
direction,
onDirectionChange,
register,
errors,
isSubmitted,
onSubmit,
canSubmit,
isSaving,
}: SegmentDialogProps) {
const projectSelectPortalRef = useRef<HTMLDivElement | null>(null);
const packageSelectPortalRef = useRef<HTMLDivElement | null>(null);
const getError = (field: keyof SegmentFormValues) =>
isSubmitted ? errors[field]?.message : undefined;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="flex max-h-[calc(100vh-2rem)] flex-col gap-0 p-0 sm:max-w-2xl"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<DialogHeader className="shrink-0 border-b px-6 py-4 pr-12">
<DialogTitle className="text-lg leading-none font-semibold tracking-tight">
{segmentId ? 'Edit Segment' : 'Create Segment'}
</DialogTitle>
<DialogDescription className="text-sm">
Manage segment binding, chainage values, direction, and coordinates.
</DialogDescription>
</DialogHeader>
<form
noValidate
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, package, segment name, and direction.
</p>
</div>
{!segmentId ? (
<div className="grid gap-4 md:grid-cols-2">
<FormField
label="Project"
required
error={getError('project_id')}
>
<ProjectSelect
value={projectId}
onValueChange={onProjectChange}
enabled={open}
disabled={isSaving}
portalContainer={projectSelectPortalRef}
/>
<input
type="hidden"
{...register('project_id')}
value={projectId}
readOnly
/>
</FormField>
<FormField
label="Package"
required
error={getError('package_id')}
>
<PackageSelect
value={packageId}
onValueChange={onPackageChange}
projectId={projectId}
enabled={open && Boolean(projectId)}
disabled={!projectId || isSaving}
portalContainer={packageSelectPortalRef}
/>
<input
type="hidden"
{...register('package_id')}
value={packageId}
readOnly
/>
</FormField>
</div>
) : null}
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="segment-name"
label="Segment Name"
required
error={getError('segment_name')}
>
<Input
id="segment-name"
placeholder="Enter segment name"
aria-invalid={!!getError('segment_name')}
{...register('segment_name')}
/>
</FormField>
<FormField label="Direction" required>
<Select value={direction} onValueChange={onDirectionChange}>
<SelectTrigger>
<SelectValue placeholder="Select direction" />
</SelectTrigger>
<SelectContent>
<SelectItem value="UP">UP</SelectItem>
<SelectItem value="DOWN">DOWN</SelectItem>
</SelectContent>
</Select>
</FormField>
</div>
</section>
<section className="space-y-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">Chainage</h3>
<p className="text-sm text-muted-foreground">
Required start and end kilometre values.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="segment-start-km"
label="Start Chainage"
required
error={getError('chainage_start_km')}
>
<Input
id="segment-start-km"
type="number"
step="1"
min="0"
placeholder="Enter start chainage"
aria-invalid={!!getError('chainage_start_km')}
{...register('chainage_start_km')}
/>
</FormField>
<FormField
id="segment-end-km"
label="End Chainage"
required
error={getError('chainage_end_km')}
>
<Input
id="segment-end-km"
type="number"
step="1"
min="0"
placeholder="Enter end chainage"
aria-invalid={!!getError('chainage_end_km')}
{...register('chainage_end_km')}
/>
</FormField>
</div>
</section>
<section className="space-y-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold">Coordinates</h3>
<p className="text-sm text-muted-foreground">
Required start and end point latitude and longitude.
</p>
</div>
<div className="space-y-5">
<div className="grid grid-cols-2 gap-4">
<FormField
id="start-lat"
label="Start Latitude"
required
error={getError('start_lat')}
>
<Input
id="start-lat"
type="number"
step="any"
min="-90"
max="90"
placeholder="-90 to 90"
aria-invalid={!!getError('start_lat')}
{...register('start_lat')}
/>
</FormField>
<FormField
id="start-lng"
label="Start Longitude"
required
error={getError('start_lng')}
>
<Input
id="start-lng"
type="number"
step="any"
min="-180"
max="180"
placeholder="-180 to 180"
aria-invalid={!!getError('start_lng')}
{...register('start_lng')}
/>
</FormField>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
id="end-lat"
label="End Latitude"
required
error={getError('end_lat')}
>
<Input
id="end-lat"
type="number"
step="any"
min="-90"
max="90"
placeholder="-90 to 90"
aria-invalid={!!getError('end_lat')}
{...register('end_lat')}
/>
</FormField>
<FormField
id="end-lng"
label="End Longitude"
required
error={getError('end_lng')}
>
<Input
id="end-lng"
type="number"
step="any"
min="-180"
max="180"
placeholder="-180 to 180"
aria-invalid={!!getError('end_lng')}
{...register('end_lng')}
/>
</FormField>
</div>
</div>
</section>
</div>
<DialogFooter className="shrink-0 border-t px-6 py-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
Cancel
</Button>
<Button type="submit" disabled={isSaving || !canSubmit}>
{isSaving ? (
<Loader2 className="mr-2 size-4 animate-spin" />
) : null}
{segmentId ? 'Update Segment' : 'Create Segment'}
</Button>
</DialogFooter>
</form>
<div ref={projectSelectPortalRef} />
<div ref={packageSelectPortalRef} />
</DialogContent>
</Dialog>
);
}