39331-vm/archive_bootstrap.php
Flatlogic Bot 522a55296c arsip_demo
2026-03-26 11:04:24 +00:00

528 lines
18 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . "/db/config.php";
date_default_timezone_set("UTC");
if (session_status() !== PHP_SESSION_ACTIVE) {
$secure = (!empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off");
session_set_cookie_params([
"httponly" => 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 = '<ul class="tree-level list-unstyled mb-0">';
foreach ($nodes as $index => $node) {
$pathParts = [...$parents, $node["label"]];
$path = implode(" / ", $pathParts);
$mainMenu = $parents[0] ?? $node["label"];
if (!can_access_menu($user, $mainMenu)) {
continue;
}
$hasChildren = !empty($node["children"]);
$isOpen = $activeFolder && str_starts_with($activeFolder, $path);
$targetId = "tree-" . substr(md5($path . '-' . $index), 0, 10);
$html .= '<li class="tree-item">';
if ($hasChildren) {
$html .= '<button type="button" class="tree-toggle' . ($isOpen ? ' open' : '') . '" data-tree-target="#' . htmlspecialchars($targetId, ENT_QUOTES) . '" aria-expanded="' . ($isOpen ? 'true' : 'false') . '">';
$html .= '<span class="tree-icon">+</span><span class="tree-label">' . htmlspecialchars($node["label"]) . '</span></button>';
$html .= '<div id="' . htmlspecialchars($targetId, ENT_QUOTES) . '" class="tree-children' . ($isOpen ? ' show' : '') . '">';
$html .= render_tree_nodes($node["children"], $activeFolder, $user, $pathParts);
$html .= '</div>';
} else {
$isActive = $activeFolder === $path;
$html .= '<a class="tree-leaf' . ($isActive ? ' active' : '') . '" href="index.php?folder=' . urlencode($path) . '#arsip-form">';
$html .= '<span class="tree-dot"></span><span class="tree-label">' . htmlspecialchars($node["label"]) . '</span></a>';
}
$html .= '</li>';
}
$html .= '</ul>';
return $html;
}
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, "UTF-8");
}
function alert_class(string $type): string
{
return match ($type) {
"success" => "success",
"warning" => "warning",
default => "danger",
};
}
function record_badge_class(string $confidentiality): string
{
return match ($confidentiality) {
"Rahasia" => "text-bg-dark",
"Terbatas" => "text-bg-secondary",
default => "text-bg-light border",
};
}