true, "samesite" => "Lax", "secure" => $secure, ]); session_start(); } function project_meta(): array { $projectName = $_SERVER["PROJECT_NAME"] ?? "KBRI Harare Archive"; $projectDescription = $_SERVER["PROJECT_DESCRIPTION"] ?? "Aplikasi arsip internal KBRI Harare untuk pencatatan dokumen, folder bertingkat, dan kontrol akses kerja."; $projectImageUrl = $_SERVER["PROJECT_IMAGE_URL"] ?? ""; return [$projectName, $projectDescription, $projectImageUrl]; } function archive_tree(): array { return [ [ "label" => "INFORMASI NEGARA", "children" => [ [ "label" => "Zimbabwe", "children" => [ ["label" => "Keterangan Dasar"], ["label" => "Informasi Penting"], ["label" => "Catatan Peristiwa Penting"], ], ], [ "label" => "Zambia", "children" => [ ["label" => "Keterangan Dasar"], ["label" => "Informasi Penting"], ["label" => "Catatan Peristiwa Penting"], ], ], ], ], [ "label" => "POLITIK", "children" => [ [ "label" => "Nota Diplomatik", "children" => [ ["label" => "Keluar"], ["label" => "Masuk"], ], ], ["label" => "Perjanjian & Kesepakatan"], ["label" => "Hubungan Bilateral"], ["label" => "Kebijakan Strategis"], ], ], [ "label" => "PENSOSBUD", "children" => [ ["label" => "Program Kerja Sama"], ["label" => "Event Budaya"], [ "label" => "Scholarship", "children" => [ ["label" => "Permohonan"], ["label" => "Persetujuan"], ], ], ], ], [ "label" => "EKONOMI & PERDAGANGAN", "children" => [ [ "label" => "Nota Kesepahaman", "children" => [ ["label" => "Zimbabwe"], ["label" => "Zambia"], ], ], [ "label" => "Agreement Investasi", "children" => [ ["label" => "Indonesia"], ["label" => "Zimbabwe"], ["label" => "Zambia"], ], ], [ "label" => "Kerjasama Perusahaan", "children" => [ ["label" => "Indonesia"], ["label" => "Zimbabwe"], ["label" => "Zambia"], ], ], ], ], [ "label" => "KEKONSULERAN", "children" => [ [ "label" => "Dokumen Perjalanan & Identitas", "children" => [ ["label" => "Visa & Izin Tinggal"], ["label" => "SPLP"], ["label" => "Surat Keterangan"], ["label" => "Pencatatan Sipil"], ["label" => "Bantuan Hukum"], ["label" => "Repatriasi Jenazah"], ["label" => "Izin Diplomatik"], ], ], ["label" => "Legalisasi Dokumen"], ["label" => "Perlindungan WNI"], ["label" => "Fasilitas Diplomatik"], ], ], [ "label" => "KANSELERAI / HOC", "children" => [ ["label" => "SOP"], ["label" => "Kepegawaian"], ["label" => "Perkantoran"], ["label" => "Wisma Duta"], ], ], [ "label" => "PID", "children" => [ ["label" => "Contingency Plan"], ["label" => "Komputerisasi"], ["label" => "Pengamanan Terpadu"], ], ], [ "label" => "ADMIN & INTERNAL", "children" => [ ["label" => "Surat Edaran"], ["label" => "Laporan Internal"], ["label" => "Arsip Kepegawaian"], ["label" => "Inventaris & Logistik"], ], ], [ "label" => "Gallery", "children" => [ ["label" => "File Penting"], ["label" => "Photo Kegiatan"], ["label" => "Video Dokumentasi & Lain-lain"], ], ], ]; } function flatten_tree(array $nodes, array $parents = []): array { $flat = []; foreach ($nodes as $node) { $pathParts = [...$parents, $node["label"]]; $path = implode(" / ", $pathParts); $mainMenu = $parents[0] ?? $node["label"]; $flat[] = [ "label" => $node["label"], "path" => $path, "main_menu" => $mainMenu, "has_children" => !empty($node["children"]), "depth" => count($parents), ]; if (!empty($node["children"])) { $flat = array_merge($flat, flatten_tree($node["children"], $pathParts)); } } return $flat; } function folder_lookup(): array { static $lookup = null; if ($lookup !== null) { return $lookup; } $lookup = []; foreach (flatten_tree(archive_tree()) as $item) { $lookup[$item["path"]] = $item; } return $lookup; } function main_menu_options(): array { return array_map(static fn(array $node): string => $node["label"], archive_tree()); } function users_catalog(): array { static $users = null; if ($users !== null) { return $users; } $defaultPassword = "Kbri2026!"; $hash = password_hash($defaultPassword, PASSWORD_DEFAULT); $users = [ "super.admin1" => ["name" => "Super Admin 1", "role" => "super_admin", "unit" => "Pimpinan", "allowed_menus" => main_menu_options(), "password_hash" => $hash], "super.admin2" => ["name" => "Super Admin 2", "role" => "super_admin", "unit" => "Pimpinan", "allowed_menus" => main_menu_options(), "password_hash" => $hash], "politik.head" => ["name" => "Kabid Politik", "role" => "kepala_bagian", "unit" => "Politik", "allowed_menus" => ["INFORMASI NEGARA", "POLITIK"], "password_hash" => $hash], "pensosbud.head" => ["name" => "Kabid Pensosbud", "role" => "kepala_bagian", "unit" => "Pensosbud", "allowed_menus" => ["PENSOSBUD"], "password_hash" => $hash], "ekonomi.head" => ["name" => "Kabid Ekonomi", "role" => "kepala_bagian", "unit" => "Ekonomi & Perdagangan", "allowed_menus" => ["EKONOMI & PERDAGANGAN"], "password_hash" => $hash], "konsuler.head" => ["name" => "Kabid Kekonsuleran", "role" => "kepala_bagian", "unit" => "Kekonsuleran", "allowed_menus" => ["KEKONSULERAN"], "password_hash" => $hash], "hoc.head" => ["name" => "Kabid Kanselerai/HOC", "role" => "kepala_bagian", "unit" => "Kanselerai / HOC", "allowed_menus" => ["KANSELERAI / HOC"], "password_hash" => $hash], "pid.head" => ["name" => "Kabid PID", "role" => "kepala_bagian", "unit" => "PID", "allowed_menus" => ["PID"], "password_hash" => $hash], "admin.head" => ["name" => "Kabid Admin & Internal", "role" => "kepala_bagian", "unit" => "Admin & Internal", "allowed_menus" => ["ADMIN & INTERNAL", "Gallery"], "password_hash" => $hash], "politik.staff" => ["name" => "Staf Politik", "role" => "staf", "unit" => "Politik", "allowed_menus" => ["INFORMASI NEGARA", "POLITIK"], "password_hash" => $hash], "pensosbud.staff" => ["name" => "Staf Pensosbud", "role" => "staf", "unit" => "Pensosbud", "allowed_menus" => ["PENSOSBUD"], "password_hash" => $hash], "ekonomi.staff" => ["name" => "Staf Ekonomi", "role" => "staf", "unit" => "Ekonomi & Perdagangan", "allowed_menus" => ["EKONOMI & PERDAGANGAN"], "password_hash" => $hash], "konsuler.staff" => ["name" => "Staf Konsuler", "role" => "staf", "unit" => "Kekonsuleran", "allowed_menus" => ["KEKONSULERAN"], "password_hash" => $hash], "admin.staff" => ["name" => "Staf Admin", "role" => "staf", "unit" => "Admin & Internal", "allowed_menus" => ["ADMIN & INTERNAL", "Gallery"], "password_hash" => $hash], ]; return $users; } function role_badge_label(string $role): string { return match ($role) { "super_admin" => "Super Admin", "kepala_bagian" => "Kepala Bagian", default => "Staf", }; } function current_user(): ?array { $username = $_SESSION["auth_username"] ?? null; if (!$username) { return null; } $catalog = users_catalog(); if (!isset($catalog[$username])) { unset($_SESSION["auth_username"]); return null; } return ["username" => $username] + $catalog[$username]; } function require_login(): array { $user = current_user(); if (!$user) { flash("error", "Silakan masuk dengan username dan password internal Anda."); header("Location: index.php"); exit; } return $user; } function csrf_token(): string { if (empty($_SESSION["csrf_token"])) { $_SESSION["csrf_token"] = bin2hex(random_bytes(24)); } return $_SESSION["csrf_token"]; } function verify_csrf_or_fail(): void { $token = $_POST["csrf_token"] ?? ""; if (!hash_equals($_SESSION["csrf_token"] ?? "", $token)) { http_response_code(419); exit("CSRF token tidak valid."); } } function flash(string $type, string $message): void { $_SESSION["flash"] = ["type" => $type, "message" => $message]; } function pull_flash(): ?array { if (empty($_SESSION["flash"])) { return null; } $flash = $_SESSION["flash"]; unset($_SESSION["flash"]); return $flash; } function ensure_archive_table(): void { db()->exec( "CREATE TABLE IF NOT EXISTS archive_records ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, reference_code VARCHAR(100) NOT NULL, title VARCHAR(180) NOT NULL, main_menu VARCHAR(120) NOT NULL, folder_path VARCHAR(255) NOT NULL, country_tag VARCHAR(40) NOT NULL, owner_unit VARCHAR(120) NOT NULL, created_by_username VARCHAR(80) NOT NULL, created_by_name VARCHAR(120) NOT NULL, record_day TINYINT UNSIGNED NOT NULL, record_month TINYINT UNSIGNED NOT NULL, record_year SMALLINT UNSIGNED NOT NULL, document_date DATE DEFAULT NULL, confidentiality VARCHAR(40) NOT NULL, keywords VARCHAR(255) DEFAULT NULL, description TEXT NOT NULL, attachment_name VARCHAR(255) DEFAULT NULL, attachment_path VARCHAR(255) DEFAULT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_folder_path (folder_path), INDEX idx_main_menu (main_menu), INDEX idx_creator (created_by_username), INDEX idx_owner_unit (owner_unit) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); } function upload_dir(): string { $path = __DIR__ . "/uploads/archives"; if (!is_dir($path)) { mkdir($path, 0775, true); } return $path; } function allowed_file_extensions(): array { return ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "jpg", "jpeg", "png", "mp4", "zip"]; } function can_access_menu(array $user, string $mainMenu): bool { return $user["role"] === "super_admin" || in_array($mainMenu, $user["allowed_menus"], true); } function can_view_record(array $user, array $record): bool { if ($user["role"] === "super_admin" || $user["role"] === "kepala_bagian") { return true; } return $record["created_by_username"] === $user["username"]; } function can_edit_record(array $user, array $record): bool { if ($user["role"] === "super_admin") { return true; } if ($user["role"] === "kepala_bagian") { return $record["owner_unit"] === $user["unit"]; } return $record["created_by_username"] === $user["username"]; } function can_delete_record(array $user, array $record): bool { if ($user["role"] === "super_admin") { return true; } return $user["role"] === "staf" && $record["created_by_username"] === $user["username"]; } function available_folder_paths_for_user(array $user): array { $paths = []; foreach (flatten_tree(archive_tree()) as $node) { if ($node["has_children"]) { continue; } if (can_access_menu($user, $node["main_menu"])) { $paths[] = $node["path"]; } } return $paths; } function normalize_folder_path(string $folderPath): ?array { $lookup = folder_lookup(); return $lookup[$folderPath] ?? null; } function validate_record_date(int $day, int $month, int $year): ?string { if ($year < 2000 || $year > 2100) { return null; } if (!checkdate($month, $day, $year)) { return null; } return sprintf("%04d-%02d-%02d", $year, $month, $day); } function fetch_record_by_id(int $id): ?array { $stmt = db()->prepare("SELECT * FROM archive_records WHERE id = :id LIMIT 1"); $stmt->bindValue(":id", $id, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetch() ?: null; } function fetch_records(array $user, ?string $folderFilter = null, int $limit = 12): array { $sql = "SELECT * FROM archive_records"; $conditions = []; $params = []; if ($user["role"] === "staf") { $conditions[] = "created_by_username = :username"; $params[":username"] = $user["username"]; } if ($folderFilter) { $conditions[] = "folder_path = :folder_path"; $params[":folder_path"] = $folderFilter; } if ($conditions) { $sql .= " WHERE " . implode(" AND ", $conditions); } $sql .= " ORDER BY updated_at DESC LIMIT :limit_rows"; $stmt = db()->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value, PDO::PARAM_STR); } $stmt->bindValue(":limit_rows", $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function fetch_summary_counts(array $user): array { $sql = "SELECT COUNT(*) AS total_records, SUM(CASE WHEN attachment_path IS NOT NULL AND attachment_path <> '' THEN 1 ELSE 0 END) AS total_files, SUM(CASE WHEN DATE(created_at) = CURDATE() THEN 1 ELSE 0 END) AS created_today FROM archive_records"; $params = []; if ($user["role"] === "staf") { $sql .= " WHERE created_by_username = :username"; $params[":username"] = $user["username"]; } $stmt = db()->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value, PDO::PARAM_STR); } $stmt->execute(); return $stmt->fetch() ?: ["total_records" => 0, "total_files" => 0, "created_today" => 0]; } function render_tree_nodes(array $nodes, ?string $activeFolder, array $user, array $parents = []): string { $html = '