feat: implement email sending for contact details with PDF attachment

This commit is contained in:
Marco Pedone 2025-11-20 11:59:24 +01:00
parent e5c0ca04ee
commit 3ea9354abb
8 changed files with 214 additions and 33 deletions

View file

@ -41,7 +41,7 @@ const Base = ({
<Preview>{preview}</Preview> <Preview>{preview}</Preview>
<Body className="py-2 text-center font-sans text-base text-neutral-700"> <Body className="py-2 text-center font-sans text-base text-neutral-700">
<Container className="max-w-xl rounded-lg border border-[#eaeaea] border-solid pt-2 pb-10"> <Container className="max-w-2xl rounded-lg border border-[#eaeaea] border-solid pt-2 pb-10">
<Img <Img
alt="Infoalloggi.it" alt="Infoalloggi.it"
className="mx-auto my-2 scale-80" className="mx-auto my-2 scale-80"
@ -51,9 +51,9 @@ const Base = ({
width={230.5} width={230.5}
/> />
<Section className="max-w-lg px-8"> <Section className="max-w-xl px-4">
<Hr /> <Hr />
<Section className="max-w-md">{children}</Section> <Section className="max-w-lg">{children}</Section>
<Section className="mt-4"> <Section className="mt-4">
{noreply && ( {noreply && (

View file

@ -1,24 +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 Base from "./base"; import Base from "./base";
export type Generic_NewMail = { export type Generic_NewMail = {
mailType: "generic"; mailType: "generic";
props: GenericProps; props: GenericProps;
}; };
type Corpo = {
testo?: undefined;
corpo: ReactNode;
};
type Testo = {
testo: string;
corpo?: undefined;
};
type GenericProps = { type GenericProps = {
title: string; title: string;
noreply?: boolean; noreply?: boolean;
testo: string;
link?: { link?: {
href: string; href: string;
label?: string; label?: string;
}; };
}; } & (Corpo | Testo);
const Generic = ({ const Generic = ({
title, title,
noreply = false, noreply = false,
testo, testo,
corpo,
link = undefined, link = undefined,
}: GenericProps) => { }: GenericProps) => {
return ( return (
@ -26,9 +35,13 @@ const Generic = ({
<> <>
<Heading className="text-center text-3xl leading-8">{title}</Heading> <Heading className="text-center text-3xl leading-8">{title}</Heading>
<Section className="px-5 text-left text-base md:px-12"> <Section className="px-5 text-left text-base md:px-12">
{corpo ? (
corpo
) : (
<Row> <Row>
<Text>{testo}</Text> <Text>{testo}</Text>
</Row> </Row>
)}
{link && ( {link && (
<Section className="mb-5"> <Section className="mb-5">
<Button <Button

View file

@ -0,0 +1,79 @@
import { Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type SchedaContatto_NewMail = {
mailType: "emailSchedaContatto";
props: EmailSchedaContattoProps;
};
type EmailSchedaContattoProps = {
nominativo: string;
telefono: string;
indirizzo: string;
};
const EmailSchedaContatto = ({
nominativo,
telefono,
indirizzo,
}: EmailSchedaContattoProps) => {
return (
<Base noreply={true} preview="Dati Contatto Annuncio - Infoalloggi.it">
<>
<Heading className="text-center text-3xl leading-8">
Dati Contatto Annuncio
</Heading>
<Section className="px-5 text-left md:px-12">
<Row>
<Text className="text-base">Salve,</Text>
<Text className="text-base">
come da accordi le invio i dettagli del proprietario/referente.
</Text>
<Text className="text-base">
Scriva un messaggio WhatsApp ({" "}
<a href={`https://wa.me/${telefono}`} rel="noopener noreferrer">
Clicca per aprire WhatsApp
</a>{" "}
) avvisando di aver ricevuto da Infoalloggi il suo contatto e
chieda un appuntamento, poi ci aggiorniamo per l&apos;esito.
</Text>
<Text className="text-base">Nominativo: {nominativo}</Text>
<Text className="text-base">Tel: {telefono}</Text>
<Text className="text-base">Indirizzo immobile: {indirizzo}</Text>
<Text className="text-base">
In allegato trova la scheda completa dell&apos;annuncio
</Text>
<br />
<Text className="text-base">
Per ulteriori informazioni e chiarimenti non esiti a contattarci
all&apos;indirizzo{" "}
<a href="mailto:web@infoalloggi.it" rel="noopener noreferrer">
web@infoalloggi.it
</a>{" "}
oppure tramite tel. 04241760915.
</Text>
<Text className="text-base">Cordiali saluti</Text>
<br />
<Text className="text-xs">
{" "}
Le informazioni contenute in questo messaggio di posta elettronica
sono riservate confidenziali e ne è vietata la diffusione in
qualunque modo eseguita. Qualora Lei non fosse la persona a cui il
presente messaggio è destinato, La invitiamo gentilmente ad
eliminarlo dopo averne dato tempestiva comunicazione al mittente e
a non utilizzare in alcun caso il suo contenuto. Qualsiasi
utilizzo non autorizzato di questo messaggio e dei suoi eventuali
allegati espone il responsabile alle relative conseguenze civili e
penali.
</Text>
</Row>
</Section>
</>
</Base>
);
};
export default EmailSchedaContatto;

View file

@ -3,7 +3,9 @@
import html2canvas from "html2canvas-pro"; import html2canvas from "html2canvas-pro";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { useReactToPrint } from "react-to-print"; import { useReactToPrint } from "react-to-print";
import z from "zod";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card"; import { Card } from "~/components/ui/card";
import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils"; import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils";
@ -15,28 +17,63 @@ import type { Annunci } from "~/schemas/public/Annunci";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { IconMatrix } from "./IconComponents"; import { IconMatrix } from "./IconComponents";
import { GoogleMapsIcon, LogoSvg, WhatsAppIcon } from "./svgs"; import { GoogleMapsIcon, LogoSvg, WhatsAppIcon } from "./svgs";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) { export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
const [inputEmail, setInputEmail] = useState<string>("");
const [openPopover, setOpenPopover] = useState<boolean>(false);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const reactToPrintFn = useReactToPrint({ const reactToPrintFn = useReactToPrint({
contentRef, contentRef,
//print: async (target) => {console.log("Printing...", target.contentDocument);}, //print: async (target) => {console.log("Printing...", target.contentDocument);},
}); });
const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation(); const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation({
const reactToPrintFn2 = useReactToPrint({ onSuccess: () => {
toast.success("Email inviata con successo!", {
id: "send-scheda-annuncio-email",
});
},
onError: (error) => {
toast.error(`Errore durante l'invio dell'email: ${error.message}`, {
id: "send-scheda-annuncio-email",
});
},
});
const sendEmail = useReactToPrint({
contentRef, contentRef,
print: async (target) => { print: async (target) => {
try { try {
const htmlContent = target.contentDocument?.documentElement; const htmlContent = target.contentDocument?.documentElement;
if (!htmlContent) return; if (!htmlContent) {
toast.error("Impossibile generare il contenuto per la stampa.");
return;
}
if (!data || !inputEmail || !data.locatore || !data.numero) {
toast.error("Dati annuncio o indirizzo email mancanti.");
return;
}
toast.loading("Invio email in corso...", {
id: "send-scheda-annuncio-email",
});
setOpenPopover(false);
htmlContent.querySelectorAll(".print\\:border-none").forEach((el) => { htmlContent.querySelectorAll(".print\\:border-none").forEach((el) => {
el.classList.remove("border"); el.classList.remove("border");
}); });
htmlContent.querySelectorAll(".print\\:shadow-none").forEach((el) => { htmlContent.querySelectorAll(".print\\:shadow-none").forEach((el) => {
el.classList.remove("shadow-sm"); el.classList.remove("shadow-sm");
}); });
htmlContent.querySelectorAll("#navigationLink").forEach((el) => {
el.classList.remove("flex");
el.classList.add("hidden");
});
htmlContent.querySelectorAll("#mainLogoSvg text").forEach((el) => {
el.classList.remove("font-sans");
el.classList.add("font-[system-ui]");
});
const canvas = await html2canvas(htmlContent, { const canvas = await html2canvas(htmlContent, {
//scale: 2, //scale: 2,
useCORS: true, useCORS: true,
@ -44,7 +81,7 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
backgroundColor: "#ffffff", backgroundColor: "#ffffff",
}); });
const imgData = canvas.toDataURL("image/png", 0.85); const imgData = canvas.toDataURL("image/png");
const pdf = new jsPDF("p", "mm", "a4"); const pdf = new jsPDF("p", "mm", "a4");
// A4 dimensions in mm // A4 dimensions in mm
@ -119,19 +156,25 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
} }
} }
const pdfBase64 = pdf.output("dataurlstring"); const pdfBase64 = pdf.output("dataurlstring");
console.log(
"PDF size:",
((pdfBase64.length * 0.75) / 1024 / 1024).toFixed(2),
"MB",
);
mutate({ mutate({
pdf: pdfBase64, pdf: pdfBase64,
to: "m.pedone98@gmail.com", to: inputEmail,
codice: data.codice, codice: data.codice,
numero: data.numero,
nominativo: data.locatore,
indirizzo: `${data.indirizzo}, ${data.civico} ${data.comune} ${data.cap} (${data.provincia})`,
}); });
setInputEmail("");
} catch (e) { } catch (e) {
toast.error(
`Errore durante l'invio dell'email: ${(e as Error).message}`,
{
id: "send-scheda-annuncio-email",
},
);
console.error("Error during print:", e); console.error("Error during print:", e);
setInputEmail("");
} }
}, },
}); });
@ -141,13 +184,42 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="font-bold text-2xl">Scheda Annuncio Stampabile</h1> <h1 className="font-bold text-2xl">Scheda Annuncio Stampabile</h1>
<div className="flex gap-2">
<Button className="print:hidden" onClick={() => reactToPrintFn()}> <Button className="print:hidden" onClick={() => reactToPrintFn()}>
Stampa Stampa Scheda
</Button> </Button>
<Button className="print:hidden" onClick={() => reactToPrintFn2()}> <Popover onOpenChange={setOpenPopover} open={openPopover}>
Stampa <PopoverTrigger asChild>
<Button className="print:hidden" variant="outline">
Invia tramite email
</Button> </Button>
</PopoverTrigger>
<PopoverContent className="flex w-80 flex-col gap-4 print:hidden">
<Label htmlFor="indirizzo">Indirizzo Email</Label>
<Input
id="indirizzo"
onChange={(e) => setInputEmail(e.target.value)}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
z.safeParse(z.email(), inputEmail).success
) {
sendEmail();
}
}}
type="email"
/>
<Button
disabled={
z.safeParse(z.email(), inputEmail).success === false
}
onClick={sendEmail}
>
Invia
</Button>
</PopoverContent>
</Popover>
</div>
</div> </div>
<div ref={contentRef}> <div ref={contentRef}>
<SchedaAnnuncio data={data} /> <SchedaAnnuncio data={data} />
@ -239,6 +311,7 @@ export function SchedaAnnuncio({ data }: { data: Annunci }) {
<a <a
className="flex items-center gap-2 text-blue-600 underline underline-offset-2" className="flex items-center gap-2 text-blue-600 underline underline-offset-2"
href={navigationUrl} href={navigationUrl}
id="navigationLink"
rel="noreferrer" rel="noreferrer"
target="_blank" target="_blank"
> >

View file

@ -427,6 +427,7 @@ export const LogoSvg = (props: SVGProps<SVGSVGElement>) => (
width={258.758} width={258.758}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
id="mainLogoSvg"
> >
<title>{"Infolloggi.it "}</title> <title>{"Infolloggi.it "}</title>
<defs> <defs>

View file

@ -15,6 +15,7 @@ export type initialValues = {
id: FlagsId; id: FlagsId;
value: string; value: string;
}; };
//TODO salvare flags come boolean
type FormProps = { type FormProps = {
initialValues: initialValues | null; initialValues: initialValues | null;
submitMutation: (values: initialValues) => void; submitMutation: (values: initialValues) => void;

View file

@ -45,6 +45,9 @@ export const comunicazioniRouter = createTRPCRouter({
z.object({ z.object({
pdf: z.string(), pdf: z.string(),
codice: z.string(), codice: z.string(),
numero: z.string(),
nominativo: z.string(),
indirizzo: z.string(),
to: z.email(), to: z.email(),
}), }),
) )
@ -53,13 +56,14 @@ export const comunicazioniRouter = createTRPCRouter({
const base64Content = input.pdf.includes("base64,") const base64Content = input.pdf.includes("base64,")
? input.pdf.split("base64,")[1] ? input.pdf.split("base64,")[1]
: input.pdf; : input.pdf;
await NewMail({ await NewMail({
template: { template: {
mailType: "generic", mailType: "emailSchedaContatto",
props: { props: {
noreply: true, nominativo: input.nominativo,
testo: `In allegato la scheda annuncio stampabile per l'annuncio con codice ${input.codice}.`, telefono: input.numero,
title: "Scheda Annuncio Stampabile", indirizzo: input.indirizzo,
}, },
}, },
mail: { mail: {
@ -67,7 +71,7 @@ export const comunicazioniRouter = createTRPCRouter({
to: input.to, to: input.to,
attachments: [ attachments: [
{ {
filename: `scheda-annuncio-${input.codice}.pdf`, filename: `scheda annuncio ${input.codice}.pdf`,
content: base64Content, content: base64Content,
encoding: "base64", encoding: "base64",
contentType: "application/pdf", contentType: "application/pdf",

View file

@ -25,6 +25,9 @@ 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 type { RegistrazioneAvvenuta_NewMail } from "emails/registrazione-avvenuta";
import RegistrazioneAvvenuta from "emails/registrazione-avvenuta"; import RegistrazioneAvvenuta from "emails/registrazione-avvenuta";
import EmailSchedaContatto, {
type SchedaContatto_NewMail,
} from "emails/scheda-annuncio";
import VerificaEmail, { import VerificaEmail, {
type VerificaEmail_NewMail, type VerificaEmail_NewMail,
} from "emails/VerificaEmail"; } from "emails/VerificaEmail";
@ -85,7 +88,8 @@ export type MailsTemplates =
| RegistrazioneAvvenuta_NewMail | RegistrazioneAvvenuta_NewMail
| VerificaEmail_NewMail | VerificaEmail_NewMail
| OnboardingServizio_NewMail | OnboardingServizio_NewMail
| Generic_NewMail; | Generic_NewMail
| SchedaContatto_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;
@ -165,6 +169,12 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
}); });
title = mail.props.title || "Email Generica"; title = mail.props.title || "Email Generica";
break; break;
case "emailSchedaContatto":
mailComponent = EmailSchedaContatto({
...mail.props,
});
title = "Scheda Contatto Annuncio";
break;
} }
if (mailComponent === null) { if (mailComponent === null) {
throw new TRPCError({ throw new TRPCError({