# M5 App-Kern Stand: 2026-07-14 (fortgeführt) M5 stellt die operativen App-Seiten schrittweise auf das neue tenant-sichere Modell um. Der Start erfolgt bewusst read-only, damit Summen, Links und Darstellung gegen die bestehende Legacy-Oberfläche vergleichbar bleiben. ## Ziel - Kernseiten aus `app/ledger.php` lesen lassen. - Bestehendes Tabellenlayout und Bediengefühl erhalten. - Schreibende Legacy-Flows erst nach stabiler Leseparität umstellen. - Tenant-Kontext serverseitig bestimmen, nicht aus Formularfeldern. ## Umgesetzte Schritte Umgesetzte Dateien: ```text kaffeeliste.php teilnehmerauswertung.php index.php ``` Umgesetzter Umfang `kaffeeliste.php`: - Die Gesamtübersicht liest aktive Teilnehmer aus `ledger_fetch_participant_summaries()`. - Angezeigte Werte bleiben fachlich gleich: aktueller Stand, Gesamtausgabe, Gesamtstriche und Gesamteinzahlungen. - Links zur bestehenden `teilnehmerauswertung.php` bleiben über `participants.legacy_mitarbeiter_id` erhalten. - Owner, Admin und Treasurer dürfen die SaaS-Ansicht lesen. - Der lokale Legacy-/Dev-Admin-Fallback nutzt nur ohne SaaS-Session den Default-Tenant. - Die Sidebar zeigt Legacy-Schreibseiten weiterhin nur für Legacy-Admins; Ledger-Leseansichten sind für passende SaaS-Rollen sichtbar. - Export, letzte Einträge, CSV-Upload und Info-Mail bleiben noch Legacy-Flows. Umgesetzter Umfang `teilnehmerauswertung.php`: - Die Detailauswertung liest Teilnehmerdaten über `ledger_fetch_participant_summary_by_legacy_id()`. - Die Route bleibt kompatibel zu bestehenden Links: `teilnehmerauswertung.php?user_id=`. - Gesamtwerte, Jahresübersicht und letzte Buchungen werden aus `ledger_entries` geladen. - PayPal-Anzeige nutzt die neuen Tenant-Settings statt `kl_config`. - Owner, Admin und Treasurer dürfen die SaaS-Ansicht lesen; der lokale Legacy-/Dev-Admin-Fallback nutzt weiterhin den Default-Tenant. Ergänzte Ledger-Helfer: - `ledger_fetch_default_tenant()` - `ledger_fetch_participant_summary_by_legacy_id()` - `ledger_mirror_legacy_consumption()` - Filter `legacy_mitarbeiter_ids` in `ledger_fetch_participant_summaries()` Umgesetzter Umfang `index.php`: - Das persönliche Dashboard liest den aktuellen Stand, Jahreswerte und letzte Buchungen aus `ledger_entries`. - Die Teilnehmerzuordnung erfolgt über den aktuellen SaaS-User oder im lokalen Legacy-/Dev-Fallback über die E-Mail-Adresse des Legacy-Mitarbeiters. - PayPal-Anzeige und Preis pro Strich nutzen die Tenant-Settings. - Eigene Web-Striche bleiben mit dem bestehenden Legacy-Datensatz kompatibel und werden direkt ins Ledger gespiegelt. ## Fortsetzung: Schreibseiten und Storno Umgesetzte Dateien: ```text stricheintragen.php einzahlung.php letzteneintraege.php app/ledger.php scripts/check-m4-ledger-migration.php ``` Umgesetzter Umfang `stricheintragen.php` und `einzahlung.php`: - Beide Sammelerfassungsseiten waren zuvor ohne jede Zugriffsprüfung erreichbar (nur CSRF-Schutz, kein `checkKaffeelisteAdmin`- oder Rollen-Check). Das ist jetzt behoben: Zugriff erfordert SaaS-Rolle `owner`, `admin` oder `treasurer`, oder im Legacy-/Dev-Fallback `checkKaffeelisteAdmin` mit dem Default-Tenant. - Jede eingetragene Legacy-Zeile (`kl_Kaffeeverbrauch` beziehungsweise `kl_Einzahlungen`) wird innerhalb derselben Transaktion sofort über `ledger_mirror_legacy_consumption()` beziehungsweise die neue `ledger_mirror_legacy_payment()` ins Ledger gespiegelt. Schlägt die Spiegelung fehl, wird die gesamte Sammelerfassung zurückgerollt statt teilweise gespeichert zu werden. - Ein vorbestehender Bug wurde nebenbei behoben: Bei einer POST-Anfrage blieb `$sqlMitarbeiter` unbelegt, was einen PHP-Warning erzeugte und die Mitarbeiterliste nach dem Speichern leer ließ. Umgesetzter Umfang `letzteneintraege.php`: - Der Seitenzugriff nutzte bisher ausschließlich den Legacy-Check `checkKaffeelisteAdmin`, wodurch neu registrierte SaaS-Mandanten (ohne Legacy-`kl_Mitarbeiter`-Zeile) die Seite nie hätten nutzen können. Jetzt gilt dieselbe Rollen-/Fallback-Logik wie in `kaffeeliste.php`. - Löschen erzeugt keinen harten Delete im Ledger mehr. Neue Funktion `ledger_void_entry_by_legacy_id()` setzt `voided_at` auf dem gespiegelten Ledger-Eintrag, bevor die Legacy-Zeile aus `kl_Einzahlungen` oder `kl_Kaffeeverbrauch` entfernt wird (in einer Transaktion). Der Ledger-Eintrag bleibt damit für die Revision erhalten; alle Lesepfade filtern bereits konsistent auf `voided_at IS NULL`. - Zwei nie aufgerufene Funktionen (`berechneGesamtausgabe`, `berechneGesamtstriche`, `berechneGesamteinzahlungen`) wurden als toten Code entfernt. Angepasstes Check-Skript: - `scripts/check-m4-ledger-migration.php` ging bisher davon aus, dass jeder gespiegelte Ledger-Eintrag eine noch existierende Legacy-Zeile hat. Das ist durch das Storno-Modell nicht mehr korrekt: Ein stornierter Eintrag hat absichtlich keine Legacy-Zeile mehr, bleibt aber im Ledger stehen. Die Prüfungen filtern jetzt zusätzlich auf `voided_at IS NULL`, sodass echte Dateninkonsistenzen weiterhin erkannt werden, stornierte Einträge aber nicht mehr als Fehler zählen. Alle drei Flows wurden gegen die Remote-Dev-Datenbank live per HTTP getestet (Sammeleintrag Striche, Sammeleinzahlung, Storno beider Buchungsarten über `letzteneintraege.php`) und anschließend wieder auf den Ausgangsstand zurückgesetzt. ## Fortsetzung: Mitgliederverwaltung Umgesetzte Dateien: ```text mitarbeiterverwalten.php app/ledger.php ``` Umfang: - Zugriff nutzte bisher ausschließlich `checkKaffeelisteAdmin` und hätte neue SaaS-Mandanten ohne Legacy-Mitarbeiterzeile ausgeschlossen. Jetzt gilt die gleiche Rollen-/Fallback-Logik wie in `kaffeeliste.php`, mit den Rollen `owner` und `admin` (nicht `treasurer`, da Mitgliederpflege sensibler ist als reine Zahlungsvorgänge). - Anlegen, Bearbeiten, Aktivieren und Deaktivieren schreiben weiterhin zuerst in `kl_Mitarbeiter` und spiegeln danach in derselben Transaktion über die neue Funktion `ledger_mirror_legacy_participant()` nach `participants`. Ledger-Ansichten (Dashboard, Kaffeeliste, Teilnehmerauswertung) sehen neue oder geänderte Mitglieder damit sofort, ohne auf einen manuellen Lauf von `scripts/backfill-default-tenant.php` zu warten. - Eine gespeicherte-XSS-Lücke wurde behoben: Name und E-Mail wurden beim Bearbeiten-Formular und in der Mitgliederliste bisher ungeschützt ausgegeben (nur `paypalname` war escaped). Beide Stellen nutzen jetzt `saas_html()`. - Live gegen die Dev-Datenbank getestet: Anlegen mit einem HTML/Skript-Payload im Namen (korrekt escaped in der Ausgabe, korrekt gespiegelt in `participants`), Deaktivieren (spiegelt `active = 0`). Bewusst nicht umgesetzt: - Der Haken "Administrator" bleibt ein reines Legacy-Feld auf `kl_Mitarbeiter.admin` und wird nicht automatisch in eine `tenant_memberships`-Rolle übersetzt. Das würde einen Login-Account ohne Einladung/Passwort-Setzung anlegen, was ein eigenes, sauber zu bauendes Einladungs-Flow braucht (E-Mail-Versand, Token, Passwortsetzung). Admin-Rollen für neue SaaS-Nutzer laufen bis dahin weiter über `scripts/backfill-default-tenant.php` oder die Registrierung. ## Fortsetzung: Hinweise und rollenbasierter Zugang Umgesetzte Dateien: ```text database/migrations/0007_saas_notices.sql app/notices.php hinweise.php header.php mitarbeiterverwalten.php (grundlegend neu) app/ledger.php (Participant-CRUD) app/saas-auth.php (Zugangsvergabe/-entzug) app/saas-mail.php (Einladungsmail) ``` ### Hinweise auf Notices umgezogen - Neue Tabelle `notices` (tenant-scoped, mit `deleted_at` für Soft-Delete) ersetzt `kl_hinweise` als aktive Datenquelle. `kl_hinweise` bleibt unangetastet als Golden-Master-Referenz. - Die Migration übernimmt einmalig alle zum Migrationszeitpunkt noch gültigen `kl_hinweise`-Einträge in `notices` des Default-Mandanten, damit keine sichtbaren Banner verloren gehen. - `hinweise.php` liest/schreibt jetzt ausschließlich `notices`, tenant-scoped mit dem gleichen Rollen-/Legacy-Fallback-Muster wie andere Admin-Seiten. Löschen ist ein Soft-Delete (`deleted_at`), kein Hard-Delete. - `header.php` (Banner-Anzeige auf allen App-Seiten) löst den Mandanten jetzt selbst auf (SaaS-Session oder Default-Tenant-Fallback) und zeigt den aktuell gültigen Hinweis dieses Mandanten statt eines global-legacy Hinweises. ### Mitgliederverwaltung: participant-nativ mit Rollen-Zugang `mitarbeiterverwalten.php` wurde grundlegend umgebaut, nicht mehr additiv gepatcht: - Datenquelle ist jetzt `participants` (tenant-scoped), nicht mehr die global unscoped `kl_Mitarbeiter`-Tabelle. Das behebt nebenbei ein Datenleck: Da `kl_Mitarbeiter` keine `tenant_id` hat, hätte jeder SaaS-Mandant mit Owner/Admin-Rolle über die vorherige Version dieser Seite die komplette Mitgliederliste des Default-Mandanten sehen und bearbeiten können. - Für den Default-Mandanten wird weiterhin dual-write nach `kl_Mitarbeiter` betrieben (`ledger_create_participant()`, `ledger_update_participant()`, `ledger_set_participant_active()`), damit `stricheintragen.php` und `einzahlung.php` (deren Mitarbeiter-Picker weiterhin direkt aus `kl_Mitarbeiter` liest) neue Mitglieder sofort anzeigen. Andere Mandanten haben keine Legacy-Schattentabelle und werden rein participant-nativ verwaltet. - Die Legacy-„Administrator"-Checkbox ist aus dem Anlegen-/Bearbeiten- Formular entfernt. Stattdessen gibt es je Mitglied eine eigene „Zugang"-Spalte: Rolle wählen (`member`, `treasurer`, `admin`) und „Zugang gewähren", oder bei bestehendem Zugang Rolle ändern beziehungsweise „Zugang entziehen". `owner` ist über diese UI nicht vergebbar oder entziehbar (nur bei Registrierung gesetzt). - Name und E-Mail eines Mitglieds (`participants.display_name`/`email`) bleiben unabhängig vom Login: Ein Mitglied kann ohne jeden Zugang existieren (nur für Kaffeeliste/Benachrichtigung), und ein Zugang kann jederzeit gewährt oder entzogen werden, ohne den Mitgliedsdatensatz zu berühren. - Zugangsvergabe (`saas_grant_participant_access()`) legt bei Bedarf einen `users`-Datensatz ohne Passwort an, setzt/aktualisiert die `tenant_memberships`-Rolle und verknüpft `participants.user_id`. Anschließend wird ein Einladungslink über den bestehenden Passwort-Reset-Mechanismus verschickt (`saas_send_invite_mail()`, gleicher Token-Typ `password_reset`, gleiche Zielseite `passwort-zuruecksetzen.php` wie beim regulären Passwort-Reset). - Zugangsentzug (`saas_revoke_participant_access()`) setzt die `tenant_memberships`-Zeile auf `status = 'revoked'`, statt sie zu löschen oder den `user_id`-Verweis zu entfernen. Der Zugang kann später erneut gewährt werden, ohne den Account neu anzulegen. Der Login prüft bereits überall auf `tm.status = 'active'`, wodurch ein entzogener Zugang sofort wirkt. - Live gegen die Dev-Datenbank getestet: Mitglied anlegen (inklusive Dual-Write-Check), Zugang mit Rolle `treasurer` gewähren, Einladungsmail geprüft, Passwort über den Einladungslink gesetzt, erfolgreicher Login, Rollenschutz geprüft (kein Zugriff auf `mitarbeiterverwalten.php`, Zugriff auf `kaffeeliste.php`), Zugang entzogen, anschließender Login-Versuch korrekt mit „kein aktiver Mandant" abgelehnt. ## Fortsetzung: Sammelerfassung tenant-nativ Umgesetzte Dateien: ```text stricheintragen.php (Picker + Schreibpfad neu) einzahlung.php (Picker + Schreibpfad neu) app/ledger.php (ledger_record_consumption, ledger_record_payment, ledger_fetch_participants_by_window_marks) ``` `stricheintragen.php` und `einzahlung.php` lasen ihren Mitarbeiter-Picker bisher direkt aus der global unscoped `kl_Mitarbeiter`-Tabelle. Für jeden Mandanten außer dem Default-Mandanten war das faktisch nutzlos: Die Liste zeigte fremde (Default-Mandanten-)Namen an, und Schreiben schlug wegen des Tenant-Checks in `ledger_mirror_legacy_*` sicher, aber ohne verständliche Fehlermeldung fehl. - Der Picker kommt jetzt aus `ledger_fetch_participant_summaries()` (tenant-scoped), Formularfelder verwenden `participant_id` statt `MitarbeiterID`. - Beim Speichern wird pro Teilnehmer entschieden: Hat der Teilnehmer eine `legacy_mitarbeiter_id` (Default-Mandant), läuft der Schreibpfad wie bisher über `kl_Kaffeeverbrauch`/`kl_Einzahlungen` plus Ledger-Spiegelung. Ohne Legacy-Verknüpfung (jeder andere Mandant) wird direkt über die neuen Funktionen `ledger_record_consumption()`/`ledger_record_payment()` ins Ledger gebucht, ohne Legacy-Tabellen zu berühren. - Die "Vorderseite"/"Rückseite"-Filter (100-Tage-Listen-Regel, `>= 10` beziehungsweise `< 10` Striche im Fenster) bleiben für den Default- Mandanten exakt auf der bisherigen Legacy-Logik (Anker: jüngstes Datum in `kl_Kaffeeverbrauch`). Andere Mandanten nutzen die neue, Ledger-native Fensterprüfung `ledger_fetch_participants_by_window_marks()` mit dem in `tenant_settings.sheet_window_days` konfigurierten Fenster. - Nebenbei zwei Legacy-Bugs behoben: In `einzahlung.php` verlinkten die Vorderseite/Rückseite/Alle-Buttons auf `?aktion=...`, ausgewertet wurde aber `$_GET['action']` – die Filter griffen also nie. Und in beiden Dateien blieb nach einem POST-Speichern `$sqlMitarbeiter` unbelegt (behoben bereits in der vorherigen Session, jetzt strukturell nicht mehr möglich, da die Anzeige nach dem Speichern regulär über denselben Picker-Code läuft). - Preis-pro-Strich-Vorbelegung kommt jetzt aus `tenant_settings.mark_price_cents` statt aus der mandantenunabhängigen `kl_config`-Tabelle. Das ist auch für den Default-Mandanten eine Korrektur: `kl_config` wird seit M3 nicht mehr über `mandant-einstellungen.php` gepflegt und kann veraltet sein. - Live gegen die Dev-Datenbank getestet: eigener isolierter Test-Mandant angelegt, Picker zeigt nur dessen Teilnehmer, Striche und Einzahlung rein Ledger-nativ gebucht (korrekter Saldo), Vorder-/Rückseiten-Schwelle bei 10 Strichen korrekt ein-/aussortiert, Default-Mandant weiterhin mit Dual-Write geprüft. Alle Testdaten anschließend entfernt. ## Noch offen - Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im Dashboard integriert). - Export, Mail und Jahresprozesse bleiben M6-Themen. ## Aktueller Prüfstatus - M4 Ledger-Migration: grün mit 73 Assertions (Storno-fähig). - M4 Ledger-Service: grün mit 115 Assertions. - Golden Master: grün mit 104 Assertions. - HTTP-Smoke: grün mit 23 geprüften Seiten. - Live-Test der Schreibflows gegen die Dev-DB: Sammelstriche, Sammeleinzahlung und Storno beider Buchungsarten erfolgreich geprüft. - Live-Test der Mitgliederverwaltung gegen die Dev-DB: Anlegen (inklusive XSS-Payload-Check), Deaktivieren, participants-Spiegelung erfolgreich geprüft. - Live-Test Hinweise/Notices: Carry-over-Migration, Anlegen mit HTML-Payload (korrekt escaped), Soft-Delete, Banner-Anzeige auf Mandant geprüft. - Live-Test Zugangsvergabe/-entzug: kompletter Flow von Einladung bis Login-Sperre nach Entzug erfolgreich geprüft (siehe oben). - Live-Test Sammelerfassung für Nicht-Default-Mandanten: eigener Test-Tenant, Striche/Einzahlung Ledger-nativ gebucht, Vorder-/Rückseiten-Fenster geprüft (siehe oben).