# Journal 2026-09-03

## Bewegungszentrum: Geschwisterkinder in Kapazität, Buchungsdetails und Export

**Was:** Geschwisterkinder wurden bei BZ-Kursen weder erfasst noch in der Teilnehmerzahl geführt.

**Warum:** Seit `Course::usesChildBookingForm()` für **alle** BZ-Kurse (außer Familien-Bewegungszeit)
gilt, wird nur noch das Kind-Formular gerendert. Dessen Geschwister-Block war an
`course.siblingPrices` gekoppelt — bei allen `kids_abo`-Kursen und Workshops ist `sibling_prices = []`,
also fehlte die Eingabe komplett. Ohne erfasste Geschwister zählen sie auch nicht in
`CourseBooking::getTotalParticipants()` und fehlen in Detailansicht/Export.

**Änderungen:**

- `templates/course_booking/step4_personal_data.html.twig`
  - Geschwister-Block im Kind-Formular an `course.allowSiblings` gekoppelt (statt an `siblingPrices`).
  - Anzahl der Geschwisterkinder wird durch die freien Plätze begrenzt (`availablePlaces - 1`);
    bei Kursen mit Warteliste kein Hard-Limit, stattdessen Hinweis, dass die Anmeldung auf die
    Warteliste kommt. Limit zählt jetzt die tatsächlich vorhandenen Karten (Entfernen wirkte vorher nicht).
  - Kostenzusammenfassung berücksichtigt `priceMonthly` (mit Zusatz „pro Monat“).
- `src/Controller/CourseBookingController.php`
  - Geschwister werden nur bei `allowSiblings` übernommen, leere Einträge (`normalizeSiblings()`)
    verworfen — beides vor der Kapazitätsprüfung, damit keine Überbuchung entsteht.
  - Preislogik über `resolveSiblingPrice()` vereinheitlicht; Kind-Formular nutzt wieder
    `price ?? priceMonthly` (Abo-Buchungen wurden zuletzt mit 0,00 € gespeichert).
  - **Keine automatische Preis-Herleitung:** ohne hinterlegte `siblingPrices` fällt kein
    Zusatzbeitrag an (Entscheidung: Preise werden im Admin nachgepflegt). Deshalb warnt
    `templates/admin/bz_courses/index.html.twig` beim Kurs-Bearbeiten, wenn „Geschwisterkinder
    erlauben“ aktiv ist, aber kein Preis > 0 hinterlegt wurde.
- `src/Controller/Admin/BzDashboardController.php` — „Kurse mit freien Plätzen“ zählt Teilnehmer
  (inkl. Geschwister und Teamgröße) statt Buchungen. Beispiel: Herbst-Ferienspiele Woche 2 vorher
  12/20, korrekt 17/20.
- `src/Controller/Admin/CourseBookingAdminController.php` — CSV-Export um „Teilnehmer (gesamt)“,
  „Anzahl Geschwisterkinder“ und „Geschwisterkinder (Namen)“ erweitert; `apiGet` liefert
  `totalParticipants` und `teamSize`.
- `templates/admin/course_bookings/index.html.twig` — Buchungsdetails zeigen belegte Plätze
  (Hauptkind + Geschwister bzw. Teamgröße) und die Anzahl im Geschwister-Block.
- `templates/admin/bz_courses/details.html.twig` — Statistik „Belegte Plätze (inkl. Geschwister)“.
- `src/MessageHandler/SendCourseBookingEmailMessageHandler.php` — Geschwisterliste über
  `getSiblingLines()` (inkl. `raw`-Einträge, ungültige Geburtsdaten werden nicht mehr als „heute“
  ausgegeben) und zusätzlich im Block „übermittelte Daten“.

**Hinweis:** Bestandsbuchungen mit Geschwisterkindern im Notizfeld sind nicht rückwirkend korrigiert.

## Nachtrag: Geschwister-Details in allen Admin-Ansichten

**Was:** Prüfung, ob alle erfassten Geschwister-Angaben im Admin und im Export sichtbar sind.

In der Datenbank existieren drei Datenformen (`siblings`-JSON): Kind-Formular (Name, Geburtsdatum,
Vorerkrankungen, Allergien/Medikamente, Ernährung, Foto/Video-Einwilligung), `raw`-Freitext
(Familien-Bewegungszeit + manuelle Admin-Buchung) und die Altform (nur Name + Geburtsdatum).
Alle drei werden in `/admin/course-bookings` (Detail-Modal + CSV) und in den Mails korrekt aufgelöst.

**Lücken behoben:**

- `templates/admin/bz_courses/waitlist.html.twig` — Wartelisten-Detail-Modal zeigte nur
  „Teilnehmer: N“. Jetzt Geschwister-Block mit Name, Geburtsdatum, Vorerkrankungen,
  Allergien/Medikamente, Ernährung und Foto/Video-Einwilligung (relevant, weil alle
  `kids_abo`-Kurse über die Warteliste laufen).
- `src/Controller/Admin/CourseBookingAdminController.php` — im CSV wurde eine **nicht beantwortete**
  Foto/Video-Einwilligung des Geschwisterkindes als „Nein“ exportiert (`array_key_exists` auf `null`);
  jetzt wird sie weggelassen, analog zum Detail-Modal.

**Offen (Erfassung, nicht Anzeige):** Für Geschwisterkinder wird kein Geschlecht erfasst
(nur für das Hauptkind, `childGender`).

## Nachtrag: „Plätze: 5 von 20" war irreführend

**Was:** In der Kursübersicht stand „Plätze: X von Y", wobei X die **freien** Plätze sind — gelesen
wurde es als „X belegt". Bei Herbst-Ferienspiele Woche 1 (20 Plätze): 10 aktive Buchungen mit
14 Teilnehmern (4 × 1 Geschwisterkind) + 1 extern vergebener Platz = 15 belegt, also 5 frei.
Die extern vergebenen Plätze waren in der Übersicht gar nicht sichtbar.

**Änderungen:**

- `src/Entity/Course.php` — neue Methode `getBookedPlaces()` (Teilnehmer inkl. Geschwister/Teamgröße
  plus extern vergebene Plätze); `getAvailablePlaces()` nutzt sie jetzt. Bewusst **ohne** das Flag
  „als ausgebucht anzeigen", damit die echte Belegung sichtbar bleibt.
- `src/Controller/Admin/CourseAdminController.php` — `apiList` liefert `bookedPlaces` und
  `showAsFullyBooked`.
- `templates/admin/bz_courses/index.html.twig` — Spalte heißt „Freie Plätze“, Anzeige
  „5 von 20 frei“ + Zusatzzeile „15 belegt, davon 1 extern“; bei manuell als ausgebucht markierten
  Kursen (alle `kids_abo`) wird das explizit ausgewiesen, sonst wirkt „0 frei“ falsch.
- `templates/admin/bz_courses/details.html.twig` — gleiche Darstellung; die zuvor per Twig-`reduce`
  berechnete Statistik nutzt jetzt `course.bookedPlaces`.

**Preis 185 €:** korrekt aus dem Kursdatensatz — alle Ferienspiele-Wochen stehen auf 185 € mit
Geschwisterpreisen 160/135/110 € (2 Kinder = 345 €, deckt sich mit den vorhandenen Buchungen).

## Nachtrag: Geschwister-Angaben als eigene Felder/Spalten

**Was:** Die Angaben aus dem Buchungsformular (Vorname, Nachname, Geburtsdatum, Vorerkrankungen,
Allergien/Medikamente/Unverträglichkeiten, Ernährung, Foto/Video-Einwilligung) waren vorhanden, aber
schwer auffindbar: im Detail-Modal als eine gequetschte Listenzeile, im CSV als Sammelspalte
„Geschwisterkinder (Details)“. Leere Felder fielen ganz weg, dadurch wirkten sie verloren.

**Änderungen:**

- `templates/admin/course_bookings/index.html.twig` — `showDetails()` rendert pro Geschwisterkind
  eine eigene Karte mit einer Zeile je Formularfeld (gleiche `renderDetailRow()`/`renderConsentRow()`
  Logik wie beim Hauptkind, d. h. leere Felder erscheinen über „leere Felder anzeigen“ als
  „Nicht angegeben“ und zählen in den Zähler). Geburtsdatum jetzt als `dd.mm.yyyy`.
- `src/Controller/Admin/CourseBookingAdminController.php` — CSV: Sammelspalte
  „Geschwisterkinder (Details)“ ersetzt durch je 6 Spalten pro Kind am Zeilenende
  („Geschwisterkind N Name/Geburtsdatum/Vorerkrankungen/Allergien…/Ernährung/Foto-Video“).
  Spaltenzahl richtet sich nach der größten Geschwisteranzahl im jeweiligen Export;
  `formatSiblingsForExport()` entfernt, `formatSiblingNameForExport()` und
  `formatSiblingBirthDateForExport()` ergänzt. `raw`-Altdaten landen in der Namensspalte.
- `templates/admin/bz_courses/waitlist.html.twig` — Geburtsdatum ebenfalls als `dd.mm.yyyy`.

**Geprüft:** Export Kurs 28 → 45 Spalten, alle Zeilen gleich lang; Werte z. B.
„Lilly Schopp / 17.12.2017 / Keine / Nein / keine Besonderheiten / Nein“.

