Damit ist der geplante Funktionsumfang beisammen. Phase 4 abgeschlossen - Konfigurierbare Cockpit-Kacheln; abgewählte werden nicht berechnet - Einnahmen/Ausgaben je Monat als Balkenpaare mit Saldolinie, aufgesetzt auf den Summenbericht statt auf eine eigene Abfrage - Meine Ausgaben als Ring auf der obersten Kategorieebene - Offene Vorgänge aus allen Modulen Phase 5 – Online-Banking - nemiah/php-fints als Fork auf Codeberg, psr/log direkt eingebaut: die Anwendung hängt an keinem fremden Update-Zyklus - PIN AES-256-GCM-verschlüsselt; Schlüssel aus APP_KEY oder aus data/schluessel/app_key, den der Entrypoint einmalig erzeugt - TAN-Dialog über mehrere Requests (FinTs::persist plus serialisierte Aktion in der Sitzung) – ein wartender Prozess wäre mit php-fpm der falsche Weg - Abgleich statt Direktbuchung: verbinden, übernehmen oder verwerfen - Regeln mit vier Vergleichsarten, Betragsgrenzen und Split-Schema Phase 6 – Wertpapiere - Depotbuchung und Geldbuchung als zwei Seiten eines Geschäfts; betrag ist der Bruttowert, Nebenkosten stehen daneben - Einstand nach gleitendem Durchschnitt, realisierte Gewinne, Erträge - Kursabruf über Leeway oder FMP, täglicher Lauf per Cron - QIF-Import für !Type:Invst Dabei behoben: Der Import buchte den Geldfluss jedes Wertpapiergeschäfts als Abgang – auch bei Verkäufen und Dividenden; und er erkannte das Verrechnungskonto nicht, wenn dem Konto im L-Feld eine Kategorie vorangeht (bei Verkäufen also immer). Phase 7 – Nebenmodule und Zahlungsverkehr - Buchungsvorlagen inklusive QIF-Import, Splits werden mitskaliert - Regelmäßige Buchungen mit Fälligkeitslauf, Finanzkalender, Erinnerungen - Adressen mit IBAN-Prüfung, Verträge mit Kündigungsfrist, Sparziele - Kredite mit gerechnetem Tilgungsplan - Überweisungen über FinTS; die Buchung entsteht erst nach Annahme durch die Bank. Die pain.001-Nachricht erzeugt SepaXml selbst Nebenbei: .dockerignore ergänzt (ohne sie brach der Build an data/mariadb ab), PlatzhalterController entfernt, zwei Inline-Handler beseitigt, die die Content-Security-Policy ohnehin blockierte. 888 PHP-Prüfungen und 45 E2E-Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
5.9 KiB
PHP
138 lines
5.9 KiB
PHP
<?php
|
||
|
||
use Finanzmanager\Services\Vermoegensentwicklung;
|
||
use Finanzmanager\View;
|
||
|
||
/**
|
||
* Einnahmen und Ausgaben je Monat als Balkenpaar, darüber der Saldo als Linie.
|
||
*
|
||
* Aufbau wie beim Vermögensdiagramm: Achsenbeschriftung als festes SVG links,
|
||
* Zeichenfläche daneben mit waagerechten Prozentpositionen – so rücken die
|
||
* Balken in einer breiten Kachel auseinander, statt breiter zu werden.
|
||
*
|
||
* @var array{
|
||
* monate: list<array{schluessel:string, kurz:string, titel:string}>,
|
||
* einnahmen: list<float>,
|
||
* ausgaben: list<float>,
|
||
* salden: list<float>,
|
||
* summe_einnahmen: float,
|
||
* summe_ausgaben: float,
|
||
* beschriftung: string,
|
||
* hatWerte: bool
|
||
* } $reihe
|
||
*/
|
||
|
||
$monate = $reihe['monate'];
|
||
$anzahl = max(1, count($monate));
|
||
|
||
$skala = Vermoegensentwicklung::skala(
|
||
array_merge($reihe['einnahmen'], $reihe['ausgaben'], $reihe['salden'], [0.0])
|
||
);
|
||
|
||
$balken = 8;
|
||
$luecke = 2;
|
||
$achse = 56;
|
||
$oben = 10;
|
||
$plotH = 150;
|
||
$unten = 32;
|
||
$hoehe = $oben + $plotH + $unten;
|
||
|
||
$y = static function (float $wert) use ($skala, $oben, $plotH): float {
|
||
return round($oben + $plotH * ($skala['max'] - $wert) / ($skala['max'] - $skala['min']), 2);
|
||
};
|
||
|
||
$mitte = static function (int $nummer) use ($anzahl): string {
|
||
return round(($nummer + 0.5) / $anzahl * 100, 3) . '%';
|
||
};
|
||
|
||
$achsenzahl = static fn(float $wert): string => number_format($wert, 0, ',', '.');
|
||
|
||
$nulllinie = $y(0.0);
|
||
|
||
/** Ein Balken von der Nulllinie bis zum Wert – y und Höhe in einem. */
|
||
$saeule = static function (float $wert) use ($y, $nulllinie): array {
|
||
$spitze = $y($wert);
|
||
return ['y' => min($spitze, $nulllinie), 'h' => round(abs($nulllinie - $spitze), 2)];
|
||
};
|
||
?>
|
||
|
||
<div class="diagrammbereich">
|
||
<div class="diagrammflaeche">
|
||
<svg class="diagrammachse" width="<?= $achse ?>" height="<?= $hoehe ?>" aria-hidden="true">
|
||
<?php for ($wert = $skala['min']; $wert <= $skala['max'] + 0.001; $wert += $skala['schritt']): ?>
|
||
<text class="diagramm-achse" x="<?= $achse - 8 ?>" y="<?= $y($wert) + 4 ?>"
|
||
text-anchor="end"><?= View::h($achsenzahl($wert)) ?></text>
|
||
<?php endfor; ?>
|
||
</svg>
|
||
|
||
<svg class="diagramm" height="<?= $hoehe ?>" role="img"
|
||
aria-label="Einnahmen und Ausgaben je Monat, <?= View::h($reihe['beschriftung']) ?>">
|
||
<title>Einnahmen und Ausgaben <?= View::h($reihe['beschriftung']) ?></title>
|
||
|
||
<?php for ($wert = $skala['min']; $wert <= $skala['max'] + 0.001; $wert += $skala['schritt']): ?>
|
||
<line class="diagramm-gitter" x1="0" x2="100%"
|
||
y1="<?= $y($wert) ?>" y2="<?= $y($wert) ?>"></line>
|
||
<?php endfor; ?>
|
||
|
||
<line class="diagramm-nulllinie" x1="0" x2="100%" y1="<?= $nulllinie ?>" y2="<?= $nulllinie ?>"></line>
|
||
|
||
<?php foreach ($monate as $nummer => $monat): ?>
|
||
<?php
|
||
$einnahme = $reihe['einnahmen'][$nummer] ?? 0.0;
|
||
$ausgabe = $reihe['ausgaben'][$nummer] ?? 0.0;
|
||
$e = $saeule($einnahme);
|
||
$a = $saeule($ausgabe);
|
||
?>
|
||
<?php /* Das Paar sitzt mittig über der Spalte: eine halbe Paarbreite
|
||
nach links, dann stehen beide Balken um die Mitte herum. */ ?>
|
||
<g transform="translate(-<?= $balken + $luecke / 2 ?> 0)">
|
||
<rect class="balken-einnahme" x="<?= $mitte($nummer) ?>" y="<?= $e['y'] ?>"
|
||
width="<?= $balken ?>" height="<?= $e['h'] ?>">
|
||
<title><?= View::h('Einnahmen ' . $monat['titel'] . ': ' . View::betrag($einnahme)) ?></title>
|
||
</rect>
|
||
</g>
|
||
<g transform="translate(<?= $luecke / 2 ?> 0)">
|
||
<rect class="balken-ausgabe" x="<?= $mitte($nummer) ?>" y="<?= $a['y'] ?>"
|
||
width="<?= $balken ?>" height="<?= $a['h'] ?>">
|
||
<title><?= View::h('Ausgaben ' . $monat['titel'] . ': ' . View::betrag($ausgabe)) ?></title>
|
||
</rect>
|
||
</g>
|
||
|
||
<text class="diagramm-beschriftung" x="<?= $mitte($nummer) ?>" y="<?= $hoehe - 18 ?>"
|
||
text-anchor="middle"><?= View::h(substr($monat['kurz'], 0, 3)) ?></text>
|
||
<?php if (str_ends_with($monat['schluessel'], '-01') || $nummer === 0): ?>
|
||
<text class="diagramm-beschriftung schwach" x="<?= $mitte($nummer) ?>" y="<?= $hoehe - 5 ?>"
|
||
text-anchor="middle"><?= View::h(substr($monat['schluessel'], 2, 2)) ?></text>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
|
||
<?php /* Der Saldo als Linie über den Balken: Ein dritter Balken machte
|
||
die Spalte eng, die Linie zeigt den Verlauf ohnehin besser. */ ?>
|
||
<?php foreach ($reihe['salden'] as $nummer => $saldo): ?>
|
||
<?php if ($nummer > 0): ?>
|
||
<line class="diagramm-saldo" x1="<?= $mitte($nummer - 1) ?>" y1="<?= $y($reihe['salden'][$nummer - 1]) ?>"
|
||
x2="<?= $mitte($nummer) ?>" y2="<?= $y($saldo) ?>"></line>
|
||
<?php endif; ?>
|
||
<circle class="diagramm-saldo-punkt" cx="<?= $mitte($nummer) ?>" cy="<?= $y($saldo) ?>" r="2.5">
|
||
<title><?= View::h('Saldo ' . $monate[$nummer]['titel'] . ': ' . View::betrag($saldo)) ?></title>
|
||
</circle>
|
||
<?php endforeach; ?>
|
||
</svg>
|
||
</div>
|
||
|
||
<ul class="diagrammlegende">
|
||
<li>
|
||
<span class="legendenfarbe balken-einnahme" aria-hidden="true"></span>
|
||
<span class="legendenname">Einnahmen <?= View::h(View::betrag($reihe['summe_einnahmen'])) ?></span>
|
||
</li>
|
||
<li>
|
||
<span class="legendenfarbe balken-ausgabe" aria-hidden="true"></span>
|
||
<span class="legendenname">Ausgaben <?= View::h(View::betrag($reihe['summe_ausgaben'])) ?></span>
|
||
</li>
|
||
<li>
|
||
<span class="legendenfarbe saldofarbe" aria-hidden="true"></span>
|
||
<span class="legendenname">Saldo
|
||
<?= View::h(View::betrag($reihe['summe_einnahmen'] - $reihe['summe_ausgaben'])) ?></span>
|
||
</li>
|
||
</ul>
|
||
</div>
|