feat: add email queue management features
- Implemented FormNewMail for creating new email entries in the queue. - Enhanced AdminPotenziali page layout for better responsiveness. - Added Coda component to manage user email queues, including adding, removing, and clearing emails. - Updated API routes for managing email queue operations, including adding, removing, and clearing user email queues. - Introduced new mail templates and integrated them into the mailer service. - Improved error handling in the email queue controller and service. - Added Zod validation for event IDs in the utility types.
This commit is contained in:
parent
2acd6eca92
commit
964abfbf46
29 changed files with 1178 additions and 209 deletions
|
|
@ -1,18 +1,20 @@
|
||||||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
|
import z from "zod";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type VerificaEmail_NewMail = {
|
|
||||||
mailType: "verificaEmail";
|
|
||||||
props: VerificaEmailProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
type VerificaEmailProps = {
|
export const VerificaEmailMailTag = "verificaEmail";
|
||||||
token: string;
|
export const VerificaEmailPropsSchema = z.object({
|
||||||
userId: string;
|
token: z.string(),
|
||||||
redirect?: string;
|
userId: z.string(),
|
||||||
};
|
redirect: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
const VerificaEmail = ({ token, userId, redirect }: VerificaEmailProps) => {
|
const VerificaEmail = ({
|
||||||
|
token,
|
||||||
|
userId,
|
||||||
|
redirect,
|
||||||
|
}: z.infer<typeof VerificaEmailPropsSchema>) => {
|
||||||
return (
|
return (
|
||||||
<Base noreply preview="Verifica la tua email">
|
<Base noreply preview="Verifica la tua email">
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Heading, Link, Row, Section, Text } from "@react-email/components";
|
import { Heading, Link, Row, Section, Text } from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type AvvisoInterruzione_NewMail = {
|
|
||||||
mailType: "avvisoInterruzione";
|
export const AvvisoInterruzioneMailTag = "avvisoInterruzione";
|
||||||
};
|
|
||||||
|
|
||||||
const AvvisoInterruzione = () => {
|
const AvvisoInterruzione = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
import { Heading, Row, Section, Text } from "@react-email/components";
|
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||||
|
import z from "zod";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
|
|
||||||
export type ContattoAdmin_NewMail = {
|
export const ContattoAdminMailTag = "contattoAdminEmail";
|
||||||
mailType: "contattoAdminEmail";
|
|
||||||
props: ContattoAdminEmailProps;
|
export const ContattoAdminEmailPropsSchema = z.object({
|
||||||
};
|
nome: z.string(),
|
||||||
|
cognome: z.string(),
|
||||||
|
email: z.email(),
|
||||||
|
telefono: z.string(),
|
||||||
|
messaggio: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
type ContattoAdminEmailProps = {
|
|
||||||
nome: string;
|
|
||||||
cognome: string;
|
|
||||||
email: string;
|
|
||||||
telefono: string;
|
|
||||||
messaggio: string;
|
|
||||||
};
|
|
||||||
const ContattoAdminEmail = ({
|
const ContattoAdminEmail = ({
|
||||||
nome,
|
nome,
|
||||||
cognome,
|
cognome,
|
||||||
email,
|
email,
|
||||||
telefono,
|
telefono,
|
||||||
messaggio,
|
messaggio,
|
||||||
}: ContattoAdminEmailProps) => {
|
}: z.infer<typeof ContattoAdminEmailPropsSchema>) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Contatto Email">
|
<Base preview="Contatto Email">
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,25 @@
|
||||||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
|
import z from "zod";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
|
|
||||||
export type ContattoAnnuncio_NewMail = {
|
export const ContattoAnnuncioMailTag = "contattoAnnuncioEmail";
|
||||||
mailType: "contattoAnnuncioEmail";
|
|
||||||
props: ContattoAnnuncioEmailProps;
|
export const ContattoAnnuncioPropsSchema = z.object({
|
||||||
};
|
nome: z.string(),
|
||||||
|
codice: z.string(),
|
||||||
|
email: z.email(),
|
||||||
|
telefono: z.string(),
|
||||||
|
messaggio: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
type ContattoAnnuncioEmailProps = {
|
|
||||||
nome: string;
|
|
||||||
codice: string;
|
|
||||||
email: string;
|
|
||||||
telefono: string;
|
|
||||||
messaggio: string;
|
|
||||||
};
|
|
||||||
const ContattoAnnuncioEmail = ({
|
const ContattoAnnuncioEmail = ({
|
||||||
nome,
|
nome,
|
||||||
codice,
|
codice,
|
||||||
email,
|
email,
|
||||||
telefono,
|
telefono,
|
||||||
messaggio,
|
messaggio,
|
||||||
}: ContattoAnnuncioEmailProps) => {
|
}: z.infer<typeof ContattoAnnuncioPropsSchema>) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Contatto Email">
|
<Base preview="Contatto Email">
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { Heading, Row, Section, Text } from "@react-email/components";
|
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||||
|
import z from "zod";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type ContattoInteressato_NewMail = {
|
|
||||||
mailType: "emailContattoInteressato";
|
export const ContattoInteressatoMailTag = "emailContattoInteressato";
|
||||||
props: EmailContattoInteressatoProps;
|
export const ContattoInteressatoPropsSchema = z.object({
|
||||||
};
|
nome_cognome: z.string(),
|
||||||
type EmailContattoInteressatoProps = {
|
nome_cognome_utente: z.string(),
|
||||||
nome_cognome: string;
|
telefono: z.string(),
|
||||||
nome_cognome_utente: string;
|
indirizzo: z.string().nullable(),
|
||||||
telefono: string;
|
sesso: z.enum(["M", "F"]).nullable(),
|
||||||
indirizzo: string | null;
|
});
|
||||||
sesso: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const EmailContattoInteressato = ({
|
const EmailContattoInteressato = ({
|
||||||
nome_cognome,
|
nome_cognome,
|
||||||
|
|
@ -18,7 +17,7 @@ const EmailContattoInteressato = ({
|
||||||
telefono,
|
telefono,
|
||||||
indirizzo,
|
indirizzo,
|
||||||
sesso,
|
sesso,
|
||||||
}: EmailContattoInteressatoProps) => {
|
}: z.infer<typeof ContattoInteressatoPropsSchema>) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Contatto Email">
|
<Base preview="Contatto Email">
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,33 @@
|
||||||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import z from "zod";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type Generic_NewMail = {
|
|
||||||
mailType: "generic";
|
|
||||||
props: GenericProps;
|
|
||||||
};
|
|
||||||
type Corpo = {
|
|
||||||
testo?: undefined;
|
|
||||||
corpo: ReactNode;
|
|
||||||
};
|
|
||||||
type Testo = {
|
|
||||||
testo: string;
|
|
||||||
corpo?: undefined;
|
|
||||||
};
|
|
||||||
type GenericProps = {
|
|
||||||
title: string;
|
|
||||||
noreply?: boolean;
|
|
||||||
|
|
||||||
link?: {
|
export const GenericMailTag = "generic";
|
||||||
href: string;
|
|
||||||
label?: string;
|
const props_base = z.object({
|
||||||
};
|
title: z.string(),
|
||||||
} & (Corpo | Testo);
|
noreply: z.boolean().optional(),
|
||||||
|
link: z
|
||||||
|
.object({
|
||||||
|
href: z.string(),
|
||||||
|
label: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const props_content = z.union([
|
||||||
|
z.object({
|
||||||
|
corpo: z.custom<ReactNode>(),
|
||||||
|
testo: z.undefined().optional(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
testo: z.string(),
|
||||||
|
corpo: z.undefined().optional(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const GenericPropsSchema = props_base.and(props_content);
|
||||||
|
|
||||||
const Generic = ({
|
const Generic = ({
|
||||||
title,
|
title,
|
||||||
|
|
@ -29,7 +35,7 @@ const Generic = ({
|
||||||
testo,
|
testo,
|
||||||
corpo,
|
corpo,
|
||||||
link = undefined,
|
link = undefined,
|
||||||
}: GenericProps) => {
|
}: z.infer<typeof GenericPropsSchema>) => {
|
||||||
return (
|
return (
|
||||||
<Base noreply={noreply} preview={title}>
|
<Base noreply={noreply} preview={title}>
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Heading, Row, Section, Text } from "@react-email/components";
|
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type Interruzione_NewMail = {
|
|
||||||
mailType: "interruzione";
|
export const InterruzioneMailTag = "interruzione";
|
||||||
};
|
|
||||||
|
|
||||||
const Interruzione = () => {
|
const Interruzione = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,21 @@
|
||||||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
|
import z from "zod";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type Invito_NewMail = {
|
|
||||||
mailType: "invito";
|
|
||||||
props: InvitoProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
type InvitoProps = {
|
export const InvitoMailTag = "invito";
|
||||||
nome: string;
|
export const InvitoPropsSchema = z.object({
|
||||||
//email: string;
|
nome: z.string(),
|
||||||
//password: string;
|
//email: z.email(),
|
||||||
inviteUrl: string;
|
//password: z.string(),
|
||||||
};
|
inviteUrl: z.url(),
|
||||||
|
});
|
||||||
|
|
||||||
const Invito = ({
|
const Invito = ({
|
||||||
nome,
|
nome,
|
||||||
inviteUrl,
|
inviteUrl,
|
||||||
//email,
|
//email,
|
||||||
//password
|
//password
|
||||||
}: InvitoProps) => {
|
}: z.infer<typeof InvitoPropsSchema>) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Procedi ora su Infoalloggi.it">
|
<Base preview="Procedi ora su Infoalloggi.it">
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Heading, Row, Section, Text } from "@react-email/components";
|
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type PagamentoConferma_NewMail = {
|
|
||||||
mailType: "pagamentoConferma";
|
export const PagamentoConfermaMailTag = "pagamentoConferma";
|
||||||
};
|
|
||||||
|
|
||||||
const PagamentoConferma = () => {
|
const PagamentoConferma = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type PagamentoErrore_NewMail = {
|
|
||||||
mailType: "pagamentoErrore";
|
export const PagamentoErroreMailTag = "pagamentoErrore";
|
||||||
};
|
|
||||||
|
|
||||||
const PagamentoErrore = () => {
|
const PagamentoErrore = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type PagamentoRicezione_NewMail = {
|
|
||||||
mailType: "pagamentoRicezione";
|
export const PagamentoRicezioneMailTag = "pagamentoRicezione";
|
||||||
};
|
|
||||||
|
|
||||||
const PagamentoRicezione = () => {
|
const PagamentoRicezione = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
|
import z from "zod";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type PwResetLink_NewMail = {
|
|
||||||
mailType: "pwResetLink";
|
|
||||||
props: PwResetLinkProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PwResetLinkProps = {
|
export const PwResetLinkMailTag = "pwResetLink";
|
||||||
resetlink: string;
|
|
||||||
};
|
export const PwResetLinkPropsSchema = z.object({
|
||||||
const PwResetLink = ({ resetlink }: PwResetLinkProps) => {
|
resetlink: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const PwResetLink = ({ resetlink }: z.infer<typeof PwResetLinkPropsSchema>) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Link Reset Password">
|
<Base preview="Link Reset Password">
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Heading, Row, Section, Text } from "@react-email/components";
|
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type Recesso_NewMail = {
|
|
||||||
mailType: "recesso";
|
export const RecessoMailTag = "recesso";
|
||||||
};
|
|
||||||
const Recesso = () => {
|
const Recesso = () => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Richiesta di recesso">
|
<Base preview="Richiesta di recesso">
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type RegistrazioneAvvenuta_NewMail = {
|
|
||||||
mailType: "registrazioneAvvenuta";
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export const RegistrazioneAvvenutaMailTag = "registrazioneAvvenuta";
|
||||||
const RegistrazioneAvvenuta = () => {
|
const RegistrazioneAvvenuta = () => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Registrazione Avvenuta">
|
<Base preview="Registrazione Avvenuta">
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
import { Heading, Row, Section, Text } from "@react-email/components";
|
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||||
|
import z from "zod";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type SchedaContatto_NewMail = {
|
|
||||||
mailType: "emailSchedaContatto";
|
|
||||||
props: EmailSchedaContattoProps;
|
|
||||||
};
|
|
||||||
type EmailSchedaContattoProps = {
|
|
||||||
nominativo: string;
|
|
||||||
telefono: string;
|
|
||||||
indirizzo: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export const SchedaContattoMailTag = "emailSchedaContatto";
|
||||||
|
export const SchedaContattoPropsSchema = z.object({
|
||||||
|
nominativo: z.string(),
|
||||||
|
telefono: z.string(),
|
||||||
|
indirizzo: z.string(),
|
||||||
|
});
|
||||||
const EmailSchedaContatto = ({
|
const EmailSchedaContatto = ({
|
||||||
nominativo,
|
nominativo,
|
||||||
telefono,
|
telefono,
|
||||||
indirizzo,
|
indirizzo,
|
||||||
}: EmailSchedaContattoProps) => {
|
}: z.infer<typeof SchedaContattoPropsSchema>) => {
|
||||||
return (
|
return (
|
||||||
<Base noreply={true} preview="Dati Contatto Annuncio - Infoalloggi.it">
|
<Base noreply={true} preview="Dati Contatto Annuncio - Infoalloggi.it">
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Heading, Row, Section, Text } from "@react-email/components";
|
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type ServizioAttivato_NewMail = {
|
|
||||||
mailType: "servizioAttivato";
|
export const ServizioAttivatoMailTag = "servizioAttivato";
|
||||||
};
|
|
||||||
|
|
||||||
const ServizioAttivato = () => {
|
const ServizioAttivato = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ export const EmailAccordion = ({
|
||||||
const iframeHeight = mobile ? 600 : 500;
|
const iframeHeight = mobile ? 600 : 500;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full py-4">
|
<div className="mx-auto w-full space-y-2 py-4">
|
||||||
|
<h3>Email inviate:</h3>
|
||||||
<Accordion
|
<Accordion
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
collapsible
|
collapsible
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,16 @@ import {
|
||||||
import { Label } from "src/components/ui/label";
|
import { Label } from "src/components/ui/label";
|
||||||
import { cn } from "src/lib/utils";
|
import { cn } from "src/lib/utils";
|
||||||
|
|
||||||
|
/*
|
||||||
|
React Hook Form DevTools, only in development mode.
|
||||||
|
|
||||||
|
const DevT: React.ElementType = dynamic(
|
||||||
|
() => import('@hookform/devtools').then((module) => module.DevTool),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
const Form = FormProvider;
|
const Form = FormProvider;
|
||||||
|
|
||||||
type FormFieldContextValue<
|
type FormFieldContextValue<
|
||||||
|
|
@ -171,12 +181,12 @@ const FormMessage = React.forwardRef<
|
||||||
FormMessage.displayName = "FormMessage";
|
FormMessage.displayName = "FormMessage";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
|
||||||
FormField,
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useFormField,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex min-h-20 w-full rounded-lg border border-border bg-background px-3 py-2 shadow-sm outline-hidden transition-all duration-200 hover:border-muted-foreground focus:border-foreground focus:ring-foreground disabled:cursor-not-allowed disabled:opacity-50 dark:border-input dark:bg-input/30 dark:hover:border-muted-foreground dark:hover:bg-input/50 dark:placeholder:text-neutral-400`,
|
`flex min-h-20 w-full rounded-lg border border-border bg-card px-3 py-2 shadow-sm outline-hidden transition-all duration-200 hover:border-muted-foreground focus:border-foreground focus:ring-foreground disabled:cursor-not-allowed disabled:opacity-50 dark:border-input dark:bg-input/30 dark:hover:border-muted-foreground dark:hover:bg-input/50 dark:placeholder:text-neutral-400`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
|
|
|
||||||
165
apps/infoalloggi/src/forms/FormEditQueueMail.tsx
Normal file
165
apps/infoalloggi/src/forms/FormEditQueueMail.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { it } from "date-fns/locale";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Confirm } from "~/components/confirm";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "~/components/custom_ui/form";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Calendar } from "~/components/ui/calendar";
|
||||||
|
import Input from "~/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
|
import { useZodForm } from "~/lib/zodForm";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import type { EventQueue, EventQueueUpdate } from "~/schemas/public/EventQueue";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
lock_date: z.date(),
|
||||||
|
lock_time: z.string(),
|
||||||
|
data: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof Schema>;
|
||||||
|
export const FormEditQueueMail = ({
|
||||||
|
data,
|
||||||
|
submitMutation,
|
||||||
|
del,
|
||||||
|
}: {
|
||||||
|
data: EventQueue;
|
||||||
|
submitMutation: (fields: EventQueueUpdate) => void;
|
||||||
|
del: () => void;
|
||||||
|
}) => {
|
||||||
|
const { locale, t } = useTranslation();
|
||||||
|
|
||||||
|
z.config(z.locales[locale]());
|
||||||
|
|
||||||
|
const form = useZodForm(Schema, {
|
||||||
|
defaultValues: {
|
||||||
|
lock_date: new Date(data.lock_expires),
|
||||||
|
lock_time: format(new Date(data.lock_expires), "HH:mm:ss"),
|
||||||
|
data: JSON.stringify(data.data, null, 2),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit(fields: FormValues) {
|
||||||
|
submitMutation({
|
||||||
|
data: fields.data,
|
||||||
|
lock_expires: new Date(
|
||||||
|
format(fields.lock_date, "yyyy-MM-dd", { locale: it }) +
|
||||||
|
"T" +
|
||||||
|
fields.lock_time,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="space-y-8 px-0.5" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<div className="grid w-full grid-cols-2 items-center gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="lock_date"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="titolo">Data Invio</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-full justify-between bg-card font-normal"
|
||||||
|
id="date-picker-optional"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? format(field.value, "PPP", { locale: it })
|
||||||
|
: "Seleziona data"}
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
align="start"
|
||||||
|
className="w-auto overflow-hidden p-0"
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
captionLayout="dropdown"
|
||||||
|
defaultMonth={field.value}
|
||||||
|
mode="single"
|
||||||
|
onSelect={(date) => {
|
||||||
|
field.onChange(date);
|
||||||
|
}}
|
||||||
|
selected={field.value}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="lock_time"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="titolo">Ora Invio</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
className="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||||
|
defaultValue={field.value}
|
||||||
|
id="time-picker-optional"
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
step="1"
|
||||||
|
type="time"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="data"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="data">{t.contact_form.messaggio}</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea className="min-h-50" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full justify-end gap-2">
|
||||||
|
<Button type="submit">Salva</Button>
|
||||||
|
<Confirm
|
||||||
|
description="Sei sicuro di voler svuotare la coda email?"
|
||||||
|
onConfirm={del}
|
||||||
|
title="Elimina"
|
||||||
|
>
|
||||||
|
<Button aria-label="Delete" variant="destructive">
|
||||||
|
Rimuovi dalla coda
|
||||||
|
</Button>
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
421
apps/infoalloggi/src/forms/FormNewMail.tsx
Normal file
421
apps/infoalloggi/src/forms/FormNewMail.tsx
Normal file
|
|
@ -0,0 +1,421 @@
|
||||||
|
import { format, isAfter } from "date-fns";
|
||||||
|
import { it } from "date-fns/locale";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "~/components/custom_ui/form";
|
||||||
|
import { MultiSelect } from "~/components/custom_ui/multiselect";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Calendar } from "~/components/ui/calendar";
|
||||||
|
import Input from "~/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import { useZodForm } from "~/lib/zodForm";
|
||||||
|
import {
|
||||||
|
type MailsTemplates,
|
||||||
|
TemplateKeys,
|
||||||
|
Templates,
|
||||||
|
type TemplateType,
|
||||||
|
} from "~/server/services/mail-templates";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type FormProps = {
|
||||||
|
submitMutation: (values: {
|
||||||
|
lock_expires: Date;
|
||||||
|
template: MailsTemplates;
|
||||||
|
subject: string;
|
||||||
|
}) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Schema = z
|
||||||
|
.object({
|
||||||
|
expires_date: z.date().refine((d) => isAfter(d, new Date()), {
|
||||||
|
message: "La data deve essere nel futuro",
|
||||||
|
}),
|
||||||
|
expires_time: z.string(),
|
||||||
|
mailType: z.enum(TemplateKeys),
|
||||||
|
mailProps: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
subject: z.string(),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
const propsSchema = Templates[data.mailType as TemplateType].props;
|
||||||
|
if (propsSchema) {
|
||||||
|
const result = propsSchema.safeParse(data.mailProps);
|
||||||
|
if (!result.success) {
|
||||||
|
for (const issue of result.error.issues) {
|
||||||
|
ctx.addIssue({
|
||||||
|
...issue,
|
||||||
|
path: ["mailProps", ...(issue.path ?? [])],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
type FormValues = z.infer<typeof Schema>;
|
||||||
|
export const FormAddCodaEmail = ({ submitMutation }: FormProps) => {
|
||||||
|
z.config(z.locales.it());
|
||||||
|
|
||||||
|
const form = useZodForm(Schema, {
|
||||||
|
defaultValues: {
|
||||||
|
expires_date: new Date(),
|
||||||
|
expires_time: "00:00:00",
|
||||||
|
mailType: TemplateKeys[0],
|
||||||
|
mailProps: undefined,
|
||||||
|
subject: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit(fields: FormValues) {
|
||||||
|
const template = {
|
||||||
|
mailType: fields.mailType,
|
||||||
|
props: fields.mailProps as MailsTemplates["props"],
|
||||||
|
} as MailsTemplates;
|
||||||
|
|
||||||
|
const values: {
|
||||||
|
lock_expires: Date;
|
||||||
|
template: MailsTemplates;
|
||||||
|
subject: string;
|
||||||
|
} = {
|
||||||
|
lock_expires: new Date(
|
||||||
|
`${fields.expires_date.toISOString().split("T")[0]}T${fields.expires_time}`,
|
||||||
|
),
|
||||||
|
template,
|
||||||
|
subject: fields.subject,
|
||||||
|
};
|
||||||
|
submitMutation(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = form.watch("mailType");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValue("mailProps", {});
|
||||||
|
}, [type, form]);
|
||||||
|
|
||||||
|
useWatch({
|
||||||
|
name: "mailProps",
|
||||||
|
control: form.control,
|
||||||
|
compute: (props) => {
|
||||||
|
if (!props) return;
|
||||||
|
if ("title" in props) {
|
||||||
|
form.setValue("subject", props.title as string);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form {...form}>
|
||||||
|
|
||||||
|
<form
|
||||||
|
className="space-y-8 px-0.5"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div className="grid w-full grid-cols-2 items-center gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="expires_date"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="titolo">Data Invio</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-full justify-between bg-card font-normal"
|
||||||
|
id="date-picker-optional"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? format(field.value, "PPP", { locale: it })
|
||||||
|
: "Seleziona data"}
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
align="start"
|
||||||
|
className="w-auto overflow-hidden p-0"
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
captionLayout="dropdown"
|
||||||
|
defaultMonth={field.value}
|
||||||
|
mode="single"
|
||||||
|
onSelect={(date) => {
|
||||||
|
field.onChange(date);
|
||||||
|
}}
|
||||||
|
selected={field.value}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="expires_time"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="titolo">Ora Invio</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
className="appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||||
|
defaultValue="10:30:00"
|
||||||
|
id="time-picker-optional"
|
||||||
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
|
step="1"
|
||||||
|
type="time"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mailType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="select-mailType">Modello</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<MultiSelect
|
||||||
|
defaultValue={
|
||||||
|
field.value
|
||||||
|
? {
|
||||||
|
label: Templates[field.value].title,
|
||||||
|
value: field.value,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
elementId="select-mailType"
|
||||||
|
elementName="mailType"
|
||||||
|
isMulti={false}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value.value as TemplateType);
|
||||||
|
}}
|
||||||
|
options={TemplateKeys.map((key) => ({
|
||||||
|
label: Templates[key].title,
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
placeholder="Seleziona..."
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MailPropSection mailType={type as TemplateType} />
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="subject"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel htmlFor="subject">Oggetto</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="" {...field} id="subject" />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">Salva</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Zod v4 schema introspection helpers ---
|
||||||
|
|
||||||
|
function getType(schema: unknown): string {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Zod v4 internal introspection
|
||||||
|
return (schema as any)?.type ?? "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrap(schema: unknown): unknown {
|
||||||
|
const t = getType(schema);
|
||||||
|
if (t === "optional" || t === "nullable")
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Zod v4 internal introspection
|
||||||
|
return unwrap((schema as any).def.innerType);
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractShape(schema: unknown): Record<string, unknown> {
|
||||||
|
if (!schema || typeof schema !== "object") return {};
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Zod v4 internal introspection
|
||||||
|
const s = schema as any;
|
||||||
|
const t = getType(s);
|
||||||
|
|
||||||
|
if (t === "optional" || t === "nullable")
|
||||||
|
return extractShape(s.def.innerType);
|
||||||
|
if (s.shape && typeof s.shape === "object") return s.shape;
|
||||||
|
if (t === "intersection")
|
||||||
|
return { ...extractShape(s.def.left), ...extractShape(s.def.right) };
|
||||||
|
if (t === "union" && Array.isArray(s.def.options)) {
|
||||||
|
const merged: Record<string, unknown> = {};
|
||||||
|
for (const opt of s.def.options) {
|
||||||
|
for (const [k, v] of Object.entries(extractShape(opt))) {
|
||||||
|
if (!merged[k] || getFieldType(merged[k]) === "skip") merged[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldType = "text" | "checkbox" | "number" | "object" | "skip";
|
||||||
|
|
||||||
|
function getFieldType(schema: unknown): FieldType {
|
||||||
|
const inner = unwrap(schema);
|
||||||
|
const t = getType(inner);
|
||||||
|
switch (t) {
|
||||||
|
case "boolean":
|
||||||
|
return "checkbox";
|
||||||
|
case "number":
|
||||||
|
return "number";
|
||||||
|
case "object":
|
||||||
|
return "object";
|
||||||
|
case "custom":
|
||||||
|
case "undefined":
|
||||||
|
case "unknown":
|
||||||
|
return "skip";
|
||||||
|
default:
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOptionalField(schema: unknown): boolean {
|
||||||
|
const t = getType(schema);
|
||||||
|
return t === "optional" || t === "nullable";
|
||||||
|
}
|
||||||
|
|
||||||
|
const FieldRenderer = ({
|
||||||
|
fieldSchema,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
fieldSchema: unknown;
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
}) => {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
const fieldType = getFieldType(fieldSchema);
|
||||||
|
const optional = isOptionalField(fieldSchema);
|
||||||
|
|
||||||
|
if (fieldType === "skip") return null;
|
||||||
|
|
||||||
|
if (fieldType === "object") {
|
||||||
|
const nestedShape = extractShape(fieldSchema);
|
||||||
|
const entries = Object.entries(nestedShape);
|
||||||
|
if (entries.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<fieldset className="space-y-3 rounded-md border p-3">
|
||||||
|
<legend className="px-1 font-medium text-sm">
|
||||||
|
{label}
|
||||||
|
{optional && (
|
||||||
|
<span className="ml-1 text-muted-foreground text-xs">
|
||||||
|
(opzionale)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</legend>
|
||||||
|
{entries.map(([key, nested]) => (
|
||||||
|
<FieldRenderer
|
||||||
|
fieldSchema={nested}
|
||||||
|
key={key}
|
||||||
|
label={key}
|
||||||
|
name={`${name}.${key}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={control}
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic field name
|
||||||
|
name={name as any}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
<FormLabel>
|
||||||
|
{label}
|
||||||
|
{optional && (
|
||||||
|
<span className="ml-1 text-muted-foreground text-xs">
|
||||||
|
(opzionale)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
{fieldType === "checkbox" ? (
|
||||||
|
<input
|
||||||
|
checked={!!(field.value as boolean)}
|
||||||
|
className="h-4 w-4"
|
||||||
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
onChange={
|
||||||
|
fieldType === "number"
|
||||||
|
? (e) => field.onChange(Number(e.target.value))
|
||||||
|
: field.onChange
|
||||||
|
}
|
||||||
|
type={fieldType}
|
||||||
|
value={(field.value as string | number) ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MailPropSection = ({ mailType }: { mailType: TemplateType }) => {
|
||||||
|
const template = Templates[mailType];
|
||||||
|
if (!template.props) return null;
|
||||||
|
|
||||||
|
const shape = extractShape(template.props);
|
||||||
|
const entries = Object.entries(shape);
|
||||||
|
if (entries.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{entries.map(([key, fieldSchema]) => (
|
||||||
|
<FieldRenderer
|
||||||
|
fieldSchema={fieldSchema}
|
||||||
|
key={key}
|
||||||
|
label={key}
|
||||||
|
name={`mailProps.${key}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -26,9 +26,9 @@ const AdminPotenziali: NextPageWithLayout = () => {
|
||||||
<Head>
|
<Head>
|
||||||
<title>Potenziali - Infoalloggi.it</title>
|
<title>Potenziali - Infoalloggi.it</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="mx-1 flex-1 overflow-auto">
|
<div className="mx-1 w-full flex-1">
|
||||||
<div className="mx-auto pt-4">
|
<div className="mx-auto w-full pt-4">
|
||||||
<div className="mx-3 flex-wrap items-start justify-between pb-5 sm:flex">
|
<div className="mx-3 flex flex-wrap items-start justify-between pb-5">
|
||||||
<div className="flex max-w-lg items-center gap-3">
|
<div className="flex max-w-lg items-center gap-3">
|
||||||
<h3 className="font-bold text-accent-foreground text-xl sm:text-2xl">
|
<h3 className="font-bold text-accent-foreground text-xl sm:text-2xl">
|
||||||
Potenziali
|
Potenziali
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
import { MoreVertical, Trash2 } from "lucide-react";
|
import { Maximize2, MoreVertical, Plus, Trash2 } from "lucide-react";
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { EmailAccordion } from "~/components/area-riservata/comunicazioni";
|
import { EmailAccordion } from "~/components/area-riservata/comunicazioni";
|
||||||
import { Confirm } from "~/components/confirm";
|
import { Confirm } from "~/components/confirm";
|
||||||
import { AreaRiservataLayoutUserView } from "~/components/Layout";
|
import { AreaRiservataLayoutUserView } from "~/components/Layout";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
import { Label } from "~/components/ui/label";
|
import { Label } from "~/components/ui/label";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
|
|
@ -12,9 +21,13 @@ import {
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "~/components/ui/popover";
|
} from "~/components/ui/popover";
|
||||||
import { Switch } from "~/components/ui/switch";
|
import { Switch } from "~/components/ui/switch";
|
||||||
|
import { FormEditQueueMail } from "~/forms/FormEditQueueMail";
|
||||||
|
import { FormAddCodaEmail } from "~/forms/FormNewMail";
|
||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
import { useEnforcedSession } from "~/providers/SessionProvider";
|
import { useEnforcedSession } from "~/providers/SessionProvider";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import type { Email_EventData } from "~/server/controllers/event_queue.controller";
|
||||||
|
|
||||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
import { zUserId } from "~/server/utils/zod_types";
|
import { zUserId } from "~/server/utils/zod_types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
@ -77,6 +90,7 @@ const ComunicazioniUser: NextPageWithLayout<ComunicazioniUserProps> = ({
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<CommsSwitch userId={userId} />
|
<CommsSwitch userId={userId} />
|
||||||
|
<Coda userId={userId} />
|
||||||
<EmailAccordion isAdmin={user.isAdmin} userId={userId} />
|
<EmailAccordion isAdmin={user.isAdmin} userId={userId} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
@ -106,6 +120,9 @@ export const getServerSideProps = (async (context) => {
|
||||||
await helpers.trpc.comunicazioni.getEmails.prefetch({
|
await helpers.trpc.comunicazioni.getEmails.prefetch({
|
||||||
userId: parsed.data,
|
userId: parsed.data,
|
||||||
});
|
});
|
||||||
|
await helpers.trpc.comunicazioni.getUserQueuedEmails.prefetch({
|
||||||
|
userId: parsed.data,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
userId: parsed.data,
|
userId: parsed.data,
|
||||||
|
|
@ -154,3 +171,159 @@ const CommsSwitch = ({ userId }: { userId: UsersId }) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Coda = ({ userId }: { userId: UsersId }) => {
|
||||||
|
const { data, isLoading } = api.comunicazioni.getUserQueuedEmails.useQuery({
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { mutate } = api.comunicazioni.clearUserEmailQueue.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast.success("Coda email pulita");
|
||||||
|
await utils.comunicazioni.getUserQueuedEmails.invalidate({ userId });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Errore durante la pulizia della coda email");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { mutate: removeFromQueue } =
|
||||||
|
api.comunicazioni.removeFromUserEmailQueue.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast.success("Email rimossa dalla coda");
|
||||||
|
await utils.comunicazioni.getUserQueuedEmails.invalidate({ userId });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Errore durante la rimozione dell'email dalla coda");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: addToQueue } =
|
||||||
|
api.comunicazioni.addToUserEmailQueue.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast.success("Email aggiunta alla coda");
|
||||||
|
await utils.comunicazioni.getUserQueuedEmails.invalidate({ userId });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Errore durante l'aggiunta dell'email alla coda");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: updateEvent } = api.comunicazioni.updateEvent.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast.success("Email aggiornata nella coda");
|
||||||
|
await utils.comunicazioni.getUserQueuedEmails.invalidate({ userId });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Errore durante l'aggiornamento dell'email nella coda");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [newOpen, setNewOpen] = useState(false);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div>Errore nel caricamento coda delle email</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-2">
|
||||||
|
<h3>Email in coda:</h3>
|
||||||
|
{data.length === 0 && <div>Nessuna email in coda</div>}
|
||||||
|
{data.map((email) => {
|
||||||
|
const mailData = email.data as Email_EventData;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 rounded-md border p-1.5"
|
||||||
|
key={email.event_id}
|
||||||
|
>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Maximize2 />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Dettagli</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
dettagli dell'email in coda
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FormEditQueueMail
|
||||||
|
data={email}
|
||||||
|
del={() => removeFromQueue({ eventId: email.event_id })}
|
||||||
|
submitMutation={(values) => {
|
||||||
|
updateEvent({ eventId: email.event_id, data: values });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<div className="font-semibold">
|
||||||
|
{mailData.data.mail.subject ||
|
||||||
|
mailData.data.template?.mailType ||
|
||||||
|
"Nessun oggetto"}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
data invio:{" "}
|
||||||
|
{new Date(email.lock_expires).toLocaleDateString("it-IT", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="flex w-full justify-start gap-2">
|
||||||
|
<Dialog onOpenChange={setNewOpen} open={newOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Plus /> Aggiungi email alla coda
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Aggiungi email alla coda</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
aggiungi email alla coda
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FormAddCodaEmail
|
||||||
|
submitMutation={(values) => {
|
||||||
|
addToQueue({ userId, ...values });
|
||||||
|
setNewOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Confirm
|
||||||
|
description="Sei sicuro di voler svuotare la coda email?"
|
||||||
|
onConfirm={() => {
|
||||||
|
mutate({ userId });
|
||||||
|
}}
|
||||||
|
title="Elimina"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
aria-label="Delete"
|
||||||
|
disabled={data.length === 0}
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
Pulisci coda email
|
||||||
|
</Button>
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import type { EmailsIdEmail } from "~/schemas/public/Emails";
|
import type { EmailsIdEmail } from "~/schemas/public/Emails";
|
||||||
|
import type { EventQueueUpdate } from "~/schemas/public/EventQueue";
|
||||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
|
|
@ -9,6 +11,9 @@ import {
|
||||||
} from "~/server/api/trpc";
|
} from "~/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
addEventToQueue,
|
addEventToQueue,
|
||||||
|
clearUserEmailQueue,
|
||||||
|
removeEventFromQueue,
|
||||||
|
updateEvent,
|
||||||
userEmailQueue,
|
userEmailQueue,
|
||||||
} from "~/server/controllers/event_queue.controller";
|
} from "~/server/controllers/event_queue.controller";
|
||||||
import { UtenteInteressatoAdAnnuncioEmail } from "~/server/controllers/servizio.controller";
|
import { UtenteInteressatoAdAnnuncioEmail } from "~/server/controllers/servizio.controller";
|
||||||
|
|
@ -18,10 +23,15 @@ import {
|
||||||
deleteEmail,
|
deleteEmail,
|
||||||
getEmails,
|
getEmails,
|
||||||
} from "~/server/services/email.service";
|
} from "~/server/services/email.service";
|
||||||
import { NewMail } from "~/server/services/mailer";
|
import { type MailsTemplates, NewMail } from "~/server/services/mailer";
|
||||||
import { CONDIZIONI_DEFAULT } from "~/server/services/prezziario.service";
|
import { CONDIZIONI_DEFAULT } from "~/server/services/prezziario.service";
|
||||||
import { genPdfSchedaAnnuncioBase64 } from "~/server/services/puppeteer.service";
|
import { genPdfSchedaAnnuncioBase64 } from "~/server/services/puppeteer.service";
|
||||||
import { zAnnuncioId, zOrdineId, zUserId } from "~/server/utils/zod_types";
|
import {
|
||||||
|
zAnnuncioId,
|
||||||
|
zEventId,
|
||||||
|
zOrdineId,
|
||||||
|
zUserId,
|
||||||
|
} from "~/server/utils/zod_types";
|
||||||
|
|
||||||
export const comunicazioniRouter = createTRPCRouter({
|
export const comunicazioniRouter = createTRPCRouter({
|
||||||
getEmails: protectedProcedure
|
getEmails: protectedProcedure
|
||||||
|
|
@ -130,9 +140,11 @@ export const comunicazioniRouter = createTRPCRouter({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new TRPCError({
|
||||||
`Errore nell'invio dell'email: ${(e as Error).message}`,
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
);
|
message: `Errore nell'invio dell'email: ${(e as Error).message}`,
|
||||||
|
cause: e instanceof Error ? e : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -210,9 +222,11 @@ export const comunicazioniRouter = createTRPCRouter({
|
||||||
lock_expires: new Date(Date.now() + 3000), // Lock for 30 sec
|
lock_expires: new Date(Date.now() + 3000), // Lock for 30 sec
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new TRPCError({
|
||||||
`Errore nell'invio dell'email: ${(e as Error).message}`,
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
);
|
message: `Errore nell'aggiunta dell'email alla coda: ${(e as Error).message}`,
|
||||||
|
cause: e instanceof Error ? e : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getUserQueuedEmails: protectedProcedure
|
getUserQueuedEmails: protectedProcedure
|
||||||
|
|
@ -220,4 +234,73 @@ export const comunicazioniRouter = createTRPCRouter({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await userEmailQueue(input.userId);
|
return await userEmailQueue(input.userId);
|
||||||
}),
|
}),
|
||||||
|
addToUserEmailQueue: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
lock_expires: z.date(),
|
||||||
|
userId: zUserId,
|
||||||
|
template: z.custom<MailsTemplates>(),
|
||||||
|
subject: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const user = await db
|
||||||
|
.selectFrom("users")
|
||||||
|
.selectAll()
|
||||||
|
.where("id", "=", input.userId)
|
||||||
|
.executeTakeFirstOrThrow(() => {
|
||||||
|
throw new Error(`User with id ${input.userId} not found`);
|
||||||
|
});
|
||||||
|
await addEventToQueue({
|
||||||
|
data: {
|
||||||
|
tipo: "email",
|
||||||
|
data: {
|
||||||
|
template: input.template,
|
||||||
|
mail: {
|
||||||
|
to: user.email,
|
||||||
|
subject: input.subject,
|
||||||
|
},
|
||||||
|
userId: input.userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lock_expires: input.lock_expires,
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: `Errore nell'aggiunta dell'email alla coda: ${
|
||||||
|
error instanceof Error ? error.message : "Errore sconosciuto"
|
||||||
|
}`,
|
||||||
|
cause: error instanceof Error ? error : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeFromUserEmailQueue: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
eventId: zEventId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return await removeEventFromQueue(input.eventId);
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearUserEmailQueue: adminProcedure
|
||||||
|
.input(z.object({ userId: zUserId }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return await clearUserEmailQueue(input.userId);
|
||||||
|
}),
|
||||||
|
updateEvent: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
eventId: zEventId,
|
||||||
|
data: z.custom<EventQueueUpdate>(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return await updateEvent(input.eventId, input.data);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import { add, set } from "date-fns";
|
||||||
import { sql } from "kysely";
|
import { sql } from "kysely";
|
||||||
import type { Attachment } from "nodemailer/lib/mailer";
|
import type { Attachment } from "nodemailer/lib/mailer";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||||
import type { NewEventQueue } from "~/schemas/public/EventQueue";
|
import type {
|
||||||
|
EventQueueEventId,
|
||||||
|
EventQueueUpdate,
|
||||||
|
NewEventQueue,
|
||||||
|
} from "~/schemas/public/EventQueue";
|
||||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
|
@ -26,7 +30,7 @@ type GeneratedAttachmentTypes =
|
||||||
| { type: "condizioni"; stringId: TestiEStringheStingaId };
|
| { type: "condizioni"; stringId: TestiEStringheStingaId };
|
||||||
|
|
||||||
type CustomAttachment = Attachment & { generate?: GeneratedAttachmentTypes };
|
type CustomAttachment = Attachment & { generate?: GeneratedAttachmentTypes };
|
||||||
type Email_EventData = {
|
export type Email_EventData = {
|
||||||
tipo: "email";
|
tipo: "email";
|
||||||
data: NewMailProps & {
|
data: NewMailProps & {
|
||||||
mail: {
|
mail: {
|
||||||
|
|
@ -174,3 +178,55 @@ export const userEmailQueue = async (userId: UsersId) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeEventFromQueue = async (eventId: EventQueueEventId) => {
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.deleteFrom("event_queue")
|
||||||
|
.where("event_id", "=", eventId)
|
||||||
|
.execute();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: `Error while removing event from queue, ${(e as Error).message}`,
|
||||||
|
cause: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const clearUserEmailQueue = async (userId: UsersId) => {
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.deleteFrom("event_queue")
|
||||||
|
.where(sql`data->>'tipo'`, "=", "email")
|
||||||
|
.where(sql`data->'data'->>'userId'`, "=", userId)
|
||||||
|
.execute();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: `Error while clearing user events from queue, ${(e as Error).message}`,
|
||||||
|
cause: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateEvent = async (
|
||||||
|
eventId: EventQueueEventId,
|
||||||
|
data: EventQueueUpdate,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.updateTable("event_queue")
|
||||||
|
.set(data)
|
||||||
|
.where("event_id", "=", eventId)
|
||||||
|
.execute();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: `Error while updating event in queue, ${(e as Error).message}`,
|
||||||
|
cause: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
|
||||||
import {
|
import {
|
||||||
addEventToQueue,
|
addEventToQueue,
|
||||||
canSendNotification,
|
canSendNotification,
|
||||||
|
clearUserEmailQueue,
|
||||||
nextTimeForNotification,
|
nextTimeForNotification,
|
||||||
} from "~/server/controllers/event_queue.controller";
|
} from "~/server/controllers/event_queue.controller";
|
||||||
import {
|
import {
|
||||||
|
|
@ -830,7 +831,7 @@ export const SbloccaContatti = async ({
|
||||||
indirizzo: data.annuncio.indirizzo,
|
indirizzo: data.annuncio.indirizzo,
|
||||||
nome_cognome: `${data.annuncio.locatore}${data.annuncio?.tipo_locatore ? ` (${data.annuncio.tipo_locatore})` : ""}`,
|
nome_cognome: `${data.annuncio.locatore}${data.annuncio?.tipo_locatore ? ` (${data.annuncio.tipo_locatore})` : ""}`,
|
||||||
nome_cognome_utente: data.user.username,
|
nome_cognome_utente: data.user.username,
|
||||||
sesso: data.anagrafica.sesso,
|
sesso: data.anagrafica.sesso as "M" | "F" | null,
|
||||||
telefono: data.user.telefono,
|
telefono: data.user.telefono,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -971,6 +972,9 @@ export const interruzioneServizio = async (
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// cleanup coda notifiche: rimuove notifiche future relative a questo servizio
|
||||||
|
await clearUserEmailQueue(utente.id);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|
|
||||||
108
apps/infoalloggi/src/server/services/mail-templates.ts
Normal file
108
apps/infoalloggi/src/server/services/mail-templates.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import {
|
||||||
|
AvvisoInterruzioneMailTag,
|
||||||
|
} from "emails/avviso-interruzione";
|
||||||
|
import {
|
||||||
|
ContattoAdminEmailPropsSchema,
|
||||||
|
ContattoAdminMailTag,
|
||||||
|
} from "emails/contatto";
|
||||||
|
import {
|
||||||
|
ContattoAnnuncioMailTag,
|
||||||
|
ContattoAnnuncioPropsSchema,
|
||||||
|
} from "emails/contatto_annuncio";
|
||||||
|
import {
|
||||||
|
ContattoInteressatoMailTag,
|
||||||
|
ContattoInteressatoPropsSchema,
|
||||||
|
} from "emails/email-interessato";
|
||||||
|
import {
|
||||||
|
GenericMailTag,
|
||||||
|
GenericPropsSchema,
|
||||||
|
} from "emails/gereric-email";
|
||||||
|
import { InterruzioneMailTag } from "emails/interruzione";
|
||||||
|
import { InvitoMailTag, InvitoPropsSchema } from "emails/invito";
|
||||||
|
import {
|
||||||
|
PagamentoConfermaMailTag,
|
||||||
|
} from "emails/pagamento-conferma";
|
||||||
|
import {
|
||||||
|
PagamentoErroreMailTag,
|
||||||
|
} from "emails/pagamento-errore";
|
||||||
|
import {
|
||||||
|
PagamentoRicezioneMailTag,
|
||||||
|
} from "emails/pagamento-ricezione";
|
||||||
|
import {
|
||||||
|
PwResetLinkMailTag,
|
||||||
|
PwResetLinkPropsSchema,
|
||||||
|
} from "emails/pw-reset-link";
|
||||||
|
import { RecessoMailTag } from "emails/recesso";
|
||||||
|
import {
|
||||||
|
RegistrazioneAvvenutaMailTag,
|
||||||
|
} from "emails/registrazione-avvenuta";
|
||||||
|
import {
|
||||||
|
SchedaContattoMailTag,
|
||||||
|
SchedaContattoPropsSchema,
|
||||||
|
} from "emails/scheda-annuncio";
|
||||||
|
import {
|
||||||
|
ServizioAttivatoMailTag,
|
||||||
|
} from "emails/servizio-attivato";
|
||||||
|
import {
|
||||||
|
VerificaEmailMailTag,
|
||||||
|
VerificaEmailPropsSchema,
|
||||||
|
} from "emails/VerificaEmail";
|
||||||
|
import type { ZodSchema, z } from "zod";
|
||||||
|
|
||||||
|
export const Templates = {
|
||||||
|
[GenericMailTag]: { title: "Email Generica", props: GenericPropsSchema },
|
||||||
|
[AvvisoInterruzioneMailTag]: { title: "Avviso Interruzione", props: null },
|
||||||
|
[ContattoAnnuncioMailTag]: {
|
||||||
|
title: "Contatto Annuncio",
|
||||||
|
props: ContattoAnnuncioPropsSchema,
|
||||||
|
},
|
||||||
|
[ContattoAdminMailTag]: {
|
||||||
|
title: "Contatto Amministratore",
|
||||||
|
props: ContattoAdminEmailPropsSchema,
|
||||||
|
},
|
||||||
|
[ContattoInteressatoMailTag]: {
|
||||||
|
title: "Contatto Interessato",
|
||||||
|
props: ContattoInteressatoPropsSchema,
|
||||||
|
},
|
||||||
|
[InterruzioneMailTag]: { title: "Interruzione Servizio", props: null },
|
||||||
|
[InvitoMailTag]: { title: "Invito al sito", props: InvitoPropsSchema },
|
||||||
|
[PagamentoConfermaMailTag]: { title: "Conferma pagamento", props: null },
|
||||||
|
[PagamentoErroreMailTag]: { title: "Errore pagamento", props: null },
|
||||||
|
[PagamentoRicezioneMailTag]: { title: "Ricezione pagamento", props: null },
|
||||||
|
[PwResetLinkMailTag]: {
|
||||||
|
title: "Reset password",
|
||||||
|
props: PwResetLinkPropsSchema,
|
||||||
|
},
|
||||||
|
[RecessoMailTag]: { title: "Richiesta di recesso", props: null },
|
||||||
|
[RegistrazioneAvvenutaMailTag]: {
|
||||||
|
title: "Registrazione avvenuta",
|
||||||
|
props: null,
|
||||||
|
},
|
||||||
|
[SchedaContattoMailTag]: {
|
||||||
|
title: "Scheda contatto annuncio",
|
||||||
|
props: SchedaContattoPropsSchema,
|
||||||
|
},
|
||||||
|
[ServizioAttivatoMailTag]: { title: "Servizio Attivato", props: null },
|
||||||
|
[VerificaEmailMailTag]: {
|
||||||
|
title: "Verifica email",
|
||||||
|
props: VerificaEmailPropsSchema,
|
||||||
|
},
|
||||||
|
} satisfies Record<string, { title: string; props: ZodSchema | null }>;
|
||||||
|
|
||||||
|
export const TemplateKeys = Object.keys(Templates) as Array<
|
||||||
|
keyof typeof Templates
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const TemplateSchemas = Object.entries(Templates)
|
||||||
|
.map(([_, value]) => value.props)
|
||||||
|
.filter((schema) => schema !== null);
|
||||||
|
|
||||||
|
export type TemplateType = keyof typeof Templates;
|
||||||
|
|
||||||
|
export type MailsTemplates = {
|
||||||
|
[K in TemplateType]: {
|
||||||
|
mailType: K;
|
||||||
|
} & ((typeof Templates)[K] extends { props: z.ZodSchema }
|
||||||
|
? { props: z.infer<(typeof Templates)[K]["props"]> }
|
||||||
|
: { props?: never });
|
||||||
|
}[TemplateType];
|
||||||
|
|
@ -1,41 +1,21 @@
|
||||||
import { render } from "@react-email/render";
|
import { render } from "@react-email/render";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import type { AvvisoInterruzione_NewMail } from "emails/avviso-interruzione";
|
|
||||||
import AvvisoInterruzione from "emails/avviso-interruzione";
|
import AvvisoInterruzione from "emails/avviso-interruzione";
|
||||||
import ContattoAdminEmail, {
|
import ContattoAdminEmail from "emails/contatto";
|
||||||
type ContattoAdmin_NewMail,
|
|
||||||
} from "emails/contatto";
|
|
||||||
import type { ContattoAnnuncio_NewMail } from "emails/contatto_annuncio";
|
|
||||||
import ContattoAnnuncioEmail from "emails/contatto_annuncio";
|
import ContattoAnnuncioEmail from "emails/contatto_annuncio";
|
||||||
import EmailContattoInteressato, {
|
import EmailContattoInteressato from "emails/email-interessato";
|
||||||
type ContattoInteressato_NewMail,
|
|
||||||
} from "emails/email-interessato";
|
|
||||||
import type { Generic_NewMail } from "emails/gereric-email";
|
|
||||||
import Generic from "emails/gereric-email";
|
import Generic from "emails/gereric-email";
|
||||||
import type { Interruzione_NewMail } from "emails/interruzione";
|
|
||||||
import Interruzione from "emails/interruzione";
|
import Interruzione from "emails/interruzione";
|
||||||
import type { Invito_NewMail } from "emails/invito";
|
|
||||||
import Invito from "emails/invito";
|
import Invito from "emails/invito";
|
||||||
import type { PagamentoConferma_NewMail } from "emails/pagamento-conferma";
|
|
||||||
import PagamentoConferma from "emails/pagamento-conferma";
|
import PagamentoConferma from "emails/pagamento-conferma";
|
||||||
import type { PagamentoErrore_NewMail } from "emails/pagamento-errore";
|
|
||||||
import PagamentoErrore from "emails/pagamento-errore";
|
import PagamentoErrore from "emails/pagamento-errore";
|
||||||
import type { PagamentoRicezione_NewMail } from "emails/pagamento-ricezione";
|
|
||||||
import PagamentoRicezione from "emails/pagamento-ricezione";
|
import PagamentoRicezione from "emails/pagamento-ricezione";
|
||||||
import type { PwResetLink_NewMail } from "emails/pw-reset-link";
|
|
||||||
import PwResetLink from "emails/pw-reset-link";
|
import PwResetLink from "emails/pw-reset-link";
|
||||||
import type { Recesso_NewMail } from "emails/recesso";
|
|
||||||
import Recesso from "emails/recesso";
|
import Recesso from "emails/recesso";
|
||||||
import type { RegistrazioneAvvenuta_NewMail } from "emails/registrazione-avvenuta";
|
|
||||||
import RegistrazioneAvvenuta from "emails/registrazione-avvenuta";
|
import RegistrazioneAvvenuta from "emails/registrazione-avvenuta";
|
||||||
import EmailSchedaContatto, {
|
import EmailSchedaContatto from "emails/scheda-annuncio";
|
||||||
type SchedaContatto_NewMail,
|
|
||||||
} from "emails/scheda-annuncio";
|
|
||||||
import type { ServizioAttivato_NewMail } from "emails/servizio-attivato";
|
|
||||||
import ServizioAttivato from "emails/servizio-attivato";
|
import ServizioAttivato from "emails/servizio-attivato";
|
||||||
import VerificaEmail, {
|
import VerificaEmail from "emails/VerificaEmail";
|
||||||
type VerificaEmail_NewMail,
|
|
||||||
} from "emails/VerificaEmail";
|
|
||||||
import { createTransport } from "nodemailer";
|
import { createTransport } from "nodemailer";
|
||||||
import type { Options } from "nodemailer/lib/mailer";
|
import type { Options } from "nodemailer/lib/mailer";
|
||||||
import { htmlToText } from "nodemailer-html-to-text";
|
import { htmlToText } from "nodemailer-html-to-text";
|
||||||
|
|
@ -46,6 +26,19 @@ import { GetFlagValueHandler } from "~/server/controllers/flags.controller";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { storeEmail } from "~/server/services/email.service";
|
import { storeEmail } from "~/server/services/email.service";
|
||||||
|
|
||||||
|
export {
|
||||||
|
type MailsTemplates,
|
||||||
|
TemplateKeys,
|
||||||
|
TemplateSchemas,
|
||||||
|
Templates,
|
||||||
|
type TemplateType,
|
||||||
|
} from "~/server/services/mail-templates";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type MailsTemplates,
|
||||||
|
Templates,
|
||||||
|
} from "~/server/services/mail-templates";
|
||||||
|
|
||||||
const mailer = async (options: Options) => {
|
const mailer = async (options: Options) => {
|
||||||
const smtpTransport = createTransport({
|
const smtpTransport = createTransport({
|
||||||
auth: {
|
auth: {
|
||||||
|
|
@ -81,117 +74,74 @@ export type NewMailProps = {
|
||||||
userId?: UsersId;
|
userId?: UsersId;
|
||||||
mail: Options;
|
mail: Options;
|
||||||
};
|
};
|
||||||
export type MailsTemplates =
|
|
||||||
| ContattoAdmin_NewMail
|
|
||||||
| ContattoAnnuncio_NewMail
|
|
||||||
| ContattoInteressato_NewMail
|
|
||||||
| PagamentoConferma_NewMail
|
|
||||||
| PagamentoErrore_NewMail
|
|
||||||
| PagamentoRicezione_NewMail
|
|
||||||
| PwResetLink_NewMail
|
|
||||||
| Recesso_NewMail
|
|
||||||
| RegistrazioneAvvenuta_NewMail
|
|
||||||
| VerificaEmail_NewMail
|
|
||||||
| Generic_NewMail
|
|
||||||
| SchedaContatto_NewMail
|
|
||||||
| Invito_NewMail
|
|
||||||
| ServizioAttivato_NewMail
|
|
||||||
| AvvisoInterruzione_NewMail
|
|
||||||
| Interruzione_NewMail;
|
|
||||||
|
|
||||||
export const genMail = async ({ ...mail }: MailsTemplates) => {
|
export const genMail = async ({ ...mail }: MailsTemplates) => {
|
||||||
let mailComponent: JSX.Element | null = null;
|
let mailComponent: JSX.Element | null = null;
|
||||||
let title = "";
|
const title = Templates[mail.mailType].title;
|
||||||
|
|
||||||
switch (mail.mailType) {
|
switch (mail.mailType) {
|
||||||
case "contattoAdminEmail":
|
case "contattoAdminEmail":
|
||||||
mailComponent = ContattoAdminEmail({
|
mailComponent = ContattoAdminEmail({
|
||||||
...mail.props,
|
...mail.props,
|
||||||
});
|
});
|
||||||
title = "Contatto amministratore";
|
|
||||||
break;
|
break;
|
||||||
case "contattoAnnuncioEmail":
|
case "contattoAnnuncioEmail":
|
||||||
mailComponent = ContattoAnnuncioEmail({
|
mailComponent = ContattoAnnuncioEmail({
|
||||||
...mail.props,
|
...mail.props,
|
||||||
});
|
});
|
||||||
title = "Contatto annuncio";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "emailContattoInteressato":
|
case "emailContattoInteressato":
|
||||||
mailComponent = EmailContattoInteressato({
|
mailComponent = EmailContattoInteressato({
|
||||||
...mail.props,
|
...mail.props,
|
||||||
});
|
});
|
||||||
title = "Contatto interessato";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "pagamentoConferma":
|
case "pagamentoConferma":
|
||||||
mailComponent = PagamentoConferma();
|
mailComponent = PagamentoConferma();
|
||||||
title = "Conferma pagamento";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "pagamentoErrore":
|
case "pagamentoErrore":
|
||||||
mailComponent = PagamentoErrore();
|
mailComponent = PagamentoErrore();
|
||||||
title = "Errore pagamento";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "pagamentoRicezione":
|
case "pagamentoRicezione":
|
||||||
mailComponent = PagamentoRicezione();
|
mailComponent = PagamentoRicezione();
|
||||||
title = "Ricezione pagamento";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "pwResetLink":
|
case "pwResetLink":
|
||||||
mailComponent = PwResetLink({
|
mailComponent = PwResetLink({
|
||||||
...mail.props,
|
...mail.props,
|
||||||
});
|
});
|
||||||
title = "Reset password";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "recesso":
|
case "recesso":
|
||||||
mailComponent = Recesso();
|
mailComponent = Recesso();
|
||||||
title = "Recesso";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "registrazioneAvvenuta":
|
case "registrazioneAvvenuta":
|
||||||
mailComponent = RegistrazioneAvvenuta();
|
mailComponent = RegistrazioneAvvenuta();
|
||||||
title = "Registrazione avvenuta";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "verificaEmail":
|
case "verificaEmail":
|
||||||
mailComponent = VerificaEmail({
|
mailComponent = VerificaEmail({
|
||||||
...mail.props,
|
...mail.props,
|
||||||
});
|
});
|
||||||
title = "Verifica email";
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "generic":
|
case "generic":
|
||||||
mailComponent = Generic({
|
mailComponent = Generic({
|
||||||
...mail.props,
|
...mail.props,
|
||||||
});
|
});
|
||||||
title = mail.props.title || "Email Generica";
|
|
||||||
break;
|
break;
|
||||||
case "emailSchedaContatto":
|
case "emailSchedaContatto":
|
||||||
mailComponent = EmailSchedaContatto({
|
mailComponent = EmailSchedaContatto({
|
||||||
...mail.props,
|
...mail.props,
|
||||||
});
|
});
|
||||||
title = "Scheda Contatto Annuncio";
|
|
||||||
break;
|
break;
|
||||||
case "invito":
|
case "invito":
|
||||||
mailComponent = Invito({
|
mailComponent = Invito({
|
||||||
...mail.props,
|
...mail.props,
|
||||||
});
|
});
|
||||||
title = "Invito al sito";
|
|
||||||
break;
|
break;
|
||||||
case "servizioAttivato":
|
case "servizioAttivato":
|
||||||
mailComponent = ServizioAttivato();
|
mailComponent = ServizioAttivato();
|
||||||
title = "Servizio Attivato";
|
|
||||||
break;
|
break;
|
||||||
case "avvisoInterruzione":
|
case "avvisoInterruzione":
|
||||||
mailComponent = AvvisoInterruzione();
|
mailComponent = AvvisoInterruzione();
|
||||||
title = "Avviso Interruzione";
|
|
||||||
break;
|
break;
|
||||||
case "interruzione":
|
case "interruzione":
|
||||||
mailComponent = Interruzione();
|
mailComponent = Interruzione();
|
||||||
title = "Interruzione servizio";
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.error(`Tipo di email non valido: ${mail satisfies never}`);
|
console.error(`Tipo di email non valido: ${mail satisfies never}`);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { BanlistId } from "~/schemas/public/Banlist";
|
||||||
import BanType from "~/schemas/public/BanType";
|
import BanType from "~/schemas/public/BanType";
|
||||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||||
import type { EtichetteIdEtichetta } from "~/schemas/public/Etichette";
|
import type { EtichetteIdEtichetta } from "~/schemas/public/Etichette";
|
||||||
|
import type { EventQueueEventId } from "~/schemas/public/EventQueue";
|
||||||
import type { FlagsId } from "~/schemas/public/Flags";
|
import type { FlagsId } from "~/schemas/public/Flags";
|
||||||
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
||||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||||
|
|
@ -91,3 +92,8 @@ export const zRinnovoId = z.custom<RinnoviId>(
|
||||||
(val) => typeof val === "string",
|
(val) => typeof val === "string",
|
||||||
"falied to validate RinnovoId",
|
"falied to validate RinnovoId",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const zEventId = z.custom<EventQueueEventId>(
|
||||||
|
(val) => typeof val === "number",
|
||||||
|
"falied to validate EventId",
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue