From 1fc78abc11822f38893e01960b9b481413ff2618 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Thu, 1 Oct 2026 10:48:51 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20n.n.:=20Regelm=C3=A4=C3=9Fige=20Buchungen?= =?UTF-8?q?=20=E2=80=93=20Buchen=20zum=20gew=C3=A4hlten=20Datum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- Changes.md | 4 ++++ public/static/reihenliste.js | 12 ++++++++---- src/Models/RegelmaessigeBuchung.php | 9 ++++++++- src/Views/termine/index.php | 23 +++++++++++++++++------ tests/52_RegelmaessigeLoeschenTest.php | 22 ++++++++++++++++++++++ tests/e2e/specs/module.spec.js | 19 +++++++++++++++++++ 6 files changed, 78 insertions(+), 11 deletions(-) diff --git a/Changes.md b/Changes.md index 2f05b8f..b729359 100644 --- a/Changes.md +++ b/Changes.md @@ -11,6 +11,10 @@ - Neue Buchungen ohne Nummer erhalten beim Anlegen die nächste laufende Nummer ihres Kontos (höchste rein numerische Nummer + 1, `Buchung::naechsteNummer()`). Vorgegebene Nummern (QIF, CSV, Formular) bleiben, bestehende Buchungen werden nicht verändert; die interne ID bleibt verborgen. - Test: `53_BuchungsnummerTest.php`. +## Regelmäßige Buchungen: Buchen zum gewählten Datum (01.10.2026) + +- „Buchen“ in „Jetzt fällig“ schlug mit einer Fehlermeldung fehl, weil das Formular ein leeres Datum schickte. Jetzt ist „Fällig am“ per Klick editierbar; „Buchen“ bucht zu diesem Datum. Die Änderung gilt nur für diese Buchung und verändert die Reihe nicht. Ein leeres Datum fällt auf den Termin zurück, ein ungültiges wird abgewiesen. + ## „+ Neu“ öffnet das Formular als Dialog (01.10.2026) - Das Formular zur Neuanlage stand in elf Ansichten unter der Liste. Jetzt öffnet der Knopf „+ Neu“ es als modalen Dialog (`app.js`, `.neudialog`); nach dem Speichern erscheint die Liste wieder, mit dem neuen Eintrag. Betroffen: Klassen, Buchungsvorlagen, Regelmäßige Buchungen, Erinnerungen, Verträge, Adressen, Sparziele, Kredite, Abgleichregeln, Budgets, Berichtsgruppen. diff --git a/public/static/reihenliste.js b/public/static/reihenliste.js index 87c55ca..73f9c4a 100644 --- a/public/static/reihenliste.js +++ b/public/static/reihenliste.js @@ -5,8 +5,8 @@ (function () { 'use strict'; - const tabelle = document.querySelector('table.reihen'); - if (!tabelle) { + const tabellen = document.querySelectorAll('table.reihen, table.faellig'); + if (tabellen.length === 0) { return; } @@ -59,6 +59,7 @@ anzeige.hidden = false; } + tabellen.forEach(function (tabelle) { tabelle.querySelectorAll('.feld').forEach(function (feld) { const anzeige = feld.querySelector('.anzeige'); const steuer = feld.querySelector('input, select'); @@ -99,6 +100,7 @@ } }); }); + }); function geaendert(e) { const zeile = e.target.closest('tr'); @@ -107,6 +109,8 @@ knopf.hidden = false; } } - tabelle.addEventListener('input', geaendert); - tabelle.addEventListener('change', geaendert); + tabellen.forEach(function (tabelle) { + tabelle.addEventListener('input', geaendert); + tabelle.addEventListener('change', geaendert); + }); })(); diff --git a/src/Models/RegelmaessigeBuchung.php b/src/Models/RegelmaessigeBuchung.php index a8b22cd..8cddcb8 100644 --- a/src/Models/RegelmaessigeBuchung.php +++ b/src/Models/RegelmaessigeBuchung.php @@ -169,7 +169,14 @@ class RegelmaessigeBuchung throw new \RuntimeException('Die regelmäßige Buchung gibt es nicht.'); } - $datum = (string)($abweichungen['datum'] ?? $reihe['naechste_faelligkeit'] ?? date('Y-m-d')); + $datum = trim((string)($abweichungen['datum'] ?? '')); + if ($datum === '') { + $datum = (string)($reihe['naechste_faelligkeit'] ?? date('Y-m-d')); + } + $pruef = \DateTimeImmutable::createFromFormat('!Y-m-d', $datum); + if ($pruef === false || $pruef->format('Y-m-d') !== $datum) { + throw new \InvalidArgumentException('Ungültiges Datum.'); + } $betrag = array_key_exists('betrag', $abweichungen) && $abweichungen['betrag'] !== '' ? round((float)$abweichungen['betrag'], 2) : (float)$reihe['betrag']; diff --git a/src/Views/termine/index.php b/src/Views/termine/index.php index 9dfc497..c26ac25 100644 --- a/src/Views/termine/index.php +++ b/src/Views/termine/index.php @@ -42,7 +42,13 @@ include __DIR__ . '/../layout/kopf.php';

