138 lines
3.7 KiB
TypeScript
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,
|
|
};
|
|
}
|