feat(forms): Add skip options and expiration date for temporary motivation in service forms

This commit is contained in:
Marco Pedone 2025-10-30 19:39:35 +01:00
parent 7dcbaa68f3
commit 9e495caf29
5 changed files with 405 additions and 104 deletions

View file

@ -303,7 +303,7 @@ const EditPreferenze = () => {
Preferenze Preferenze
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody className="max-h-[80vh] w-full overflow-y-auto pb-5"> <CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5">
{isLoading ? ( {isLoading ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (

View file

@ -32,6 +32,9 @@ const Schema = z.object({
isOkConsulenza: z.boolean(), isOkConsulenza: z.boolean(),
isOkSaldo: z.boolean(), isOkSaldo: z.boolean(),
isSent: z.boolean(), isSent: z.boolean(),
skipPayment: z.boolean(),
skipControlloDoc: z.boolean(),
skipDocMotivazione: z.boolean(),
}); });
export type FormValues = z.infer<typeof Schema>; export type FormValues = z.infer<typeof Schema>;
@ -58,6 +61,9 @@ export const FormEditServizio = ({
isOkConsulenza: false, isOkConsulenza: false,
isOkSaldo: false, isOkSaldo: false,
isSent: false, isSent: false,
skipPayment: false,
skipControlloDoc: false,
skipDocMotivazione: false,
}; };
} }
const { locale, t } = useTranslation(); const { locale, t } = useTranslation();
@ -161,10 +167,7 @@ export const FormEditServizio = ({
autoFocus autoFocus
captionLayout="dropdown" captionLayout="dropdown"
defaultMonth={field.value || new Date()} defaultMonth={field.value || new Date()}
disabled={(date) => endMonth={new Date(2050, 12)}
date > new Date() || date < new Date("1900-01-01")
}
endMonth={new Date()}
locale={DatePickerLocale} locale={DatePickerLocale}
mode="single" mode="single"
onSelect={field.onChange} onSelect={field.onChange}
@ -266,6 +269,85 @@ export const FormEditServizio = ({
)} )}
/> />
</div> </div>
<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
checked={field.value}
className="data-[state=checked]:bg-neutral-700"
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
checked={field.value}
className="data-[state=checked]:bg-neutral-700"
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
checked={field.value}
className="data-[state=checked]:bg-neutral-700"
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>
<Button type="submit">Salva</Button> <Button type="submit">Salva</Button>
</form> </form>
</Form> </Form>

View file

@ -1,3 +1,6 @@
import { format } from "date-fns";
import { it } from "date-fns/locale";
import { CalendarIcon } from "lucide-react";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { Counter } from "~/components/counter"; import { Counter } from "~/components/counter";
import { import {
@ -12,7 +15,14 @@ import {
import { MultiSelect, UnpackOptions } from "~/components/custom_ui/multiselect"; import { MultiSelect, UnpackOptions } from "~/components/custom_ui/multiselect";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import { Label } from "~/components/ui/label"; 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 { Slider } from "~/components/ui/slider";
import { Switch } from "~/components/ui/switch"; import { Switch } from "~/components/ui/switch";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
@ -39,6 +49,10 @@ const Schema = z
reddito: z.string().nullable(), reddito: z.string().nullable(),
terrazzo: z.boolean(), terrazzo: z.boolean(),
tipologia: z.custom<TipologiaPosizioneEnum>(), tipologia: z.custom<TipologiaPosizioneEnum>(),
scadenza_motivazione_transitoria: z.date().nullable(),
skipPayment: z.boolean(),
skipControlloDoc: z.boolean(),
skipDocMotivazione: z.boolean(),
}) })
.superRefine(({ n_adulti }, refinementContext) => { .superRefine(({ n_adulti }, refinementContext) => {
if (n_adulti <= 0) { if (n_adulti <= 0) {
@ -87,6 +101,10 @@ export const FormNewServizio = ({
reddito: "", reddito: "",
terrazzo: false, terrazzo: false,
tipologia: TipologiaPosizioneEnum.Transitorio, tipologia: TipologiaPosizioneEnum.Transitorio,
scadenza_motivazione_transitoria: null,
skipPayment: false,
skipControlloDoc: false,
skipDocMotivazione: false,
}; };
} }
const { locale, t } = useTranslation(); const { locale, t } = useTranslation();
@ -381,66 +399,118 @@ export const FormNewServizio = ({
/> />
{tipologia === TipologiaPosizioneEnum.Transitorio ? ( {tipologia === TipologiaPosizioneEnum.Transitorio ? (
<FormField <div className="items-top flex w-full flex-col justify-between gap-4 sm:flex-row">
control={form.control} <FormField
key={tipologia} control={form.control}
name="motivazione_transitorio" key={tipologia}
render={({ field }) => { name="motivazione_transitorio"
const existingValue = t.preferenze.motivazioni_mappings.find( render={({ field }) => {
(m) => m.id === field.value, const existingValue = t.preferenze.motivazioni_mappings.find(
); (m) => m.id === field.value,
return ( );
<FormItem> return (
<FormItem className="w-full">
<div className="flex w-full flex-wrap items-center gap-x-2">
<FormLabel htmlFor="motivazione_transitorio">
{t.preferenze.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.preferenze.motivazioni_mappings.map(
(option) => ({
label: option.title,
value: option.id,
}),
)}
placeholder={t.seleziona_placeholder}
/>
<FormDescription>
{t.preferenze.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"> <div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="motivazione_transitorio"> <FormLabel htmlFor="scadenza_motivazione_transitoria">
{t.preferenze.motivazione_label} Scadenza motivazione
</FormLabel> </FormLabel>
<FormMessage /> <FormMessage />
</div> </div>
{isLimited ? ( <Popover>
<div className="space-y-0"> <PopoverTrigger asChild>
<p className="font-semibold text-lg"> <FormControl>
{existingValue?.title} <Button
</p> className={cn(
<p className="text-muted-foreground text-sm"> "h-[42px] w-full pl-3 text-left font-medium",
La motivazione non può essere modificata. Contatta il !field.value && "text-muted-foreground",
supporto se necessario. `rounded-lg border border-neutral-300 bg-white text-primary shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm hover:border-neutral-400 focus:border-neutral-400 disabled:cursor-not-allowed disabled:opacity-50 aria-expanded:border-neutral-400 dark:border-neutral-500 dark:bg-primary dark:text-white dark:aria-expanded:border-neutral-400 dark:focus:border-transparent dark:hover:border-neutral-400 dark:placeholder:text-neutral-400`,
</p> )}
</div> id="scadenza_motivazione_transitoria"
) : ( variant={"outline"}
<> >
<MultiSelect {field.value ? (
defaultValue={ format(field.value, "PPP", {
existingValue locale: it,
? { })
label: existingValue.title, ) : (
value: existingValue.id, <span>{t.anagrafica.scegli_data}</span>
} )}
: undefined <CalendarIcon className="ml-auto size-4 opacity-50" />
} </Button>
elementId="select-motivazione_transitorio" </FormControl>
elementName="motivazione_transitorio" </PopoverTrigger>
isMulti={false} <PopoverContent align="start" className="w-auto p-0">
onValueChange={(value) => { <Calendar
field.onChange(value.value); autoFocus
}} captionLayout="dropdown"
options={t.preferenze.motivazioni_mappings.map( defaultMonth={field.value || new Date()}
(option) => ({ endMonth={new Date(2050, 12)}
label: option.title, locale={it}
value: option.id, mode="single"
}), onSelect={field.onChange}
)} selected={field.value || undefined}
placeholder={t.seleziona_placeholder}
/> />
<FormDescription> </PopoverContent>
{t.preferenze.motivazione_desc} </Popover>
</FormDescription>
</>
)}
</FormItem> </FormItem>
); )}
}} />
/> </div>
) : ( ) : (
<FormField <FormField
control={form.control} control={form.control}
@ -511,6 +581,87 @@ export const FormNewServizio = ({
})} })}
</div> </div>
</div> </div>
<Separator />
<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
checked={field.value}
className="data-[state=checked]:bg-neutral-700"
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
checked={field.value}
className="data-[state=checked]:bg-neutral-700"
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
checked={field.value}
className="data-[state=checked]:bg-neutral-700"
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>
<Button type="submit">Salva</Button> <Button type="submit">Salva</Button>
</form> </form>

View file

@ -112,6 +112,7 @@ const FormNewServizioSchema = z.object({
telefono: z.string(), telefono: z.string(),
terrazzo: z.boolean(), terrazzo: z.boolean(),
tipologia: z.custom<TipologiaPosizioneEnum>(), tipologia: z.custom<TipologiaPosizioneEnum>(),
scadenza_motivazione_transitoria: z.date().nullable(),
}); });
type FormNewServizioValues = z.infer<typeof FormNewServizioSchema>; type FormNewServizioValues = z.infer<typeof FormNewServizioSchema>;
@ -140,6 +141,7 @@ export const FormNewServizioAcquisto = ({
permanenza, permanenza,
entromese, entromese,
motivazione_transitorio, motivazione_transitorio,
scadenza_motivazione_transitoria,
reddito, reddito,
azienda, azienda,
p_iva, p_iva,
@ -254,6 +256,14 @@ export const FormNewServizioAcquisto = ({
path: ["motivazione_transitorio"], path: ["motivazione_transitorio"],
}); });
} }
if (!scadenza_motivazione_transitoria) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci la scadenza della motivazione",
path: ["scadenza_motivazione_transitoria"],
});
}
} else { } else {
if (reddito === "" || reddito === null) { if (reddito === "" || reddito === null) {
refinementContext.issues.push({ refinementContext.issues.push({
@ -684,54 +694,107 @@ export const FormNewServizioAcquisto = ({
</div> </div>
{tipologia === TipologiaPosizioneEnum.Transitorio ? ( {tipologia === TipologiaPosizioneEnum.Transitorio ? (
<FormField <>
control={form.control} <FormField
key={tipologia} control={form.control}
name="motivazione_transitorio" key={tipologia}
render={({ field }) => { name="motivazione_transitorio"
const existingValue = t.preferenze.motivazioni_mappings.find( render={({ field }) => {
(m) => m.id === field.value, const existingValue =
); t.preferenze.motivazioni_mappings.find(
(m) => m.id === field.value,
);
return ( return (
<FormItem> <FormItem>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="motivazione_transitorio">
{t.preferenze.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.preferenze.motivazioni_mappings.map(
(option) => ({
label: option.title,
value: option.id,
}),
)}
placeholder={t.seleziona_placeholder}
/>
<FormDescription>
{t.preferenze.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"> <div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="motivazione_transitorio"> <FormLabel htmlFor="scadenza_motivazione_transitoria">
{t.preferenze.motivazione_label} Scadenza motivazione
</FormLabel> </FormLabel>
<FormMessage /> <FormMessage />
</div> </div>
<MultiSelect <Popover>
defaultValue={ <PopoverTrigger asChild>
existingValue <FormControl>
? { <Button
label: existingValue.title, className={cn(
value: existingValue.id, "h-[42px] w-full pl-3 text-left font-medium",
} !field.value && "text-muted-foreground",
: undefined `rounded-lg border border-neutral-300 bg-white text-primary shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm hover:border-neutral-400 focus:border-neutral-400 disabled:cursor-not-allowed disabled:opacity-50 aria-expanded:border-neutral-400 dark:border-neutral-500 dark:bg-primary dark:text-white dark:aria-expanded:border-neutral-400 dark:focus:border-transparent dark:hover:border-neutral-400 dark:placeholder:text-neutral-400`,
} )}
elementId="select-motivazione_transitorio" id="scadenza_motivazione_transitoria"
elementName="motivazione_transitorio" variant={"outline"}
isMulti={false} >
onValueChange={(value) => { {field.value ? (
field.onChange(value.value); format(field.value, "PPP", {
}} locale: it,
options={t.preferenze.motivazioni_mappings.map( })
(option) => ({ ) : (
label: option.title, <span>{t.anagrafica.scegli_data}</span>
value: option.id, )}
}), <CalendarIcon className="ml-auto size-4 opacity-50" />
)} </Button>
placeholder={t.seleziona_placeholder} </FormControl>
/> </PopoverTrigger>
<PopoverContent align="start" className="w-auto p-0">
<FormDescription> <Calendar
{t.preferenze.motivazione_desc} autoFocus
</FormDescription> 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> </FormItem>
); )}
}} />
/> </>
) : ( ) : (
<FormField <FormField
control={form.control} control={form.control}
@ -920,9 +983,6 @@ export const FormNewServizioAcquisto = ({
autoFocus autoFocus
captionLayout="dropdown" captionLayout="dropdown"
defaultMonth={field.value} defaultMonth={field.value}
disabled={(date) =>
date > new Date() || date < new Date("1900-01-01")
}
endMonth={new Date()} endMonth={new Date()}
locale={DatePickerLocale} locale={DatePickerLocale}
mode="single" mode="single"

View file

@ -66,6 +66,14 @@ export default interface ServizioTable {
doc_personale_retro_ref: ColumnType<UsersStorageUserStorageId | null, UsersStorageUserStorageId | null, UsersStorageUserStorageId | null>; doc_personale_retro_ref: ColumnType<UsersStorageUserStorageId | null, UsersStorageUserStorageId | null, UsersStorageUserStorageId | null>;
doc_motivazione_ref: ColumnType<UsersStorageUserStorageId | null, UsersStorageUserStorageId | null, UsersStorageUserStorageId | null>; doc_motivazione_ref: ColumnType<UsersStorageUserStorageId | null, UsersStorageUserStorageId | null, UsersStorageUserStorageId | null>;
scadenza_motivazione_transitoria: ColumnType<Date | null, Date | string | null, Date | string | null>;
skipPayment: ColumnType<boolean, boolean | undefined, boolean>;
skipControlloDoc: ColumnType<boolean, boolean | undefined, boolean>;
skipDocMotivazione: ColumnType<boolean, boolean | undefined, boolean>;
} }
export type Servizio = Selectable<ServizioTable>; export type Servizio = Selectable<ServizioTable>;