Finanzmanager/src/Controllers/AuswertungenController.php
fds2610-bot 233b570e76 Fix #84: Cockpit-Legendeneinträge öffnen die Buchungen dahinter
Jeder Legendeneintrag der Cockpit-Kacheln führt zu den Buchungen, aus denen
sich sein Wert zusammensetzt: Kategorien, „Nicht kategorisiert“ und „Übrige“
in „Meine Ausgaben“, Einnahmen/Ausgaben/Saldo in „Einnahmen und Ausgaben“,
Konten der Vermögensentwicklung ins Kontoblatt. Ziel ist der Drilldown der
Auswertungen mit dem Filter der Kachel; zeilenbuchungen() kennt dafür einen
Art-Filter, „alle Bewegungen“ und mehrere Kategorien.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:27:32 +02:00

304 lines
10 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
declare(strict_types=1);
namespace Finanzmanager\Controllers;
use Finanzmanager\Models\Bericht;
use Finanzmanager\Models\Berichtsgruppe;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Klasse;
use Finanzmanager\Models\Konto;
use Finanzmanager\Services\Auswertung;
use Finanzmanager\Services\BerichtExport;
use Finanzmanager\Services\Zeitraum;
use Finanzmanager\View;
/**
* Der Bereich *Auswertungen*: die Übersicht mit Berichtsgruppen und
* Standardvorlagen, die Berichtsansicht mit ihrer Filterleiste, der Drilldown
* in die Buchungen und der CSV-Export.
*
* Ein Bericht wird nie gerechnet gespeichert, sondern immer nur seine Frage.
* Die Filterleiste schickt ihre Felder deshalb per GET: Jeder Zwischenstand ist
* eine Adresse, die man sich merken, weitergeben und mit „Speichern unter“
* festhalten kann.
*/
class AuswertungenController extends BaseController
{
public function index(): void
{
$this->verlangeAnmeldung();
$this->render('auswertungen/index.php', [
'titel' => 'Auswertungen',
'gruppen' => Bericht::nachGruppe(),
'letzte' => Bericht::letzte(),
'typen' => Auswertung::TYPEN,
'anzahl' => Bericht::anzahl(),
]);
}
/** Ein Bericht aus einer Standardvorlage – die Filter stehen in der Adresse. */
public function bericht(): void
{
$this->verlangeAnmeldung();
$this->zeige(null, $this->filterAusQuery());
}
/** Ein gespeicherter Bericht. */
public function gespeichert(array $params): void
{
$this->verlangeAnmeldung();
$gespeichert = Bericht::find((int)$params['id']);
if ($gespeichert === null) {
View::redirect('/auswertungen', 'Diesen Bericht gibt es nicht.', 'fehler');
}
Bericht::benutzt((int)$gespeichert['id']);
$this->zeige($gespeichert, $this->filterFuer($gespeichert));
}
/**
* „Speichern“ überschreibt den geöffneten Bericht, „Speichern unter“ legt
* unter neuem Namen an. Beides landet hier – der Unterschied steckt allein
* darin, ob eine Id mitkommt.
*/
public function speichern(): void
{
$this->verlangeAnmeldung();
$filter = Auswertung::normalisiere($_POST);
$name = $this->post('name');
$gruppe = (int)$this->post('gruppe_id');
$id = (int)$this->post('bericht_id');
// Eine neue Gruppe darf direkt im Speichern-Dialog entstehen; wer sie
// erst anlegen müsste, verlöre den eingestellten Filter.
$neueGruppe = $this->post('neue_gruppe');
if ($neueGruppe !== '') {
try {
$gruppe = Berichtsgruppe::findenOderAnlegen($neueGruppe);
} catch (\Throwable $e) {
View::redirect('/auswertungen', $e->getMessage(), 'fehler');
}
}
try {
if ($id > 0 && Bericht::find($id) !== null) {
Bericht::speichern($id, $name, $filter, $gruppe);
} else {
$id = Bericht::anlegen($name, $filter, $gruppe);
}
} catch (\Throwable $e) {
View::redirect(
'/auswertungen/bericht?' . $this->alsQuery($filter),
$e->getMessage(),
'fehler'
);
}
View::redirect('/auswertungen/bericht/' . $id, 'Bericht „' . $name . '“ gespeichert.');
}
public function loeschen(array $params): void
{
$this->verlangeAnmeldung();
$bericht = Bericht::find((int)$params['id']);
if ($bericht === null) {
View::redirect('/auswertungen', 'Diesen Bericht gibt es nicht.', 'fehler');
}
Bericht::loeschen((int)$bericht['id']);
View::redirect('/auswertungen', 'Bericht „' . $bericht['name'] . '“ gelöscht.');
}
/** Die Buchungen hinter einer Berichtszeile. */
public function drilldown(): void
{
$this->verlangeAnmeldung();
$filter = $this->filterAusQuery();
$zeile = Auswertung::zeilenbuchungen(
$filter,
$this->query('zeile'),
$this->query('spalte'),
$this->query('ast') === '1',
null,
$this->query('art')
);
// Aus einer Cockpit-Kachel geht es zurück ins Cockpit, nicht in einen
// Bericht, den der Nutzer nie geöffnet hat.
$ausCockpit = $this->query('herkunft') === 'cockpit';
// Nicht „daten“ nennen: View::render() extrahiert mit EXTR_SKIP, und
// eine gleichnamige lokale Variable dort bliebe unangetastet – die View
// bekäme dann das gesamte Datenarray statt dieses einen Eintrags.
$this->render('auswertungen/drilldown.php', [
'titel' => $zeile['name'],
'zeile' => $zeile,
'zurueck' => $ausCockpit ? '/' : $this->berichtsadresse($filter, $this->query('bericht_id')),
'ausCockpit' => $ausCockpit,
]);
}
/** Derselbe Bericht als CSV-Datei. */
public function csv(): void
{
$this->verlangeAnmeldung();
$filter = $this->filterAusQuery();
$bericht = Auswertung::bericht($filter);
$id = (int)$this->query('bericht_id');
if ($id > 0) {
$gespeichert = Bericht::find($id);
if ($gespeichert !== null) {
$bericht['name'] = (string)$gespeichert['name'];
}
}
$inhalt = BerichtExport::csv($bericht);
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . BerichtExport::dateiname($bericht) . '"');
header('Content-Length: ' . strlen($inhalt));
echo $inhalt;
}
// ── Berichtsgruppen ──────────────────────────────────────────────────────
public function gruppen(): void
{
$this->verlangeAnmeldung();
$this->render('auswertungen/gruppen.php', [
'titel' => 'Berichtsgruppen',
'gruppen' => Bericht::nachGruppe(),
]);
}
public function gruppeAnlegen(): void
{
$this->verlangeAnmeldung();
try {
Berichtsgruppe::anlegen($this->post('name'), (int)$this->post('sortierung'));
} catch (\Throwable $e) {
View::redirect('/auswertungen/gruppen', $e->getMessage(), 'fehler');
}
View::redirect('/auswertungen/gruppen', 'Berichtsgruppe angelegt.');
}
public function gruppeSpeichern(array $params): void
{
$this->verlangeAnmeldung();
try {
Berichtsgruppe::speichern((int)$params['id'], $this->post('name'), (int)$this->post('sortierung'));
} catch (\Throwable $e) {
View::redirect('/auswertungen/gruppen', $e->getMessage(), 'fehler');
}
View::redirect('/auswertungen/gruppen', 'Berichtsgruppe gespeichert.');
}
public function gruppeLoeschen(array $params): void
{
$this->verlangeAnmeldung();
Berichtsgruppe::loeschen((int)$params['id']);
View::redirect('/auswertungen/gruppen', 'Berichtsgruppe gelöscht; ihre Berichte bleiben erhalten.');
}
// ── Innereien ────────────────────────────────────────────────────────────
/**
* Stellt die Berichtsansicht dar.
*
* @param array<string,mixed>|null $gespeichert
* @param array<string,mixed> $filter
*/
private function zeige(?array $gespeichert, array $filter): void
{
$bericht = Auswertung::bericht($filter);
if ($gespeichert !== null) {
$bericht['name'] = (string)$gespeichert['name'];
}
$this->render('auswertungen/bericht.php', [
'titel' => ($gespeichert['name'] ?? $bericht['typname'])
. ($gespeichert === null ? '' : ' – ' . $bericht['typname']),
'bericht' => $bericht,
'gespeichert' => $gespeichert,
'gruppen' => Berichtsgruppe::alle(),
'konten' => Konto::alle(),
'kategorien' => Kategorie::baumFlach(),
'klassen' => Klasse::alle(),
'vorgaben' => Zeitraum::VORGABEN,
'aufteilungen' => Zeitraum::AUFTEILUNGEN,
'zeilensummen' => Auswertung::ZEILENSUMMEN,
'typen' => Auswertung::TYPEN,
'query' => $this->alsQuery($bericht['filter']),
]);
}
/**
* Der Filter für einen gespeicherten Bericht: aus der Datenbank, sofern die
* Filterleiste nicht gerade etwas anderes gesendet hat. Das Formular setzt
* dazu ein Feld `angepasst` – ohne dieses Kennzeichen wäre nicht zu
* unterscheiden, ob ein fehlender Parameter „nicht gesendet“ oder
* „abgewählt“ bedeutet.
*
* @param array<string,mixed> $gespeichert
* @return array<string,mixed>
*/
private function filterFuer(array $gespeichert): array
{
return $this->query('angepasst') === '1'
? $this->filterAusQuery()
: Bericht::filter($gespeichert);
}
/** @return array<string,mixed> */
private function filterAusQuery(): array
{
return Auswertung::normalisiere($_GET);
}
/**
* Der Filter als Query-String – für den CSV-Link, den Drilldown und den Weg
* zurück.
*
* @param array<string,mixed> $filter
*/
private function alsQuery(array $filter): string
{
return http_build_query(array_map(
static fn(mixed $wert): mixed => is_bool($wert) ? ($wert ? '1' : '0') : $wert,
$filter + ['angepasst' => '1']
));
}
/**
* Zurück zum Bericht: zum gespeicherten, wenn es einen gibt, sonst zur
* Vorlage mit denselben Filtern.
*
* @param array<string,mixed> $filter
*/
private function berichtsadresse(array $filter, string $id): string
{
$query = $this->alsQuery($filter);
return (int)$id > 0
? '/auswertungen/bericht/' . (int)$id . '?' . $query
: '/auswertungen/bericht?' . $query;
}
}