Files
anouma/lib/email/bookingTemplates.ts
maroandClaude Sonnet 5 1cd15aff25 Add email verification, personal calendar feed, and full SEO implementation
- Customer accounts now require email verification (hashed, single-use,
  time-limited tokens) before they can request/confirm bookings, with
  resend flows on login/account/booking widget and rate limiting.
- Admins get a private, rotatable iCalendar (ICS) subscription feed of
  their confirmed bookings and public events, timezone-correct for
  Europe/Berlin including DST, never exposing meeting passwords.
- Adds a full SEO layer: per-page canonical/OG/Twitter metadata with
  CMS-editable overrides and content-derived fallbacks, a dynamic
  sitemap.xml and robots.txt driven by real published content, JSON-LD
  (Organization/LocalBusiness, WebSite, WebPage, BreadcrumbList, Service,
  Event, BlogPosting) that never fabricates data, and a CMS-managed
  redirect table for changed slugs.
- Global ANOUMA-naming audit: the brand name is never used to label
  personal account/calendar areas anywhere in the app, CMS, or emails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 22:57:31 +02:00

153 lines
5.5 KiB
TypeScript

import { escapeHtml, paragraph, renderButton, renderEmailShell, renderFactBox } from "./layout";
type BaseArgs = {
name: string;
offerTitle: string;
dateLabel: string;
timeLabel: string;
};
export function bookingRequestReceivedEmail(args: BaseArgs & { appointmentTypeLabel: string }): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph(`Hallo ${args.name},`),
paragraph("deine Terminanfrage bei ANOUMA wurde übermittelt.", { italic: true }),
renderFactBox([
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
{ label: "Terminart", value: args.appointmentTypeLabel },
]),
paragraph("Der Termin ist noch nicht verbindlich bestätigt. Du erhältst eine weitere E-Mail, sobald Anna deine Anfrage bestätigt hat.", { small: true }),
].join("\n"),
});
return { subject: "Deine Terminanfrage bei ANOUMA", html };
}
export function newBookingRequestAdminEmail(args: {
customerName: string;
customerEmail: string;
offerTitle: string;
dateLabel: string;
timeLabel: string;
appointmentTypeLabel: string;
adminUrl: string;
}): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph("Neue Terminanfrage", { italic: true }),
renderFactBox([
{ label: "Name", value: args.customerName },
{ label: "E-Mail", value: args.customerEmail },
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
{ label: "Terminart", value: args.appointmentTypeLabel },
]),
renderButton("Im Admin-Bereich ansehen", args.adminUrl),
].join("\n"),
});
return { subject: "Neue Terminanfrage", html };
}
export function bookingConfirmedEmail(
args: BaseArgs & {
appointmentTypeLabel: string;
online?: { joinUrl: string; password: string };
onsite?: { address: string; mapsUrl: string };
},
): { subject: string; html: string } {
const rows = [
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
{ label: "Ort", value: args.appointmentTypeLabel },
];
if (args.onsite) rows.push({ label: "Adresse", value: args.onsite.address });
const extras: string[] = [];
if (args.online) {
extras.push(renderButton("Meeting betreten", args.online.joinUrl));
extras.push(
`<p style="margin:0 0 4px;color:#66505f;font-size:14px;"><strong>Meeting-Passwort:</strong></p>` +
`<p style="margin:0 0 20px;color:#66505f;font-size:20px;letter-spacing:0.08em;font-family:'Courier New',monospace;">${escapeHtml(args.online.password)}</p>`,
);
}
if (args.onsite) {
extras.push(renderButton("Route öffnen", args.onsite.mapsUrl));
}
const html = renderEmailShell({
bodyHtml: [
paragraph(`Hallo ${args.name},`),
paragraph("dein Termin bei ANOUMA wurde bestätigt.", { italic: true }),
renderFactBox(rows),
...extras,
].join("\n"),
});
return { subject: "Dein Termin bei ANOUMA wurde bestätigt", html };
}
export function bookingRejectedEmail(args: BaseArgs): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph(`Hallo ${args.name},`),
paragraph("leider kann der folgende Termin nicht stattfinden.", { italic: true }),
renderFactBox([
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
]),
paragraph("Melde dich gerne für einen neuen Termin — schau einfach wieder in deinem Konto vorbei.", { small: true }),
].join("\n"),
});
return { subject: "Deine Terminanfrage bei ANOUMA", html };
}
export function alternativeProposedEmail(args: {
name: string;
offerTitle: string;
originalDateLabel: string;
originalTimeLabel: string;
altDateLabel: string;
altTimeLabel: string;
accountUrl: string;
}): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph(`Hallo ${args.name},`),
paragraph("Anna hat einen alternativen Termin für dich vorgeschlagen.", { italic: true }),
renderFactBox([
{ label: "Angebot", value: args.offerTitle },
{ label: "Ursprünglich angefragt", value: `${args.originalDateLabel}, ${args.originalTimeLabel}` },
{ label: "Neuer Vorschlag", value: `${args.altDateLabel}, ${args.altTimeLabel}` },
]),
renderButton("Zum Termin in meinem Konto", args.accountUrl),
paragraph("Dort kannst du den neuen Termin annehmen oder einen anderen anfragen.", { small: true }),
].join("\n"),
});
return { subject: "Alternativer Terminvorschlag von ANOUMA", html };
}
export function bookingCancelledAdminEmail(args: {
customerName: string;
offerTitle: string;
dateLabel: string;
timeLabel: string;
adminUrl: string;
}): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph("Ein Termin wurde storniert.", { italic: true }),
renderFactBox([
{ label: "Name", value: args.customerName },
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
]),
renderButton("Im Admin-Bereich ansehen", args.adminUrl),
].join("\n"),
});
return { subject: "Termin storniert", html };
}