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

687 lines
20 KiB
TypeScript
Raw Normal View History

import { format } from "date-fns";
import { it } from "date-fns/locale";
import { CalendarIcon } from "lucide-react";
import { z } from "zod/v4";
2025-08-28 18:27:07 +02:00
import { Counter } from "~/components/counter";
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
2025-08-04 17:45:44 +02:00
} from "~/components/custom_ui/form";
2025-08-28 18:27:07 +02:00
import { MultiSelect, UnpackOptions } from "~/components/custom_ui/multiselect";
import { Badge } from "~/components/ui/badge";
2025-08-04 17:45:44 +02:00
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
2025-08-28 18:27:07 +02:00
import { Label } from "~/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { Separator } from "~/components/ui/separator";
2025-08-04 17:45:44 +02:00
import { Slider } from "~/components/ui/slider";
import { Switch } from "~/components/ui/switch";
2025-08-28 18:27:07 +02:00
import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm";
import { useTranslation } from "~/providers/I18nProvider";
2025-08-04 17:45:44 +02:00
import type { Servizio } from "~/schemas/public/Servizio";
2025-08-28 18:27:07 +02:00
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import { FieldResetButton } from "./comps";
2025-08-04 17:45:44 +02:00
const Schema = z
2025-08-28 18:27:07 +02:00
.object({
2025-08-29 16:18:32 +02:00
animali: z.boolean(),
arredato: z.boolean(),
ascensore: z.boolean(),
2025-08-28 18:27:07 +02:00
budget: z.number(),
entromese: z.number().nullable(),
2025-08-29 16:18:32 +02:00
fumatori: z.boolean(),
giardino: z.boolean(),
motivazione_transitorio: z.string().nullable(),
2025-08-28 18:27:07 +02:00
n_adulti: z.number().min(1),
n_minori: z.number(),
2025-08-29 16:18:32 +02:00
parcheggio: z.boolean(),
2025-08-28 18:27:07 +02:00
permanenza: z.number().nullable(),
2025-08-29 16:18:32 +02:00
pianoterra: z.boolean(),
2025-08-28 18:27:07 +02:00
reddito: z.string().nullable(),
terrazzo: z.boolean(),
2025-08-29 16:18:32 +02:00
tipologia: z.custom<TipologiaPosizioneEnum>(),
scadenza_motivazione_transitoria: z.date().nullable(),
skipPayment: z.boolean(),
skipControlloDoc: z.boolean(),
skipDocMotivazione: z.boolean(),
2025-08-28 18:27:07 +02:00
})
.superRefine(({ n_adulti }, refinementContext) => {
if (n_adulti <= 0) {
refinementContext.issues.push({
code: "custom",
2025-08-29 16:18:32 +02:00
input: "",
2025-08-28 18:27:07 +02:00
message: "Inserisci il numero di persone",
path: ["npersone"],
});
}
});
2025-08-04 17:45:44 +02:00
export type FormValues = z.infer<typeof Schema>;
2025-08-04 17:45:44 +02:00
export const FormServizio = ({
2025-08-28 18:27:07 +02:00
initialData,
onSubmit,
isLimited,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
initialData: Servizio | undefined;
onSubmit: (fields: FormValues) => void;
isLimited: boolean;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
// This can come from your database or API.
let defaultValues: FormValues;
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
if (initialData) {
defaultValues = {
...initialData,
};
} else {
defaultValues = {
2025-08-29 16:18:32 +02:00
animali: false,
arredato: false,
ascensore: false,
2025-08-28 18:27:07 +02:00
budget: 0,
entromese: null,
2025-08-29 16:18:32 +02:00
fumatori: false,
giardino: false,
motivazione_transitorio: "",
2025-08-28 18:27:07 +02:00
n_adulti: 1,
n_minori: 0,
2025-08-29 16:18:32 +02:00
parcheggio: false,
2025-08-28 18:27:07 +02:00
permanenza: null,
2025-08-29 16:18:32 +02:00
pianoterra: false,
2025-08-28 18:27:07 +02:00
reddito: "",
terrazzo: false,
2025-08-29 16:18:32 +02:00
tipologia: TipologiaPosizioneEnum.Transitorio,
scadenza_motivazione_transitoria: null,
skipPayment: false,
skipControlloDoc: true,
skipDocMotivazione: true,
2025-08-28 18:27:07 +02:00
};
}
const { locale, t } = useTranslation();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
z.config(z.locales[locale]());
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const form = useZodForm(Schema, {
defaultValues: defaultValues,
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
type altrePrefMappingType = [
["animali", string],
["fumatori", string],
["giardino", string],
["parcheggio", string],
["terrazzo", string],
["ascensore", string],
["pianoterra", string],
];
const altrePrefMapping = t.altriParamsMapping as altrePrefMappingType;
2025-08-28 18:27:07 +02:00
const tipologia = form.watch("tipologia");
return (
<Form {...form}>
2025-08-29 16:18:32 +02:00
<form className="space-y-8 px-0.5" onSubmit={form.handleSubmit(onSubmit)}>
2025-08-28 18:27:07 +02:00
<FormField
control={form.control}
name="tipologia"
render={({ field }) => (
<FormItem>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-tipologia">Tipologia</FormLabel>
<FormMessage />
</div>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
{isLimited ? (
<div className="space-y-0">
2025-10-10 16:18:43 +02:00
<p className="font-semibold text-lg">{field.value}</p>
2025-08-28 18:27:07 +02:00
<p className="text-muted-foreground text-sm">
La tipologia non può essere modificata. Contatta il supporto
se necessario.
</p>
</div>
) : (
<MultiSelect
defaultValue={
field.value === TipologiaPosizioneEnum.Transitorio
? {
label: "Transitorio",
2025-08-29 16:18:32 +02:00
value: TipologiaPosizioneEnum.Transitorio,
2025-08-28 18:27:07 +02:00
}
: {
label: "Stabile",
2025-08-29 16:18:32 +02:00
value: TipologiaPosizioneEnum.Stabile,
2025-08-28 18:27:07 +02:00
}
}
2025-08-29 16:18:32 +02:00
elementId="select-tipologia"
elementName="tipologia"
isMulti={false}
onValueChange={(value) => {
field.onChange(value.value);
form.setValue("motivazione_transitorio", null);
form.setValue("reddito", null);
}}
2025-08-28 18:27:07 +02:00
options={[
{
label: "Transitorio",
2025-08-29 16:18:32 +02:00
value: TipologiaPosizioneEnum.Transitorio,
2025-08-28 18:27:07 +02:00
},
{
label: "Stabile",
2025-08-29 16:18:32 +02:00
value: TipologiaPosizioneEnum.Stabile,
2025-08-28 18:27:07 +02:00
},
]}
2025-08-29 16:18:32 +02:00
placeholder={t.seleziona_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="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.toLocaleString("it")},00 ${
t.parametri.budget_label2
2025-08-28 18:27:07 +02:00
}`}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Slider
defaultValue={[field.value]}
2025-08-29 16:18:32 +02:00
id="budgetslider"
max={2000}
min={0}
2025-08-28 18:27:07 +02:00
onValueChange={(vals) => {
field.onChange(Number(vals[0]));
}}
2025-08-29 16:18:32 +02:00
step={50}
2025-08-28 18:27:07 +02:00
value={[form.getValues("budget")]}
/>
</FormControl>
<FormDescription>{t.parametri.budget_desc}</FormDescription>
2025-08-28 18:27:07 +02:00
</FormItem>
)}
/>
<div className="flex w-full items-start justify-between gap-x-4">
<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}...
2025-08-28 18:27:07 +02:00
</FormLabel>
{field.value !== null && !initialData && (
<FieldResetButton
2025-08-28 18:27:07 +02:00
onClick={() => form.setValue("entromese", null)}
/>
2025-08-28 18:27:07 +02:00
)}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<FormMessage />
</div>
<FormControl>
<MultiSelect
defaultValue={
field.value !== null
? {
// biome-ignore lint/style/noNonNullAssertion: <known size>
label: t.parametri.mesi[field.value]!,
2025-08-29 16:18:32 +02:00
value: String(field.value),
2025-08-28 18:27:07 +02:00
}
: undefined
}
2025-08-29 16:18:32 +02:00
elementId="select-mesi"
elementName="entro_mese"
isMulti={false}
key={field.value}
2025-08-28 18:27:07 +02:00
onValueChange={(value) => {
field.onChange(
value.value !== "" ? parseInt(value.value) : null,
);
}}
2025-08-29 16:18:32 +02:00
options={UnpackOptions({
options: t.parametri.mesi,
2025-08-29 16:18:32 +02:00
})}
2025-08-28 18:27:07 +02:00
placeholder="Seleziona..."
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
disabled={tipologia !== TipologiaPosizioneEnum.Transitorio}
2025-08-29 16:18:32 +02:00
name="permanenza"
2025-08-28 18:27:07 +02:00
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}
2025-08-28 18:27:07 +02:00
</FormLabel>
{field.value !== null && !initialData && (
<FieldResetButton
2025-08-28 18:27:07 +02:00
onClick={() => form.setValue("permanenza", null)}
/>
2025-08-28 18:27:07 +02:00
)}
<FormMessage />
</div>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<FormControl>
<MultiSelect
defaultValue={
field.value !== null
2025-08-28 18:27:07 +02:00
? {
label: t.parametri.permanenza[field.value] || "",
2025-08-29 16:18:32 +02:00
value: field.value.toString(),
2025-08-28 18:27:07 +02:00
}
: undefined
}
2025-08-29 16:18:32 +02:00
disabled={tipologia !== TipologiaPosizioneEnum.Transitorio}
elementId="permanenza"
elementName="permanenza"
isMulti={false}
key={field.value}
2025-08-28 18:27:07 +02:00
onValueChange={(value) => {
field.onChange(
value.value !== "" ? parseInt(value.value) : null,
);
}}
2025-08-29 16:18:32 +02:00
options={UnpackOptions({
options: t.parametri.permanenza,
2025-08-29 16:18:32 +02:00
})}
2025-08-28 18:27:07 +02:00
placeholder="Seleziona..."
/>
</FormControl>
{tipologia === TipologiaPosizioneEnum.Transitorio && (
<FormDescription>
{t.parametri.permanenza_desc}
</FormDescription>
)}
2025-08-28 18:27:07 +02:00
</FormItem>
)}
/>
</div>
<div className="flex w-full items-center justify-between gap-x-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
2025-10-10 16:18:43 +02:00
className={cn("font-semibold text-base")}
2025-08-28 18:27:07 +02:00
htmlFor="n_adulti"
>
{t.parametri.nadulti_label}
2025-08-28 18:27:07 +02:00
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Counter
name="n_adulti"
onlyPositive
2025-08-29 16:18:32 +02:00
setValue={(value) => field.onChange(Number(value))}
value={Number(field.value)}
2025-08-28 18:27:07 +02:00
/>
</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
2025-10-10 16:18:43 +02:00
className={cn("font-semibold text-base")}
2025-08-28 18:27:07 +02:00
htmlFor="n_minori"
>
{t.parametri.nminori_label}
2025-08-28 18:27:07 +02:00
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Counter
name="n_minori"
onlyPositive
2025-08-29 16:18:32 +02:00
setValue={(value) => field.onChange(Number(value))}
value={Number(field.value)}
2025-08-28 18:27:07 +02:00
/>
</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}
2025-08-28 18:27:07 +02:00
</FormLabel>
<FormControl>
<Switch
className="data-[state=checked]:bg-neutral-700"
defaultChecked={field.value}
2025-08-29 16:18:32 +02:00
id="arredato"
onCheckedChange={field.onChange}
2025-08-28 18:27:07 +02:00
/>
</FormControl>
<FormMessage />
</div>
2025-08-04 17:45:44 +02:00
<FormDescription>{t.parametri.arredato_desc}</FormDescription>
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
{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>
{isLimited ? (
<div className="space-y-0">
<p className="font-semibold text-lg">
{existingValue?.title}
</p>
<p className="text-muted-foreground text-sm">
La motivazione non può essere modificata. Contatta il
supporto se necessario.
</p>
</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">
2025-08-28 18:27:07 +02:00
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="scadenza_motivazione_transitoria">
Scadenza motivazione
2025-08-28 18:27:07 +02:00
</FormLabel>
2025-08-28 18:27:07 +02:00
<FormMessage />
</div>
{isLimited ? (
<div className="space-y-0">
<p className="font-semibold text-lg">
{field.value
? format(field.value, "dd/MM/yyyy")
: "Errore data invalida"}
</p>
<p className="text-muted-foreground text-sm">
La scadenza non può essere modificata. Contatta il
supporto se necessario.
</p>
</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="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>
)}
<FormDescription>
es. scadenza contratto di lavoro, fine corso di studi, ecc.
</FormDescription>
2025-08-28 18:27:07 +02:00
</FormItem>
)}
/>
</div>
2025-08-28 18:27:07 +02:00
) : (
<FormField
control={form.control}
2025-08-29 16:18:32 +02:00
key={tipologia}
2025-08-28 18:27:07 +02:00
name="reddito"
render={({ field }) => (
<FormItem>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="select-reddito">
{t.parametri.reddito_label}
2025-08-28 18:27:07 +02:00
</FormLabel>
<FormMessage />
</div>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<MultiSelect
2025-08-29 16:18:32 +02:00
defaultValue={undefined}
2025-08-28 18:27:07 +02:00
elementId="select-reddito"
elementName="reddito"
isMulti={false}
onValueChange={(value) => {
field.onChange(value.value);
}}
2025-08-29 16:18:32 +02:00
options={UnpackOptions({
options: t.parametri.reddito_options,
2025-08-29 16:18:32 +02:00
})}
placeholder={t.seleziona_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
<div className="flex flex-col space-y-2">
<Label htmlFor="altrePrefDialogButton">
{t.parametri.altriparams_label}
2025-08-28 18:27:07 +02:00
</Label>
<FormDescription>{t.parametri.altriparams_desc}</FormDescription>
2025-08-28 18:27:07 +02:00
<div className="mt-2 flex flex-wrap gap-2">
{altrePrefMapping.map((pair) => {
2025-08-28 18:27:07 +02:00
if (pair) {
return (
<FormField
control={form.control}
key={pair[0]}
2025-08-28 18:27:07 +02:00
name={pair[0]}
render={({ field }) => (
<FormItem>
<FormControl>
<Badge
className={cn("cursor-pointer text-sm")}
2025-08-29 16:18:32 +02:00
onClick={() => {
field.onChange(!field.value);
}}
variant={field.value ? "default" : "secondary"}
2025-08-28 18:27:07 +02:00
>
{pair[1]}
</Badge>
</FormControl>
</FormItem>
)}
/>
);
}
})}
</div>
</div>
<Separator />
{!isLimited && (
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
<FormField
control={form.control}
name="skipPayment"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="skipPayment">Salta Pagamento</FormLabel>
<FormControl>
<Switch
className="data-[state=checked]:bg-neutral-700"
defaultChecked={field.value}
id="skipPayment"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</div>
<FormDescription>
Salta la richiesta di pagamento al cliente (per tutti i
pagamenti)
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="skipControlloDoc"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="skipControlloDoc">
Salta controllo documento
</FormLabel>
<FormControl>
<Switch
className="data-[state=checked]:bg-neutral-700"
defaultChecked={field.value}
id="skipControlloDoc"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</div>
<FormDescription>
Salta la richiesta del documento del cliente al momento
dello sblocco contatti
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="skipDocMotivazione"
render={({ field }) => (
<FormItem className="w-full">
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="skipDocMotivazione">
Salta documento motivazione
</FormLabel>
<FormControl>
<Switch
className="data-[state=checked]:bg-neutral-700"
defaultChecked={field.value}
id="skipDocMotivazione"
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</div>
<FormDescription>
Salta l&apos;invio del documento di motivazione al cliente
al momento della conferma annuncio
</FormDescription>
</FormItem>
)}
/>
</div>
)}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<Button type="submit">Salva</Button>
</form>
</Form>
);
2025-08-04 17:45:44 +02:00
};