Finanzmanager/src/Views/buchungen/kontoblatt.php
fds2610-bot 78f1b91bc0 Online-Banking: Kontenzuordnung, Sammellöschen und drei Umsatz-Fehler
Kontenzuordnung als dritter Einrichtungsschritt: Welches Bankkonto gehört zu
welchem eigenen Konto? Umsätze merken sich über `bank_konto_id`, woher sie
stammen, und ziehen mit, wenn die Zuordnung erst nach einem Abruf fällt.
Abgerufene Umsätze buchen sich auf Wunsch selbst (`Services\Bankabgleich`).

Im Kontoblatt lassen sich Buchungen sammelweise löschen; Statusmeldungen liegen
jetzt über der Ansicht, statt den Inhalt springen zu lassen.

Dazu drei Fehler, die zusammen dafür sorgten, dass abgerufene Umsätze spurlos
verschwanden – der Abruf meldete sie als vorhanden, das Kontoblatt zeigte
nichts, und in der Vorschlagsliste standen sie ebenfalls nicht:

- Eine gelöschte Buchung ließ ihren Umsatz auf „uebernommen“ stehen. Der
  Fremdschlüssel setzte nur `buchung_id` auf NULL. `Buchung::loeschen()` gibt
  den Umsatz jetzt vorher frei, `Bankumsatz::verwaisteOeffnen()` fängt die Wege
  ab, die daran vorbeiführen – Kaskaden lösen in MariaDB keine Trigger aus.
- Ein Bankkonto entstand doppelt, wenn die Bank die IBAN nicht bei jedem Dialog
  mitliefert. Gesucht wird jetzt über die Kontonummer; eine nachgelieferte IBAN
  wird am vorhandenen Eintrag nachgetragen.
- Der Fingerabdruck hing an der IBAN und kippte mit ihr. Er nimmt jetzt die
  `bank_konto_id`, die fest steht, solange es den Bankzugang gibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:54:23 +02:00

