From 9839bd5b16800680ade7a4c30737b69dbbcd1941 Mon Sep 17 00:00:00 2001
From: fds2610-bot
Date: Mon, 17 Aug 2026 22:24:14 +0200
Subject: [PATCH] =?UTF-8?q?Banking:=20Ums=C3=A4tze=20sammelweise=20l=C3=B6?=
=?UTF-8?q?schen?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Die Umsatzlisten haben jetzt dieselbe Mehrfachauswahl wie ein Kontoblatt:
ein Ankreuzfeld vor jedem Umsatz, eines in der Titelleiste für alle
angezeigten und darüber der Knopf „Auswahl löschen“. Er steht in jedem
Reiter – offen, übernommen und verworfen –, nennt die Anzahl der Auswahl
und bleibt ohne Auswahl gesperrt. Bisher ließ sich ein abgerufener Umsatz
nur übernehmen oder verwerfen; wegräumen ging gar nicht.
Gelöscht wird allein die abgerufene Zeile. Eine daraus entstandene
Buchung gehört ins Kontoblatt und bleibt dort stehen – wer sie loswerden
will, löscht sie dort, und dann kehrt ihr Umsatz ohnehin in die
Vorschlagsliste zurück. Mit dem Umsatz geht aber sein Fingerabdruck: Was
im nächsten Abrufzeitraum liegt, kommt danach wieder herein, bei einem
verworfenen Umsatz also erneut als offen.
- Neue Route POST /banking/umsaetze/loeschen auf
Bankumsatz::mehrereLoeschen(); der Status hängt am Formularziel, sodass
der Weg in denselben Reiter zurückführt. Ohne JavaScript weist der
Controller eine leere Auswahl ab.
- Die Liste ist keine Tabelle, sondern eine Folge von Karten. Die
Kopfzeile, die eine Tabelle von selbst hätte, gibt es deshalb neu als
.listenkopf; die Mechanik dahinter (.alle-waehlen mit data-waehlt) ist
unverändert die des Kontoblatts.
Co-Authored-By: Claude Opus 5
---
Changes.md | 19 ++
docs/banking.md | 20 +++
public/index.php | 1 +
public/static/app.css | 28 +++
src/Controllers/BankingController.php | 30 ++++
src/Models/Bankumsatz.php | 26 +++
src/Views/banking/umsaetze.php | 40 ++++-
tests/28_UmsaetzeLoeschenTest.php | 238 ++++++++++++++++++++++++++
8 files changed, 397 insertions(+), 5 deletions(-)
create mode 100644 tests/28_UmsaetzeLoeschenTest.php
diff --git a/Changes.md b/Changes.md
index df18f6e..0ec2372 100644
--- a/Changes.md
+++ b/Changes.md
@@ -1,5 +1,24 @@
# Änderungen
+## Umsätze im Banking sammelweise löschen (17.08.2026)
+
+Die Umsatzlisten des Bankings haben jetzt dieselbe Löschfunktion wie ein Kontoblatt: ein
+Ankreuzfeld vor jedem Umsatz, eines in der Titelleiste für alle angezeigten und darüber den
+Knopf **Auswahl löschen**. Bisher ließ sich ein abgerufener Umsatz nur übernehmen oder
+verwerfen – wegräumen ging gar nicht.
+
+- Der Knopf steht in jedem Reiter – *offen*, *übernommen* und *verworfen* –, nennt die Anzahl
+ der Auswahl und bleibt ohne Auswahl gesperrt. Die Rückfrage vor dem Löschen nennt sie
+ ebenfalls.
+- Gelöscht wird allein die abgerufene Zeile. Eine daraus entstandene Buchung bleibt im
+ Kontoblatt stehen; mit dem Umsatz geht aber sein Fingerabdruck, sodass er beim nächsten
+ Abruf wieder hereinkommen kann.
+- Neue Route `POST /banking/umsaetze/loeschen` auf `Bankumsatz::mehrereLoeschen()`; sie kehrt
+ in den Reiter zurück, aus dem gelöscht wurde.
+- Die Liste ist keine Tabelle, sondern eine Folge von Karten. Die Kopfzeile, die eine Tabelle
+ von selbst hätte, gibt es deshalb neu als `.listenkopf`; die Mechanik dahinter ist
+ dieselbe wie im Kontoblatt (`.alle-waehlen` mit `data-waehlt`).
+
## Ruhige Buchungszeilen im Kontoblatt (17.08.2026)
Die Zeilen des Kontoblatts sind jetzt alle gleich hoch – zwei Textzeilen – und ihr Inhalt
diff --git a/docs/banking.md b/docs/banking.md
index e0ce944..a5b42fc 100644
--- a/docs/banking.md
+++ b/docs/banking.md
@@ -242,6 +242,20 @@ Nichts verschwindet von allein: Ein Umsatz, der weder übernommen noch verworfen
offen und steht beim nächsten Öffnen wieder da. Das Cockpit zählt die offenen unter *zum
Abgleich*.
+### Umsätze löschen
+
+Jeder Reiter der Liste – *offen*, *übernommen*, *verworfen* – hat dieselbe Mehrfachauswahl
+wie ein Kontoblatt: ein Ankreuzfeld vor jedem Umsatz, eines in der Titelleiste für alle
+angezeigten und darüber der Knopf **Auswahl löschen** (`POST /banking/umsaetze/loeschen`,
+`Bankumsatz::mehrereLoeschen()`). Der Knopf nennt die Anzahl und bleibt ohne Auswahl
+gesperrt; ohne JavaScript weist der Controller eine leere Auswahl ab.
+
+Gelöscht wird allein die abgerufene Zeile. Eine daraus entstandene Buchung gehört ins
+Kontoblatt und bleibt dort stehen – wer sie loswerden will, löscht sie dort, und dann kehrt
+ihr Umsatz ohnehin in die Vorschlagsliste zurück. Mit dem Umsatz geht aber sein
+Fingerabdruck: Was innerhalb des nächsten Abrufzeitraums liegt, kommt danach wieder herein –
+bei einem verworfenen Umsatz also erneut als offen.
+
## Regeln
Eine Regel prüft ein Muster gegen Empfänger und/oder Verwendungszweck:
@@ -312,6 +326,12 @@ angehängt, und das Speichern des Zugangsformulars lässt die Wahl stehen. Die A
Bank steckt hinter derselben Grenze wie der Dialog – geprüft wird, was mit ihrem Ergebnis
geschieht.
+`tests/28_UmsaetzeLoeschenTest.php` prüft das Sammellöschen: Es trifft genau die
+ausgewählten Zeilen, eine schon erzeugte Buchung überlebt es, jeder Status lässt sich
+löschen, leere und unbekannte Auswahl bleiben folgenlos, eine doppelt gesendete Nummer zählt
+einmal – und die gerenderte Ansicht trägt in jedem Reiter Ankreuzfelder, Titelleiste und
+Knopf, während die leere Liste keinen Knopf zeigt.
+
`tests/19_KontenzuordnungTest.php` nimmt sich den Weg vor, den ein Umsatz nimmt, wenn die
Zuordnung erst nach dem Abruf getroffen wird: Offene Umsätze ziehen mit, übernommene nicht,
und nur das Bankkonto, um das es geht. Dazu das automatische Buchen – mit Regel, ohne Regel,
diff --git a/public/index.php b/public/index.php
index 7f0eb68..3a3f488 100644
--- a/public/index.php
+++ b/public/index.php
@@ -278,6 +278,7 @@ $router->post('/banking/neu', fn() => (new BankingController())->anlegen());
// „/banking/umsaetze“ und „/banking/regeln“.
$router->get('/banking/umsaetze', fn() => (new BankingController())->umsaetze());
$router->post('/banking/umsaetze/alle', fn() => (new BankingController())->alleUebernehmen());
+$router->post('/banking/umsaetze/loeschen', fn() => (new BankingController())->umsaetzeLoeschen());
$router->post('/banking/umsaetze/{id}/uebernehmen',
fn(array $p) => (new BankingController())->uebernehmen($p));
$router->post('/banking/umsaetze/{id}/verwerfen',
diff --git a/public/static/app.css b/public/static/app.css
index f8ad4fc..82b0dfb 100644
--- a/public/static/app.css
+++ b/public/static/app.css
@@ -1637,6 +1637,34 @@ table.bericht .wertlink:hover {
gap: 1rem;
}
+/* Das Ankreuzfeld der Mehrfachauswahl steht vor dem Text und richtet sich an
+ der ersten Zeile aus, nicht an der Mitte der Karte. */
+.umsatzwahl {
+ flex: none;
+ margin-top: .25rem;
+}
+
+/* Titelleiste einer Kartenliste: Sie vertritt die Kopfzeile, die eine Tabelle
+ von selbst hätte – hier steht das Ankreuzfeld „alle wählen“. */
+.listenkopf {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: .35rem .9rem;
+ margin-bottom: .4rem;
+ border-bottom: 1px solid var(--rand);
+ font-size: .85rem;
+ color: var(--text-gedaempft);
+}
+
+.listenkopf .ankreuzfeld {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ font-weight: 500;
+}
+
.umsatztext { min-width: 0; }
.umsatztext .nebentext { display: block; font-size: .8rem; }
diff --git a/src/Controllers/BankingController.php b/src/Controllers/BankingController.php
index dc6c81e..df7c300 100644
--- a/src/Controllers/BankingController.php
+++ b/src/Controllers/BankingController.php
@@ -557,6 +557,36 @@ class BankingController extends BaseController
View::redirect('/banking/umsaetze?status=offen', 'Umsatz steht wieder zum Abgleich bereit.');
}
+ /**
+ * Mehrere in der Abgleichliste angekreuzte Umsätze auf einmal löschen.
+ *
+ * Gelöscht wird nur die abgerufene Zeile. Eine daraus entstandene Buchung
+ * bleibt im Kontoblatt stehen – wer sie loswerden will, löscht sie dort.
+ */
+ public function umsaetzeLoeschen(): void
+ {
+ $this->verlangeAnmeldung();
+
+ // Zurück in denselben Reiter, aus dem gelöscht wurde.
+ $status = $this->query('status', 'offen');
+ if (!array_key_exists($status, Bankumsatz::STATUS)) {
+ $status = 'offen';
+ }
+ $ziel = '/banking/umsaetze?status=' . $status;
+
+ $ids = $this->postIds('umsaetze');
+ if ($ids === []) {
+ View::redirect($ziel, 'Kein Umsatz ausgewählt.', 'warnung');
+ return;
+ }
+
+ $anzahl = Bankumsatz::mehrereLoeschen($ids);
+
+ View::redirect($ziel, $anzahl === 1
+ ? 'Umsatz gelöscht.'
+ : $anzahl . ' Umsätze gelöscht.');
+ }
+
/**
* Alles übernehmen, was einen eindeutigen Vorschlag hat.
*
diff --git a/src/Models/Bankumsatz.php b/src/Models/Bankumsatz.php
index a6a925b..7cdb835 100644
--- a/src/Models/Bankumsatz.php
+++ b/src/Models/Bankumsatz.php
@@ -363,4 +363,30 @@ class Bankumsatz
{
Database::execute('DELETE FROM bank_umsaetze WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
}
+
+ /**
+ * Mehrere Umsätze auf einmal löschen – die Mehrfachauswahl der Abgleichliste.
+ *
+ * Eine schon erzeugte Buchung bleibt stehen: Sie gehört ins Kontoblatt und
+ * wird auch nur dort gelöscht. Weg ist allein die abgerufene Zeile – und mit
+ * ihr ihr Fingerabdruck, weshalb ein gelöschter Umsatz beim nächsten Abruf
+ * wieder hereinkommen kann.
+ *
+ * @param list $ids
+ * @return int Wie viele Umsätze tatsächlich gelöscht wurden.
+ */
+ public static function mehrereLoeschen(array $ids): int
+ {
+ $ids = array_values(array_unique(array_filter($ids, static fn (int $id): bool => $id > 0)));
+ if ($ids === []) {
+ return 0;
+ }
+
+ $platzhalter = implode(', ', array_fill(0, count($ids), '?'));
+
+ return Database::execute(
+ 'DELETE FROM bank_umsaetze WHERE benutzer_id = ? AND id IN (' . $platzhalter . ')',
+ array_merge([Mandant::id()], $ids)
+ );
+ }
}
diff --git a/src/Views/banking/umsaetze.php b/src/Views/banking/umsaetze.php
index 114603e..86b109a 100644
--- a/src/Views/banking/umsaetze.php
+++ b/src/Views/banking/umsaetze.php
@@ -38,14 +38,28 @@ include __DIR__ . '/../layout/kopf.php';
Regeln
-
+