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>
202 lines
8.7 KiB
PHP
202 lines
8.7 KiB
PHP
<?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'; ?>
|