Telefonnummer bei Buchung übertragen und in Bestätigungsmail ergänzt
parent
9bfa8b78b0
commit
6f2b239e57
|
|
@ -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
|
POST-Anfragen benötigen `Content-Type: application/json`. Beim Buchen ist
|
||||||
zusätzlich ein `Idempotency-Key` als UUID v4 erforderlich. Das Formular sendet
|
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`,
|
Alternativ akzeptiert die API das Swagger-Objekt `customer` mit `firstName`,
|
||||||
`lastName`, `email` sowie optional `phone`, `ageGroup` und `notes`. Dabei muss
|
`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
|
`bookedPackage` speichert den Bestellstand als JSON, `customer` die optionalen
|
||||||
strukturierten Kundendaten und `acceptedTerms` die zugehörige Zustimmung.
|
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
|
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
|
Speichern erweitert. Für alte Buchungen ohne Bestellstand liefert die API
|
||||||
`bookedPackage: null`.
|
`bookedPackage: null`.
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,7 @@ export function mountBookingPage(
|
||||||
</div>
|
</div>
|
||||||
<div class="booking__field">
|
<div class="booking__field">
|
||||||
<label for="${instanceId}-booking-phone">Telefonnummer (optional)</label>
|
<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" />
|
<input id="${instanceId}-booking-phone" name="phone" type="tel" autocomplete="tel" maxlength="50" aria-describedby="${instanceId}-booking-phone-error" />
|
||||||
<p id="${instanceId}-booking-phone-hint">Die Telefonnummer wird derzeit noch nicht an die Buchungsstelle übermittelt.</p>
|
|
||||||
<p class="booking__field-error" id="${instanceId}-booking-phone-error"></p>
|
<p class="booking__field-error" id="${instanceId}-booking-phone-error"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="booking__actions">
|
<div class="booking__actions">
|
||||||
|
|
@ -252,7 +251,7 @@ export function mountBookingPage(
|
||||||
if (stored.order) renderOrder(stored.order);
|
if (stored.order) renderOrder(stored.order);
|
||||||
name.value = stored.payload.name;
|
name.value = stored.payload.name;
|
||||||
email.value = stored.payload.email;
|
email.value = stored.payload.email;
|
||||||
phone.value = stored.phone || "";
|
phone.value = stored.payload.phone || "";
|
||||||
lockFields(true);
|
lockFields(true);
|
||||||
button.textContent = "Status prüfen / erneut versuchen";
|
button.textContent = "Status prüfen / erneut versuchen";
|
||||||
if (stored.result) showResult(stored.result, false);
|
if (stored.result) showResult(stored.result, false);
|
||||||
|
|
@ -301,8 +300,6 @@ export function mountBookingPage(
|
||||||
const nextAttempt = {
|
const nextAttempt = {
|
||||||
key: crypto.randomUUID(),
|
key: crypto.randomUUID(),
|
||||||
order: selected,
|
order: selected,
|
||||||
// Bis zur Backend-Erweiterung nur für die Wiederherstellung.
|
|
||||||
phone: phone.value,
|
|
||||||
payload: {
|
payload: {
|
||||||
packageId: selected.packageId,
|
packageId: selected.packageId,
|
||||||
...(selected.variantId
|
...(selected.variantId
|
||||||
|
|
@ -310,6 +307,7 @@ export function mountBookingPage(
|
||||||
: {}),
|
: {}),
|
||||||
name: name.value,
|
name: name.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
|
...(phone.value ? { phone: phone.value } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,18 @@ function validateInput(input, key) {
|
||||||
) {
|
) {
|
||||||
fields.email = "Bitte eine gültige E-Mail-Adresse eingeben.";
|
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) {
|
if (Object.keys(fields).length) {
|
||||||
throw new BookingError(
|
throw new BookingError(
|
||||||
400,
|
400,
|
||||||
|
|
@ -305,6 +317,7 @@ export function createBookingService({
|
||||||
acceptedTerms: data.acceptedTerms ?? null,
|
acceptedTerms: data.acceptedTerms ?? null,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
|
phone: data.customer?.phone ?? data.phone ?? "",
|
||||||
emailStatus: "pending",
|
emailStatus: "pending",
|
||||||
idempotencyKey: key,
|
idempotencyKey: key,
|
||||||
requestHash,
|
requestHash,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ const legacyColumns = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const orderColumns = [...legacyColumns, "bookedPackage"];
|
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"];
|
const jsonColumns = ["bookedPackage", "customer", "acceptedTerms"];
|
||||||
|
|
||||||
// Apostroph-Präfix schützt Tabellenprogramme. Ein vorhandenes Apostroph wird
|
// Apostroph-Präfix schützt Tabellenprogramme. Ein vorhandenes Apostroph wird
|
||||||
|
|
@ -47,6 +48,7 @@ export function createCsvStorage(filePath) {
|
||||||
columns(header) {
|
columns(header) {
|
||||||
if (
|
if (
|
||||||
header.join(",") !== columns.join(",") &&
|
header.join(",") !== columns.join(",") &&
|
||||||
|
header.join(",") !== customerColumns.join(",") &&
|
||||||
header.join(",") !== legacyColumns.join(",") &&
|
header.join(",") !== legacyColumns.join(",") &&
|
||||||
header.join(",") !== orderColumns.join(",")
|
header.join(",") !== orderColumns.join(",")
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export function createEmailService(
|
||||||
const from = env.SMTP_FROM || "Tri-Hub <buchung@example.test>";
|
const from = env.SMTP_FROM || "Tri-Hub <buchung@example.test>";
|
||||||
return async function sendConfirmation(booking) {
|
return async function sendConfirmation(booking) {
|
||||||
const order = booking.bookedPackage;
|
const order = booking.bookedPackage;
|
||||||
|
const phone = booking.phone || booking.customer?.phone;
|
||||||
const lines = [
|
const lines = [
|
||||||
`Hallo ${booking.name},`,
|
`Hallo ${booking.name},`,
|
||||||
"",
|
"",
|
||||||
|
|
@ -48,9 +49,7 @@ export function createEmailService(
|
||||||
: []),
|
: []),
|
||||||
`Kunde: ${booking.name}`,
|
`Kunde: ${booking.name}`,
|
||||||
`E-Mail: ${booking.email}`,
|
`E-Mail: ${booking.email}`,
|
||||||
...(booking.customer?.phone
|
...(phone ? [`Telefon: ${phone}`] : []),
|
||||||
? [`Telefon: ${booking.customer.phone}`]
|
|
||||||
: []),
|
|
||||||
...(booking.customer?.notes
|
...(booking.customer?.notes
|
||||||
? [`Deine Anmerkungen: ${booking.customer.notes}`]
|
? [`Deine Anmerkungen: ${booking.customer.notes}`]
|
||||||
: []),
|
: []),
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,12 @@
|
||||||
"maxLength": 254,
|
"maxLength": 254,
|
||||||
"example": "thomas.mueller@example.de"
|
"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": {
|
"customer": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["firstName", "lastName", "email"],
|
"required": ["firstName", "lastName", "email"],
|
||||||
|
|
@ -595,7 +601,7 @@
|
||||||
"required": ["name", "email"]
|
"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": {
|
"BookingConfirmation": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue