# 2026-09-10

## Fit wie ein Fisch: Mailvorlagen, Buchungsflow, Sammelanmeldung, Warteliste, Listen

**Auftrag:** E-Mail-Vorlagen wie in den anderen Systemen (Versand vorläufig über die bestehende
Adresse, mit Hinweis), Sammelanmeldung für Schulen mit kurz codierten Elternlinks, Formularbuilder
für Einzel-/Schul-/Elternformular, Buchungsflow mit Validierungslink, Standort-Zusatzinfos in den
Mails, Warteliste mit Detailansicht und Automatik, Kontaktdaten und CSV in den Listen.

**Leitentscheidung:** Nichts mit BZ/Schwimmen/Event Center vermischen (Wartbarkeit). Das Fisch-System
bekommt deshalb eigene Vorlagen-Tabelle, eigenen Fallback-Katalog, eigenen Mail- und
Wartelisten-Service — auch wenn die Muster aus den anderen Systemen übernommen sind.

**Ausgangslage:** Es gab im Fisch-System bisher **keinen** Mailversand. `/admin/fisch/email-templates`
und `/admin/fisch/sammelanmeldung` waren Platzhalterseiten, Standorte kannten nur Name/Slug/Sortierung,
die Warteliste konnte nur händisch übernommen werden.

### Datenmodell

Migration `Version20260910150000` (angewandt):

- neu `fisch_email_template` (Typ, Betreff, Inhalt, Absender, optional Standort-Bindung),
  neu `fisch_form_definition` (Formulartyp + Felder als JSON).
- `fisch_pool` +6 Spalten: `address`, `directions`, `parking_info`, `maps_url`, `maps_embed_url`,
  `info_text`.
- `fisch_registration` + `pending_until`, `institution_name`, `extra_fields`.
- `fisch_waitlist_entry` + `offer_token` (unique), `offered_at`, `offer_until`, `offered_course_id`,
  `notes`, `extra_fields`.
- `fisch_bulk_registration` + Status, `token` (unique), Kontaktname, `confirmed_at`,
  `invitation_sent_at`, Notizen, Extrafelder; `fisch_bulk_registration_link` + Elternmail,
  Kindbezeichnung, `sent_at`.

### Services

- `FischMailService` — 32 Platzhalter inklusive `{bad_adresse}`, `{bad_anfahrt}`, `{bad_parkplatz}`,
  `{bad_maps_link}`; rendert Vorlage oder Fallback (`FischFallbackEmailCatalog`) und liefert die
  Quelle mit. Absender: Vorlage, sonst `ADMIN_EMAIL`. Versandfehler werden geloggt, brechen den
  Buchungsvorgang nie ab.
- `FischRegistrationService` — Anmeldung erzeugt `pending` + Token + 72-Stunden-Frist und schickt die
  Validierungsmail; `verify()` bestätigt und schickt die Bestätigungsmail. Storno benachrichtigt und
  bietet den Platz der Warteliste an.
- `FischWaitlistService` — Angebot mit 24-Stunden-Frist, FIFO-Weitergabe je Altersgruppe (unter
  Berücksichtigung bevorzugter Bäder/Zeiten), Annahme, Ablauf, Rücknahme, händisches Übernehmen.
- `FischCapacityService` — `countBooked()` zählt jetzt bestätigte **und** offene Anmeldungen, offene
  Sammelanmeldungs-Reservierungen und laufende Wartelisten-Angebote. Ohne das könnte der
  Validierungslink zu Überbuchungen führen.
- `FischBulkRegistrationService` — Schulcode 8, Elterncode 10 Zeichen aus einem Alphabet ohne
  verwechselbare Zeichen (`I`, `O`, `0`, `1`); Einladung → Bestätigung durch die Schule → Elternlinks.
- `FischFormDefinitionService` — Defaults je Formulartyp, Merge mit gespeicherter Konfiguration,
  Validierung. Kernfelder (Kindname, Geburtsdatum, Elternmail) sind gesperrt, weil Altersgruppe,
  Kapazitätsprüfung und Mailversand daran hängen.

### Admin

- `/admin/fisch/email-templates` — Liste je Typ mit Fallback-Kennzeichnung, Editor mit klickbaren
  Platzhaltern, Vorschau; Warnbanner zur noch fehlenden Fisch-Absenderadresse.
- `/admin/fisch/formulare` — Formularbuilder (Einzel / Schule / Eltern) mit Feldtypen, Pflichtflag,
  Sichtbarkeit und Zurücksetzen auf die Standardfelder.
- `/admin/fisch/sammelanmeldung` — Einladung anlegen, Schul-Link und Elternlinks anzeigen/erneut
  senden, stornieren.
- `/admin/fisch/warteliste` — Kontaktspalte, Status mit Angebotsfrist, Detail-Modal (JSON-Endpunkt),
  „Platz anbieten", Angebot zurücknehmen, „Direkt übernehmen", CSV um Status/Frist erweitert.
- `/admin/fisch/standorte` — Adresse, Anfahrt, Parkplatz, Maps-Link, Maps-Ausschnitt, Hinweistext.
- Kurs-Teilnehmerliste und `/admin/fisch/teilnehmer` — Kontakt-, Adress- und Statusspalten; neuer
  CSV-Export je Kurs (`/admin/fisch/kurse/{id}/teilnehmer/export.csv`, 14 Spalten).

### Öffentlich

Neu: `/booking/fisch/bestaetigen?token=` (Validierung), `/booking/fisch/warteliste/annehmen`
(Platzangebot), `/booking/fisch/schule/{token}` (Schule bucht den Slot),
`/booking/fisch/e/{token}` (Eltern melden das Kind endgültig an). Die Formularfelder kommen aus dem
Builder (`_dynamic_fields.html.twig`).

### Commands

`app:fisch:waitlist-expire` und `app:fisch:expire-pending`, beide mit `--dry-run`, dokumentiert in
`docs/CRONJOBS.md`.

**Test:** `lint:container` und `lint:twig` (34 Dateien) fehlerfrei, `doctrine:schema:validate
--skip-sync` ohne Fisch-Befund (die gemeldeten Mapping-Fehler betreffen ältere Entities anderer
Systeme), alle 9 Mailtypen gerendert (aktuell durchweg Fallback, da noch keine Vorlage in der DB),
Formularvalidierung gegen alle drei Formulartypen, Kapazitätszählung gegen echte Kurse, beide neue
Commands mit `--dry-run`. Zusätzlich `MAILER_DSN=null://null php bin/console app:fisch:e2e-test`:
Anmeldung→Verify, Warteliste Angebot/Claim, Sammelanmeldung Schule→Eltern — grün. Nebenbei Fix:
bei Elternanmeldung über Sammellink wird `usedSlots` vor der Kapazitätsprüfung geflusht, sonst
scheitert die Anmeldung bei voller Reservierung.

**Offen:** Absenderadresse, Vorlagen anlegen, Cronjobs einrichten — siehe `docs/TODO.md`.
Anmeldephase startet planmäßig 2026-09-17.

## Standortinfos vorausgefüllt

Adresse, Anfahrt, Parkplatz, Maps-Link und Embed-URL für alle sechs `fisch_pool`-Einträge anhand
öffentlicher Adressen gesetzt (Rurbad Nippesstraße 6; Goethestraße 1a; Birkesdorf Weidenpesch;
Merken Paulstraße; Derichsweiler Kreuzherrenstraße; Arnoldsweiler Auf dem Horstert). Parkhinweise
bewusst allgemein — bitte im Admin gegenprüfen.
