From 87ccb00f9532d21de6aeb265dcb5e207bd2f3b05 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Sun, 26 Jul 2026 16:35:36 +0200 Subject: [PATCH 1/2] Fix #139: Ereignisanzeige aktualisiert sich automatisch (Ein-/Aus-Button) --- Changes.md | 9 ++ docs/funktionen.md | 1 + public/index.php | 5 +- src/Controllers/EventController.php | 18 +++- .../ereignisse/_nextcloud_import_body.php | 28 ++++++ src/Views/ereignisse/nextcloud_import.php | 99 +++++++++++++------ tests/58_EreignisAutoReloadTest.php | 65 ++++++++++++ 7 files changed, 193 insertions(+), 32 deletions(-) create mode 100644 src/Views/ereignisse/_nextcloud_import_body.php create mode 100644 tests/58_EreignisAutoReloadTest.php diff --git a/Changes.md b/Changes.md index f36bb60..d98e67c 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,15 @@ --- +## 2026-07-26 – Feature: Ereignisanzeige aktualisiert sich automatisch (Issue #139) + +- Die Ereignisanzeige (`/ereignisse/nextcloud-import`) war statisch – neu geloggte Ereignisse erschienen erst nach manuellem Neuladen der Seite. +- **`src/Controllers/EventController.php`:** Neue Methode `nextcloudImportData()` liefert die aktuellen Ereignisse als JSON (Anzahl + serverseitig gerendertes HTML-Fragment), damit initiale Seite und Auto-Reload denselben Rendering-Code (`ereignisse/_nextcloud_import_body.php`) verwenden. +- **`src/Views/ereignisse/_nextcloud_import_body.php`:** Tabellen-/Leer-Markup aus `nextcloud_import.php` in ein Partial ausgelagert, das von initialer Seite und Datenendpunkt gemeinsam genutzt wird. +- **`src/Views/ereignisse/nextcloud_import.php`:** Neuer Ein-/Aus-Button oben rechts in der Kartenkopfzeile schaltet automatisches Neuladen per `setInterval` (alle 60s, ohne externe Bibliothek) ein/aus; Zustand wird per `localStorage` gemerkt. Ein echter Push-Mechanismus (z. B. Server-Sent Events) hätte dauerhafte PHP-FPM-Worker gebunden – bei diesem Anwendungsfall (Log-Anzeige) ist periodisches Polling die robustere Lösung. +- **`public/index.php`:** Neue Route `GET /ereignisse/nextcloud-import/data`. +- **`tests/58_EreignisAutoReloadTest.php`:** Neuer Test prüft Button, Polling-Code, Partial-Wiederverwendung und Routen-Registrierung. + ## 2026-07-21 – Fix: Zwei dauerhaft fehlschlagende Tests (Session-Cookie-Härtung, Nextcloud-SSRF) - **Test 54 (Session-Cookie-Härtung):** Der Test rief echtes `Auth::startSession()` auf. Da `tests/run.php` Ergebnisse laufend per `echo` ausgibt und Test 54 der einzige Test ist, der Sessions anfasst, war zum Testzeitpunkt bereits Output gesendet – PHP lehnt `session_set_cookie_params()`/`session_start()` danach mit „headers already sent" ab (Warnung + wirkungslos). Ein nachträglicher Output-Buffer im Test half nicht, da bereits vor dem Test unbuffered Output geflossen war. diff --git a/docs/funktionen.md b/docs/funktionen.md index c54da1c..5bc5025 100644 --- a/docs/funktionen.md +++ b/docs/funktionen.md @@ -16,6 +16,7 @@ Vollständige Kontenliste: [kontenrahmen.md](kontenrahmen.md) - KI-gestützter Kontierungsvorschlag (optional, siehe [konfiguration.md](konfiguration.md)) - Nextcloud-Import via WebDAV (optional) - Belege können nicht gelöscht werden, solange Buchungen darauf verweisen (GoBD) +- **Ereignisanzeige** (`/ereignisse/nextcloud-import`): Log der letzten 500 Import-Ereignisse (importiert/übersprungen/Fehler); aktualisiert sich per Ein-/Aus-Button oben rechts automatisch alle 60 Sekunden (Zustand wird im Browser gemerkt) --- diff --git a/public/index.php b/public/index.php index 09b54ef..3eb7d8a 100644 --- a/public/index.php +++ b/public/index.php @@ -186,8 +186,9 @@ $router->get('/api/buchungsvorlagen/{id}/modal', fn($p) = $router->post('/belege/{belegId}/vorlage/{vorlageId}/buchen', fn($p) => (new BuchungsvorlageController())->buchen($p)); // ── Ereignisse ─────────────────────────────────────────────── -$router->get('/ereignisse', fn($p) => (new EventController())->index($p)); -$router->get('/ereignisse/nextcloud-import', fn($p) => (new EventController())->nextcloudImport($p)); +$router->get('/ereignisse', fn($p) => (new EventController())->index($p)); +$router->get('/ereignisse/nextcloud-import', fn($p) => (new EventController())->nextcloudImport($p)); +$router->get('/ereignisse/nextcloud-import/data', fn($p) => (new EventController())->nextcloudImportData($p)); // ── Login / Logout ──────────────────────────────────────────── $router->get('/login', fn($p) => (new LoginController())->showLogin($p)); diff --git a/src/Controllers/EventController.php b/src/Controllers/EventController.php index c989236..c077783 100644 --- a/src/Controllers/EventController.php +++ b/src/Controllers/EventController.php @@ -22,8 +22,24 @@ class EventController extends BaseController $view->render('layout/base.php', [ 'pageTitle' => 'Ereignisse – Nextcloud-Import', 'content' => $this->renderView('ereignisse/nextcloud_import', [ - 'events' => $events, + 'events' => $events, + 'eventsHtml' => $this->renderView('ereignisse/_nextcloud_import_body', [ + 'events' => $events, + ]), ]), ]); } + + public function nextcloudImportData(array $params): void + { + $events = EventLog::findByChannel('nextcloud-import', 500); + + header('Content-Type: application/json'); + echo json_encode([ + 'count' => count($events), + 'html' => $this->renderView('ereignisse/_nextcloud_import_body', [ + 'events' => $events, + ]), + ], JSON_UNESCAPED_UNICODE); + } } diff --git a/src/Views/ereignisse/_nextcloud_import_body.php b/src/Views/ereignisse/_nextcloud_import_body.php new file mode 100644 index 0000000..2e19645 --- /dev/null +++ b/src/Views/ereignisse/_nextcloud_import_body.php @@ -0,0 +1,28 @@ + View::h($v); ?> + +
Keine Ereignisse vorhanden. Einträge entstehen, sobald der Nextcloud-Import Dateien importiert, überspringt oder auf Fehler stößt.
+ +
+ + + + + + + + + + + +
[]
+
+ diff --git a/src/Views/ereignisse/nextcloud_import.php b/src/Views/ereignisse/nextcloud_import.php index 3923423..4ad281f 100644 --- a/src/Views/ereignisse/nextcloud_import.php +++ b/src/Views/ereignisse/nextcloud_import.php @@ -1,35 +1,76 @@ - View::h($v); ?> -
Nextcloud-Import - Einträge (neueste zuerst) +
+ Einträge (neueste zuerst) + +
- -
Keine Ereignisse vorhanden. Einträge entstehen, sobald der Nextcloud-Import Dateien importiert, überspringt oder auf Fehler stößt.
- -
- - - - - - - - - - - -
[]
+
+
-
+ + diff --git a/tests/58_EreignisAutoReloadTest.php b/tests/58_EreignisAutoReloadTest.php new file mode 100644 index 0000000..d789707 --- /dev/null +++ b/tests/58_EreignisAutoReloadTest.php @@ -0,0 +1,65 @@ +assert($view !== false, 'View-Datei ereignisse/nextcloud_import.php ist lesbar'); + + $this->assert( + str_contains($view, 'id="events-autoreload-toggle"'), + 'Ein-/Aus-Button für Auto-Reload ist vorhanden' + ); + + $this->assert( + str_contains($view, 'localStorage'), + 'Zustand des Auto-Reload-Buttons wird persistiert (localStorage)' + ); + + $this->assert( + str_contains($view, 'setInterval') && str_contains($view, "'/ereignisse/nextcloud-import/data'"), + 'JavaScript pollt periodisch den Daten-Endpunkt' + ); + + $this->assert( + str_contains($view, "toggle.addEventListener('click'"), + 'Button schaltet Auto-Reload per Klick ein/aus' + ); + + // Partial für Tabellen-Body existiert und wird sowohl beim initialen + // Laden als auch für den Daten-Endpunkt verwendet + $partialFile = __DIR__ . '/../src/Views/ereignisse/_nextcloud_import_body.php'; + $this->assert(file_exists($partialFile), 'Partial _nextcloud_import_body.php existiert'); + + $controllerFile = __DIR__ . '/../src/Controllers/EventController.php'; + $controller = file_get_contents($controllerFile); + $this->assert( + str_contains($controller, 'function nextcloudImportData'), + 'EventController::nextcloudImportData() ist implementiert' + ); + $this->assert( + str_contains($controller, "'ereignisse/_nextcloud_import_body'"), + 'Daten-Endpunkt rendert dasselbe Partial wie die initiale Seite' + ); + + $routesFile = __DIR__ . '/../public/index.php'; + $routes = file_get_contents($routesFile); + $this->assert( + str_contains($routes, "'/ereignisse/nextcloud-import/data'") && str_contains($routes, 'nextcloudImportData'), + 'Route /ereignisse/nextcloud-import/data ist registriert' + ); + } +} From 9f61624b40174bae779613449265f92aad698163 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Sun, 26 Jul 2026 16:39:32 +0200 Subject: [PATCH 2/2] Fix #140: Essensbeleg-Vorlage um Eingabezeile ohne USt. (Trinkgeld) erweitert --- Changes.md | 6 ++ ...8_buchungsvorlage_essensbeleg_ohne_ust.sql | 21 ++++++ tests/59_EssensbelegOhneUstTest.php | 72 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 migrations/088_buchungsvorlage_essensbeleg_ohne_ust.sql create mode 100644 tests/59_EssensbelegOhneUstTest.php diff --git a/Changes.md b/Changes.md index d98e67c..2923240 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,12 @@ --- +## 2026-07-26 – Feature: Dritte Eingabezeile ohne USt. in der Essensbeleg-Buchungsvorlage (Issue #140) + +- Die Buchungsvorlage „Essensbeleg n. abzugsfähig (nur USt.)" hatte nur Eingabezeilen für 7 % und 19 % USt. – Beträge ohne USt. (z. B. Trinkgeld) ließen sich damit nicht erfassen. +- **`migrations/088_buchungsvorlage_essensbeleg_ohne_ust.sql`:** Ergänzt die Vorlage um ein drittes `ust_zeile`-Feld `netto_0` (0 % USt.) sowie eine Buchungsregel, die den eingegebenen Betrag auf Konto 4657 („Nicht abzugsfähige Bewirtungskosten ohne Vorsteuer") bucht – Formular-Rendering (`buchungsvorlagen/modal.php`), Live-Berechnung (`app.js`) und Buchungsausführung (`VorlageBuchungsService`) sind bereits vollständig datengetrieben und benötigten keine Code-Änderung. +- **`tests/59_EssensbelegOhneUstTest.php`:** Neuer Test prüft das neue Feld, die zugehörige Regel und die korrekte Buchung auf Konto 4657 bei gleichzeitiger Nutzung aller drei Zeilen. + ## 2026-07-26 – Feature: Ereignisanzeige aktualisiert sich automatisch (Issue #139) - Die Ereignisanzeige (`/ereignisse/nextcloud-import`) war statisch – neu geloggte Ereignisse erschienen erst nach manuellem Neuladen der Seite. diff --git a/migrations/088_buchungsvorlage_essensbeleg_ohne_ust.sql b/migrations/088_buchungsvorlage_essensbeleg_ohne_ust.sql new file mode 100644 index 0000000..ceeb9bb --- /dev/null +++ b/migrations/088_buchungsvorlage_essensbeleg_ohne_ust.sql @@ -0,0 +1,21 @@ +-- 088_buchungsvorlage_essensbeleg_ohne_ust.sql +-- Ergänzt die Vorlage "Essensbeleg n. abzugsfähig (nur USt.)" um eine dritte +-- Eingabezeile für Beträge ohne USt. (z.B. Trinkgeld). +-- Idempotent: Feld/Regel werden nur angelegt wenn nicht vorhanden. + +SET @vorlage_name = 'Essensbeleg n. abzugsfähig (nur USt.)'; +SET @vid = (SELECT id FROM buchungsvorlagen WHERE name = @vorlage_name LIMIT 1); + +INSERT INTO buchungsvorlage_felder (vorlage_id, gruppe_nr, sort_order, feldname, label, typ, ust_satz, meta_key) +SELECT @vid, 2, 30, 'netto_0', '0% USt. (z.B. Trinkgeld)', 'ust_zeile', 0.00, NULL +WHERE @vid IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM buchungsvorlage_felder WHERE vorlage_id = @vid AND feldname = 'netto_0'); + +-- Regel: Betrag ohne USt. → nicht abzugsfähige Bewirtungskosten ohne Vorsteuer (Konto 4657) +INSERT INTO buchungsvorlage_regeln (vorlage_id, sort_order, bedingung_feld, bedingung_op, buchungen) +SELECT @vid, 30, 'netto_0', 'neq_zero', + JSON_ARRAY( + JSON_OBJECT('betrag_ref', 'netto_0', 'nach_konto', '4657') + ) +WHERE @vid IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM buchungsvorlage_regeln WHERE vorlage_id = @vid AND bedingung_feld = 'netto_0'); diff --git a/tests/59_EssensbelegOhneUstTest.php b/tests/59_EssensbelegOhneUstTest.php new file mode 100644 index 0000000..dd90fe5 --- /dev/null +++ b/tests/59_EssensbelegOhneUstTest.php @@ -0,0 +1,72 @@ +assertNotEmpty($vorlage, 'Seed-Vorlage "Essensbeleg n. abzugsfähig (nur USt.)" vorhanden'); + $vid = (int)$vorlage['id']; + + // ── Feld netto_0 mit 0% USt. vorhanden ────────────────────────────── + $felder = \Steuerkram\Models\Buchungsvorlage::getFelder($vid); + $feldNamen = array_column($felder, 'feldname'); + $this->assert(in_array('netto_0', $feldNamen, true), 'Feld netto_0 (0% USt.) ist Teil der Vorlage'); + + $feldNull = current(array_filter($felder, fn($f) => $f['feldname'] === 'netto_0')); + $this->assertEquals('ust_zeile', $feldNull['typ'], 'netto_0 ist eine ust_zeile'); + $this->assertEquals(0.0, (float)$feldNull['ust_satz'], 'netto_0 hat 0% USt.-Satz'); + + // ── Regel für netto_0 bucht auf Konto 4657 ────────────────────────── + $regeln = \Steuerkram\Models\Buchungsvorlage::getRegeln($vid); + $regelNull = current(array_filter($regeln, fn($r) => $r['bedingung_feld'] === 'netto_0')); + $this->assertNotEmpty($regelNull, 'Regel für netto_0 vorhanden'); + $this->assertEquals('4657', $regelNull['buchungen'][0]['nach_konto'], 'netto_0 bucht auf Konto 4657 (ohne Vorsteuer)'); + + // ── Buchungsausführung mit allen drei Zeilen (7%, 19%, 0%) ───────── + $bankId = \Steuerkram\Database::fetchOne( + "SELECT id FROM accounts WHERE kontonummer = '1200' LIMIT 1" + ); + $this->assertNotEmpty($bankId, 'Konto 1200 gefunden'); + + $belegNr = 'TEST-ESS-' . uniqid(); + $belegId = \Steuerkram\Models\Beleg::create([ + 'beleg_nr' => $belegNr, + 'filename' => 'test-essensbeleg-trinkgeld.pdf', + 'storage_path' => 'static/uploads/test/test-essensbeleg-trinkgeld.pdf', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', $belegNr), + 'beleg_datum' => date('Y-m-d'), + 'belegtyp' => 'Eingangsrechnung', + ]); + $this->assertGreaterThan(0, $belegId, 'Beleg angelegt'); + + $svc = new \Steuerkram\Services\VorlageBuchungsService(); + $ids = $svc->buchen($belegId, $vid, [ + 'netto_7' => '10.00', + 'ust_7' => '0.70', + 'netto_19' => '20.00', + 'ust_19' => '3.80', + 'netto_0' => '5.00', + 'zahlungskonto_id' => (int)$bankId['id'], + 'lieferant' => 'Test-Restaurant', + 'belegdatum' => date('Y-m-d'), + ], date('Y-m-d')); + + $this->assertEquals(5, count($ids), '5 Buchungszeilen erzeugt (2x Netto+USt, 1x Trinkgeld ohne USt.)'); + + $bookings = \Steuerkram\Models\Booking::forBeleg($belegId); + $kontonummern = array_column($bookings, 'kategorie_nr'); + sort($kontonummern); + $this->assertEquals(['1571', '1576', '4654', '4656', '4657'], $kontonummern, 'Buchungen auf Konten 1571, 1576, 4654, 4656 und 4657'); + + $trinkgeldBuchung = current(array_filter($bookings, fn($b) => $b['kategorie_nr'] === '4657')); + $this->assertEquals(5.0, (float)$trinkgeldBuchung['betrag_netto'], 'Trinkgeld-Betrag korrekt gebucht'); + } +}