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,20 @@
'use client';
import { useCallback, useState } from 'react';
export function useProjectFilters() {
const [skip, setSkip] = useState(0);
const [limit, setLimitValue] = useState(10);
const setLimit = useCallback((value: number) => {
setLimitValue(value);
setSkip(0);
}, []);
return {
skip,
setSkip,
limit,
setLimit,
};
}

View File

@@ -0,0 +1,146 @@
'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 { projectService } from '@/services/api';
import type { Project, ProjectCreate, ProjectUpdate } from '@/types';
import { projectKeys } from '../queries/projectKeys';
const optionalCoordinate = z.string().refine(
(value) => {
if (!value.trim()) {
return true;
}
return Number.isFinite(Number(value));
},
{ message: 'Enter a valid number' },
);
const projectFormSchema = z.object({
id: z.string().optional(),
name: z.string().trim().min(1, 'Project name is required'),
state: z.string().optional(),
corridor_name: z.string().optional(),
start_lat: optionalCoordinate,
start_lng: optionalCoordinate,
end_lat: optionalCoordinate,
end_lng: optionalCoordinate,
});
export type ProjectFormValues = z.infer<typeof projectFormSchema>;
const defaultValues: ProjectFormValues = {
name: '',
state: '',
corridor_name: '',
start_lat: '',
start_lng: '',
end_lat: '',
end_lng: '',
};
function trimOptional(value?: string) {
return value?.trim() || null;
}
function toNullableNumber(value: string) {
return value.trim() ? Number(value) : null;
}
function toProjectPayload(values: ProjectFormValues): ProjectCreate | ProjectUpdate {
return {
name: values.name.trim(),
state: trimOptional(values.state),
corridor_name: trimOptional(values.corridor_name),
start_lat: toNullableNumber(values.start_lat),
start_lng: toNullableNumber(values.start_lng),
end_lat: toNullableNumber(values.end_lat),
end_lng: toNullableNumber(values.end_lng),
};
}
export function useProjectForm({ onSaved }: { onSaved: () => void }) {
const queryClient = useQueryClient();
const {
register,
handleSubmit,
reset,
control,
formState: { errors, isSubmitting, touchedFields },
} = useForm<ProjectFormValues>({
defaultValues,
mode: 'onTouched',
reValidateMode: 'onChange',
resolver: zodResolver(projectFormSchema),
});
const projectId = useWatch({ control, name: 'id' });
const projectName = useWatch({ control, name: 'name' }) || '';
const canSubmit = projectName.trim().length > 0;
const saveMutation = useMutation({
mutationFn: (values: ProjectFormValues) => {
const payload = toProjectPayload(values);
return values.id
? projectService.updateProject(values.id, payload)
: projectService.createProject(payload as ProjectCreate);
},
onSuccess: (_data, values) => {
toast.success(values.id ? 'Project updated' : 'Project created');
reset(defaultValues);
onSaved();
queryClient.invalidateQueries({ queryKey: projectKeys.all });
},
onError: (_error, values) => {
toast.error(values.id ? 'Failed to update project' : 'Failed to create project');
},
});
const onSubmit = handleSubmit(
(values) => saveMutation.mutate(values),
(formErrors) => {
if (formErrors.name?.message) {
toast.error(formErrors.name.message);
}
},
);
const openCreate = useCallback(() => {
reset(defaultValues);
}, [reset]);
const openEdit = useCallback(
(project: Project) => {
reset({
id: project.id,
name: project.name || '',
state: project.state || '',
corridor_name: project.corridor_name || '',
start_lat: project.start_lat?.toString() ?? '',
start_lng: project.start_lng?.toString() ?? '',
end_lat: project.end_lat?.toString() ?? '',
end_lng: project.end_lng?.toString() ?? '',
});
},
[reset],
);
return {
register,
onSubmit,
openCreate,
openEdit,
resetForm: () => reset(defaultValues),
projectId,
errors,
touchedFields,
canSubmit,
isSaving: isSubmitting || saveMutation.isPending,
};
}

View File

@@ -0,0 +1,43 @@
'use client';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useMemo } from 'react';
import { toast } from 'sonner';
import { projectService } from '@/services/api';
import type { PaginationParams, Project } from '@/types';
import { projectKeys } from '../queries/projectKeys';
interface UseProjectsQueryParams {
skip: number;
limit: number;
}
export function useProjectsQuery({ skip, limit }: UseProjectsQueryParams) {
const listParams = useMemo<PaginationParams>(() => ({ skip, limit }), [limit, skip]);
return useQuery({
queryKey: projectKeys.list(listParams),
queryFn: () => projectService.getProjects(listParams),
});
}
export function useDeleteProjectMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (project: Project) => projectService.deleteProject(project.id),
onSuccess: (_data, project) => {
toast.success('Project deleted', {
description: `${project.name} has been removed from the system.`,
});
queryClient.invalidateQueries({ queryKey: projectKeys.all });
},
onError: () => {
toast.error('Deletion failed', {
description: 'The project could not be removed. Please try again or check your permissions.',
});
},
});
}