diff --git a/README.md b/README.md index 0ca7144..70a0012 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,12 @@ angezeigt. Der Ablauf bleibt eine Simulation ohne Zahlung oder verbindliche Beratung. Buchungen liegen lokal in `src/server/data/bookings.csv`. Diese Datei 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=relindis-agethen) +öffnet das neue Popup. Kundenprüfung, Mailpit-Versand, Profil-Einbindung und +Checkliste sind in [docs/contact.md](docs/contact.md) dokumentiert. + ## Prüfen ```bash diff --git a/berater.html b/berater.html new file mode 100644 index 0000000..722f850 --- /dev/null +++ b/berater.html @@ -0,0 +1,31 @@ + + + + + + + Beraterprofil · Tri-Hub + + + + + + +
+
+
+
+

PERSÖNLICH FÜR DICH DA

+

Lerne deine Ernährungsberater kennen.

+

Hinter einer guten Ernährungsstrategie stehen Menschen, die zuhören und ihre Erfahrung in deinen Alltag und dein Training einbringen.

+
+
+

Beraterprofile werden geladen …

+
+
+
+
+ + + + diff --git a/contact-test.html b/contact-test.html new file mode 100644 index 0000000..9705d86 --- /dev/null +++ b/contact-test.html @@ -0,0 +1,26 @@ + + + + + + Berater kontaktieren – Tri-Hub Testseite + + +
+

TRI-HUB · KONTAKT-DEMO

+

Dein direkter Draht zur Beratung

+

+ +

+ Testseite für das spätere Beraterprofil. Nutze eine + E-Mail-Adresse aus einer vorhandenen Testbuchung. +

