diff --git a/Changes.md b/Changes.md index 738e2d1..1fa7536 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,17 @@ --- +## 2026-06-29 – Feature: Ereignisse-Log für Nextcloud-Import (Issue #109) + +- **`migrations/083_system_events.sql`:** Neue Tabelle `system_events` (id, channel, message, created_at) +- **`src/Models/EventLog.php`:** Neues Model mit `add()` und `findByChannel()` +- **`src/Services/NextcloudImportService.php`:** Persistiert relevante Ereignisse (Import, Duplikat, Fehler) automatisch; stille Läufe ohne Dateiaktivität erzeugen keine Einträge; Zusammenfassung wird nur bei tatsächlicher Aktivität geloggt +- **`src/Controllers/EventController.php`:** Neuer Controller für `/ereignisse` und `/ereignisse/nextcloud-import` +- **`src/Views/ereignisse/nextcloud_import.php`:** Log-Ansicht mit Zeitstempel, Kanal und farbiger Meldung +- **`src/Views/layout/nav.php`:** Neuer Menüpunkt „Ereignisse" unter Verwaltung mit aufklappbarem Unterpunkt „Nextcloud-Import" + +--- + ## 2026-06-29 – Feature: Lieferant in Belegliste inline bearbeiten (Issue #108) - **`src/Views/belege/list.php`:** Lieferant-Spalte ist direkt in der Liste klickbar; zeigt ein Inline-Textfeld zur Eingabe, speichert mit Enter, bricht mit Escape ab; andere Spalten öffnen weiterhin die Vorschau diff --git a/migrations/083_system_events.sql b/migrations/083_system_events.sql new file mode 100644 index 0000000..cc6d149 --- /dev/null +++ b/migrations/083_system_events.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS system_events ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + channel VARCHAR(50) NOT NULL, + message TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_channel_created (channel, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/public/index.php b/public/index.php index 5392cb8..09b54ef 100644 --- a/public/index.php +++ b/public/index.php @@ -28,6 +28,7 @@ use Steuerkram\Controllers\AssetController; use Steuerkram\Controllers\UserAdminController; use Steuerkram\Controllers\BankController; use Steuerkram\Controllers\BuchungsvorlageController; +use Steuerkram\Controllers\EventController; Config::load(); @@ -184,6 +185,10 @@ $router->post('/einstellungen/buchungsvorlagen/{id}/loeschen', fn($p) = $router->get('/api/buchungsvorlagen/{id}/modal', fn($p) => (new BuchungsvorlageController())->modal($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)); + // ── Login / Logout ──────────────────────────────────────────── $router->get('/login', fn($p) => (new LoginController())->showLogin($p)); $router->post('/login', fn($p) => (new LoginController())->handleLogin($p)); diff --git a/src/Controllers/EventController.php b/src/Controllers/EventController.php new file mode 100644 index 0000000..c989236 --- /dev/null +++ b/src/Controllers/EventController.php @@ -0,0 +1,29 @@ +render('layout/base.php', [ + 'pageTitle' => 'Ereignisse – Nextcloud-Import', + 'content' => $this->renderView('ereignisse/nextcloud_import', [ + 'events' => $events, + ]), + ]); + } +} diff --git a/src/Models/EventLog.php b/src/Models/EventLog.php new file mode 100644 index 0000000..6899012 --- /dev/null +++ b/src/Models/EventLog.php @@ -0,0 +1,30 @@ + 0, 'skipped' => 0, 'errors' => 0, 'details' => [], 'imported_ids' => []]; if (!$this->baseUrl || !$this->user || !$this->pass) { - $result['details'][] = 'FEHLER: NEXTCLOUD_URL, NEXTCLOUD_USER und NEXTCLOUD_PASS müssen gesetzt sein.'; + $msg = 'FEHLER: NEXTCLOUD_URL, NEXTCLOUD_USER und NEXTCLOUD_PASS müssen gesetzt sein.'; + $result['details'][] = $msg; $result['errors']++; + $this->persistEvent($msg); return $result; } $files = $this->listFiles(); if ($files === null) { - $result['details'][] = 'FEHLER: PROPFIND fehlgeschlagen. URL/Zugangsdaten prüfen.'; + $msg = 'FEHLER: PROPFIND fehlgeschlagen. URL/Zugangsdaten prüfen.'; + $result['details'][] = $msg; $result['errors']++; + $this->persistEvent($msg); return $result; } @@ -90,9 +97,24 @@ class NextcloudImportService $this->processFile($file, $result); } + if ($result['imported'] + $result['skipped'] + $result['errors'] > 0) { + $elapsed = round(microtime(true) - $start, 2); + $summary = sprintf( + 'Fertig in %ss – importiert: %d, übersprungen: %d, Fehler: %d', + $elapsed, $result['imported'], $result['skipped'], $result['errors'] + ); + $this->persistEvent($summary); + $result['details'][] = $summary; + } + return $result; } + private function persistEvent(string $message): void + { + EventLog::add('nextcloud-import', $message); + } + /** * Listet alle Dateien im konfigurierten Ordner via PROPFIND. * Gibt null zurück bei Verbindungsfehler. @@ -198,7 +220,9 @@ class NextcloudImportService // MIME prüfen if (!isset(self::ALLOWED_MIME[$contentType])) { - $result['details'][] = "Übersprungen (unbekannter Typ «{$contentType}»): {$filename}"; + $msg = "Übersprungen (unbekannter Typ «{$contentType}»): {$filename}"; + $result['details'][] = $msg; + $this->persistEvent($msg); $result['skipped']++; return; } @@ -210,7 +234,9 @@ class NextcloudImportService $tmpPath = sys_get_temp_dir() . '/nextcloud_import_' . uniqid() . '.' . $ext; if (!$this->downloadFile($fileUrl, $tmpPath)) { - $result['details'][] = "FEHLER beim Download: {$filename}"; + $msg = "FEHLER beim Download: {$filename}"; + $result['details'][] = $msg; + $this->persistEvent($msg); $result['errors']++; return; } @@ -222,7 +248,9 @@ class NextcloudImportService [$rawHash] ); if ($existing) { - $result['details'][] = "Duplikat (Beleg {$existing['beleg_nr']}): {$filename}"; + $msg = "Duplikat (Beleg {$existing['beleg_nr']}): {$filename}"; + $result['details'][] = $msg; + $this->persistEvent($msg); $result['skipped']++; unlink($tmpPath); $this->moveFile($file['href'], $filename); @@ -236,7 +264,9 @@ class NextcloudImportService $absDir = dirname(__DIR__, 2) . "/public/{$relDir}"; if (!is_dir($absDir) && !mkdir($absDir, 0755, true)) { - $result['details'][] = "FEHLER: Verzeichnis konnte nicht erstellt werden: {$absDir}"; + $msg = "FEHLER: Verzeichnis konnte nicht erstellt werden: {$absDir}"; + $result['details'][] = $msg; + $this->persistEvent($msg); $result['errors']++; unlink($tmpPath); return; @@ -246,7 +276,9 @@ class NextcloudImportService $absPath = dirname(__DIR__, 2) . "/public/{$storagePath}"; if (!rename($tmpPath, $absPath)) { - $result['details'][] = "FEHLER: Datei konnte nicht verschoben werden: {$filename}"; + $msg = "FEHLER: Datei konnte nicht verschoben werden: {$filename}"; + $result['details'][] = $msg; + $this->persistEvent($msg); $result['errors']++; @unlink($tmpPath); return; @@ -278,7 +310,9 @@ class NextcloudImportService // Datei in Nextcloud verschieben/löschen $this->moveFile($file['href'], $filename); - $result['details'][] = "Importiert: {$filename} → Beleg {$belegNr}"; + $msg = "Importiert: {$filename} → Beleg {$belegNr}"; + $result['details'][] = $msg; + $this->persistEvent($msg); $result['imported']++; $result['imported_ids'][] = $belegId; } diff --git a/src/Views/ereignisse/nextcloud_import.php b/src/Views/ereignisse/nextcloud_import.php new file mode 100644 index 0000000..3923423 --- /dev/null +++ b/src/Views/ereignisse/nextcloud_import.php @@ -0,0 +1,35 @@ + View::h($v); ?> + +
| = $h(date('Y-m-d H:i:s', strtotime($e['created_at']))) ?> | +[= $h($e['channel']) ?>] | += $h($msg) ?> | +