From 6f2b239e57ee6849b0bc3657e4172bfd773c42c0 Mon Sep 17 00:00:00 2001 From: Marvin Gemlin Date: Mon, 28 Sep 2026 16:48:35 +0000 Subject: [PATCH] =?UTF-8?q?Telefonnummer=20bei=20Buchung=20=C3=BCbertragen?= =?UTF-8?q?=20und=20in=20Best=C3=A4tigungsmail=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/booking.md | 5 ++++- src/features/booking/booking-page.js | 8 +++----- src/server/services/booking-service.js | 13 +++++++++++++ src/server/services/csv-storage.js | 4 +++- src/server/services/email-service.js | 5 ++--- src/server/swagger.json | 8 +++++++- 6 files changed, 32 insertions(+), 11 deletions(-) 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(
- -

Die Telefonnummer wird derzeit noch nicht an die Buchungsstelle übermittelt.

+

@@ -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",