# Änderungen ## Termine und Verträge wandern unter die Verwaltung (13.08.2026) Die Seitenleiste war lang geworden. *Termine* und *Verträge* sind kein täglicher Einstieg – die Reihen laufen von selbst, Verträge sind Stammdaten – und stehen deshalb nicht mehr im Hauptmenü, sondern als letzte Kacheln in der Verwaltung. **Navigation** (`Views/layout/kopf.php`) - Die Menüpunkte *Termine* und *Verträge* entfallen. - Ein Menüeintrag kann jetzt weitere Pfade mitführen (`auch`), unter denen er als aktiv gilt. *Verwaltung* leuchtet damit auch auf `/termine`, `/termine/kalender`, `/termine/erinnerungen` und unterhalb von `/vertraege` – sonst stünde die Seitenleiste dort ohne Markierung. **Verwaltung** (`Views/verwaltung/index.php`, `VerwaltungController`) - Neue Kachel *Termine* mit der Zahl der aktiven Reihen, den offenen Erinnerungen und Wegen zu regelmäßigen Buchungen, Finanzkalender und Erinnerungen. - Neue letzte Kachel *Verträge* mit der Zahl der laufenden Verträge, den in den nächsten 90 Tagen ablaufenden Kündigungsfristen und Wegen zu Verträgen, Adressen, Sparzielen und Krediten. - Beide Kacheln führen mehrere Wege und nutzen deshalb kleine Knöpfe und einen knappen Beitext. - Alle übrigen Kacheln ziehen nach: *Kategorien*, *Klassen*, *Konten*, *QIF-Import* und *Online-Banking* tragen jetzt dieselben kleinen Knöpfe. Vorher standen zwei Größen nebeneinander im selben Raster. **Knopfleisten stehen am Kachelfuß** (`static/app.css`) - Das Raster der Verwaltung heißt `kachelraster fussbuendig`: die Kacheln werden zu Flexspalten, der freie Platz sammelt sich über der Knopfleiste. Die Knöpfe stehen damit in jeder Kachel einer Zeile auf gleicher Höhe unten, statt der jeweiligen Textlänge zu folgen. Der Abstand zum Text steckt im Padding – `margin-top: auto` schluckt den Außenabstand und ließe die Knöpfe in der höchsten Kachel sonst am Text kleben. **Knopfleisten brechen um** (`static/app.css`) - `.knopfleiste` bekommt `flex-wrap: wrap`. Bisher ragten Leisten mit mehreren Knöpfen bei schmalen Kacheln über den rechten Rand hinaus, statt in die nächste Zeile zu rutschen. Das gilt für alle Knopfleisten, nicht nur die der Verwaltung. Alle Adressen unter `/termine` und `/vertraege` bleiben unverändert erreichbar, ebenso die Reiter innerhalb der Seiten und die Verlinkungen aus Cockpit und Buchungsvorlagen. **Test:** `tests/24_NavigationTest.php` ## QIF-Import: nur ausgewählte Konten übernehmen (12.08.2026) Der QIF-Import kannte bisher nur den Zeitraum als Filter – ein Export ließ sich nur ganz einlesen. Jetzt lassen sich einzelne Konten wählen, etwa um ein Konto nachzuziehen, ohne die übrigen anzufassen. **Kontoauswahl im Importeur** (`QifBuchungsImport`) - Neue Option `konten`. Maßgeblich ist nicht allein der `!Account`-Abschnitt, sondern jedes Konto, das der Eintrag berührt: das eigene, die Umbuchungsziele aus `L` und den Splitzeilen und beim Wertpapiergeschäft das Verrechnungskonto. Eine Umbuchung steht nur einmal in der Datei – sonst fehlte dem gewählten Konto die Gegenseite. - Die Anfangssalden werden weiter über die ganze Datei gerechnet, aber nur für die gewählten Konten geschrieben. Ein Teilimport überschreibt so keine fremden Salden. - Die Fingerabdrücke der Buchungen entstehen vorab in einem eigenen Durchgang. Der laufende Zähler für gleiche Fingerabdrücke hing sonst an der Auswahl, und dieselbe Buchung wäre bei einem zweiten Lauf mit anderer Auswahl doppelt angelegt worden. - `kontenDerDatei()` nennt die Konten eines Exports samt der Zahl der Einträge, die sie berühren – auch die, die nur Umbuchungsziel sind und keine eigene Zeile führen. **Dialog** (`Views/verwaltung/import.php`, `ImportController`) - Nach dem Trockenlauf steht unter *Konten* die Ankreuzliste der Konten dieser Datei; vor dem ersten Lesen gibt es nichts zu wählen. Von dort geht es weiter zum erneuten Trockenlauf mit der Auswahl oder zum Übernehmen. - Die Vorschau zählt mit, wie viele Buchungen zu keinem der gewählten Konten gehören. - Was im Formular ankommt, wird gegen die Konten der Datei geprüft. **Kommandozeile** (`scripts/qif-import.php`) - `--konto=NAME` (mehrfach angebbar) grenzt den Import ein, `--konten` nennt die Konten der Datei und beendet sich. Ein Name, den die Datei nicht führt, bricht ab – sonst liefe der Import durch und legte nichts an. ## Umbuchungen: Gegenkonto sichtbar, Zuordnung im Kontoblatt (12.08.2026) Eine Umbuchung war auf dem Gegenkonto nicht mehr zuzuordnen: Die Zeile trug die Marke *Umbuchung*, die Kategoriespalte daneben blieb leer. Zu sehen etwa bei den Buchungen zwischen DKB Girokonto und Paypal – die Daten waren richtig, die Anzeige verschwieg sie. **Beide Sichten nennen das Gegenüber** (`Views/buchungen/kontoblatt.php`) - In der Kategoriespalte steht jetzt auch in der Gegensicht das Konto in eckigen Klammern, von dem die Umbuchung kommt. Die Abfrage lieferte das Konto längst mit; die Ansicht hat es nur nicht gezeigt. **Kategorie und Gegenkonto in einem Feld** (`Views/buchungen/zuordnungsliste.php`, `BuchungenController`) - Unter den Kategorien stehen die eigenen Konten in eckigen Klammern – wie im FinanzManager, dessen Kategoriefeld `[Konto]` annimmt. Eines davon macht die Buchung zur Umbuchung. Die Auswahl *Ausgabe / Einnahme* bestimmt seither nur noch die Richtung; das eigene Konto fehlt in der Liste. - Dieselbe Liste steht in den Splitzeilen. Eine Splitzeile mit Gegenkonto ließ sich bisher nur im Import anlegen und ging beim Bearbeiten der Buchung stillschweigend verloren. **Schnellzuordnung im Kontoblatt** (`public/static/kontoblatt.js`, `Buchung::zuordnen()`) - Ein Klick auf die Kategoriespalte öffnet die Liste in der Zelle: leerer Eintrag, *Splitbuchung*, Kategorien, Konten. Die Auswahl speichert sofort (`POST /buchungen/{id}/zuordnung`) und kommt in dieselbe Ansicht zurück – Filter und Seite hängen am Ziel. - Die Liste gibt es einmal und wandert in die angeklickte Zelle. Eine Liste je Zeile wären bei hundert Zeilen ebenso viele vollständige Kategoriebäume im HTML. - Feld und Liste sind gleich breit und gleich hoch, die Zelle zeigt nur, was auch in der Liste steht – sonst rückte beim Anklicken die halbe Zeile. - Ohne JavaScript führt derselbe Klick in die Buchungsmaske: Die Zelle ist ein Link. - Auch die Gegenseite einer fremden Umbuchung ist anklickbar; sie führt zu ihrer Buchung auf dem anderen Konto, wo sie geändert wird. **Splitdialog** (`Views/buchungen/kontoblatt.php`, `Buchung::splitsErsetzen()`) - Der Eintrag *Splitbuchung* öffnet ein modales Fenster mit Zeilen für Kategorie oder Gegenkonto, Klasse, Notiz und Betrag; der Restbetrag wird mitgerechnet, weitere Zeilen kommen auf Knopfdruck. Eine bestehende Aufteilung steht schon drin. - Ein Klick auf eine bereits gesplittete Buchung öffnet direkt den Dialog statt der Auswahlliste: Für sie hatte die Liste nichts anzubieten außer dem Weg dorthin. - Rechts unten im Dialog steht *Splitbuchung löschen*. Der Knopf schickt dasselbe Formular mit `aktion=loeschen`, verwirft alle Zeilen samt ihren Zuordnungen – auch die Gegenseite einer Splitzeile mit Gegenkonto – und die Buchung steht danach unzugeordnet im Kontoblatt. Er erscheint nur dort, wo es etwas aufzuheben gibt, und steht abseits von *Speichern* und *Abbrechen*. - Bleiben alle Zeilen leer, wird die Splitbuchung ebenso aufgehoben. - Beide Wege achten den Monatsabschluss und lassen den Rest der Buchung unberührt. `public/static/betrag.js` bündelt das Lesen deutscher Beträge und die Restbetragsrechnung für Maske und Kontoblatt. `tests/23_ZuordnungTest.php` prüft Zuordnung, Splitzeilen und Gegensicht; `tests/e2e/specs/buchungen.spec.js` die drei Wege im Browser. ## Wertpapiere: Depotbuchungen sammelweise löschen (12.08.2026) Die Liste der Depotbuchungen räumt jetzt so auf wie das Kontoblatt – nach einem Import, der schiefging, war das Löschen Zeile für Zeile mühsam. **Auswahl löschen** (`Views/wertpapiere/buchungen.php`, `WertpapiereController`, `public/index.php`) - Ankreuzfeld je Zeile, das Feld in der Titelzeile wählt alle angezeigten Buchungen. Der Knopf steht oben in der Aktionsleiste, nennt die Anzahl und bleibt ohne Auswahl gesperrt; die Rückfrage nennt sie ebenfalls. Ohne JavaScript funktioniert alles weiter: Dann ist der Knopf offen und `POST /wertpapiere/buchungen/loeschen` weist eine leere Auswahl ab. - Mit jeder Depotbuchung geht ihre Geldbuchung – wie beim Löschen einer einzelnen. - **Am Bestand ist nichts nachzuziehen.** Er wird bei jedem Aufruf aus den Depotbuchungen gerechnet und gibt die Löschung von selbst wieder her, ebenso realisierter Gewinn, Veräußerungen und Erträge. - Der eingestellte Filter (Depot, Wertpapier, Vorgang) hängt am Ziel des Formulars, deshalb steht danach wieder dieselbe Ansicht da. Das gilt jetzt auch für das *Löschen* je Zeile, das bisher zur ungefilterten Liste zurücksprang. `tests/22_DepotbuchungenLoeschenTest.php` prüft Geldseite, Bestand und Auswertungen nach dem Löschen; `tests/e2e/specs/module.spec.js` den Ablauf im Browser. ## Wertpapiere: Depotbestand aus einer CSV-Datei übernehmen (12.08.2026) Ein Depot, das nie im FinanzManager geführt wurde, lässt sich jetzt in einem Zug einlesen – aus der Bestandsliste, die die Bank zum Herunterladen anbietet. **Neuer Knopf *Import*** (`Views/wertpapiere/index.php`, `Views/wertpapiere/import.php`) - Steht auf der Bestandsseite neben *Depotbuchung erfassen*. Der Ablauf ist zweistufig: hochladen, Vorschau prüfen, übernehmen. Die Vorschau zeigt je Position Stück, beide Kurse, Einstand, Wert und ob sie neu ist oder schon einmal importiert wurde. - Vor der Übernahme lassen sich Depot, Buchungsdatum und Verrechnungskonto noch ändern. **Der Import** (`Services\DepotbestandImport`, `WertpapiereController`) - Erwartet die Spalten Wertpapierbezeichnung, WKN, ISIN, Einstiegskurs, Bewertungskurs, Stückzahl und Assetklasse; Trennzeichen und Zeichensatz erkennt er selbst. Beträge wie `"1.471,88 €"` und Bruchstückzahlen wie `55,6863` werden gelesen, wie sie kommen. - **Die Vergangenheit fehlt im Export.** Er beschreibt nur einen Stichtag. Je Position entsteht deshalb *eine* Depotbuchung: ein Kauf zum Stichtag über die volle Stückzahl zum Einstiegskurs. Der Einstand stimmt damit auf den Cent; frühere Käufe, Verkäufe und Dividenden bleiben leer. - **Der Bewertungskurs wird als Kurs des Stichtags hinterlegt.** Ohne ihn stünde der frisch übernommene Bestand sofort wieder ohne Bewertung da. Am echten Export nachgerechnet: 33.176,12 € Einstand, 47.120,25 € Wert – die Bank weist zehn Cent weniger Gewinn aus, was an ihren auf zwei Stellen gerundeten Kursen liegt. - **Kein Geldfluss.** Ein Verrechnungskonto ist wählbar, bleibt aber vorbelegt leer: Das Geld ist vor Jahren geflossen, eine Belastung zum heutigen Tag würde das Girokonto verfälschen. - Ohne ausdrückliche Wahl findet der Import das Depot über die **Kontonummer**, die zur Depotnummer der Datei passt. Ein `import_hash` je Position verhindert, dass dieselbe Datei zweimal denselben Bestand anlegt. - Assetklassen werden übersetzt: *Aktien* → Aktie, *ETFs* und *Fonds* → Fonds, *Anleihen* → Anleihe, *Zertifikate* → Zertifikat. **Neue Herkunft „CSV-Import“** (`Models\Buchung`) - `Buchung::QUELLEN` kennt jetzt `csv`; Depot- und Geldbuchungen aus dem Bestandsimport sind damit als solche erkennbar und fallen nicht mehr auf „manuell erfasst“ zurück. `tests/21_DepotbestandImportTest.php` prüft das an einem Ausschnitt des echten Exports. ## Online-Banking: Umsätze, die niemand mehr zu sehen bekam (12.08.2026) Drei Fehler, die zusammen dazu führten, dass abgerufene Umsätze spurlos verschwanden: Der Abruf meldete sie als schon vorhanden, im Kontoblatt stand nichts, und in der Vorschlagsliste tauchten sie ebenfalls nicht auf. **Eine gelöschte Buchung gibt ihren Umsatz wieder frei** (`Models\Buchung`, `Models\Bankumsatz`) - Der Fremdschlüssel setzte beim Löschen zwar `bank_umsaetze.buchung_id` auf NULL, ließ den Status aber auf *übernommen* stehen. Der Umsatz war damit unsichtbar: keine Buchung, kein Eintrag in der Vorschlagsliste, und beim nächsten Abruf über den Fingerabdruck als schon vorhanden übersprungen. `Buchung::loeschen()` setzt ihn jetzt vorher auf *offen* zurück. - `Bankumsatz::verwaisteOeffnen()` fängt die Wege ab, die daran vorbeiführen: Wird ein Konto gelöscht, räumt der Fremdschlüssel seine Buchungen per CASCADE ab – und Kaskaden lösen in MariaDB keine Trigger aus. Der Aufruf steht vor dem Abruf und vor der Vorschlagsliste, damit sich dieser Zustand von selbst wieder auflöst. **Ein Bankkonto entsteht nicht mehr doppelt** (`Models\Bankzugang`) - Gesucht wird über die **Kontonummer**, nicht über IBAN und Kontonummer zusammen. Manche Banken liefern die IBAN nicht bei jedem Dialog mit – die Comdirect nennt sie beim Kontenabruf, beim Umsatzabruf aber nicht. So entstand beim zweiten Abruf ein zweiter Eintrag fürs selbe Konto: einmal mit IBAN, einmal ohne, mit eigener Zuordnung und eigenem Fingerabdruck. Jeder Umsatz stand daraufhin doppelt da. - Liefert die Bank die IBAN erst später, wird sie am vorhandenen Eintrag nachgetragen. **Der Fingerabdruck hängt am Bankkonto, nicht an der IBAN** (`Models\Bankumsatz`) - `Bankumsatz::externId()` nimmt die `bank_konto_id` statt IBAN oder Kontonummer. Welche Kennung eine Bank liefert, wechselt von Dialog zu Dialog; mit ihr kippte der Fingerabdruck, und derselbe Umsatz kam ein zweites Mal herein. Die Kennung der Zeile in `bank_konten` steht dagegen fest, solange es den Bankzugang gibt. ## Kontoblatt: Buchungen sammelweise löschen, Meldungen ohne Sprung (12.08.2026) **Mehrfachauswahl im Kontoblatt** (`Views/buchungen/kontoblatt.php`, `BuchungenController`) - Links vor dem Datum steht je Buchung ein Ankreuzfeld, in der Titelzeile eines für alle gerade angezeigten Buchungen. Es zeigt auch den Zwischenstand an, wenn nur ein Teil ausgewählt ist. - Neben *Kontenübersicht* löscht **Auswahl löschen** die angekreuzten Buchungen nach einer Rückfrage, die die Anzahl nennt. Ohne Auswahl bleibt der Knopf gesperrt; ohne JavaScript weist der Controller eine leere Auswahl ab. - Die Gegenseite einer fremden Umbuchung bekommt kein Ankreuzfeld – sie gehört dem Ursprungskonto und wird auch weiterhin nur dort gelöscht. - Nach dem Löschen erscheint wieder dieselbe Ansicht: Filter und Seite hängen am Ziel des Formulars und werden aus bekannten Feldern neu zusammengesetzt, nicht übernommen. **Statusmeldungen liegen über der Ansicht** (`Views/layout/meldungen.php`) - Die Meldungen schieben sich von ganz oben ins Bild, statt im Inhalt Platz zu belegen. Der Inhalt bleibt beim Ein- und Ausblenden an Ort und Stelle – vorher rückte die halbe Seite auf und beim Verschwinden wieder zurück. - Eine Meldung ist so breit wie ihr Text und bricht erst bei 80 % Fensterbreite um. - Erfolgsmeldungen gehen nach fünf Sekunden von selbst, alle Meldungen lassen sich anklicken – eine stehenbleibende Fehlermeldung verdeckte sonst die Werkzeugleiste. - Beide Rahmen (`kopf.php`, `schlicht.php`) binden dieselbe Teilansicht ein. ## Online-Banking: Kontenzuordnung als dritter Schritt, Umsätze buchen sich selbst (12.08.2026) **Einrichtung in drei Schritten** (`Views/banking/konten.php`, Migration `010_kontenzuordnung.sql`) - Nach Zugangsdaten (1) und TAN-Verfahren (2) folgt die **Kontenzuordnung** (3): Welches Bankkonto gehört zu welchem eigenen Konto? Die Liste kommt frisch von der Bank – das Übernehmen des Verfahrens startet die Abfrage gleich mit, samt TAN-Maske, wenn die Bank eine verlangt. - Fehlt ein passendes eigenes Konto, legt *neues Konto anlegen* es an: IBAN, Kontonummer und Bank stehen schon fest, einzutragen bleiben Name, Kontoart und Anfangssaldo. Letzterer deckt die Zeit ab, die der Abruf nicht mehr hergibt. - Die Zuordnung ist von der Zugangsmaske dorthin umgezogen – dort stand sie in einer Tabelle, die nur zuordnen, aber nichts anlegen konnte. Geblieben ist eine Übersicht mit Verweis. - `bank_umsaetze.bank_konto_id` merkt sich, von welchem Bankkonto ein Umsatz stammt. Wird die Zuordnung erst nach einem Abruf getroffen, ziehen die **offenen** Umsätze mit; übernommene bleiben, wo sie sind – ihre Buchung steht bereits in einem Kontoblatt. - Der Abruf kennt dafür einen kurzen Modus (`FinTsAbruf::starten($id, 'konten')`), der nach der Kontenabfrage anhält. Dieselbe Zustandsmaschine, dieselbe TAN-Behandlung. - Am Ende der Einrichtung geht es zurück in die **Übersicht der Bankverbindungen** – dort steht die neue Verbindung fertig zwischen den anderen. Die Zugangsmaske böte nur noch an, was gerade durchlaufen wurde. Dasselbe gilt beim vorzeitigen Verlassen über *Später einstellen* und *Später zuordnen*; einzeln aufgerufen bleibt die Zuordnung auf ihrer Seite. **Abgerufene Umsätze buchen sich selbst** (`Services\Bankabgleich`) - Neue Einstellung *Abgerufene Umsätze selbst buchen* (`banking_automatisch_buchen`, Vorgabe an). Nach jedem Abruf – und nach dem Speichern der Kontenzuordnung – landen die Umsätze im zugeordneten Konto: verbunden mit einer schon erfassten Buchung, wenn eine passt, sonst als neue Buchung mit der Kategorie aus der Regel, sonst ohne. - Der Lauf läuft **einmal am Ende**, nicht je Konto: So sieht er alle Umsätze und kann jeden mit der Buchung verbinden, die zu ihm passt. - Ein einzelner Umsatz bricht den Lauf nicht ab. Ein abgeschlossener Monat oder ein doppelter Fingerabdruck betrifft nur ihn; er bleibt offen und steht weiter in der Vorschlagsliste. Die Meldung nennt die ersten Fälle. - Umsätze ohne zugeordnetes Konto bleiben liegen – ohne Konto gibt es kein Kontoblatt, in das sie gehörten. - *Alles übernehmen* in der Vorschlagsliste bleibt zurückhaltender und lässt liegen, was keine Regel trifft: Wer den Dialog öffnet, will die Zuordnung selbst treffen. Beide Wege teilen sich jetzt denselben Dienst. **Kontenübersicht: fett heißt „hier liegt Arbeit“** - Konten mit Buchungen aus dem laufenden Jahr ohne Kategorie stehen **fett** (`Buchung::unkategorisierteJeKonto()`), mit der Zahl im Titel. Dieselbe Zählweise wie im Cockpit: keine Kategorie, kein Gegenkonto, nicht gesplittet – eine Umbuchung oder eine Splitbuchung ist zugeordnet. - Das ist die Gegenprobe zum automatischen Buchen: Die Umsätze stehen sofort im Kontoblatt, und die Übersicht zeigt, wo die Kategorie noch fehlt. ## Phasen 4 bis 7 – Cockpit, Online-Banking, Wertpapiere, Nebenmodule (12.08.2026) Damit ist der geplante Funktionsumfang beisammen: Von der Kontenübersicht bis zur Überweisung läuft alles in der Anwendung. ### Phase 4 abgeschlossen – die übrigen Grafiken und das Cockpit **Konfigurierbare Kacheln** (`src/Services/Cockpit.php`) - Fünf Kacheln: Vermögensentwicklung, Einnahmen und Ausgaben, Meine Ausgaben, Kontostände, Offene Vorgänge. Die Auswahl steht im Einstellungsdialog unter *Darstellung*; abgewählte Kacheln werden nicht nur ausgeblendet, sondern gar nicht erst berechnet. - Die Reihenfolge im Raster kommt aus dem Katalog, nicht aus der Reihenfolge der Ankreuzfelder – letztere wäre für den Nutzer keine sinnvolle Sortierung. **Einnahmen und Ausgaben je Monat** - Balkenpaare über zwölf Monate, darüber der Saldo als Linie. - Die Zahlen kommen aus dem Summenbericht, nicht aus einer eigenen Abfrage: So zählt die Kachel dieselben Bewegungen wie der Bericht und beide laufen nie auseinander. **Meine Ausgaben als Ring** - `Auswertung::verteilung()` fasst auf der **obersten Kategorie** zusammen – „300 - Wohnen“ statt fünfzehn Unterkategorien. Höchstens acht Segmente, der Rest wird gesammelt. - Negative Kategoriesalden bekommen kein Segment, zählen aber in der Summe mit; sonst ergänzten sich die Anteile nicht mehr zu hundert Prozent. - Sonderfall Vollkreis: Bei einer einzigen Kategorie fielen Anfangs- und Endpunkt des Bogens zusammen und es entstünde keine Fläche – deshalb zwei Halbringe. **Offene Vorgänge** - Nicht kategorisierte Buchungen, nicht abgeglichene Buchungen, fälliger Monatsabschluss – später ergänzt um offene Bankumsätze, fällige Termine und Erinnerungen. ### Phase 5 – Online-Banking über FinTS **Die Bibliothek als eigener Fork** - `nemiah/php-fints` liegt als Fork auf Codeberg (`fds2610-bot/phpFinTS`), eingebunden über ein VCS-Repository, Version fest auf `4.1.1` gepinnt. - **psr/log ist dort direkt eingebaut** (acht Interface-Dateien unter `psr-log/`, eigener Autoload-Eintrag, kein `require` mehr). Der Finanzmanager hängt damit an keinem fremden Update-Zyklus; wie der Eingriff beim Nachziehen zu wiederholen ist, steht im Fork. **Zugangsdaten** - Migration `006_banking.sql`: `bank_zugaenge`, `bank_konten`, `bank_umsaetze`, `abgleich_regeln`, `bank_abrufe`. - PIN und TAN-Medium AES-256-GCM-verschlüsselt (`Services\Crypto`, übernommen aus `Steuerkram`). Der Schlüssel kommt aus `APP_KEY` oder aus `data/schluessel/app_key`, den der Entrypoint beim ersten Start erzeugt – eigenes Bind-Mount, damit er einen Neubau des Images überlebt. - Ein leeres PIN-Feld heißt „unverändert“, nicht „PIN löschen“. **Abruf über mehrere Requests** - Fast jede Bank verlangt schon beim Login eine TAN. `Services\FinTsAbruf` sichert den Dialog (`FinTs::persist()`) und die laufende Aktion in der Sitzung und macht nach der Eingabe an derselben Stelle weiter. Ein wartender Prozess wäre mit php-fpm der falsche Weg. - Klassische Verfahren zeigen die photoTAN-Grafik, entkoppelte (pushTAN) fragen auf Knopfdruck nach, statt zu warten. - Umsätze, die vor einer TAN-Unterbrechung geschrieben wurden, bleiben erhalten. **Wahl des TAN-Verfahrens** (`Services\FinTsVerfahren`, Migration `009_tan_verfahren.sql`) - **Einrichtung in zwei Schritten**: Zugangsdaten erfassen und mit *Weiter* speichern, dann das TAN-Verfahren. Vorher lässt sich die Bank nicht fragen – ohne gespeicherte Zugangsdaten gibt es keine Verbindung. - Der zweite Schritt holt die Liste beim Aufruf selbst (`getTanModes()`, ohne Anmeldung und ohne TAN); ein Knopf davor wäre ein Schritt, dessen Ergebnis feststeht. Ein erneuter Anlauf steht daneben. TAN-Medien werden nur für die Verfahren geholt, die eines brauchen. - Das Ergebnis liegt in der Sitzung, auch ein gescheitertes – sonst liefe jeder Seitenaufruf wieder in dieselbe Zeitüberschreitung. Die sind für die Abfrage kürzer gesetzt als in der Bibliothek (8/20 statt 15/30 Sekunden), weil sie an einem Seitenaufruf hängt. - Scheitert die Abfrage, erklärt die Seite, wo der Fehler meist steckt, und verlinkt zurück auf die Zugangsdaten; überspringen lässt sich der Schritt ebenfalls. - Abgefragt statt eingetippt: Die Kennungen sind bankspezifisch, 913 heißt nicht überall chipTAN QR. Der Name wird mitgespeichert (`tan_verfahren_name`), damit das Formular ihn ohne Bankverbindung zeigen kann. - Beim Übernehmen wird gegen die abgefragte Liste geprüft; ein Verfahren ohne Medienzwang bekommt kein Medium angehängt, weil manche Banken den Dialog sonst ablehnen. - Ohne Wahl entscheidet weiterhin das erste gemeldete Verfahren – diese Notwahl wird festgehalten, eine ausdrückliche Wahl dagegen **nie überschrieben**. - Behoben: Das Speichern des Zugangsformulars setzte Verfahren und TAN-Medium zurück, weil beide Felder darin nicht vorkommen. Sie werden jetzt wie die PIN unter Vorbehalt gestellt. - Abruf und Überweisung benutzen dieselbe Wahl; die doppelte Auswahllogik in `FinTsUeberweisung` ist entfallen. **Abgleich statt Direktbuchung** - Abgerufene Umsätze landen in `bank_umsaetze` und werden nicht sofort gebucht. Die Vorschlagsliste kennt drei Wege: mit einer erfassten Buchung **verbinden**, als neue Buchung **übernehmen** oder **verwerfen**. - Gleiches Konto, gleicher Betrag, Datum innerhalb der Toleranz (Vorgabe fünf Tage, einstellbar) – so findet der Abgleich die schon erfasste Buchung. Eine bereits verbundene Buchung wird nie ein zweites Mal vorgeschlagen. - `extern_id` verhindert, dass derselbe Umsatz bei überlappenden Abrufzeiträumen zweimal hereinkommt. **Regeln** - Muster auf Empfänger und/oder Verwendungszweck, vier Vergleichsarten samt regulärem Ausdruck, Betragsgrenzen mit Vorzeichen, Wirkung auf Kategorie, Klasse, Gegenkonto oder ein festes Split-Schema in Prozent. - Die erste passende Regel gewinnt (nach Priorität) – kein Sammeln über mehrere Regeln, sonst wäre nicht mehr nachvollziehbar, warum am Ende die eine Kategorie dasteht. - Reguläre Ausdrücke werden beim Speichern geprüft; ein kaputtes Muster brächte sonst jeden Abruf zum Stehen. ### Phase 6 – Wertpapiere und Depots - Migration `007_wertpapiere.sql`: `wertpapiere`, `depot_buchungen`, `kurse`. - **Zwei Seiten je Geschäft**: die Depotbuchung mit Stück und Kurs, die Geldbuchung auf dem Verrechnungskonto. `betrag` ist der Bruttowert, Gebühren und Steuern stehen daneben – nur so enthält der Einstand die Nebenkosten genau einmal. - **Einstand nach gleitendem Durchschnitt**, realisierte Gewinne beim Verkauf. (Für die Steuer gilt FIFO; maßgeblich ist dort die Abrechnung der Bank.) - Ohne Kurs wird nicht bewertet: Die Position sagt „kein Kurs“, statt den Einstand als Ersatzwert auszugeben. - **Kursabruf** über Leeway.tech oder Financial Modeling Prep, Kürzel wird nach dem ersten Fund gespeichert. Abgerufen wird nur für Papiere mit Bestand und nur einmal am Tag; täglicher Lauf über `scripts/kursabruf.php`. - **QIF-Import für `!Type:Invst`**: Wertpapier, Depotbuchung und Geldbuchung in einem Zug. - Depots erscheinen in der Kontenübersicht mit ihrem bewerteten Bestand. **Dabei behoben:** Der Import buchte den Geldfluss *jedes* Wertpapiergeschäfts als Abgang – auch bei Verkäufen und Dividenden. Das QIF nennt im Feld `$` immer einen positiven Betrag, die Richtung steckt allein im Vorgang. Zudem wurde das Verrechnungskonto nicht erkannt, wenn dem Konto im `L`-Feld eine Kategorie vorangeht (`LKursgewinne:Realisierte Gewinne|[Konto]`) – bei Verkäufen also immer. ### Phase 7 – Nebenmodule und Zahlungsverkehr - Migration `008_nebenmodule.sql`: `buchungsvorlagen`, `regelmaessige_buchungen`, `erinnerungen`, `adressen`, `vertraege`, `sparziele`, `kredite`, `zahlungsauftraege`. - **Buchungsvorlagen** inklusive QIF-Import (`!Type:Memorized`); Splitzeilen werden bei abweichendem Betrag im Verhältnis mitskaliert. - **Regelmäßige Buchungen** mit Fälligkeitslauf: automatisch oder auf Bestätigung, jeder zurückliegende Termin wird einzeln gebucht, abgeschlossene Monate bleiben unangetastet. - **Finanzkalender** aus vier Quellen – gerechnet, nicht gespeichert. - **Adressen** mit IBAN-Prüfung nach ISO 13616 (Länge je Land, Prüfziffer modulo 97). - **Verträge** mit Kündigungsfrist und Jahresbetrag; ablaufende Fristen stehen oben und im Kalender. - **Sparziele**, deren Stand aus dem Kontosaldo kommt statt aus einem zweiten Zähler. - **Kredite** mit gerechnetem Tilgungsplan (Annuität, Sondertilgung, Restschuld zum Zinsbindungsende). Deckt die Rate die Zinsen nicht, gibt es einen Hinweis statt eines Plans. - **Zahlungsverkehr**: Überweisungen und Terminüberweisungen über FinTS. Die Buchung entsteht erst, wenn die Bank den Auftrag angenommen hat; gesendete Aufträge bleiben als Beleg unveränderlich stehen. - **SEPA-XML selbst erzeugt** (`Services\SepaXml`, pain.001.001.03) statt einer weiteren Abhängigkeit – samt Umschrift der Umlaute in den SEPA-Zeichenvorrat. ### Nebenbei - `.dockerignore` ergänzt: Ohne sie schickte der Build das ganze Projektverzeichnis an den Daemon, einschließlich `data/mariadb` – der Build brach mit „permission denied“ ab. - `PlatzhalterController` entfernt: Alle Navigationspunkte haben jetzt ihr Modul. - Neue Reiter im Einstellungsdialog: *Online-Banking* und *Wertpapiere*. **Doku:** `docs/cockpit.md`, `docs/banking.md`, `docs/wertpapiere.md`, `docs/nebenmodule.md`. **Tests:** 15 bis 18 (Cockpit, Banking, Wertpapiere, Nebenmodule) – zusammen 888 Prüfungen. ## Phase 4 – Auswertungen mit Berichtsmodul (11.08.2026) **Datenmodell** - Migration `005_berichte.sql`: `berichte`, `berichtsgruppen` - Ein Bericht ist Name, Typ und Filter – die Zahlen entstehen bei jedem Aufruf neu aus den Buchungen. Gespeichert wird ausschließlich die Frage. - Der Filter liegt als JSON in einer Spalte und läuft beim Lesen immer durch `Auswertung::normalisiere()`: Unbekannte Felder fallen weg, unbekannte Werte auf die Vorgabe zurück. Keine eigene Spalte je Filterfeld – das sind Anzeigeparameter ohne Fremdschlüssel, und jede neue Filtermöglichkeit bräuchte sonst eine Migration. - Keine neuen Einstellungen: Zeitraum, Zeilen-Summe und Einrückung gehören zum einzelnen Bericht, nicht in den Einstellungsdialog. **Fünf Berichtstypen** (`src/Services/Auswertung.php`) - **Summenbericht**: Summen je Zeilen-Summe und Spalte, getrennt nach Einnahmen und Ausgaben, mit Schlusszeile *Gesamt Einnahmen − Ausgaben* - **Buchungsbericht**: die Einzelbuchungen, gruppiert mit Zwischensummen - **Vergleichsbericht**: Zeitraum gegen denselben Zeitraum des Vorjahres, mit Differenz und Veränderung in Prozent - **Kontostandsbericht**: Salden je Konto zu den Enden der Spaltenzeiträume - **Budgetbericht**: Budget gegen Ist je Kategorie, mit Saldo **Was gezählt wird** - Bewegungen statt Buchungen: nicht gesplittete Buchungen mit ihrem Betrag, gesplittete über ihre Splitzeilen – sonst zählte eine Splitbuchung doppelt oder gar nicht. - **Umbuchungen bleiben außen vor** (weder Einnahme noch Ausgabe, wie im Budgetplaner). Nur der Buchungsbericht führt sie auf Wunsch mit; sie erscheinen dort mit dem Gegenkonto in eckigen Klammern. - Unkategorisierte Buchungen stehen als eigene Zeile *Nicht kategorisiert* am Ende ihres Abschnitts, statt lautlos aus der Summe zu fallen. - Einnahme oder Ausgabe entscheidet die **Art der Kategorie**, nicht das Vorzeichen: Eine Erstattung ist eine negative Ausgabe und gehört unter die Ausgaben. - Aggregiert wird in PHP. Die Datenbank liefert die Bewegungen, das Einsortieren in Zeilen und Spalten passiert im Service – Baumsummen, Vorzeichenregeln und Spaltenraster wären in SQL erheblich schwerer nachzulesen. **Vorzeichen** - Intern durchgehend Buchhaltung (Ausgaben negativ), in der Anzeige dreht der Ausgabenabschnitt sein Vorzeichen – wie im FinanzManager steht dort 636,39 statt −636,39. Die Schlusszeile rechnet mit den echten Werten. - Buchungs- und Budgetbericht sowie der Drilldown behalten das Vorzeichen: Sie haben keine Abschnitte, die es erklären. **Zeitraum und Spalten** (`src/Services/Zeitraum.php`, reine Kalenderrechnung ohne Datenbank) - Neun Vorgaben von *Aktueller Monat* bis *Gesamter Zeitraum*; sie rechnen sich bei jedem Aufruf neu aus dem Tagesdatum aus. Ein Bericht mit *Letzter Monat* zeigt im August den Juli, nicht für immer den Monat seiner Entstehung. - Spalten-Aufteilung nach Monat, Quartal oder Jahr; die Randspalten werden auf den Zeitraum gestutzt. - *Gesamter Zeitraum* nimmt die Datumsgrenzen der Buchungen (`Buchung::datumsgrenzen()`) und endet spätestens heute – ein Ende in der Zukunft brächte nur leere Spalten. - Beim Vorjahresvergleich bleibt ein Monatsende ein Monatsende, sonst träfe der 28.02. auf einen 29.02. **Summenbericht als Baum** - Jede Kategorie zeigt ihre eigenen Werte, Kategorien mit Nachfahren bekommen dahinter *Gesamt <Name>* mit der Astsumme. Eine Elternkategorie ohne eigene Buchungen steht als bloße Kopfzeile da – genau wie im Original. - *ohne Einrückung* schaltet auf eine flache Liste mit vollem Pfad und ohne Astsummen. - Ein Kategoriefilter schließt die Unterkategorien ein: Wer „400 - Leben“ wählt, meint den ganzen Ast. **Oberfläche** - Übersicht mit Kachel *Standardvorlagen* und je einer Kachel pro Berichtsgruppe, Menü *Letzte Berichte*, Verwaltung der Gruppen unter `/auswertungen/gruppen`. - Filterleiste sendet per GET: Jeder Zwischenstand ist eine Adresse, die man sich merken, weitergeben und mit *Speichern unter* festhalten kann. - *Speichern unter* kann die Berichtsgruppe gleich mit anlegen – wer sie erst anderswo erzeugen müsste, verlöre den eingestellten Filter. - Eine gelöschte Gruppe nimmt ihre Berichte nicht mit: Sie rücken unter *Nicht gruppiert*. - Die Aufklappmenüs sind `
`-Elemente, kein JavaScript. - Der Platzhalter für `/auswertungen` entfällt aus dem `PlatzhalterController`. **Drilldown** - Zeilenname und jede Zahl führen in die Buchungen dahinter, die Zahl zusätzlich eingeschränkt auf ihre Spalte. - Gefiltert wird durch Nachsieben derselben Bewegungen, aus denen die Zelle entstand – nicht durch Übersetzen der Zeile in einen neuen Filter. Nur so trifft der Drilldown auch *Nicht kategorisiert* oder eine Astsumme über mehrere Unterkategorien. **CSV-Export** (`src/Services/BerichtExport.php`) - Dieselben Zeilen und Vorzeichen wie am Bildschirm, je Berichtstyp eigene Spalten. - Semikolon, Dezimalkomma, kein Tausenderpunkt, CRLF und BOM – sonst zeigt die Tabellenkalkulation „Übrige“ statt „Übrige“ und liest Beträge als Text. **Tests** - `tests/14_AuswertungTest.php` (142 Prüfungen) und `tests/e2e/specs/auswertungen.spec.js` (7 Abläufe) - Einzelheiten in `docs/auswertungen.md` ## Cockpit: Vermögensentwicklung als gestapeltes Balkendiagramm (11.08.2026) - Die Kachel *Vermögensentwicklung* war ein Platzhalter und zeigt jetzt die Kontostände der letzten zwölf Monate je Monatsende – ein Balken pro Monat, ein Farbton pro Konto (`Vermoegensentwicklung::reihe()`, `src/Views/cockpit/vermoegensdiagramm.php`). - Negative Stände stapeln unter der Nulllinie. - Schalter *nur aktive* oben rechts an der Kachel, im Standard eingeschaltet: Er blendet stillgelegte Konten samt Legendeneintrag aus und merkt sich seinen Zustand über die Einstellungen (`cockpit_vermoegen_nur_aktive`) – auch über das Abmelden hinaus. Er ist das einzige Kriterium; das Σ-Kennzeichen filtert hier **nicht** mit, weil stillgelegte Konten es meist nicht tragen und beim Ausschalten sonst genau das fehlte, was man sehen will. - `Buchung::bewegungenJeKontoUndMonat()` liefert die Monatsbewegungen in einer Abfrage je Quelle – zwölf Stichtagsabfragen wären es sonst. - Mehr Konten als Farbtöne: Die größten behalten ihre Farbe, der Rest wird zu *Übrige Konten (n)* zusammengefasst. Zwei Konten in derselben Farbe wären in der Legende nicht auseinanderzuhalten. - Eigenes SVG, keine Diagrammbibliothek. Die Farben stehen als Klasse am Element und folgen damit dem hellen wie dem dunklen Thema; jedes Balkenstück trägt ein `` als Tooltip, ohne eine Zeile JavaScript. - **Übrige Konten (n)** ist ein Link: Er führt eine Ebene tiefer (`/?ebene=N`) und zeigt die nächstkleineren Konten einzeln – beliebig tief, bis der Rest in die Palette passt. Unter der Legende steht der Weg zurück. Jedes Konto erscheint auf genau einer Ebene, und eine zu hohe Ebene fällt auf die letzte zurück (`Vermoegensentwicklung::ebenen()`). - Die Balken nutzen die Kachelbreite aus, indem sie **auseinanderrücken**, nicht indem sie breiter werden: Die Zeichenfläche rechnet waagerecht in Prozent, die Balkenbreite bleibt bei 16 Punkten. Achsenbeschriftung und Zeichenfläche sind dafür zwei SVGs nebeneinander – ein einzelnes könnte von seinen Prozentangaben keine feste Achsenbreite abziehen. - Die Kachel ist so groß wie ihre Nachbarn: Der Ausgriff über zwei Rasterspalten (`.kachel.breit`) und das engere Cockpit-Raster (`.kachelraster.dicht`) entfallen wieder. - Die Legende steht unter der Zeichnung statt daneben und in kleinerer Schrift (.74rem); sie füllt so viele Spalten, wie die Kachel hergibt. Der Schalter teilt sich die erste Zeile mit der Überschrift, damit er in der schmaleren Kachel oben rechts bleibt. ## Budgetplaner: drei Ansichten und Verteilen über das Zahnrad (11.08.2026) - Abgelaufene Monate werden **nicht mehr zusammengefasst**. Alle zwölf Monate stehen einzeln da; die Sammelspalte „Januar–Vormonat“ entfällt samt `Budgetplaner::abgelaufenBis()` und `Budgetplaner::summe()`. - Statt *Verlauf* und *alle Monate* gibt es drei Ansichten: **Alles** (Budget, Ist und Saldo je Monat), **Nur Budget** und **Nur IST** mit je einer Spalte. Alte URLs mit `ansicht=verlauf|alle` fallen auf *Alles* zurück. - Die Budgetspalte ist überall dort, wo sie steht, editierbar – auch in vergangenen Monaten. - Vor jedem Eingabefeld sitzt ein **⚙**: *Ganzes Jahr* oder *Restliches Jahr* schreibt den Wert des Feldes in die übrigen Monate der Zeile und überschreibt, was dort stand. Gearbeitet wird auf den Eingabefeldern, sichtbar vor dem Speichern; der Knopf **↦** und `Budget::aufFolgemonateUebertragen()` entfallen dafür. - `app.js` war doppelt eingebunden (Kopf **und** Fuß): Jeder Handler lief zweimal, jede Rückfrage kam zweimal. Jetzt nur noch im Kopf. ## Phase 3 – Budgetplaner und Monatsabschluss (11.08.2026) **Datenmodell** - Migration `004_budget_monatsabschluss.sql`: `budgets`, `budget_positionen`, `monatsabschluesse` - Ein Budget gilt für ein Jahr, mehrere je Jahr sind möglich, genau eines ist aktiv - Ein Monat ohne Zeile in `budget_positionen` ist *nicht geplant* – etwas anderes als *mit 0,00 geplant* **Budgetplaner** - Zeilen sind der Kategoriebaum (auf- und zuklappbar), Spalten die Monate; die ganze Tabelle wird in einem Formular gespeichert, damit mehrere Änderungen zusammen ankommen - Zwei Ansichten: *Verlauf* (Jahressumme, abgelaufene Monate zusammengefasst, laufender Monat mit Budget/Ist/Saldo, danach Folgemonate) und *alle Monate* zum Planen - Ausgaben werden ohne Vorzeichen eingetragen; das Vorzeichen kommt aus der Kategorieart. Saldo ist Ist minus Budget – positiv heißt besser als geplant, negativ ist die Überschreitung und wird rot ausgewiesen - Übergeordnete Kategorien summieren ihre Nachfahren und sind selbst nicht beplanbar - Wert auf alle Folgemonate übertragen, Budget als Kopie ins neue Jahr **Monatsabschluss** - Prüflauf über nicht kategorisierte Buchungen, nicht abgeglichene Umsätze und Splitsummen-Differenzen; er hält niemanden auf, sondern zeigt den Stand - Snapshot der Kontosalden und Kategoriesummen als JSON, auch nach Wiedereröffnung - Abgeschlossene Monate sperren Buchungen (`Buchung::anlegen/speichern/loeschen`), in beide Richtungen: weder hinein- noch herausschieben. Abschaltbar über die Einstellung `monatsabschluss_sperrt_buchungen` - Wiedereröffnen bleibt am Datensatz vermerkt **Vorarbeiten in der Oberfläche** - `.knopf.primaer` trug einen eigenen `margin-top` und stand dadurch in jeder Knopfleiste tiefer als seine Nachbarn – sichtbar etwa an „Abbrechen“ neben „Konto anlegen“. Der Abstand liegt jetzt allein bei der Knopfleiste. - Kontengruppen werden wie Konten und Kategorien auf einer eigenen Formularseite gepflegt statt inline in der Tabelle. Dort hängt auch die Zuordnung der Konten: ankreuzen statt jedes Konto einzeln zu öffnen (`Kontengruppe::kontenZuordnen()`). - Der QIF-Export enthält **keine** Kontengruppen – kein Feld im `!Account`-Eintrag, nichts in der `QSD`-Datei. Import-Oberfläche und Dokumentation sagen das jetzt und verweisen auf die Zuordnung. - Die Einrückung des Kategoriebaums lief über ein `style`-Attribut und wurde von der Content-Security-Policy (`style-src 'self'`) stillschweigend verworfen; sie steht jetzt als `data-ebene` im Markup. Aus demselben Grund blendete `app.js` Erfolgsmeldungen nie aus – das läuft jetzt über eine Klasse statt über `element.style`. **Tests** - `11_BudgetTest`, `12_MonatsabschlussTest` (76 Prüfungen), E2E `planung.spec.js` - Einzelheiten in `docs/planung.md` ## Korrektur – Σ-Kennzeichen ließ sich nicht abschalten (10.08.2026) Das Kennzeichen *Zählt zur Summe der Kontostände* blieb beim Speichern eines Kontos unverändert stehen, obwohl die Anwendung „Konto gespeichert“ meldete. - Ursache: `KontenController::formulardaten()` reichte `in_summe` gar nicht durch. Ein nicht angehaktes Ankreuzfeld steht nicht im POST, also griff im Model der Standardwert „zählt mit“ – gespeichert wurde ein Ja, das niemand angegeben hatte. - `formulardaten()` nimmt das POST-Array jetzt als Parameter und ist damit ohne HTTP prüfbar; die Ankreuzfelder werden ausdrücklich auf `true`/`false` abgebildet. - Der Standard „zählt zur Summe“ bleibt für Aufrufe ohne den Schlüssel bestehen – der QIF-Import gibt das Feld nicht an und soll es auch nicht. - `07_KontenTest` prüft die Formularabbildung und den Weg durch das Model; E2E `stammdaten.spec.js` hakt das Kennzeichen in der Oberfläche ab, setzt es wieder und sieht in beiden Fällen nach, statt der Erfolgsmeldung zu glauben. ## Phase 2 – Buchungen, Kontoblatt und Buchungsimport (10.08.2026) **Erkenntnis aus dem Vollexport** Der Plan ging davon aus, Umbuchungen stünden zweimal in der Datei und müssten zusammengeführt werden. Das Gegenteil ist der Fall: Eine Umbuchung steht **einmal**, in dem Konto, in dem sie erfasst wurde; die Gegenseite ist nur durch `L[Zielkonto]` benannt und muss beim Import erzeugt werden. Von 965 Umbuchungen im Export hat keine ein zweites Vorkommen. Mit erzeugter Gegenseite stimmen acht gegen den FinanzManager geprüfte Kontosalden auf den Cent. **Datenmodell** - Migration `003_buchungen.sql`: `buchungen`, `buchung_splits`, `buchungsadressen` - Eine Umbuchung ist ein Datensatz mit `gegenkonto_id`, kein gespiegeltes Paar – das Kontoblatt des Gegenkontos zeigt sie mit umgekehrtem Vorzeichen - Auch einzelne Splitzeilen dürfen ein Gegenkonto tragen - Konten führen `in_summe` (die Σ-Spalte des FinanzManagers) sowie `saldo_online` **Kontoblatt und Buchungsmaske** - Kontoblatt mit laufendem Saldo, Filter auf Zeitraum, Kategorie, Klasse und Abgleich, Volltextsuche und Seitenblättern - Buchungsmaske für Einnahme, Ausgabe und Umbuchung in beide Richtungen; Splitzeilen mit Restbetragsanzeige, geprüft auch serverseitig - Aktueller und erwarteter Saldo; in die Summen zählen nur Konten mit Σ-Kennzeichen **QIF-Buchungsimport** - `QifBuchungsImport` mit Stichtag: alles davor wird je Konto zum Anfangssaldo aufsummiert – einschließlich der erzeugten Gegenbuchungen –, alles danach importiert - Buchungen mit Datum nach dem Importtag gelten als Vorausberechnung und bleiben draußen - Eröffnungsbuchungen (`L[eigenes Konto]`) werden nicht zu Umbuchungen - Wertpapiergeschäfte schlagen vorerst nur mit ihrem Geldfluss auf dem Verrechnungskonto zu Buche; Bestände folgen in Phase 6 - Idempotent über einen Fingerabdruck je Buchung, der auch echte Doppelbuchungen am selben Tag auseinanderhält - Am Vollexport geprüft: 833 Buchungen ab 01.01.2026, 7946 in die Anfangssalden von 35 Konten verrechnet, eine vorausberechnete verworfen **Behoben** - Die `confirm()`-Rückfragen vor dem Löschen waren wirkungslos. `public/static/app.js` brachte den Mechanismus (`data-bestaetigen`) von Anfang an mit, war aber nie im Layout eingebunden; ersatzweise standen Inline-Handler (`onsubmit`) im HTML, und die verstoßen gegen die eigene Content-Security-Policy (`script-src 'self'`) – der Browser verwarf sie stillschweigend, gelöscht wurde ohne Rückfrage. Jetzt ist das Skript eingebunden, die Inline-Handler sind raus, und ein E2E-Test besteht auf dem Dialog. **Tests** - `09_BuchungenTest` – Umbuchungen mit zwei Sichten, Splitzeilen samt Summenprüfung, Splitzeile mit Gegenkonto, aktueller und erwarteter Saldo, Σ-Kennzeichen, Filter - `10_QifBuchungsImportTest` – Stichtag, Anfangssalden aus der Vorgeschichte, erzeugte Gegenbuchung, Eröffnungsbuchung, Zukunftsfilter, zweiter Lauf ohne Wirkung - E2E `buchungen.spec.js` – Ausgabe, Umbuchung auf beiden Kontoblättern, Splitbuchung, abgewiesene Splitsumme, Suche, Löschbestätigung ## Phase 1 – Stammdaten und QIF-Stammdatenimport (09.08.2026) **Datenmodell** - Migration `002_stammdaten.sql`: `kontengruppen`, `konten`, `kategorien`, `klassen` - Alle Tabellen führen `benutzer_id`; `Mandant::id()` liefert die Kennung aus der Session oder – im CLI – vom ersten aktiven Benutzer **Konten** - Kontenübersicht nach Kontengruppen gebündelt, mit Gruppensummen und Gesamtsumme - Die drei Saldospalten des FinanzManagers (Online-Saldo, aktueller und erwarteter Saldo) sind angelegt und werden bis Phase 2 aus dem Anfangssaldo gespeist - Kontoarten des FinanzManagers samt Zuordnung zu den QIF-Typen - Kontenpflege mit Anfangssaldo, Kreditlimit, IBAN/BIC, Kontengruppe, aktiv/inaktiv - Kontengruppen anlegen, umbenennen, sortieren, löschen – die Konten bleiben erhalten **Kategorien und Klassen** - Hierarchische Kategorien mit `:` als Trenner; der vollständige Pfad wird mitgeführt und beim Umbenennen oder Verschieben für den ganzen Teilbaum neu geschrieben - Art (Einnahme/Ausgabe) gilt für den ganzen Ast und vererbt sich an Unterkategorien - Verschieben, Zusammenführen und Löschen; Verschieben in den eigenen Teilbaum wird abgewiesen - Register *Alle / Einnahmen / Ausgaben / Umbuchungskonten* wie im Original - Klassen als flache zweite Auswertungsdimension, in der Liste direkt bearbeitbar **QIF-Import** - `QifParser`: zustandsbehaftetes, sequenzielles Lesen; ISO-8859-1 nach UTF-8, US-Beträge, Datumsformat aus der Kopfzeile, Mehrfachfelder für spätere Splitzeilen - `QifStammdatenImport` für Klassen, Kategorien und Konten – idempotent, mit Trockenlauf und Protokoll; fehlende Zwischenstufen im Kategoriepfad entstehen selbsttätig - Dialog unter *Verwaltung → QIF-Import* mit Upload, Trockenlauf und Übernahme - `scripts/qif-import.php` für dieselbe Aufgabe auf der Kommandozeile - Geprüft am echten Export: 17 Klassen, 105 Kategorien, 37 Konten **Tests** - `06_KategorienTest` – Hierarchie, Pfadpflege, Artvererbung, Verschieben, Zusammenführen - `07_KontenTest` – Kontoarten, Gruppierung und Summen, Betragseingabe - `08_QifImportTest` – Parser, Zeichensatz, Beträge und Datum, Import, Trockenlauf, Prüfung der Dateinamen - E2E `stammdaten.spec.js` – Konto, Kategorie mit Unterkategorie, Register, Klasse, QIF-Upload mit Trockenlauf ## Korrektur – Monatsnamen auf Deutsch (09.08.2026) - Der Einstellungsdialog zeigte unter *Buchhaltung* englische Monatsnamen, weil `date('F')` sprachlich nicht umschaltbar ist. - Monatsnamen liegen jetzt zentral als `View::MONATE` und `View::MONATE_KURZ` vor, abrufbar über `View::monat()`. Budgetplaner und Auswertungen greifen später darauf zu. - `05_DarstellungTest` prüft Monatsnamen, Betrags- und Datumsformat gegen eine feste Sollliste – der Vergleich gegen `date('F')` allein würde April, August, September und November nicht auffallen lassen. ## Phase 0 – Projektgerüst (09.08.2026) Grundgerüst der Anwendung. Fachliche Module folgen ab Phase 1. **Betrieb** - Docker Compose mit drei Services: `nginx` (Port 8000), `php` (php-fpm 8.3), `db` (MariaDB 11) - `docker-entrypoint.sh` legt Datenverzeichnisse an und spielt Migrationen ein - `scripts/migrate.php` – nummerierte SQL-Migrationen, idempotent, mit Journal-Tabelle - `scripts/update.sh` – Update für den Produktionsserver: Backup, `git pull`, Image-Neubau bei Bedarf, Migrationen, Rechte, Tests - `scripts/backup.sh` – Datenbank- und Dateisicherung mit Rotation (7 Tage / 4 Wochen / 12 Monate), gegen Parallelläufe per `flock` abgesichert **Anwendung** - `Config`, `Database` (PDO-Wrapper, verschachtelbare Transaktionen), `Router` (Platzhalter-Routen, CSRF-Prüfung für alle verändernden Requests), `View`, `Auth` - Anmeldung mit Session-Härtung, Ersteinrichtung des ersten Benutzers - Zentraler Einstellungsdialog mit Reitern (Allgemein, Darstellung, Buchhaltung, Online-Banking, Import/Export, Wertpapiere, Diagnose); Werte in Tabelle `einstellungen` - Debug-Log, ein- und ausschaltbar, mit Maskierung von PINs, Passwörtern, TANs und API-Keys; Ausnahmen werden immer protokolliert - Layout nach Vorbild des FinanzManagers: feste Navigation links, Werkzeugleiste oben, Tab-Leiste; Farbschema hell/dunkel/System **Tests** - Test-Harness mit Transaktions-Rollback und Aufräumen temporärer Pfade - `01_AuthTest` – Anmeldung, Passwort-Hashing, CSRF, Cookie-Attribute - `02_EinstellungenTest` – Lesen/Schreiben, Cache-Invalidierung, Bereichsabdeckung - `03_RouterTest` – Pfad-Muster, Platzhalter, keine Treffer über Pfadgrenzen - `04_DebugLogTest` – Schalter, Maskierung, Fehlerprotokollierung