## Nachtrag: Geschwister-Gesundheitsfelder sind jetzt Pflicht

**Anlass:** Buchung 435 (`CRS-575B1B9D0C29`, Christina/Paula Jansen, Herbst-Ferienspiele Woche 1):
Beim Geschwisterkind Moritz sind Vorerkrankungen, Allergien/Medikamente und Ernährung leer.
Ursache ist keine Anzeige-Lücke — die Felder waren im Formular nur für das **Hauptkind** Pflicht
(`Course::requiresHealthFields()`, nur Ferienspiele), in der Geschwister-Karte gar nicht.
435 ist die erste Buchung überhaupt, in der die Geschwister-Gesundheitsfelder existieren (alle `null`);
spätere Buchungen (438/440/446) sind gefüllt.

**Änderung:** `templates/course_booking/step4_personal_data.html.twig` — in `addHolidaySiblingCard()`
sind Vorerkrankungen, Allergien/Medikamente, Ernährung und Foto/Video-Einwilligung `required`
(alle Kurse mit Geschwister-Eingabe, Entscheidung des Auftraggebers). Platzhalter weisen auf
„keine“ als Antwort hin. Validierung greift über das bestehende `checkValidity()` des Formulars.

**Asymmetrie offen:** Beim Hauptkind bleiben Vorerkrankungen/Allergien nur bei Ferienspielen Pflicht
und Ernährung ganz optional — Geschwisterkinder sind damit strenger als das Hauptkind.

## Geschwisterkinder nur mit hinterlegtem Preis — Anzahl = Anzahl der Preise

**Regel (Vorgabe):** Geschwisterkinder werden im Buchungsformular nur angeboten, wenn
„Geschwisterkinder erlauben“ **und** mindestens ein Geschwisterpreis hinterlegt ist; **0,00 € zählt mit**
(kostenlos, aber buchbar). Die Anzahl der hinterlegten Preise ist das Maximum: drei Preise = maximal
drei Geschwisterkinder, zwei Preise = maximal zwei.

**Änderungen:**

- `templates/admin/bz_courses/index.html.twig`
  - Beim Speichern wurden Geschwisterpreise mit `.filter(p => p > 0)` verworfen — **0,00 € war nicht
    speicherbar**. Neu `collectSiblingPrices()`: übernimmt jeden ausgefüllten Wert (inkl. 0), verwirft
    nur leere Felder, akzeptiert Komma als Dezimaltrennzeichen.
  - Hinweistexte: Anzahl der Einträge = maximale Geschwisterzahl; ohne Preis ist gar keine
    Geschwisterbuchung möglich (statt „belegt Platz, kein Beitrag“).
- `templates/course_booking/step4_personal_data.html.twig`
  - Kind-Formular: Geschwister-Block nur bei `allowSiblings` **und** `siblingPrices|length > 0`.
  - Familien-Bewegungszeit: genau so viele Eingabefelder wie Preise (vorher mindestens eines).
  - JS: `holidaySiblingMax = min(Anzahl Preise, freie Plätze - 1)`; Hinweistext nennt das Maximum und
    zusätzlich die freien Plätze bzw. den Wartelisten-Fall.
- `src/Controller/CourseBookingController.php` — `normalizeSiblings()` bekommt den Kurs und schneidet
  serverseitig auf die Anzahl der hinterlegten Preise ab (Schutz gegen manipulierte/veraltete Formulare).
- `templates/admin/bz_courses/details.html.twig` — „Geschwisterkinder erlaubt“ zeigt jetzt
  „Ja, maximal N (160,00 € / 135,00 € / 110,00 €)“ bzw. warnt „ohne Geschwisterpreis — nicht buchbar“.

**Geprüft** (lokaler Dev-Server, `/booking/courses/step/4?course=…`):
Kurs 27 (Ferienspiele, `[160,135,110]`) → Eingabe sichtbar, Max 3; Kurs 13 (`[7.5]`) → Max 1;
Kurs 12 und 45 (keine Preise) → keine Geschwister-Eingabe; Kurs 39 (Familienzeit, `[10,10]`) → genau
zwei Felder („2. Kind“, „3. Kind“).

**Betroffene aktive Kurse ohne Geschwisterpreis** (dort ist die Geschwister-Eingabe jetzt weg, bis
Preise hinterlegt sind): Sommer-Ferienspiele Woche 6, Moving/Ninja/Power Kids, Power Minis, Fun4Kids,
Bewegungsforscher, PRAMA Teens, Die Entdecker, Sportzwerge, Athletikcamp I + II.
