feat: implement email functionality for contact details with attached PDF scheda
This commit is contained in:
parent
783dc137ea
commit
a36fd52af2
11 changed files with 363 additions and 149 deletions
80
apps/infoalloggi/emails/scheda-contatti.tsx
Normal file
80
apps/infoalloggi/emails/scheda-contatti.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||
import Base from "./base";
|
||||
export type ContattiConScheda_NewMail = {
|
||||
mailType: "emailContattiConScheda";
|
||||
props: EmailContattiConSchedaProps;
|
||||
};
|
||||
type EmailContattiConSchedaProps = {
|
||||
nominativo: string;
|
||||
telefono: string;
|
||||
indirizzo: string;
|
||||
codice: string;
|
||||
};
|
||||
|
||||
const EmailContattiConScheda = ({
|
||||
nominativo,
|
||||
telefono,
|
||||
indirizzo,
|
||||
codice,
|
||||
}: EmailContattiConSchedaProps) => {
|
||||
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">
|
||||
I contatti per l'annuncio {codice} sono stati sbloccati con
|
||||
successo.
|
||||
</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'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'annuncio
|
||||
</Text>
|
||||
<br />
|
||||
|
||||
<Text className="text-base">
|
||||
Per ulteriori informazioni e chiarimenti non esiti a contattarci
|
||||
all'indirizzo{" "}
|
||||
<a href="mailto:web@infoalloggi.it" rel="noopener noreferrer">
|
||||
web@infoalloggi.it
|
||||
</a>{" "}
|
||||
oppure tramite tel. 0424529869.
|
||||
</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 EmailContattiConScheda;
|
||||
|
|
@ -18,27 +18,16 @@ import { Input } from "./ui/input";
|
|||
import { Label } from "./ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
|
||||
const blobToBase64 = (blob: Blob): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = reject;
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
// Remove the Data-URL prefix (e.g. "data:application/pdf;base64,") to get just the base64
|
||||
resolve(reader.result.split(",")[1] || "");
|
||||
} else {
|
||||
reject(new Error("Failed to convert blob to text"));
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||
const [inputEmail, setInputEmail] = useState<string>("");
|
||||
const [openPopover, setOpenPopover] = useState<boolean>(false);
|
||||
|
||||
const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation({
|
||||
onMutate: () => {
|
||||
toast.loading("Invio email in corso...", {
|
||||
id: "send-scheda-annuncio-email",
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Email inviata con successo!", {
|
||||
id: "send-scheda-annuncio-email",
|
||||
|
|
@ -56,23 +45,9 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
|||
toast.error("Dati annuncio mancanti.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
toast.loading("Generazione PDF in corso...", {
|
||||
id: "generate-pdf",
|
||||
});
|
||||
const response = await fetch(`/api/generate-pdf?id=${data.id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Errore nella generazione del PDF: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
|
||||
const pdfBase64 = await blobToBase64(blob);
|
||||
|
||||
mutate({
|
||||
pdf: pdfBase64,
|
||||
id: data.id,
|
||||
to: inputEmail,
|
||||
codice: data.codice,
|
||||
numero: data.numero,
|
||||
|
|
@ -80,40 +55,22 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
|||
indirizzo: `${data.indirizzo}, ${data.civico} ${data.comune} ${data.cap} (${data.provincia})`,
|
||||
});
|
||||
setInputEmail("");
|
||||
|
||||
toast.success("PDF generato con successo!", {
|
||||
id: "generate-pdf",
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Errore durante la generazione del PDF: ${(error as Error).message}`,
|
||||
{
|
||||
id: "generate-pdf",
|
||||
},
|
||||
);
|
||||
console.error("Error generating PDF:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const generatePdf = async () => {
|
||||
if (!data) {
|
||||
toast.error("Dati annuncio mancanti.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { mutate: generatePdf } = api.annunci.getSchedaAnnuncio.useMutation({
|
||||
onMutate: () => {
|
||||
toast.loading("Generazione PDF in corso...", {
|
||||
id: "generate-pdf",
|
||||
});
|
||||
const response = await fetch(`/api/generate-pdf?id=${data.id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Errore nella generazione del PDF: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
toast.success("PDF generato con successo!", {
|
||||
id: "generate-pdf",
|
||||
});
|
||||
|
||||
const blob = new Blob([res as unknown as ArrayBuffer], {
|
||||
type: "application/pdf",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
|
|
@ -122,16 +79,13 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
|||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Errore durante la generazione del PDF: ${(error as Error).message}`,
|
||||
{
|
||||
id: "generate-pdf",
|
||||
},
|
||||
);
|
||||
console.error("Error generating PDF:", error);
|
||||
}
|
||||
};
|
||||
onError: (error) => {
|
||||
toast.error(`Errore durante la generazione del PDF: ${error.message}`, {
|
||||
id: "generate-pdf",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl">
|
||||
|
|
@ -139,7 +93,9 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
|||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 className="font-bold text-2xl">Scheda Annuncio Stampabile</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => generatePdf()}>Scarica Scheda</Button>
|
||||
<Button onClick={() => generatePdf({ id: data.id })}>
|
||||
Scarica Scheda
|
||||
</Button>
|
||||
|
||||
<Popover onOpenChange={setOpenPopover} open={openPopover}>
|
||||
<PopoverTrigger asChild>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
FileBadge,
|
||||
FileText,
|
||||
FolderKanban,
|
||||
Mail,
|
||||
PackageCheck,
|
||||
Pointer,
|
||||
Printer,
|
||||
|
|
@ -51,7 +52,8 @@ export const AdminActions = () => {
|
|||
if (!isAdmin) return null;
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-fit flex-wrap justify-start gap-3 sm:justify-end">
|
||||
<div className="flex w-full max-w-fit flex-wrap justify-start gap-3">
|
||||
{data.open_contatti_at != null && <AdminSendScheda />}
|
||||
{data.user_confirmed_at != null && <AdminLavoraConferma />}
|
||||
{data.accettato_conferma_at != null && (
|
||||
<>
|
||||
|
|
@ -122,6 +124,46 @@ export const AdminActions = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const AdminSendScheda = () => {
|
||||
const { userId } = useServizio();
|
||||
const { annuncioId } = useServizioAnnuncio();
|
||||
|
||||
const { mutate } = api.comunicazioni.sendSchedaAnnuncioServizio.useMutation({
|
||||
onMutate: () => {
|
||||
toast.loading("Invio email in corso...", {
|
||||
id: "send-scheda-annuncio-email",
|
||||
});
|
||||
},
|
||||
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",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
aria-label="Invia scheda annuncio via email"
|
||||
className="w-fit"
|
||||
onClick={() => {
|
||||
mutate({
|
||||
annuncioId,
|
||||
userId,
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<Mail />
|
||||
<span>Re-Invia email contatti</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserActions = () => {
|
||||
const { servizioId } = useServizio();
|
||||
const { data, annuncioId } = useServizioAnnuncio();
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { env } from "~/env";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
let browser = null;
|
||||
|
||||
try {
|
||||
browser = await puppeteer.connect({
|
||||
browserWSEndpoint: env.BROWSER_WS_URL,
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
if (req.cookies && Object.keys(req.cookies).length > 0) {
|
||||
const productionDomain = new URL(env.INTERNAL_BASE_URL).hostname;
|
||||
|
||||
const cookies = Object.entries(req.cookies).map(([name, value]) => ({
|
||||
name,
|
||||
value: value || "",
|
||||
domain:
|
||||
env.NODE_ENV === "production"
|
||||
? productionDomain
|
||||
: "host.docker.internal",
|
||||
path: "/",
|
||||
}));
|
||||
await browser.setCookie(...cookies);
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
env.NODE_ENV === "production"
|
||||
? env.INTERNAL_BASE_URL
|
||||
: `http://host.docker.internal:3000`;
|
||||
const targetUrl = `${baseUrl}/area-riservata/admin/scheda-annuncio-stampa/${req.query.id}?raw=true`;
|
||||
await page.goto(targetUrl, { waitUntil: "networkidle0" });
|
||||
|
||||
const pdf = await page.pdf({
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
});
|
||||
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.send(Buffer.from(pdf));
|
||||
} catch (error) {
|
||||
console.error("PDF Generation Error:", error);
|
||||
res.status(500).json({ error: "Failed to generate PDF" });
|
||||
} finally {
|
||||
// 'disconnect' tells browserless you're done with this tab
|
||||
if (browser) await browser.disconnect();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { add } from "date-fns";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "~/env";
|
||||
import type { ProxyFn } from "~/proxy";
|
||||
|
||||
import { TOKEN_CONFIG } from "~/server/auth/configs";
|
||||
|
|
@ -8,6 +9,15 @@ import { signToken, verifyToken } from "~/server/auth/jwt";
|
|||
export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||
const path = req.nextUrl.pathname;
|
||||
|
||||
// permette a puppeteer di accedere alla pagina di generazione PDF bypassando l'autenticazione normale
|
||||
if (path.startsWith("/area-riservata/admin/scheda-annuncio-stampa/")) {
|
||||
const overrideToken = req.cookies.get(
|
||||
TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME,
|
||||
)?.value;
|
||||
if (overrideToken && overrideToken === env.EXP_API_PASS) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const refreshToken = req.cookies.get(
|
||||
TOKEN_CONFIG.REFRESH_TOKEN_COOKIE_NAME,
|
||||
)?.value;
|
||||
|
|
@ -17,9 +27,7 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
|
|||
)?.value;
|
||||
|
||||
const isProtectedRoute =
|
||||
(path.startsWith("/area-riservata") ||
|
||||
path.startsWith("/servizio") ||
|
||||
path.startsWith("/api/generate-pdf")) &&
|
||||
(path.startsWith("/area-riservata") || path.startsWith("/servizio")) &&
|
||||
!path.startsWith("/servizio/pre-onboard");
|
||||
|
||||
if (isProtectedRoute) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
import type { AnnunciUpdate } from "~/schemas/public/Annunci";
|
||||
import {
|
||||
|
|
@ -25,7 +26,9 @@ import {
|
|||
} from "~/server/controllers/annunci.controller";
|
||||
import { db } from "~/server/db";
|
||||
import { NewMail } from "~/server/services/mailer";
|
||||
import { genPdfSchedaAnnuncio } from "~/server/services/puppeteer.service";
|
||||
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
|
||||
import { getKeydbClient } from "~/utils/keydb";
|
||||
|
||||
export const annunciRouter = createTRPCRouter({
|
||||
editAnnuncio: adminProcedure
|
||||
|
|
@ -33,6 +36,10 @@ export const annunciRouter = createTRPCRouter({
|
|||
z.object({ annuncioId: zAnnuncioId, data: z.custom<AnnunciUpdate>() }),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const keybd = getKeydbClient();
|
||||
if (keybd) {
|
||||
await keybd.del(`scheda-annuncio-${input.annuncioId}`);
|
||||
}
|
||||
return await editAnnuncioHandler({
|
||||
...input,
|
||||
});
|
||||
|
|
@ -152,4 +159,20 @@ export const annunciRouter = createTRPCRouter({
|
|||
...input,
|
||||
});
|
||||
}),
|
||||
getSchedaAnnuncio: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: zAnnuncioId,
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await genPdfSchedaAnnuncio(input.id);
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Errore nella generazione della scheda: ${(e as Error).message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
getEmails,
|
||||
} from "~/server/services/email.service";
|
||||
import { NewMail } from "~/server/services/mailer";
|
||||
import { genPdfSchedaAnnuncioBase64 } from "~/server/services/puppeteer.service";
|
||||
import { zAnnuncioId, zUserId } from "~/server/utils/zod_types";
|
||||
|
||||
export const comunicazioniRouter = createTRPCRouter({
|
||||
|
|
@ -54,7 +55,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
|||
sendSchedaAnnuncioEmail: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
pdf: z.string(),
|
||||
id: zAnnuncioId,
|
||||
codice: z.string(),
|
||||
numero: z.string(),
|
||||
nominativo: z.string(),
|
||||
|
|
@ -64,9 +65,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
|||
)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const base64Content = input.pdf.includes("base64,")
|
||||
? input.pdf.split("base64,")[1]
|
||||
: input.pdf;
|
||||
const scheda = await genPdfSchedaAnnuncioBase64(input.id);
|
||||
|
||||
await NewMail({
|
||||
template: {
|
||||
|
|
@ -83,7 +82,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
|||
attachments: [
|
||||
{
|
||||
filename: `scheda annuncio ${input.codice}.pdf`,
|
||||
content: base64Content,
|
||||
content: scheda,
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
|
|
@ -110,6 +109,64 @@ export const comunicazioniRouter = createTRPCRouter({
|
|||
);
|
||||
}
|
||||
}),
|
||||
sendSchedaAnnuncioServizio: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: zUserId,
|
||||
annuncioId: zAnnuncioId,
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const scheda = await genPdfSchedaAnnuncioBase64(input.annuncioId);
|
||||
const user = await db
|
||||
.selectFrom("users")
|
||||
.select("email")
|
||||
.where("id", "=", input.userId)
|
||||
.executeTakeFirstOrThrow();
|
||||
const annuncio = await db
|
||||
.selectFrom("annunci")
|
||||
.select([
|
||||
"codice",
|
||||
"locatore",
|
||||
"indirizzo",
|
||||
"civico",
|
||||
"comune",
|
||||
"cap",
|
||||
"provincia",
|
||||
"numero",
|
||||
])
|
||||
.where("id", "=", input.annuncioId)
|
||||
.executeTakeFirstOrThrow();
|
||||
await NewMail({
|
||||
template: {
|
||||
mailType: "emailContattiConScheda",
|
||||
props: {
|
||||
codice: annuncio.codice,
|
||||
nominativo: annuncio.locatore || "",
|
||||
telefono: annuncio.numero || "",
|
||||
indirizzo: `${annuncio.indirizzo || ""}, ${annuncio.civico || ""} ${annuncio.comune || ""} ${annuncio.cap || ""} (${annuncio.provincia || ""})`,
|
||||
},
|
||||
},
|
||||
mail: {
|
||||
subject: `Contatti Annuncio ${annuncio.codice} - Infoalloggi.it`,
|
||||
to: user.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: `scheda annuncio ${annuncio.codice}.pdf`,
|
||||
content: scheda,
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Errore nell'invio dell'email: ${(e as Error).message}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
sendAnnunciEmail: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export const TOKEN_CONFIG = {
|
||||
ACCESS_TOKEN_COOKIE_NAME: "access_token",
|
||||
REFRESH_TOKEN_COOKIE_NAME: "refresh_token",
|
||||
OVERRIDE_TOKEN_COOKIE_NAME: "override_token",
|
||||
ACCESS_TOKEN_EXPIRY: "1h",
|
||||
REFRESH_TOKEN_EXPIRY: "7d",
|
||||
ACCESS_TOKEN_COOKIE_EXPIRY: 1,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import {
|
|||
} from "~/utils/kysely-helper";
|
||||
import { GetUserInterests } from "../services/interests.service";
|
||||
import { getPrezziarioByIdHandler } from "../services/prezziario.service";
|
||||
import { genPdfSchedaAnnuncioBase64 } from "../services/puppeteer.service";
|
||||
import type { AnnuncioRicerca } from "./annunci.controller";
|
||||
|
||||
export const addServizio = async (data: NewServizio) => {
|
||||
|
|
@ -1308,20 +1309,25 @@ export const SbloccaContatti = async ({
|
|||
|
||||
const EmailInteressato: NewMailProps = {
|
||||
template: {
|
||||
mailType: "generic",
|
||||
mailType: "emailContattiConScheda",
|
||||
props: {
|
||||
link: {
|
||||
href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`,
|
||||
label: "Vai al servizio",
|
||||
},
|
||||
noreply: true,
|
||||
testo: `I contatti per l'annuncio ${data.annuncio.codice} sono stati sbloccati con successo.`,
|
||||
title: "Contatti sbloccati per il servizio di ricerca affitto",
|
||||
nominativo: data.annuncio.locatore || "",
|
||||
telefono: data.annuncio.numero || "",
|
||||
indirizzo: data.annuncio.indirizzo || "",
|
||||
codice: data.annuncio.codice,
|
||||
},
|
||||
},
|
||||
mail: {
|
||||
subject: "Contatti sbloccati per il servizio di ricerca affitto",
|
||||
subject: `Contatti Annuncio ${data.annuncio.codice} - InfoAlloggi`,
|
||||
to: data.user.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: `scheda annuncio ${data.annuncio.codice}.pdf`,
|
||||
content: await genPdfSchedaAnnuncioBase64(data.annuncioId),
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
userId: data.user.id,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import RegistrazioneAvvenuta from "emails/registrazione-avvenuta";
|
|||
import EmailSchedaContatto, {
|
||||
type SchedaContatto_NewMail,
|
||||
} from "emails/scheda-annuncio";
|
||||
import type { ContattiConScheda_NewMail } from "emails/scheda-contatti";
|
||||
import EmailContattiConScheda from "emails/scheda-contatti";
|
||||
import ShareAnnunci, { type ShareAnnunci_NewMail } from "emails/share-annunci";
|
||||
import VerificaEmail, {
|
||||
type VerificaEmail_NewMail,
|
||||
|
|
@ -90,7 +92,8 @@ export type MailsTemplates =
|
|||
| ShareAnnunci_NewMail
|
||||
| Generic_NewMail
|
||||
| SchedaContatto_NewMail
|
||||
| Invito_NewMail;
|
||||
| Invito_NewMail
|
||||
| ContattiConScheda_NewMail;
|
||||
|
||||
export const genMail = async ({ ...mail }: MailsTemplates) => {
|
||||
let mailComponent: JSX.Element | null = null;
|
||||
|
|
@ -182,6 +185,12 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
|
|||
});
|
||||
title = "Invito al sito";
|
||||
break;
|
||||
case "emailContattiConScheda":
|
||||
mailComponent = EmailContattiConScheda({
|
||||
...mail.props,
|
||||
});
|
||||
title = "Contatti Con Scheda";
|
||||
break;
|
||||
default:
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
|
|
|
|||
86
apps/infoalloggi/src/server/services/puppeteer.service.ts
Normal file
86
apps/infoalloggi/src/server/services/puppeteer.service.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { env } from "~/env";
|
||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||
import { getKeydbClient } from "~/utils/keydb";
|
||||
import { TOKEN_CONFIG } from "../auth/configs";
|
||||
|
||||
export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => {
|
||||
const pdf = await genPdfSchedaAnnuncio(annuncioId);
|
||||
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
||||
const base64Content = pdfBase64.includes("base64,")
|
||||
? pdfBase64.split("base64,")[1]
|
||||
: pdfBase64;
|
||||
if (!base64Content) {
|
||||
throw new Error("Failed to convert PDF to base64");
|
||||
}
|
||||
return base64Content;
|
||||
};
|
||||
|
||||
export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
||||
const keybd = getKeydbClient();
|
||||
if (keybd) {
|
||||
const cacheKey = `scheda-annuncio-${annuncioId}`;
|
||||
const cached = await keybd.get(cacheKey);
|
||||
if (cached) {
|
||||
return new Uint8Array(Buffer.from(cached, "base64"));
|
||||
}
|
||||
const scheda = await puppeteerSchedaAnnuncio(annuncioId);
|
||||
await keybd.set(
|
||||
cacheKey,
|
||||
Buffer.from(scheda).toString("base64"),
|
||||
"EX",
|
||||
3600 * 5,
|
||||
);
|
||||
return scheda;
|
||||
}
|
||||
return await puppeteerSchedaAnnuncio(annuncioId);
|
||||
};
|
||||
|
||||
const puppeteerSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
||||
let browser = null;
|
||||
|
||||
try {
|
||||
browser = await puppeteer.connect({
|
||||
browserWSEndpoint: env.BROWSER_WS_URL,
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
const productionDomain = new URL(env.INTERNAL_BASE_URL).hostname;
|
||||
const newCookies = [
|
||||
{
|
||||
name: TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME,
|
||||
value: env.EXP_API_PASS,
|
||||
domain:
|
||||
env.NODE_ENV === "production"
|
||||
? productionDomain
|
||||
: "host.docker.internal",
|
||||
path: "/",
|
||||
},
|
||||
];
|
||||
await browser.setCookie(...newCookies);
|
||||
|
||||
const baseUrl =
|
||||
env.NODE_ENV === "production"
|
||||
? env.INTERNAL_BASE_URL
|
||||
: `http://host.docker.internal:3000`;
|
||||
|
||||
const targetUrl = `${baseUrl}/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`;
|
||||
await page.goto(targetUrl, { waitUntil: "networkidle0" });
|
||||
|
||||
const pdf = await page.pdf({
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
});
|
||||
return pdf;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Errore nella generazione del PDF: ${(error as Error).message}`,
|
||||
});
|
||||
} finally {
|
||||
// 'disconnect' tells browserless you're done with this tab
|
||||
if (browser) await browser.disconnect();
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue