refactor: modularize road management modules
This commit is contained in:
20
src/app/(modules)/project/hooks/useProjectFilters.ts
Normal file
20
src/app/(modules)/project/hooks/useProjectFilters.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
146
src/app/(modules)/project/hooks/useProjectForm.ts
Normal file
146
src/app/(modules)/project/hooks/useProjectForm.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
43
src/app/(modules)/project/hooks/useProjectQueries.ts
Normal file
43
src/app/(modules)/project/hooks/useProjectQueries.ts
Normal 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.',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user