From 6ce2c72cd92d064be4a4001c65ebd807cf2c38c9 Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Mon, 29 Jun 2026 18:15:39 +0200 Subject: [PATCH] Fix #108: Lieferant in Belegliste inline bearbeiten --- Changes.md | 9 +++++ public/index.php | 5 ++- src/Controllers/BelegController.php | 24 ++++++++++++ src/Models/Beleg.php | 8 ++++ src/Views/belege/list.php | 53 +++++++++++++++++++++++++- src/version.txt | 2 +- tests/46_BelegLieferantInlineTest.php | 55 +++++++++++++++++++++++++++ 7 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 tests/46_BelegLieferantInlineTest.php diff --git a/Changes.md b/Changes.md index 73d46b2..738e2d1 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,15 @@ --- +## 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 +- **`src/Models/Beleg.php`:** Neue Methode `updateSupplier()` aktualisiert nur die `supplier_id` ohne andere Metadaten zu verändern +- **`src/Controllers/BelegController.php`:** Neue Methode `updateSupplier()` als AJAX-Endpunkt +- **`public/index.php`:** Route `POST /belege/{id}/supplier` registriert + +--- + ## 2026-06-11 – Fix: Undefined-Array-Key-Warning bei Buchungsvorlagen ohne Standard-Lieferant - **`src/Models/Buchungsvorlage.php`:** `create()`/`update()` prüften `$data['standard_lieferant']` direkt ohne Null-Coalescing, was bei fehlendem Schlüssel eine PHP-Warning auslöste diff --git a/public/index.php b/public/index.php index 8a9ef53..5392cb8 100644 --- a/public/index.php +++ b/public/index.php @@ -60,8 +60,9 @@ $router->post('/belege/upload', fn($p) => (new BelegController())->upload($p)); $router->get('/belege/{id}', fn($p) => (new BelegController())->show($p)); $router->get('/belege/{id}/file', fn($p) => (new BelegController())->serveFile($p)); $router->post('/belege/{id}/delete', fn($p) => (new BelegController())->destroy($p)); -$router->post('/belege/{id}/meta', fn($p) => (new BelegController())->updateMeta($p)); -$router->post('/belege/{id}/crop', fn($p) => (new BelegController())->cropImage($p)); +$router->post('/belege/{id}/meta', fn($p) => (new BelegController())->updateMeta($p)); +$router->post('/belege/{id}/supplier', fn($p) => (new BelegController())->updateSupplier($p)); +$router->post('/belege/{id}/crop', fn($p) => (new BelegController())->cropImage($p)); $router->post('/belege/{id}/vorschlag/generate', fn($p) => (new BelegController())->generateVorschlag($p)); $router->post('/belege/{id}/vorschlag/reset', fn($p) => (new BelegController())->resetVorschlag($p)); diff --git a/src/Controllers/BelegController.php b/src/Controllers/BelegController.php index 4d145f9..eb98f58 100644 --- a/src/Controllers/BelegController.php +++ b/src/Controllers/BelegController.php @@ -366,6 +366,30 @@ class BelegController extends BaseController View::redirect("/belege/{$id}", 'Metadaten gespeichert.'); } + public function updateSupplier(array $params): void + { + header('Content-Type: application/json'); + $id = (int)$params['id']; + $beleg = Beleg::find($id); + + if (!$beleg) { + http_response_code(404); + echo json_encode(['error' => 'Nicht gefunden.']); + return; + } + if ($beleg['locked']) { + http_response_code(403); + echo json_encode(['error' => 'GoBD: Gesperrter Beleg.']); + return; + } + + $supplierName = trim($_POST['supplier_name'] ?? ''); + $supplierId = $supplierName !== '' ? Supplier::findOrCreate($supplierName) : null; + Beleg::updateSupplier($id, $supplierId); + + echo json_encode(['ok' => true, 'supplier_name' => $supplierName !== '' ? $supplierName : null]); + } + public function generateVorschlag(array $params): void { header('Content-Type: application/json'); diff --git a/src/Models/Beleg.php b/src/Models/Beleg.php index aea9bc0..56781c7 100644 --- a/src/Models/Beleg.php +++ b/src/Models/Beleg.php @@ -231,6 +231,14 @@ class Beleg ); } + public static function updateSupplier(int $id, ?int $supplierId): void + { + Database::execute( + 'UPDATE belege SET supplier_id=?, updated_at=NOW() WHERE id=? AND locked=0', + [$supplierId, $id] + ); + } + public static function delete(int $id): bool { $beleg = self::find($id); diff --git a/src/Views/belege/list.php b/src/Views/belege/list.php index 3487c6d..b7f1600 100644 --- a/src/Views/belege/list.php +++ b/src/Views/belege/list.php @@ -162,7 +162,13 @@ - + @@ -245,4 +251,49 @@ }); }); })(); + +function blInlineSupplier(td) { + if (td.querySelector('input')) return; + + const current = td.dataset.supplier || ''; + const belegId = td.dataset.belegId; + let committed = false; + + td.textContent = ''; + const input = document.createElement('input'); + input.type = 'text'; + input.value = current; + input.style.cssText = 'width:100%;border:1px solid var(--color-primary,#3b82f6);border-radius:3px;padding:.1rem .3rem;font-size:inherit;background:#fff;box-sizing:border-box;color:inherit'; + + function commit(val) { + if (committed) return; + committed = true; + const fd = new FormData(); + fd.append('supplier_name', val); + fetch('/belege/' + belegId + '/supplier', { + method: 'POST', + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + body: fd, + }) + .then(r => r.json()) + .then(data => { + td.dataset.supplier = val; + td.textContent = val || '–'; + }) + .catch(() => { td.textContent = current || '–'; }); + } + + input.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { e.preventDefault(); commit(input.value.trim()); } + if (e.key === 'Escape') { committed = true; td.textContent = current || '–'; } + }); + + input.addEventListener('blur', function () { + commit(input.value.trim()); + }); + + td.appendChild(input); + input.focus(); + input.select(); +} diff --git a/src/version.txt b/src/version.txt index 161bbd1..08f076b 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -1-260611 +1-260629 diff --git a/tests/46_BelegLieferantInlineTest.php b/tests/46_BelegLieferantInlineTest.php new file mode 100644 index 0000000..1174fd3 --- /dev/null +++ b/tests/46_BelegLieferantInlineTest.php @@ -0,0 +1,55 @@ + 'TEST-SUPP-' . uniqid(), + 'filename' => 'inline-supplier-test.pdf', + 'storage_path' => '', + 'mime_type' => 'application/pdf', + 'file_hash' => hash('sha256', 'inline-supplier-' . uniqid()), + 'beleg_datum' => date('Y-m-d'), + 'belegtyp' => 'Eingangsrechnung', + ]); + $this->assertGreaterThan(0, $belegId, 'Beleg angelegt'); + + // Kein Lieferant initial + $beleg = Beleg::find($belegId); + $this->assert($beleg['supplier_name'] === null || $beleg['supplier_name'] === '', 'Kein Lieferant initial'); + + // Lieferant setzen + $supplierId = Supplier::findOrCreate('Testfirma GmbH'); + $this->assertGreaterThan(0, $supplierId, 'Supplier angelegt oder gefunden'); + Beleg::updateSupplier($belegId, $supplierId); + + $beleg = Beleg::find($belegId); + $this->assertEquals('Testfirma GmbH', $beleg['supplier_name'], 'Lieferant nach updateSupplier gesetzt'); + + // Lieferant auf null zurücksetzen (löschen) + Beleg::updateSupplier($belegId, null); + $beleg = Beleg::find($belegId); + $this->assert( + $beleg['supplier_name'] === null || $beleg['supplier_name'] === '', + 'Lieferant nach Nullsetzung leer' + ); + + // updateSupplier darf bei gesperrtem Beleg nichts ändern + \Steuerkram\Database::execute('UPDATE belege SET locked=1 WHERE id=?', [$belegId]); + $supplierId2 = Supplier::findOrCreate('Gesperrte Firma'); + Beleg::updateSupplier($belegId, $supplierId2); + $beleg = Beleg::find($belegId); + $this->assert( + $beleg['supplier_name'] === null || $beleg['supplier_name'] === '', + 'Lieferant bei gesperrtem Beleg unverändert' + ); + } +}