701 lines
19 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
};
|