Finanzmanager/public/index.php
fds2610-bot 0b4a216954 Sammel-Commit: PayPal-Bankverbindung und Bankleitzahlen-Verzeichnis
Zwei Features an derselben Stelle, die im Arbeitsverzeichnis
nebeneinander lagen und sich dieselben Dateien teilen (Routen,
BankingController, Bankzugang, Zugangsformular, Dokumentation) –
getrennte Commits ließen sich daraus nur mit einem kaputten
Zwischenstand schneiden.

PayPal als zweite Art von Bankverbindung
----------------------------------------
„Bankverbindung anlegen“ führt zur Wahl der Art: HBCI/FinTS oder
PayPal (bank_zugaenge.typ, Migration 014). Die Abfrage läuft über die
Classic-NVP-API (Services\PaypalNvp, TransactionSearch und GetBalance).
Kein Dreischritt, da PayPal genau ein Guthabenkonto führt –
Zugangsdaten und Kontenzuordnung stehen in einem Formular. Konten,
Umsätze, Protokoll, Regeln und Abgleich bleiben dieselben.

Bankleitzahlen-Verzeichnis
--------------------------
Verwaltung → Bankleitzahlen (/verwaltung/bankinstitute, Migration 015)
nimmt die CSV-Institutsliste der Deutschen Kreditwirtschaft auf und
beantwortet damit die eine Angabe, die beim Anlegen einer
FinTS-Verbindung regelmäßig fehlt: den PIN/TAN-Zugangspunkt.

Reine Nachschlagedaten ohne benutzer_id; ein Import ersetzt den
gesamten Bestand, statt ihn zu ergänzen. „Nur prüfen“ und „Übernehmen“
sind zwei Knöpfe an derselben Datei – abgelegt wird sie nicht, ihr
Inhalt steht ja in der Tabelle.

Services\FintsInstituteImport nimmt der Datei ihre Eigenheiten ab:
ISO-8859-1 statt UTF-8, ein Institutsname über zwei Zeilen in
Anführungszeichen, dieselbe Bankleitzahl vielfach (eine Zeile je
Filiale), Hunderte leerer Zeilen am Ende, Text statt Adresse, wo es
keinen PIN/TAN-Zugang gibt. Spalten werden über ihre Überschrift
angesprochen; fehlen BLZ und Institut, bricht der Import ab.

Gesucht wird über Bankleitzahl, BIC, Name und Ort in einem Zug: „1822“
steht bei der 1822direkt im Namen und bei der Frankfurter Sparkasse im
BIC, in keiner der beiden Bankleitzahlen.

Aus der Trefferliste führt „Verbindung anlegen“ ins gefüllte
FinTS-Formular. Die FinTS-Adresse ist damit kein Pflichtfeld mehr –
bleibt sie leer, wird sie zur Bankleitzahl nachgeschlagen. Findet auch
das nichts, weist Bankzugang den Zugang ab, statt ihn ohne Gegenstelle
zu speichern.

Tests: 31_PaypalZugangTest, 32_BankinstituteTest,
e2e/specs/banking.spec.js, e2e/specs/bankinstitute.spec.js.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 08:48:14 +02:00

