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>
304 lines
10 KiB
PHP
304 lines
10 KiB
PHP
<?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;
|
||
}
|
||
}
|