refactor: remove ServiziHeader component and integrate its functionality directly into RicercaUser page
feat: update Servizio component to handle interruzioneDays and acceptedInterr logic refactor: simplify servizio_actions by removing unused InterruzioneServizio and EditParametri components feat: enhance FormEditServizioAdmin to include interruzioneDays and acceptedInterr fields fix: adjust email service to delete obsolete emails and handle invalid email types gracefully chore: update stripe controller to utilize interruzioneDays for lock expiration logic fix: ensure correct handling of servizio expiration and interruzione logic in service controller
This commit is contained in:
parent
b4bfccd4c7
commit
5bff7219e8
15 changed files with 579 additions and 368 deletions
7
apps/db/migrations/40_servizio_interr.up.sql
Normal file
7
apps/db/migrations/40_servizio_interr.up.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
ALTER TABLE IF EXISTS public.servizio
|
||||
ADD COLUMN IF NOT EXISTS "interruzioneDays" integer NOT NULL DEFAULT 14;
|
||||
|
||||
ALTER TABLE IF EXISTS public.servizio
|
||||
ADD COLUMN IF NOT EXISTS "acceptedInterr" boolean NOT NULL DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE IF EXISTS public.servizio DROP COLUMN IF EXISTS "isSent";
|
||||
|
|
@ -120,6 +120,7 @@ const SelectedComp = memo(
|
|||
href={`/annuncio/${selected.codice}`}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
key={selected.id}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
transition={{
|
||||
duration: 0.1,
|
||||
|
|
|
|||
249
apps/infoalloggi/src/components/servizio/interruzione.tsx
Normal file
249
apps/infoalloggi/src/components/servizio/interruzione.tsx
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
import { add, differenceInDays, isBefore } from "date-fns";
|
||||
import { ArrowRight, Clock, ClockAlert, XCircle } from "lucide-react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Confirm } from "~/components/confirm";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import { useServizio } from "~/providers/ServizioProvider";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export const DisclaimerInterruzioneServizio = () => {
|
||||
const { servizio, servizioId } = useServizio();
|
||||
const { t } = useTranslation();
|
||||
const utils = api.useUtils();
|
||||
const { mutate: interrompiServizio } =
|
||||
api.servizio.interruzioneServizio.useMutation({
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error("Errore durante la richiesta di interruzione");
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await utils.servizio.invalidate();
|
||||
toast.success("Interruzione richiesta");
|
||||
},
|
||||
});
|
||||
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error("Errore");
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await utils.servizio.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
if (!servizio.decorrenza) return null;
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-4xl px-4 py-6 md:py-8">
|
||||
<div className="mb-10 text-center">
|
||||
<div className="mx-auto mb-6 flex size-16 items-center justify-center rounded-full bg-secondary">
|
||||
<Clock className="size-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="mb-4 text-balance font-bold text-2xl text-foreground tracking-tight md:text-4xl">
|
||||
Sono passati {differenceInDays(new Date(), servizio.decorrenza)}{" "}
|
||||
giorni dall'inizio della tua ricerca
|
||||
</h1>
|
||||
<p className="mx-auto max-w-xl text-pretty text-lg text-muted-foreground leading-relaxed">
|
||||
Hai trovato qualche immobile interessante? <br /> Ricordati che{" "}
|
||||
<b>se non intendi confermare</b> nessun immobile, puoi interrompere la
|
||||
ricerca <b>entro 14 giorni dalla decorrenza</b>, evitando così di
|
||||
dover pagare il saldo del servizio acquistato.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card className="border-2 border-primary shadow-lg">
|
||||
<CardHeader className="pb-2 text-center">
|
||||
<div className="mx-auto mb-3 flex size-14 items-center justify-center rounded-full bg-primary/10">
|
||||
<ArrowRight className="size-7 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-foreground text-xl">Continua</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
Continua a cercare casa
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<CardContent className="">
|
||||
<p className="mb-4 text-muted-foreground text-sm">
|
||||
Avrai sempre modo di interrompere il servizio usando il pulsante
|
||||
relativo.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
className="h-12 w-full bg-primary font-medium text-base text-primary-foreground hover:bg-primary/90"
|
||||
onClick={() =>
|
||||
updateServizio({ servizioId, data: { acceptedInterr: true } })
|
||||
}
|
||||
>
|
||||
Continua con la ricerca
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="border-border">
|
||||
<CardHeader className="pb-2 text-center">
|
||||
<div className="mx-auto mb-3 flex size-14 items-center justify-center rounded-full bg-muted">
|
||||
<XCircle className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
<CardTitle className="text-foreground text-xl">
|
||||
Interrompi
|
||||
</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
Termina la tua ricerca ora
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<CardContent className="">
|
||||
<ul className="mb-6 space-y-2 text-muted-foreground text-sm">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="size-1.5 rounded-full bg-muted-foreground" />
|
||||
Terminazione immediata del servizio
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="size-1.5 rounded-full bg-muted-foreground" />
|
||||
Nessun addebito aggiuntivo applicato
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Confirm
|
||||
description={t.servizio.interruzione.description}
|
||||
onConfirm={() => {
|
||||
interrompiServizio({ servizioId });
|
||||
}}
|
||||
title={t.servizio.interruzione.title}
|
||||
>
|
||||
<Button
|
||||
className="h-12 w-full border-border font-medium text-base text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
variant="outline"
|
||||
>
|
||||
Interrompi il mio servizio ora
|
||||
</Button>
|
||||
</Confirm>
|
||||
</CardFooter>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<p className="mt-8 text-center text-muted-foreground text-sm">
|
||||
Hai fino al{" "}
|
||||
{add(servizio.decorrenza, {
|
||||
days: servizio.interruzioneDays,
|
||||
}).toLocaleDateString("it-IT")}{" "}
|
||||
per interrompere il servizio senza costi aggiuntivi.
|
||||
<br />
|
||||
Se hai bisogno di assistenza, contattaci.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export const InterruzioneServizio = () => {
|
||||
const { servizio, isAdmin } = useServizio();
|
||||
const { t } = useTranslation();
|
||||
const utils = api.useUtils();
|
||||
const { mutate } = api.servizio.interruzioneServizio.useMutation({
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error("Errore durante la richiesta di interruzione");
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await utils.servizio.invalidate();
|
||||
toast.success("Interruzione richiesta");
|
||||
},
|
||||
});
|
||||
if (!servizio.decorrenza) return null;
|
||||
const fineInterruzione = add(servizio.decorrenza, {
|
||||
days: servizio.interruzioneDays,
|
||||
});
|
||||
|
||||
const canInterrompere =
|
||||
!servizio.isInterrotto &&
|
||||
(isAdmin ||
|
||||
(isBefore(new Date(), fineInterruzione) &&
|
||||
servizio.annunci.every((a) => a.accettato_conferma_at === null)));
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
{canInterrompere ? (
|
||||
<Button
|
||||
aria-label="Request Interruzione"
|
||||
className="w-full"
|
||||
disabled={!canInterrompere}
|
||||
>
|
||||
<ClockAlert /> <span>{t.servizio.interruzione.btn}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-block w-full">
|
||||
<Button
|
||||
aria-label="Request Interruzione"
|
||||
className="w-full"
|
||||
disabled={!canInterrompere}
|
||||
>
|
||||
<ClockAlert /> <span>{t.servizio.interruzione.btn}</span>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Periodo di interruzione scaduto, contattaci.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t.servizio.interruzione.title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="space-y-2">
|
||||
<p>
|
||||
<b>
|
||||
Fine periodo di interruzione:{" "}
|
||||
{fineInterruzione.toLocaleDateString("it-IT")}
|
||||
</b>
|
||||
</p>
|
||||
<p className="whitespace-pre-line">
|
||||
{t.servizio.interruzione.description}
|
||||
</p>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t.annulla}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
aria-label="Confirm Interruzione"
|
||||
onClick={() => mutate({ servizioId: servizio.servizio_id })}
|
||||
>
|
||||
{t.conferma}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +1,19 @@
|
|||
import { format } from "date-fns";
|
||||
import { it } from "date-fns/locale";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { CalendarIcon, SlidersHorizontal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { z } from "zod/v4";
|
||||
import { Counter } from "~/components/counter";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle,
|
||||
CredenzaTrigger,
|
||||
} from "~/components/custom_ui/credenza";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
|
|
@ -16,6 +27,14 @@ import { MultiSelect, UnpackOptions } from "~/components/custom_ui/multiselect";
|
|||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Calendar } from "~/components/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
|
|
@ -24,12 +43,16 @@ import {
|
|||
} from "~/components/ui/popover";
|
||||
import { Slider } from "~/components/ui/slider";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { FieldResetButton } from "~/forms/comps";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useZodForm } from "~/lib/zodForm";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import { useServizio } from "~/providers/ServizioProvider";
|
||||
import type { Servizio } from "~/schemas/public/Servizio";
|
||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||
import { FieldResetButton } from "./comps";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import { api } from "~/utils/api";
|
||||
import { DEFAULT_SERVIZIO_INTERRUZIONE_DURATION_DAYS } from "~/utils/config";
|
||||
|
||||
const Schema = z
|
||||
.object({
|
||||
|
|
@ -53,6 +76,8 @@ const Schema = z
|
|||
skipPayment: z.boolean(),
|
||||
skipControlloDoc: z.boolean(),
|
||||
skipDocMotivazione: z.boolean(),
|
||||
interruzioneDays: z.number().min(0),
|
||||
acceptedInterr: z.boolean(),
|
||||
})
|
||||
.superRefine(({ n_adulti }, refinementContext) => {
|
||||
if (n_adulti <= 0) {
|
||||
|
|
@ -65,9 +90,9 @@ const Schema = z
|
|||
}
|
||||
});
|
||||
|
||||
export type FormValues = z.infer<typeof Schema>;
|
||||
type FormValues = z.infer<typeof Schema>;
|
||||
|
||||
export const FormServizio = ({
|
||||
const FormServizio = ({
|
||||
initialData,
|
||||
onSubmit,
|
||||
isLimited,
|
||||
|
|
@ -105,6 +130,8 @@ export const FormServizio = ({
|
|||
skipPayment: false,
|
||||
skipControlloDoc: true,
|
||||
skipDocMotivazione: true,
|
||||
interruzioneDays: DEFAULT_SERVIZIO_INTERRUZIONE_DURATION_DAYS, // default value for interruzioneDays
|
||||
acceptedInterr: false,
|
||||
};
|
||||
}
|
||||
const { locale, t } = useTranslation();
|
||||
|
|
@ -613,6 +640,7 @@ export const FormServizio = ({
|
|||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<Separator />
|
||||
{!isLimited && (
|
||||
|
|
@ -701,3 +729,114 @@ export const FormServizio = ({
|
|||
</Form>
|
||||
);
|
||||
};
|
||||
export const NewServizioModal = ({ userId }: { userId: UsersId }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutate: add } = api.servizio.addServizio.useMutation({
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success("Servizio creato con successo");
|
||||
await utils.servizio.invalidate();
|
||||
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(fields: FormValues) {
|
||||
add({ data: { ...fields, user_id: userId } });
|
||||
}
|
||||
return (
|
||||
<Dialog onOpenChange={setOpen} open={open}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Nuovo Servizio
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80%] w-full overflow-auto p-4 sm:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Creazione Servizio</DialogTitle>
|
||||
<DialogDescription className="sr-only">desc</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-y-auto">
|
||||
<FormServizio
|
||||
initialData={undefined}
|
||||
isLimited={false}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditServizioModal = () => {
|
||||
const { servizioId, isAdmin, servizio } = useServizio();
|
||||
const { t } = useTranslation();
|
||||
const [openEditParametri, setOpenEditParametri] = useState(false);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutate: update } = api.servizio.updateServizio.useMutation({
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success("Servizio modificato con successo");
|
||||
await utils.servizio.invalidate();
|
||||
|
||||
setOpenEditParametri(false);
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(fields: FormValues) {
|
||||
update({
|
||||
data: {
|
||||
...fields,
|
||||
},
|
||||
servizioId,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Credenza onOpenChange={setOpenEditParametri} open={openEditParametri}>
|
||||
<CredenzaTrigger asChild>
|
||||
<Button
|
||||
aria-label="Parametri di ricerca"
|
||||
className="w-full"
|
||||
disabled={
|
||||
!isAdmin &&
|
||||
(servizio.isInterrotto ||
|
||||
servizio.annunci.some((a) => a.hasConfermaAdmin))
|
||||
}
|
||||
variant="secondary"
|
||||
>
|
||||
<SlidersHorizontal />
|
||||
{t.servizio.parametri.btn}
|
||||
</Button>
|
||||
</CredenzaTrigger>
|
||||
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t.servizio.parametri.title}</CredenzaTitle>
|
||||
<CredenzaDescription className="sr-only">
|
||||
{t.servizio.parametri.title}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5 sm:max-w-5xl">
|
||||
<FormServizio
|
||||
initialData={servizio}
|
||||
isLimited={!isAdmin}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</CredenzaBody>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { NewRinnovoModal } from "~/components/servizio/rinnovo";
|
||||
import { FormServizio, type FormValues } from "~/forms/FormServizio";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import { api } from "~/utils/api";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog";
|
||||
|
||||
export const ServiziHeader = ({ userId }: { userId: UsersId }) => {
|
||||
return (
|
||||
<div className="flex w-full flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="font-semibold text-xl">Servizi cliente</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<NewServizioModal userId={userId} />
|
||||
<NewRinnovoModal userId={userId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NewServizioModal = ({ userId }: { userId: UsersId }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutate: add } = api.servizio.addServizio.useMutation({
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success("Servizio creato con successo");
|
||||
await utils.servizio.invalidate();
|
||||
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(fields: FormValues) {
|
||||
add({ data: { ...fields, user_id: userId } });
|
||||
}
|
||||
return (
|
||||
<Dialog onOpenChange={setOpen} open={open}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Nuovo Servizio
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[80%] w-full overflow-auto p-4 sm:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Creazione Servizio</DialogTitle>
|
||||
<DialogDescription className="sr-only">desc</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-y-auto">
|
||||
<FormServizio
|
||||
initialData={undefined}
|
||||
isLimited={false}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { add } from "date-fns";
|
||||
import { add, isAfter, isBefore } from "date-fns";
|
||||
import {
|
||||
ArrowRight,
|
||||
BadgeCheck,
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import toast from "react-hot-toast";
|
||||
import { DisclaimerInterruzioneServizio } from "~/components/servizio/interruzione";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import {
|
||||
|
|
@ -28,6 +29,7 @@ import type { Rinnovi } from "~/schemas/public/Rinnovi";
|
|||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import type { ServizioData } from "~/server/controllers/servizio.controller";
|
||||
import { api } from "~/utils/api";
|
||||
import { DEFAULT_SERVIZIO_DURATION_DAYS } from "~/utils/config";
|
||||
import { Confirm } from "../confirm";
|
||||
import { AlarmClockSVG } from "../svgs";
|
||||
import {
|
||||
|
|
@ -165,7 +167,10 @@ const ServizioLinkCard = ({
|
|||
} else if (servizio.decorrenza) {
|
||||
const now = new Date();
|
||||
const decorrenza = new Date(servizio.decorrenza);
|
||||
if (now > add(decorrenza, { days: 60 }) && !servizio.isOkSaldo) {
|
||||
if (
|
||||
now > add(decorrenza, { days: DEFAULT_SERVIZIO_DURATION_DAYS }) &&
|
||||
!servizio.isOkSaldo
|
||||
) {
|
||||
status = "scaduto";
|
||||
StatusIcon = CircleMinus;
|
||||
}
|
||||
|
|
@ -285,9 +290,9 @@ const Content = () => {
|
|||
? new Date(servizio.decorrenza).toLocaleDateString("it-IT")
|
||||
: "";
|
||||
const end_txt = servizio.decorrenza
|
||||
? new Date(add(servizio.decorrenza, { days: 60 })).toLocaleDateString(
|
||||
"it-IT",
|
||||
)
|
||||
? new Date(
|
||||
add(servizio.decorrenza, { days: DEFAULT_SERVIZIO_DURATION_DAYS }),
|
||||
).toLocaleDateString("it-IT")
|
||||
: "";
|
||||
|
||||
return (
|
||||
|
|
@ -341,6 +346,7 @@ const Main = () => {
|
|||
},
|
||||
});
|
||||
|
||||
// se interrotto, mostra avvisos
|
||||
if (servizio.isInterrotto) {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -349,6 +355,7 @@ const Main = () => {
|
|||
</>
|
||||
);
|
||||
}
|
||||
// mostra onboarding
|
||||
if (!servizio.onboardOk) {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-8">
|
||||
|
|
@ -376,6 +383,7 @@ const Main = () => {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
// se non ok acconto, mostra invito a completare pagamento acconto o ad attivare se acconto già inserito
|
||||
if (!servizio.isOkAcconto || servizio.decorrenza === null) {
|
||||
const acconti = servizio.ordini.filter(
|
||||
(o) => o.type === OrderTypeEnum.Acconto,
|
||||
|
|
@ -468,13 +476,15 @@ const Main = () => {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
//servizio scaduto se decorrenza + 60 giorni è passato e non è ok saldo
|
||||
if (
|
||||
new Date() >
|
||||
add(servizio.decorrenza, {
|
||||
days: 60,
|
||||
days: DEFAULT_SERVIZIO_DURATION_DAYS,
|
||||
}) &&
|
||||
!servizio.isOkSaldo
|
||||
) {
|
||||
//todo capire come fare con saldo quando scade
|
||||
return (
|
||||
<>
|
||||
<p className="font-medium text-lg">{t.servizio.scaduto}</p>
|
||||
|
|
@ -482,10 +492,12 @@ const Main = () => {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const annuncioConfermato = servizio.annunci.filter(
|
||||
(a) => a.accettato_conferma_at !== null,
|
||||
);
|
||||
|
||||
// in conferma, mostra solo annunci confermati
|
||||
if (servizio.isOkSaldo && annuncioConfermato.length > 0) {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -503,6 +515,23 @@ const Main = () => {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// se superato periodo interruzione, ma non interrotto, non scaduto e non in conferma, mostra avviso
|
||||
if (
|
||||
!servizio.acceptedInterr &&
|
||||
isBefore(
|
||||
new Date(),
|
||||
add(servizio.decorrenza, { days: servizio.interruzioneDays }),
|
||||
) &&
|
||||
isAfter(
|
||||
new Date(),
|
||||
add(servizio.decorrenza, { days: servizio.interruzioneDays - 5 }),
|
||||
)
|
||||
) {
|
||||
return <DisclaimerInterruzioneServizio />;
|
||||
}
|
||||
|
||||
// normal case
|
||||
return (
|
||||
<>
|
||||
{annuncioConfermato.length > 0 && (
|
||||
|
|
@ -511,31 +540,29 @@ const Main = () => {
|
|||
<PackageCheck className="size-6 text-primary" />
|
||||
<span className="font-medium">{t.servizio.saldi_in_attesa}</span>
|
||||
</div>
|
||||
{servizio.annunci
|
||||
.filter((a) => a.accettato_conferma_at !== null)
|
||||
.map((a) => (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
|
||||
key={a.annunci_id}
|
||||
>
|
||||
<h3>
|
||||
{a.codice} -{" "}
|
||||
{a.accettato_conferma_at &&
|
||||
new Date(a.accettato_conferma_at).toLocaleDateString("it", {
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</h3>
|
||||
<SaldoButton
|
||||
annuncioId={a.annunci_id}
|
||||
className="border-none"
|
||||
variant={"outline"}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{annuncioConfermato.map((a) => (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
|
||||
key={a.annunci_id}
|
||||
>
|
||||
<h3>
|
||||
{a.codice} -{" "}
|
||||
{a.accettato_conferma_at &&
|
||||
new Date(a.accettato_conferma_at).toLocaleDateString("it", {
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
})}
|
||||
</h3>
|
||||
<SaldoButton
|
||||
annuncioId={a.annunci_id}
|
||||
className="border-none"
|
||||
variant={"outline"}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="@container grid grid-flow-row @sm:grid-cols-2 grid-cols-1 gap-4">
|
||||
|
|
@ -550,7 +577,7 @@ const Main = () => {
|
|||
</ServizioAnnuncioProvider>
|
||||
);
|
||||
})}
|
||||
{servizio.annunci.length % 2 === 1 && (
|
||||
{servizio.annunci.length > 1 && servizio.annunci.length % 2 === 1 && (
|
||||
<div className="@sm:block hidden size-full rounded-md border border-(--pattern-fg) bg-[repeating-linear-gradient(45deg,var(--pattern-fg)_0,var(--pattern-fg)_1px,transparent_0,transparent_50%)] bg-size-[10px_10px] bg-fixed opacity-10 [--pattern-fg:#696969] dark:[--pattern-fg:#e1e1e1]" />
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import { add, isBefore } from "date-fns";
|
||||
import {
|
||||
Bug,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ClockAlert,
|
||||
Cog,
|
||||
Euro,
|
||||
ExternalLink,
|
||||
Logs,
|
||||
Plus,
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
|
@ -28,17 +25,8 @@ import {
|
|||
} from "~/components/custom_ui/credenza";
|
||||
import { MultiSelect } from "~/components/custom_ui/multiselect";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import { InterruzioneServizio } from "~/components/servizio/interruzione";
|
||||
import { EditServizioModal } from "~/components/servizio/parametri_ricerca";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
|
|
@ -54,17 +42,11 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
import {
|
||||
type FormValues as EditFormValues,
|
||||
FormEditServizio,
|
||||
} from "~/forms/FormEditServizioAdmin";
|
||||
import { FormNewOrder } from "~/forms/FormNewOrdine";
|
||||
import { FormServizio, type FormValues } from "~/forms/FormServizio";
|
||||
import { formatCurrency } from "~/lib/utils";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import { useServizio } from "~/providers/ServizioProvider";
|
||||
|
|
@ -118,7 +100,7 @@ const UserActions = () => {
|
|||
if (isAdmin || canUserEdit) {
|
||||
return (
|
||||
<>
|
||||
<EditParametri />
|
||||
<EditServizioModal />
|
||||
<InterruzioneServizio />
|
||||
</>
|
||||
);
|
||||
|
|
@ -581,155 +563,6 @@ const AddAnnuncio = ({
|
|||
);
|
||||
};
|
||||
|
||||
const InterruzioneServizio = () => {
|
||||
const { servizio, isAdmin } = useServizio();
|
||||
const { t } = useTranslation();
|
||||
const utils = api.useUtils();
|
||||
const { mutate } = api.servizio.interruzioneServizio.useMutation({
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error("Errore durante la richiesta di interruzione");
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await utils.servizio.invalidate();
|
||||
toast.success("Interruzione richiesta");
|
||||
},
|
||||
});
|
||||
if (!servizio.decorrenza) return null;
|
||||
const fineInterruzione = add(servizio.decorrenza, {
|
||||
days: 14,
|
||||
});
|
||||
|
||||
const canInterrompere =
|
||||
!servizio.isInterrotto &&
|
||||
(isAdmin ||
|
||||
(isBefore(new Date(), fineInterruzione) &&
|
||||
servizio.annunci.every((a) => a.accettato_conferma_at === null)));
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
{canInterrompere ? (
|
||||
<Button
|
||||
aria-label="Request Interruzione"
|
||||
className="w-full"
|
||||
disabled={!canInterrompere}
|
||||
>
|
||||
<ClockAlert /> <span>{t.servizio.interruzione.btn}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-block w-full">
|
||||
<Button
|
||||
aria-label="Request Interruzione"
|
||||
className="w-full"
|
||||
disabled={!canInterrompere}
|
||||
>
|
||||
<ClockAlert /> <span>{t.servizio.interruzione.btn}</span>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Periodo di interruzione scaduto, contattaci.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t.servizio.interruzione.title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="space-y-2">
|
||||
<p>
|
||||
<b>
|
||||
Fine periodo di interruzione:{" "}
|
||||
{fineInterruzione.toLocaleDateString("it-IT")}
|
||||
</b>
|
||||
</p>
|
||||
<p className="whitespace-pre-line">
|
||||
{t.servizio.interruzione.description}
|
||||
</p>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t.annulla}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
aria-label="Confirm Interruzione"
|
||||
onClick={() => mutate({ servizioId: servizio.servizio_id })}
|
||||
>
|
||||
{t.conferma}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const EditParametri = () => {
|
||||
const { servizioId, isAdmin, servizio } = useServizio();
|
||||
const { t } = useTranslation();
|
||||
const [openEditParametri, setOpenEditParametri] = useState(false);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutate: update } = api.servizio.updateServizio.useMutation({
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success("Servizio modificato con successo");
|
||||
await utils.servizio.invalidate();
|
||||
|
||||
setOpenEditParametri(false);
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(fields: FormValues) {
|
||||
update({
|
||||
data: {
|
||||
...fields,
|
||||
},
|
||||
servizioId,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Credenza onOpenChange={setOpenEditParametri} open={openEditParametri}>
|
||||
<CredenzaTrigger asChild>
|
||||
<Button
|
||||
aria-label="Parametri di ricerca"
|
||||
className="w-full"
|
||||
disabled={
|
||||
!isAdmin &&
|
||||
(servizio.isInterrotto ||
|
||||
servizio.annunci.some((a) => a.hasConfermaAdmin))
|
||||
}
|
||||
variant="secondary"
|
||||
>
|
||||
<SlidersHorizontal />
|
||||
{t.servizio.parametri.btn}
|
||||
</Button>
|
||||
</CredenzaTrigger>
|
||||
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t.servizio.parametri.title}</CredenzaTitle>
|
||||
<CredenzaDescription className="sr-only">
|
||||
{t.servizio.parametri.title}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5 sm:max-w-5xl">
|
||||
<FormServizio
|
||||
initialData={servizio}
|
||||
isLimited={!isAdmin}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</CredenzaBody>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
};
|
||||
|
||||
const EditServizioAdmin = () => {
|
||||
const { servizioId, userId, servizio } = useServizio();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { format } from "date-fns";
|
|||
import { enUS, it } from "date-fns/locale";
|
||||
import { CalendarIcon, X } from "lucide-react";
|
||||
import { z } from "zod/v4";
|
||||
import { Counter } from "~/components/counter";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
|
|
@ -24,6 +25,7 @@ import { cn } from "~/lib/utils";
|
|||
import { useZodForm } from "~/lib/zodForm";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type { Servizio } from "~/schemas/public/Servizio";
|
||||
import { DEFAULT_SERVIZIO_INTERRUZIONE_DURATION_DAYS } from "~/utils/config";
|
||||
|
||||
const Schema = z.object({
|
||||
decorrenza: z.date().nullable(),
|
||||
|
|
@ -31,11 +33,12 @@ const Schema = z.object({
|
|||
isOkAcconto: z.boolean(),
|
||||
isOkConsulenza: z.boolean(),
|
||||
isOkSaldo: z.boolean(),
|
||||
isSent: z.boolean(),
|
||||
skipPayment: z.boolean(),
|
||||
skipControlloDoc: z.boolean(),
|
||||
skipDocMotivazione: z.boolean(),
|
||||
onboardOk: z.boolean(),
|
||||
interruzioneDays: z.number(),
|
||||
acceptedInterr: z.boolean(),
|
||||
});
|
||||
|
||||
export type FormValues = z.infer<typeof Schema>;
|
||||
|
|
@ -61,11 +64,12 @@ export const FormEditServizio = ({
|
|||
isOkAcconto: false,
|
||||
isOkConsulenza: false,
|
||||
isOkSaldo: false,
|
||||
isSent: false,
|
||||
skipPayment: false,
|
||||
skipControlloDoc: false,
|
||||
skipDocMotivazione: false,
|
||||
onboardOk: false,
|
||||
interruzioneDays: DEFAULT_SERVIZIO_INTERRUZIONE_DURATION_DAYS,
|
||||
acceptedInterr: false,
|
||||
};
|
||||
}
|
||||
const { locale, t } = useTranslation();
|
||||
|
|
@ -82,30 +86,6 @@ export const FormEditServizio = ({
|
|||
<Form {...form}>
|
||||
<form className="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="isSent"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<div className="flex flex-wrap items-center gap-x-2">
|
||||
<FormLabel htmlFor="isSent">Inviato</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="data-[state=checked]:bg-neutral-700"
|
||||
defaultChecked={field.value}
|
||||
id="isSent"
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
|
||||
<FormDescription>
|
||||
La mail di benvenuto al cliente è stata inviata
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isInterrotto"
|
||||
|
|
@ -128,6 +108,53 @@ export const FormEditServizio = ({
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="acceptedInterr"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<div className="flex flex-wrap items-center gap-x-2">
|
||||
<FormLabel htmlFor="acceptedInterr">
|
||||
Informativa Interruzione Vista
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
className="data-[state=checked]:bg-neutral-700"
|
||||
defaultChecked={field.value}
|
||||
id="acceptedInterr"
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="interruzioneDays"
|
||||
render={({ field }) => (
|
||||
<FormItem className={cn("w-full")}>
|
||||
<div className="flex flex-wrap items-center gap-x-2">
|
||||
<FormLabel
|
||||
className={cn("font-semibold text-base")}
|
||||
htmlFor="interruzioneDays"
|
||||
>
|
||||
Giorni interruzione
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Counter
|
||||
name="interruzioneDays"
|
||||
onlyPositive
|
||||
setValue={(value) => field.onChange(Number(value))}
|
||||
value={Number(field.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row">
|
||||
<FormField
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@ import type { KyselyPlugin, UnknownRow } from "kysely";
|
|||
import {
|
||||
createQueryId,
|
||||
DummyDriver,
|
||||
type Insertable,
|
||||
Kysely,
|
||||
NoResultError,
|
||||
PostgresAdapter,
|
||||
PostgresIntrospector,
|
||||
PostgresQueryCompiler,
|
||||
type Selectable,
|
||||
type Updateable,
|
||||
} from "kysely";
|
||||
import { jsonArrayFrom } from "kysely/helpers/postgres";
|
||||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
|
|
@ -39,11 +36,6 @@ interface ItemTable {
|
|||
updated_at: Date | null;
|
||||
}
|
||||
|
||||
export type Box = Selectable<BoxTable>;
|
||||
export type Item = Selectable<ItemTable>;
|
||||
export type NewItem = Insertable<ItemTable>;
|
||||
export type ItemUpdate = Updateable<ItemTable>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DummyDriver db — used only for compile-time type tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { GetServerSideProps } from "next";
|
||||
import { AreaRiservataLayoutUserView } from "~/components/Layout";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { ServiziHeader } from "~/components/servizio/servizi_header";
|
||||
import { NewServizioModal } from "~/components/servizio/parametri_ricerca";
|
||||
import { NewRinnovoModal } from "~/components/servizio/rinnovo";
|
||||
import { ServizioList } from "~/components/servizio/servizio";
|
||||
import { AnnunciRichiesti } from "~/components/servizio/servizio_annunci_accordions";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
|
|
@ -27,7 +28,13 @@ const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
|
|||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<ServiziHeader userId={userId} />
|
||||
<div className="flex w-full flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="font-semibold text-xl">Servizi cliente</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<NewServizioModal userId={userId} />
|
||||
<NewRinnovoModal userId={userId} />
|
||||
</div>
|
||||
</div>
|
||||
<AnnunciRichiesti userId={userId} />
|
||||
<ServizioList data={data || []} isAdmin userId={userId} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -45,8 +45,6 @@ export default interface ServizioTable {
|
|||
|
||||
reddito: ColumnType<string | null, string | null, string | null>;
|
||||
|
||||
isSent: ColumnType<boolean, boolean | undefined, boolean>;
|
||||
|
||||
decorrenza: ColumnType<Date | null, Date | string | null, Date | string | null>;
|
||||
|
||||
isInterrotto: ColumnType<boolean, boolean | undefined, boolean>;
|
||||
|
|
@ -72,6 +70,10 @@ export default interface ServizioTable {
|
|||
skipDocMotivazione: ColumnType<boolean, boolean | undefined, boolean>;
|
||||
|
||||
onboardOk: ColumnType<boolean, boolean | undefined, boolean>;
|
||||
|
||||
interruzioneDays: ColumnType<number, number | undefined, number>;
|
||||
|
||||
acceptedInterr: ColumnType<boolean, boolean | undefined, boolean>;
|
||||
}
|
||||
|
||||
export type Servizio = Selectable<ServizioTable>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Override } from "TypeHelpers.type";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { add } from "date-fns";
|
||||
import { add, isBefore } from "date-fns";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
|
||||
import { z } from "zod/v4";
|
||||
import type { PurchaseData } from "~/components/acquisto_receipt";
|
||||
|
|
@ -224,14 +224,20 @@ export const attivaServizio_SaltaPagamentoAcconto = async (
|
|||
() => new Error(`Ordine not found - ordineId: ${ordineId}`),
|
||||
);
|
||||
|
||||
await tx
|
||||
const servizio = await tx
|
||||
.updateTable("servizio")
|
||||
.set({
|
||||
decorrenza: new Date(),
|
||||
isOkAcconto: true,
|
||||
})
|
||||
.where("servizio_id", "=", ordine.servizio_id)
|
||||
.execute();
|
||||
.returning(["interruzioneDays"])
|
||||
.executeTakeFirstOrThrow(
|
||||
() =>
|
||||
new Error(
|
||||
`Failed to update servizio - servizioId: ${ordine.servizio_id}`,
|
||||
),
|
||||
);
|
||||
|
||||
await NewMail({
|
||||
template: {
|
||||
|
|
@ -271,7 +277,7 @@ export const attivaServizio_SaltaPagamentoAcconto = async (
|
|||
userId: ordine.userId,
|
||||
},
|
||||
},
|
||||
lock_expires: add(new Date(), { days: 10 }),
|
||||
lock_expires: add(new Date(), { days: servizio.interruzioneDays - 5 }),
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
|
@ -290,7 +296,7 @@ export const AdminAttivaServizio_Ufficio = async (
|
|||
return await db.transaction().execute(async (tx) => {
|
||||
const data = await tx
|
||||
.selectFrom("servizio")
|
||||
.select("servizio.user_id")
|
||||
.select(["servizio.user_id", "servizio.interruzioneDays"])
|
||||
.innerJoin("users", "users.id", "servizio.user_id")
|
||||
.select("users.email")
|
||||
.where("servizio_id", "=", servizioId)
|
||||
|
|
@ -322,7 +328,7 @@ export const AdminAttivaServizio_Ufficio = async (
|
|||
userId: data.user_id,
|
||||
},
|
||||
},
|
||||
lock_expires: add(today, { days: 10 }),
|
||||
lock_expires: add(today, { days: data.interruzioneDays - 5 }), //avviso 5 giorni prima della scadenza del periodo di ripensamento
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
|
@ -908,11 +914,12 @@ export const interruzioneServizio = async (servizioId: ServizioServizioId) => {
|
|||
message: "Servizio decorrenza not set",
|
||||
});
|
||||
}
|
||||
const isWithinRipensamento =
|
||||
new Date() <=
|
||||
const isWithinRipensamento = isBefore(
|
||||
new Date(),
|
||||
add(servizio.decorrenza, {
|
||||
days: 14,
|
||||
});
|
||||
days: servizio.interruzioneDays,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isWithinRipensamento) {
|
||||
throw new TRPCError({
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ export const whIntentSucceededHandler = async ({
|
|||
db: trx,
|
||||
userId: ordine.userid,
|
||||
});
|
||||
const lock_expires = new Date(Date.now() + 3000); // Lock for 30 sec
|
||||
|
||||
const lock_expires = add(new Date(), { seconds: 30 }); // Lock for 30 sec
|
||||
|
||||
switch (ordine.type) {
|
||||
case OrderTypeEnum.Acconto: {
|
||||
|
|
@ -108,14 +109,20 @@ export const whIntentSucceededHandler = async ({
|
|||
);
|
||||
}
|
||||
|
||||
await trx
|
||||
const servizio = await trx
|
||||
.updateTable("servizio")
|
||||
.where("servizio_id", "=", ordine.servizio_id)
|
||||
.set({
|
||||
decorrenza: new Date(),
|
||||
isOkAcconto: true,
|
||||
})
|
||||
.execute();
|
||||
.returning(["interruzioneDays"])
|
||||
.executeTakeFirstOrThrow(
|
||||
() =>
|
||||
new Error(
|
||||
`Failed to update servizio - servizioId: ${ordine.servizio_id}`,
|
||||
),
|
||||
);
|
||||
|
||||
await addEventToQueue({
|
||||
data: {
|
||||
|
|
@ -163,13 +170,15 @@ export const whIntentSucceededHandler = async ({
|
|||
mailType: "avvisoInterruzione",
|
||||
},
|
||||
mail: {
|
||||
subject: "Avviso Interruzione",
|
||||
subject: "Promemoria durata servizio - Infoalloggi.it",
|
||||
to: utente.email,
|
||||
},
|
||||
userId: utente.id,
|
||||
},
|
||||
},
|
||||
lock_expires: add(new Date(), { days: 10 }),
|
||||
lock_expires: add(new Date(), {
|
||||
days: servizio.interruzioneDays - 5,
|
||||
}),
|
||||
});
|
||||
//SERVIZIO ATTIVATO
|
||||
|
||||
|
|
@ -305,35 +314,6 @@ export const whIntentSucceededHandler = async ({
|
|||
break;
|
||||
}
|
||||
case OrderTypeEnum.Altro:
|
||||
// Handle "Altro" type if needed
|
||||
// await addEventToQueue({
|
||||
// data: {
|
||||
// tipo: "email",
|
||||
// data: {
|
||||
// template: {
|
||||
// mailType: "pagamentoConferma",
|
||||
// },
|
||||
// mail: {
|
||||
// subject: "Pagamento Confermato - Infoalloggi.it",
|
||||
// to: utente.email,
|
||||
// attachments: [
|
||||
// {
|
||||
// filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
|
||||
// generate: {
|
||||
// type: "ricevuta_cortesia",
|
||||
// ordineId: ordine.ordine_id,
|
||||
// },
|
||||
// encoding: "base64",
|
||||
// contentType: "application/pdf",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// userId: utente.id,
|
||||
// },
|
||||
// },
|
||||
// lock_expires,
|
||||
// });
|
||||
break;
|
||||
}
|
||||
await addEventToQueue({
|
||||
|
|
|
|||
|
|
@ -47,6 +47,19 @@ export const getEmails = async ({ userId }: { userId: UsersId }) => {
|
|||
}),
|
||||
);
|
||||
|
||||
const obsoleteMails = gen.filter((mail) => mail.html === "");
|
||||
|
||||
if (obsoleteMails.length > 0) {
|
||||
await db
|
||||
.deleteFrom("emails")
|
||||
.where(
|
||||
"id_email",
|
||||
"in",
|
||||
obsoleteMails.map((mail) => mail.id_email),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
return gen;
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
|
|
|
|||
|
|
@ -183,15 +183,12 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
|
|||
title = "Servizio Attivato";
|
||||
break;
|
||||
case "avvisoInterruzione":
|
||||
//TODO da fare
|
||||
mailComponent = AvvisoInterruzione();
|
||||
title = "Avviso Interruzione";
|
||||
break;
|
||||
default:
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Tipo di email non valido: ${mail satisfies never}`,
|
||||
});
|
||||
console.error(`Tipo di email non valido: ${mail satisfies never}`);
|
||||
return { html: "", title: "Email non valido/modello obsoleto" };
|
||||
}
|
||||
|
||||
return { html: await render(mailComponent), title };
|
||||
|
|
@ -206,6 +203,11 @@ export const NewMail = async ({ template, userId, mail }: NewMailProps) => {
|
|||
try {
|
||||
if (template) {
|
||||
const { html } = await genMail({ ...template });
|
||||
if (!html || html.trim() === "") {
|
||||
throw new Error(
|
||||
`Il template per il tipo ${template.mailType} è obsoleto e non genera più l'html. Email non inviata.`,
|
||||
);
|
||||
}
|
||||
await mailer({
|
||||
html,
|
||||
...mail,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue