Fix #108: Lieferant in Belegliste inline bearbeiten

This commit is contained in:
fds2610-bot 2026-06-29 18:15:39 +02:00
parent fe3f64ae89
commit 6ce2c72cd9
7 changed files with 152 additions and 4 deletions

View file

@ -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 ## 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 - **`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

View file

@ -61,6 +61,7 @@ $router->get('/belege/{id}', fn($p) => (new BelegController())->show($p));
$router->get('/belege/{id}/file', fn($p) => (new BelegController())->serveFile($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}/delete', fn($p) => (new BelegController())->destroy($p));
$router->post('/belege/{id}/meta', fn($p) => (new BelegController())->updateMeta($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}/crop', fn($p) => (new BelegController())->cropImage($p));
$router->post('/belege/{id}/vorschlag/generate', fn($p) => (new BelegController())->generateVorschlag($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)); $router->post('/belege/{id}/vorschlag/reset', fn($p) => (new BelegController())->resetVorschlag($p));

View file

@ -366,6 +366,30 @@ class BelegController extends BaseController
View::redirect("/belege/{$id}", 'Metadaten gespeichert.'); 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 public function generateVorschlag(array $params): void
{ {
header('Content-Type: application/json'); header('Content-Type: application/json');

View file

@ -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 public static function delete(int $id): bool
{ {
$beleg = self::find($id); $beleg = self::find($id);

View file

@ -162,7 +162,13 @@
</div> </div>
</td> </td>
<td class="td-muted"><?= View::formatDate($b['beleg_datum']) ?></td> <td class="td-muted"><?= View::formatDate($b['beleg_datum']) ?></td>
<td class="td-muted"><?= $h($b['supplier_name'] ?? '–') ?></td> <td class="td-muted bl-supplier-cell"
data-beleg-id="<?= (int)$b['id'] ?>"
data-supplier="<?= $h($b['supplier_name'] ?? '') ?>"
onclick="event.stopPropagation(); blInlineSupplier(this)"
style="cursor:text;min-width:80px"
title="Klicken zum Lieferant bearbeiten"
><?= $h($b['supplier_name'] ?? '–') ?></td>
<td class="td-muted"><?= $h($b['rechnungs_nr'] ?? '–') ?></td> <td class="td-muted"><?= $h($b['rechnungs_nr'] ?? '–') ?></td>
<td class="td-right td-mono"><?= $b['betrag_brutto'] !== null ? View::formatMoney($b['betrag_brutto']) : '–' ?></td> <td class="td-right td-mono"><?= $b['betrag_brutto'] !== null ? View::formatMoney($b['betrag_brutto']) : '–' ?></td>
<td onclick="event.stopPropagation()"> <td onclick="event.stopPropagation()">
@ -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();
}
</script> </script>

View file

@ -1 +1 @@
1-260611 1-260629

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
use Steuerkram\Models\Beleg;
use Steuerkram\Models\Supplier;
class BelegLieferantInlineTest extends TestCase
{
protected string $testName = 'Test 46: Beleg-Lieferant Inline-Update via updateSupplier()';
public function run(): void
{
$belegId = Beleg::create([
'beleg_nr' => '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'
);
}
}