Jetzt fällig

- + + + + + +
@@ -50,7 +56,14 @@ include __DIR__ . '/../layout/kopf.php'; > - +
Fällig amNameKontoBetrag ArtAktion
+
+ + +
+
@@ -62,10 +75,8 @@ include __DIR__ . '/../layout/kopf.php'; : 'auf Bestätigung' ?> -
- - -
+
diff --git a/tests/52_RegelmaessigeLoeschenTest.php b/tests/52_RegelmaessigeLoeschenTest.php index 1ef05f3..48a31d0 100644 --- a/tests/52_RegelmaessigeLoeschenTest.php +++ b/tests/52_RegelmaessigeLoeschenTest.php @@ -30,6 +30,7 @@ class RegelmaessigeLoeschenTest extends TestCase $this->manuelleBuchungBleibtUnberuehrt(); $this->buchungstextWirdUebernommen(); $this->naechsterTerminLaesstSichSetzen(); + $this->buchenMitAbweichendemDatum(); } private function reihe(string $name, string $beginn, ?string $ende = null): int @@ -125,4 +126,25 @@ class RegelmaessigeLoeschenTest extends TestCase RegelmaessigeBuchung::speichern($id, ['naechste_faelligkeit' => 'Unsinn'] + $daten); $this->assertEquals('2026-12-15', $this->faelligkeit($id), 'Ungültige Angaben werden ignoriert'); } + + private function buchenMitAbweichendemDatum(): void + { + $id = $this->reihe('Datumsbuchung', '2026-10-01'); + $b1 = RegelmaessigeBuchung::buchen($id, ['datum' => '', 'betrag' => '']); + $this->assertEquals('2026-10-01', (string)Buchung::find($b1)['datum'], 'Leeres Datum bucht zum Termin'); + + $id = $this->reihe('Datumsbuchung 2', '2026-10-01'); + $termin = $this->faelligkeit($id); + $b2 = RegelmaessigeBuchung::buchen($id, ['datum' => '2026-10-05']); + $this->assertEquals('2026-10-05', (string)Buchung::find($b2)['datum'], 'Das gewählte Datum ist das Buchungsdatum'); + $this->assertEquals($termin, (string)Buchung::find($b2)['faelligkeit'], 'Der Termin der Reihe bleibt der gespeicherte'); + + $fehler = false; + try { + RegelmaessigeBuchung::buchen($id, ['datum' => '31.02.2026']); + } catch (\InvalidArgumentException) { + $fehler = true; + } + $this->assert($fehler, 'Ein ungültiges Datum wird abgewiesen'); + } } diff --git a/tests/e2e/specs/module.spec.js b/tests/e2e/specs/module.spec.js index 0d32552..bbbbdf9 100644 --- a/tests/e2e/specs/module.spec.js +++ b/tests/e2e/specs/module.spec.js @@ -339,6 +339,25 @@ test.describe('Module der Phasen 4 bis 7', () => { await expect(page.locator('table.reihen tr', { hasText: 'E2E-Reihe' })).toHaveCount(0) }) + test('Fälliges Datum lässt sich vor dem Buchen per Klick ändern', async ({ page }) => { + await page.goto('/termine') + await page.click('#neu-knopf') + await page.fill('#name', 'E2E-Fällig') + await page.fill('#neu_betrag', '-1,00') + await page.click('form[action="/termine/neu"] button.primaer') + + const zeile = page.locator('table.faellig tr', { hasText: 'E2E-Fällig' }) + const datum = zeile.locator('input[name="datum"]') + await expect(datum).toBeHidden() + await zeile.locator('.feld[data-datum] .anzeige').click() + await expect(datum).toBeVisible() + await expect(zeile.locator('button:has-text("Buchen")')).toBeVisible() + + page.once('dialog', (dialog) => dialog.accept()) + await page.locator('table.reihen tr', { hasText: 'E2E-Fällig' }).locator('button:has-text("Löschen")').click() + await expect(page.locator('table.reihen tr', { hasText: 'E2E-Fällig' })).toHaveCount(0) + }) + test('Der Finanzkalender ist erreichbar und lässt sich einstellen', async ({ page }) => { await page.goto('/termine/kalender') await expect(page.locator('h1')).toContainText('Finanzkalender')