refactor: modularize road management modules
This commit is contained in:
20
src/app/(modules)/segment/hooks/useSegmentFilters.ts
Normal file
20
src/app/(modules)/segment/hooks/useSegmentFilters.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export function useSegmentFilters() {
|
||||
const [skip, setSkip] = useState(0);
|
||||
const [limit, setLimitValue] = useState(10);
|
||||
|
||||
const setLimit = useCallback((value: number) => {
|
||||
setLimitValue(value);
|
||||
setSkip(0);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
skip,
|
||||
setSkip,
|
||||
limit,
|
||||
setLimit,
|
||||
};
|
||||
}
|
||||
207
src/app/(modules)/segment/hooks/useSegmentForm.ts
Normal file
207
src/app/(modules)/segment/hooks/useSegmentForm.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
'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,
|
||||
};
|
||||
}
|
||||
65
src/app/(modules)/segment/hooks/useSegmentQueries.ts
Normal file
65
src/app/(modules)/segment/hooks/useSegmentQueries.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { chainageService, packageService, projectService } from '@/services/api';
|
||||
import type { Chainage, PaginationParams } from '@/types';
|
||||
|
||||
import { segmentKeys } from '../queries/segmentKeys';
|
||||
|
||||
interface UseSegmentsQueryParams {
|
||||
skip: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export function useSegmentsQuery({ skip, limit }: UseSegmentsQueryParams) {
|
||||
const listParams = useMemo<PaginationParams>(() => ({ skip, limit }), [limit, skip]);
|
||||
|
||||
return useQuery({
|
||||
queryKey: segmentKeys.list(listParams),
|
||||
queryFn: () => chainageService.getChainages(listParams),
|
||||
});
|
||||
}
|
||||
|
||||
export function useProjectOptionsQuery() {
|
||||
return useQuery({
|
||||
queryKey: ['projects', 'options'],
|
||||
queryFn: () => projectService.getProjects({ skip: 0, limit: 1000 }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useAllPackageOptionsQuery() {
|
||||
return useQuery({
|
||||
queryKey: ['packages', 'options'],
|
||||
queryFn: () => packageService.getPackages({ skip: 0, limit: 1000 }),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePackagesByProjectQuery(projectId: string, enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: ['packages', 'by-project', projectId],
|
||||
queryFn: () => packageService.getPackagesByProject(projectId, { skip: 0, limit: 1000 }),
|
||||
enabled: enabled && Boolean(projectId),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteSegmentMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (segment: Chainage) => chainageService.deleteChainage(segment.id),
|
||||
onSuccess: (_data, segment) => {
|
||||
toast.success('Segment deleted', {
|
||||
description: `${segment.segment_name} has been removed from the system.`,
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: segmentKeys.all });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Deletion failed', {
|
||||
description: 'The segment could not be removed. Please try again.',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user