Compare commits

..

1 Commits

Author SHA1 Message Date
Flatlogic Bot
522a55296c arsip_demo 2026-03-26 11:04:24 +00:00
10 changed files with 1676 additions and 550 deletions

527
archive_bootstrap.php Normal file
View File

@ -0,0 +1,527 @@
<?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",
};
}

26
archive_delete.php Normal file
View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/archive_bootstrap.php';
ensure_archive_table();
$user = require_login();
verify_csrf_or_fail();
$id = (int) ($_POST['id'] ?? 0);
$record = fetch_record_by_id($id);
if (!$record || !can_delete_record($user, $record)) {
flash('error', 'Arsip tidak dapat dihapus oleh akun ini.');
header('Location: index.php');
exit;
}
$stmt = db()->prepare('DELETE FROM archive_records WHERE id = :id LIMIT 1');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();
if (!empty($record['attachment_path']) && is_file(__DIR__ . '/' . $record['attachment_path'])) {
@unlink(__DIR__ . '/' . $record['attachment_path']);
}
flash('success', 'Arsip berhasil dihapus.');
header('Location: index.php');

139
archive_detail.php Normal file
View File

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/archive_bootstrap.php';
ensure_archive_table();
$user = require_login();
[$projectName, $projectDescription, $projectImageUrl] = project_meta();
$id = (int) ($_GET['id'] ?? 0);
$record = fetch_record_by_id($id);
if (!$record || !can_view_record($user, $record)) {
http_response_code(404);
exit('Arsip tidak ditemukan atau akses ditolak.');
}
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($record['title']) ?> — <?= h($projectName) ?></title>
<meta name="description" content="<?= h($projectDescription) ?>">
<meta property="og:title" content="<?= h($record['title']) ?>">
<meta property="og:description" content="<?= h($projectDescription) ?>">
<meta property="twitter:title" content="<?= h($record['title']) ?>">
<meta property="twitter:description" content="<?= h($projectDescription) ?>">
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
</head>
<body>
<div class="app-shell">
<header class="topbar border-bottom no-print">
<div class="container-fluid px-3 px-lg-4 py-3 d-flex justify-content-between align-items-center gap-3 flex-wrap">
<div>
<div class="eyebrow">Detail arsip</div>
<h1 class="app-title mb-0"><?= h($record['title']) ?></h1>
</div>
<div class="d-flex gap-2 flex-wrap align-items-center">
<span class="badge badge-soft"><?= h(role_badge_label($user['role'])) ?></span>
<a href="index.php?folder=<?= urlencode($record['folder_path']) ?>" class="btn btn-outline-secondary btn-sm">Kembali ke dashboard</a>
<?php if (can_edit_record($user, $record)): ?>
<a href="index.php?edit=<?= (int) $record['id'] ?>#arsip-form" class="btn btn-outline-secondary btn-sm">Edit</a>
<?php endif; ?>
<button type="button" class="btn btn-dark btn-sm" data-print-trigger>Cetak</button>
</div>
</div>
</header>
<main class="container-fluid px-3 px-lg-4 py-4">
<div class="row g-4">
<div class="col-lg-8">
<div class="panel print-panel mb-4">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-3">
<div>
<div class="eyebrow">Nomor referensi</div>
<h2 class="section-title mb-1"><?= h($record['reference_code']) ?></h2>
<p class="text-secondary mb-0"><?= h($record['folder_path']) ?></p>
</div>
<span class="badge <?= h(record_badge_class($record['confidentiality'])) ?>"><?= h($record['confidentiality']) ?></span>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="metric-card h-100">
<div class="metric-label">Tanggal arsip</div>
<div class="metric-value small-value"><?= h($record['record_day']) ?>/<?= h($record['record_month']) ?>/<?= h($record['record_year']) ?></div>
<small><?= h((string) $record['document_date']) ?></small>
</div>
</div>
<div class="col-md-4">
<div class="metric-card h-100">
<div class="metric-label">Pemilik unit</div>
<div class="metric-value small-value"><?= h($record['owner_unit']) ?></div>
<small>Dibuat oleh <?= h($record['created_by_name']) ?></small>
</div>
</div>
<div class="col-md-4">
<div class="metric-card h-100">
<div class="metric-label">Negara / cakupan</div>
<div class="metric-value small-value"><?= h($record['country_tag']) ?></div>
<small>Menu <?= h($record['main_menu']) ?></small>
</div>
</div>
</div>
<article class="detail-body">
<h3 class="small-heading">Ringkasan dokumen</h3>
<p class="mb-0"><?= nl2br(h($record['description'])) ?></p>
</article>
</div>
</div>
<div class="col-lg-4">
<div class="panel h-100 d-flex flex-column gap-4">
<div>
<div class="eyebrow">Metadata</div>
<h2 class="section-title mb-3">Informasi dokumen</h2>
<dl class="meta-grid mb-0">
<div><dt>Kata kunci</dt><dd><?= h($record['keywords'] ?: '—') ?></dd></div>
<div><dt>Penginput</dt><dd><?= h($record['created_by_name']) ?> · <?= h($record['created_by_username']) ?></dd></div>
<div><dt>Dibuat</dt><dd><?= h((string) $record['created_at']) ?></dd></div>
<div><dt>Diperbarui</dt><dd><?= h((string) $record['updated_at']) ?></dd></div>
</dl>
</div>
<div>
<div class="eyebrow">Lampiran</div>
<h2 class="section-title mb-3">Aksi cepat</h2>
<?php if (!empty($record['attachment_name'])): ?>
<div class="file-card mb-3">
<strong><?= h($record['attachment_name']) ?></strong>
<p class="text-secondary mb-0">Lampiran arsip siap diunduh sesuai hak akses.</p>
</div>
<div class="d-grid gap-2">
<a href="archive_download.php?id=<?= (int) $record['id'] ?>" class="btn btn-dark">Unduh lampiran</a>
</div>
<?php else: ?>
<div class="empty-state mb-0">
<strong>Belum ada lampiran.</strong>
<p class="mb-0">Arsip ini masih tersimpan sebagai entri metadata dan ringkasan dokumen.</p>
</div>
<?php endif; ?>
</div>
<?php if (can_delete_record($user, $record)): ?>
<form action="archive_delete.php" method="post" class="no-print">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="id" value="<?= (int) $record['id'] ?>">
<button type="submit" class="btn btn-outline-danger w-100" onclick="return confirm('Hapus arsip ini secara permanen?');">Hapus arsip</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>