343 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/**
* Front-Controller. nginx leitet jeden Request hierher.
*/
require dirname(__DIR__) . '/vendor/autoload.php';
use Finanzmanager\Auth;
use Finanzmanager\Config;
use Finanzmanager\Controllers\AuswertungenController;
use Finanzmanager\Controllers\BankingController;
use Finanzmanager\Controllers\BankinstituteController;
use Finanzmanager\Controllers\BuchungenController;
use Finanzmanager\Controllers\BudgetController;
use Finanzmanager\Controllers\CockpitController;
use Finanzmanager\Controllers\EinstellungenController;
use Finanzmanager\Controllers\ExportController;
use Finanzmanager\Controllers\ImportController;
use Finanzmanager\Controllers\KategorienController;
use Finanzmanager\Controllers\KontenController;
use Finanzmanager\Controllers\LoginController;
use Finanzmanager\Controllers\MonatsabschlussController;
use Finanzmanager\Controllers\PlanungController;
use Finanzmanager\Controllers\TermineController;
use Finanzmanager\Controllers\VertraegeController;
use Finanzmanager\Controllers\VerwaltungController;
use Finanzmanager\Controllers\VorlagenController;
use Finanzmanager\Controllers\ZahlungsverkehrController;
use Finanzmanager\Controllers\WertpapiereController;
use Finanzmanager\Models\Einstellung;
use Finanzmanager\Router;
use Finanzmanager\Services\DebugLog;
use Finanzmanager\View;
Config::load();
// Die Einstellung aus der Datenbank hat Vorrang vor dem Env-Schalter – sie ist
// der Weg, den der Nutzer im Dialog bedient. Schlägt der Zugriff fehl, bleibt
// es beim Env-Wert; das Log soll gerade dann funktionieren, wenn die Datenbank
// das Problem ist.
try {
DebugLog::setAktiv(
Einstellung::getBool('debug_log_aktiv', (bool)Config::get('debug_log', false))
);
} catch (\Throwable) {
// Datenbank noch nicht bereit – Env-Schalter gilt weiter.
}
// Session vor dem Request-Log starten, sonst stünde dort nie ein Benutzer.
Auth::startSession();
DebugLog::request();
$router = new Router();
$router->get('/login', fn() => (new LoginController())->formular());
$router->post('/login', fn() => (new LoginController())->anmelden());
$router->post('/logout', fn() => (new LoginController())->abmelden());
$router->get('/einrichtung', fn() => (new LoginController())->einrichtungFormular());
$router->post('/einrichtung', fn() => (new LoginController())->einrichtungSpeichern());
$router->get('/', fn() => (new CockpitController())->index());
$router->post('/cockpit/vermoegen/konten', fn() => (new CockpitController())->kontenfilter());
$router->get('/einstellungen', fn() => (new EinstellungenController())->index());
$router->post('/einstellungen', fn() => (new EinstellungenController())->speichern());
$router->post('/einstellungen/log-leeren', fn() => (new EinstellungenController())->logLeeren());
// Konten. Die festen Pfade stehen vor den Mustern mit {id}, sonst schluckt
// "/konten/{id}/…" die Route "/konten/neu".
$router->get('/konten', fn() => (new KontenController())->index());
$router->get('/konten/neu', fn() => (new KontenController())->neu());
$router->post('/konten/neu', fn() => (new KontenController())->anlegen());
$router->get('/konten/{id}/bearbeiten', fn(array $p) => (new KontenController())->bearbeiten($p));
$router->post('/konten/{id}/bearbeiten', fn(array $p) => (new KontenController())->speichern($p));
$router->post('/konten/{id}/loeschen', fn(array $p) => (new KontenController())->loeschen($p));
// Kontoblatt und Buchungen.
$router->get('/konten/{id}', fn(array $p) => (new BuchungenController())->kontoblatt($p));
$router->get('/konten/{id}/buchung', fn(array $p) => (new BuchungenController())->neu($p));
$router->post('/konten/{id}/buchung', fn(array $p) => (new BuchungenController())->anlegen($p));
$router->post('/konten/{id}/buchungen/loeschen',
fn(array $p) => (new BuchungenController())->auswahlLoeschen($p));
$router->get('/buchungen/{id}', fn(array $p) => (new BuchungenController())->bearbeiten($p));
$router->post('/buchungen/{id}', fn(array $p) => (new BuchungenController())->speichern($p));
$router->post('/buchungen/{id}/loeschen', fn(array $p) => (new BuchungenController())->loeschen($p));
// Schnellzuordnung und Splitdialog aus dem Kontoblatt.
$router->post('/buchungen/{id}/zuordnung',
fn(array $p) => (new BuchungenController())->zuordnung($p));
$router->post('/buchungen/{id}/splitbuchung',
fn(array $p) => (new BuchungenController())->splitbuchung($p));
$router->post('/buchungen/{id}/klasse',
fn(array $p) => (new BuchungenController())->klasse($p));
// Das Wertpapiergeschäft zu einer Buchung – Dialog im Kontoblatt, Seite ohne
// JavaScript, ein Ziel für beide.
$router->get('/buchungen/{id}/wertpapier',
fn(array $p) => (new BuchungenController())->wertpapierFormular($p));
$router->post('/buchungen/{id}/wertpapier',
fn(array $p) => (new BuchungenController())->wertpapier($p));
// Verwaltung: Kategorien, Klassen, Kontengruppen, Bankleitzahlen, QIF-Import.
$router->get('/verwaltung', fn() => (new VerwaltungController())->index());
$router->get('/verwaltung/kategorien', fn() => (new KategorienController())->index());
$router->get('/verwaltung/kategorien/neu', fn() => (new KategorienController())->neu());
$router->post('/verwaltung/kategorien/neu', fn() => (new KategorienController())->anlegen());
$router->get('/verwaltung/kategorien/{id}', fn(array $p) => (new KategorienController())->bearbeiten($p));
$router->post('/verwaltung/kategorien/{id}', fn(array $p) => (new KategorienController())->speichern($p));
$router->post('/verwaltung/kategorien/{id}/verschieben',
fn(array $p) => (new KategorienController())->verschieben($p));
$router->post('/verwaltung/kategorien/{id}/zusammenfuehren',
fn(array $p) => (new KategorienController())->zusammenfuehren($p));
$router->post('/verwaltung/kategorien/{id}/loeschen',
fn(array $p) => (new KategorienController())->loeschen($p));
$router->get('/verwaltung/klassen', fn() => (new VerwaltungController())->klassen());
$router->post('/verwaltung/klassen/neu', fn() => (new VerwaltungController())->klasseAnlegen());
$router->post('/verwaltung/klassen/{id}', fn(array $p) => (new VerwaltungController())->klasseSpeichern($p));
$router->post('/verwaltung/klassen/{id}/loeschen',
fn(array $p) => (new VerwaltungController())->klasseLoeschen($p));
$router->get('/verwaltung/kontengruppen', fn() => (new VerwaltungController())->kontengruppen());
$router->get('/verwaltung/kontengruppen/neu', fn() => (new VerwaltungController())->kontengruppeNeu());
$router->post('/verwaltung/kontengruppen/neu', fn() => (new VerwaltungController())->kontengruppeAnlegen());
$router->get('/verwaltung/kontengruppen/{id}',
fn(array $p) => (new VerwaltungController())->kontengruppeBearbeiten($p));
$router->post('/verwaltung/kontengruppen/{id}',
fn(array $p) => (new VerwaltungController())->kontengruppeSpeichern($p));
$router->post('/verwaltung/kontengruppen/{id}/loeschen',
fn(array $p) => (new VerwaltungController())->kontengruppeLoeschen($p));
// Das Bankleitzahlen-Verzeichnis: nachschlagen und die Institutsliste einlesen.
$router->get('/verwaltung/bankinstitute', fn() => (new BankinstituteController())->index());
$router->post('/verwaltung/bankinstitute/import', fn() => (new BankinstituteController())->importieren());
$router->post('/verwaltung/bankinstitute/leeren', fn() => (new BankinstituteController())->leeren());
// ── Planung: Budgetplaner und Monatsabschluss ───────────────────────────────
$router->get('/planung', fn() => (new PlanungController())->index());
$router->get('/planung/budget', fn() => (new BudgetController())->planer());
$router->get('/planung/budget/verwaltung', fn() => (new BudgetController())->verwaltung());
$router->post('/planung/budget/neu', fn() => (new BudgetController())->anlegen());
$router->post('/planung/budget/{id}/positionen',
fn(array $p) => (new BudgetController())->positionenSpeichern($p));
$router->get('/planung/budget/{id}/bearbeiten',
fn(array $p) => (new BudgetController())->bearbeiten($p));
$router->post('/planung/budget/{id}/bearbeiten',
fn(array $p) => (new BudgetController())->speichern($p));
$router->post('/planung/budget/kopieren', fn() => (new BudgetController())->kopieren());
$router->post('/planung/budget/{id}/aktivieren',
fn(array $p) => (new BudgetController())->aktivieren($p));
$router->post('/planung/budget/{id}/loeschen',
fn(array $p) => (new BudgetController())->loeschen($p));
$router->get('/planung/monatsabschluss', fn() => (new MonatsabschlussController())->index());
$router->post('/planung/monatsabschluss', fn() => (new MonatsabschlussController())->abschliessen());
$router->get('/planung/monatsabschluss/{id}',
fn(array $p) => (new MonatsabschlussController())->snapshot($p));
$router->post('/planung/monatsabschluss/{id}/wiedereroeffnen',
fn(array $p) => (new MonatsabschlussController())->wiedereroeffnen($p));
// ── Auswertungen: Berichte, Drilldown, CSV ──────────────────────────────────
$router->get('/auswertungen', fn() => (new AuswertungenController())->index());
// Die festen Pfade stehen vor „/auswertungen/bericht/{id}“, sonst schluckt das
// Muster auch „/auswertungen/bericht/speichern“.
$router->get('/auswertungen/csv', fn() => (new AuswertungenController())->csv());
$router->get('/auswertungen/buchungen', fn() => (new AuswertungenController())->drilldown());
$router->get('/auswertungen/bericht', fn() => (new AuswertungenController())->bericht());
$router->post('/auswertungen/bericht/speichern', fn() => (new AuswertungenController())->speichern());
$router->get('/auswertungen/bericht/{id}', fn(array $p) => (new AuswertungenController())->gespeichert($p));
$router->post('/auswertungen/bericht/{id}/loeschen',
fn(array $p) => (new AuswertungenController())->loeschen($p));
$router->get('/auswertungen/gruppen', fn() => (new AuswertungenController())->gruppen());
$router->post('/auswertungen/gruppen/neu', fn() => (new AuswertungenController())->gruppeAnlegen());
$router->post('/auswertungen/gruppen/{id}',
fn(array $p) => (new AuswertungenController())->gruppeSpeichern($p));
$router->post('/auswertungen/gruppen/{id}/loeschen',
fn(array $p) => (new AuswertungenController())->gruppeLoeschen($p));
// ── Buchungsvorlagen ────────────────────────────────────────────────────────
$router->get('/vorlagen', fn() => (new VorlagenController())->index());
$router->post('/vorlagen/neu', fn() => (new VorlagenController())->anlegen());
$router->post('/vorlagen/aus-buchung', fn() => (new VorlagenController())->ausBuchung());
$router->post('/vorlagen/{id}', fn(array $p) => (new VorlagenController())->speichern($p));
$router->post('/vorlagen/{id}/buchen', fn(array $p) => (new VorlagenController())->buchen($p));
$router->post('/vorlagen/{id}/loeschen', fn(array $p) => (new VorlagenController())->loeschen($p));
// ── Termine: regelmäßige Buchungen, Kalender, Erinnerungen ──────────────────
$router->get('/termine', fn() => (new TermineController())->index());
$router->post('/termine/neu', fn() => (new TermineController())->anlegen());
$router->post('/termine/lauf', fn() => (new TermineController())->lauf());
$router->get('/termine/kalender', fn() => (new TermineController())->kalender());
$router->get('/termine/erinnerungen', fn() => (new TermineController())->erinnerungen());
$router->post('/termine/erinnerungen/neu', fn() => (new TermineController())->erinnerungAnlegen());
$router->post('/termine/erinnerungen/{id}/erledigen',
fn(array $p) => (new TermineController())->erinnerungErledigen($p));
$router->post('/termine/erinnerungen/{id}/loeschen',
fn(array $p) => (new TermineController())->erinnerungLoeschen($p));
$router->post('/termine/{id}', fn(array $p) => (new TermineController())->speichern($p));
$router->post('/termine/{id}/buchen', fn(array $p) => (new TermineController())->buchen($p));
$router->post('/termine/{id}/ueberspringen',
fn(array $p) => (new TermineController())->ueberspringen($p));
$router->post('/termine/{id}/loeschen', fn(array $p) => (new TermineController())->loeschen($p));
// ── Verträge, Adressen, Sparziele, Kredite ──────────────────────────────────
$router->get('/vertraege', fn() => (new VertraegeController())->index());
$router->post('/vertraege/neu', fn() => (new VertraegeController())->anlegen());
$router->get('/vertraege/adressen', fn() => (new VertraegeController())->adressen());
$router->post('/vertraege/adressen/neu', fn() => (new VertraegeController())->adresseAnlegen());
$router->post('/vertraege/adressen/{id}',
fn(array $p) => (new VertraegeController())->adresseSpeichern($p));
$router->post('/vertraege/adressen/{id}/loeschen',
fn(array $p) => (new VertraegeController())->adresseLoeschen($p));
$router->get('/vertraege/sparziele', fn() => (new VertraegeController())->sparziele());
$router->post('/vertraege/sparziele/neu', fn() => (new VertraegeController())->sparzielAnlegen());
$router->post('/vertraege/sparziele/{id}',
fn(array $p) => (new VertraegeController())->sparzielSpeichern($p));
$router->post('/vertraege/sparziele/{id}/loeschen',
fn(array $p) => (new VertraegeController())->sparzielLoeschen($p));
$router->get('/vertraege/kredite', fn() => (new VertraegeController())->kredite());
$router->post('/vertraege/kredite/neu', fn() => (new VertraegeController())->kreditAnlegen());
$router->post('/vertraege/kredite/{id}',
fn(array $p) => (new VertraegeController())->kreditSpeichern($p));
$router->post('/vertraege/kredite/{id}/loeschen',
fn(array $p) => (new VertraegeController())->kreditLoeschen($p));
$router->post('/vertraege/{id}', fn(array $p) => (new VertraegeController())->speichern($p));
$router->post('/vertraege/{id}/loeschen', fn(array $p) => (new VertraegeController())->loeschen($p));
// ── Zahlungsverkehr ─────────────────────────────────────────────────────────
$router->get('/zahlungsverkehr', fn() => (new ZahlungsverkehrController())->index());
$router->get('/zahlungsverkehr/neu', fn() => (new ZahlungsverkehrController())->neu());
$router->post('/zahlungsverkehr/neu', fn() => (new ZahlungsverkehrController())->anlegen());
$router->get('/zahlungsverkehr/tan', fn() => (new ZahlungsverkehrController())->tanFormular());
$router->post('/zahlungsverkehr/tan', fn() => (new ZahlungsverkehrController())->tanEinreichen());
$router->post('/zahlungsverkehr/tan/abbrechen', fn() => (new ZahlungsverkehrController())->tanAbbrechen());
$router->get('/zahlungsverkehr/{id}', fn(array $p) => (new ZahlungsverkehrController())->bearbeiten($p));
$router->post('/zahlungsverkehr/{id}', fn(array $p) => (new ZahlungsverkehrController())->speichern($p));
$router->post('/zahlungsverkehr/{id}/senden', fn(array $p) => (new ZahlungsverkehrController())->senden($p));
$router->post('/zahlungsverkehr/{id}/stornieren',
fn(array $p) => (new ZahlungsverkehrController())->stornieren($p));
$router->post('/zahlungsverkehr/{id}/loeschen', fn(array $p) => (new ZahlungsverkehrController())->loeschen($p));
// ── Wertpapiere: Bestände, Depotbuchungen, Kurse, Auswertungen ──────────────
$router->get('/wertpapiere', fn() => (new WertpapiereController())->index());
$router->get('/wertpapiere/auswertung', fn() => (new WertpapiereController())->auswertung());
$router->get('/wertpapiere/papier/{id}', fn(array $p) => (new WertpapiereController())->papier($p));
$router->get('/wertpapiere/buchungen', fn() => (new WertpapiereController())->buchungen());
$router->get('/wertpapiere/buchungen/neu', fn() => (new WertpapiereController())->neu());
$router->post('/wertpapiere/buchungen/neu', fn() => (new WertpapiereController())->anlegen());
$router->post('/wertpapiere/buchungen/loeschen',
fn() => (new WertpapiereController())->auswahlLoeschen());
$router->post('/wertpapiere/buchungen/{id}/loeschen',
fn(array $p) => (new WertpapiereController())->loeschen($p));
$router->get('/wertpapiere/stammdaten', fn() => (new WertpapiereController())->stammdaten());
$router->post('/wertpapiere/stammdaten/{id}',
fn(array $p) => (new WertpapiereController())->speichern($p));
$router->post('/wertpapiere/stammdaten/{id}/kurs',
fn(array $p) => (new WertpapiereController())->kursSetzen($p));
$router->post('/wertpapiere/kurse', fn() => (new WertpapiereController())->kurseAbrufen());
$router->get('/wertpapiere/import', fn() => (new WertpapiereController())->importFormular());
$router->post('/wertpapiere/import', fn() => (new WertpapiereController())->importAusfuehren());
// ── Online-Banking: Zugänge, Abruf, Abgleich, Regeln ────────────────────────
$router->get('/banking', fn() => (new BankingController())->index());
$router->get('/banking/neu', fn() => (new BankingController())->neu());
// Je Art eine eigene Maske – die Angaben für FinTS und PayPal haben nichts
// miteinander gemein. Die Pfade stehen vor „/banking/{id}/…“, sonst wäre „neu“
// eine Kennung.
$router->get('/banking/neu/{typ}', fn(array $p) => (new BankingController())->neuFormular($p));
$router->post('/banking/neu/{typ}', fn(array $p) => (new BankingController())->anlegen($p));
// Die festen Pfade stehen vor „/banking/{id}“, sonst schluckt das Muster auch
// „/banking/umsaetze“ und „/banking/regeln“.
$router->get('/banking/umsaetze', fn() => (new BankingController())->umsaetze());
$router->post('/banking/umsaetze/alle', fn() => (new BankingController())->alleUebernehmen());
$router->post('/banking/umsaetze/loeschen', fn() => (new BankingController())->umsaetzeLoeschen());
$router->post('/banking/umsaetze/{id}/uebernehmen',
fn(array $p) => (new BankingController())->uebernehmen($p));
$router->post('/banking/umsaetze/{id}/verwerfen',
fn(array $p) => (new BankingController())->verwerfen($p));
$router->post('/banking/umsaetze/{id}/zuruecksetzen',
fn(array $p) => (new BankingController())->zuruecksetzen($p));
$router->get('/banking/regeln', fn() => (new BankingController())->regeln());
$router->post('/banking/regeln/neu', fn() => (new BankingController())->regelAnlegen());
$router->post('/banking/regeln/pruefen', fn() => (new BankingController())->regelnPruefen());
$router->post('/banking/regeln/{id}', fn(array $p) => (new BankingController())->regelSpeichern($p));
$router->post('/banking/regeln/{id}/loeschen',
fn(array $p) => (new BankingController())->regelLoeschen($p));
$router->get('/banking/tan', fn() => (new BankingController())->tanFormular());
$router->post('/banking/tan', fn() => (new BankingController())->tanEinreichen());
$router->post('/banking/tan/abbrechen', fn() => (new BankingController())->tanAbbrechen());
$router->get('/banking/{id}', fn(array $p) => (new BankingController())->bearbeiten($p));
$router->post('/banking/{id}', fn(array $p) => (new BankingController())->speichern($p));
$router->post('/banking/{id}/abrufen', fn(array $p) => (new BankingController())->abrufen($p));
$router->post('/banking/{id}/loeschen', fn(array $p) => (new BankingController())->loeschen($p));
$router->get('/banking/{id}/verfahren', fn(array $p) => (new BankingController())->verfahren($p));
$router->post('/banking/{id}/verfahren', fn(array $p) => (new BankingController())->verfahrenWaehlen($p));
$router->post('/banking/{id}/verfahren/abfragen',
fn(array $p) => (new BankingController())->verfahrenAbfragen($p));
$router->get('/banking/{id}/konten', fn(array $p) => (new BankingController())->konten($p));
$router->post('/banking/{id}/konten', fn(array $p) => (new BankingController())->kontenSpeichern($p));
$router->post('/banking/{id}/konten/abfragen',
fn(array $p) => (new BankingController())->kontenAbfragen($p));
$router->get('/verwaltung/import', fn() => (new ImportController())->index());
$router->post('/verwaltung/import/hochladen', fn() => (new ImportController())->hochladen());
$router->post('/verwaltung/import/ausfuehren', fn() => (new ImportController())->ausfuehren());
$router->post('/verwaltung/import/{datei}/loeschen', fn(array $p) => (new ImportController())->loeschen($p));
// Der Filter kommt als POST: Sind viele Kategorien angehakt, wird eine
// GET-Adresse länger, als der Webserver annimmt. Die GET-Route bleibt für den
// ersten Aufruf und für kurze Adressen, die man sich merken mag.
$router->get('/verwaltung/export', fn() => (new ExportController())->index());
$router->post('/verwaltung/export', fn() => (new ExportController())->index());
$router->get('/verwaltung/export/datei', fn() => (new ExportController())->datei());
$router->post('/verwaltung/export/datei', fn() => (new ExportController())->datei());
try {
$router->dispatch();
} catch (\Throwable $e) {
DebugLog::fehler($e);
http_response_code(500);
(new View())->render('layout/500.php', []);
}