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 { Label } from "./ui/label";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
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 }) {
|
export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
const [inputEmail, setInputEmail] = useState<string>("");
|
const [inputEmail, setInputEmail] = useState<string>("");
|
||||||
const [openPopover, setOpenPopover] = useState<boolean>(false);
|
const [openPopover, setOpenPopover] = useState<boolean>(false);
|
||||||
|
|
||||||
const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation({
|
const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation({
|
||||||
|
onMutate: () => {
|
||||||
|
toast.loading("Invio email in corso...", {
|
||||||
|
id: "send-scheda-annuncio-email",
|
||||||
|
});
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Email inviata con successo!", {
|
toast.success("Email inviata con successo!", {
|
||||||
id: "send-scheda-annuncio-email",
|
id: "send-scheda-annuncio-email",
|
||||||
|
|
@ -56,64 +45,32 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
toast.error("Dati annuncio mancanti.");
|
toast.error("Dati annuncio mancanti.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
toast.loading("Generazione PDF in corso...", {
|
|
||||||
id: "generate-pdf",
|
|
||||||
});
|
|
||||||
const response = await fetch(`/api/generate-pdf?id=${data.id}`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
mutate({
|
||||||
throw new Error(
|
id: data.id,
|
||||||
`Errore nella generazione del PDF: ${response.statusText}`,
|
to: inputEmail,
|
||||||
);
|
codice: data.codice,
|
||||||
}
|
numero: data.numero,
|
||||||
const blob = await response.blob();
|
nominativo: data.locatore,
|
||||||
|
indirizzo: `${data.indirizzo}, ${data.civico} ${data.comune} ${data.cap} (${data.provincia})`,
|
||||||
const pdfBase64 = await blobToBase64(blob);
|
});
|
||||||
|
setInputEmail("");
|
||||||
mutate({
|
|
||||||
pdf: pdfBase64,
|
|
||||||
to: inputEmail,
|
|
||||||
codice: data.codice,
|
|
||||||
numero: data.numero,
|
|
||||||
nominativo: data.locatore,
|
|
||||||
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 () => {
|
const { mutate: generatePdf } = api.annunci.getSchedaAnnuncio.useMutation({
|
||||||
if (!data) {
|
onMutate: () => {
|
||||||
toast.error("Dati annuncio mancanti.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
toast.loading("Generazione PDF in corso...", {
|
toast.loading("Generazione PDF in corso...", {
|
||||||
id: "generate-pdf",
|
id: "generate-pdf",
|
||||||
});
|
});
|
||||||
const response = await fetch(`/api/generate-pdf?id=${data.id}`);
|
},
|
||||||
if (!response.ok) {
|
onSuccess: (res) => {
|
||||||
throw new Error(
|
|
||||||
`Errore nella generazione del PDF: ${response.statusText}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const blob = await response.blob();
|
|
||||||
toast.success("PDF generato con successo!", {
|
toast.success("PDF generato con successo!", {
|
||||||
id: "generate-pdf",
|
id: "generate-pdf",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([res as unknown as ArrayBuffer], {
|
||||||
|
type: "application/pdf",
|
||||||
|
});
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
|
|
@ -122,16 +79,13 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
link.click();
|
link.click();
|
||||||
link.remove();
|
link.remove();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
} catch (error) {
|
},
|
||||||
toast.error(
|
onError: (error) => {
|
||||||
`Errore durante la generazione del PDF: ${(error as Error).message}`,
|
toast.error(`Errore durante la generazione del PDF: ${error.message}`, {
|
||||||
{
|
id: "generate-pdf",
|
||||||
id: "generate-pdf",
|
});
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
console.error("Error generating PDF:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-4xl">
|
<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">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<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">
|
<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}>
|
<Popover onOpenChange={setOpenPopover} open={openPopover}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
FileBadge,
|
FileBadge,
|
||||||
FileText,
|
FileText,
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
|
Mail,
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
Pointer,
|
Pointer,
|
||||||
Printer,
|
Printer,
|
||||||
|
|
@ -51,7 +52,8 @@ export const AdminActions = () => {
|
||||||
if (!isAdmin) return null;
|
if (!isAdmin) return null;
|
||||||
|
|
||||||
return (
|
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.user_confirmed_at != null && <AdminLavoraConferma />}
|
||||||
{data.accettato_conferma_at != null && (
|
{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 = () => {
|
export const UserActions = () => {
|
||||||
const { servizioId } = useServizio();
|
const { servizioId } = useServizio();
|
||||||
const { data, annuncioId } = useServizioAnnuncio();
|
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 { add } from "date-fns";
|
||||||
import { type NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
|
import { env } from "~/env";
|
||||||
import type { ProxyFn } from "~/proxy";
|
import type { ProxyFn } from "~/proxy";
|
||||||
|
|
||||||
import { TOKEN_CONFIG } from "~/server/auth/configs";
|
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) => {
|
export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
const path = req.nextUrl.pathname;
|
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(
|
const refreshToken = req.cookies.get(
|
||||||
TOKEN_CONFIG.REFRESH_TOKEN_COOKIE_NAME,
|
TOKEN_CONFIG.REFRESH_TOKEN_COOKIE_NAME,
|
||||||
)?.value;
|
)?.value;
|
||||||
|
|
@ -17,9 +27,7 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
)?.value;
|
)?.value;
|
||||||
|
|
||||||
const isProtectedRoute =
|
const isProtectedRoute =
|
||||||
(path.startsWith("/area-riservata") ||
|
(path.startsWith("/area-riservata") || path.startsWith("/servizio")) &&
|
||||||
path.startsWith("/servizio") ||
|
|
||||||
path.startsWith("/api/generate-pdf")) &&
|
|
||||||
!path.startsWith("/servizio/pre-onboard");
|
!path.startsWith("/servizio/pre-onboard");
|
||||||
|
|
||||||
if (isProtectedRoute) {
|
if (isProtectedRoute) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import type { AnnunciUpdate } from "~/schemas/public/Annunci";
|
import type { AnnunciUpdate } from "~/schemas/public/Annunci";
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,7 +26,9 @@ import {
|
||||||
} from "~/server/controllers/annunci.controller";
|
} from "~/server/controllers/annunci.controller";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { NewMail } from "~/server/services/mailer";
|
import { NewMail } from "~/server/services/mailer";
|
||||||
|
import { genPdfSchedaAnnuncio } from "~/server/services/puppeteer.service";
|
||||||
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
|
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
|
||||||
|
import { getKeydbClient } from "~/utils/keydb";
|
||||||
|
|
||||||
export const annunciRouter = createTRPCRouter({
|
export const annunciRouter = createTRPCRouter({
|
||||||
editAnnuncio: adminProcedure
|
editAnnuncio: adminProcedure
|
||||||
|
|
@ -33,6 +36,10 @@ export const annunciRouter = createTRPCRouter({
|
||||||
z.object({ annuncioId: zAnnuncioId, data: z.custom<AnnunciUpdate>() }),
|
z.object({ annuncioId: zAnnuncioId, data: z.custom<AnnunciUpdate>() }),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
const keybd = getKeydbClient();
|
||||||
|
if (keybd) {
|
||||||
|
await keybd.del(`scheda-annuncio-${input.annuncioId}`);
|
||||||
|
}
|
||||||
return await editAnnuncioHandler({
|
return await editAnnuncioHandler({
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|
@ -152,4 +159,20 @@ export const annunciRouter = createTRPCRouter({
|
||||||
...input,
|
...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,
|
getEmails,
|
||||||
} from "~/server/services/email.service";
|
} from "~/server/services/email.service";
|
||||||
import { NewMail } from "~/server/services/mailer";
|
import { NewMail } from "~/server/services/mailer";
|
||||||
|
import { genPdfSchedaAnnuncioBase64 } from "~/server/services/puppeteer.service";
|
||||||
import { zAnnuncioId, zUserId } from "~/server/utils/zod_types";
|
import { zAnnuncioId, zUserId } from "~/server/utils/zod_types";
|
||||||
|
|
||||||
export const comunicazioniRouter = createTRPCRouter({
|
export const comunicazioniRouter = createTRPCRouter({
|
||||||
|
|
@ -54,7 +55,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
||||||
sendSchedaAnnuncioEmail: adminProcedure
|
sendSchedaAnnuncioEmail: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
pdf: z.string(),
|
id: zAnnuncioId,
|
||||||
codice: z.string(),
|
codice: z.string(),
|
||||||
numero: z.string(),
|
numero: z.string(),
|
||||||
nominativo: z.string(),
|
nominativo: z.string(),
|
||||||
|
|
@ -64,9 +65,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const base64Content = input.pdf.includes("base64,")
|
const scheda = await genPdfSchedaAnnuncioBase64(input.id);
|
||||||
? input.pdf.split("base64,")[1]
|
|
||||||
: input.pdf;
|
|
||||||
|
|
||||||
await NewMail({
|
await NewMail({
|
||||||
template: {
|
template: {
|
||||||
|
|
@ -83,7 +82,7 @@ export const comunicazioniRouter = createTRPCRouter({
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
filename: `scheda annuncio ${input.codice}.pdf`,
|
filename: `scheda annuncio ${input.codice}.pdf`,
|
||||||
content: base64Content,
|
content: scheda,
|
||||||
encoding: "base64",
|
encoding: "base64",
|
||||||
contentType: "application/pdf",
|
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
|
sendAnnunciEmail: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export const TOKEN_CONFIG = {
|
export const TOKEN_CONFIG = {
|
||||||
ACCESS_TOKEN_COOKIE_NAME: "access_token",
|
ACCESS_TOKEN_COOKIE_NAME: "access_token",
|
||||||
REFRESH_TOKEN_COOKIE_NAME: "refresh_token",
|
REFRESH_TOKEN_COOKIE_NAME: "refresh_token",
|
||||||
|
OVERRIDE_TOKEN_COOKIE_NAME: "override_token",
|
||||||
ACCESS_TOKEN_EXPIRY: "1h",
|
ACCESS_TOKEN_EXPIRY: "1h",
|
||||||
REFRESH_TOKEN_EXPIRY: "7d",
|
REFRESH_TOKEN_EXPIRY: "7d",
|
||||||
ACCESS_TOKEN_COOKIE_EXPIRY: 1,
|
ACCESS_TOKEN_COOKIE_EXPIRY: 1,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import {
|
||||||
} from "~/utils/kysely-helper";
|
} from "~/utils/kysely-helper";
|
||||||
import { GetUserInterests } from "../services/interests.service";
|
import { GetUserInterests } from "../services/interests.service";
|
||||||
import { getPrezziarioByIdHandler } from "../services/prezziario.service";
|
import { getPrezziarioByIdHandler } from "../services/prezziario.service";
|
||||||
|
import { genPdfSchedaAnnuncioBase64 } from "../services/puppeteer.service";
|
||||||
import type { AnnuncioRicerca } from "./annunci.controller";
|
import type { AnnuncioRicerca } from "./annunci.controller";
|
||||||
|
|
||||||
export const addServizio = async (data: NewServizio) => {
|
export const addServizio = async (data: NewServizio) => {
|
||||||
|
|
@ -1308,20 +1309,25 @@ export const SbloccaContatti = async ({
|
||||||
|
|
||||||
const EmailInteressato: NewMailProps = {
|
const EmailInteressato: NewMailProps = {
|
||||||
template: {
|
template: {
|
||||||
mailType: "generic",
|
mailType: "emailContattiConScheda",
|
||||||
props: {
|
props: {
|
||||||
link: {
|
nominativo: data.annuncio.locatore || "",
|
||||||
href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`,
|
telefono: data.annuncio.numero || "",
|
||||||
label: "Vai al servizio",
|
indirizzo: data.annuncio.indirizzo || "",
|
||||||
},
|
codice: data.annuncio.codice,
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mail: {
|
mail: {
|
||||||
subject: "Contatti sbloccati per il servizio di ricerca affitto",
|
subject: `Contatti Annuncio ${data.annuncio.codice} - InfoAlloggi`,
|
||||||
to: data.user.email,
|
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,
|
userId: data.user.id,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import RegistrazioneAvvenuta from "emails/registrazione-avvenuta";
|
||||||
import EmailSchedaContatto, {
|
import EmailSchedaContatto, {
|
||||||
type SchedaContatto_NewMail,
|
type SchedaContatto_NewMail,
|
||||||
} from "emails/scheda-annuncio";
|
} 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 ShareAnnunci, { type ShareAnnunci_NewMail } from "emails/share-annunci";
|
||||||
import VerificaEmail, {
|
import VerificaEmail, {
|
||||||
type VerificaEmail_NewMail,
|
type VerificaEmail_NewMail,
|
||||||
|
|
@ -90,7 +92,8 @@ export type MailsTemplates =
|
||||||
| ShareAnnunci_NewMail
|
| ShareAnnunci_NewMail
|
||||||
| Generic_NewMail
|
| Generic_NewMail
|
||||||
| SchedaContatto_NewMail
|
| SchedaContatto_NewMail
|
||||||
| Invito_NewMail;
|
| Invito_NewMail
|
||||||
|
| ContattiConScheda_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;
|
||||||
|
|
@ -182,6 +185,12 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
|
||||||
});
|
});
|
||||||
title = "Invito al sito";
|
title = "Invito al sito";
|
||||||
break;
|
break;
|
||||||
|
case "emailContattiConScheda":
|
||||||
|
mailComponent = EmailContattiConScheda({
|
||||||
|
...mail.props,
|
||||||
|
});
|
||||||
|
title = "Contatti Con Scheda";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
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