26
archive_download.php Normal file
View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/archive_bootstrap.php';
ensure_archive_table();
$user = require_login();
$id = (int) ($_GET['id'] ?? 0);
$record = fetch_record_by_id($id);
if (!$record || !can_view_record($user, $record) || empty($record['attachment_path'])) {
http_response_code(404);
exit('Lampiran tidak ditemukan.');
}
$file = __DIR__ . '/' . $record['attachment_path'];
if (!is_file($file)) {
http_response_code(404);
exit('File tidak tersedia di server.');
}
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . rawurlencode((string) $record['attachment_name']) . '"');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;

155
archive_save.php Normal file
View File

@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/archive_bootstrap.php';
ensure_archive_table();
$user = require_login();
verify_csrf_or_fail();
$id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
$referenceCode = trim((string) ($_POST['reference_code'] ?? ''));
$title = trim((string) ($_POST['title'] ?? ''));
$mainMenu = trim((string) ($_POST['main_menu'] ?? ''));
$folderPath = trim((string) ($_POST['folder_path'] ?? ''));
$countryTag = trim((string) ($_POST['country_tag'] ?? ''));
$recordDay = (int) ($_POST['record_day'] ?? 0);
$recordMonth = (int) ($_POST['record_month'] ?? 0);
$recordYear = (int) ($_POST['record_year'] ?? 0);
$confidentiality = trim((string) ($_POST['confidentiality'] ?? ''));
$keywords = trim((string) ($_POST['keywords'] ?? ''));
$description = trim((string) ($_POST['description'] ?? ''));
if ($referenceCode === '' || $title === '' || $folderPath === '' || $description === '' || $mainMenu === '' || $countryTag === '' || $confidentiality === '') {
flash('error', 'Semua field wajib harus diisi sebelum menyimpan arsip.');
header('Location: index.php#arsip-form');
exit;
}
$folderInfo = normalize_folder_path($folderPath);
if (!$folderInfo || $folderInfo['has_children']) {
flash('error', 'Pilih folder terdalam yang valid.');
header('Location: index.php#arsip-form');
exit;
}
if ($folderInfo['main_menu'] !== $mainMenu) {
flash('error', 'Menu utama tidak sesuai dengan folder yang dipilih.');
header('Location: index.php#arsip-form');
exit;
}
if (!can_access_menu($user, $mainMenu)) {
flash('error', 'Akun ini tidak memiliki izin untuk folder tersebut.');
header('Location: index.php');
exit;
}
$documentDate = validate_record_date($recordDay, $recordMonth, $recordYear);
if (!$documentDate) {
flash('error', 'Tanggal arsip tidak valid.');
header('Location: index.php#arsip-form');
exit;
}
$existing = null;
if ($id > 0) {
$existing = fetch_record_by_id($id);
if (!$existing || !can_edit_record($user, $existing)) {
flash('error', 'Arsip tidak ditemukan atau tidak dapat diedit.');
header('Location: index.php');
exit;
}
}
$attachmentName = $existing['attachment_name'] ?? null;
$attachmentPath = $existing['attachment_path'] ?? null;
if (!empty($_FILES['attachment']['name'])) {
if (!isset($_FILES['attachment']['error']) || $_FILES['attachment']['error'] !== UPLOAD_ERR_OK) {
flash('error', 'Lampiran gagal diunggah.');
header('Location: index.php#arsip-form');
exit;
}
if ((int) $_FILES['attachment']['size'] > 8 * 1024 * 1024) {
flash('error', 'Ukuran file maksimal 8 MB.');
header('Location: index.php#arsip-form');
exit;
}
$originalName = basename((string) $_FILES['attachment']['name']);
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($extension, allowed_file_extensions(), true)) {
flash('error', 'Format lampiran belum didukung.');
header('Location: index.php#arsip-form');
exit;
}
$safeName = date('YmdHis') . '-' . bin2hex(random_bytes(6)) . '.' . $extension;
$relativePath = 'uploads/archives/' . $safeName;
$destination = __DIR__ . '/' . $relativePath;
upload_dir();
if (!move_uploaded_file($_FILES['attachment']['tmp_name'], $destination)) {
flash('error', 'Lampiran gagal disimpan ke server.');
header('Location: index.php#arsip-form');
exit;
}
if ($attachmentPath && is_file(__DIR__ . '/' . $attachmentPath)) {
@unlink(__DIR__ . '/' . $attachmentPath);
}
$attachmentName = $originalName;
$attachmentPath = $relativePath;
}
$ownerUnit = $user['unit'];
if ($mainMenu === 'INFORMASI NEGARA' && in_array($user['unit'], ['Politik', 'Pimpinan'], true)) {
$ownerUnit = 'Politik';
}
if ($id > 0) {
$stmt = db()->prepare('UPDATE archive_records SET
reference_code = :reference_code,
title = :title,
main_menu = :main_menu,
folder_path = :folder_path,
country_tag = :country_tag,
owner_unit = :owner_unit,
record_day = :record_day,
record_month = :record_month,
record_year = :record_year,
document_date = :document_date,
confidentiality = :confidentiality,
keywords = :keywords,
description = :description,
attachment_name = :attachment_name,
attachment_path = :attachment_path
WHERE id = :id');
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
} else {
$stmt = db()->prepare('INSERT INTO archive_records (
reference_code, title, main_menu, folder_path, country_tag, owner_unit,
created_by_username, created_by_name, record_day, record_month, record_year,
document_date, confidentiality, keywords, description, attachment_name, attachment_path
) VALUES (
:reference_code, :title, :main_menu, :folder_path, :country_tag, :owner_unit,
:created_by_username, :created_by_name, :record_day, :record_month, :record_year,
:document_date, :confidentiality, :keywords, :description, :attachment_name, :attachment_path
)');
$stmt->bindValue(':created_by_username', $user['username'], PDO::PARAM_STR);
$stmt->bindValue(':created_by_name', $user['name'], PDO::PARAM_STR);
}
$stmt->bindValue(':reference_code', $referenceCode, PDO::PARAM_STR);
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
$stmt->bindValue(':main_menu', $mainMenu, PDO::PARAM_STR);
$stmt->bindValue(':folder_path', $folderPath, PDO::PARAM_STR);
$stmt->bindValue(':country_tag', $countryTag, PDO::PARAM_STR);
$stmt->bindValue(':owner_unit', $ownerUnit, PDO::PARAM_STR);
$stmt->bindValue(':record_day', $recordDay, PDO::PARAM_INT);
$stmt->bindValue(':record_month', $recordMonth, PDO::PARAM_INT);
$stmt->bindValue(':record_year', $recordYear, PDO::PARAM_INT);
$stmt->bindValue(':document_date', $documentDate, PDO::PARAM_STR);
$stmt->bindValue(':confidentiality', $confidentiality, PDO::PARAM_STR);
$stmt->bindValue(':keywords', $keywords !== '' ? $keywords : null, $keywords !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
$stmt->bindValue(':description', $description, PDO::PARAM_STR);
$stmt->bindValue(':attachment_name', $attachmentName, $attachmentName ? PDO::PARAM_STR : PDO::PARAM_NULL);
$stmt->bindValue(':attachment_path', $attachmentPath, $attachmentPath ? PDO::PARAM_STR : PDO::PARAM_NULL);
$stmt->execute();
$recordId = $id > 0 ? $id : (int) db()->lastInsertId();
flash('success', $id > 0 ? 'Perubahan arsip berhasil disimpan.' : 'Arsip baru berhasil ditambahkan ke database.');
header('Location: archive_detail.php?id=' . $recordId);

View File

@ -1,403 +1,336 @@
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
/* KBRI Harare Archive Desk */
:root {
--bg: #f4f5f7;
--surface: #ffffff;
--surface-alt: #f8f9fb;
--border: #d7dce3;
--border-strong: #c3cad4;
--text: #101828;
--text-muted: #667085;
--primary: #1f2937;
--primary-soft: #eef1f4;
--accent: #4b5563;
--success: #0f766e;
--warning: #92400e;
--danger: #b42318;
--shadow-sm: 0 10px 30px rgba(16, 24, 40, 0.04);
--shadow-lg: 0 22px 50px rgba(16, 24, 40, 0.08);
--radius-sm: 10px;
--radius-md: 14px;
--radius-lg: 18px;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 2rem;
}
.main-wrapper {
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
}
a { color: inherit; text-decoration: none; }
a:hover { color: inherit; }
.auth-shell,
.app-shell {
min-height: 100vh;
background: #f4f5f7;
}
.topbar {
background: rgba(255,255,255,0.92);
backdrop-filter: blur(10px);
}
.panel,
.metric-card,
.record-card,
.file-card,
.empty-state {
background: rgba(255,255,255,0.98);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.panel { padding: var(--space-5); }
.panel-hero {
position: relative;
overflow: hidden;
box-shadow: var(--shadow-lg);
}
.panel-hero::after {
content: "";
position: absolute;
inset: auto -10% -40% auto;
width: 220px;
height: 220px;
border-radius: 50%;
background: rgba(31, 41, 55, 0.05);
box-shadow: inset -24px -24px 40px rgba(255,255,255,0.7), 20px 20px 40px rgba(31,41,55,0.05);
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.72rem;
font-weight: 700;
color: var(--text-muted);
}
.display-title,
.app-title,
.section-title {
letter-spacing: -0.02em;
font-weight: 700;
color: var(--text);
}
.display-title { font-size: clamp(2rem, 4vw, 3.35rem); max-width: 12ch; }
.app-title { font-size: 1.35rem; }
.section-title { font-size: 1.15rem; }
.lead-copy,
.text-secondary,
.form-text,
.inline-note,
.record-meta,
.record-path,
.policy-list li,
.empty-state p,
.callout,
small { color: var(--text-muted) !important; }
.badge-soft {
background: var(--primary-soft);
color: var(--primary);
border: 1px solid var(--border);
font-weight: 600;
}
.badge-outline {
background: transparent;
color: var(--text);
border: 1px solid var(--border-strong);
font-weight: 600;
}
.metric-card {
padding: 1rem 1.1rem;
position: relative;
}
.metric-card::before {
content: "";
position: absolute;
inset: 12px 12px auto auto;
width: 52px;
height: 52px;
border-radius: 14px;
border: 1px solid rgba(31,41,55,0.08);
background: rgba(255,255,255,0.72);
box-shadow: 12px 12px 25px rgba(31,41,55,0.04), inset -10px -10px 15px rgba(255,255,255,0.9);
}
.metric-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 0.4rem;
position: relative;
z-index: 1;
}
.metric-value {
font-size: 1.9rem;
font-weight: 700;
letter-spacing: -0.03em;
position: relative;
z-index: 1;
}
.small-value { font-size: 1.3rem; }
.callout,
.inline-note,
.file-card,
.empty-state {
padding: 0.9rem 1rem;
border-radius: var(--radius-md);
background: var(--surface-alt);
border: 1px solid var(--border);
}
.form-control,
.form-select {
border-radius: var(--radius-sm);
border-color: var(--border-strong);
padding: 0.8rem 0.9rem;
color: var(--text);
background-color: #fff;
}
.form-control:focus,
.form-select:focus {
border-color: #98a2b3;
box-shadow: 0 0 0 0.2rem rgba(17,24,39,0.08);
}
.form-label {
font-size: 0.88rem;
font-weight: 600;
color: var(--text);
}
.btn {
border-radius: 12px;
font-weight: 600;
padding: 0.72rem 1rem;
}
.btn-dark {
background: #111827;
border-color: #111827;
box-shadow: 0 10px 25px rgba(17, 24, 39, 0.18);
}
.btn-dark:hover,
.btn-dark:focus { background: #0b1220; border-color: #0b1220; }
.btn-outline-secondary,
.btn-outline-danger {
border-color: var(--border-strong);
}
.tree-nav {
max-height: 70vh;
overflow: auto;
padding-right: 0.35rem;
}
.tree-level { padding-left: 0; }
.tree-item + .tree-item { margin-top: 0.35rem; }
.tree-toggle,
.tree-leaf {
width: 100%;
display: flex;
align-items: center;
gap: 0.7rem;
border: 0;
background: transparent;
padding: 0.65rem 0.7rem;
border-radius: 12px;
text-align: left;
}
.tree-toggle:hover,
.tree-leaf:hover,
.tree-leaf.active {
background: var(--primary-soft);
}
.tree-icon {
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
z-index: 1;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.chat-container {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
display: flex;
flex-direction: column;
height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden;
}
.chat-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border: 1px solid var(--border-strong);
background: #fff;
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
flex: 0 0 22px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
.tree-toggle.open .tree-icon { content: ""; }
.tree-toggle.open .tree-icon,
.tree-leaf.active .tree-dot {
background: #111827;
color: #fff;
border-bottom-right-radius: 4px;
border-color: #111827;
}
.tree-label { font-size: 0.94rem; color: var(--text); }
.tree-children {
display: none;
margin-left: 0.9rem;
border-left: 1px dashed var(--border);
padding-left: 0.75rem;
}
.tree-children.show { display: block; }
.tree-dot {
width: 10px;
height: 10px;
border-radius: 999px;
border: 1px solid var(--border-strong);
background: #fff;
flex: 0 0 10px;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
.record-card {
padding: 1rem;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
}
.chat-input-area input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
}
.chat-input-area button {
background: #212529;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.chat-input-area button:hover {
background: #000;
.record-card + .record-card { margin-top: 0.85rem; }
.record-card:hover,
.record-card.active {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
box-shadow: var(--shadow-lg);
border-color: #b6bec9;
}
.record-title { font-size: 1rem; margin-bottom: 0.35rem; }
.record-meta,
.record-path { font-size: 0.84rem; }
.record-path { min-height: 2.4rem; }
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
.credential-table th,
.credential-table td,
.meta-grid dt,
.meta-grid dd { font-size: 0.9rem; }
.meta-grid div + div { border-top: 1px solid var(--border); }
.meta-grid dt {
color: var(--text-muted);
margin-bottom: 0.25rem;
padding-top: 0.9rem;
}
.blob {
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.header-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.header-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
.meta-grid dd {
margin-bottom: 0;
font-weight: 600;
padding-bottom: 0.9rem;
}
.policy-list {
padding-left: 1rem;
display: grid;
gap: 0.55rem;
}
.small-heading {
font-size: 0.82rem;
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-links {
display: flex;
gap: 1rem;
}
.admin-card {
background: rgba(255, 255, 255, 0.6);
padding: 2rem;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
margin-bottom: 2.5rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.admin-card h3 {
margin-top: 0;
margin-bottom: 1.5rem;
font-weight: 700;
color: var(--text-muted);
}
.detail-body {
border-top: 1px solid var(--border);
padding-top: 1rem;
}
.btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
.toast {
border-radius: 14px;
box-shadow: var(--shadow-sm);
}
.btn-add {
background: #212529;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
.no-print { display: block; }
@media (max-width: 991.98px) {
.panel { padding: 1rem; }
.tree-nav { max-height: none; }
.display-title { max-width: none; }
}
.btn-save {
background: #0088cc;
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
width: 100%;
transition: all 0.3s ease;
@media print {
body { background: #fff; }
.no-print,
.btn,
.topbar { display: none !important; }
.panel,
.metric-card,
.file-card {
box-shadow: none;
border-color: #d1d5db;
background: #fff;
}
.print-panel { padding: 0; border: 0; }
}
.webhook-url {
font-size: 0.85em;
color: #555;
margin-top: 0.5rem;
}
.history-table-container {
overflow-x: auto;
background: rgba(255, 255, 255, 0.4);
padding: 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.history-table {
width: 100%;
}
.history-table-time {
width: 15%;
white-space: nowrap;
font-size: 0.85em;
color: #555;
}
.history-table-user {
width: 35%;
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 8px;
}
.history-table-ai {
width: 50%;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 8px;
}
.no-messages {
text-align: center;
color: #777;
}

View File

@ -1,39 +1,46 @@
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
document.querySelectorAll('.tree-toggle').forEach((toggle) => {
const targetSelector = toggle.getAttribute('data-tree-target');
const target = targetSelector ? document.querySelector(targetSelector) : null;
const icon = toggle.querySelector('.tree-icon');
const syncState = () => {
const open = target && target.classList.contains('show');
toggle.classList.toggle('open', !!open);
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
if (icon) {
icon.textContent = open ? '' : '+';
}
};
syncState();
toggle.addEventListener('click', () => {
if (target) {
target.classList.toggle('show');
}
syncState();
});
});
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
document.querySelectorAll('.toast').forEach((el) => {
const toast = new bootstrap.Toast(el, { delay: 4500 });
toast.show();
});
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
const folderSelect = document.getElementById('folder_path');
const mainMenuSelect = document.getElementById('main_menu');
if (folderSelect && mainMenuSelect) {
const syncMenuFromFolder = () => {
const selectedOption = folderSelect.options[folderSelect.selectedIndex];
if (!selectedOption || !selectedOption.value) return;
const mainMenu = selectedOption.value.split(' / ')[0];
if ([...mainMenuSelect.options].some((option) => option.value === mainMenu)) {
mainMenuSelect.value = mainMenu;
}
};
folderSelect.addEventListener('change', syncMenuFromFolder);
syncMenuFromFolder();
}
appendMessage(message, 'visitor');
chatInput.value = '';
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
// Artificial delay for realism
setTimeout(() => {
appendMessage(data.reply, 'bot');
}, 500);
} catch (error) {
console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
}
document.querySelectorAll('[data-print-trigger]').forEach((button) => {
button.addEventListener('click', () => window.print());
});
});

21
auth_login.php Normal file
View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/archive_bootstrap.php';
ensure_archive_table();
verify_csrf_or_fail();
$username = trim((string) ($_POST['username'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
$catalog = users_catalog();
if ($username === '' || $password === '' || !isset($catalog[$username]) || !password_verify($password, $catalog[$username]['password_hash'])) {
flash('error', 'Username atau password tidak sesuai.');
header('Location: index.php');
exit;
}
session_regenerate_id(true);
$_SESSION['auth_username'] = $username;
flash('success', 'Login berhasil. Silakan lanjutkan pengelolaan arsip.');
header('Location: index.php');

560
index.php
View File

@ -1,150 +1,432 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
require_once __DIR__ . '/archive_bootstrap.php';
ensure_archive_table();
[$projectName, $projectDescription, $projectImageUrl] = project_meta();
$user = current_user();
$flash = pull_flash();
$folderFilter = isset($_GET['folder']) ? trim((string) $_GET['folder']) : null;
$folderInfo = $folderFilter ? normalize_folder_path($folderFilter) : null;
if ($folderFilter && !$folderInfo) {
$folderFilter = null;
}
$editRecord = null;
if ($user && isset($_GET['edit'])) {
$editRecord = fetch_record_by_id((int) $_GET['edit']);
if (!$editRecord || !can_edit_record($user, $editRecord)) {
$editRecord = null;
flash('error', 'Data yang dipilih tidak dapat diedit oleh akun ini.');
header('Location: index.php');
exit;
}
}
$selectedRecord = null;
if ($user && isset($_GET['record'])) {
$selectedRecord = fetch_record_by_id((int) $_GET['record']);
if (!$selectedRecord || !can_view_record($user, $selectedRecord)) {
$selectedRecord = null;
}
}
$monthNames = [
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April', 5 => 'Mei', 6 => 'Juni',
7 => 'Juli', 8 => 'Agustus', 9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember',
];
$yearOptions = range((int) date('Y') + 1, 2020);
$loginUsers = users_catalog();
?>
<!doctype html>
<html lang="en">
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($projectName) ?> — Arsip Internal</title>
<meta name="description" content="<?= h($projectDescription) ?>">
<meta property="og:title" content="<?= h($projectName) ?>">
<meta property="og:description" content="<?= h($projectDescription) ?>">
<meta property="twitter:title" content="<?= h($projectName) ?>">
<meta property="twitter:description" content="<?= h($projectDescription) ?>">
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
<?php if (!$user): ?>
<main class="auth-shell">
<section class="container py-4 py-lg-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<div class="row g-4 align-items-stretch">
<div class="col-lg-7">
<div class="panel panel-hero h-100">
<div class="eyebrow mb-3">Portal Arsip Internal KBRI Harare</div>
<h1 class="display-title mb-3">Arsip terstruktur, aman, dan siap dipakai untuk alur kerja harian.</h1>
<p class="lead-copy mb-4">Versi awal ini sudah menyiapkan login internal, struktur menu tree lengkap, input dokumen bertanggal, daftar arsip, detail, edit, print, dan unduh lampiran.</p>
<div class="row g-3">
<div class="col-sm-6">
<div class="metric-card h-100">
<div class="metric-label">Peran awal</div>
<div class="metric-value">14 akun</div>
<small>2 super admin, 7 kepala bagian, 5 staf</small>
</div>
</div>
<div class="col-sm-6">
<div class="metric-card h-100">
<div class="metric-label">Kontrol kerja</div>
<div class="metric-value">Tree + RBAC</div>
<small>Akses folder sesuai unit kerja</small>
</div>
</div>
</div>
<div class="callout mt-4">
<strong>Default keamanan tahap awal:</strong> semua akun demo memakai password <code>Kbri2026!</code>. Setelah alur disetujui, langkah paling penting berikutnya adalah password individual + audit trail lanjutan.
</div>
</div>
</div>
<div class="col-lg-5">
<div class="panel h-100">
<div class="d-flex justify-content-between align-items-start mb-4 gap-3 flex-wrap">
<div>
<div class="eyebrow">Masuk</div>
<h2 class="section-title mb-1">Akun internal</h2>
<p class="text-secondary mb-0">Gunakan salah satu username resmi di bawah.</p>
</div>
<span class="badge badge-soft">High security MVP</span>
</div>
<?php if ($flash): ?>
<div class="alert alert-<?= h(alert_class($flash['type'])) ?> mb-3" role="alert"><?= h($flash['message']) ?></div>
<?php endif; ?>
<form action="auth_login.php" method="post" class="mb-4">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="mis. super.admin1" required autocomplete="username">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password internal" required autocomplete="current-password">
</div>
<button type="submit" class="btn btn-dark w-100">Masuk ke dashboard arsip</button>
</form>
<div class="credentials-list">
<div class="d-flex justify-content-between align-items-center mb-2">
<h3 class="small-heading mb-0">Akun demo siap pakai</h3>
<span class="text-secondary small">password sama untuk semua</span>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0 credential-table">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<?php foreach ($loginUsers as $username => $account): ?>
<tr>
<td><code><?= h($username) ?></code></td>
<td><?= h(role_badge_label($account['role'])) ?> — <?= h($account['unit']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<?php else: ?>
<?php
$counts = fetch_summary_counts($user);
$records = fetch_records($user, $folderFilter, 10);
$availableFolders = available_folder_paths_for_user($user);
$formFolder = $editRecord['folder_path'] ?? $folderFilter ?? ($availableFolders[0] ?? '');
$formFolderInfo = $formFolder ? normalize_folder_path($formFolder) : null;
$formMainMenu = $editRecord['main_menu'] ?? ($formFolderInfo['main_menu'] ?? ($user['allowed_menus'][0] ?? ''));
$todayUtc = gmdate('d M Y, H:i') . ' UTC';
?>
<div class="app-shell">
<header class="topbar border-bottom">
<div class="container-fluid px-3 px-lg-4 py-3">
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap">
<div>
<div class="eyebrow">Internal archive workspace</div>
<h1 class="app-title mb-0">KBRI Harare Archive Desk</h1>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="badge badge-soft"><?= h(role_badge_label($user['role'])) ?></span>
<span class="badge badge-outline"><?= h($user['name']) ?> · <?= h($user['username']) ?></span>
<span class="text-secondary small">Sinkron UTC <?= h($todayUtc) ?></span>
<form action="logout.php" method="post" class="m-0">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<button type="submit" class="btn btn-outline-secondary btn-sm">Keluar</button>
</form>
</div>
</div>
</div>
</header>
<main class="container-fluid px-3 px-lg-4 py-4">
<div class="row g-4 mb-4">
<div class="col-12 col-xl-3">
<div class="panel h-100">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<div class="eyebrow">Navigasi pohon</div>
<h2 class="section-title mb-0">Menu utama</h2>
</div>
<span class="badge badge-outline">+</span>
</div>
<p class="text-secondary small mb-3">Klik tanda + untuk membuka sub-menu dan pilih folder terdalam sebagai lokasi arsip.</p>
<nav class="tree-nav" aria-label="Struktur arsip KBRI">
<?= render_tree_nodes(archive_tree(), $folderFilter, $user) ?>
</nav>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="panel panel-hero mb-4">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-start">
<div>
<div class="eyebrow">First delivery</div>
<h2 class="section-title mb-2">Workflow arsip harian dari input sampai detail.</h2>
<p class="text-secondary mb-0">Form di bawah terhubung ke database, mengikuti folder tree, menyimpan lampiran, dan menampilkan daftar arsip sesuai hak akses akun yang aktif.</p>
</div>
<div class="hero-actions d-flex gap-2 flex-wrap">
<a href="#arsip-form" class="btn btn-dark btn-sm">Input arsip</a>
<?php if ($selectedRecord): ?>
<a href="archive_detail.php?id=<?= (int) $selectedRecord['id'] ?>" class="btn btn-outline-secondary btn-sm">Lihat detail</a>
<?php endif; ?>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-sm-4">
<div class="metric-card h-100">
<div class="metric-label">Total arsip</div>
<div class="metric-value"><?= (int) $counts['total_records'] ?></div>
<small><?= $user['role'] === 'staf' ? 'Hanya milik akun ini' : 'Seluruh arsip yang terlihat' ?></small>
</div>
</div>
<div class="col-sm-4">
<div class="metric-card h-100">
<div class="metric-label">Lampiran aktif</div>
<div class="metric-value"><?= (int) $counts['total_files'] ?></div>
<small>Siap diunduh dari detail arsip</small>
</div>
</div>
<div class="col-sm-4">
<div class="metric-card h-100">
<div class="metric-label">Masuk hari ini</div>
<div class="metric-value"><?= (int) $counts['created_today'] ?></div>
<small>Dokumen baru per <?= h($todayUtc) ?></small>
</div>
</div>
</div>
<div class="panel" id="arsip-form">
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap mb-3">
<div>
<div class="eyebrow">Form arsip</div>
<h2 class="section-title mb-1"><?= $editRecord ? 'Edit dokumen arsip' : 'Input dokumen baru' ?></h2>
<p class="text-secondary mb-0">Tanggal dipecah menjadi hari, bulan, dan tahun. Folder harus mengikuti menu kerja akun aktif.</p>
</div>
<?php if ($editRecord): ?>
<a href="index.php<?= $folderFilter ? '?folder=' . urlencode($folderFilter) : '' ?>#arsip-form" class="btn btn-outline-secondary btn-sm">Batal edit</a>
<?php endif; ?>
</div>
<?php if ($flash): ?>
<div class="toast align-items-center text-bg-<?= h(alert_class($flash['type'])) ?> border-0 show mb-3" role="status" aria-live="polite" aria-atomic="true">
<div class="d-flex">
<div class="toast-body"><?= h($flash['message']) ?></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<?php endif; ?>
<form action="archive_save.php" method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
<?php if ($editRecord): ?>
<input type="hidden" name="id" value="<?= (int) $editRecord['id'] ?>">
<?php endif; ?>
<div class="col-md-6">
<label class="form-label" for="reference_code">Nomor referensi</label>
<input class="form-control" id="reference_code" name="reference_code" maxlength="100" required value="<?= h($editRecord['reference_code'] ?? '') ?>" placeholder="mis. POL/HAR/ND/026/2026">
</div>
<div class="col-md-6">
<label class="form-label" for="title">Judul arsip</label>
<input class="form-control" id="title" name="title" maxlength="180" required value="<?= h($editRecord['title'] ?? '') ?>" placeholder="Judul dokumen atau peristiwa">
</div>
<div class="col-md-4">
<label class="form-label" for="main_menu">Menu utama</label>
<select class="form-select" id="main_menu" name="main_menu" required>
<?php foreach (main_menu_options() as $menu): ?>
<?php if (can_access_menu($user, $menu)): ?>
<option value="<?= h($menu) ?>" <?= $formMainMenu === $menu ? 'selected' : '' ?>><?= h($menu) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-8">
<label class="form-label" for="folder_path">Folder nested</label>
<select class="form-select" id="folder_path" name="folder_path" required>
<option value="">Pilih folder terdalam</option>
<?php foreach ($availableFolders as $path): ?>
<option value="<?= h($path) ?>" <?= $formFolder === $path ? 'selected' : '' ?>><?= h($path) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="country_tag">Negara / cakupan</label>
<select class="form-select" id="country_tag" name="country_tag" required>
<?php $countryValue = $editRecord['country_tag'] ?? 'Zimbabwe'; ?>
<?php foreach (['Zimbabwe', 'Zambia', 'Indonesia', 'Regional', 'Internal'] as $country): ?>
<option value="<?= h($country) ?>" <?= $countryValue === $country ? 'selected' : '' ?>><?= h($country) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="record_day">Tanggal</label>
<select class="form-select" id="record_day" name="record_day" required>
<?php $selectedDay = (int) ($editRecord['record_day'] ?? date('j')); ?>
<?php foreach (range(1, 31) as $day): ?>
<option value="<?= $day ?>" <?= $selectedDay === $day ? 'selected' : '' ?>><?= $day ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="record_month">Bulan</label>
<select class="form-select" id="record_month" name="record_month" required>
<?php $selectedMonth = (int) ($editRecord['record_month'] ?? date('n')); ?>
<?php foreach ($monthNames as $number => $label): ?>
<option value="<?= $number ?>" <?= $selectedMonth === $number ? 'selected' : '' ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="record_year">Tahun</label>
<select class="form-select" id="record_year" name="record_year" required>
<?php $selectedYear = (int) ($editRecord['record_year'] ?? date('Y')); ?>
<?php foreach ($yearOptions as $year): ?>
<option value="<?= $year ?>" <?= $selectedYear === (int) $year ? 'selected' : '' ?>><?= $year ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="confidentiality">Klasifikasi</label>
<select class="form-select" id="confidentiality" name="confidentiality" required>
<?php $classification = $editRecord['confidentiality'] ?? 'Terbuka'; ?>
<?php foreach (['Terbuka', 'Terbatas', 'Rahasia'] as $item): ?>
<option value="<?= h($item) ?>" <?= $classification === $item ? 'selected' : '' ?>><?= h($item) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="keywords">Kata kunci</label>
<input class="form-control" id="keywords" name="keywords" maxlength="255" value="<?= h($editRecord['keywords'] ?? '') ?>" placeholder="mis. bilateral, nota, visa, SOP">
</div>
<div class="col-12">
<label class="form-label" for="description">Ringkasan / catatan</label>
<textarea class="form-control" id="description" name="description" rows="5" required placeholder="Tuliskan isi pokok dokumen, konteks, dan tindak lanjut."><?= h($editRecord['description'] ?? '') ?></textarea>
</div>
<div class="col-12">
<label class="form-label" for="attachment">Lampiran file</label>
<input class="form-control" id="attachment" type="file" name="attachment" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.mp4,.zip">
<div class="form-text">Format umum dokumen, gambar, video, dan ZIP didukung. Maksimal 8 MB.</div>
<?php if (!empty($editRecord['attachment_name'])): ?>
<div class="inline-note mt-2">Lampiran aktif: <strong><?= h($editRecord['attachment_name']) ?></strong></div>
<?php endif; ?>
</div>
<div class="col-12 d-flex flex-wrap gap-2 align-items-center">
<button type="submit" class="btn btn-dark"><?= $editRecord ? 'Simpan perubahan' : 'Simpan arsip' ?></button>
<?php if ($formFolder): ?>
<a class="btn btn-outline-secondary" href="index.php?folder=<?= urlencode($formFolder) ?>">Lihat folder ini</a>
<?php endif; ?>
<span class="inline-note">Akun aktif: <?= h($user['name']) ?> · unit <?= h($user['unit']) ?></span>
</div>
</form>
</div>
</div>
<div class="col-12 col-xl-3">
<div class="panel h-100 d-flex flex-column gap-4">
<div>
<div class="d-flex justify-content-between align-items-center mb-3 gap-2 flex-wrap">
<div>
<div class="eyebrow">Daftar arsip</div>
<h2 class="section-title mb-0"><?= $folderFilter ? 'Folder aktif' : 'Arsip terbaru' ?></h2>
</div>
<?php if ($folderFilter): ?>
<a href="index.php" class="btn btn-outline-secondary btn-sm">Reset</a>
<?php endif; ?>
</div>
<?php if ($folderFilter): ?>
<div class="inline-note mb-3">Filter: <?= h($folderFilter) ?></div>
<?php endif; ?>
<?php if (!$records): ?>
<div class="empty-state">
<strong>Belum ada arsip.</strong>
<p class="mb-0">Pilih folder dari tree lalu input dokumen pertama untuk mulai membangun database arsip.</p>
</div>
<?php else: ?>
<div class="list-stack">
<?php foreach ($records as $record): ?>
<article class="record-card <?= $selectedRecord && (int) $selectedRecord['id'] === (int) $record['id'] ? 'active' : '' ?>">
<div class="d-flex justify-content-between align-items-start gap-2 mb-2">
<span class="badge <?= h(record_badge_class($record['confidentiality'])) ?>"><?= h($record['confidentiality']) ?></span>
<small class="text-secondary">#<?= (int) $record['id'] ?></small>
</div>
<h3 class="record-title"><?= h($record['title']) ?></h3>
<p class="record-meta mb-2"><?= h($record['reference_code']) ?> · <?= h($record['record_day']) ?>/<?= h($record['record_month']) ?>/<?= h($record['record_year']) ?></p>
<p class="record-path mb-3"><?= h($record['folder_path']) ?></p>
<div class="d-flex gap-2 flex-wrap">
<a href="archive_detail.php?id=<?= (int) $record['id'] ?>" class="btn btn-sm btn-dark">Detail</a>
<?php if (can_edit_record($user, $record)): ?>
<a href="index.php?edit=<?= (int) $record['id'] ?>#arsip-form" class="btn btn-sm btn-outline-secondary">Edit</a>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div>
<div class="eyebrow">Hak akses aktif</div>
<h2 class="section-title mb-3">Kontrol akun</h2>
<ul class="policy-list mb-0">
<?php if ($user['role'] === 'super_admin'): ?>
<li>Melihat, input, edit, unduh, dan hapus semua arsip.</li>
<li>Akses ke seluruh menu utama dan nested folder.</li>
<?php elseif ($user['role'] === 'kepala_bagian'): ?>
<li>Melihat seluruh database yang tersedia di dashboard.</li>
<li>Edit hanya arsip dari unit <?= h($user['unit']) ?>, tanpa fitur hapus.</li>
<?php else: ?>
<li>Input, edit, hapus, dan unduh arsip milik akun sendiri.</li>
<li>Folder input dibatasi ke unit <?= h($user['unit']) ?>.</li>
<?php endif; ?>
</ul>
</div>
</div>
</div>
</div>
</main>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
<?php endif; ?>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>

10
logout.php Normal file
View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/archive_bootstrap.php';
verify_csrf_or_fail();
unset($_SESSION['auth_username']);
session_regenerate_id(true);
flash('success', 'Anda telah keluar dari dashboard arsip.');
header('Location: index.php');