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

701 lines
19 KiB
TypeScript
Raw Normal View History

2025-08-28 18:27:07 +02:00
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";
2025-08-28 18:27:07 +02:00
import toast from "react-hot-toast";
import { z } from "zod/v4";
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
2025-08-04 17:45:44 +02:00
} from "~/components/custom_ui/form";
import Input from "~/components/custom_ui/input";
2025-11-13 15:45:55 +01:00
import { MultiSelect } from "~/components/custom_ui/multiselect";
2025-08-28 18:27:07 +02:00
import { Button } from "~/components/ui/button";
2025-08-04 17:45:44 +02:00
import { Calendar } from "~/components/ui/calendar";
import {
2025-08-28 18:27:07 +02:00
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
2025-08-04 17:45:44 +02:00
import { Separator } from "~/components/ui/separator";
2025-11-13 15:45:55 +01:00
import {
getProvinciaFromSigla,
ITALY_CATASTO_CODE,
parseDBComune,
parseDBNazione,
} from "~/lib/catasto";
import {
checkFiscalCodeValidity,
NullableStringOnChange,
} from "~/lib/form_utils";
2025-08-28 18:27:07 +02:00
import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm";
2025-11-13 15:45:55 +01:00
import { useCatasto } from "~/providers/CatastoProvider";
2025-08-28 18:27:07 +02:00
import { useTranslation } from "~/providers/I18nProvider";
2025-11-20 16:48:29 +01:00
import type { CompleteUserData } from "~/server/services/user.service";
2025-08-28 18:27:07 +02:00
import { api } from "~/utils/api";
2025-08-04 17:45:44 +02:00
const profileFormSchema = z.object({
cap_residenza: z.string().optional(),
civico_residenza: z.string().optional(),
codice_fiscale: z.string().optional(),
2025-11-13 15:45:55 +01:00
comune_residenza: z.string().nullable(),
data_nascita: z.date().optional(),
2025-11-13 15:45:55 +01:00
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>;
2025-08-04 17:45:44 +02:00
export const ProfileFormAnagrafica = ({
2025-08-28 18:27:07 +02:00
userData,
2025-08-04 17:45:44 +02:00
}: {
2025-11-20 16:48:29 +01:00
userData: CompleteUserData;
2025-08-04 17:45:44 +02:00
}) => {
2025-11-13 15:45:55 +01:00
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,
2025-11-13 15:45:55 +01:00
comuneCatasto: comuni.find((c) => c.catasto === luogo_nascita),
nazioneCatasto: nazioni.find((c) => c.catasto === nazione_nascita),
});
}
},
);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const defaultValues: ProfileFormValues = {
2025-08-29 16:18:32 +02:00
cap_residenza: userData.cap_residenza || "",
civico_residenza: userData.civico_residenza || "",
codice_fiscale: userData.codice_fiscale || "",
2025-11-13 15:45:55 +01:00
comune_residenza: userData.comune_residenza,
2025-08-28 18:27:07 +02:00
data_nascita: userData.data_nascita || new Date(),
2025-11-13 15:45:55 +01:00
luogo_nascita: userData.luogo_nascita,
nazione_nascita: userData.nazione_nascita,
nazione_residenza: userData.nazione_residenza,
provincia_residenza: userData.provincia_residenza,
2025-08-29 16:18:32 +02:00
sesso: userData.sesso || "",
via_residenza: userData.via_residenza || "",
2025-08-28 18:27:07 +02:00
};
const { locale, t } = useTranslation();
const DatePickerLocale = locale === "it" ? it : enUS;
2025-08-28 18:27:07 +02:00
z.config(z.locales[locale]());
2025-08-04 17:45:44 +02:00
const form = useZodForm(schema, {
2025-08-28 18:27:07 +02:00
defaultValues: defaultValues,
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutate } = api.users.editUserAnagrafica.useMutation({
2025-08-29 16:18:32 +02:00
onError: (error) => {
toast.error(error.message);
},
2025-08-28 18:27:07 +02:00
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,
});
2025-08-28 18:27:07 +02:00
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
function onSubmit(fields: ProfileFormValues) {
mutate({
userId: userData.id,
data: { ...fields },
2025-08-28 18:27:07 +02:00
});
}
2025-08-28 18:27:07 +02:00
return (
<>
<Form {...form}>
{/* <FormDebug control={form.control} /> */}
2025-08-28 18:27:07 +02:00
<form
className="h-full space-y-8 px-0.5"
2025-08-29 16:18:32 +02:00
onSubmit={form.handleSubmit(onSubmit)}
2025-08-28 18:27:07 +02:00
>
<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.5 w-full pl-3 text-left font-medium",
2025-08-28 18:27:07 +02:00
!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",
2025-08-28 18:27:07 +02:00
)}
2025-08-29 16:18:32 +02:00
id="data_nascita"
variant={"outline"}
2025-08-28 18:27:07 +02:00
>
{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>
2025-08-29 16:18:32 +02:00
<PopoverContent align="start" className="w-auto p-0">
2025-08-28 18:27:07 +02:00
<Calendar
2025-08-29 16:18:32 +02:00
autoFocus
2025-08-28 18:27:07 +02:00
captionLayout="dropdown"
2025-08-29 16:18:32 +02:00
defaultMonth={field.value}
endMonth={new Date()}
locale={DatePickerLocale}
mode="single"
onSelect={field.onChange}
selected={field.value}
2025-08-28 18:27:07 +02:00
/>
</PopoverContent>
</Popover>
</FormItem>
)}
/>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<FormField
control={form.control}
name="nazione_nascita"
2025-11-13 15:45:55 +01:00
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>
2025-08-04 17:45:44 +02:00
2025-11-13 15:45:55 +01:00
<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>
);
}}
2025-08-28 18:27:07 +02:00
/>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<FormField
control={form.control}
name="luogo_nascita"
render={({ field }) => {
2025-11-13 15:45:55 +01:00
if (form.watch("nazione_nascita") === ITALY_CATASTO_CODE) {
const parsedComuneNascita = parseDBComune(
field.value,
comuni,
);
2025-08-28 18:27:07 +02:00
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>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<FormControl>
<MultiSelect
defaultValue={
2025-11-13 15:45:55 +01:00
field.value && parsedComuneNascita
? {
label: `${parsedComuneNascita.nome} (${parsedComuneNascita.sigla})`,
value: parsedComuneNascita.catasto,
}
2025-08-28 18:27:07 +02:00
: undefined
}
2025-08-29 16:18:32 +02:00
elementId="select-comune"
elementName="luogo_nascita"
isMulti={false}
2025-08-28 18:27:07 +02:00
onValueChange={async (value) => {
field.onChange(value.value);
await form.trigger("luogo_nascita");
await form.trigger("codice_fiscale");
}}
2025-11-13 15:45:55 +01:00
options={comuni.map((c) => ({
label: `${c.nome} (${c.sigla})`,
value: c.catasto,
}))}
2025-08-28 18:27:07 +02:00
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);
2025-08-28 18:27:07 +02:00
await form.trigger("luogo_nascita");
await form.trigger("codice_fiscale");
}}
2025-08-29 16:18:32 +02:00
type="text"
2025-11-13 15:45:55 +01:00
value={field.value || ""}
2025-08-28 18:27:07 +02:00
/>
</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,
)}
2025-08-29 16:18:32 +02:00
elementId="select-sesso"
elementName="sesso"
isMulti={false}
onValueChange={async (value) => {
2025-08-28 18:27:07 +02:00
field.onChange(value.value);
await form.trigger("codice_fiscale");
2025-08-28 18:27:07 +02:00
}}
2025-08-29 16:18:32 +02:00
options={t.anagrafica.sesso_options}
placeholder=""
2025-08-28 18:27:07 +02:00
/>
</FormItem>
)}
/>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<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}
2025-08-29 16:18:32 +02:00
className="uppercase"
id="codice_fiscale"
2025-08-28 18:27:07 +02:00
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>;
}) => {
2025-11-13 15:45:55 +01:00
const { comuni, nazioni, provincie } = useCatasto();
const { t } = useTranslation();
2025-11-13 15:45:55 +01:00
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"
2025-11-13 15:45:55 +01:00
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 }) => {
2025-11-13 15:45:55 +01:00
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>
2025-11-13 15:45:55 +01:00
{nazione_residenza === ITALY_CATASTO_CODE ? (
2025-08-28 18:27:07 +02:00
<MultiSelect
defaultValue={
2025-11-13 15:45:55 +01:00
field.value && parseProvincia
? {
2025-11-13 15:45:55 +01:00
label: parseProvincia.nome,
value: parseProvincia.sigla,
}
2025-08-28 18:27:07 +02:00
: undefined
}
elementId="provincia_residenza"
elementName="provincia"
2025-08-28 18:27:07 +02:00
isMulti={false}
onValueChange={(value) => {
2025-08-29 16:18:32 +02:00
field.onChange(value.value);
2025-11-13 15:45:55 +01:00
setValue("comune_residenza", null);
2025-08-29 16:18:32 +02:00
}}
2025-11-13 15:45:55 +01:00
options={provincie.map((prov) => ({
label: prov.nome,
value: prov.sigla,
}))}
2025-08-28 18:27:07 +02:00
placeholder="Seleziona"
/>
) : (
<FormControl>
2025-11-13 15:45:55 +01:00
<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 }) => {
2025-11-13 15:45:55 +01:00
if (nazione_residenza === ITALY_CATASTO_CODE) {
const parsedComuneResidenza = parseDBComune(field.value, comuni);
const options = comuni
.filter((comune) =>
provincia_residenza
2025-11-13 15:45:55 +01:00
? comune.sigla === provincia_residenza
: true,
)
2025-11-13 15:45:55 +01:00
.map((comune) => ({
label: comune.nome,
value: comune.catasto,
}));
2025-08-04 17:45:44 +02:00
return (
2025-08-28 18:27:07 +02:00
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-comune">
{t.recapiti.comune}
2025-08-28 18:27:07 +02:00
</FormLabel>
<FormMessage />
</div>
2025-08-04 17:45:44 +02:00
<FormControl>
2025-08-28 18:27:07 +02:00
<MultiSelect
defaultValue={
2025-11-13 15:45:55 +01:00
field.value && parsedComuneResidenza
? {
label: parsedComuneResidenza.nome,
value: parsedComuneResidenza.catasto,
}
2025-08-28 18:27:07 +02:00
: undefined
}
elementId="select-comune"
elementName="comune_residenza"
2025-08-28 18:27:07 +02:00
isMulti={false}
onValueChange={async (value) => {
2025-08-29 16:18:32 +02:00
field.onChange(value.value);
await trigger("comune_residenza");
2025-08-29 16:18:32 +02:00
}}
2025-11-13 15:45:55 +01:00
options={options}
placeholder="Seleziona..."
2025-08-28 18:27:07 +02:00
/>
</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"
2025-11-13 15:45:55 +01:00
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>
2025-08-04 17:45:44 +02:00
<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>
)}
/>
2025-08-04 17:45:44 +02:00
<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>
2025-08-04 17:45:44 +02:00
<FormMessage />
</div>
<FormControl>
<Input
{...field}
id="civico_residenza"
placeholder="50"
type="text"
/>
</FormControl>
</FormItem>
)}
/>
</div>
2025-08-28 18:27:07 +02:00
</>
);
2025-08-04 17:45:44 +02:00
};