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";
import { env } from "~/env";
import Base from "./base";
export type OnboardingServizio_NewMail = {
mailType: "onboardingServizio";
props: OnboardingServizioProps;
export type ShareAnnunci_NewMail = {
mailType: "shareAnnunci";
props: ShareAnnunciProps;
};
type OnboardingServizioProps = {
token: string;
type ShareAnnunciProps = {
nome: string;
annunci?: {
titolo: string | null;
@ -25,11 +24,7 @@ type OnboardingServizioProps = {
}[];
};
const OnboardingServizio = ({
token,
nome,
annunci,
}: OnboardingServizioProps) => {
const ShareAnnunci = ({ nome, annunci }: ShareAnnunciProps) => {
return (
<Base preview="Procedi ora su Infoalloggi.it">
<>
@ -48,9 +43,9 @@ const OnboardingServizio = ({
<Section className="mb-5">
<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"
href={`${env.BASE_URL}/servizio/pre-onboard/${token}`}
href={`${env.BASE_URL}`}
>
Vai al servizio
Vai al Sito
</Button>
</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 { 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 { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button";
import { Label } from "~/components/ui/label";
import type { NextPageWithLayout } from "~/pages/_app";
import type { AnnunciId } from "~/schemas/public/Annunci";
import { api } from "~/utils/api";
const Impostazioni: NextPageWithLayout = () => {
@ -17,6 +25,7 @@ const Impostazioni: NextPageWithLayout = () => {
</div>
</div>
<StripeSection />
<SendEmailSection />
</div>
</>
);
@ -59,3 +68,91 @@ const StripeSection = () => {
</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,
getAnnunciRicerca,
getCodici_AnnunciHandler,
getOnline_AnnuncioIdCodice_AnnunciHandler,
getOptions_AnnunciHandler,
getProprietarioDataHandler,
} from "~/server/controllers/annunci.controller";
@ -39,6 +40,9 @@ export const annunciRouter = createTRPCRouter({
getAnnunciOptions: publicProcedure.query(async () => {
return await getOptions_AnnunciHandler();
}),
getOnline_AnnuncioIdCodice: publicProcedure.query(async () => {
return await getOnline_AnnuncioIdCodice_AnnunciHandler();
}),
getAnnuncio: publicProcedure
.input(
z.object({

View file

@ -5,13 +5,14 @@ import {
createTRPCRouter,
protectedProcedure,
} from "~/server/api/trpc";
import { db } from "~/server/db";
import {
deleteAllUserEmails,
deleteEmail,
getEmails,
} from "~/server/services/email.service";
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({
// 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[]> => {
try {
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 type { Invito_NewMail } 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 PagamentoConferma from "emails/pagamento-conferma";
import type { PagamentoErrore_NewMail } from "emails/pagamento-errore";
@ -30,6 +27,7 @@ import RegistrazioneAvvenuta from "emails/registrazione-avvenuta";
import EmailSchedaContatto, {
type SchedaContatto_NewMail,
} from "emails/scheda-annuncio";
import ShareAnnunci, { type ShareAnnunci_NewMail } from "emails/share-annunci";
import VerificaEmail, {
type VerificaEmail_NewMail,
} from "emails/VerificaEmail";
@ -89,7 +87,7 @@ export type MailsTemplates =
| Recesso_NewMail
| RegistrazioneAvvenuta_NewMail
| VerificaEmail_NewMail
| OnboardingServizio_NewMail
| ShareAnnunci_NewMail
| Generic_NewMail
| SchedaContatto_NewMail
| Invito_NewMail;
@ -160,8 +158,8 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
title = "Verifica email";
break;
case "onboardingServizio":
mailComponent = OnboardingServizio({
case "shareAnnunci":
mailComponent = ShareAnnunci({
...mail.props,
});
title = "Onboarding Servizio";