-. Any file updated on the * server gets a new URL, so clients never run a stale cached JS/CSS file. */ function asset(string $path): string { $file = __DIR__ . '/../' . $path; $v = app_version() . (is_file($file) ? '-' . base_convert((string)filemtime($file), 10, 36) : ''); return base_url($path) . '?v=' . rawurlencode($v); } /** Public pages: security headers + always revalidate (so the newest versioned assets load). */ function send_no_cache_headers(): void { send_security_headers(false); } const SESSION_COOKIE = 'serubi_admin'; function start_session(): void { if (session_status() === PHP_SESSION_ACTIVE) { return; } session_name(SESSION_COOKIE); session_set_cookie_params([ 'lifetime' => 0, 'path' => base_url(), 'httponly' => true, 'samesite' => 'Strict', 'secure' => is_https(), ]); session_start(); } function csrf_token(): string { start_session(); if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } function csrf_field(): string { return ''; } function csrf_check(): void { start_session(); $sent = $_POST['csrf'] ?? ''; if (!is_string($sent) || empty($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $sent)) { http_response_code(403); header('Cache-Control: no-store'); exit('Sesi formulir kedaluwarsa. Muat ulang halaman lalu coba lagi.'); } } function flash(?string $type = null, ?string $message = null): ?array { start_session(); if ($type !== null) { $_SESSION['flash'] = ['type' => $type, 'message' => $message]; return null; } $f = $_SESSION['flash'] ?? null; unset($_SESSION['flash']); return $f; } function redirect(string $url): void { header('Location: ' . $url, true, 303); exit; } function audit(string $action, string $summary, ?array $changes = null): void { $admin = $_SESSION['admin'] ?? null; db()->prepare('INSERT INTO audit_log (admin_id, username, action, summary, changes, ip) VALUES (?,?,?,?,?,?)') ->execute([$admin['id'] ?? null, $admin['username'] ?? null, $action, $summary, $changes ? json_encode($changes, JSON_UNESCAPED_UNICODE) : null, client_ip()]); // Every parameter change is audited after it is written: drop the cached parameters here. if (strpos($action, 'param') === 0) { params_cache_clear(); } } // ---- Number formatting / parsing (Indonesian conventions) ------------------- function fmt_idr(?float $v): string { return $v === null ? '' : number_format($v, 0, ',', '.'); } /** 0.0075 -> "0,75" */ function fmt_pct(?float $v): string { if ($v === null) { return ''; } $s = rtrim(rtrim(number_format($v * 100, 4, ',', ''), '0'), ','); return $s === '' || $s === '-0' ? '0' : $s; } /** "1.000.000" -> 1000000.0; "" -> null */ function parse_idr($s): ?float { $s = preg_replace('/[^0-9]/', '', input_str($s)); return $s === '' ? null : (float)$s; } /** "0,75" or "0.75" -> 0.0075; "" -> null; invalid -> false */ function parse_pct($s) { $s = trim(str_replace([' ', '%'], '', input_str($s))); if ($s === '') { return null; } $s = str_replace(',', '.', $s); if (!preg_match('/^-?\d+(\.\d+)?$/', $s)) { return false; } return round(((float)$s) / 100, 8); } function fmt_setting(array $row): string { $v = (float)$row['value']; switch ($row['unit']) { case 'pct': return fmt_pct($v); case 'idr': return fmt_idr($v); default: return rtrim(rtrim(number_format($v, 2, ',', ''), '0'), ','); } } function fmt_datetime(?string $s): string { if (!$s) { return '—'; } $months = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; $t = strtotime($s); return date('j', $t) . ' ' . $months[(int)date('n', $t)] . ' ' . date('Y, H.i', $t); } // ---- Icons (Lucide, ISC licence) -------------------------------------------- function icon(string $name, string $class = 'icon'): string { static $paths = [ 'printer' => '', 'copy' => '', 'reset' => '', 'info' => '', 'alert' => '', 'check' => '', 'lock' => '', 'logout' => '', 'plus' => '', 'trash' => '', 'users' => '', 'history' => '', 'sliders' => '', 'wallet' => '', 'grid' => '', 'pie' => '', 'calc' => '', 'sun' => '', 'moon' => '', 'chevron' => '', 'save' => '', 'key' => '', 'external' => '', 'menu' => '', 'x' => '', ]; return ''; } /** * Official Bank Papua logo. 'auto' swaps the color logo for the white one in * dark mode; 'white' is for navy surfaces; 'color' for paper/print. * The images are decorative (alt=""); the surrounding link or heading carries the name. */ function bank_logo(string $variant = 'auto', string $class = ''): string { $img = function (string $file, string $cls): string { return ''; }; $color = $img('logo-bank-papua.png', 'logo-color'); $white = $img('logo-bank-papua-white.png', 'logo-white'); $html = $variant === 'white' ? $white : ($variant === 'color' ? $color : $color . $white); return '' . $html . ''; } /** Inline script that applies the saved theme before first paint. */ function theme_boot_script(): string { return '"; }