Finanzmanager/src/Views/buchungen/kontoblatt.php
fds2610-bot 275acd3285 Fix #134: Buchungen bewusst ohne Kategorie ermöglichen
Neuer Schalter „Bewusst ohne Kategorie“ in der Buchungsmaske für
Abschreibungen und rein rechnerische Solarerträge. Solche Buchungen
erscheinen nicht unter den offenen Vorgängen im Cockpit, nicht im
Prüflauf des Monatsabschlusses, setzen das Konto in der Kontenübersicht
nicht fett und werden von der Regelanwendung sowie dem automatischen
Zusammenführen zu Umbuchungen ausgelassen. Kommt eine Kategorie, ein
Gegenkonto oder eine Splitbuchung hinzu, entfällt die Markierung.

Migration 028_bewusst_ohne_kategorie.sql, Test 59 und E2E-Test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:22:22 +02:00

623 lines
30 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\Models\Adresse;
use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Wertpapier;
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 list<array<string,mixed>> $konten */
/** @var array<int, list<array<string,mixed>>> $splits */
/** @var bool $bargeld Ist ein Bargeld-Tool eingerichtet? */
/** @var array<string,mixed>|null $neueRegel Aus der Buchungsmaske erzeugte Regel, zum Prüfen im Dialog */
$kategorieNamen = [];
foreach ($kategorien as $k) {
$kategorieNamen[(int)$k['id']] = $k['pfad'];
}
$klassenNamen = [];
foreach ($klassen as $k) {
$klassenNamen[(int)$k['id']] = $k['name'];
}
$kontenNamen = array_column($konten, 'name', 'id');
$eigenesKonto = (int)$konto['id'];
$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. Dasselbe gilt für die
// Schnellzuordnung: Ihre Ziele setzt kontoblatt.js aus dieser Abfrage zusammen.
$abfrage = $seitenPfad($seite);
$loeschZiel = '/konten/' . (int)$konto['id'] . '/buchungen/loeschen' . $abfrage;
$verknuepfenZiel = '/konten/' . (int)$konto['id'] . '/buchungen/verknuepfen' . $abfrage;
/**
* Die Zuordnung einer Zeile als Wert der gemeinsamen Auswahlliste – dieselben
* Präfixe wie in der Buchungsmaske.
*/
$zuordnungswert = static function (array $zeile): string {
if ((int)($zeile['gesplittet'] ?? 0) === 1) {
return 'split';
}
if (!empty($zeile['gegenkonto_id'])) {
return 'konto:' . (int)$zeile['gegenkonto_id'];
}
if (!empty($zeile['kategorie_id'])) {
return 'kategorie:' . (int)$zeile['kategorie_id'];
}
return '';
};
/**
* Die Splitzeilen einer Buchung, wie der Dialog sie braucht. Sie hängen als
* JSON an der Zelle statt nachgeladen zu werden: Es sind die Zeilen der wenigen
* aufgeteilten Buchungen dieser Seite, und ein zweiter Weg zu denselben Daten
* wäre auch ein zweiter Weg, sie falsch zu lesen.
*
* @param list<array<string,mixed>> $splitzeilen
*/
$splitdaten = static function (array $splitzeilen) use ($zuordnungswert): string {
return (string)json_encode(array_map(static fn(array $zeile): array => [
'zuordnung' => $zuordnungswert($zeile),
'klasse_id' => (int)($zeile['klasse_id'] ?? 0),
'notiz' => (string)$zeile['notiz'],
'betrag' => View::betrag($zeile['betrag'], false),
], $splitzeilen), JSON_UNESCAPED_UNICODE);
};
/**
* Der Tooltip einer Splitbuchung: je Zeile Kategorie und Betrag. In der Spalte
* steht nur „Splitbuchung“ – wohin das Geld geht, wäre sonst erst im Dialog zu
* sehen. Notiz und Klasse bleiben draußen; die Klassen nennt die Klassenspalte.
*
* @param list<array<string,mixed>> $splitzeilen
*/
$splitname = static fn(array $zeile): string => match (true) {
!empty($zeile['gegenkonto_id']) => '[' . ($kontenNamen[(int)$zeile['gegenkonto_id']] ?? '?') . ']',
!empty($zeile['kategorie_id']) => (string)($kategorieNamen[(int)$zeile['kategorie_id']] ?? '?'),
default => '(ohne Kategorie)',
};
$splituebersicht = static function (array $splitzeilen) use ($splitname): string {
$texte = [];
foreach ($splitzeilen as $zeile) {
$texte[] = $splitname($zeile) . ': ' . View::betrag($zeile['betrag']);
}
return implode("\n", $texte);
};
/**
* Der Tooltip der Klassenspalte, wenn die Splitzeilen verschiedene Klassen
* tragen: je Zeile Kategorie und Klasse.
*
* @param list<array<string,mixed>> $splitzeilen
*/
$splitklassen = static function (array $splitzeilen) use ($splitname, $klassenNamen): string {
$texte = [];
foreach ($splitzeilen as $zeile) {
$texte[] = $splitname($zeile) . ': '
. (string)($klassenNamen[(int)($zeile['klasse_id'] ?? 0)] ?? '(ohne Klasse)');
}
return implode("\n", $texte);
};
/**
* Das Symbol der Abgleichspalte, siehe View::bargeldMarke() – die Schnell-
* zuordnung (kontoblatt.js) braucht dasselbe Symbol für eine einzelne Zeile,
* ohne die Seite neu zu laden.
*
* @param array<string,mixed> $zeile
*/
$bargeldmarke = static fn(array $zeile): string
=> View::bargeldMarke($zeile['bargeld_status'] ?? null, $zeile['bargeld_hinweis'] ?? null);
/**
* IBAN und Bank der Gegenseite, soweit der Bankabruf sie geliefert hat – der
* Tooltip über dem Verwendungszweck. kontoblatt.js hängt ihn zusätzlich zum
* (nur bei abgeschnittenem Text nötigen) vollständigen Text an; ohne
* JavaScript bleibt er unsichtbar, wie schon der Tooltip für abgeschnittenen
* Text.
*
* @param array<string,mixed> $zeile
*/
$herkunft = static function (array $zeile): string {
$teile = [];
if (($zeile['iban'] ?? '') !== '') {
$teile[] = 'IBAN ' . Adresse::ibanFormatiert((string)$zeile['iban']);
}
if (($zeile['bankname'] ?? '') !== '') {
$teile[] = (string)$zeile['bankname'];
}
return implode("\n", $teile);
};
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 ?? '') ?>">
<?php /* Der erste Knopf im Formular bleibt „Auswahl löschen“ –
kontoblatt.js/app.js greift sich per querySelector den
ersten Absende-Knopf, um ihn nach Anzahl der Auswahl zu
beschriften und zu sperren. Verknüpfen nutzt dasselbe
Formular und Ankreuzfeld, nur mit anderem Ziel: zwei
ausgewählte Buchungen, die denselben Vorgang doppelt
zeigen (eine Umbuchung und ihre einzeln erfasste
Dublette), führt es zusammen. */ ?>
<button type="submit" id="auswahl-loeschen" class="knopf gefahr">Auswahl löschen</button>
<button type="submit" id="auswahl-verknuepfen" formaction="<?= View::h($verknuepfenZiel) ?>" class="knopf"
data-bestaetigen="Beide ausgewählten Buchungen zu einer Umbuchung verknüpfen? Die einzeln erfasste Buchung wird dabei gelöscht.">
Verknüpfen
</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" aria-label="Kategorie" data-kombobox>
<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" data-abfrage="<?= View::h($abfrage) ?>">
<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>
<?php if ($bargeld): ?>
<th class="bargeldspalte" title="Abgleich mit dem Bargeld-Tool">Bar</th>
<?php endif; ?>
<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
// Auch die Gegenseite einer fremden Umbuchung lässt sich
// ankreuzen – gelöscht wird sie dadurch nicht (das prüft
// der Controller anhand ihres wirklichen Kontos), aber
// verknüpfen lässt sie sich mit einer einzeln erfassten
// Dublette auf diesem Konto.
$auswahlLabel = $zeile['gegensicht']
? 'Umbuchung vom ' . View::datum($zeile['datum']) . ' auswählen'
: 'Buchung vom ' . View::datum($zeile['datum']) . ' auswählen';
?>
<input type="checkbox" name="buchungen[]" value="<?= (int)$zeile['id'] ?>"
form="buchungsauswahl"
aria-label="<?= View::h($auswahlLabel) ?>">
</td>
<td class="nowrap"><?= View::datum($zeile['datum']) ?></td>
<td class="nebentext"><?= View::h($zeile['nummer']) ?></td>
<td>
<?php /* Der Text steht in einem eigenen Kasten, nicht direkt in
der Zelle: Abgeschnitten wird über zwei Zeilen, und das
kann nur ein Element, das kein Tabellenfeld ist. Die
Marke steht neben dem Kasten – im Kasten zählte sie als
eine der beiden Zeilen. */ ?>
<?php if ($zeile['gegensicht']): ?>
<div class="zellenreihe">
<div class="zellentext"><?= View::h($zeile['empfaenger']) ?></div>
<span class="marke" title="Gegenseite einer Umbuchung – bearbeitbar auf dem Ursprungskonto">
Umbuchung
</span>
</div>
<?php else: ?>
<div class="zellentext">
<a href="/buchungen/<?= (int)$zeile['id'] ?>"><?= View::h($zeile['empfaenger'] ?: '(ohne Empfänger)') ?></a>
</div>
<?php endif; ?>
</td>
<?php /* Ungekürzt: Was nicht auf zwei Zeilen passt, schneidet das
Stylesheet ab – der vollständige Text bleibt für den
Tooltip erhalten. */ ?>
<td class="nebentext buchungstext"><div class="zellentext" data-herkunft="<?= View::h($herkunft($zeile)) ?>"><?= View::h($zeile['buchungstext']) ?></div></td>
<?php
// Die Gegenseite einer fremden Umbuchung nennt das Konto, von dem
// sie kommt – und zwar dort, wo sonst die Kategorie steht. Ohne
// diese Angabe stünde auf dem Gegenkonto eine Umbuchung ohne
// erkennbares Gegenüber.
$gegenkonto = empty($zeile['gegenkonto_id'])
? ''
: '[' . ($kontenNamen[(int)$zeile['gegenkonto_id']] ?? '?') . ']';
$beschriftung = match (true) {
(int)$zeile['gesplittet'] === 1 => 'Splitbuchung',
$gegenkonto !== '' => $gegenkonto,
!empty($zeile['kategorie_id']) => (string)($kategorieNamen[(int)$zeile['kategorie_id']] ?? ''),
// Absichtlich leer (#134) – sichtbar, damit es nicht wie
// vergessen aussieht.
!empty($zeile['bewusst_ohne_kategorie']) => 'bewusst ohne Kategorie',
default => '',
};
// Eine Splitbuchung nennt im Tooltip ihre Zeilen, alles andere
// seine eigene – womöglich abgeschnittene – Beschriftung.
$zuordnungstitel = match (true) {
isset($splits[(int)$zeile['id']]) => $splituebersicht($splits[(int)$zeile['id']]),
$beschriftung !== '' => $beschriftung,
default => 'Kategorie oder Gegenkonto zuordnen',
};
?>
<td class="zuordnungszelle">
<?php if ($zeile['gegensicht']): ?>
<?php /* Auch die Gegenseite führt zu ihrer Buchung – sie liegt
auf dem anderen Konto und wird nur dort geändert. */ ?>
<a href="/buchungen/<?= (int)$zeile['id'] ?>" class="zuordnungsfeld gegenseite"
title="Umbuchung von <?= View::h($kontenNamen[(int)$zeile['gegenkonto_id']] ?? '?') ?> – dort zu ändern">
<?= View::h($beschriftung) ?>
</a>
<?php else: ?>
<?php /* Ohne JavaScript führt der Klick in die Buchungsmaske;
kontoblatt.js macht daraus die Auswahl an Ort und Stelle. */ ?>
<?php
// Wertpapierabrechnungen führen ISIN und WKN im
// Verwendungszweck mit. Wer die Zeile ins Depot bucht,
// bekommt das Papier deshalb vorgeschlagen, statt es
// in einer langen Liste zu suchen.
$papier = Wertpapier::ausText(
(string)$zeile['buchungstext'] . ' ' . (string)$zeile['empfaenger']
);
?>
<a href="/buchungen/<?= (int)$zeile['id'] ?>" class="zuordnungsfeld"
data-buchung="<?= (int)$zeile['id'] ?>"
data-wert="<?= View::h($zuordnungswert($zeile)) ?>"
data-betrag="<?= View::h(number_format((float)$zeile['betrag'], 2, '.', '')) ?>"
data-datum="<?= View::h(substr((string)$zeile['datum'], 0, 10)) ?>"
data-zweck="<?= View::h((string)$zeile['buchungstext']) ?>"
data-isin="<?= View::h($papier['isin']) ?>"
data-wkn="<?= View::h($papier['wkn']) ?>"
data-bezeichnung="<?= View::h(View::datum($zeile['datum']) . ' · ' . ($zeile['empfaenger'] ?: '(ohne Empfänger)')) ?>"
<?php if (isset($splits[(int)$zeile['id']])): ?>
data-splits="<?= View::h($splitdaten($splits[(int)$zeile['id']])) ?>"
<?php endif; ?>
title="<?= View::h($zuordnungstitel) ?>">
<?php if ($beschriftung === 'Splitbuchung'): ?>
<span class="marke">Splitbuchung</span>
<?php else: ?>
<?= View::h($beschriftung) ?>
<?php endif; ?>
</a>
<?php endif; ?>
</td>
<?php
// Bei einer Splitbuchung zählen die Klassen ihrer Zeilen – wie
// bei der Kategorie: eine gemeinsame Klasse steht in der
// Spalte, verschiedene nennt der Tooltip der Marke.
$splitzeilen = !$zeile['gegensicht'] ? ($splits[(int)$zeile['id']] ?? null) : null;
$klasse = $splitzeilen !== null
? Buchung::splitKlasse($splitzeilen, $zeile['klasse_id'] !== null ? (int)$zeile['klasse_id'] : null)
: ['gemischt' => false, 'klasse_id' => (int)$zeile['klasse_id']];
$klassenname = (string)($klassenNamen[(int)$klasse['klasse_id']] ?? '');
$klassentitel = match (true) {
$klasse['gemischt'] => $splitklassen($splitzeilen),
$klassenname !== '' => $klassenname,
default => 'Klasse zuordnen',
};
?>
<td class="klassenzelle nebentext">
<?php if ($zeile['gegensicht']): ?>
<?= View::h($klassenname) ?>
<?php else: ?>
<?php /* Wie bei der Zuordnung: Ohne JavaScript führt der Klick
in die Buchungsmaske, mit ihm öffnet sich die Auswahl
an Ort und Stelle – bei einer Splitbuchung der
Splitdialog, denn dort stehen ihre Klassen. */ ?>
<a href="/buchungen/<?= (int)$zeile['id'] ?>" class="klassenfeld"
data-buchung="<?= (int)$zeile['id'] ?>"
data-klasse="<?= (int)$klasse['klasse_id'] ?>"
<?php if ($splitzeilen !== null): ?>data-split="1"<?php endif; ?>
title="<?= View::h($klassentitel) ?>">
<?php if ($klasse['gemischt']): ?>
<span class="marke">Splitbuchung</span>
<?php else: ?>
<?= View::h($klassenname) ?>
<?php endif; ?>
</a>
<?php endif; ?>
</td>
<?php if ($bargeld): ?>
<td class="bargeldspalte"><?= $bargeldmarke($zeile) ?></td>
<?php endif; ?>
<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 /* Die Auswahlliste gibt es einmal; kontoblatt.js hängt sie in die
angeklickte Zelle. Eine Liste je Zeile wären bei hundert Zeilen
ebenso viele vollständige Kategoriebäume. */ ?>
<form method="post" action="" id="zuordnungsauswahl" class="zuordnungsauswahl" hidden>
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<select name="zuordnung" aria-label="Kategorie oder Gegenkonto" data-kombobox>
<?php $gewaehlt = ''; $mitSplit = true; $mitDepot = true; ?>
<?php include __DIR__ . '/zuordnungsliste.php'; ?>
</select>
<noscript><button type="submit" class="knopf klein">Übernehmen</button></noscript>
</form>
<?php /* Dieselbe Bauart für die Klassenspalte: eine Liste, die in die
angeklickte Zelle wandert. Der erste Eintrag löscht die Klasse. */ ?>
<form method="post" action="" id="klassenauswahl" class="zuordnungsauswahl" hidden>
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<select name="klasse_id" aria-label="Klasse">
<option value="">— keine —</option>
<?php foreach ($klassen as $k): ?>
<option value="<?= (int)$k['id'] ?>"><?= View::h($k['name']) ?></option>
<?php endforeach; ?>
</select>
<noscript><button type="submit" class="knopf klein">Übernehmen</button></noscript>
</form>
<dialog id="splitdialog" class="splitdialog">
<form method="post" action="" id="splitformular">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<h2>Splitbuchung</h2>
<p class="feldhinweis">
<span id="splitbezeichnung"></span> über
<output id="splitbuchungsbetrag" class="betrag">0,00</output>
</p>
<table class="liste splittabelle">
<thead>
<tr>
<th>Kategorie / Gegenkonto</th><th>Klasse</th><th>Notiz</th>
<th class="rechts">Betrag</th>
</tr>
</thead>
<tbody id="splitdialogzeilen">
<?php for ($nr = 0; $nr < 4; $nr++): ?>
<tr>
<td>
<select name="split[<?= $nr ?>][zuordnung]">
<?php $gewaehlt = ''; $mitSplit = false; $mitDepot = false; ?>
<?php include __DIR__ . '/zuordnungsliste.php'; ?>
</select>
</td>
<td>
<select name="split[<?= $nr ?>][klasse_id]">
<option value="">— keine —</option>
<?php foreach ($klassen as $k): ?>
<option value="<?= (int)$k['id'] ?>"><?= View::h($k['name']) ?></option>
<?php endforeach; ?>
</select>
</td>
<td><input type="text" name="split[<?= $nr ?>][notiz]" value=""></td>
<td>
<input type="text" class="splitbetrag rechts" inputmode="decimal"
name="split[<?= $nr ?>][betrag]" value="">
</td>
</tr>
<?php endfor; ?>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="rechts">Restbetrag</td>
<td class="rechts"><output id="splitdialogsumme" class="betrag">0,00</output></td>
</tr>
</tfoot>
</table>
<p class="feldhinweis">
Die Summe der Zeilen muss dem Buchungsbetrag entsprechen. Bleiben alle
Zeilen leer, wird die Splitbuchung aufgehoben.
</p>
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Splitbuchung speichern</button>
<button type="button" class="knopf" id="splitzeileMehr">Zeile hinzufügen</button>
<button type="button" class="knopf" id="splitabbrechen">Abbrechen</button>
<?php /* Rechts außen, abseits der übrigen Knöpfe: hebt die Aufteilung
samt aller Zuordnungen auf. Nur sichtbar, wenn es etwas
aufzuheben gibt – kontoblatt.js blendet ihn ein. */ ?>
<button type="submit" class="knopf gefahr" id="splitloeschen"
name="aktion" value="loeschen" formnovalidate hidden>
Splitbuchung löschen
</button>
</div>
</form>
</dialog>
<?php /* Ein Wertpapiergeschäft entsteht dort, wo seine Abrechnung ankommt:
am Girokonto. Die Auswahl eines Depots in der Zuordnungsliste öffnet
diesen Dialog; die Nebenkosten liegen dahinter im zweiten. */ ?>
<dialog id="wertpapierdialog" class="splitdialog wertpapierdialog">
<form method="post" action="" id="wertpapierformular" class="formular">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<input type="hidden" name="depot_id" id="wp_depot" value="">
<h2>Wertpapiergeschäft</h2>
<p class="feldhinweis">
<span id="wp_bezeichnung"></span> über
<output id="wp_buchungsbetrag" class="betrag">0,00</output> ·
Depot <strong id="wp_depotname"></strong>
</p>
<p class="feldhinweis verwendungszweck" id="wp_verwendungszweck"></p>
<?php $datumsvorgabe = date('Y-m-d'); ?>
<?php $vorschlag = ['isin' => '', 'wkn' => '']; ?>
<?php include __DIR__ . '/wertpapierfelder.php'; ?>
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Geschäft buchen</button>
<button type="button" class="knopf" id="wp_kostenknopf">
Kosten … (<output id="wp_kostenknopfsumme" class="betrag">0,00</output>)
</button>
<button type="button" class="knopf" id="wp_abbrechen">Abbrechen</button>
<span class="fuellung"></span>
<span class="saldoanzeige">
Einstandspreis <strong><output id="wp_einstand" class="betrag">0,00</output></strong>
</span>
</div>
</form>
</dialog>
<?php /* Die Kostenfelder gehören zum Formular oben, stehen aber in einem
eigenen Dialog – verbunden über das form-Attribut, wie schon die
Ankreuzfelder der Sammellöschung. */ ?>
<dialog id="kostendialog" class="splitdialog">
<h2>Kosten</h2>
<?php $kostenFormular = 'wertpapierformular'; ?>
<?php include __DIR__ . '/wertpapierkosten.php'; ?>
<div class="knopfleiste">
<button type="button" class="knopf primaer" id="wp_kostenfertig">Übernehmen</button>
</div>
</dialog>
<script src="/static/betrag.js" defer></script>
<script src="/static/wertpapierbuchung.js" defer></script>
<script src="/static/kombobox.js" defer></script>
<script src="/static/kontoblatt.js" defer></script>
<?php endif; ?>
<?php if ($neueRegel !== null): ?>
<?php /* Öffnet sich automatisch (regeldialog.js) nach dem Speichern einer
Buchung mit gesetztem Toggle „Regel anlegen“. „Abbruch“ entfernt die
gerade erst angelegte Regel wieder – über dasselbe Löschformular wie
die Regelliste, nur mit Rücksprung hierher statt dorthin. */ ?>
<dialog id="regeldialog" class="splitdialog regeldialog">
<form method="post" action="/banking/regeln/<?= (int)$neueRegel['id'] ?>" id="regelformular" class="formular">
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
<input type="hidden" name="rueckziel_konto_id" value="<?= (int)$konto['id'] ?>">
<h2>Regel anlegen</h2>
<p class="feldhinweis">
Aus dieser Buchung wurde diese Regel erzeugt. Vor dem Speichern lässt sie
sich noch anpassen; „Abbruch“ verwirft sie wieder.
</p>
<?php $regel = $neueRegel; $idPrefix = 'regeldialog_'; ?>
<?php include __DIR__ . '/../banking/regelfelder.php'; ?>
<div class="knopfleiste">
<button type="submit" class="knopf primaer">Speichern</button>
<button type="submit" class="knopf" id="regelabbrechen"
formaction="/banking/regeln/<?= (int)$neueRegel['id'] ?>/loeschen" formnovalidate>
Abbruch
</button>
</div>
</form>
</dialog>
<script src="/static/regeldialog.js" defer></script>
<?php endif; ?>
<?php include __DIR__ . '/../layout/fuss.php'; ?>