infoalloggi-monorepo/apps/infoalloggi/src/forms/FormProfilo_Anagrafica.tsx

701 lines
19 KiB
TypeScript

import { differenceInYears, format } from "date-fns";
import { enUS, it } from "date-fns/locale";
import { CalendarIcon } from "lucide-react";
import { type Control, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod/v4";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/custom_ui/form";
import Input from "~/components/custom_ui/input";
import { MultiSelect } from "~/components/custom_ui/multiselect";
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { Separator } from "~/components/ui/separator";
import {
getProvinciaFromSigla,
ITALY_CATASTO_CODE,
parseDBComune,
parseDBNazione,
} from "~/lib/catasto";
import {
checkFiscalCodeValidity,
NullableStringOnChange,
} from "~/lib/form_utils";
import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm";
import { useCatasto } from "~/providers/CatastoProvider";
import { useTranslation } from "~/providers/I18nProvider";
import type { CompleteUserData } from "~/server/services/user.service";
import { api } from "~/utils/api";
const profileFormSchema = z.object({
cap_residenza: z.string().optional(),
civico_residenza: z.string().optional(),
codice_fiscale: z.string().optional(),
comune_residenza: z.string().nullable(),
data_nascita: z.date().optional(),
luogo_nascita: z.string().nullable(),
nazione_nascita: z.string().nullable(),
nazione_residenza: z.string().nullable(),
provincia_residenza: z.string().nullable(),
sesso: z.string().optional(),
via_residenza: z.string().optional(),
});
type ProfileFormValues = z.infer<typeof profileFormSchema>;
export const ProfileFormAnagrafica = ({
userData,
}: {
userData: CompleteUserData;
}) => {
const { comuni, nazioni } = useCatasto();
const schema = profileFormSchema.superRefine(
(
{
data_nascita,
codice_fiscale,
sesso,
luogo_nascita,
nazione_nascita,
cap_residenza,
},
refinementContext,
) => {
if (cap_residenza) {
if (cap_residenza.length !== 5) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci un CAP valido",
});
}
}
if (data_nascita) {
const diff = differenceInYears(data_nascita, new Date());
if (diff === 0) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci una data valida",
path: ["data_nascita"],
});
}
}
if (
codice_fiscale &&
codice_fiscale.length > 0 &&
data_nascita &&
sesso &&
luogo_nascita &&
nazione_nascita
) {
checkFiscalCodeValidity({
codiceFiscale: codice_fiscale,
cognome: userData.cognome?.toUpperCase() || "",
dataNascita: data_nascita,
luogoNascita: luogo_nascita,
nazioneNascita: nazione_nascita,
nome: userData.nome?.toUpperCase() || "",
path: "codice_fiscale",
refinementContext,
sesso,
comuneCatasto: comuni.find((c) => c.catasto === luogo_nascita),
nazioneCatasto: nazioni.find((c) => c.catasto === nazione_nascita),
});
}
},
);
const defaultValues: ProfileFormValues = {
cap_residenza: userData.cap_residenza || "",
civico_residenza: userData.civico_residenza || "",
codice_fiscale: userData.codice_fiscale || "",
comune_residenza: userData.comune_residenza,
data_nascita: userData.data_nascita || new Date(),
luogo_nascita: userData.luogo_nascita,
nazione_nascita: userData.nazione_nascita,
nazione_residenza: userData.nazione_residenza,
provincia_residenza: userData.provincia_residenza,
sesso: userData.sesso || "",
via_residenza: userData.via_residenza || "",
};
const { locale, t } = useTranslation();
const DatePickerLocale = locale === "it" ? it : enUS;
z.config(z.locales[locale]());
const form = useZodForm(schema, {
defaultValues: defaultValues,
});
const utils = api.useUtils();
const { mutate } = api.users.editUserAnagrafica.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
toast.success(t.profile.aggiornato);
await utils.auth.getSession.invalidate();
await utils.users.getUser.invalidate();
await utils.users.getClientProfilo.invalidate({
userId: userData.id,
});
},
});
function onSubmit(fields: ProfileFormValues) {
mutate({
userId: userData.id,
data: { ...fields },
});
}
return (
<>
<Form {...form}>
{/* <FormDebug control={form.control} /> */}
<form
className="h-full space-y-8 px-0.5"
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={form.control}
name="data_nascita"
render={({ field }) => (
<FormItem className="flex w-full flex-col">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="data_nascita">
{t.anagrafica.dataNascita}
</FormLabel>
<FormMessage />
</div>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
className={cn(
"h-10 w-full bg-card pl-3 text-left font-medium",
!field.value && "text-muted-foreground",
"rounded-lg border border-neutral-300 shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm disabled:cursor-not-allowed disabled:opacity-50 dark:focus:border-transparent",
)}
id="data_nascita"
variant={"outline"}
>
{field.value ? (
format(field.value, "PPP", {
locale: DatePickerLocale,
})
) : (
<span>{t.anagrafica.scegli_data}</span>
)}
<CalendarIcon className="ml-auto size-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-0">
<Calendar
autoFocus
captionLayout="dropdown"
defaultMonth={field.value}
endMonth={new Date()}
locale={DatePickerLocale}
mode="single"
onSelect={field.onChange}
selected={field.value}
/>
</PopoverContent>
</Popover>
</FormItem>
)}
/>
<FormField
control={form.control}
name="nazione_nascita"
render={({ field }) => {
const parsedNazioneNascita = parseDBNazione(
field.value,
nazioni,
);
return (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-naz">
{t.anagrafica.nazioneNascita}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<MultiSelect
defaultValue={
field.value
? parsedNazioneNascita
? {
label: parsedNazioneNascita.nome,
value: parsedNazioneNascita.catasto,
}
: {
label: field.value,
value: field.value,
}
: undefined
}
elementId="select-naz"
elementName="nazione_nascita"
isMulti={false}
onValueChange={async (value) => {
field.onChange(value.value);
await form.trigger("luogo_nascita");
form.setValue("luogo_nascita", null);
}}
options={nazioni.map((n) => ({
label: n.nome,
value: n.catasto,
}))}
placeholder="Seleziona..."
/>
</FormControl>
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="luogo_nascita"
render={({ field }) => {
if (form.watch("nazione_nascita") === ITALY_CATASTO_CODE) {
const parsedComuneNascita = parseDBComune(
field.value,
comuni,
);
return (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-comune">
{t.anagrafica.luogoNascita}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<MultiSelect
defaultValue={
field.value && parsedComuneNascita
? {
label: `${parsedComuneNascita.nome} (${parsedComuneNascita.sigla})`,
value: parsedComuneNascita.catasto,
}
: undefined
}
elementId="select-comune"
elementName="luogo_nascita"
isMulti={false}
onValueChange={async (value) => {
field.onChange(value.value);
await form.trigger("luogo_nascita");
await form.trigger("codice_fiscale");
}}
options={comuni.map((c) => ({
label: `${c.nome} (${c.sigla})`,
value: c.catasto,
}))}
placeholder="Seleziona..."
/>
</FormControl>
</FormItem>
);
}
return (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="luogoNascita">
{t.anagrafica.luogoNascita}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
id="luogoNascita"
onChange={async (e) => {
NullableStringOnChange(e, field.onChange);
await form.trigger("luogo_nascita");
await form.trigger("codice_fiscale");
}}
type="text"
value={field.value || ""}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={form.control}
name="sesso"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-sesso">
{t.anagrafica.sesso}
</FormLabel>
<FormMessage />
</div>
<MultiSelect
defaultValue={t.anagrafica.sesso_options.filter(
(v) => v.value === field.value,
)}
elementId="select-sesso"
elementName="sesso"
isMulti={false}
onValueChange={async (value) => {
field.onChange(value.value);
await form.trigger("codice_fiscale");
}}
options={t.anagrafica.sesso_options}
placeholder=""
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="codice_fiscale"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="codice_fiscale">
{t.anagrafica.codiceFiscale}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
className="uppercase"
id="codice_fiscale"
maxLength={16}
onChange={async (v) => {
field.onChange(v.target.value.toUpperCase());
await form.trigger("codice_fiscale");
}}
type="text"
/>
</FormControl>
</FormItem>
)}
/>
</div>
<Separator />
<ResidenzaSection
control={form.control}
setValue={form.setValue}
trigger={form.trigger}
/>
<Button type="submit">{t.profile.aggiorna}</Button>
</form>
</Form>
</>
);
};
const ResidenzaSection = ({
control,
setValue,
trigger,
}: {
control: Control<ProfileFormValues>;
// biome-ignore lint/suspicious/noExplicitAny: <fine here>
setValue: (field: keyof ProfileFormValues, value: any) => void;
trigger: (name?: keyof ProfileFormValues) => Promise<boolean>;
}) => {
const { comuni, nazioni, provincie } = useCatasto();
const { t } = useTranslation();
const nazione_residenza = useWatch({
control: control,
name: "nazione_residenza",
});
const provincia_residenza = useWatch({
control: control,
name: "provincia_residenza",
});
return (
<>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={control}
name="nazione_residenza"
render={({ field }) => {
const parsedNazioneResidenza = parseDBNazione(field.value, nazioni);
return (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="nazione_residenza">
{t.recapiti.nazione}
</FormLabel>
<FormMessage />
</div>
<MultiSelect
defaultValue={
field.value
? parsedNazioneResidenza
? {
label: parsedNazioneResidenza.nome,
value: parsedNazioneResidenza.catasto,
}
: {
label: field.value,
value: field.value,
}
: undefined
}
elementId="nazione_residenza"
elementName="nazione"
isMulti={false}
onValueChange={async (value) => {
setValue("provincia_residenza", null);
setValue("comune_residenza", null);
field.onChange(value.value);
await trigger("comune_residenza");
}}
options={nazioni.map((n) => ({
label: n.nome,
value: n.catasto,
}))}
placeholder="Seleziona"
/>
</FormItem>
);
}}
/>
<FormField
control={control}
name="provincia_residenza"
render={({ field }) => {
const parseProvincia = getProvinciaFromSigla(
field.value,
provincie,
);
return (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="provincia_residenza">
{t.recapiti.provincia}
</FormLabel>
<FormMessage />
</div>
{nazione_residenza === ITALY_CATASTO_CODE ? (
<MultiSelect
defaultValue={
field.value && parseProvincia
? {
label: parseProvincia.nome,
value: parseProvincia.sigla,
}
: undefined
}
elementId="provincia_residenza"
elementName="provincia"
isMulti={false}
onValueChange={(value) => {
field.onChange(value.value);
setValue("comune_residenza", null);
}}
options={provincie.map((prov) => ({
label: prov.nome,
value: prov.sigla,
}))}
placeholder="Seleziona"
/>
) : (
<FormControl>
<Input
{...field}
id="provincia_residenza"
type="text"
value={field.value || undefined}
/>
</FormControl>
)}
</FormItem>
);
}}
/>
</div>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={control}
name="comune_residenza"
render={({ field }) => {
if (nazione_residenza === ITALY_CATASTO_CODE) {
const parsedComuneResidenza = parseDBComune(field.value, comuni);
const options = comuni
.filter((comune) =>
provincia_residenza
? comune.sigla === provincia_residenza
: true,
)
.map((comune) => ({
label: comune.nome,
value: comune.catasto,
}));
return (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-comune">
{t.recapiti.comune}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<MultiSelect
defaultValue={
field.value && parsedComuneResidenza
? {
label: parsedComuneResidenza.nome,
value: parsedComuneResidenza.catasto,
}
: undefined
}
elementId="select-comune"
elementName="comune_residenza"
isMulti={false}
onValueChange={async (value) => {
field.onChange(value.value);
await trigger("comune_residenza");
}}
options={options}
placeholder="Seleziona..."
/>
</FormControl>
</FormItem>
);
}
return (
<FormItem className="w-full">
<div className="flex w-full flex-wrap items-center gap-x-2">
<FormLabel htmlFor="comune_residenza">
{t.recapiti.comune}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
className="w-full"
id="comune_residenza"
onChange={async (v) => {
field.onChange(v.target.value);
await trigger("comune_residenza");
}}
type="text"
value={field.value || undefined}
/>
</FormControl>
</FormItem>
);
}}
/>
<FormField
control={control}
name="cap_residenza"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="cap_residenza">{t.recapiti.cap}</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
id="cap_residenza"
placeholder="00100"
type="text"
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={control}
name="via_residenza"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="via_residenza">
{t.recapiti.indirizzo}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
id="via_residenza"
placeholder="Via Roma"
type="text"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={control}
name="civico_residenza"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="civico_residenza">
{t.recapiti.civico}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
id="civico_residenza"
placeholder="50"
type="text"
/>
</FormControl>
</FormItem>
)}
/>
</div>
</>
);
};