diff --git a/docs/email-system.md b/docs/email-system.md
index f95ee08..4b6dc53 100644
--- a/docs/email-system.md
+++ b/docs/email-system.md
@@ -1,5 +1,7 @@
# Email System Documentation
+> This doc is partially outdated. New templates can be viewed locally. See routes dir. Example: http://localhost:5173/local-only/e-mail-templates/user-invite Text versions are generated automatically and will be printed to console, when local page is opened.
+
The Open Reception appointment booking platform includes a comprehensive email system for automated communications with clients and staff members. This document provides technical details and administration guidance for managing the email system.
## Overview
diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json
index 72ad3a0..9997996 100644
--- a/project.inlang/messages/de.json
+++ b/project.inlang/messages/de.json
@@ -917,6 +917,37 @@
"introduction": "Ihr Termin wurde gebucht.",
"action": "Termin absagen",
"reason": "Sie erhalten diese E-Mail, weil jemand mit Ihrer E-Mail Adresse einen Termin bei gebucht hat."
+ },
+ "appointmentReminder": {
+ "subject": "Erinnerung: Ihr Termin bei {tenant}",
+ "introduction": "Ihr Termin wurde bei {tenant} steht in Kürze bevor.",
+ "action": "Termin absagen",
+ "reason": "Sie erhalten diese E-Mail, weil jemand mit Ihrer E-Mail Adresse einen Termin bei gebucht hat."
+ },
+ "appointmentRequest": {
+ "subject": "{channel} bei {tenant} angefragt",
+ "introduction": "Ihr Termin wurde angefragt. Sie werden benachrichtigt, sobald der Termin bestätigt wurde.",
+ "reason": "Sie erhalten diese E-Mail, weil jemand mit Ihrer E-Mail Adresse einen Termin bei angefragt hat."
+ },
+ "confirmation": {
+ "subject": "E-Mail Adresse bestätigen",
+ "introduction": "willkommen bei unserem Terminbuchungsportal. Bitte bestätige Deine E-Mail Adresse.",
+ "action": "E-Mail Adresse bestätigen",
+ "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."
+ },
+ "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.",
+ "action": "Anmelden",
+ "reason": "Sie erhalten diese E-Mail, weil jemand für die PIN für Ihr Konto geändert hat."
+ },
+ "userInvite": {
+ "subject": "E-Mail Adresse bestätigen für {tenant}",
+ "introduction": "willkommen beim Terminbuchungsportal von {tenant}. Bitte bestätige Deine E-Mail Adresse.",
+ "action": "E-Mail Adresse bestätigen",
+ "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."
}
}
}
diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json
index 08741a2..ecbfb17 100644
--- a/project.inlang/messages/en.json
+++ b/project.inlang/messages/en.json
@@ -562,7 +562,7 @@
"absences": {
"title": "Absences",
"add": {
- "title": "Add Anbsence",
+ "title": "Add Absence",
"description": "Absences are a way to mark you agents as being unavailbale.",
"fields": {
"agent": {
@@ -926,6 +926,37 @@
"introduction": "your appointment for has been booked.",
"action": "Cancel appointment",
"reason": "You are receiving this email because someone booked an appointment with your e-mail address."
+ },
+ "appointmentReminder": {
+ "subject": "Reminder: Your appointment with {tenant}",
+ "introduction": "your appointment with {tenant} is soon.",
+ "action": "Cancel appointment",
+ "reason": "You are receiving this email because someone booked an appointment with your e-mail address."
+ },
+ "appointmentRequest": {
+ "subject": "{channel} with {tenant} requested",
+ "introduction": "your appointment was requested. You will be notified once it is confirmed.",
+ "reason": "You are receiving this email because someone requested an appointment with your e-mail address."
+ },
+ "confirmation": {
+ "subject": "Confirm your E-Mail Address",
+ "introduction": "welcome our appointment booking platform. Please confirm your e-mail address.",
+ "action": "Confirm E-Mail Address",
+ "hint": "This link is only valid for {expirationMinutes} minutes and can only be used once.",
+ "reason": "You are receiving this email because someone registered an account with your e-mail address."
+ },
+ "pinReset": {
+ "subject": "PIN successfully changed",
+ "introduction": "you've successfully changed your PIN on the {tenant} appointment booking platform. You can now log-in with your new PIN.",
+ "action": "Login",
+ "reason": "You are receiving this email because someone changed the PIN-Code for your account."
+ },
+ "userInvite": {
+ "subject": "Confirm your E-Mail Address",
+ "introduction": "welcome our appointment booking platform. Please confirm your e-mail address.",
+ "action": "Confirm E-Mail Address",
+ "hint": "This link is only valid for {expirationMinutes} minutes and can only be used once.",
+ "reason": "You are receiving this email because someone registered an account with your e-mail address."
}
}
}
diff --git a/src/lib/emails/AppointmentReminder.svelte b/src/lib/emails/AppointmentReminder.svelte
new file mode 100644
index 0000000..37101c7
--- /dev/null
+++ b/src/lib/emails/AppointmentReminder.svelte
@@ -0,0 +1,65 @@
+
+
+
+ {m["emails.greeting"]({ name: user.email })}
+
+ {m["emails.appointmentReminder.introduction"]({ tenant: tenant.longName })}
+
+ {channel}
+
+ {appointment.agentName}
+ {renderAppointmentDate(appointment.appointmentDate, locale)}
+ {renderAppointmentTime(appointment.appointmentDate, locale)}
+ {m["emails.oclock"]()}
+
+ {tenant.longName}
+
+ {address.street}
+ {address.number}
+ {#if address.additionalAddressInfo}{address.additionalAddressInfo}
{/if}
+ {address.zip}
+ {address.city}
+
+ {m["emails.appointmentReminder.action"]()}
+
+ {m["emails.appointmentReminder.reason"]()}
+
+
diff --git a/src/lib/emails/AppointmentRequest.svelte b/src/lib/emails/AppointmentRequest.svelte
new file mode 100644
index 0000000..ab54418
--- /dev/null
+++ b/src/lib/emails/AppointmentRequest.svelte
@@ -0,0 +1,62 @@
+
+
+
+ {m["emails.greeting"]({ name: user.email })}
+
+ {m["emails.appointmentRequest.introduction"]()}
+
+ {channel}
+
+ {appointment.agentName}
+ {renderAppointmentDate(appointment.appointmentDate, locale)}
+ {renderAppointmentTime(appointment.appointmentDate, locale)}
+ {m["emails.oclock"]()}
+
+ {tenant.longName}
+
+ {address.street}
+ {address.number}
+ {#if address.additionalAddressInfo}{address.additionalAddressInfo}
{/if}
+ {address.zip}
+ {address.city}
+
+
+ {m["emails.appointmentRequest.reason"]()}
+
+
diff --git a/src/lib/emails/Confirmation.svelte b/src/lib/emails/Confirmation.svelte
new file mode 100644
index 0000000..8e2a279
--- /dev/null
+++ b/src/lib/emails/Confirmation.svelte
@@ -0,0 +1,37 @@
+
+
+
+ {m["emails.greeting"]({ name: user.name })}
+
+ {m["emails.confirmation.introduction"]()}
+
+ {m["emails.confirmation.action"]()}
+
+ {m["emails.confirmation.hint"]({ expirationMinutes })}
+
+
+ {m["emails.confirmation.reason"]()}
+
+
diff --git a/src/lib/emails/PinReset.svelte b/src/lib/emails/PinReset.svelte
new file mode 100644
index 0000000..5826e5d
--- /dev/null
+++ b/src/lib/emails/PinReset.svelte
@@ -0,0 +1,35 @@
+
+
+
+ {m["emails.greeting"]({ name: user.email })}
+
+ {m["emails.pinReset.introduction"]({ tenant: tenant.longName })}
+
+ {m["emails.pinReset.action"]()}
+
+ {m["emails.pinReset.reason"]()}
+
+
diff --git a/src/lib/emails/UserInvite.svelte b/src/lib/emails/UserInvite.svelte
new file mode 100644
index 0000000..bb15168
--- /dev/null
+++ b/src/lib/emails/UserInvite.svelte
@@ -0,0 +1,40 @@
+
+
+
+ {m["emails.greeting"]({ name: user.name })}
+
+ {m["emails.userInvite.introduction"]({ tenant: tenant.longName })}
+
+ {m["emails.userInvite.action"]()}
+
+ {m["emails.userInvite.hint"]({ expirationMinutes })}
+
+
+ {m["emails.userInvite.reason"]()}
+
+
diff --git a/src/lib/emails/components/EmailLayout.svelte b/src/lib/emails/components/EmailLayout.svelte
index da760e4..b2ba8b0 100644
--- a/src/lib/emails/components/EmailLayout.svelte
+++ b/src/lib/emails/components/EmailLayout.svelte
@@ -50,10 +50,13 @@
margin-top: 0;
}
.page {
+ padding: 0 16px;
+ padding: 0 1rem;
min-height: 100%;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
+ font-family: "Inter", Arial, sans-serif;
}
.container {
max-width: 600px;
diff --git a/src/lib/server/email/email-service.ts b/src/lib/server/email/email-service.ts
index ff3a91d..329e8b6 100644
--- a/src/lib/server/email/email-service.ts
+++ b/src/lib/server/email/email-service.ts
@@ -21,6 +21,8 @@ export type SelectClient = {
language: string;
};
+export type SelectUserEmail = Pick;
+
/**
* Get channel title in the user's preferred language
* @param {string} tenantId - Tenant ID
diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-reminder/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-reminder/+server.ts
new file mode 100644
index 0000000..fcb1fbf
--- /dev/null
+++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-reminder/+server.ts
@@ -0,0 +1,42 @@
+import AppointmentReminder from "$lib/emails/AppointmentReminder.svelte";
+import { renderOutputToHtml, htmlToText } from "$lib/emails/utils";
+import type { SelectTenant } from "$lib/server/db/central-schema";
+import type { SelectAppointment } from "$lib/server/db/tenant-schema";
+import type { RequestHandler } from "@sveltejs/kit";
+import { render } from "svelte/server";
+
+export const GET: RequestHandler = async () => {
+ const emailRender = render(AppointmentReminder, {
+ props: {
+ locale: "en",
+ user: {
+ email: "max.mustermann@example.com",
+ language: "en",
+ },
+ tenant: { longName: "Praxis Dr. Jane Doe" } as SelectTenant,
+ appointment: {
+ appointmentDate: new Date("2024-06-30T10:00:00"),
+ agentName: "Dr. John Doe",
+ } as SelectAppointment & { agentName: string },
+ channel: "Vaccination Appointment",
+ address: {
+ street: "Musterstraße",
+ number: "1",
+ additionalAddressInfo: "Hinterhaus",
+ zip: "20000",
+ city: "Hamburg",
+ },
+ cancelUrl: "https://open-reception.org/appointments/cancel/abc123",
+ },
+ });
+ const html = renderOutputToHtml(emailRender);
+ const text = htmlToText(html);
+
+ // Output for testing purposes
+ console.log(text);
+ return new Response(html, {
+ headers: {
+ "Content-Type": "text/html",
+ },
+ });
+};
diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-request/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-request/+server.ts
new file mode 100644
index 0000000..86914e3
--- /dev/null
+++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/appointment-request/+server.ts
@@ -0,0 +1,41 @@
+import AppointmentRequested from "$lib/emails/AppointmentRequest.svelte";
+import { renderOutputToHtml, htmlToText } from "$lib/emails/utils";
+import type { SelectTenant } from "$lib/server/db/central-schema";
+import type { SelectAppointment } from "$lib/server/db/tenant-schema";
+import type { RequestHandler } from "@sveltejs/kit";
+import { render } from "svelte/server";
+
+export const GET: RequestHandler = async () => {
+ const emailRender = render(AppointmentRequested, {
+ props: {
+ locale: "en",
+ user: {
+ email: "max.mustermann@example.com",
+ language: "en",
+ },
+ tenant: { longName: "Praxis Dr. Jane Doe" } as SelectTenant,
+ appointment: {
+ appointmentDate: new Date("2024-06-30T10:00:00"),
+ agentName: "Dr. John Doe",
+ } as SelectAppointment & { agentName: string },
+ channel: "Vaccination Appointment",
+ address: {
+ street: "Musterstraße",
+ number: "1",
+ additionalAddressInfo: "Hinterhaus",
+ zip: "20000",
+ city: "Hamburg",
+ },
+ },
+ });
+ const html = renderOutputToHtml(emailRender);
+ const text = htmlToText(html);
+
+ // Output for testing purposes
+ console.log(text);
+ return new Response(html, {
+ headers: {
+ "Content-Type": "text/html",
+ },
+ });
+};
diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/confirmation/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/confirmation/+server.ts
new file mode 100644
index 0000000..ce1aed6
--- /dev/null
+++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/confirmation/+server.ts
@@ -0,0 +1,29 @@
+import Confirmation from "$lib/emails/Confirmation.svelte";
+import { htmlToText, renderOutputToHtml } from "$lib/emails/utils";
+import type { RequestHandler } from "@sveltejs/kit";
+import { render } from "svelte/server";
+
+export const GET: RequestHandler = async () => {
+ const emailRender = render(Confirmation, {
+ props: {
+ locale: "en",
+ user: {
+ email: "max.mustermann@example.com",
+ name: "Max Mustermann",
+ language: "en",
+ },
+ confirmUrl: "https://open-reception.org/confirm/abc123",
+ expirationMinutes: 10,
+ },
+ });
+ const html = renderOutputToHtml(emailRender);
+ const text = htmlToText(html);
+
+ // Output for testing purposes
+ console.log(text);
+ return new Response(html, {
+ headers: {
+ "Content-Type": "text/html",
+ },
+ });
+};
diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/pin-reset/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/pin-reset/+server.ts
new file mode 100644
index 0000000..327f7f3
--- /dev/null
+++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/pin-reset/+server.ts
@@ -0,0 +1,29 @@
+import PinReset from "$lib/emails/PinReset.svelte";
+import { renderOutputToHtml, htmlToText } from "$lib/emails/utils";
+import type { SelectTenant } from "$lib/server/db/central-schema";
+import type { RequestHandler } from "@sveltejs/kit";
+import { render } from "svelte/server";
+
+export const GET: RequestHandler = async () => {
+ const emailRender = render(PinReset, {
+ props: {
+ locale: "en",
+ user: {
+ email: "max.mustermann@example.com",
+ language: "en",
+ },
+ tenant: { longName: "Praxis Dr. Jane Doe" } as SelectTenant,
+ loginUrl: "https://open-reception.org/login",
+ },
+ });
+ const html = renderOutputToHtml(emailRender);
+ const text = htmlToText(html);
+
+ // Output for testing purposes
+ console.log(text);
+ return new Response(html, {
+ headers: {
+ "Content-Type": "text/html",
+ },
+ });
+};
diff --git a/src/routes/(local-only)/[test=dev]/e-mail-templates/user-invite/+server.ts b/src/routes/(local-only)/[test=dev]/e-mail-templates/user-invite/+server.ts
new file mode 100644
index 0000000..386c12c
--- /dev/null
+++ b/src/routes/(local-only)/[test=dev]/e-mail-templates/user-invite/+server.ts
@@ -0,0 +1,31 @@
+import UserInvite from "$lib/emails/UserInvite.svelte";
+import { htmlToText, renderOutputToHtml } from "$lib/emails/utils";
+import type { SelectTenant } from "$lib/server/db/central-schema";
+import type { RequestHandler } from "@sveltejs/kit";
+import { render } from "svelte/server";
+
+export const GET: RequestHandler = async () => {
+ const emailRender = render(UserInvite, {
+ props: {
+ locale: "en",
+ user: {
+ email: "max.mustermann@example.com",
+ name: "Max Mustermann",
+ language: "en",
+ },
+ tenant: { longName: "Praxis Dr. Jane Doe" } as SelectTenant,
+ confirmUrl: "https://open-reception.org/confirm/abc123",
+ expirationMinutes: 10,
+ },
+ });
+ const html = renderOutputToHtml(emailRender);
+ const text = htmlToText(html);
+
+ // Output for testing purposes
+ console.log(text);
+ return new Response(html, {
+ headers: {
+ "Content-Type": "text/html",
+ },
+ });
+};