'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; 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({ 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, }; }