Files
road-monitoring-ui/src/app/(modules)/project/hooks/useProjectForm.ts

138 lines
3.7 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 {
optionalLatitude,
optionalLongitude,
} from '@/lib/validation/numberField';
import { projectService } from '@/services/api';
import type { Project, ProjectCreate, ProjectUpdate } from '@/types';
import { projectKeys } from '../queries/projectKeys';
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: optionalLatitude,
start_lng: optionalLongitude,
end_lat: optionalLatitude,
end_lng: optionalLongitude,
});
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, isSubmitted },
} = useForm<ProjectFormValues>({
defaultValues,
mode: 'onSubmit',
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));
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,
isSubmitted,
canSubmit,
isSaving: isSubmitting || saveMutation.isPending,
};
}