202 lines
8.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use Finanzmanager\View;
/** @var array<string,mixed> $konto */
/** @var list<array<string,mixed>> $zeilen */
/** @var int $gesamt */
/** @var int $seite */
/** @var int $seiten */
/** @var array<string,mixed> $filter */
/** @var list<array<string,mixed>> $kategorien */
/** @var list<array<string,mixed>> $klassen */
/** @var array<int,string> $kontenNamen */
$kategorieNamen = [];
foreach ($kategorien as $k) {
$kategorieNamen[(int)$k['id']] = $k['pfad'];
}
$klassenNamen = [];
foreach ($klassen as $k) {
$klassenNamen[(int)$k['id']] = $k['name'];
}
$gefiltert = ($filter['suche'] ?? '') !== '' || !empty($filter['von']) || !empty($filter['bis'])
|| !empty($filter['kategorie_id']) || !empty($filter['klasse_id']) || !empty($filter['nur_offene']);
$seitenPfad = static function (int $nr) use ($filter): string {
$p = array_filter([
'suche' => $filter['suche'] ?? '',
'von' => $filter['von'] ?? '',
'bis' => $filter['bis'] ?? '',
'kategorie_id' => $filter['kategorie_id'] ?? '',
'klasse_id' => $filter['klasse_id'] ?? '',
'nur_offene' => !empty($filter['nur_offene']) ? '1' : '',
'seite' => (string)$nr,
], static fn(string $v): bool => $v !== '');
return '?' . http_build_query($p);
};
// Nach dem Löschen soll dieselbe Ansicht wieder erscheinen – Filter und Seite
// hängen deshalb am Ziel des Löschformulars.
$loeschZiel = '/konten/' . (int)$konto['id'] . '/buchungen/loeschen' . $seitenPfad($seite);
include __DIR__ . '/../layout/kopf.php';
?>
<div class="tableiste">
<span class="tab aktiv">Kontoblatt</span>
<a href="/konten/<?= (int)$konto['id'] ?>/bearbeiten" class="tab">Kontodaten</a>
</div>
<div class="seitenaktionen">
<a href="/konten/<?= (int)$konto['id'] ?>/buchung" class="knopf primaer">Neue Buchung</a>
<a href="/konten" class="knopf">Kontenübersicht</a>
<?php if ($zeilen !== []): ?>
<!-- Das Formular steht hier, seine Ankreuzfelder stehen in der Tabelle;
verbunden sind sie über das form-Attribut. Ein Formular um die
ganze Tabelle würde das Filterformular verschachteln. -->
<form method="post" action="<?= View::h($loeschZiel) ?>" id="buchungsauswahl" class="inline"
data-bestaetigen="Die ausgewählten Buchungen wirklich löschen?"
data-frage-eine="Die ausgewählte Buchung wirklich löschen?"
data-frage-mehrere="{anzahl} ausgewählte Buchungen wirklich löschen?">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken ?? '') ?>">
<button type="submit" class="knopf gefahr">Auswahl löschen</button>
</form>
<?php endif; ?>
<span class="fuellung"></span>
<span class="saldoanzeige">
Aktueller Saldo
<strong class="<?= View::betragKlasse($konto['saldo_aktuell']) ?>">
<?= View::betrag($konto['saldo_aktuell']) ?>
</strong>
</span>
<?php if (abs((float)$konto['saldo_erwartet'] - (float)$konto['saldo_aktuell']) > 0.005): ?>
<span class="saldoanzeige">
Erwartet
<strong class="<?= View::betragKlasse($konto['saldo_erwartet']) ?>">
<?= View::betrag($konto['saldo_erwartet']) ?>
</strong>
</span>
<?php endif; ?>
</div>
<form method="get" action="/konten/<?= (int)$konto['id'] ?>" class="filterleiste">
<input type="search" name="suche" placeholder="Empfänger, Text, Nummer"
value="<?= View::h($filter['suche'] ?? '') ?>">
<label>von <input type="date" name="von" value="<?= View::h($filter['von'] ?? '') ?>"></label>
<label>bis <input type="date" name="bis" value="<?= View::h($filter['bis'] ?? '') ?>"></label>
<select name="kategorie_id">
<option value="">Alle Kategorien</option>
<?php foreach ($kategorien as $k): ?>
<option value="<?= (int)$k['id'] ?>"
<?= (int)($filter['kategorie_id'] ?? 0) === (int)$k['id'] ? 'selected' : '' ?>>
<?= View::h($k['pfad']) ?>
</option>
<?php endforeach; ?>
</select>
<select name="klasse_id">
<option value="">Alle Klassen</option>
<?php foreach ($klassen as $k): ?>
<option value="<?= (int)$k['id'] ?>"
<?= (int)($filter['klasse_id'] ?? 0) === (int)$k['id'] ? 'selected' : '' ?>>
<?= View::h($k['name']) ?>
</option>
<?php endforeach; ?>
</select>
<label class="ankreuzfeld">
<input type="checkbox" name="nur_offene" value="1" <?= !empty($filter['nur_offene']) ? 'checked' : '' ?>>
nur nicht abgeglichene
</label>
<button type="submit" class="knopf klein">Filtern</button>
<?php if ($gefiltert): ?>
<a href="/konten/<?= (int)$konto['id'] ?>" class="knopf klein">Filter zurücksetzen</a>
<?php endif; ?>
</form>
<?php if ($zeilen === []): ?>
<p class="platzhalter">
<?= $gefiltert ? 'Keine Buchung passt zum Filter.' : 'Für dieses Konto ist noch nichts gebucht.' ?>
</p>
<?php else: ?>
<table class="liste kontoblatt">
<thead>
<tr>
<th class="wahlspalte">
<input type="checkbox" class="alle-waehlen" data-waehlt="buchungsauswahl"
aria-label="Alle angezeigten Buchungen auswählen">
</th>
<th>Datum</th>
<th>Nr.</th>
<th>Empfänger</th>
<th>Buchungstext</th>
<th>Kategorie</th>
<th>Klasse</th>
<th class="rechts">Betrag</th>
<th class="rechts">Saldo</th>
<th>✓</th>
</tr>
</thead>
<tbody>
<?php foreach ($zeilen as $zeile): ?>
<tr class="<?= $zeile['gegensicht'] ? 'gegensicht' : '' ?>">
<td class="wahlspalte">
<?php if (!$zeile['gegensicht']): ?>
<input type="checkbox" name="buchungen[]" value="<?= (int)$zeile['id'] ?>"
form="buchungsauswahl"
aria-label="Buchung vom <?= View::h(View::datum($zeile['datum'])) ?> auswählen">
<?php endif; ?>
</td>
<td class="nowrap"><?= View::datum($zeile['datum']) ?></td>
<td class="nebentext"><?= View::h($zeile['nummer']) ?></td>
<td>
<?php if ($zeile['gegensicht']): ?>
<?= View::h($zeile['empfaenger']) ?>
<span class="marke" title="Gegenseite einer Umbuchung – bearbeitbar auf dem Ursprungskonto">
Umbuchung
</span>
<?php else: ?>
<a href="/buchungen/<?= (int)$zeile['id'] ?>"><?= View::h($zeile['empfaenger'] ?: '(ohne Empfänger)') ?></a>
<?php endif; ?>
</td>
<td class="nebentext gekuerzt"><?= View::h(mb_substr((string)$zeile['buchungstext'], 0, 90)) ?></td>
<td>
<?php if ((int)$zeile['gesplittet'] === 1): ?>
<span class="marke">Splitbuchung</span>
<?php elseif (!empty($zeile['gegenkonto_id']) && !$zeile['gegensicht']): ?>
[<?= View::h($kontenNamen[(int)$zeile['gegenkonto_id']] ?? '?') ?>]
<?php else: ?>
<?= View::h($kategorieNamen[(int)$zeile['kategorie_id']] ?? '') ?>
<?php endif; ?>
</td>
<td class="nebentext"><?= View::h($klassenNamen[(int)$zeile['klasse_id']] ?? '') ?></td>
<td class="rechts <?= View::betragKlasse($zeile['betrag']) ?>"><?= View::betrag($zeile['betrag']) ?></td>
<td class="rechts <?= $zeile['saldo'] === null ? 'nebentext' : View::betragKlasse($zeile['saldo']) ?>">
<?= $zeile['saldo'] === null ? '–' : View::betrag($zeile['saldo']) ?>
</td>
<td class="nebentext"><?= (int)$zeile['abgeglichen'] === 1 ? '✓' : '' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="blaettern">
<span class="nebentext"><?= (int)$gesamt ?> Buchungen</span>
<?php if ($seiten > 1): ?>
<?php if ($seite > 1): ?>
<a href="<?= View::h($seitenPfad($seite - 1)) ?>" class="knopf klein">Neuer</a>
<?php endif; ?>
<span class="nebentext">Seite <?= (int)$seite ?> von <?= (int)$seiten ?></span>
<?php if ($seite < $seiten): ?>
<a href="<?= View::h($seitenPfad($seite + 1)) ?>" class="knopf klein">Älter</a>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php include __DIR__ . '/../layout/fuss.php'; ?>