Telefonnummer bei Buchung übertragen und in Bestätigungsmail ergänzt

feature/booking-component
Marvin Gemlin 2026-09-28 16:48:35 +00:00
parent 9bfa8b78b0
commit 6f2b239e57
6 changed files with 32 additions and 11 deletions

View File

@ -68,7 +68,9 @@ Der vollständige Vertrag steht in `src/server/swagger.json`.
POST-Anfragen benötigen `Content-Type: application/json`. Beim Buchen ist
zusätzlich ein `Idempotency-Key` als UUID v4 erforderlich. Das Formular sendet
`packageId`, gegebenenfalls `variantId`, `name` und `email`.
`packageId`, gegebenenfalls `variantId`, `name`, `email` und optional `phone`.
Die Telefonnummer darf höchstens 50 Zeichen und keine Steuerzeichen enthalten.
Sie wird in der CSV gespeichert und in der Bestätigungsmail ausgegeben.
Alternativ akzeptiert die API das Swagger-Objekt `customer` mit `firstName`,
`lastName`, `email` sowie optional `phone`, `ageGroup` und `notes`. Dabei muss
@ -93,6 +95,7 @@ erhalten bleiben; ältere UUID-Buchungsnummern werden weiterhin unterstützt.
`bookedPackage` speichert den Bestellstand als JSON, `customer` die optionalen
strukturierten Kundendaten und `acceptedTerms` die zugehörige Zustimmung.
Die optionale Telefonnummer steht zusätzlich in der Spalte `phone`.
CSV-Dateien mit dem früheren Spaltensatz bleiben lesbar und werden beim nächsten
Speichern erweitert. Für alte Buchungen ohne Bestellstand liefert die API
`bookedPackage: null`.

View File

@ -102,8 +102,7 @@ export function mountBookingPage(
</div>
<div class="booking__field">
<label for="${instanceId}-booking-phone">Telefonnummer (optional)</label>
<input id="${instanceId}-booking-phone" name="phone" type="tel" autocomplete="tel" maxlength="50" aria-describedby="${instanceId}-booking-phone-hint ${instanceId}-booking-phone-error" />
<p id="${instanceId}-booking-phone-hint">Die Telefonnummer wird derzeit noch nicht an die Buchungsstelle übermittelt.</p>
<input id="${instanceId}-booking-phone" name="phone" type="tel" autocomplete="tel" maxlength="50" aria-describedby="${instanceId}-booking-phone-error" />
<p class="booking__field-error" id="${instanceId}-booking-phone-error"></p>
</div>
<div class="booking__actions">
@ -252,7 +251,7 @@ export function mountBookingPage(
if (stored.order) renderOrder(stored.order);
name.value = stored.payload.name;
email.value = stored.payload.email;
phone.value = stored.phone || "";
phone.value = stored.payload.phone || "";
lockFields(true);
button.textContent = "Status prüfen / erneut versuchen";
if (stored.result) showResult(stored.result, false);
@ -301,8 +300,6 @@ export function mountBookingPage(
const nextAttempt = {
key: crypto.randomUUID(),
order: selected,
// Bis zur Backend-Erweiterung nur für die Wiederherstellung.
phone: phone.value,
payload: {
packageId: selected.packageId,
...(selected.variantId
@ -310,6 +307,7 @@ export function mountBookingPage(
: {}),
name: name.value,
email: email.value,
...(phone.value ? { phone: phone.value } : {}),
},
};
try {

View File

@ -112,6 +112,18 @@ function validateInput(input, key) {
) {
fields.email = "Bitte eine gültige E-Mail-Adresse eingeben.";
}
if (!customer && input.phone !== undefined) {
if (
typeof input.phone !== "string" ||
input.phone.length > 50 ||
/[\x00-\x1f\x7f]/.test(input.phone)
) {
fields.phone =
"Bitte höchstens 50 Zeichen ohne Zeilenumbrüche eingeben.";
} else if (input.phone.trim()) {
result.phone = input.phone.trim();
}
}
if (Object.keys(fields).length) {
throw new BookingError(
400,
@ -305,6 +317,7 @@ export function createBookingService({
acceptedTerms: data.acceptedTerms ?? null,
name: data.name,
email: data.email,
phone: data.customer?.phone ?? data.phone ?? "",
emailStatus: "pending",
idempotencyKey: key,
requestHash,

View File

@ -17,7 +17,8 @@ const legacyColumns = [
];
const orderColumns = [...legacyColumns, "bookedPackage"];
export const columns = [...orderColumns, "customer", "acceptedTerms"];
const customerColumns = [...orderColumns, "customer", "acceptedTerms"];
export const columns = [...customerColumns, "phone"];
const jsonColumns = ["bookedPackage", "customer", "acceptedTerms"];
// Apostroph-Präfix schützt Tabellenprogramme. Ein vorhandenes Apostroph wird
@ -47,6 +48,7 @@ export function createCsvStorage(filePath) {
columns(header) {
if (
header.join(",") !== columns.join(",") &&
header.join(",") !== customerColumns.join(",") &&
header.join(",") !== legacyColumns.join(",") &&
header.join(",") !== orderColumns.join(",")
) {

View File

@ -36,6 +36,7 @@ export function createEmailService(
const from = env.SMTP_FROM || "Tri-Hub <buchung@example.test>";
return async function sendConfirmation(booking) {
const order = booking.bookedPackage;
const phone = booking.phone || booking.customer?.phone;
const lines = [
`Hallo ${booking.name},`,
"",
@ -48,9 +49,7 @@ export function createEmailService(
: []),
`Kunde: ${booking.name}`,
`E-Mail: ${booking.email}`,
...(booking.customer?.phone
? [`Telefon: ${booking.customer.phone}`]
: []),
...(phone ? [`Telefon: ${phone}`] : []),
...(booking.customer?.notes
? [`Deine Anmerkungen: ${booking.customer.notes}`]
: []),

View File

@ -550,6 +550,12 @@
"maxLength": 254,
"example": "thomas.mueller@example.de"
},
"phone": {
"type": "string",
"maxLength": 50,
"description": "Optionale Telefonnummer bei Übergabe von name und email, ohne Steuerzeichen. Bei customer gilt customer.phone.",
"example": "+49 170 1234567"
},
"customer": {
"type": "object",
"required": ["firstName", "lastName", "email"],
@ -595,7 +601,7 @@
"required": ["name", "email"]
}
],
"description": "Kundendaten gemäß customer mit acceptedTerms=true. Bestehende Clients können alternativ name und email übergeben."
"description": "Kundendaten gemäß customer mit acceptedTerms=true. Bestehende Clients können alternativ name und email sowie optional phone übergeben."
},
"BookingConfirmation": {
"type": "object",