diff --git a/README.md b/README.md index 77a7c5b..70a0012 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ nicht vorab leer anlegen: Das Backend erzeugt sie bei der ersten Buchung selbst. ## Beraterkontakt testen -Die [Kontakt-Testseite](http://localhost:5173/contact-test.html?berater-id=anna-mueller) +Die [Kontakt-Testseite](http://localhost:5173/contact-test.html?berater-id=relindis-agethen) öffnet das neue Popup. Kundenprüfung, Mailpit-Versand, Profil-Einbindung und Checkliste sind in [docs/contact.md](docs/contact.md) dokumentiert. diff --git a/docs/contact-integration.md b/docs/contact-integration.md index 3040cb8..8d04360 100644 --- a/docs/contact-integration.md +++ b/docs/contact-integration.md @@ -7,8 +7,7 @@ kein eigenes Kontaktformular und keine separate E-Mail-Prüfung. ## Relevante Dateien - `src/shared/berater-daten.json`: gemeinsame Datenquelle für Profile und Kontakt; - Array mit eindeutigen String-IDs, `name` und `email`. Die aktuellen Einträge - sind Demo-Daten. Eigene öffentliche Profilfelder dürfen ergänzt werden. + Array mit eindeutigen String-IDs, `vorname`, `nachname`, optionalem `spitzname` und `email`. Eigene öffentliche Profilfelder dürfen ergänzt werden. - `src/shared/berater.js`: `findAdvisor(advisorId)` liefert den passenden Eintrag. - `src/features/contact/contact-dialog.js`: exportiert `openContactDialog({ advisorId })`. - `src/features/contact/contact.css`: wird automatisch vom Popup-Modul importiert; @@ -23,9 +22,11 @@ kein eigenes Kontaktformular und keine separate E-Mail-Prüfung. ```json [ { - "id": "anna-mueller", - "name": "Anna Müller", - "email": "anna.mueller@tri-hub.de" + "id": "relindis-agethen", + "vorname": "Relindis", + "nachname": "Agethen", + "spitzname": "Lilli", + "email": "relindis.agethen@tri-hub.de" } ] ``` @@ -40,7 +41,7 @@ des Frontends. Nach Änderungen API neu starten und Frontend neu bauen. Beispielbutton (ID aus dem jeweiligen Profil): ```html - ``` @@ -79,9 +80,13 @@ Falls die Profilseite selbst Daten aus der gemeinsamen Datei benötigt: import berater from "./shared/berater-daten.json" with { type: "json" }; const profile = berater.find((entry) => entry.id === advisorId); -// profile.name und profile.email stehen bei bekannter ID zur Verfügung. +// profile.vorname, profile.nachname, profile.spitzname und profile.email sind verfügbar. ``` +Das Popup zeigt den getrimmten `spitzname`, falls nicht leer, sonst den +getrimmten `vorname`. `nachname` und das bisherige Feld `name` werden für die +Überschrift nicht verwendet. Es wird kein „kontaktieren“ angehängt. + Das Popup ermittelt den Anzeigenamen selbst und sendet beim Absenden `{ email, advisorId, reason, simulationAccepted }` an `/api/contact`. Die Profilseite muss weder Name noch E-Mail an das Popup schicken. Kundenprüfung, @@ -91,7 +96,7 @@ implementiert. Unbekannte IDs sperren das Absenden. ## Testen Mit laufendem Frontend, API und Mailpit: -`http://localhost:5173/contact-test.html?berater-id=anna-mueller`. +`http://localhost:5173/contact-test.html?berater-id=relindis-agethen`. Für die Kundenadresse eine vorhandene Testbuchung verwenden. Die Prüfung findet ausschließlich beim Absenden statt. Der Branch ist `feature/terminbuchung-kontaktfeld`; die Kontaktdateien müssen im Arbeitsstand diff --git a/docs/contact.md b/docs/contact.md index 4026dfa..dc85f84 100644 --- a/docs/contact.md +++ b/docs/contact.md @@ -10,7 +10,7 @@ npm run dev:server npm run dev ``` -Öffne . +Öffne . Der Button öffnet ein modales Fenster. Gib eine E-Mail aus einer vorhandenen Buchung in `src/server/data/bookings.csv` ein, wähle einen Kontaktgrund und bestätige den Simulationshinweis. Beim Klick auf „Anfrage senden & Termin wählen“ @@ -19,7 +19,7 @@ prüft der Server die E-Mail und versendet nur bei vorhandener Buchung. Bei Beda Nach dem Absenden müssen zwei getrennte Nachrichten in erscheinen: an die Kundenadresse und beispielsweise -`anna.mueller@tri-hub.de`. Der Browser wechselt anschließend zur vom Auftrag +`relindis.agethen@tri-hub.de`. Der Browser wechselt anschließend zur vom Auftrag vorgegebenen Microsoft-Bookings-Adresse. Es wird dort kein Termin automatisch gebucht. Die Checkliste spricht von Microsoft Forms; umgesetzt ist der konkret angegebene Bookings-Link. @@ -53,9 +53,7 @@ Popup-Styles und die lokalen DM-Sans-Schriftdateien mit. Die vollständige Einbauanleitung für die Profilseite steht in [contact-integration.md](contact-integration.md). Die gemeinsame Datenquelle `src/shared/berater-daten.json` enthält ein Array -mit eindeutigen String-IDs und den Feldern `id`, `name`, `email`. Die beiden -vorhandenen Einträge sind Demo-Daten und können durch die tatsächlichen -Profildaten ersetzt werden. Zusätzliche Profilfelder sind möglich. Die Datei +mit eindeutigen String-IDs und den Feldern `id`, `vorname`, `nachname`, `spitzname`, `email`. Die vorhandenen Einträge enthalten die Beraterprofile. Zusätzliche Profilfelder sind möglich. Die Datei wird im Frontend eingebunden: ausschließlich öffentliche Profildaten eintragen. `src/shared/berater.js` stellt `findAdvisor(advisorId)` bereit. Popup und Server @@ -65,8 +63,8 @@ nicht übernommen. E-Mail-Adressen müssen gültig sein und auf `@tri-hub.de` enden. Eine unbekannte ID wird abgelehnt und sperrt das Absenden im Popup. Die Testseite liest den URL-Parameter `berater-id`, zum Beispiel -`contact-test.html?berater-id=max-mustermann`. Ohne Parameter verwendet sie -`anna-mueller`. Nach Änderungen an der JSON den API-Server neu starten und für +`contact-test.html?berater-id=maren-hoffmann`. Ohne Parameter verwendet sie +`relindis-agethen`. Nach Änderungen an der JSON den API-Server neu starten und für das Deployment das Frontend neu bauen. Das native `dialog` hält den Tastaturfokus im Popup. Escape und Schließen @@ -90,7 +88,7 @@ Beide Endpunkte erwarten POST mit `Content-Type: application/json`, maximal ```json { "email": "kunde@example.test", - "advisorId": "anna-mueller", + "advisorId": "relindis-agethen", "reason": "coaching", "simulationAccepted": true } diff --git a/src/features/contact/contact-dialog.js b/src/features/contact/contact-dialog.js index c30192f..0defdee 100644 --- a/src/features/contact/contact-dialog.js +++ b/src/features/contact/contact-dialog.js @@ -1,4 +1,4 @@ -import { findAdvisor } from "../../shared/berater.js"; +import { findAdvisor, advisorDisplayName } from "../../shared/berater.js"; import { contactReasons, contactNotice, @@ -19,7 +19,7 @@ export function openContactDialog({ dialog.innerHTML = `

