faq.php zeigte bislang fuer jeden Mandanten denselben, komplett AOK-spezifischen Inhalt (Kaffeemaschinen-Bedienung, interner Ansprechpartner) - fuer andere Kunden unbrauchbar und inhaltlich falsch. - Neue Tabelle faq_entries (tenant-scoped, Soft-Delete, sort_order). - Migration uebernimmt die bisherigen AOK-Inhalte einmalig als FAQ des migrierten Default-Mandanten; andere Mandanten sehen sie nicht. - Neue Mandanten bekommen bei der Registrierung automatisch eine kurze generische Starter-FAQ (faq_seed_default_entries), frei editierbar. - faq.php: lesbar fuer alle angemeldeten Mitglieder eines Mandanten, Anlegen/Bearbeiten/Entfernen auf owner/admin beschraenkt. - Landingpage verweist nicht mehr auf faq.php (jetzt interner, mandantengebundener Inhalt statt oeffentlicher Marketing-Seite). Live getestet: AOK-Carry-over fuer Default-Mandant, frisch registrierter Test-Mandant bekam isolierte generische Starter-FAQ, Anlegen mit HTML-Payload (korrekt escaped), Bearbeiten und Soft-Delete geprueft. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
17 KiB
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.phplesen 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:
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.phpbleiben überparticipants.legacy_mitarbeiter_iderhalten. - 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=<legacy_mitarbeiter_id>. - Gesamtwerte, Jahresübersicht und letzte Buchungen werden aus
ledger_entriesgeladen. - 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_idsinledger_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:
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-Rolleowner,adminodertreasurer, oder im Legacy-/Dev-FallbackcheckKaffeelisteAdminmit dem Default-Tenant. - Jede eingetragene Legacy-Zeile (
kl_Kaffeeverbrauchbeziehungsweisekl_Einzahlungen) wird innerhalb derselben Transaktion sofort überledger_mirror_legacy_consumption()beziehungsweise die neueledger_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
$sqlMitarbeiterunbelegt, 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 inkaffeeliste.php. - Löschen erzeugt keinen harten Delete im Ledger mehr. Neue Funktion
ledger_void_entry_by_legacy_id()setztvoided_atauf dem gespiegelten Ledger-Eintrag, bevor die Legacy-Zeile auskl_Einzahlungenoderkl_Kaffeeverbrauchentfernt wird (in einer Transaktion). Der Ledger-Eintrag bleibt damit für die Revision erhalten; alle Lesepfade filtern bereits konsistent aufvoided_at IS NULL. - Zwei nie aufgerufene Funktionen (
berechneGesamtausgabe,berechneGesamtstriche,berechneGesamteinzahlungen) wurden als toten Code entfernt.
Angepasstes Check-Skript:
scripts/check-m4-ledger-migration.phpging 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 aufvoided_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:
mitarbeiterverwalten.php
app/ledger.php
Umfang:
- Zugriff nutzte bisher ausschließlich
checkKaffeelisteAdminund hätte neue SaaS-Mandanten ohne Legacy-Mitarbeiterzeile ausgeschlossen. Jetzt gilt die gleiche Rollen-/Fallback-Logik wie inkaffeeliste.php, mit den Rollenownerundadmin(nichttreasurer, da Mitgliederpflege sensibler ist als reine Zahlungsvorgänge). - Anlegen, Bearbeiten, Aktivieren und Deaktivieren schreiben weiterhin zuerst
in
kl_Mitarbeiterund spiegeln danach in derselben Transaktion über die neue Funktionledger_mirror_legacy_participant()nachparticipants. Ledger-Ansichten (Dashboard, Kaffeeliste, Teilnehmerauswertung) sehen neue oder geänderte Mitglieder damit sofort, ohne auf einen manuellen Lauf vonscripts/backfill-default-tenant.phpzu 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
paypalnamewar escaped). Beide Stellen nutzen jetztsaas_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 (spiegeltactive = 0).
Bewusst nicht umgesetzt:
- Der Haken "Administrator" bleibt ein reines Legacy-Feld auf
kl_Mitarbeiter.adminund wird nicht automatisch in einetenant_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 überscripts/backfill-default-tenant.phpoder die Registrierung.
Fortsetzung: Hinweise und rollenbasierter Zugang
Umgesetzte Dateien:
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, mitdeleted_atfür Soft-Delete) ersetztkl_hinweiseals aktive Datenquelle.kl_hinweisebleibt unangetastet als Golden-Master-Referenz. - Die Migration übernimmt einmalig alle zum Migrationszeitpunkt noch
gültigen
kl_hinweise-Einträge innoticesdes Default-Mandanten, damit keine sichtbaren Banner verloren gehen. hinweise.phpliest/schreibt jetzt ausschließlichnotices, 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 unscopedkl_Mitarbeiter-Tabelle. Das behebt nebenbei ein Datenleck: Dakl_Mitarbeiterkeinetenant_idhat, 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_Mitarbeiterbetrieben (ledger_create_participant(),ledger_update_participant(),ledger_set_participant_active()), damitstricheintragen.phpundeinzahlung.php(deren Mitarbeiter-Picker weiterhin direkt auskl_Mitarbeiterliest) 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".ownerist ü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 einenusers-Datensatz ohne Passwort an, setzt/aktualisiert dietenant_memberships-Rolle und verknüpftparticipants.user_id. Anschließend wird ein Einladungslink über den bestehenden Passwort-Reset-Mechanismus verschickt (saas_send_invite_mail(), gleicher Token-Typpassword_reset, gleiche Zielseitepasswort-zuruecksetzen.phpwie beim regulären Passwort-Reset). - Zugangsentzug (
saas_revoke_participant_access()) setzt dietenant_memberships-Zeile aufstatus = 'revoked', statt sie zu löschen oder denuser_id-Verweis zu entfernen. Der Zugang kann später erneut gewährt werden, ohne den Account neu anzulegen. Der Login prüft bereits überall auftm.status = 'active', wodurch ein entzogener Zugang sofort wirkt. - Live gegen die Dev-Datenbank getestet: Mitglied anlegen (inklusive
Dual-Write-Check), Zugang mit Rolle
treasurergewähren, Einladungsmail geprüft, Passwort über den Einladungslink gesetzt, erfolgreicher Login, Rollenschutz geprüft (kein Zugriff aufmitarbeiterverwalten.php, Zugriff aufkaffeeliste.php), Zugang entzogen, anschließender Login-Versuch korrekt mit „kein aktiver Mandant" abgelehnt.
Fortsetzung: Sammelerfassung tenant-nativ
Umgesetzte Dateien:
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 verwendenparticipant_idstattMitarbeiterID. - Beim Speichern wird pro Teilnehmer entschieden: Hat der Teilnehmer eine
legacy_mitarbeiter_id(Default-Mandant), läuft der Schreibpfad wie bisher überkl_Kaffeeverbrauch/kl_Einzahlungenplus Ledger-Spiegelung. Ohne Legacy-Verknüpfung (jeder andere Mandant) wird direkt über die neuen Funktionenledger_record_consumption()/ledger_record_payment()ins Ledger gebucht, ohne Legacy-Tabellen zu berühren. - Die "Vorderseite"/"Rückseite"-Filter (100-Tage-Listen-Regel,
>= 10beziehungsweise< 10Striche im Fenster) bleiben für den Default- Mandanten exakt auf der bisherigen Legacy-Logik (Anker: jüngstes Datum inkl_Kaffeeverbrauch). Andere Mandanten nutzen die neue, Ledger-native Fensterprüfungledger_fetch_participants_by_window_marks()mit dem intenant_settings.sheet_window_dayskonfigurierten Fenster. - Nebenbei zwei Legacy-Bugs behoben: In
einzahlung.phpverlinkten 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$sqlMitarbeiterunbelegt (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_centsstatt aus der mandantenunabhängigenkl_config-Tabelle. Das ist auch für den Default-Mandanten eine Korrektur:kl_configwird seit M3 nicht mehr übermandant-einstellungen.phpgepflegt 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.
Nachtrag: FAQ pro Mandant
Umgesetzte Dateien:
database/migrations/0012_saas_faq_entries.sql
app/faq.php
faq.php
app/saas-auth.php (Starter-FAQ bei Registrierung)
landing.php (Link auf die jetzt mandantengebundene faq.php entfernt)
Hintergrund: faq.php war komplett AOK-spezifischer Inhalt
(Kaffeemaschinen-Bedienung, Milch-/Zucker-Standort, interner
Ansprechpartner) und für jeden Mandanten identisch sichtbar – nicht
brauchbar für andere Kunden.
- Neue Tabelle
faq_entries(tenant-scoped, Soft-Delete viadeleted_at,sort_orderfür die Reihenfolge). - Die Migration übernimmt die bisherigen AOK-Inhalte einmalig als erste
FAQ-Einträge des migrierten Default-Mandanten (
kl_hinweise-Migration als Vorbild). Andere Mandanten sehen diese Inhalte nicht. - Neue Mandanten bekommen bei der Registrierung automatisch eine kurze,
generische Starter-FAQ (
faq_seed_default_entries(), vier Fragen zu Einrichtung, Rollen, PayPal, eigenem Stand) – editierbar, nicht bindend. faq.phpist jetzt für alle angemeldeten Mitglieder eines Mandanten lesbar (auch Legacy-Fallback übercheckKaffeelisteAccess); Anlegen, Bearbeiten und Entfernen einzelner Fragen ist aufowner/adminbeschränkt (beziehungsweisecheckKaffeelisteAdminim Legacy-Fallback).- Die Landingpage verweist nicht mehr auf
faq.php(das ist jetzt interner, mandantengebundener App-Inhalt), sondern zeigt den vollständigen generischen FAQ-Auszug direkt auf der Seite selbst.
Live getestet: Carry-over der AOK-Inhalte für den Default-Mandanten geprüft, ein frisch registrierter Test-Mandant bekam korrekt die generische Starter-FAQ (isoliert von den AOK-Inhalten), Anlegen mit HTML/Skript-Payload im Text (korrekt escaped), Bearbeiten und Soft-Delete-Löschen erfolgreich geprüft. Testdaten anschließend entfernt.
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).