refactor: modularize road management modules

This commit is contained in:
2026-06-17 13:31:25 +05:30
parent 9bb740e446
commit 182d4ac293
41 changed files with 2386 additions and 1894 deletions

View File

@@ -0,0 +1,293 @@
'use client';
import type { ComponentProps } from 'react';
import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form';
import { Loader2, MapPin, Milestone } from 'lucide-react';
import { FormField } from '@/components/form';
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 { Package, Project } from '@/types';
import type { SegmentFormValues } from '../hooks/useSegmentForm';
interface SegmentDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
segmentId?: string;
projects: Project[];
packages: Package[];
isProjectsLoading: boolean;
isPackagesLoading: boolean;
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>;
touchedFields: UseFormReturn<SegmentFormValues>['formState']['touchedFields'];
onSubmit: ComponentProps<'form'>['onSubmit'];
canSubmit: boolean;
isSaving: boolean;
}
export function SegmentDialog({
open,
onOpenChange,
segmentId,
projects,
packages,
isProjectsLoading,
isPackagesLoading,
projectId,
onProjectChange,
packageId,
onPackageChange,
direction,
onDirectionChange,
register,
errors,
touchedFields,
onSubmit,
canSubmit,
isSaving,
}: SegmentDialogProps) {
const getError = (field: keyof SegmentFormValues) =>
touchedFields[field] ? errors[field]?.message : undefined;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-h-[calc(100vh-2rem)] overflow-y-auto sm:max-w-2xl"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<DialogHeader className="gap-2">
<DialogTitle className="flex items-center gap-3 text-xl">
<div className="rounded-lg bg-primary p-2 text-primary-foreground shadow-sm">
<Milestone className="size-5" />
</div>
{segmentId ? 'Edit Segment' : 'Create Segment'}
</DialogTitle>
<DialogDescription className="text-sm">
{segmentId
? 'Update the technical specifications for your road segment.'
: 'Select a project and package, then provide the segment data.'}
</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmit} className="space-y-6">
{!segmentId ? (
<div className="grid gap-5 md:grid-cols-2">
<FormField label="Project" required error={getError('project_id')}>
<Select value={projectId} onValueChange={onProjectChange}>
<SelectTrigger aria-invalid={!!getError('project_id')}>
{isProjectsLoading ? (
<span className="flex items-center gap-2 text-sm 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>
<FormField label="Package" required error={getError('package_id')}>
<Select
value={packageId}
onValueChange={onPackageChange}
disabled={!projectId || isPackagesLoading}
>
<SelectTrigger aria-invalid={!!getError('package_id')}>
{isPackagesLoading ? (
<span className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
Loading...
</span>
) : (
<SelectValue placeholder={projectId ? 'Choose a package' : 'Select project first'} />
)}
</SelectTrigger>
<SelectContent>
{packages.map((pkg) => (
<SelectItem key={pkg.id} value={pkg.id}>
{pkg.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input type="hidden" {...register('package_id')} value={packageId} readOnly />
</FormField>
</div>
) : null}
<div className="grid gap-5 md:grid-cols-2">
<FormField
id="segment-name"
label="Segment Name"
required
error={getError('segment_name')}
>
<Input
id="segment-name"
placeholder="e.g. Mumbai to Pune"
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>
<FormField
id="segment-start-km"
label="Start (km)"
required
error={getError('chainage_start_km')}
>
<Input
id="segment-start-km"
type="number"
step="any"
min="0"
placeholder="0.0"
aria-invalid={!!getError('chainage_start_km')}
{...register('chainage_start_km')}
/>
</FormField>
<FormField
id="segment-end-km"
label="End (km)"
required
error={getError('chainage_end_km')}
>
<Input
id="segment-end-km"
type="number"
step="any"
min="0"
placeholder="1.0"
aria-invalid={!!getError('chainage_end_km')}
{...register('chainage_end_km')}
/>
</FormField>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-4">
<p className="flex items-center gap-2 text-[11px] font-black tracking-widest text-muted-foreground">
<MapPin className="size-3.5 opacity-60" />
START COORDINATES
</p>
<div className="grid grid-cols-2 gap-4">
<FormField id="start-lat" label="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="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>
<div className="space-y-4">
<p className="flex items-center gap-2 text-[11px] font-black tracking-widest text-muted-foreground">
<MapPin className="size-3.5 opacity-60" />
END COORDINATES
</p>
<div className="grid grid-cols-2 gap-4">
<FormField id="end-lat" label="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="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>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
Cancel
</Button>
<Button type="submit" disabled={isSaving || !canSubmit}>
{isSaving ? <Loader2 className="size-4 animate-spin" /> : null}
{segmentId ? 'Update Segment' : 'Create Segment'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}