mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-08-17 21:25:52 +02:00
Send notifications for appointment requests via email
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { m } from "$i18n/messages";
|
||||
import { setLocale } from "$i18n/runtime";
|
||||
import type { SupportedLocale } from "$lib/const/locales";
|
||||
import type { SelectUserEmail } from "$lib/server/email/email-service";
|
||||
import EmailButton from "./components/EmailButton.svelte";
|
||||
import EmailLayout from "./components/EmailLayout.svelte";
|
||||
import EmailText from "./components/EmailText.svelte";
|
||||
|
||||
let {
|
||||
locale,
|
||||
user,
|
||||
dashboardUrl,
|
||||
}: {
|
||||
locale: SupportedLocale;
|
||||
user: SelectUserEmail;
|
||||
dashboardUrl: string;
|
||||
} = $props();
|
||||
|
||||
$effect(() => {
|
||||
setLocale(locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<EmailLayout>
|
||||
<EmailText variant="md">{m["emails.greeting"]({ name: user.name })}</EmailText>
|
||||
<EmailText variant="md">
|
||||
{m["emails.notification.introduction"]()}
|
||||
</EmailText>
|
||||
<EmailButton href={dashboardUrl}>{m["emails.notification.action"]()}</EmailButton>
|
||||
<EmailText variant="md" color="text-light">
|
||||
{m["emails.notification.reason"]()}
|
||||
</EmailText>
|
||||
</EmailLayout>
|
||||
@@ -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<void>}
|
||||
*/
|
||||
export async function sendNotificationEmail(
|
||||
user: SelectUser,
|
||||
tenant: { domain: string; longName: string },
|
||||
): Promise<void> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
>
|
||||
{m["login.action"]()}
|
||||
</Form.Button>
|
||||
<Button href={resolve(ROUTES.LOGIN)} variant="link">{m["clients.login.altAction"]()}</Button>
|
||||
<Button href={resolve(ROUTES.DASHBOARD.MAIN)} variant="link">
|
||||
{m["clients.login.altAction"]()}
|
||||
</Button>
|
||||
</CenteredCard.Action>
|
||||
</CenteredCard.Root>
|
||||
|
||||
Reference in New Issue
Block a user