infoalloggi-monorepo/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx
Marco Pedone 6c36bb9b74 feat: add order editing form and payment processing pages
- Implemented FormEditOrder component for editing orders with validation and submission handling.
- Created SchedaAnnuncioPage for displaying printable announcement details.
- Added BonificoManualePage for manual bank transfer payment instructions.
- Developed PagamentoPage for Stripe payment processing with PaymentIntent creation.
- Introduced PreviewPurchasePage for preparing payment with service details.
- Added pagamenti.controller for handling payment preparation and retrieval of payment data.
2026-03-08 01:02:57 +01:00

1472 lines
42 KiB
TypeScript

"use client";
import { format } from "date-fns";
import { enUS, it } from "date-fns/locale";
import { CalendarIcon, HelpCircleIcon, Mail } from "lucide-react";
import { useRouter } from "next/router";
import { type Control, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod/v4";
import { Counter } from "~/components/counter";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/custom_ui/form";
import Input from "~/components/custom_ui/input";
import InputWIcon from "~/components/custom_ui/input-icon";
import { MultiSelect, UnpackOptions } from "~/components/custom_ui/multiselect";
import { PhoneInput } from "~/components/phone-input";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Label } from "~/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { Separator } from "~/components/ui/separator";
import { Slider } from "~/components/ui/slider";
import { Switch } from "~/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";
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 { Servizio, ServizioUpdate } from "~/schemas/public/Servizio";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { Users, UsersId } from "~/schemas/public/Users";
import type {
UsersAnagrafica,
UsersAnagraficaUpdate,
} from "~/schemas/public/UsersAnagrafica";
import { api } from "~/utils/api";
type InitialData = {
servizio: Servizio;
userData: Pick<Users, "id" | "nome" | "cognome" | "email" | "telefono">;
anagrafica: UsersAnagrafica | undefined;
};
const FormNewServizioSchema = z.object({
animali: z.boolean(),
arredato: z.boolean(),
ascensore: z.boolean(),
budget: z.number().positive(),
cap_recapiti: z.string().length(5),
citta_recapiti: z.string().nullable(),
civico_recapiti: z.string(),
codice_fiscale: z.string(),
cognome: z.string().nonempty("Inserisci un cognome valido"),
data_nascita: z.date(),
email: z.email(),
entromese: z.number().nullable(),
fumatori: z.boolean(),
giardino: z.boolean(),
indirizzo_recapiti: z.string().nonempty("Inserisci un indirizzo"),
luogo_nascita: z.string().nullable(),
motivazione_transitorio: z.string().nullable(),
n_adulti: z.number().min(1),
n_minori: z.number(),
nazione_nascita: z.string().nullable(),
nazione_recapiti: z.string().nullable(),
nome: z.string().nonempty("Inserisci un nome valido"),
parcheggio: z.boolean(),
permanenza: z.number().nullable(),
pianoterra: z.boolean(),
provincia_recapiti: z.string().nullable(),
reddito: z.string().nullable(),
sesso: z.string(),
telefono: z.string(),
terrazzo: z.boolean(),
tipologia: z.custom<TipologiaPosizioneEnum>(),
scadenza_motivazione_transitoria: z.date().nullable(),
skipPayment: z.boolean(),
});
type FormNewServizioValues = z.infer<typeof FormNewServizioSchema>;
export const FormNewServizioAcquisto = ({
initialData,
userId,
isAdmin,
}: {
initialData: InitialData;
userId: UsersId;
isAdmin: boolean;
}) => {
const router = useRouter();
const { comuni, nazioni } = useCatasto();
console.log(isAdmin);
const schema = FormNewServizioSchema.superRefine(
(
{
telefono,
codice_fiscale,
sesso,
cognome,
nome,
data_nascita,
luogo_nascita,
nazione_nascita,
n_adulti,
permanenza,
entromese,
motivazione_transitorio,
scadenza_motivazione_transitoria,
reddito,
citta_recapiti,
nazione_recapiti,
provincia_recapiti,
},
refinementContext,
) => {
if (entromese === null) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci il mese di inizio",
path: ["entromese"],
});
}
if (!telefono || telefono.length < 10) {
refinementContext.issues.push({
code: "custom",
input: "",
message:
"Il numero di telefono deve essere lungo almeno 10 caratteri",
path: ["telefono"],
});
}
if (!luogo_nascita || luogo_nascita.length < 1) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci il luogo di nascita",
path: ["luogo_nascita"],
});
}
if (!nazione_nascita || nazione_nascita.length < 1) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci la nazione di nascita",
path: ["nazione_nascita"],
});
}
if (!citta_recapiti || citta_recapiti.length < 1) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci la città di recapiti",
path: ["citta_recapiti"],
});
}
if (!nazione_recapiti || nazione_recapiti.length < 1) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci la nazione di recapiti",
path: ["nazione_recapiti"],
});
}
if (!provincia_recapiti || provincia_recapiti.length < 1) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci la provincia di recapiti",
path: ["provincia_recapiti"],
});
}
if (luogo_nascita && nazione_nascita) {
checkFiscalCodeValidity({
codiceFiscale: codice_fiscale,
cognome,
dataNascita: data_nascita,
luogoNascita: luogo_nascita,
nazioneNascita: nazione_nascita,
nome,
path: "codice_fiscale",
refinementContext,
sesso,
comuneCatasto: comuni.find((c) => c.catasto === luogo_nascita),
nazioneCatasto: nazioni.find((c) => c.catasto === nazione_nascita),
});
}
if (n_adulti <= 0) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci il numero di persone",
path: ["n_adulti"],
});
}
if (tipologia === TipologiaPosizioneEnum.Transitorio) {
if (
motivazione_transitorio === "" ||
motivazione_transitorio === null
) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci la motivazione",
path: ["motivazione_transitorio"],
});
}
if (!scadenza_motivazione_transitoria) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci la scadenza della motivazione",
path: ["scadenza_motivazione_transitoria"],
});
}
if (permanenza == null) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci permanenza",
path: ["permanenza"],
});
}
} else {
if (reddito === "" || reddito === null) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci il reddito",
path: ["reddito"],
});
}
}
},
);
// This can come from your database or API.
const defaultValues: FormNewServizioValues = {
...initialData.userData,
cap_recapiti: initialData.anagrafica?.cap_residenza || "",
citta_recapiti: initialData.anagrafica?.comune_residenza || null,
civico_recapiti: initialData.anagrafica?.civico_residenza || "",
codice_fiscale: initialData.anagrafica?.codice_fiscale || "",
data_nascita: initialData.anagrafica?.data_nascita
? new Date(initialData.anagrafica.data_nascita)
: new Date(),
indirizzo_recapiti: initialData.anagrafica?.via_residenza || "",
luogo_nascita: initialData.anagrafica?.luogo_nascita || null,
nazione_nascita: initialData.anagrafica?.nazione_nascita || null,
nazione_recapiti: initialData.anagrafica?.nazione_residenza || null,
provincia_recapiti: initialData.anagrafica?.provincia_residenza || null,
sesso: initialData.anagrafica?.sesso || "M",
...initialData.servizio,
};
const { locale, t } = useTranslation();
z.config(z.locales[locale]());
const form = useZodForm(schema, {
defaultValues: defaultValues,
});
const DatePickerLocale = locale === "it" ? it : enUS;
const { mutate } = api.servizio.processServizioOnboard.useMutation({
onError: (error) => {
toast.error(`Errore invio dati: ${error.message}`, {
id: "salvataggio",
});
},
onMutate: () => {
toast.loading("Invio dati in corso", {
id: "salvataggio",
});
},
onSuccess: async () => {
toast.success("Servizio impostato con successo", {
id: "salvataggio",
});
if (isAdmin) {
await router.push(
`/area-riservata/admin/user-view/servizio/${userId}/${initialData.servizio.servizio_id}`,
);
} else {
await router.push(
`/area-riservata/servizio/${initialData.servizio.servizio_id}`,
);
}
},
});
function onSubmit(fields: FormNewServizioValues) {
const a: UsersAnagraficaUpdate = {
cap_residenza: fields.cap_recapiti,
civico_residenza: fields.civico_recapiti,
codice_fiscale: fields.codice_fiscale,
comune_residenza: fields.citta_recapiti,
data_nascita: fields.data_nascita,
idanagrafica: initialData.anagrafica?.idanagrafica,
luogo_nascita: fields.luogo_nascita,
nazione_nascita: fields.nazione_nascita,
nazione_residenza: fields.nazione_recapiti,
provincia_residenza: fields.provincia_recapiti,
sesso: fields.sesso,
userid: userId,
via_residenza: fields.indirizzo_recapiti,
};
const s: ServizioUpdate = {
animali: fields.animali,
arredato: fields.arredato,
ascensore: fields.ascensore,
budget: fields.budget,
entromese: fields.entromese,
fumatori: fields.fumatori,
giardino: fields.giardino,
motivazione_transitorio: fields.motivazione_transitorio,
n_adulti: fields.n_adulti,
n_minori: fields.n_minori,
parcheggio: fields.parcheggio,
permanenza: fields.permanenza,
pianoterra: fields.pianoterra,
reddito: fields.reddito,
terrazzo: fields.terrazzo,
tipologia: fields.tipologia,
skipPayment: fields.skipPayment,
scadenza_motivazione_transitoria: fields.scadenza_motivazione_transitoria,
};
const u: Pick<
Users,
"nome" | "cognome" | "email" | "telefono" | "username"
> = {
cognome: fields.cognome,
email: fields.email,
nome: fields.nome,
telefono: fields.telefono.replace(/\s/g, ""),
username: `${fields.nome.trim()} ${fields.cognome.trim()}`,
};
mutate({
anagrafica: a,
servizio: s,
servizioId: initialData.servizio.servizio_id,
user: u,
userId,
});
}
type altrePrefMappingType = [
["animali", string],
["fumatori", string],
["giardino", string],
["parcheggio", string],
["terrazzo", string],
["ascensore", string],
["pianoterra", string],
];
const altrePrefMapping = t.altriParamsMapping as altrePrefMappingType;
const [tipologia, nazione_nascita, budget] = form.watch([
"tipologia",
"nazione_nascita",
"budget",
]);
const texts =
locale === "it"
? {
dettagli: "Dettagli del servizio",
dettagli_desc: "Questi dati rappresentano la tua ricerca",
personali: "Dati personali",
personali_desc: "Inserisci i tuoi dati personali",
salva: "Salva e procedi",
sdi: "Codice destinatario SDI",
}
: {
dettagli: "Service Details",
dettagli_desc: "These data represent your search",
personali: "Personal Data",
personali_desc: "Enter your personal data",
salva: "Save and proceed",
sdi: "SDI Recipient Code",
};
return (
<Form {...form}>
<form
className="space-y-8 px-0.5 pb-5"
onSubmit={form.handleSubmit(onSubmit)}
>
<Card>
<CardHeader>
<CardTitle>{texts.dettagli}</CardTitle>
<CardDescription>{texts.dettagli_desc}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<FormField
control={form.control}
name="tipologia"
render={({ field }) => (
<FormItem>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-tipologia">
{t.parametri.tipologia}
</FormLabel>
<FormMessage />
</div>
<MultiSelect
defaultValue={
field.value === TipologiaPosizioneEnum.Transitorio
? {
label: "Transitorio",
value: TipologiaPosizioneEnum.Transitorio,
}
: {
label: "Stabile",
value: TipologiaPosizioneEnum.Stabile,
}
}
elementId="select-tipologia"
elementName="tipologia"
isMulti={false}
onValueChange={(value) => {
field.onChange(value.value);
form.setValue("motivazione_transitorio", null);
form.setValue("reddito", null);
}}
options={[
{
label: "Transitorio",
value: TipologiaPosizioneEnum.Transitorio,
},
{
label: "Stabile",
value: TipologiaPosizioneEnum.Stabile,
},
]}
placeholder={t.seleziona_placeholder}
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="budget"
render={({ field }) => (
<FormItem>
<div className="flex flex-wrap items-center gap-x-2 pb-1">
<FormLabel htmlFor="budgetslider">
{`${t.parametri.budget_label1} - ${field.value},00 ${
t.parametri.budget_label2
}`}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Slider
defaultValue={[field.value]}
id="budgetslider"
max={2000}
min={0}
onValueChange={(vals) => {
field.onChange(Number(vals[0]));
}}
step={50}
value={[budget]}
/>
</FormControl>
<FormDescription>{t.parametri.budget_desc}</FormDescription>
</FormItem>
)}
/>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={form.control}
name="entromese"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-mesi">
{t.parametri.dalmese_label}...
</FormLabel>
<FormMessage />
</div>
<FormControl>
<MultiSelect
defaultValue={
field.value !== null
? {
// biome-ignore lint/style/noNonNullAssertion: <known size>
label: t.parametri.mesi[field.value]!,
value: String(field.value),
}
: undefined
}
elementId="select-mesi"
elementName="entro_mese"
isMulti={false}
onValueChange={(value) => {
field.onChange(
value.value !== "" ? parseInt(value.value) : null,
);
}}
options={UnpackOptions({
options: t.parametri.mesi,
})}
placeholder="Seleziona..."
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
disabled={tipologia !== TipologiaPosizioneEnum.Transitorio}
name="permanenza"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel
className={cn(
tipologia !== TipologiaPosizioneEnum.Transitorio &&
"opacity-50",
)}
htmlFor="permanenza"
>
{t.parametri.permanenza_label}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<MultiSelect
defaultValue={
field.value
? {
label:
t.parametri.permanenza[field.value] || "",
value: field.value.toString(),
}
: undefined
}
disabled={
tipologia !== TipologiaPosizioneEnum.Transitorio
}
elementId="permanenza"
elementName="permanenza"
isMulti={false}
onValueChange={(value) => {
field.onChange(
value.value !== "" ? parseInt(value.value) : null,
);
}}
options={UnpackOptions({
options: t.parametri.permanenza,
})}
placeholder="Seleziona..."
/>
</FormControl>
{tipologia === TipologiaPosizioneEnum.Transitorio && (
<FormDescription>
{t.parametri.permanenza_desc}
</FormDescription>
)}
</FormItem>
)}
/>
</div>
<div className="flex w-full flex-col items-center justify-between gap-4 sm:flex-row">
<div className="flex w-full items-center justify-between gap-4">
<FormField
control={form.control}
name="n_adulti"
render={({ field }) => (
<FormItem className={cn("w-full")}>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel className={cn("")} htmlFor="n_adulti">
{t.parametri.nadulti_label}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Counter
name="n_adulti"
onlyPositive
setValue={(v) => field.onChange(Number(v))}
value={field.value}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="n_minori"
render={({ field }) => (
<FormItem className={cn("w-full")}>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel className={cn("")} htmlFor="n_minori">
{t.parametri.nminori_label}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Counter
name="n_minori"
onlyPositive
setValue={(v) => field.onChange(Number(v))}
value={field.value}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="arredato"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="arredato">
{t.parametri.arredato_label}
</FormLabel>
<FormControl>
<Switch
className="data-[state=checked]:bg-neutral-700"
defaultChecked={field.value}
id="arredato"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</div>
<FormDescription>
{t.parametri.arredato_desc}
</FormDescription>
</FormItem>
)}
/>
</div>
{tipologia === TipologiaPosizioneEnum.Transitorio ? (
<div className="items-top flex w-full flex-col justify-between gap-4 sm:flex-row">
<FormField
control={form.control}
key={tipologia}
name="motivazione_transitorio"
render={({ field }) => {
const existingValue = t.parametri.motivazioni_mappings.find(
(m) => m.id === field.value,
);
return (
<FormItem className="w-full">
<div className="flex w-full flex-wrap items-center gap-x-2">
<FormLabel htmlFor="motivazione_transitorio">
{t.parametri.motivazione_label}
</FormLabel>
<FormMessage />
</div>
<MultiSelect
defaultValue={
existingValue
? {
label: existingValue.title,
value: existingValue.id,
}
: undefined
}
elementId="select-motivazione_transitorio"
elementName="motivazione_transitorio"
isMulti={false}
onValueChange={(value) => {
field.onChange(value.value);
}}
options={t.parametri.motivazioni_mappings.map(
(option) => ({
label: option.title,
value: option.id,
}),
)}
placeholder={t.seleziona_placeholder}
/>
<FormDescription>
{t.parametri.motivazione_desc}
</FormDescription>
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="scadenza_motivazione_transitoria"
render={({ field }) => (
<FormItem className="flex w-full flex-col">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="scadenza_motivazione_transitoria">
Scadenza motivazione
</FormLabel>
<FormMessage />
</div>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
className={cn(
"h-10.5 w-full 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="scadenza_motivazione_transitoria"
variant={"outline"}
>
{field.value ? (
format(field.value, "PPP", {
locale: it,
})
) : (
<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 || new Date()}
endMonth={new Date(2050, 12)}
locale={it}
mode="single"
onSelect={field.onChange}
selected={field.value || undefined}
/>
</PopoverContent>
</Popover>
</FormItem>
)}
/>
</div>
) : (
<FormField
control={form.control}
key={tipologia}
name="reddito"
render={({ field }) => (
<FormItem>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-reddito">
{t.parametri.reddito_label}
</FormLabel>
<FormMessage />
</div>
<MultiSelect
defaultValue={undefined}
elementId="select-reddito"
elementName="reddito"
isMulti={false}
onValueChange={(value) => {
field.onChange(value.value);
}}
options={UnpackOptions({
options: t.parametri.reddito_options,
})}
placeholder={t.seleziona_placeholder}
/>
</FormItem>
)}
/>
)}
<div className="flex flex-col space-y-2">
<Label htmlFor="altrePrefDialogButton">
{t.parametri.altriparams_label}
</Label>
<FormDescription>{t.parametri.altriparams_desc}</FormDescription>
<div className="mt-2 flex flex-wrap gap-2">
{altrePrefMapping.map((pair) => {
if (pair) {
return (
<FormField
control={form.control}
key={pair[0]}
name={pair[0]}
render={({ field }) => (
<FormItem>
<FormControl>
<Badge
className={cn("cursor-pointer text-sm")}
onClick={() => {
field.onChange(!field.value);
}}
variant={field.value ? "default" : "secondary"}
>
{pair[1]}
</Badge>
</FormControl>
</FormItem>
)}
/>
);
}
})}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{texts.personali}</CardTitle>
<CardDescription>{texts.personali_desc}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={form.control}
name="nome"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="nome">{t.account.nome}</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input {...field} id="nome" type="text" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cognome"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="cognome">
{t.account.cognome}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input {...field} id="cognome" type="text" />
</FormControl>
</FormItem>
)}
/>
</div>
<Separator />
<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="dataNascita">
{t.anagrafica.dataNascita}
</FormLabel>
<FormMessage />
</div>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
className={cn(
"h-10.5 w-full 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="dataNascita"
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-nascita">
{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-nascita"
elementName="nazioneNascita"
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 (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-nascita">
{t.anagrafica.luogoNascita}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<MultiSelect
defaultValue={
field.value
? parsedComuneNascita
? {
label: `${parsedComuneNascita.nome} (${parsedComuneNascita.sigla})`,
value: parsedComuneNascita.catasto,
}
: {
label: field.value,
value: field.value,
}
: undefined
}
elementId="select-comune-nascita"
elementName="luogoNascita"
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>
);
}}
/>
<FormField
control={form.control}
name="sesso"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex items-center gap-x-2">
<FormLabel htmlFor="select-sesso">
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger className="relative flex items-center gap-x-2">
{t.anagrafica.sesso}{" "}
<HelpCircleIcon className="absolute -right-6 size-5" />
</TooltipTrigger>
<TooltipContent>
{t.anagrafica.sesso_disclamer}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormMessage />
</div>
<MultiSelect
defaultValue={t.anagrafica.sesso_options.filter(
(v) => v.value === field.value,
)}
elementId="select-sesso"
elementName="sesso"
isMulti={false}
onValueChange={(value) => {
field.onChange(value.value);
}}
options={t.anagrafica.sesso_options}
placeholder=""
/>
</FormItem>
)}
/>
</div>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<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>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="email">{t.account.email}</FormLabel>
<FormMessage />
</div>
<FormControl>
<InputWIcon
{...field}
iconPosition="start"
id="email"
type="email"
>
<Mail aria-hidden="true" size={16} strokeWidth={2} />
</InputWIcon>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="telefono"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="numero">
{t.account.telefono}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<PhoneInput {...field} id="telefono" />
</FormControl>
</FormItem>
)}
/>
</div>
<Separator />
<ResidenzaSection
control={form.control}
setValue={form.setValue}
trigger={form.trigger}
/>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={form.control}
name="indirizzo_recapiti"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="indirizzo">
{t.recapiti.indirizzo}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
id="indirizzo"
placeholder="Via Roma"
type="text"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="civico_recapiti"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="civico">
{t.recapiti.civico}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
id="civico"
placeholder="50"
type="text"
/>
</FormControl>
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
<div className="flex w-full flex-wrap items-center justify-center gap-2 pb-8 sm:justify-end">
<Button className="w-full sm:w-auto" type="submit">
{texts.salva}
</Button>
</div>
</form>
</Form>
);
};
const ResidenzaSection = ({
control,
trigger,
setValue,
}: {
control: Control<FormNewServizioValues>;
trigger: (name?: keyof FormNewServizioValues) => Promise<boolean>;
// biome-ignore lint/suspicious/noExplicitAny: <fine here>
setValue: (field: keyof FormNewServizioValues, value: any) => void;
}) => {
const { t } = useTranslation();
const { comuni, nazioni, provincie } = useCatasto();
const nazione_recapiti = useWatch({
control,
name: "nazione_recapiti",
});
const provincia_recapiti = useWatch({
control,
name: "provincia_recapiti",
});
return (
<>
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={control}
name="nazione_recapiti"
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="select-naz">
{t.recapiti.nazione}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<MultiSelect
defaultValue={
field.value
? parsedNazioneResidenza
? {
label: parsedNazioneResidenza.nome,
value: parsedNazioneResidenza.catasto,
}
: {
label: field.value,
value: field.value,
}
: undefined
}
elementId="select-naz"
elementName="nazioneRecapiti"
isMulti={false}
onValueChange={async (value) => {
setValue("provincia_recapiti", null);
setValue("citta_recapiti", null);
field.onChange(value.value);
await trigger("citta_recapiti");
}}
options={nazioni.map((n) => ({
label: n.nome,
value: n.catasto,
}))}
placeholder="Seleziona..."
/>
</FormControl>
</FormItem>
);
}}
/>
<FormField
control={control}
name="provincia_recapiti"
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="provinciaRecapiti">
{t.recapiti.provincia}
</FormLabel>
<FormMessage />
</div>
{nazione_recapiti === ITALY_CATASTO_CODE ? (
<MultiSelect
defaultValue={
field.value && parseProvincia
? {
label: parseProvincia.nome,
value: parseProvincia.sigla,
}
: undefined
}
elementId="provinciaRecapiti"
elementName="provincia"
isMulti={false}
onValueChange={(value) => {
field.onChange(value.value);
setValue("citta_recapiti", null);
}}
options={provincie.map((prov) => ({
label: prov.nome,
value: prov.sigla,
}))}
placeholder="Seleziona"
/>
) : (
<FormControl>
<Input
{...field}
id="provinciaRecapiti"
onChange={(e) =>
NullableStringOnChange(e, field.onChange)
}
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={control}
name="citta_recapiti"
render={({ field }) => {
if (nazione_recapiti === ITALY_CATASTO_CODE) {
const parsedComuneResidenza = parseDBComune(field.value, comuni);
const options = comuni
.filter((comune) =>
provincia_recapiti
? comune.sigla === provincia_recapiti
: 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="citta_recapiti"
isMulti={false}
onValueChange={async (value) => {
field.onChange(value.value);
await trigger("citta_recapiti");
}}
options={options}
placeholder="Seleziona..."
/>
</FormControl>
</FormItem>
);
}
return (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="citta_recapiti">
{t.recapiti.comune}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
{...field}
id="citta_recapiti"
onChange={async (e) => {
NullableStringOnChange(e, field.onChange);
await trigger("citta_recapiti");
}}
type="text"
value={field.value || ""}
/>
</FormControl>
</FormItem>
);
}}
/>
<FormField
control={control}
name="cap_recapiti"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="cap">{t.recapiti.cap}</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input {...field} id="cap" placeholder="00100" type="text" />
</FormControl>
</FormItem>
)}
/>
</div>
</>
);
};