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.",
|
"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."
|
"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": {
|
"pinReset": {
|
||||||
"subject": "PIN erfolgreich geändert",
|
"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.",
|
"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",
|
"action": "Login",
|
||||||
"reason": "You are receiving this email because someone changed the PIN-Code for your account."
|
"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": {
|
"userInvite": {
|
||||||
"subject": "Confirm your E-Mail Address",
|
"subject": "Confirm your E-Mail Address",
|
||||||
"introduction": "welcome our appointment booking platform. Please 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 PinReset from "$lib/emails/PinReset.svelte";
|
||||||
import UserInvite from "$lib/emails/UserInvite.svelte";
|
import UserInvite from "$lib/emails/UserInvite.svelte";
|
||||||
import { dev } from "$app/environment";
|
import { dev } from "$app/environment";
|
||||||
|
import Notification from "$lib/emails/Notification.svelte";
|
||||||
|
|
||||||
export type SelectClient = {
|
export type SelectClient = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -541,3 +542,34 @@ export async function sendAppointmentCancelledEmail(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
await sendEmail(recipient as any, subject, html, text, tenant.longName);
|
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 {
|
import {
|
||||||
notification,
|
notification,
|
||||||
channelStaff,
|
channelStaff,
|
||||||
notificationTypes,
|
notificationTypes,
|
||||||
type NotificationType,
|
type NotificationType,
|
||||||
} from "../db/tenant-schema";
|
} 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 logger from "$lib/logger";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ValidationError, NotFoundError } from "../utils/errors";
|
import { ValidationError, NotFoundError } from "../utils/errors";
|
||||||
|
import { sendNotificationEmail } from "../email/email-service";
|
||||||
|
import { TenantAdminService } from "./tenant-admin-service";
|
||||||
|
|
||||||
const notificationCreationSchema = z.object({
|
const notificationCreationSchema = z.object({
|
||||||
channelId: z.uuid({ message: "Invalid UUID format" }),
|
channelId: z.uuid({ message: "Invalid UUID format" }),
|
||||||
@@ -105,6 +108,30 @@ export class NotificationService {
|
|||||||
.values(notificationsToCreate)
|
.values(notificationsToCreate)
|
||||||
.returning({ id: notification.id });
|
.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", {
|
log.info("Created notifications for channel", {
|
||||||
channelId: request.channelId,
|
channelId: request.channelId,
|
||||||
count: createdNotifications.length,
|
count: createdNotifications.length,
|
||||||
|
|||||||
@@ -47,6 +47,8 @@
|
|||||||
>
|
>
|
||||||
{m["login.action"]()}
|
{m["login.action"]()}
|
||||||
</Form.Button>
|
</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.Action>
|
||||||
</CenteredCard.Root>
|
</CenteredCard.Root>
|
||||||
|
|||||||
Reference in New Issue
Block a user