diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index 15a9555..e6d8abe 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -1034,6 +1034,12 @@ "hint": "Dieser Link ist nur {expirationMinutes} Minuten gültig und kann nur einmal verwendet werden.", "reason": "Du erhältst diese E-Mail, weil jemand für Deine E-Mail Adresse ein Konto registriert hat." }, + "notification": { + "subject": "Aktivität in Deinem Terminbuchungsportal", + "introduction": "in Deinem Terminbuchungsportal gab es eine neue Aktivität. Bitte logge Dich ein, um die Details zu sehen.", + "action": "Dashboard öffnen", + "reason": "Du erhältst diese E-Mail, weil Du für einen Kanal Benachrichtigungen aktiviert hast." + }, "pinReset": { "subject": "PIN erfolgreich geändert", "introduction": "Sie haben erfolgreich Ihre PIN beim Terminbuchungsportal von {tenant} geändert und können sich ab sofort mit Ihrer PIN anmelden.", diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index dba5a45..d0f8f1e 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -1049,6 +1049,12 @@ "action": "Login", "reason": "You are receiving this email because someone changed the PIN-Code for your account." }, + "notification": { + "subject": "Activity in your appointment booking platform", + "introduction": "There is new activity in your appointment booking platform. Please log in to view details.", + "action": "Open Dashboard", + "reason": "You are receiving this email because you have notifications enabled for a channel." + }, "userInvite": { "subject": "Confirm your E-Mail Address", "introduction": "welcome our appointment booking platform. Please confirm your e-mail address.", diff --git a/src/lib/emails/Notification.svelte b/src/lib/emails/Notification.svelte new file mode 100644 index 0000000..ad4061a --- /dev/null +++ b/src/lib/emails/Notification.svelte @@ -0,0 +1,34 @@ + + + + {m["emails.greeting"]({ name: user.name })} + + {m["emails.notification.introduction"]()} + + {m["emails.notification.action"]()} + + {m["emails.notification.reason"]()} + + diff --git a/src/lib/server/email/email-service.ts b/src/lib/server/email/email-service.ts index 6011768..f16ba5e 100644 --- a/src/lib/server/email/email-service.ts +++ b/src/lib/server/email/email-service.ts @@ -24,6 +24,7 @@ import Confirmation from "$lib/emails/Confirmation.svelte"; import PinReset from "$lib/emails/PinReset.svelte"; import UserInvite from "$lib/emails/UserInvite.svelte"; import { dev } from "$app/environment"; +import Notification from "$lib/emails/Notification.svelte"; export type SelectClient = { email: string; @@ -541,3 +542,34 @@ export async function sendAppointmentCancelledEmail( // eslint-disable-next-line @typescript-eslint/no-explicit-any await sendEmail(recipient as any, subject, html, text, tenant.longName); } + +/** + * Send notification email + * @param {SelectClient | SelectUser} user - Database user object or client data + * @param {SelectTenant} tenant - Tenant information for branding + * @param {SelectAppointment} appointment - Cancelled appointment details + * @param {string} [channelTitle] - Optional channel title/name + * @throws {Error} When email sending fails + * @returns {Promise} + */ +export async function sendNotificationEmail( + user: SelectUser, + tenant: { domain: string; longName: string }, +): Promise { + // Create recipient directly for SelectClient type, use helper for SelectUser + const { recipient, locale } = await getRecipient(user); + // Generate email + const subject = m["emails.notification.subject"](); + const emailRender = render(Notification, { + props: { + locale, + user, + dashboardUrl: dev ? `http://localhost:5173/dashboard` : `https://${tenant.domain}/dashboard`, + }, + }); + const html = renderOutputToHtml(emailRender); + const text = htmlToText(html); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await sendEmail(recipient as any, subject, html, text, tenant.longName); +} diff --git a/src/lib/server/services/notification-service.ts b/src/lib/server/services/notification-service.ts index 02c1901..6f49a24 100644 --- a/src/lib/server/services/notification-service.ts +++ b/src/lib/server/services/notification-service.ts @@ -1,14 +1,17 @@ -import { getTenantDb } from "../db"; +import { centralDb, getTenantDb } from "../db"; import { notification, channelStaff, notificationTypes, type NotificationType, } from "../db/tenant-schema"; -import { eq, and, desc } from "drizzle-orm"; +import { user } from "$lib/server/db/central-schema"; +import { eq, and, desc, inArray } from "drizzle-orm"; import logger from "$lib/logger"; import { z } from "zod"; import { ValidationError, NotFoundError } from "../utils/errors"; +import { sendNotificationEmail } from "../email/email-service"; +import { TenantAdminService } from "./tenant-admin-service"; const notificationCreationSchema = z.object({ channelId: z.uuid({ message: "Invalid UUID format" }), @@ -105,6 +108,30 @@ export class NotificationService { .values(notificationsToCreate) .returning({ id: notification.id }); + // Send e-mail notifications + if (request.type === "APPOINTMENT_REQUESTED") { + const userAccounts = await centralDb + .select() + .from(user) + .where( + inArray( + user.id, + notificationsToCreate.map((n) => n.staffId), + ), + ); + if (userAccounts.length > 0) { + const adminService = await TenantAdminService.getTenantById(this.tenantId); + const tenant = adminService.tenantData; + if (tenant) { + await Promise.all( + userAccounts.map((staff) => + sendNotificationEmail(staff, { domain: tenant.domain, longName: tenant.longName }), + ), + ); + } + } + } + log.info("Created notifications for channel", { channelId: request.channelId, count: createdNotifications.length, diff --git a/src/routes/(pages)/(clients)/clients/login/+page.svelte b/src/routes/(pages)/(clients)/clients/login/+page.svelte index a97af2d..091ffbc 100644 --- a/src/routes/(pages)/(clients)/clients/login/+page.svelte +++ b/src/routes/(pages)/(clients)/clients/login/+page.svelte @@ -47,6 +47,8 @@ > {m["login.action"]()} - +