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 ). * 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; } }