DEIN NÄCHSTER SCHRITT

-

Berater kontaktieren

+

@@ -33,8 +33,10 @@ export function openContactDialog({

`; + dialog.querySelector("#contact-title").textContent = + advisorDisplayName(advisor) || "Berater nicht gefunden"; dialog.querySelector(".contact-advisor").textContent = advisor - ? `Deine Anfrage an ${advisor.name}` + ? "Sende deine Kontaktanfrage." : "Die Berater-ID fehlt oder ist unbekannt. Bitte öffne das Fenster über ein Beraterprofil."; dialog.querySelector(".contact-consent span").textContent = contactNotice; const form = dialog.querySelector("form"); diff --git a/src/features/contact/contact-entry.js b/src/features/contact/contact-entry.js index c8b4da8..9dbf977 100644 --- a/src/features/contact/contact-entry.js +++ b/src/features/contact/contact-entry.js @@ -1,12 +1,13 @@ import { openContactDialog } from "./contact-dialog.js"; -import { findAdvisor } from "../../shared/berater.js"; +import { findAdvisor, advisorDisplayName } from "../../shared/berater.js"; import "./contact-test.css"; const advisorId = - new URLSearchParams(location.search).get("berater-id") ?? "anna-mueller"; + new URLSearchParams(location.search).get("berater-id") ?? + "relindis-agethen"; const advisor = findAdvisor(advisorId); document.querySelector("#advisor-name").textContent = advisor - ? `Dein Berater: ${advisor.name}` + ? `Dein Berater: ${advisorDisplayName(advisor)}` : "Unbekannte Berater-ID"; document .querySelector("#open-contact") diff --git a/src/server/services/contact-email.js b/src/server/services/contact-email.js index 6186150..c6adad0 100644 --- a/src/server/services/contact-email.js +++ b/src/server/services/contact-email.js @@ -1,3 +1,4 @@ +import { advisorDisplayName } from "../../shared/berater.js"; import { createMailTransport } from "./email-service.js"; import { BookingError } from "./booking-service.js"; import { contactNotice } from "../../shared/contact.js"; @@ -17,11 +18,11 @@ export function createContactEmailService(makeTransport) { [ { to: email, - text: `Deine Kontaktanfrage an ${advisor.name} ist in der Simulation eingegangen.`, + text: `Deine Kontaktanfrage an ${advisorDisplayName(advisor)} ist in der Simulation eingegangen.`, }, { to: advisor.email, - text: `Hallo ${advisor.name}, eine simulierte Kontaktanfrage von ${email} ist eingegangen.`, + text: `Hallo ${advisorDisplayName(advisor)}, eine simulierte Kontaktanfrage von ${email} ist eingegangen.`, }, ].map(async ({ to, text }) => { const result = await transport.sendMail({ diff --git a/src/server/services/contact-service.js b/src/server/services/contact-service.js index ea8ec82..6ba349f 100644 --- a/src/server/services/contact-service.js +++ b/src/server/services/contact-service.js @@ -12,8 +12,8 @@ function advisorDetails(advisorId) { ); } if ( - typeof advisor.name !== "string" || - !advisor.name.trim() || + typeof advisor.vorname !== "string" || + !advisor.vorname.trim() || typeof advisor.email !== "string" || advisor.email.length > 254 || !emailPattern.test(advisor.email) || diff --git a/src/server/swagger.json b/src/server/swagger.json index f703ecf..a380052 100644 --- a/src/server/swagger.json +++ b/src/server/swagger.json @@ -385,7 +385,7 @@ }, "advisorId": { "type": "string", - "example": "anna-mueller", + "example": "relindis-agethen", "description": "ID aus src/shared/berater-daten.json. Name und E-Mail werden serverseitig ausschließlich dort nachgeschlagen." } } diff --git a/src/shared/berater-daten.json b/src/shared/berater-daten.json index c539096..28a9b44 100644 --- a/src/shared/berater-daten.json +++ b/src/shared/berater-daten.json @@ -16,7 +16,10 @@ "Leistungssport-Perspektive", "Wettkampforientierte Entwicklung" ], - "bildquelle": "" + "bildquelle": "", + "vorname": "Relindis", + "nachname": "Agethen", + "spitzname": "Lilli" }, { "id": "maren-hoffmann", @@ -33,6 +36,9 @@ "Wettkampfverpflegung", "Alltagstaugliche Ernährungsroutinen" ], - "bildquelle": "" + "bildquelle": "", + "vorname": "Maren", + "nachname": "Hoffmann", + "spitzname": "" } ] diff --git a/src/shared/berater.js b/src/shared/berater.js index 606e86f..f724079 100644 --- a/src/shared/berater.js +++ b/src/shared/berater.js @@ -5,3 +5,7 @@ export function findAdvisor(advisorId) { ? berater.find((entry) => entry.id === advisorId) : undefined; } + +export function advisorDisplayName(advisor) { + return advisor?.spitzname?.trim() || advisor?.vorname?.trim() || ""; +} diff --git a/src/tests/browser/contact.spec.js b/src/tests/browser/contact.spec.js index e869afe..abc6203 100644 --- a/src/tests/browser/contact.spec.js +++ b/src/tests/browser/contact.spec.js @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto"; import { contactBookingUrl } from "../../shared/contact.js"; async function open(page) { - await page.goto("/contact-test.html?berater-id=max-mustermann"); + await page.goto("/contact-test.html?berater-id=maren-hoffmann"); await page .getByRole("button", { name: "Berater kontaktieren", exact: true }) .click(); @@ -20,7 +20,7 @@ test("Popup prüft E-Mail beim Absenden und leitet nur bekannte Kunden weiter", }); expect(booking.ok()).toBeTruthy(); await open(page); - await expect(page.getByRole("dialog")).toContainText("Max Mustermann"); + await expect(page.getByRole("dialog")).toContainText("Maren"); const submit = page.getByRole("button", { name: "Anfrage senden" }); await expect( page.getByRole("button", { name: "E-Mail prüfen" }), @@ -42,7 +42,7 @@ test("Popup prüft E-Mail beim Absenden und leitet nur bekannte Kunden weiter", request.url().endsWith("/api/contact"), ); await submit.click(); - expect((await sentRequest).postDataJSON().advisorId).toBe("max-mustermann"); + expect((await sentRequest).postDataJSON().advisorId).toBe("maren-hoffmann"); await expect(page).toHaveURL(contactBookingUrl); }); diff --git a/src/tests/contact.test.js b/src/tests/contact.test.js index 63e3a97..ee6bc34 100644 --- a/src/tests/contact.test.js +++ b/src/tests/contact.test.js @@ -44,7 +44,7 @@ async function fixture(t, options = {}) { } const input = { email: "kunde@example.test", - advisorId: "anna-mueller", + advisorId: "relindis-agethen", reason: "coaching", simulationAccepted: true, }; @@ -65,14 +65,14 @@ test("CSV-Abgleich normalisiert E-Mail; Versand erhält Berater und festen Redir assert.equal(result.status, 201); assert.equal(result.body.redirectUrl, contactBookingUrl); assert.equal(result.body.simulated, true); - assert.equal(f.sent[0].advisor.email, "anna.mueller@tri-hub.de"); - assert.equal(f.sent[0].advisor.name, "Anna Müller"); + assert.equal(f.sent[0].advisor.email, "relindis.agethen@tri-hub.de"); + assert.equal(f.sent[0].advisor.vorname, "Relindis"); const second = await f.post("/api/contact", { ...input, - advisorId: "max-mustermann", + advisorId: "maren-hoffmann", }); assert.equal(second.status, 201); - assert.equal(f.sent[1].advisor.email, "max.mustermann@tri-hub.de"); + assert.equal(f.sent[1].advisor.email, "maren.hoffmann@tri-hub.de"); assert.deepEqual(await f.storage.readAll(), before); }); @@ -147,7 +147,7 @@ test("Zwei getrennte simulierte Bestätigungen; Teilfehler verhindern Erfolg", a assert.equal((await f.post("/api/contact", input)).status, 201); assert.deepEqual( messages.map((m) => m.to), - [input.email, "anna.mueller@tri-hub.de"], + [input.email, "relindis.agethen@tri-hub.de"], ); for (const message of messages) { assert.match(message.text, /Simulation/); @@ -163,3 +163,27 @@ test("Zwei getrennte simulierte Bestätigungen; Teilfehler verhindern Erfolg", a assert.equal(result.status, 502); assert.equal(result.body.redirectUrl, undefined); }); + +test("Anzeigename bevorzugt Spitzname, sonst Vorname, ohne Nachnamen", async () => { + const { advisorDisplayName } = await import("../shared/berater.js"); + assert.equal( + advisorDisplayName({ + spitzname: " Lilli ", + vorname: "Relindis", + nachname: "Agethen", + }), + "Lilli", + ); + assert.equal( + advisorDisplayName({ + spitzname: " ", + vorname: "Maren", + nachname: "Hoffmann", + }), + "Maren", + ); + assert.equal( + advisorDisplayName({ vorname: "Maren", nachname: "Hoffmann" }), + "Maren", + ); +});