Eine falsche Splitsumme zeigte zwar die Fehlermeldung, leitete aber auf eine frische Buchungsmaske weiter und riss dabei alle gerade erfassten Splitzeilen (und übrigen Feldwerte) heraus, obwohl in der Datenbank nichts geändert wurde. anlegen()/speichern() zeichnen die Maske bei einem Validierungsfehler jetzt direkt mit den zuletzt abgeschickten Werten neu, statt wegzuleiten. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
179 lines
6.3 KiB
PHP
179 lines
6.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Finanzmanager;
|
||
|
||
class View
|
||
{
|
||
private string $viewDir;
|
||
private array $flash = [];
|
||
|
||
public function __construct()
|
||
{
|
||
$this->viewDir = __DIR__ . '/Views';
|
||
|
||
Auth::startSession();
|
||
if (isset($_SESSION['flash'])) {
|
||
$this->flash = $_SESSION['flash'];
|
||
unset($_SESSION['flash']);
|
||
}
|
||
}
|
||
|
||
public function render(string $template, array $daten = []): void
|
||
{
|
||
// Eine Meldung, die der Aufrufer selbst mitgibt (statt über die Session),
|
||
// zeigt sich zusätzlich zu vorgemerkten – etwa ein Validierungsfehler, der
|
||
// die Maske ohne Weiterleitung neu zeichnet und dabei die Eingaben behält.
|
||
$daten['flash'] = array_merge($this->flash, $daten['flash'] ?? []);
|
||
$daten['aktuellerPfad'] = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||
|
||
extract($daten, EXTR_SKIP);
|
||
include $this->viewDir . '/' . $template;
|
||
}
|
||
|
||
public static function redirect(string $pfad, string $meldung = '', string $typ = 'erfolg'): void
|
||
{
|
||
self::flash($meldung, $typ);
|
||
header('Location: ' . $pfad);
|
||
exit;
|
||
}
|
||
|
||
/** Eine Meldung für die nächste Seite vormerken – es dürfen mehrere sein. */
|
||
public static function flash(string $meldung, string $typ = 'erfolg'): void
|
||
{
|
||
Auth::startSession();
|
||
if ($meldung !== '') {
|
||
$_SESSION['flash'][] = ['typ' => $typ, 'meldung' => $meldung];
|
||
}
|
||
}
|
||
|
||
public static function h(mixed $wert): string
|
||
{
|
||
return htmlspecialchars((string)$wert, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||
}
|
||
|
||
/**
|
||
* Beträge im deutschen Format. Das Leerzeichen vor dem Euro-Zeichen ist
|
||
* geschützt, sonst rutscht das Zeichen in schmalen Tabellenspalten allein
|
||
* in die nächste Zeile.
|
||
*/
|
||
public static function betrag(mixed $wert, bool $mitWaehrung = true): string
|
||
{
|
||
$text = number_format((float)$wert, 2, ',', '.');
|
||
return $mitWaehrung ? $text . "\u{00A0}€" : $text;
|
||
}
|
||
|
||
/**
|
||
* Stückzahlen. Fonds werden in Bruchteilen geführt, Aktien in ganzen
|
||
* Stücken – dieselbe Spalte trägt beides, deshalb sechs Nachkommastellen
|
||
* ohne die Nullen, die niemand liest: „12“ statt „12,000000“.
|
||
*/
|
||
public static function stueck(mixed $wert): string
|
||
{
|
||
return rtrim(rtrim(number_format((float)$wert, 6, ',', '.'), '0'), ',');
|
||
}
|
||
|
||
/**
|
||
* Monatsnamen auf Deutsch. Nicht über date('F') oder IntlDateFormatter –
|
||
* date() ist immer englisch und intl ist keine Standarderweiterung. Der
|
||
* Budgetplaner und die Auswertungen greifen später ebenfalls hierauf zu.
|
||
*/
|
||
public const MONATE = [
|
||
1 => 'Januar', 2 => 'Februar', 3 => 'März', 4 => 'April',
|
||
5 => 'Mai', 6 => 'Juni', 7 => 'Juli', 8 => 'August',
|
||
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember',
|
||
];
|
||
|
||
/** Kurzform für schmale Spalten (zwölf Monatsspalten im Budgetplaner). */
|
||
public const MONATE_KURZ = [
|
||
1 => 'Jan', 2 => 'Feb', 3 => 'Mär', 4 => 'Apr',
|
||
5 => 'Mai', 6 => 'Jun', 7 => 'Jul', 8 => 'Aug',
|
||
9 => 'Sep', 10 => 'Okt', 11 => 'Nov', 12 => 'Dez',
|
||
];
|
||
|
||
public static function monat(int $monat, bool $kurz = false): string
|
||
{
|
||
$namen = $kurz ? self::MONATE_KURZ : self::MONATE;
|
||
return $namen[$monat] ?? (string)$monat;
|
||
}
|
||
|
||
/** Wochentagskürzel, Montag zuerst – wie MONATE nicht über date() oder intl. */
|
||
public const WOCHENTAGE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||
|
||
public static function wochentag(?string $datum): string
|
||
{
|
||
if (!$datum) {
|
||
return '';
|
||
}
|
||
|
||
$zeit = strtotime(substr($datum, 0, 10));
|
||
if ($zeit === false) {
|
||
return '';
|
||
}
|
||
|
||
// date('N') zählt Montag als 1.
|
||
return self::WOCHENTAGE[(int)date('N', $zeit) - 1] ?? '';
|
||
}
|
||
|
||
public static function datum(?string $datum): string
|
||
{
|
||
if (!$datum) {
|
||
return '–';
|
||
}
|
||
$d = \DateTime::createFromFormat('Y-m-d', substr($datum, 0, 10));
|
||
return $d ? $d->format('d.m.Y') : $datum;
|
||
}
|
||
|
||
/** CSS-Klasse für Beträge: negative Werte werden rot dargestellt. */
|
||
public static function betragKlasse(mixed $wert): string
|
||
{
|
||
return (float)$wert < 0 ? 'betrag negativ' : 'betrag';
|
||
}
|
||
|
||
/**
|
||
* Das Symbol der Abgleichspalte im Kontoblatt. Vier Zustände, in dieser
|
||
* Reihenfolge zu lesen: gefunden, nicht gefunden, Abruf gescheitert – und
|
||
* der graue Kreis für alles, was mit dem Bargeld-Tool nichts zu tun hat.
|
||
*
|
||
* Die Zeichen stehen als Text und nicht als Grafik da: Sie erben damit
|
||
* Schriftgröße und – beim Ausrufezeichen ausdrücklich gewünscht – die
|
||
* Farbe des übrigen Textes, in jedem Farbschema.
|
||
*
|
||
* Eigenständige Methode statt einer Schleife im Kontoblatt, weil auch die
|
||
* Schnellzuordnung (kontoblatt.js) dasselbe Symbol braucht, ohne die Seite
|
||
* neu zu laden.
|
||
*/
|
||
public static function bargeldMarke(?string $status, ?string $hinweis): string
|
||
{
|
||
$hinweis = trim((string)$hinweis);
|
||
|
||
[$zeichen, $klasse, $standardtitel] = match ($status) {
|
||
'gefunden' => ['✓', 'gefunden', 'Im Bargeld-Tool gefunden'],
|
||
'fehlt' => ['✗', 'fehlt', 'Im Bargeld-Tool nicht gefunden'],
|
||
'fehler' => ['!', 'fehler', 'Der Abruf beim Bargeld-Tool ist gescheitert'],
|
||
default => ['●', 'neutral', 'Wird nicht mit dem Bargeld-Tool abgeglichen'],
|
||
};
|
||
|
||
return '<span class="bargeldmarke bargeld-' . $klasse . '" title="'
|
||
. self::h($hinweis !== '' ? $hinweis : $standardtitel) . '" role="img" aria-label="'
|
||
. self::h($standardtitel) . '">' . $zeichen . '</span>';
|
||
}
|
||
|
||
/**
|
||
* Farbschema für das data-thema-Attribut: 'hell', 'dunkel' oder '' für die
|
||
* Systemvorgabe. Fällt auf die Systemvorgabe zurück, wenn die Einstellung
|
||
* nicht lesbar ist – die Fehlerseite muss auch ohne Datenbank darstellbar
|
||
* bleiben.
|
||
*/
|
||
public static function thema(): string
|
||
{
|
||
try {
|
||
$thema = \Finanzmanager\Models\Einstellung::get('darstellung_thema', 'system');
|
||
} catch (\Throwable) {
|
||
return '';
|
||
}
|
||
|
||
return in_array($thema, ['hell', 'dunkel'], true) ? $thema : '';
|
||
}
|
||
}
|