diff --git a/docs/booking.md b/docs/booking.md
index 884e0d4..c6788e1 100644
--- a/docs/booking.md
+++ b/docs/booking.md
@@ -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`.
diff --git a/src/features/booking/booking-page.js b/src/features/booking/booking-page.js
index 546c653..4d97d52 100644
--- a/src/features/booking/booking-page.js
+++ b/src/features/booking/booking-page.js
@@ -102,8 +102,7 @@ export function mountBookingPage(
@@ -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 {
diff --git a/src/server/services/booking-service.js b/src/server/services/booking-service.js
index bcc0dcc..f62c6d3 100644
--- a/src/server/services/booking-service.js
+++ b/src/server/services/booking-service.js
@@ -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,
diff --git a/src/server/services/csv-storage.js b/src/server/services/csv-storage.js
index 0ffbf22..2236e99 100644
--- a/src/server/services/csv-storage.js
+++ b/src/server/services/csv-storage.js
@@ -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(",")
) {
diff --git a/src/server/services/email-service.js b/src/server/services/email-service.js
index 6a55fca..a3415ec 100644
--- a/src/server/services/email-service.js
+++ b/src/server/services/email-service.js
@@ -36,6 +36,7 @@ export function createEmailService(
const from = env.SMTP_FROM || "Tri-Hub ";
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}`]
: []),
diff --git a/src/server/swagger.json b/src/server/swagger.json
index 5db3c03..409c41d 100644
--- a/src/server/swagger.json
+++ b/src/server/swagger.json
@@ -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",