158 lines
5.3 KiB
PHP
158 lines
5.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Steuerkram;
|
||
|
||
use Steuerkram\Auth;
|
||
use Steuerkram\Models\EventLog;
|
||
|
||
class View
|
||
{
|
||
private string $viewDir;
|
||
private array $flash = [];
|
||
|
||
public function __construct()
|
||
{
|
||
$this->viewDir = dirname(__DIR__) . '/src/Views';
|
||
|
||
// Session-based flash messages
|
||
Auth::startSession();
|
||
if (isset($_SESSION['flash'])) {
|
||
$this->flash = $_SESSION['flash'];
|
||
unset($_SESSION['flash']);
|
||
}
|
||
}
|
||
|
||
public function render(string $template, array $data = []): void
|
||
{
|
||
$data['flash'] = $this->flash;
|
||
$data['currentPath'] = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||
$data['h'] ??= fn($v) => self::h($v);
|
||
$data['fmt'] ??= fn($v) => self::formatMoney($v);
|
||
|
||
extract($data, EXTR_SKIP);
|
||
include $this->viewDir . '/' . $template;
|
||
}
|
||
|
||
public static function redirect(string $path, string $message = '', string $type = 'success'): void
|
||
{
|
||
Auth::startSession();
|
||
if ($message !== '') {
|
||
$_SESSION['flash'][] = ['type' => $type, 'message' => $message];
|
||
self::logFlash($type, $message);
|
||
}
|
||
header('Location: ' . $path);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Fehler- und Warnhinweise, die dem Nutzer angezeigt werden, wandern
|
||
* zusätzlich in die Ereignisliste – dort sind sie auch dann noch
|
||
* nachvollziehbar, wenn die Meldung längst weggeklickt ist (Issue #165).
|
||
*/
|
||
private static function logFlash(string $type, string $message): void
|
||
{
|
||
$level = match ($type) {
|
||
'error' => EventLog::LEVEL_ERROR,
|
||
'warning' => EventLog::LEVEL_WARNING,
|
||
default => null,
|
||
};
|
||
|
||
if ($level === null) {
|
||
return;
|
||
}
|
||
|
||
$pfad = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
|
||
EventLog::tryAdd(
|
||
'meldungen',
|
||
$pfad ? "{$message} ({$pfad})" : $message,
|
||
$level
|
||
);
|
||
}
|
||
|
||
public static function h(mixed $value): string
|
||
{
|
||
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||
}
|
||
|
||
public static function formatMoney(mixed $value): string
|
||
{
|
||
// Geschütztes Leerzeichen: in schmalen Tabellenspalten rutscht das
|
||
// Währungszeichen sonst allein in die nächste Zeile.
|
||
return number_format((float)$value, 2, ',', '.') . "\u{00A0}€";
|
||
}
|
||
|
||
public static function formatDate(?string $date): string
|
||
{
|
||
if (!$date) return '–';
|
||
$d = \DateTime::createFromFormat('Y-m-d', $date);
|
||
return $d ? $d->format('d.m.Y') : $date;
|
||
}
|
||
|
||
/**
|
||
* Baut den Wert des Content-Disposition-Headers für einen (ggf. vom Nutzer
|
||
* beim Upload frei wählbaren) Dateinamen. PATHINFO_BASENAME entfernt zwar
|
||
* Verzeichnisanteile, nicht aber Anführungszeichen/Backslashes – ohne
|
||
* Maskierung könnte ein präparierter Dateiname aus dem filename="..."-Parameter
|
||
* ausbrechen und einen zusätzlichen filename*=-Parameter einschleusen, der von
|
||
* vielen Browsern bevorzugt ausgewertet wird (Issue #217).
|
||
*/
|
||
public static function contentDispositionHeader(string $disposition, string $filename): string
|
||
{
|
||
$safe = (string)preg_replace('/[\x00-\x1f"\\\\]/', '_', $filename);
|
||
return $disposition . '; filename="' . $safe . '"; filename*=UTF-8\'\'' . rawurlencode($filename);
|
||
}
|
||
|
||
public static function buildQuery(array $params): string
|
||
{
|
||
$safe = [];
|
||
foreach ($params as $k => $v) {
|
||
$safe[rawurlencode((string)$k)] = rawurlencode((string)$v);
|
||
}
|
||
return http_build_query($safe);
|
||
}
|
||
|
||
/**
|
||
* Lesbare Textfarbe (dunkel/hell) für einen Hex-Hintergrund ermitteln.
|
||
* Ungültige oder zu kurze Werte liefern die Standardfarbe statt einer
|
||
* stillen Rechnung auf 0 (siehe Issue #158).
|
||
*/
|
||
public static function kontrastfarbe(string $hex, string $dunkel = '#1e293b', string $hell = '#ffffff'): string
|
||
{
|
||
if (preg_match('/^#([0-9a-fA-F]{3})$/', $hex, $m)) {
|
||
$hex = '#' . $m[1][0] . $m[1][0] . $m[1][1] . $m[1][1] . $m[1][2] . $m[1][2];
|
||
}
|
||
|
||
if (!preg_match('/^#[0-9a-fA-F]{6}$/', $hex)) {
|
||
return $dunkel;
|
||
}
|
||
|
||
$helligkeit = 0.299*hexdec(substr($hex,1,2)) + 0.587*hexdec(substr($hex,3,2)) + 0.114*hexdec(substr($hex,5,2));
|
||
|
||
return $helligkeit > 128 ? $dunkel : $hell;
|
||
}
|
||
|
||
/**
|
||
* Konten nach kontenart gruppieren (für <optgroup>).
|
||
* Bei $nurZahlungskonten1200 werden Zahlungskonten außerhalb 1200–1299
|
||
* (Kasse, Verrechnungskonto, USt-Konten) ausgeschlossen – das ist die
|
||
* Konvention für Buchungsformulare (siehe BuchungsvorlageController,
|
||
* belege/_buchungsform.php): dort sind nur reine Bankkonten als
|
||
* Zahlungskonto wählbar.
|
||
*/
|
||
public static function gruppiereKonten(array $accounts, bool $nurZahlungskonten1200 = false): array
|
||
{
|
||
$gruppen = [];
|
||
foreach ($accounts as $acc) {
|
||
if ($nurZahlungskonten1200 && $acc['kontenart'] === 'Zahlungskonto') {
|
||
$nr = (int)$acc['kontonummer'];
|
||
if ($nr < 1200 || $nr > 1299) {
|
||
continue;
|
||
}
|
||
}
|
||
$gruppen[$acc['kontenart']][] = $acc;
|
||
}
|
||
return $gruppen;
|
||
}
|
||
}
|