Files
road-monitoring-ui/src/app/(modules)/segment/hooks/useSegmentForm.ts

208 lines
6.4 KiB
TypeScript

'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
import { chainageService } from '@/services/api';
import type { Chainage, ChainageCreate, ChainageUpdate } from '@/types';
import { segmentKeys } from '../queries/segmentKeys';
const requiredNumber = (message: string) =>
z
.string()
.trim()
.min(1, message)
.refine((value) => Number.isFinite(Number(value)), { message: 'Enter a valid number' });
const latitude = requiredNumber('Latitude is required').refine(
(value) => {
const numberValue = Number(value);
return numberValue >= -90 && numberValue <= 90;
},
{ message: 'Latitude must be between -90 and 90' },
);
const longitude = requiredNumber('Longitude is required').refine(
(value) => {
const numberValue = Number(value);
return numberValue >= -180 && numberValue <= 180;
},
{ message: 'Longitude must be between -180 and 180' },
);
const segmentFormSchema = z.object({
id: z.string().optional(),
project_id: z.string().trim().min(1, 'Project is required'),
package_id: z.string().trim().min(1, 'Package is required'),
segment_name: z.string().trim().min(1, 'Segment name is required'),
chainage_start_km: requiredNumber('Start km is required'),
chainage_end_km: requiredNumber('End km is required'),
start_lat: latitude,
start_lng: longitude,
end_lat: latitude,
end_lng: longitude,
direction: z.enum(['UP', 'DOWN']),
});
export type SegmentFormValues = z.infer<typeof segmentFormSchema>;
const defaultValues: SegmentFormValues = {
project_id: '',
package_id: '',
segment_name: '',
chainage_start_km: '',
chainage_end_km: '',
start_lat: '',
start_lng: '',
end_lat: '',
end_lng: '',
direction: 'UP',
};
function toSegmentPayload(values: SegmentFormValues): ChainageCreate | ChainageUpdate {
return {
package_id: values.package_id,
segment_name: values.segment_name.trim(),
chainage_start_km: Number(values.chainage_start_km),
chainage_end_km: Number(values.chainage_end_km),
start_lat: Number(values.start_lat),
start_lng: Number(values.start_lng),
end_lat: Number(values.end_lat),
end_lng: Number(values.end_lng),
direction: values.direction,
};
}
export function useSegmentForm({ onSaved }: { onSaved: () => void }) {
const queryClient = useQueryClient();
const {
register,
handleSubmit,
reset,
setValue,
control,
formState: { errors, isSubmitting, touchedFields },
} = useForm<SegmentFormValues>({
defaultValues,
mode: 'onTouched',
reValidateMode: 'onChange',
resolver: zodResolver(segmentFormSchema),
});
const segmentId = useWatch({ control, name: 'id' });
const projectId = useWatch({ control, name: 'project_id' }) || '';
const packageId = useWatch({ control, name: 'package_id' }) || '';
const direction = useWatch({ control, name: 'direction' }) || 'UP';
const segmentName = useWatch({ control, name: 'segment_name' }) || '';
const chainageStartKm = useWatch({ control, name: 'chainage_start_km' }) || '';
const chainageEndKm = useWatch({ control, name: 'chainage_end_km' }) || '';
const startLat = useWatch({ control, name: 'start_lat' }) || '';
const startLng = useWatch({ control, name: 'start_lng' }) || '';
const endLat = useWatch({ control, name: 'end_lat' }) || '';
const endLng = useWatch({ control, name: 'end_lng' }) || '';
const canSubmit =
projectId.trim().length > 0 &&
packageId.trim().length > 0 &&
segmentName.trim().length > 0 &&
chainageStartKm.trim().length > 0 &&
chainageEndKm.trim().length > 0 &&
startLat.trim().length > 0 &&
startLng.trim().length > 0 &&
endLat.trim().length > 0 &&
endLng.trim().length > 0;
const saveMutation = useMutation({
mutationFn: (values: SegmentFormValues) => {
const payload = toSegmentPayload(values);
return values.id
? chainageService.updateChainage(values.id, payload)
: chainageService.createChainage(payload as ChainageCreate);
},
onSuccess: (_data, values) => {
toast.success(values.id ? 'Segment updated' : 'Segment created');
reset(defaultValues);
onSaved();
queryClient.invalidateQueries({ queryKey: segmentKeys.all });
},
onError: (_error, values) => {
toast.error(values.id ? 'Failed to update segment' : 'Failed to create segment');
},
});
const onSubmit = handleSubmit(
(values) => saveMutation.mutate(values),
(formErrors) => {
const firstMessage = Object.values(formErrors).find((error) => error?.message)?.message;
if (firstMessage) {
toast.error(String(firstMessage));
}
},
);
const openCreate = useCallback(() => {
reset(defaultValues);
}, [reset]);
const openEdit = useCallback(
(segment: Chainage, projectIdForSegment: string) => {
reset({
id: segment.id,
project_id: projectIdForSegment,
package_id: segment.package_id,
segment_name: segment.segment_name || '',
chainage_start_km: segment.chainage_start_km?.toString() || '',
chainage_end_km: segment.chainage_end_km?.toString() || '',
start_lat: segment.start_lat?.toString() || '',
start_lng: segment.start_lng?.toString() || '',
end_lat: segment.end_lat?.toString() || '',
end_lng: segment.end_lng?.toString() || '',
direction: segment.direction || 'UP',
});
},
[reset],
);
const setProjectId = useCallback(
(value: string) => {
setValue('project_id', value, { shouldDirty: true, shouldValidate: true });
setValue('package_id', '', { shouldDirty: true, shouldValidate: true });
},
[setValue],
);
const setPackageId = useCallback(
(value: string) => setValue('package_id', value, { shouldDirty: true, shouldValidate: true }),
[setValue],
);
const setDirection = useCallback(
(value: 'UP' | 'DOWN') =>
setValue('direction', value, { shouldDirty: true, shouldValidate: true }),
[setValue],
);
return {
register,
onSubmit,
openCreate,
openEdit,
resetForm: () => reset(defaultValues),
segmentId,
projectId,
setProjectId,
packageId,
setPackageId,
direction,
setDirection,
errors,
touchedFields,
canSubmit,
isSaving: isSubmitting || saveMutation.isPending,
};
}