+
+ + + diff --git a/docs/contact-integration.md b/docs/contact-integration.md new file mode 100644 index 0000000..8d04360 --- /dev/null +++ b/docs/contact-integration.md @@ -0,0 +1,103 @@ +# Übergabe an die KI der Beraterprofilseite + +Binde das vorhandene Kontakt-Popup in die Kontaktbuttons der Beraterprofile +ein. Übergib ausschließlich die Berater-ID als `advisorId`. Implementiere +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, `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; + die Schriftdateien liegen unter `public/fonts/`. +- `src/features/contact/contact-entry.js` und `contact-test.html`: Beispielintegration. +- `src/server/services/contact-service.js`: prüft Kunden-E-Mail und Berater-ID, + lädt die Empfängeradresse serverseitig aus der JSON. +- `src/server/swagger.json`: API-Vertrag für `POST /api/contact`. + +## Datenvertrag + +```json +[ + { + "id": "relindis-agethen", + "vorname": "Relindis", + "nachname": "Agethen", + "spitzname": "Lilli", + "email": "relindis.agethen@tri-hub.de" + } +] +``` + +Die Profilseite und das Popup müssen dieselben IDs verwenden. E-Mail-Adressen +werden explizit gepflegt und nicht aus Namen erzeugt. Sie müssen auf `@tri-hub.de` +enden. Keine vertraulichen Informationen in die JSON schreiben: Sie ist Teil +des Frontends. Nach Änderungen API neu starten und Frontend neu bauen. + +## Einzubindender Code + +Beispielbutton (ID aus dem jeweiligen Profil): + +```html + +``` + +Im JavaScript-Modul der Profilseite, einmalig registrieren: + +```js +// Importpfad relativ zu dieser Datei anpassen. +import { openContactDialog } from "./src/features/contact/contact-dialog.js"; + +document.addEventListener("click", (event) => { + const button = event.target.closest("button[data-berater-id]"); + if (!button) return; + openContactDialog({ advisorId: button.dataset.beraterId }); +}); +``` + +Bei dynamischer Erzeugung des Buttons die ID aus dem Profil setzen: + +```js +button.dataset.beraterId = profile.id; +``` + +Alternativ direkt am einzelnen Button binden (nicht zusätzlich zur Delegation): + +```js +button.addEventListener("click", () => { + openContactDialog({ advisorId: profile.id }); +}); +``` + +Falls die Profilseite selbst Daten aus der gemeinsamen Datei benötigt: + +```js +// Beispiel für ein Modul direkt unter src/; Pfad ggf. anpassen. +import berater from "./shared/berater-daten.json" with { type: "json" }; + +const profile = berater.find((entry) => entry.id === advisorId); +// 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, +Simulation, Mailpit-Versand und Microsoft-Bookings-Weiterleitung sind bereits +implementiert. Unbekannte IDs sperren das Absenden. + +## Testen + +Mit laufendem Frontend, API und Mailpit: +`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 +der Profilseite vorhanden sein. diff --git a/docs/contact.md b/docs/contact.md new file mode 100644 index 0000000..dc85f84 --- /dev/null +++ b/docs/contact.md @@ -0,0 +1,154 @@ +# Beraterkontakt (Simulation) + +## Testseite starten + +Wie bei der Buchung in drei Terminals starten: + +```sh +npm run dev:mail +npm run dev:server +npm run dev +``` + +Ö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“ +prüft der Server die E-Mail und versendet nur bei vorhandener Buchung. Bei Bedarf zuerst +über die bestehende Angebotsseite eine Testbuchung anlegen. + +Nach dem Absenden müssen zwei getrennte Nachrichten in + erscheinen: an die Kundenadresse und beispielsweise +`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. + +## Checkliste + +- [x] Kontaktgrund auswählbar: Coaching, Ernährungsplan, Wettkampfverpflegung, + Regeneration, Paketfragen, Termin und Sonstiges. +- [x] Getrennte Eingangsbestätigungen an Kunde und Berater, ausschließlich + simuliert über Mailpit, mit Kontaktgrund und Simulationshinweis. +- [x] Weiterleitung zur vorhandenen Microsoft-Bookings-Adresse nach Annahme + beider Nachrichten durch Mailpit; zusätzlicher Link als Rückfalloption. +- [x] Kontakt nur nach erfolgreichem E-Mail-Abgleich mit `bookings.csv`; + der Server prüft beim tatsächlichen Absenden erneut. + +## Einbindung in Beraterprofile + +Die Profilseite importiert `openContactDialog` aus +`src/features/contact/contact-dialog.js` und übergibt ausschließlich die ID: + +```js +import { openContactDialog } from "./src/features/contact/contact-dialog.js"; + +button.addEventListener("click", () => { + openContactDialog({ advisorId: profile.id }); +}); +``` + +Den Importpfad relativ zur einbindenden Datei anpassen. Das Modul bringt die +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`, `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 +verwenden dieselbe Zuordnung. Der Server holt die Empfängeradresse ausschließlich +aus dieser JSON; vom Browser gesendete Namen oder Empfängeradressen werden +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=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 +bringen ihn zum auslösenden Button zurück. Während einer Anfrage ist Schließen +kurz gesperrt, damit der Versand nicht unbemerkt weiterläuft. Das Fenster ist +auf schmalen Bildschirmen scrollbar. Die E-Mail wird bei jedem Absenden geprüft; +ein separater Prüfbutton ist nicht erforderlich. Es gibt keine zusätzlichen +Laufzeitabhängigkeiten. + +## API + +Der vollständige OpenAPI-Vertrag steht in `src/server/swagger.json`. +Beide Endpunkte erwarten POST mit `Content-Type: application/json`, maximal +8 KiB. Fehler enthalten `error.code` und `error.message`. + +| Endpunkt | Eingabe | Erfolg | +| ----------------------- | --------------------------------------------------- | ---------------------------------------------------------- | +| `/api/contact/validate` | `{ "email": "kunde@example.test" }` | 200: `{ "valid": true, "email": "kunde@example.test" }` | +| `/api/contact` | E-Mail, Berater-ID, Grund, Zustimmung (siehe unten) | 201: `simulated`, `emailStatus: "accepted"`, `redirectUrl` | + +```json +{ + "email": "kunde@example.test", + "advisorId": "relindis-agethen", + "reason": "coaching", + "simulationAccepted": true +} +``` + +400 bedeutet ungültige Eingaben, 403 keine passende Buchung, 405 falsche Methode, +413 zu große Anfrage, 415 falscher Medientyp, 503 nicht lesbarer CSV-Speicher oder ungültige Beraterkontaktdaten. +502 bedeutet, dass mindestens eine Mailannahme fehlgeschlagen oder unklar ist; +der Browser leitet dann nicht weiter. 500 bezeichnet einen unerwarteten Fehler. +Die CSV wird ausschließlich gelesen und bleibt unverändert. Der separate +Validierungsendpunkt bleibt für API-Nutzer verfügbar; das Popup verwendet +ausschließlich `/api/contact` mit integrierter Kundenprüfung. + +## Grenzen der Simulation + +Der E-Mail-Abgleich ignoriert Groß-/Kleinschreibung und äußere Leerzeichen. +Er bestätigt einen Eintrag in der Buchungsdatei, nicht den Besitz des Postfachs. +Die Prüfantwort ist kein Anmeldetoken und gibt keine Buchungsdetails zurück. +Vor einem öffentlichen Produktiveinsatz wären eine Anmeldung oder Bestätigung +per E-Mail sowie ein Schutz vor automatisierten Adressabfragen erforderlich. + +Kontaktmails verwenden fest `127.0.0.1:1025`, unabhängig von einer eventuell +externen SMTP-Konfiguration des Buchungsablaufs. Mailpit muss deshalb auf +demselben Host laufen. Kunden- und Berateradresse sind simulierte Empfänger; +es erfolgt kein externer Versand. SMTP-Annahme ist keine reale Zustellbestätigung. + +Kontaktanfragen werden nicht dauerhaft gespeichert und haben keinen +Idempotenzschlüssel. Doppelklicks sind während des Versands gesperrt. Bei +Verbindungsabbrüchen oder Teilfehlern kann bereits eine Nachricht vorliegen: +vor manuellem Wiederholen Mailpit prüfen. Es gibt keinen automatischen Neuversand. + +## Prüfungen + +- `npm test`: API, CSV-Abgleich, erneute Validierung beim Absenden, ungültige + Eingaben, SMTP-Empfänger und Teilfehler; isolierte temporäre CSV-Dateien. +- `npm run test:browser`: vollständiger Ablauf einschließlich abgefangener + externer Weiterleitung, unbekannter Adresse, geänderter E-Mail, Versandfehler, + mobiler Darstellung, Fokus und Escape; zusätzlich bestehende Buchungstests. +- `npm run build` und `git diff --check`. +- Lokaler SMTP-Smoke-Test mit Mailpit: beide Empfänger und Simulationshinweis + in den empfangenen Nachrichten geprüft. + +Chromium muss für Playwright installiert sein (`npx playwright install chromium`). +In dieser Arbeitsumgebung liegt der Testbrowser unter `/tmp/trihub-playwright`; +hier mit `PLAYWRIGHT_BROWSERS_PATH=/tmp/trihub-playwright npm run test:browser` starten. +Der globale Formatcheck hat bereits bestehende Abweichungen in `src/main.js` +und `src/components/navigationsbar/README.md`; die Kontaktdateien sind formatiert. +Der bestehende Gesamt-Build meldet außerdem fehlende ältere Schriftdateien aus +dem globalen Stylesheet. Das Kontakt-Popup verwendet eigene lokale Schriftdateien. + +### Ergebnis dieser Umsetzung + +31 API-/Servicetests und alle vier Kontakt-Browsertests bestanden. +Build, lokale Mailpit-Prüfung und Formatierung der geänderten Dateien bestanden. +Die bestehenden Buchungs-Browsertests sind teilweise nicht mehr an das schon +vorhandene Buchungs-Popup angepasst: Sie suchen Formularfelder, ohne vorher +„Jetzt buchen“ zu klicken. Weitere Bestandsfälle erwarten einen nicht mehr vorhandenen Startseiten-Link +oder eine Seitennavigation, wo bereits ein Popup geöffnet wird. Beim ursprünglichen Gesamtlauf bestanden 7 von 15 Browsertests; die 8 Fehler +betrafen ausschließlich die unveränderten Buchungstests. Die gezielten +Kontakttests werden nach Anpassungen separat ausgeführt. +`git diff --check` ist für die eigenen Änderungen sauber; die zuvor bereits +geänderte `bookings.csv` erzeugt separat CRLF-Whitespace-Hinweise und wurde +von dieser Umsetzung nicht verändert. diff --git a/public/fonts/DM-Sans-LICENSE.txt b/public/fonts/DM-Sans-LICENSE.txt new file mode 100644 index 0000000..917aaa6 --- /dev/null +++ b/public/fonts/DM-Sans-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts) DMSans-Italic[opsz,wght].ttf: Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/dm-sans-latin-400-normal.woff2 b/public/fonts/dm-sans-latin-400-normal.woff2 new file mode 100644 index 0000000..0b8bc55 Binary files /dev/null and b/public/fonts/dm-sans-latin-400-normal.woff2 differ diff --git a/public/fonts/dm-sans-latin-500-normal.woff2 b/public/fonts/dm-sans-latin-500-normal.woff2 new file mode 100644 index 0000000..48e1612 Binary files /dev/null and b/public/fonts/dm-sans-latin-500-normal.woff2 differ diff --git a/public/fonts/dm-sans-latin-700-normal.woff2 b/public/fonts/dm-sans-latin-700-normal.woff2 new file mode 100644 index 0000000..26edc56 Binary files /dev/null and b/public/fonts/dm-sans-latin-700-normal.woff2 differ diff --git a/src/components/navigationsbar/navigation.html b/src/components/navigationsbar/navigation.html index 37097df..1d4044c 100644 --- a/src/components/navigationsbar/navigation.html +++ b/src/components/navigationsbar/navigation.html @@ -1,7 +1,7 @@