Fünf Berichtstypen wie im FinanzManager – Summen-, Buchungs-, Vergleichs-, Kontostands- und Budgetbericht – mit Filterleiste, gespeicherten Berichten in Berichtsgruppen, Drilldown in die Buchungen und CSV-Export. Ein Bericht speichert nur seine Frage; die Zahlen entstehen bei jedem Aufruf neu aus den Buchungen. Gezählt werden Bewegungen (gesplittete Buchungen über ihre Splitzeilen), Umbuchungen bleiben außen vor, Unkategorisiertes bekommt eine eigene Zeile. Im Ausgabenabschnitt steht der Betrag ohne Minus wie im Original, die Schlusszeile rechnet mit den echten Vorzeichen. Einzelheiten in docs/auswertungen.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
297 lines
9.9 KiB
PHP
297 lines
9.9 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'
|
||
);
|
||
|
||
// 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' => $this->berichtsadresse($filter, $this->query('bericht_id')),
|
||
]);
|
||
}
|
||
|
||
/** 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;
|
||
}
|
||
}
|