$params */ public function kontoblatt(array $params): void { $this->verlangeAnmeldung(); $konto = Konto::find((int)$params['id']); if ($konto === null) { $this->nichtGefunden('/konten/' . $params['id']); return; } $filter = [ 'suche' => $this->query('suche'), 'von' => $this->query('von'), 'bis' => $this->query('bis'), 'kategorie_id' => $this->query('kategorie_id'), 'klasse_id' => $this->query('klasse_id'), 'nur_offene' => $this->query('nur_offene') === '1', ]; $zeilen = Buchung::kontoblatt((int)$konto['id'], $filter); // Die neuesten Buchungen zuerst zeigen, den Saldo aber chronologisch // aufbauen – deshalb erst rechnen, dann umdrehen. $zeilen = array_reverse($zeilen); $grenze = Einstellung::getInt('zeilen_pro_seite', 100); $gesamt = count($zeilen); $seite = max(1, (int)$this->query('seite', '1')); $zeilen = array_slice($zeilen, ($seite - 1) * $grenze, $grenze); // Die Splitzeilen der angezeigten Buchungen in einer Abfrage – der // Splitdialog zeigt damit eine bestehende Aufteilung, ohne sie // nachzuladen. $gesplittete = []; foreach ($zeilen as $zeile) { if ((int)$zeile['gesplittet'] === 1 && !$zeile['gegensicht']) { $gesplittete[] = (int)$zeile['id']; } } $this->render('buchungen/kontoblatt.php', [ 'titel' => 'Kontoblatt: ' . $konto['name'], 'konto' => Konto::mitSalden($konto), 'zeilen' => $zeilen, 'gesamt' => $gesamt, 'seite' => $seite, 'seiten' => max(1, (int)ceil($gesamt / max(1, $grenze))), 'filter' => $filter, 'kategorien' => Kategorie::baumFlach(), 'klassen' => Klasse::alle(), 'konten' => Konto::alle(), 'splits' => Buchung::splitsJeBuchung($gesplittete), ]); } /** @param array $params */ public function neu(array $params): void { $this->verlangeAnmeldung(); $konto = Konto::find((int)$params['id']); if ($konto === null) { $this->nichtGefunden('/konten/' . $params['id']); return; } $this->maske($konto, null); } /** @param array $params */ public function anlegen(array $params): void { $this->verlangeAnmeldung(); $kontoId = (int)$params['id']; if (Konto::find($kontoId) === null) { $this->nichtGefunden('/konten/' . $params['id']); return; } try { [$daten, $splits] = $this->formulardaten($kontoId); Buchung::anlegen($daten, $splits); } catch (\Throwable $e) { View::redirect('/konten/' . $kontoId . '/buchung', $e->getMessage(), 'fehler'); return; } View::redirect('/konten/' . $kontoId, 'Buchung angelegt.'); } /** @param array $params */ public function bearbeiten(array $params): void { $this->verlangeAnmeldung(); $buchung = Buchung::find((int)$params['id']); if ($buchung === null) { $this->nichtGefunden('/buchungen/' . $params['id']); return; } $this->maske(Konto::find((int)$buchung['konto_id']), $buchung); } /** @param array $params */ public function speichern(array $params): void { $this->verlangeAnmeldung(); $id = (int)$params['id']; $buchung = Buchung::find($id); if ($buchung === null) { $this->nichtGefunden('/buchungen/' . $params['id']); return; } try { [$daten, $splits] = $this->formulardaten((int)$buchung['konto_id']); Buchung::speichern($id, $daten, $splits); } catch (\Throwable $e) { View::redirect('/buchungen/' . $id, $e->getMessage(), 'fehler'); return; } View::redirect('/konten/' . $buchung['konto_id'], 'Buchung gespeichert.'); } /** @param array $params */ public function loeschen(array $params): void { $this->verlangeAnmeldung(); $buchung = Buchung::find((int)$params['id']); if ($buchung === null) { $this->nichtGefunden('/buchungen/' . $params['id']); return; } Buchung::loeschen((int)$buchung['id']); View::redirect('/konten/' . $buchung['konto_id'], 'Buchung gelöscht.'); } /** * Schnellzuordnung aus dem Kontoblatt: Kategorie oder Gegenkonto einer * Buchung setzen, ohne die Maske zu öffnen. * * @param array $params */ public function zuordnung(array $params): void { $this->verlangeAnmeldung(); $buchung = Buchung::find((int)$params['id']); if ($buchung === null) { $this->nichtGefunden('/buchungen/' . $params['id']); return; } $ziel = $this->kontoblattPfad((int)$buchung['konto_id']); try { $zuordnung = self::zuordnungAus($this->post('zuordnung')); Buchung::zuordnen( (int)$buchung['id'], $zuordnung['kategorie_id'], $zuordnung['gegenkonto_id'] ); } catch (\Throwable $e) { View::redirect($ziel, $e->getMessage(), 'fehler'); return; } View::redirect($ziel, 'Zuordnung gespeichert.'); } /** * Der Splitdialog des Kontoblatts: die Zeilen einer Buchung ersetzen. * Ohne Zeilen wird die Splitbuchung aufgehoben. * * Der Knopf „Splitbuchung löschen“ schickt dasselbe Formular mit * `aktion=loeschen`. Seine Zeilen zählen dann nicht: Gelöscht wird die * Aufteilung, und die Buchung steht danach ohne Zuordnung da. * * @param array $params */ public function splitbuchung(array $params): void { $this->verlangeAnmeldung(); $buchung = Buchung::find((int)$params['id']); if ($buchung === null) { $this->nichtGefunden('/buchungen/' . $params['id']); return; } $ziel = $this->kontoblattPfad((int)$buchung['konto_id']); $loeschen = $this->post('aktion') === 'loeschen'; try { Buchung::splitsErsetzen((int)$buchung['id'], $loeschen ? [] : $this->splitzeilen()); } catch (\Throwable $e) { View::redirect($ziel, $e->getMessage(), 'fehler'); return; } View::redirect($ziel, $loeschen ? 'Splitbuchung gelöscht.' : 'Splitbuchung gespeichert.'); } /** * Mehrere im Kontoblatt angekreuzte Buchungen auf einmal löschen. * * @param array $params */ public function auswahlLoeschen(array $params): void { $this->verlangeAnmeldung(); $kontoId = (int)$params['id']; if (Konto::find($kontoId) === null) { $this->nichtGefunden('/konten/' . $params['id']); return; } $ziel = $this->kontoblattPfad($kontoId); $ids = $this->postIds('buchungen'); if ($ids === []) { View::redirect($ziel, 'Keine Buchung ausgewählt.', 'warnung'); return; } $anzahl = 0; foreach ($ids as $id) { $buchung = Buchung::find($id); // Nur eigene Buchungen dieses Kontos: Die Gegenseite einer fremden // Umbuchung gehört dem Ursprungskonto und wird auch nur dort // gelöscht. if ($buchung === null || (int)$buchung['konto_id'] !== $kontoId) { continue; } try { Buchung::loeschen($id); } catch (\Throwable $e) { View::redirect($ziel, $e->getMessage(), 'fehler'); return; } $anzahl++; } View::redirect($ziel, $anzahl === 1 ? 'Buchung gelöscht.' : $anzahl . ' Buchungen gelöscht.'); } /** * Weg zurück ins Kontoblatt – mit demselben Filter und derselben Seite. * * Die Werte kommen aus der Abfragezeichenfolge des Formularziels und werden * hier neu zusammengesetzt statt übernommen: So kann kein fremdes Ziel * eingeschleust werden. */ private function kontoblattPfad(int $kontoId): string { $query = array_filter([ 'suche' => $this->query('suche'), 'von' => $this->query('von'), 'bis' => $this->query('bis'), 'kategorie_id' => $this->query('kategorie_id'), 'klasse_id' => $this->query('klasse_id'), 'nur_offene' => $this->query('nur_offene') === '1' ? '1' : '', 'seite' => $this->query('seite'), ], static fn(string $wert): bool => $wert !== ''); return '/konten/' . $kontoId . ($query === [] ? '' : '?' . http_build_query($query)); } /** @param array|null $konto */ private function maske(?array $konto, ?array $buchung): void { if ($konto === null) { $this->nichtGefunden('/konten'); return; } $this->render('buchungen/formular.php', [ 'titel' => $buchung === null ? 'Neue Buchung' : 'Buchung bearbeiten', 'konto' => $konto, 'buchung' => $buchung, 'splits' => $buchung === null ? [] : Buchung::splits((int)$buchung['id']), 'kategorien' => Kategorie::baumFlach(), 'klassen' => Klasse::alle(), 'konten' => Konto::alle(), ]); } /** * @return array{0: array, 1: list>} */ private function formulardaten(int $kontoId): array { // In der Maske wird ein positiver Betrag eingegeben; die Richtung // bestimmt die Auswahl Einnahme / Ausgabe. $betrag = abs(KontenController::zahl($this->post('betrag', '0'))); if ($this->post('art', 'ausgabe') !== 'einnahme') { $betrag = -$betrag; } // Kategorie und Gegenkonto stehen in einem Feld: Ein Konto in der Liste // macht die Buchung zur Umbuchung – wie das Kategoriefeld im // FinanzManager, das `[Konto]` annimmt. $zuordnung = self::zuordnungAus($this->post('zuordnung')); $daten = [ 'konto_id' => $kontoId, 'datum' => $this->post('datum'), 'valuta' => $this->post('valuta'), 'betrag' => $betrag, 'empfaenger' => $this->post('empfaenger'), 'buchungstext' => $this->post('buchungstext'), 'nummer' => $this->post('nummer'), 'abgeglichen' => isset($_POST['abgeglichen']), 'kategorie_id' => $zuordnung['kategorie_id'], 'klasse_id' => $this->post('klasse_id'), 'gegenkonto_id' => $zuordnung['gegenkonto_id'], 'quelle' => 'manuell', ]; return [$daten, $this->splitzeilen()]; } /** * Die ausgefüllten Splitzeilen eines Formulars. Zeilen ohne Betrag zählen * nicht – so bleiben die leeren Zeilen der Maske folgenlos. * * @return list> */ private function splitzeilen(): array { $zeilen = $_POST['split'] ?? []; if (!is_array($zeilen)) { return []; } $splits = []; foreach ($zeilen as $zeile) { if (!is_array($zeile)) { continue; } $wert = trim((string)($zeile['betrag'] ?? '')); if ($wert === '') { continue; } $zuordnung = self::zuordnungAus((string)($zeile['zuordnung'] ?? '')); $splits[] = [ 'kategorie_id' => $zuordnung['kategorie_id'], 'gegenkonto_id' => $zuordnung['gegenkonto_id'], 'klasse_id' => $zeile['klasse_id'] ?? null, 'betrag' => KontenController::zahl($wert), 'notiz' => (string)($zeile['notiz'] ?? ''), ]; } return $splits; } /** * Ein Auswahlwert aus der gemeinsamen Liste „Kategorie oder Gegenkonto“: * `kategorie:12`, `konto:7` oder leer. * * @return array{kategorie_id: ?int, gegenkonto_id: ?int} */ public static function zuordnungAus(string $wert): array { $leer = ['kategorie_id' => null, 'gegenkonto_id' => null]; [$art, $id] = array_pad(explode(':', $wert, 2), 2, ''); if ((int)$id <= 0) { return $leer; } return match ($art) { 'kategorie' => ['kategorie_id' => (int)$id, 'gegenkonto_id' => null], 'konto' => ['kategorie_id' => null, 'gegenkonto_id' => (int)$id], default => $leer, }; } private function nichtGefunden(string $pfad): void { http_response_code(404); $this->render('layout/404.php', ['pfad' => $pfad, 'titel' => 'Nicht gefunden']); } }