From 02c642e53e4097a286d6503d4509a5a8742d4490 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Thu, 14 May 2026 19:56:26 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Bank=20buchen=20=E2=80=93=20Import,?= =?UTF-8?q?=20Liste,=20Detail,=20Beleg-Verkn=C3=BCpfung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changes.md | 12 ++ migrations/049_bank_tables.sql | 56 +++++++ public/index.php | 10 ++ src/Controllers/BankController.php | 203 ++++++++++++++++++++++++ src/Models/BankImportFormat.php | 21 +++ src/Models/BankTransaction.php | 125 +++++++++++++++ src/Services/BankImportParser.php | 240 +++++++++++++++++++++++++++++ src/Views/bank/index.php | 121 +++++++++++++++ src/Views/bank/show.php | 129 ++++++++++++++++ src/Views/bank/upload.php | 87 +++++++++++ src/Views/layout/nav.php | 4 + 11 files changed, 1008 insertions(+) create mode 100644 migrations/049_bank_tables.sql create mode 100644 src/Controllers/BankController.php create mode 100644 src/Models/BankImportFormat.php create mode 100644 src/Models/BankTransaction.php create mode 100644 src/Services/BankImportParser.php create mode 100644 src/Views/bank/index.php create mode 100644 src/Views/bank/show.php create mode 100644 src/Views/bank/upload.php diff --git a/Changes.md b/Changes.md index b4b0248..1f8259d 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,18 @@ --- +## 2026-05-14 – Feature: Bank buchen + +- Navigation: neuer Eintrag „Bank buchen" unter Belege & Buchungen +- Migration 049: Tabellen `bank_import_formats`, `bank_import_batches`, `bank_transactions`; Comdirect-Format als erster Eintrag +- `BankImportParser`: Parst CSV/TXT (quoted, Semikolon-getrennt) und XLSX (via ZipArchive/SimpleXML, kein Composer-Paket) +- Liste `/bank`: Suche + Filter (Jahr, Monat, Einnahme/Ausgabe, Status); Standard-Filter: offen; Einnahmen grün, Ausgaben rot +- Upload `/bank/upload`: Drag-&-Drop-Zone für CSV/TXT/XLSX + Format-Auswahl +- Detail `/bank/{id}`: Bank-Zeile oben; Beleg-Upload; Liste offener Belege zum Verknüpfen +- Status-Aktionen: „Ignorieren" / „Wieder öffnen"; Verknüpfen eines Belegs setzt Status auf „gebucht" + +--- + ## 2026-05-14 – Feature: UStVA Kz 83 = ust81 + ust89 − Kz66 − Kz61 - `UstvaCalculator`: Kz 83 wird nun als `ust81 + ust89 − Kz66 − Kz61` berechnet (nicht mehr kontobasiert via ustva_kz='83') diff --git a/migrations/049_bank_tables.sql b/migrations/049_bank_tables.sql new file mode 100644 index 0000000..77d00f2 --- /dev/null +++ b/migrations/049_bank_tables.sql @@ -0,0 +1,56 @@ +-- Bank-Import-Formate +CREATE TABLE IF NOT EXISTS bank_import_formats ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + skip_rows INT NOT NULL DEFAULT 3, + has_header_row TINYINT(1) NOT NULL DEFAULT 1, + delimiter CHAR(1) NOT NULL DEFAULT ';', + enclosure CHAR(1) NOT NULL DEFAULT '"', + col_buchungstag INT NOT NULL DEFAULT 0, + col_wertstellung INT NOT NULL DEFAULT 1, + col_vorgang INT NOT NULL DEFAULT 2, + col_buchungstext INT NOT NULL DEFAULT 3, + col_wert INT NOT NULL DEFAULT 4, + date_format VARCHAR(20) NOT NULL DEFAULT 'd.m.Y', + decimal_separator CHAR(1) NOT NULL DEFAULT ',', + thousands_separator CHAR(1) NOT NULL DEFAULT '.', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Import-Batches (Datei-Uploads) +CREATE TABLE IF NOT EXISTS bank_import_batches ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + format_id INT UNSIGNED NOT NULL, + filename VARCHAR(255) NOT NULL, + row_count INT NOT NULL DEFAULT 0, + imported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (format_id) REFERENCES bank_import_formats(id) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Importierte Bankbuchungen +CREATE TABLE IF NOT EXISTS bank_transactions ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + batch_id INT UNSIGNED NOT NULL, + buchungstag DATE NOT NULL, + wertstellung DATE NOT NULL, + vorgang VARCHAR(255) NOT NULL DEFAULT '', + buchungstext TEXT NOT NULL, + wert DECIMAL(12,2) NOT NULL, + status ENUM('offen','gebucht','ignoriert') NOT NULL DEFAULT 'offen', + beleg_id INT UNSIGNED NULL DEFAULT NULL, + raw_line TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (batch_id) REFERENCES bank_import_batches(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Comdirect-Format: 3 Zeilen überspringen, dann Headerzeile, dann Daten +-- Semikolon-getrennt, Anführungszeichen-umschlossen, deutsches Datumsformat +INSERT INTO bank_import_formats + (name, skip_rows, has_header_row, delimiter, enclosure, + col_buchungstag, col_wertstellung, col_vorgang, col_buchungstext, col_wert, + date_format, decimal_separator, thousands_separator) +VALUES + ('Comdirect', 3, 1, ';', '"', + 0, 1, 2, 3, 4, + 'd.m.Y', ',', '.') +ON DUPLICATE KEY UPDATE name = name; diff --git a/public/index.php b/public/index.php index 55abdb7..e7af523 100644 --- a/public/index.php +++ b/public/index.php @@ -26,6 +26,7 @@ use Steuerkram\Controllers\SettingsController; use Steuerkram\Controllers\LoginController; use Steuerkram\Controllers\AssetController; use Steuerkram\Controllers\UserAdminController; +use Steuerkram\Controllers\BankController; Config::load(); @@ -63,6 +64,15 @@ $router->post('/belege/{id}/crop', fn($p) => (new BelegController())->cropImag $router->post('/belege/{id}/vorschlag/generate', fn($p) => (new BelegController())->generateVorschlag($p)); $router->post('/belege/{id}/vorschlag/reset', fn($p) => (new BelegController())->resetVorschlag($p)); +// ── Bank buchen ────────────────────────────────────────────── +$router->get('/bank', fn($p) => (new BankController())->index($p)); +$router->get('/bank/upload', fn($p) => (new BankController())->uploadForm($p)); +$router->post('/bank/upload', fn($p) => (new BankController())->upload($p)); +$router->get('/bank/{id}', fn($p) => (new BankController())->show($p)); +$router->post('/bank/{id}/status', fn($p) => (new BankController())->updateStatus($p)); +$router->post('/bank/{id}/link', fn($p) => (new BankController())->linkBeleg($p)); +$router->post('/bank/{id}/beleg', fn($p) => (new BankController())->uploadBeleg($p)); + // ── Buchungen ──────────────────────────────────────────────── $router->get('/journal', fn($p) => (new BookingController())->journal($p)); $router->get('/journal/export', fn($p) => (new BookingController())->export($p)); diff --git a/src/Controllers/BankController.php b/src/Controllers/BankController.php new file mode 100644 index 0000000..5c6fad5 --- /dev/null +++ b/src/Controllers/BankController.php @@ -0,0 +1,203 @@ +render('layout/base.php', [ + 'pageTitle' => 'Bank buchen', + 'content' => $this->renderView('bank/index', [ + 'rows' => $result['rows'], + 'total' => $result['total'], + 'pages' => $result['pages'], + 'page' => $page, + 'filter' => $filter, + ]), + 'topbarActions' => 'Upload', + ]); + } + + public function uploadForm(array $params): void + { + $formats = BankImportFormat::all(); + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Bank-Buchungen importieren', + 'content' => $this->renderView('bank/upload', [ + 'formats' => $formats, + 'errors' => [], + ]), + ]); + } + + public function upload(array $params): void + { + $formatId = (int)($_POST['format_id'] ?? 0); + $format = BankImportFormat::find($formatId); + + if (!$format) { + View::redirect('/bank/upload', 'Unbekanntes Import-Format.', 'error'); + } + + $file = $_FILES['bank_file'] ?? null; + if (empty($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK) { + View::redirect('/bank/upload', 'Datei-Upload fehlgeschlagen.', 'error'); + } + + $origName = $file['name'] ?? 'upload'; + $ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION)); + + if (!in_array($ext, self::ALLOWED_EXT, true)) { + View::redirect('/bank/upload', 'Nicht erlaubtes Dateiformat. Erlaubt: CSV, TXT, XLSX.', 'error'); + } + + try { + $parser = new BankImportParser(); + $rows = $parser->parse($file['tmp_name'], $ext, $format); + } catch (\RuntimeException $e) { + View::redirect('/bank/upload', 'Fehler beim Parsen: ' . $e->getMessage(), 'error'); + } + + if (empty($rows)) { + View::redirect('/bank/upload', 'Keine Buchungszeilen in der Datei gefunden.', 'error'); + } + + $batchId = BankTransaction::createBatch($formatId, $origName, count($rows)); + foreach ($rows as $row) { + BankTransaction::insertRow($batchId, $row); + } + + View::redirect('/bank', count($rows) . ' Buchungen importiert.'); + } + + public function show(array $params): void + { + $id = (int)($params['id'] ?? 0); + $transaction = BankTransaction::find($id); + + if (!$transaction) { + View::redirect('/bank', 'Buchung nicht gefunden.', 'error'); + } + + // Offene Belege (noch nicht gebucht) + $offeneBelege = Beleg::all(['booked' => '0'], 1, 200); + + $view = new View(); + $view->render('layout/base.php', [ + 'pageTitle' => 'Bankbuchung zuordnen', + 'content' => $this->renderView('bank/show', [ + 'transaction' => $transaction, + 'offeneBelege' => $offeneBelege['rows'], + ]), + ]); + } + + public function updateStatus(array $params): void + { + $id = (int)($params['id'] ?? 0); + $status = $_POST['status'] ?? ''; + + if (!in_array($status, ['offen', 'gebucht', 'ignoriert'], true)) { + View::redirect('/bank', 'Ungültiger Status.', 'error'); + } + + BankTransaction::updateStatus($id, $status); + View::redirect('/bank', 'Status aktualisiert.'); + } + + public function linkBeleg(array $params): void + { + $id = (int)($params['id'] ?? 0); + $belegId = (int)($_POST['beleg_id'] ?? 0); + + if (!$belegId) { + View::redirect("/bank/$id", 'Kein Beleg ausgewählt.', 'error'); + } + + BankTransaction::linkBeleg($id, $belegId); + View::redirect('/bank', 'Beleg verknüpft und Buchung als gebucht markiert.'); + } + + public function uploadBeleg(array $params): void + { + $id = (int)($params['id'] ?? 0); + $transaction = BankTransaction::find($id); + + if (!$transaction) { + View::redirect('/bank', 'Buchung nicht gefunden.', 'error'); + } + + // Beleg über BelegController-Logik hochladen und verknüpfen + $file = $_FILES['beleg_file'] ?? null; + if (empty($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK) { + View::redirect("/bank/$id", 'Datei-Upload fehlgeschlagen.', 'error'); + } + + $allowedMime = [ + 'application/pdf' => 'pdf', + 'image/png' => 'png', + 'image/jpeg' => 'jpg', + 'image/tiff' => 'tif', + 'application/xml' => 'xml', + 'text/xml' => 'xml', + ]; + + $mime = mime_content_type($file['tmp_name']) ?: ''; + if (!isset($allowedMime[$mime])) { + View::redirect("/bank/$id", 'Nicht erlaubtes Dateiformat.', 'error'); + } + + $ext = $allowedMime[$mime]; + $hash = hash_file('sha256', $file['tmp_name']); + $filename = $hash . '.' . $ext; + $dir = BASE_DIR . '/storage/belege/'; + if (!is_dir($dir)) mkdir($dir, 0755, true); + $dest = $dir . $filename; + if (!file_exists($dest)) { + move_uploaded_file($file['tmp_name'], $dest); + } + + $belegId = Beleg::create([ + 'beleg_nr' => Beleg::generateBelegNr(), + 'filename' => $file['name'], + 'storage_path' => 'storage/belege/' . $filename, + 'mime_type' => $mime, + 'file_hash' => $hash, + 'beleg_datum' => $transaction['wertstellung'], + 'belegtyp' => (float)$transaction['wert'] >= 0 ? 'Ausgangsrechnung' : 'Eingangsrechnung', + ]); + + BankTransaction::linkBeleg($id, $belegId); + View::redirect('/bank', 'Beleg hochgeladen und Buchung als gebucht markiert.'); + } + + private function renderView(string $name, array $data = []): string + { + $viewFile = dirname(__DIR__) . '/Views/' . $name . '.php'; + extract($data, EXTR_SKIP); + ob_start(); + include $viewFile; + return ob_get_clean(); + } +} diff --git a/src/Models/BankImportFormat.php b/src/Models/BankImportFormat.php new file mode 100644 index 0000000..accfc57 --- /dev/null +++ b/src/Models/BankImportFormat.php @@ -0,0 +1,21 @@ + 0'; + } elseif ($filter['typ'] === 'ausgabe') { + $where[] = 'bt.wert < 0'; + } + } + if (!empty($filter['search'])) { + $where[] = '(bt.buchungstext LIKE ? OR bt.vorgang LIKE ?)'; + $s = '%' . $filter['search'] . '%'; + $params[] = $s; + $params[] = $s; + } + + $whereStr = implode(' AND ', $where); + $offset = ($page - 1) * $perPage; + + $total = (int)(Database::fetchOne( + "SELECT COUNT(*) AS n FROM bank_transactions bt WHERE $whereStr", + $params + )['n'] ?? 0); + + $rows = Database::fetchAll( + "SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name + FROM bank_transactions bt + JOIN bank_import_batches bib ON bt.batch_id = bib.id + JOIN bank_import_formats bif ON bib.format_id = bif.id + WHERE $whereStr + ORDER BY bt.wertstellung DESC, bt.id DESC + LIMIT ? OFFSET ?", + array_merge($params, [$perPage, $offset]) + ); + + return [ + 'rows' => $rows, + 'total' => $total, + 'pages' => (int)ceil($total / $perPage), + ]; + } + + public static function find(int $id): ?array + { + $row = Database::fetchOne( + 'SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name + FROM bank_transactions bt + JOIN bank_import_batches bib ON bt.batch_id = bib.id + JOIN bank_import_formats bif ON bib.format_id = bif.id + WHERE bt.id = ?', + [$id] + ); + return $row ?: null; + } + + public static function updateStatus(int $id, string $status): void + { + Database::execute( + "UPDATE bank_transactions SET status = ? WHERE id = ?", + [$status, $id] + ); + } + + public static function linkBeleg(int $id, int $belegId): void + { + Database::execute( + "UPDATE bank_transactions SET beleg_id = ?, status = 'gebucht' WHERE id = ?", + [$belegId, $id] + ); + } + + public static function createBatch(int $formatId, string $filename, int $rowCount): int + { + Database::execute( + 'INSERT INTO bank_import_batches (format_id, filename, row_count) VALUES (?, ?, ?)', + [$formatId, $filename, $rowCount] + ); + return (int)Database::lastInsertId(); + } + + public static function insertRow(int $batchId, array $data): void + { + Database::execute( + 'INSERT INTO bank_transactions + (batch_id, buchungstag, wertstellung, vorgang, buchungstext, wert, raw_line) + VALUES (?, ?, ?, ?, ?, ?, ?)', + [ + $batchId, + $data['buchungstag'], + $data['wertstellung'], + $data['vorgang'], + $data['buchungstext'], + $data['wert'], + $data['raw_line'] ?? null, + ] + ); + } +} diff --git a/src/Services/BankImportParser.php b/src/Services/BankImportParser.php new file mode 100644 index 0000000..d7e8ade --- /dev/null +++ b/src/Services/BankImportParser.php @@ -0,0 +1,240 @@ +readXlsx($filePath); + } else { + $lines = $this->readCsv($filePath, $format['delimiter'], $format['enclosure']); + } + + // Übersprungene Zeilen und optionale Headerzeile entfernen + $skipRows = (int)$format['skip_rows']; + $lines = array_slice($lines, $skipRows); + if (!empty($format['has_header_row'])) { + $lines = array_slice($lines, 1); + } + + $colBuchungstag = (int)$format['col_buchungstag']; + $colWertstellung = (int)$format['col_wertstellung']; + $colVorgang = (int)$format['col_vorgang']; + $colBuchungstext = (int)$format['col_buchungstext']; + $colWert = (int)$format['col_wert']; + + $result = []; + foreach ($lines as $i => $cells) { + // Leerzeilen überspringen + $joined = implode('', $cells); + if (trim($joined) === '') { + continue; + } + + $buchungstag = $this->parseDate($cells[$colBuchungstag] ?? '', $format['date_format']); + $wertstellung = $this->parseDate($cells[$colWertstellung] ?? '', $format['date_format']); + $wert = $this->parseDecimal( + $cells[$colWert] ?? '0', + $format['decimal_separator'], + $format['thousands_separator'] + ); + + if ($buchungstag === null || $wertstellung === null) { + continue; + } + + $result[] = [ + 'buchungstag' => $buchungstag, + 'wertstellung' => $wertstellung, + 'vorgang' => trim($cells[$colVorgang] ?? ''), + 'buchungstext' => trim($cells[$colBuchungstext] ?? ''), + 'wert' => $wert, + 'raw_line' => implode($format['delimiter'], $cells), + ]; + } + + return $result; + } + + private function readCsv(string $filePath, string $delimiter, string $enclosure): array + { + $content = file_get_contents($filePath); + if ($content === false) { + throw new \RuntimeException('Datei konnte nicht gelesen werden.'); + } + + // BOM entfernen + $content = preg_replace('/^\xEF\xBB\xBF/', '', $content); + + // Verschiedene Zeilenenden normalisieren + $content = str_replace(["\r\n", "\r"], "\n", $content); + $lines = explode("\n", $content); + + $result = []; + foreach ($lines as $line) { + if (trim($line) === '') { + $result[] = []; + continue; + } + $result[] = $this->parseCsvLine($line, $delimiter, $enclosure); + } + return $result; + } + + private function parseCsvLine(string $line, string $delimiter, string $enclosure): array + { + $cells = []; + $len = strlen($line); + $i = 0; + + while ($i < $len) { + if ($line[$i] === $enclosure) { + // Quoted cell + $i++; + $cell = ''; + while ($i < $len) { + if ($line[$i] === $enclosure) { + if ($i + 1 < $len && $line[$i + 1] === $enclosure) { + $cell .= $enclosure; + $i += 2; + } else { + $i++; + break; + } + } else { + $cell .= $line[$i++]; + } + } + $cells[] = $cell; + // Delimiter überspringen + if ($i < $len && $line[$i] === $delimiter) { + $i++; + } + } else { + // Unquoted cell + $end = strpos($line, $delimiter, $i); + if ($end === false) { + $cells[] = substr($line, $i); + break; + } + $cells[] = substr($line, $i, $end - $i); + $i = $end + 1; + } + } + + return $cells; + } + + private function readXlsx(string $filePath): array + { + if (!class_exists('ZipArchive')) { + throw new \RuntimeException('ZipArchive nicht verfügbar – XLSX kann nicht gelesen werden.'); + } + + $zip = new \ZipArchive(); + if ($zip->open($filePath) !== true) { + throw new \RuntimeException('XLSX-Datei konnte nicht geöffnet werden.'); + } + + // Shared Strings laden (optional) + $sharedStrings = []; + $ssXml = $zip->getFromName('xl/sharedStrings.xml'); + if ($ssXml !== false) { + $ss = simplexml_load_string($ssXml); + foreach ($ss->si ?? [] as $si) { + $text = ''; + if (isset($si->t)) { + $text = (string)$si->t; + } else { + foreach ($si->r ?? [] as $r) { + $text .= (string)($r->t ?? ''); + } + } + $sharedStrings[] = $text; + } + } + + // Erstes Worksheet lesen + $sheetXml = $zip->getFromName('xl/worksheets/sheet1.xml'); + $zip->close(); + + if ($sheetXml === false) { + throw new \RuntimeException('Kein Arbeitsblatt in der XLSX-Datei gefunden.'); + } + + $sheet = simplexml_load_string($sheetXml); + $rows = []; + $maxCol = 0; + + foreach ($sheet->sheetData->row ?? [] as $row) { + $rowArr = []; + foreach ($row->c as $cell) { + [$colIdx] = $this->xlsxCellRef((string)$cell['r']); + if ($colIdx > $maxCol) $maxCol = $colIdx; + + $type = (string)($cell['t'] ?? ''); + $value = (string)($cell->v ?? ''); + + if ($type === 's') { + $value = $sharedStrings[(int)$value] ?? ''; + } elseif ($type === 'inlineStr') { + $value = (string)($cell->is->t ?? ''); + } + + $rowArr[$colIdx] = $value; + } + + // Lücken auffüllen + $filled = []; + for ($c = 0; $c <= $maxCol; $c++) { + $filled[] = $rowArr[$c] ?? ''; + } + $rows[] = $filled; + } + + return $rows; + } + + private function xlsxCellRef(string $ref): array + { + preg_match('/^([A-Z]+)(\d+)$/', $ref, $m); + $col = 0; + foreach (str_split($m[1] ?? 'A') as $char) { + $col = $col * 26 + (ord($char) - ord('A') + 1); + } + return [$col - 1, (int)($m[2] ?? 1) - 1]; + } + + private function parseDate(string $value, string $format): ?string + { + $value = trim($value); + if ($value === '') return null; + + $d = \DateTime::createFromFormat($format, $value); + if (!$d) return null; + + return $d->format('Y-m-d'); + } + + private function parseDecimal(string $value, string $decimal, string $thousands): float + { + $value = trim($value); + // Tausendertrennzeichen entfernen, Dezimaltrennzeichen → Punkt + $value = str_replace($thousands, '', $value); + $value = str_replace($decimal, '.', $value); + $value = preg_replace('/[^\d.\-]/', '', $value); + return (float)$value; + } +} diff --git a/src/Views/bank/index.php b/src/Views/bank/index.php new file mode 100644 index 0000000..08ec359 --- /dev/null +++ b/src/Views/bank/index.php @@ -0,0 +1,121 @@ + View::h($v); +$fmt = fn($v) => View::formatMoney($v); + +$months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez']; +?> + + +
+
+
+ + + + + + + + + + + + + Zurücksetzen +
+
+
+ + +
+
+
+ Bankbuchungen + 0): ?> + () + +
+
+
+ + + + + + + + + + + + + + + + = 0; + $color = $isPlus ? 'var(--color-success)' : 'var(--color-danger)'; + ?> + + + + + + + + + +
WertstellungBuchungstextWertStatus
+ Keine Buchungen gefunden. +
+
+ +
+ +
+ € + + 'badge-green', + 'ignoriert' => 'badge-gray', + default => 'badge-blue', + }; + ?> + +
+
+ + 1): ?> +
+ + $p])); ?> + + +
+ +
diff --git a/src/Views/bank/show.php b/src/Views/bank/show.php new file mode 100644 index 0000000..59f41c6 --- /dev/null +++ b/src/Views/bank/show.php @@ -0,0 +1,129 @@ + View::h($v); +$fmt = fn($v) => View::formatMoney($v); + +$wert = (float)$transaction['wert']; +$isPlus = $wert >= 0; +$color = $isPlus ? 'var(--color-success)' : 'var(--color-danger)'; +?> + + +
+
+
+ Wertstellung
+ +
+
+ Buchungstag
+ +
+
+ Vorgang
+ –' ?> +
+
+ Buchungstext
+ +
+
+ Wert
+ + € + +
+
+ Status
+ 'badge-green', + 'ignoriert' => 'badge-gray', + default => 'badge-blue', + }; + ?> + +
+
+
+ + +
+
+ + +
+ ← Zurück +
+
+
+ + +
+
Beleg hochladen
+
+
+
+
+ +
PDF, PNG, JPEG, TIFF, XML
+
+ +
+
+
+
+ + +
+
+
Offene Belege
+ ungebuchte Belege +
+ +
Keine offenen Belege vorhanden.
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Belegnr.DatumTypLieferant / NotizBrutto
+ + + –' ?> + + · + + + –' ?> + +
+ + +
+
+
+ +
diff --git a/src/Views/bank/upload.php b/src/Views/bank/upload.php new file mode 100644 index 0000000..81112cf --- /dev/null +++ b/src/Views/bank/upload.php @@ -0,0 +1,87 @@ + View::h($v); ?> + +
+
+
+ + +
+ +
🏦
+
Bank-Buchungen hierher ziehen oder klicken
+
CSV · TXT · XLSX
+
+
+ + +
+ + +
Format der hochgeladenen Datei
+
+ +
+ + Abbrechen +
+
+
+
+ + diff --git a/src/Views/layout/nav.php b/src/Views/layout/nav.php index 4d5e3c7..c9e7b70 100644 --- a/src/Views/layout/nav.php +++ b/src/Views/layout/nav.php @@ -24,6 +24,10 @@ function navActive(string $prefix, string $current): string { Belege + + + Bank buchen + Buchungsjournal