diff --git a/apps/db/migrations/40_servizio_interr.up.sql b/apps/db/migrations/40_servizio_interr.up.sql new file mode 100644 index 0000000..3dd0fd5 --- /dev/null +++ b/apps/db/migrations/40_servizio_interr.up.sql @@ -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"; \ No newline at end of file diff --git a/apps/infoalloggi/src/components/annunci_map.tsx b/apps/infoalloggi/src/components/annunci_map.tsx index 0484340..f65250f 100644 --- a/apps/infoalloggi/src/components/annunci_map.tsx +++ b/apps/infoalloggi/src/components/annunci_map.tsx @@ -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, diff --git a/apps/infoalloggi/src/components/servizio/interruzione.tsx b/apps/infoalloggi/src/components/servizio/interruzione.tsx new file mode 100644 index 0000000..2253a21 --- /dev/null +++ b/apps/infoalloggi/src/components/servizio/interruzione.tsx @@ -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 ( +
+
+
+ +
+

+ Sono passati {differenceInDays(new Date(), servizio.decorrenza)}{" "} + giorni dall'inizio della tua ricerca +

+

+ Hai trovato qualche immobile interessante?
Ricordati che{" "} + se non intendi confermare nessun immobile, puoi interrompere la + ricerca entro 14 giorni dalla decorrenza, evitando così di + dover pagare il saldo del servizio acquistato. +

+
+ +
+ + +
+ +
+ Continua + + Continua a cercare casa + +
+
+ +

+ Avrai sempre modo di interrompere il servizio usando il pulsante + relativo. +

+
+ + + +
+
+ + +
+ +
+ + Interrompi + + + Termina la tua ricerca ora + +
+
+ +
    +
  • + + Terminazione immediata del servizio +
  • +
  • + + Nessun addebito aggiuntivo applicato +
  • +
+
+ + { + interrompiServizio({ servizioId }); + }} + title={t.servizio.interruzione.title} + > + + + +
+
+
+

+ Hai fino al{" "} + {add(servizio.decorrenza, { + days: servizio.interruzioneDays, + }).toLocaleDateString("it-IT")}{" "} + per interrompere il servizio senza costi aggiuntivi. +
+ Se hai bisogno di assistenza, contattaci. +

