212 lines
6.7 KiB
TypeScript
212 lines
6.7 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { z } from 'zod';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import type { Visit, VisitCreateInput } from '../types';
|
|
import { useCreateVisitMutation, useUpdateVisitMutation } from '../hooks/useVisitQueries';
|
|
import { useVisitDrawerStore } from '../../../state/useVisitDrawerStore';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
const visitFormSchema = z
|
|
.object({
|
|
country: z.string().min(1, 'validation.countryRequired'),
|
|
city: z.string().optional(),
|
|
start: z.string().min(1, 'validation.startDateRequired'),
|
|
end: z.string().optional(),
|
|
notes: z.string().optional(),
|
|
lat: z.number(),
|
|
lng: z.number()
|
|
})
|
|
.refine(
|
|
(data) => {
|
|
if (!data.end) return true;
|
|
return new Date(data.end) >= new Date(data.start);
|
|
},
|
|
{ message: 'validation.endAfterStart', path: ['end'] }
|
|
);
|
|
|
|
type VisitFormSchema = z.infer<typeof visitFormSchema>;
|
|
|
|
interface VisitFormProps {
|
|
editingVisit?: Visit | null;
|
|
onCompleted?: () => void;
|
|
}
|
|
|
|
export function VisitForm({ editingVisit, onCompleted }: VisitFormProps) {
|
|
const closeDrawer = useVisitDrawerStore((state) => state.close);
|
|
const initialLocation = useVisitDrawerStore((state) => state.initialLocation);
|
|
const { t } = useTranslation();
|
|
|
|
const createMutation = useCreateVisitMutation();
|
|
const updateMutation = useUpdateVisitMutation();
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
formState: { errors, isSubmitting, isSubmitSuccessful }
|
|
} = useForm<VisitFormSchema>({
|
|
resolver: zodResolver(visitFormSchema),
|
|
defaultValues: {
|
|
country: editingVisit?.location.country ?? '',
|
|
city: editingVisit?.location.city ?? '',
|
|
start: editingVisit?.date.start ?? '',
|
|
end: editingVisit?.date.end ?? '',
|
|
notes: editingVisit?.notes ?? '',
|
|
lat: editingVisit?.location.lat ?? initialLocation?.lat ?? 0,
|
|
lng: editingVisit?.location.lng ?? initialLocation?.lng ?? 0
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (initialLocation && !editingVisit) {
|
|
setValue('lat', initialLocation.lat);
|
|
setValue('lng', initialLocation.lng);
|
|
}
|
|
}, [initialLocation, editingVisit, setValue]);
|
|
|
|
useEffect(() => {
|
|
if (isSubmitSuccessful) {
|
|
closeDrawer();
|
|
onCompleted?.();
|
|
}
|
|
}, [isSubmitSuccessful, closeDrawer, onCompleted]);
|
|
|
|
async function onSubmit(values: VisitFormSchema) {
|
|
const payload: VisitCreateInput = {
|
|
location: {
|
|
country: values.country,
|
|
city: values.city,
|
|
lat: values.lat,
|
|
lng: values.lng
|
|
},
|
|
date: {
|
|
start: values.start,
|
|
end: values.end || undefined
|
|
},
|
|
notes: values.notes || undefined
|
|
};
|
|
|
|
if (editingVisit) {
|
|
await updateMutation.mutateAsync({
|
|
id: editingVisit.id,
|
|
data: {
|
|
...payload,
|
|
location: payload.location,
|
|
date: payload.date
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
await createMutation.mutateAsync(payload);
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
<div className="grid gap-2">
|
|
<label className="text-sm font-medium" htmlFor="country">
|
|
{t('form.country')}
|
|
</label>
|
|
<input
|
|
id="country"
|
|
className="rounded border border-slate-300 px-3 py-2"
|
|
placeholder={t('form.countryPlaceholder')}
|
|
{...register('country')}
|
|
/>
|
|
{errors.country ? (
|
|
<p className="text-sm text-rose-500">{t(errors.country.message ?? '')}</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<label className="text-sm font-medium" htmlFor="city">
|
|
{t('form.city')}
|
|
</label>
|
|
<input
|
|
id="city"
|
|
className="rounded border border-slate-300 px-3 py-2"
|
|
placeholder={t('form.cityPlaceholder')}
|
|
{...register('city')}
|
|
/>
|
|
{errors.city ? <p className="text-sm text-rose-500">{t(errors.city.message ?? '')}</p> : null}
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<label className="text-sm font-medium" htmlFor="start">
|
|
{t('form.startDate')}
|
|
</label>
|
|
<input id="start" type="date" className="rounded border border-slate-300 px-3 py-2" {...register('start')} />
|
|
{errors.start ? <p className="text-sm text-rose-500">{t(errors.start.message ?? '')}</p> : null}
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<label className="text-sm font-medium" htmlFor="end">
|
|
{t('form.endDate')}
|
|
</label>
|
|
<input id="end" type="date" className="rounded border border-slate-300 px-3 py-2" {...register('end')} />
|
|
{errors.end ? <p className="text-sm text-rose-500">{t(errors.end.message ?? '')}</p> : null}
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<label className="text-sm font-medium" htmlFor="notes">
|
|
{t('form.notes')}
|
|
</label>
|
|
<textarea
|
|
id="notes"
|
|
className="min-h-[96px] rounded border border-slate-300 px-3 py-2"
|
|
placeholder={t('form.notesPlaceholder')}
|
|
{...register('notes')}
|
|
/>
|
|
{errors.notes ? <p className="text-sm text-rose-500">{t(errors.notes.message ?? '')}</p> : null}
|
|
</div>
|
|
|
|
<div className="grid gap-2 md:grid-cols-2">
|
|
<div>
|
|
<label className="text-sm font-medium" htmlFor="lat">
|
|
{t('form.latitude')}
|
|
</label>
|
|
<input
|
|
id="lat"
|
|
type="number"
|
|
step="any"
|
|
className="w-full rounded border border-slate-300 px-3 py-2"
|
|
{...register('lat', { valueAsNumber: true })}
|
|
/>
|
|
{errors.lat ? <p className="text-sm text-rose-500">{t(errors.lat.message ?? '')}</p> : null}
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium" htmlFor="lng">
|
|
{t('form.longitude')}
|
|
</label>
|
|
<input
|
|
id="lng"
|
|
type="number"
|
|
step="any"
|
|
className="w-full rounded border border-slate-300 px-3 py-2"
|
|
{...register('lng', { valueAsNumber: true })}
|
|
/>
|
|
{errors.lng ? <p className="text-sm text-rose-500">{t(errors.lng.message ?? '')}</p> : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
className="rounded border border-slate-300 px-4 py-2"
|
|
onClick={() => closeDrawer()}
|
|
>
|
|
{t('form.cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="rounded bg-primary px-4 py-2 font-semibold text-white hover:bg-primary/90 disabled:opacity-60"
|
|
>
|
|
{editingVisit ? t('form.update') : t('form.create')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|