528 lines
18 KiB
PHP
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",
|
|
};
|
|
}
|