diff --git a/.gitignore b/.gitignore index e872025..b4f54fd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ Thumbs.db # Private Buchungsdaten und lokale Testergebnisse src/server/data/* !src/server/data/.gitkeep +!src/server/data/contact-requests.csv playwright-report/ test-results/ 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/berater-tests.md b/docs/berater-tests.md new file mode 100644 index 0000000..e3b1d5a --- /dev/null +++ b/docs/berater-tests.md @@ -0,0 +1,36 @@ +# Beraterprofile und Bildergalerie testen + +Die Berater-Tests laufen auf dem Branch `feature/beraterbilder`. Dieser enthält auch die Profiländerungen aus `feature/beraterprofile`. + +## Node-Tests + +```sh +npm test +``` + +Die Node-Suite prüft die Beraterdaten, eindeutige IDs, Kontaktanzeigenamen, vorhandene Porträt- und Aktionsbilder sowie den Navigationslink zur eigenständigen Profilseite. + +## Browser-Tests + +```sh +npm run test:browser -- src/tests/browser/berater.spec.js +``` + +Die Browser-Tests prüfen die Profilkarten, den Kontakt-Dialog, die Bildgröße und das manuelle Wechseln der Galerie mit Pfeilen und Positionspunkten. Playwright startet dafür standardmäßig die Testserver auf den Ports 3000 und 5173. Falls diese Ports bereits von den lokalen Projektservern belegt sind, kann Playwright sie wiederverwenden: + +```sh +PW_REUSE_EXISTING=1 npm run test:browser -- src/tests/browser/berater.spec.js +``` + +PowerShell: + +```powershell +$env:PW_REUSE_EXISTING = "1" +npm run test:browser -- src/tests/browser/berater.spec.js +``` + +Beim ersten Browser-Testlauf kann Playwright Chromium benötigen: + +```sh +npx playwright install chromium +``` 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/docs/nutrition-contact-form.md b/docs/nutrition-contact-form.md new file mode 100644 index 0000000..32b7911 --- /dev/null +++ b/docs/nutrition-contact-form.md @@ -0,0 +1,21 @@ +# Kontaktformular der Ernährungsseite + +Das Formular in `index.html` sendet seine Eingaben als JSON an `POST /api/nutrition-contact`. +Der Server validiert Name, E-Mail, Betreff und Nachricht, speichert die Anfrage in +`src/server/data/contact-requests.csv` und sendet anschließend zwei E-Mails über +Mailpit auf `127.0.0.1:1025`: + +- eine Eingangsbestätigung an die eingegebene E-Mail-Adresse; +- beide Nachrichten mit dem Absender `Tri-Hub Ernährung `; +- die vollständige Kontaktanfrage an die Mailpit-Testadresse + `ernaehrung@tri-hub.de`; die SMTP-Verbindung ist fest auf den lokalen + Mailpit-Server gesetzt, daher wird diese Nachricht nur simuliert und nicht + an den echten Mailserver zugestellt. + +Die Mailpit-Weboberfläche ist lokal unter `http://127.0.0.1:8025` erreichbar. +Zum Entwickeln Mailpit mit `npm run dev:mail`, den API-Server mit +`npm run dev:server` und Vite mit `npm run dev` starten. + +Die CSV-Datei wird beim ersten erfolgreichen Absenden angelegt. Bei einem +Mailfehler bleibt die Anfrage gespeichert; die API meldet, dass der Versand +nicht vollständig bestätigt werden konnte. diff --git a/index.html b/index.html index 1e74bb1..b658775 100644 --- a/index.html +++ b/index.html @@ -22,8 +22,8 @@ :root { --page-bg: #0d0d1a; --page-bg-2: #1a1a2e; - --green: #10b981; - --green-dark: #059669; + --brand: #53d5cd; + --brand-hover: #36b8b1; --white: #ffffff; --light: #f8fafc; --muted: #94a3b8; @@ -48,12 +48,12 @@ background: radial-gradient( circle at 12% 0%, - rgba(16, 185, 129, 0.09), + rgba(83, 213, 205, 0.09), transparent 30% ), radial-gradient( circle at 88% 26%, - rgba(16, 185, 129, 0.055), + rgba(83, 213, 205, 0.055), transparent 27% ), linear-gradient(145deg, var(--page-bg), var(--page-bg-2)); @@ -115,7 +115,7 @@ } .brand span { - color: var(--green); + color: var(--brand); } .nav-links { @@ -138,14 +138,14 @@ .nav-cta { padding: 11px 16px; - border: 1px solid rgba(16, 185, 129, 0.42); + border: 1px solid rgba(83, 213, 205, 0.42); border-radius: 999px; color: var(--white) !important; - background: rgba(16, 185, 129, 0.07); + background: rgba(83, 213, 205, 0.07); } .nav-cta:hover { - background: rgba(16, 185, 129, 0.14); + background: rgba(83, 213, 205, 0.14); } .container { @@ -155,7 +155,7 @@ .eyebrow { margin-bottom: 18px; - color: var(--green); + color: var(--brand); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.18em; @@ -180,7 +180,7 @@ } .hero h1 .accent { - color: var(--green); + color: var(--brand); } .hero-lead { @@ -220,12 +220,12 @@ .button-primary { color: #06130f; - background: var(--green); - box-shadow: 0 14px 32px rgba(16, 185, 129, 0.18); + background: var(--brand); + box-shadow: 0 14px 32px rgba(83, 213, 205, 0.18); } .button-primary:hover { - background: #34d399; + background: #75e2dc; } .button-secondary { @@ -235,7 +235,7 @@ } .button-secondary:hover { - border-color: rgba(16, 185, 129, 0.55); + border-color: rgba(83, 213, 205, 0.55); } .hero-card { @@ -251,7 +251,7 @@ background: radial-gradient( circle at 72% 18%, - rgba(16, 185, 129, 0.2), + rgba(83, 213, 205, 0.2), transparent 32% ), linear-gradient( @@ -269,18 +269,18 @@ height: 230px; right: -75px; top: -75px; - border: 1px solid rgba(16, 185, 129, 0.24); + border: 1px solid rgba(83, 213, 205, 0.24); border-radius: 50%; box-shadow: - 0 0 0 34px rgba(16, 185, 129, 0.035), - 0 0 0 68px rgba(16, 185, 129, 0.02); + 0 0 0 34px rgba(83, 213, 205, 0.035), + 0 0 0 68px rgba(83, 213, 205, 0.02); } .hero-card-mark { position: absolute; top: 42px; left: 38px; - color: rgba(16, 185, 129, 0.88); + color: rgba(83, 213, 205, 0.88); font-size: 4.5rem; line-height: 1; } @@ -342,13 +342,13 @@ .topic:hover { transform: translateY(-4px); - border-color: rgba(16, 185, 129, 0.34); + border-color: rgba(83, 213, 205, 0.34); background: var(--panel-strong); } .topic-number { margin-bottom: 70px; - color: var(--green); + color: var(--brand); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.14em; @@ -407,8 +407,8 @@ height: 10px; margin-top: 7px; border-radius: 50%; - background: var(--green); - box-shadow: 0 0 0 5px rgba(16, 185, 129, 0.08); + background: var(--brand); + box-shadow: 0 0 0 5px rgba(83, 213, 205, 0.08); } .review-section { @@ -470,6 +470,7 @@ } input[type="text"], + input[type="email"], textarea, select { width: 100%; @@ -485,6 +486,7 @@ } input[type="text"], + input[type="email"], select { height: 48px; padding: 0 14px; @@ -504,8 +506,8 @@ input:focus, textarea:focus, select:focus { - border-color: rgba(16, 185, 129, 0.75); - box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); + border-color: rgba(83, 213, 205, 0.75); + box-shadow: 0 0 0 3px rgba(83, 213, 205, 0.1); } .rating-input { @@ -533,7 +535,7 @@ .rating-input label:hover, .rating-input label:hover ~ label, .rating-input input:checked ~ label { - color: var(--green); + color: var(--brand); } .consent { @@ -547,7 +549,7 @@ .consent input { margin-top: 4px; - accent-color: var(--green); + accent-color: var(--brand); } .review-form .button { @@ -557,10 +559,25 @@ cursor: pointer; } + #contactMessageHint.is-valid { + color: var(--brand); + } + + .field-error { + display: block; + min-height: 0; + color: #fca5a5; + font-size: 0.78rem; + } + + .form-status.is-error { + color: #fca5a5; + } + .form-status { min-height: 20px; margin-top: 12px; - color: var(--green); + color: var(--brand); font-size: 0.8rem; } @@ -594,7 +611,7 @@ flex: 0 0 auto; border-radius: 50%; color: #06130f; - background: var(--green); + background: var(--brand); font-size: 0.72rem; font-weight: 800; } @@ -613,7 +630,7 @@ } .stars { - color: var(--green); + color: var(--brand); letter-spacing: 0.08em; white-space: nowrap; } @@ -1102,6 +1119,117 @@ + + +
+
+
+
+
KONTAKT AUFNEHMEN
+

+ Schreib uns. +

+
+ +

+ Du hast Fragen oder möchtest einen Termin + vereinbaren? Schreib uns einfach. Wir melden uns + schnellstmöglich bei dir. +

+
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + + Mindestens 10 Zeichen erforderlich. + +
+ *Pflichtfelder +
+ + + +
+
+
+