feat: implement email sharing functionality for selected listings

This commit is contained in:
Marco Pedone 2026-01-27 16:30:19 +01:00
parent 07132e8cd6
commit 95315e6605
6 changed files with 177 additions and 20 deletions

View file

@ -9,13 +9,12 @@ import {
} from "@react-email/components"; } from "@react-email/components";
import { env } from "~/env"; import { env } from "~/env";
import Base from "./base"; import Base from "./base";
export type OnboardingServizio_NewMail = { export type ShareAnnunci_NewMail = {
mailType: "onboardingServizio"; mailType: "shareAnnunci";
props: OnboardingServizioProps; props: ShareAnnunciProps;
}; };
type OnboardingServizioProps = { type ShareAnnunciProps = {
token: string;
nome: string; nome: string;
annunci?: { annunci?: {
titolo: string | null; titolo: string | null;
@ -25,11 +24,7 @@ type OnboardingServizioProps = {
}[]; }[];
}; };
const OnboardingServizio = ({ const ShareAnnunci = ({ nome, annunci }: ShareAnnunciProps) => {
token,
nome,
annunci,
}: OnboardingServizioProps) => {
return ( return (
<Base preview="Procedi ora su Infoalloggi.it"> <Base preview="Procedi ora su Infoalloggi.it">
<> <>
@ -48,9 +43,9 @@ const OnboardingServizio = ({
<Section className="mb-5"> <Section className="mb-5">
<Button <Button
className="mx-auto box-border inline-flex h-10 items-center justify-center whitespace-nowrap rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold text-neutral-900" className="mx-auto box-border inline-flex h-10 items-center justify-center whitespace-nowrap rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold text-neutral-900"
href={`${env.BASE_URL}/servizio/pre-onboard/${token}`} href={`${env.BASE_URL}`}
> >
Vai al servizio Vai al Sito
</Button> </Button>
</Section> </Section>
<Section> <Section>
@ -100,4 +95,4 @@ const OnboardingServizio = ({
); );
}; };
export default OnboardingServizio; export default ShareAnnunci;

View file

@ -1,6 +1,14 @@
import Head from "next/head"; import Head from "next/head";
import { useState } from "react";
import toast from "react-hot-toast";
import Input from "~/components/custom_ui/input";
import { MultiSelect } from "~/components/custom_ui/multiselect";
import { AreaRiservataLayout } from "~/components/Layout"; import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button";
import { Label } from "~/components/ui/label";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import type { AnnunciId } from "~/schemas/public/Annunci";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
const Impostazioni: NextPageWithLayout = () => { const Impostazioni: NextPageWithLayout = () => {
@ -17,6 +25,7 @@ const Impostazioni: NextPageWithLayout = () => {
</div> </div>
</div> </div>
<StripeSection /> <StripeSection />
<SendEmailSection />
</div> </div>
</> </>
); );
@ -59,3 +68,91 @@ const StripeSection = () => {
</div> </div>
); );
}; };
const SendEmailSection = () => {
const { data: codici, isLoading: loading } =
api.annunci.getOnline_AnnuncioIdCodice.useQuery();
const [selectedAnnunci, setSelectedAnnunci] = useState<
{ value: AnnunciId; label: string }[]
>([]);
const [email, setEmail] = useState<string | undefined>(undefined);
const { mutate } = api.comunicazioni.sendAnnunciEmail.useMutation({
onSuccess: () => {
setSelectedAnnunci([]);
setEmail("");
toast.success("Email inviata con successo");
},
onError: (error) => {
console.error(error);
toast.error(`Errore durante l'invio dell'email: ${error.message}`);
},
});
return (
<div className="space-y-4 rounded-lg border p-4">
<h4 className="font-semibold text-lg">
Invia email con annunci selezionati
</h4>
<div className="flex max-w-xl flex-col gap-2">
<Label className="font-medium" htmlFor="email-to">
Indirizzo email destinatario
</Label>
<Input
id="email-to"
onChange={(e) => setEmail(e.target.value)}
placeholder="Inserisci l'indirizzo email"
type="email"
value={email || ""}
/>
</div>
<div className="flex max-w-xl flex-col gap-2">
{loading ? (
<LoadingPage />
) : (
<div className="flex max-w-xl flex-col">
<Label className="font-medium" htmlFor="select-annuncio">
Seleziona gli annunci da inviare
</Label>
<MultiSelect
defaultValue={undefined}
elementId="select-annuncio"
elementName="annuncio"
isMulti={true}
onValueChange={(value) => {
setSelectedAnnunci(
value.map((v) => ({
label: v.label,
value: parseInt(v.value) as AnnunciId,
})),
);
}}
options={
codici?.map((cod) => ({
label: cod.codice,
value: cod.id.toString(),
})) || []
}
placeholder=""
/>
</div>
)}
</div>
<Button
aria-label="Cancel Add Annuncio"
disabled={!selectedAnnunci.length || !email}
onClick={() => {
if (!selectedAnnunci.length || !email) return;
mutate({
annunci: selectedAnnunci.map((annuncio) => annuncio.value),
to: email,
});
}}
type="button"
>
Invia
</Button>
</div>
);
};

View file

@ -16,6 +16,7 @@ import {
getAnnunciMetaByCod, getAnnunciMetaByCod,
getAnnunciRicerca, getAnnunciRicerca,
getCodici_AnnunciHandler, getCodici_AnnunciHandler,
getOnline_AnnuncioIdCodice_AnnunciHandler,
getOptions_AnnunciHandler, getOptions_AnnunciHandler,
getProprietarioDataHandler, getProprietarioDataHandler,
} from "~/server/controllers/annunci.controller"; } from "~/server/controllers/annunci.controller";
@ -39,6 +40,9 @@ export const annunciRouter = createTRPCRouter({
getAnnunciOptions: publicProcedure.query(async () => { getAnnunciOptions: publicProcedure.query(async () => {
return await getOptions_AnnunciHandler(); return await getOptions_AnnunciHandler();
}), }),
getOnline_AnnuncioIdCodice: publicProcedure.query(async () => {
return await getOnline_AnnuncioIdCodice_AnnunciHandler();
}),
getAnnuncio: publicProcedure getAnnuncio: publicProcedure
.input( .input(
z.object({ z.object({

View file

@ -5,13 +5,14 @@ import {
createTRPCRouter, createTRPCRouter,
protectedProcedure, protectedProcedure,
} from "~/server/api/trpc"; } from "~/server/api/trpc";
import { db } from "~/server/db";
import { import {
deleteAllUserEmails, deleteAllUserEmails,
deleteEmail, deleteEmail,
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 { zUserId } from "~/server/utils/zod_types"; import { zAnnuncioId, zUserId } from "~/server/utils/zod_types";
export const comunicazioniRouter = createTRPCRouter({ export const comunicazioniRouter = createTRPCRouter({
// genEmail: publicProcedure // genEmail: publicProcedure
@ -95,4 +96,45 @@ export const comunicazioniRouter = createTRPCRouter({
); );
} }
}), }),
sendAnnunciEmail: adminProcedure
.input(
z.object({
annunci: zAnnuncioId.array(),
to: z.email(),
}),
)
.mutation(async ({ input }) => {
try {
const annunci = await db
.selectFrom("annunci")
.select([
"annunci.titolo_it as titolo",
"annunci.codice",
"annunci.prezzo",
])
.innerJoin("images_refs", "images_refs.codice", "annunci.codice")
.select("images_refs.img as immagine")
.where("images_refs.ordine", "=", 0)
.where("annunci.id", "in", input.annunci)
.execute();
await NewMail({
template: {
mailType: "shareAnnunci",
props: {
nome: "Admin",
annunci,
},
},
mail: {
subject: `Scheda Annuncio - Infoalloggi.it`,
to: input.to,
},
});
} catch (e) {
throw new Error(
`Errore nell'invio dell'email: ${(e as Error).message}`,
);
}
}),
}); });

View file

@ -51,6 +51,27 @@ export const getAnnunciListHandler = async (): Promise<
} }
}; };
export const getOnline_AnnuncioIdCodice_AnnunciHandler = async () => {
try {
const data = await db
.selectFrom("annunci")
.select(["codice", "id"])
.where("annunci.web", "=", true)
.where("stato", "!=", "Sospeso")
.execute();
if (!data) {
return [];
}
return data.filter((c) => c.id && c.codice);
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Errore query getOnline_AnnuncioIdCodice_AnnunciHandler: ${(e as Error).message}`,
});
}
};
export const getCodici_AnnunciHandler = async (): Promise<string[]> => { export const getCodici_AnnunciHandler = async (): Promise<string[]> => {
try { try {
const codici = await db const codici = await db

View file

@ -12,9 +12,6 @@ import type { Generic_NewMail } from "emails/gereric-email";
import Generic from "emails/gereric-email"; import Generic from "emails/gereric-email";
import type { Invito_NewMail } from "emails/invito"; import type { Invito_NewMail } from "emails/invito";
import Invito from "emails/invito"; import Invito from "emails/invito";
import OnboardingServizio, {
type OnboardingServizio_NewMail,
} from "emails/onboarding-servizio";
import type { PagamentoConferma_NewMail } from "emails/pagamento-conferma"; 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 type { PagamentoErrore_NewMail } from "emails/pagamento-errore";
@ -30,6 +27,7 @@ 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 ShareAnnunci, { type ShareAnnunci_NewMail } from "emails/share-annunci";
import VerificaEmail, { import VerificaEmail, {
type VerificaEmail_NewMail, type VerificaEmail_NewMail,
} from "emails/VerificaEmail"; } from "emails/VerificaEmail";
@ -89,7 +87,7 @@ export type MailsTemplates =
| Recesso_NewMail | Recesso_NewMail
| RegistrazioneAvvenuta_NewMail | RegistrazioneAvvenuta_NewMail
| VerificaEmail_NewMail | VerificaEmail_NewMail
| OnboardingServizio_NewMail | ShareAnnunci_NewMail
| Generic_NewMail | Generic_NewMail
| SchedaContatto_NewMail | SchedaContatto_NewMail
| Invito_NewMail; | Invito_NewMail;
@ -160,8 +158,8 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
title = "Verifica email"; title = "Verifica email";
break; break;
case "onboardingServizio": case "shareAnnunci":
mailComponent = OnboardingServizio({ mailComponent = ShareAnnunci({
...mail.props, ...mail.props,
}); });
title = "Onboarding Servizio"; title = "Onboarding Servizio";