+
+ ); +}; + +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 ( + + + {canInterrompere ? ( + + ) : ( + + + + + + + +

Periodo di interruzione scaduto, contattaci.

+
+
+ )} +
+ + + + {t.servizio.interruzione.title} + +

+ + Fine periodo di interruzione:{" "} + {fineInterruzione.toLocaleDateString("it-IT")} + +

+

+ {t.servizio.interruzione.description} +

+
+
+ + {t.annulla} + mutate({ servizioId: servizio.servizio_id })} + > + {t.conferma} + + +
+
+ ); +}; diff --git a/apps/infoalloggi/src/forms/FormServizio.tsx b/apps/infoalloggi/src/components/servizio/parametri_ricerca.tsx similarity index 84% rename from apps/infoalloggi/src/forms/FormServizio.tsx rename to apps/infoalloggi/src/components/servizio/parametri_ricerca.tsx index b86de07..dd9fe75 100644 --- a/apps/infoalloggi/src/forms/FormServizio.tsx +++ b/apps/infoalloggi/src/components/servizio/parametri_ricerca.tsx @@ -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; +type FormValues = z.infer; -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 = ({ })} + {/* {!isLimited && ( @@ -701,3 +729,114 @@ export const FormServizio = ({ ); }; +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 ( + + + + + + + Creazione Servizio + desc + +
+ +
+
+
+ ); +}; + +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 ( + + + + + + + {t.servizio.parametri.title} + + {t.servizio.parametri.title} + + + + + + + + ); +}; diff --git a/apps/infoalloggi/src/components/servizio/servizi_header.tsx b/apps/infoalloggi/src/components/servizio/servizi_header.tsx deleted file mode 100644 index c6ba010..0000000 --- a/apps/infoalloggi/src/components/servizio/servizi_header.tsx +++ /dev/null @@ -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 ( -
-

Servizi cliente

-
- - -
-
- ); -}; - -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 ( - - - - - - - Creazione Servizio - desc - -
- -
-
-
- ); -}; diff --git a/apps/infoalloggi/src/components/servizio/servizio.tsx b/apps/infoalloggi/src/components/servizio/servizio.tsx index dba2bb5..dfdb69c 100644 --- a/apps/infoalloggi/src/components/servizio/servizio.tsx +++ b/apps/infoalloggi/src/components/servizio/servizio.tsx @@ -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 (
@@ -376,6 +383,7 @@ const Main = () => {
); } + // 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 = () => { ); } + //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 ( <>

{t.servizio.scaduto}

@@ -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 ; + } + + // normal case return ( <> {annuncioConfermato.length > 0 && ( @@ -511,31 +540,29 @@ const Main = () => { {t.servizio.saldi_in_attesa} - {servizio.annunci - .filter((a) => a.accettato_conferma_at !== null) - .map((a) => ( -
-

- {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", - })} -

- -
- ))} + {annuncioConfermato.map((a) => ( +
+

+ {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", + })} +

+ +
+ ))} )}
@@ -550,7 +577,7 @@ const Main = () => { ); })} - {servizio.annunci.length % 2 === 1 && ( + {servizio.annunci.length > 1 && servizio.annunci.length % 2 === 1 && (
)}
diff --git a/apps/infoalloggi/src/components/servizio/servizio_actions.tsx b/apps/infoalloggi/src/components/servizio/servizio_actions.tsx index 739a69c..b536a8b 100644 --- a/apps/infoalloggi/src/components/servizio/servizio_actions.tsx +++ b/apps/infoalloggi/src/components/servizio/servizio_actions.tsx @@ -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 ( <> - + ); @@ -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 ( - - - {canInterrompere ? ( - - ) : ( - - - - - - - -

Periodo di interruzione scaduto, contattaci.

-
-
- )} -
- - - - {t.servizio.interruzione.title} - -

- - Fine periodo di interruzione:{" "} - {fineInterruzione.toLocaleDateString("it-IT")} - -

-

- {t.servizio.interruzione.description} -

-
-
- - {t.annulla} - mutate({ servizioId: servizio.servizio_id })} - > - {t.conferma} - - -
-
- ); -}; - -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 ( - - - - - - - {t.servizio.parametri.title} - - {t.servizio.parametri.title} - - - - - - - - ); -}; - const EditServizioAdmin = () => { const { servizioId, userId, servizio } = useServizio(); const [open, setOpen] = useState(false); diff --git a/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx b/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx index 90fbab1..21d17b4 100644 --- a/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx +++ b/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx @@ -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; @@ -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 = ({
- ( - -
- Inviato - - - - -
- - - La mail di benvenuto al cliente è stata inviata - -
- )} - /> )} /> + ( + +
+ + Informativa Interruzione Vista + + + + + +
+
+ )} + /> + ( + +
+ + Giorni interruzione + + +
+ + field.onChange(Number(value))} + value={Number(field.value)} + /> + +
+ )} + />
; -export type Item = Selectable; -export type NewItem = Insertable; -export type ItemUpdate = Updateable; - // --------------------------------------------------------------------------- // DummyDriver db — used only for compile-time type tests // --------------------------------------------------------------------------- diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ricerca/[userId].tsx b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ricerca/[userId].tsx index ea245f8..01368d2 100644 --- a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ricerca/[userId].tsx +++ b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ricerca/[userId].tsx @@ -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 = ({ return (
- +
+

Servizi cliente

+
+ + +
+
diff --git a/apps/infoalloggi/src/schemas/public/Servizio.ts b/apps/infoalloggi/src/schemas/public/Servizio.ts index e6d1dae..1c49732 100644 --- a/apps/infoalloggi/src/schemas/public/Servizio.ts +++ b/apps/infoalloggi/src/schemas/public/Servizio.ts @@ -45,8 +45,6 @@ export default interface ServizioTable { reddito: ColumnType; - isSent: ColumnType; - decorrenza: ColumnType; isInterrotto: ColumnType; @@ -72,6 +70,10 @@ export default interface ServizioTable { skipDocMotivazione: ColumnType; onboardOk: ColumnType; + + interruzioneDays: ColumnType; + + acceptedInterr: ColumnType; } export type Servizio = Selectable; diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts index ad7ec9f..5964b1d 100644 --- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts +++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts @@ -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({ diff --git a/apps/infoalloggi/src/server/controllers/stripe.controller.ts b/apps/infoalloggi/src/server/controllers/stripe.controller.ts index c98447e..d7140ae 100644 --- a/apps/infoalloggi/src/server/controllers/stripe.controller.ts +++ b/apps/infoalloggi/src/server/controllers/stripe.controller.ts @@ -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({ diff --git a/apps/infoalloggi/src/server/services/email.service.ts b/apps/infoalloggi/src/server/services/email.service.ts index 0f228e5..be39269 100644 --- a/apps/infoalloggi/src/server/services/email.service.ts +++ b/apps/infoalloggi/src/server/services/email.service.ts @@ -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({ diff --git a/apps/infoalloggi/src/server/services/mailer.ts b/apps/infoalloggi/src/server/services/mailer.ts index 4c312f6..d570364 100644 --- a/apps/infoalloggi/src/server/services/mailer.ts +++ b/apps/infoalloggi/src/server/services/mailer.ts @@ -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,