- 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>
153 lines
5.5 KiB
TypeScript
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 };
|
|
}
|