Feature: Drag&Drop-Upload in Bank-Zuordnung; Beleg-Upload ohne sofortige Verknüpfung; Button 'Verknüpfung aufheben' im Belegs-Badge

This commit is contained in:
fds2610-bot 2026-05-15 17:48:51 +02:00
parent 702b0e4664
commit b1be01ff86
3 changed files with 100 additions and 28 deletions

View file

@ -209,9 +209,12 @@ class BankController
{ {
$id = (int)($params['id'] ?? 0); $id = (int)($params['id'] ?? 0);
$belegId = (int)($_POST['beleg_id'] ?? 0); $belegId = (int)($_POST['beleg_id'] ?? 0);
$redirectTo = $_POST['redirect_to'] ?? '';
BankTransaction::unlinkBeleg($id, $belegId); BankTransaction::unlinkBeleg($id, $belegId);
View::redirect("/bank/$id", 'Beleg-Verknüpfung entfernt.');
$safe = (preg_match('#^/[a-zA-Z0-9/_-]#', $redirectTo)) ? $redirectTo : "/bank/$id";
View::redirect($safe, 'Beleg-Verknüpfung entfernt.');
} }
public function uploadBeleg(array $params): void public function uploadBeleg(array $params): void
@ -263,9 +266,7 @@ class BankController
'belegtyp' => (float)$transaction['wert'] >= 0 ? 'Ausgangsrechnung' : 'Eingangsrechnung', 'belegtyp' => (float)$transaction['wert'] >= 0 ? 'Ausgangsrechnung' : 'Eingangsrechnung',
]); ]);
$betrag = BankTransaction::getRemainingBetrag($id) ?: abs((float)$transaction['wert']); View::redirect("/bank/$id", 'Beleg hochgeladen – bitte jetzt verknüpfen.');
BankTransaction::linkBeleg($id, $belegId, $betrag);
View::redirect('/bank', 'Beleg hochgeladen und Buchung verknüpft.');
} }
private function renderView(string $name, array $data = []): string private function renderView(string $name, array $data = []): string

View file

@ -126,22 +126,81 @@ $restFormatted = number_format($remaining, 2, ',', '.');
<?php if ($remaining > 0.01): ?> <?php if ($remaining > 0.01): ?>
<!-- Beleg hochladen --> <!-- Beleg hochladen -->
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header"><div class="card-title">Neuen Beleg hochladen &amp; verknüpfen</div></div> <div class="card-header"><div class="card-title">Neuen Beleg hochladen</div></div>
<div class="card-body"> <div class="card-body" style="padding-bottom:1rem">
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/beleg" enctype="multipart/form-data"> <form method="post" action="/bank/<?= (int)$transaction['id'] ?>/beleg"
<div style="display:flex;gap:.75rem;align-items:flex-end;flex-wrap:wrap"> enctype="multipart/form-data" id="beleg-upload-form">
<div class="form-group" style="margin:0;flex:1;min-width:200px"> <div id="beleg-dnd-zone" style="
<input type="file" name="beleg_file" required border:2px dashed var(--color-border);
border-radius:var(--radius);
background:var(--color-surface);
padding:1.5rem 2rem;
text-align:center;
cursor:pointer;
transition:border-color .15s,background .15s;
position:relative;
">
<input type="file" name="beleg_file" id="beleg-dnd-input" required
accept=".pdf,.png,.jpg,.jpeg,.tif,.tiff,.xml" accept=".pdf,.png,.jpg,.jpeg,.tif,.tiff,.xml"
style="display:block"> style="position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%">
<div class="form-hint">PDF, PNG, JPEG, TIFF, XML</div> <div style="pointer-events:none">
<div style="font-size:1.5rem;line-height:1;margin-bottom:.4rem">📄</div>
<div style="font-weight:600">Beleg hierher ziehen oder klicken</div>
<div class="text-muted text-sm" style="margin-top:.25rem">PDF · PNG · JPEG · TIFF · XML</div>
<div id="beleg-dnd-status" style="margin-top:.5rem;min-height:1.25rem"></div>
</div> </div>
<button type="submit" class="btn btn-primary">Hochladen &amp; verknüpfen</button> </div>
<div style="margin-top:.75rem;display:flex;justify-content:flex-end">
<button type="submit" class="btn btn-primary" id="beleg-upload-btn" disabled>
Hochladen
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<script>
(function () {
const zone = document.getElementById('beleg-dnd-zone');
const input = document.getElementById('beleg-dnd-input');
const status = document.getElementById('beleg-dnd-status');
const btn = document.getElementById('beleg-upload-btn');
function setActive(on) {
zone.style.borderColor = on ? 'var(--color-primary)' : 'var(--color-border)';
zone.style.background = on ? '#eff6ff' : 'var(--color-surface)';
}
function onFile(file) {
if (!file) return;
const ext = file.name.split('.').pop().toLowerCase();
if (!['pdf','png','jpg','jpeg','tif','tiff','xml'].includes(ext)) {
status.innerHTML = '<span style="color:var(--color-danger)">Nicht unterstütztes Format.</span>';
btn.disabled = true;
return;
}
status.innerHTML = '<span style="color:var(--color-success)">✓ ' + file.name
+ ' (' + (file.size / 1024).toFixed(1) + ' KB)</span>';
btn.disabled = false;
}
zone.addEventListener('dragover', e => { e.preventDefault(); setActive(true); });
zone.addEventListener('dragleave', e => { if (!zone.contains(e.relatedTarget)) setActive(false); });
zone.addEventListener('drop', e => {
e.preventDefault();
setActive(false);
const file = e.dataTransfer.files[0];
if (file) {
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
onFile(file);
}
});
input.addEventListener('change', () => onFile(input.files[0]));
})();
</script>
<!-- Offene Belege + Vorschau nebeneinander --> <!-- Offene Belege + Vorschau nebeneinander -->
<div style="display:flex;gap:1rem;align-items:flex-start"> <div style="display:flex;gap:1rem;align-items:flex-start">

View file

@ -298,11 +298,11 @@ $suppliers = $suppliers ?? [];
<!-- Gespeicherte Bankbuchungs-Verknüpfung --> <!-- Gespeicherte Bankbuchungs-Verknüpfung -->
<?php if ($linkedBankTx ?? null): ?> <?php if ($linkedBankTx ?? null): ?>
<?php $lbt = $linkedBankTx; $lbwert = (float)$lbt['wert']; ?> <?php $lbt = $linkedBankTx; $lbwert = (float)$lbt['wert']; ?>
<a href="/bank/<?= (int)$lbt['id'] ?>" class="card mb-2" <div class="card mb-2" style="border-left:4px solid var(--color-success)">
style="display:block;text-decoration:none;color:inherit;border-left:4px solid var(--color-success)">
<div class="card-body" style="padding:.6rem 1.25rem"> <div class="card-body" style="padding:.6rem 1.25rem">
<div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap"> <div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap">
<span class="badge badge-green" style="font-size:.8rem">Bankbuchung</span> <span class="badge badge-green" style="font-size:.8rem">Bankbuchung</span>
<a href="/bank/<?= (int)$lbt['id'] ?>" style="display:contents;text-decoration:none;color:inherit">
<span class="td-mono text-sm"><?= View::formatDate($lbt['wertstellung']) ?></span> <span class="td-mono text-sm"><?= View::formatDate($lbt['wertstellung']) ?></span>
<?php if ($lbt['vorgang']): ?> <?php if ($lbt['vorgang']): ?>
<span class="text-sm text-muted"><?= $h($lbt['vorgang']) ?></span> <span class="text-sm text-muted"><?= $h($lbt['vorgang']) ?></span>
@ -311,10 +311,22 @@ $suppliers = $suppliers ?? [];
title="<?= $h($lbt['buchungstext']) ?>"><?= $h($lbt['buchungstext']) ?></span> title="<?= $h($lbt['buchungstext']) ?>"><?= $h($lbt['buchungstext']) ?></span>
<strong style="white-space:nowrap;color:<?= $lbwert >= 0 ? 'var(--color-success)' : 'var(--color-danger)' ?>"> <strong style="white-space:nowrap;color:<?= $lbwert >= 0 ? 'var(--color-success)' : 'var(--color-danger)' ?>">
<?= $lbwert >= 0 ? '+' : '' ?><?= number_format($lbwert, 2, ',', '.') ?> € <?= $lbwert >= 0 ? '+' : '' ?><?= number_format($lbwert, 2, ',', '.') ?> €
<?php if (!empty($lbt['alloc_betrag']) && (float)$lbt['alloc_betrag'] !== abs($lbwert)): ?>
<span class="text-muted" style="font-weight:normal;font-size:.85em">
(zugeordnet: <?= number_format((float)$lbt['alloc_betrag'], 2, ',', '.') ?> €)
</span>
<?php endif; ?>
</strong> </strong>
</div>
</div>
</a> </a>
<form method="post" action="/bank/<?= (int)$lbt['id'] ?>/unlink"
data-confirm="Verknüpfung mit dieser Bankbuchung wirklich aufheben?">
<input type="hidden" name="beleg_id" value="<?= (int)$beleg['id'] ?>">
<input type="hidden" name="redirect_to" value="/belege/<?= (int)$beleg['id'] ?>">
<button type="submit" class="btn btn-secondary btn-sm">Verknüpfung aufheben</button>
</form>
</div>
</div>
</div>
<?php endif; ?> <?php endif; ?>
<!-- BankBuchung-Badge (ersetzt KI-Badge wenn aus Bank-Kontext) --> <!-- BankBuchung-Badge (ersetzt KI-Badge wenn aus Bank-Kontext) -->