Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,527 +0,0 @@
|
|||||||
<?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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<?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');
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<?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
155
archive_save.php
@ -1,155 +0,0 @@
|
|||||||
<?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);
|
|
||||||
@ -1,336 +1,403 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
html { scroll-behavior: smooth; }
|
|
||||||
body {
|
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;
|
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;
|
min-height: 100vh;
|
||||||
background: #f4f5f7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar {
|
.main-wrapper {
|
||||||
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;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
justify-content: center;
|
||||||
border-radius: 8px;
|
min-height: 100vh;
|
||||||
border: 1px solid var(--border-strong);
|
width: 100%;
|
||||||
background: #fff;
|
padding: 20px;
|
||||||
font-weight: 700;
|
box-sizing: border-box;
|
||||||
flex: 0 0 22px;
|
position: relative;
|
||||||
}
|
z-index: 1;
|
||||||
.tree-toggle.open .tree-icon { content: "−"; }
|
|
||||||
.tree-toggle.open .tree-icon,
|
|
||||||
.tree-leaf.active .tree-dot {
|
|
||||||
background: #111827;
|
|
||||||
color: #fff;
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-card {
|
@keyframes gradient {
|
||||||
padding: 1rem;
|
0% {
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
background-position: 0% 50%;
|
||||||
}
|
}
|
||||||
.record-card + .record-card { margin-top: 0.85rem; }
|
50% {
|
||||||
.record-card:hover,
|
background-position: 100% 50%;
|
||||||
.record-card.active {
|
}
|
||||||
transform: translateY(-2px);
|
100% {
|
||||||
box-shadow: var(--shadow-lg);
|
background-position: 0% 50%;
|
||||||
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; }
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.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-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
.detail-body {
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
border-radius: 14px;
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-print { display: block; }
|
|
||||||
|
|
||||||
@media (max-width: 991.98px) {
|
|
||||||
.panel { padding: 1rem; }
|
|
||||||
.tree-nav { max-height: none; }
|
|
||||||
.display-title { max-width: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@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; }
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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%);
|
||||||
|
color: #fff;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.bot {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #212529;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background Animations */
|
||||||
|
.bg-animations {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
font-weight: 600;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add {
|
||||||
|
background: #212529;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
@ -1,46 +1,39 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('.tree-toggle').forEach((toggle) => {
|
const chatForm = document.getElementById('chat-form');
|
||||||
const targetSelector = toggle.getAttribute('data-tree-target');
|
const chatInput = document.getElementById('chat-input');
|
||||||
const target = targetSelector ? document.querySelector(targetSelector) : null;
|
const chatMessages = document.getElementById('chat-messages');
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.toast').forEach((el) => {
|
const appendMessage = (text, sender) => {
|
||||||
const toast = new bootstrap.Toast(el, { delay: 4500 });
|
const msgDiv = document.createElement('div');
|
||||||
toast.show();
|
msgDiv.classList.add('message', sender);
|
||||||
});
|
msgDiv.textContent = text;
|
||||||
|
chatMessages.appendChild(msgDiv);
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
const folderSelect = document.getElementById('folder_path');
|
chatForm.addEventListener('submit', async (e) => {
|
||||||
const mainMenuSelect = document.getElementById('main_menu');
|
e.preventDefault();
|
||||||
if (folderSelect && mainMenuSelect) {
|
const message = chatInput.value.trim();
|
||||||
const syncMenuFromFolder = () => {
|
if (!message) return;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-print-trigger]').forEach((button) => {
|
appendMessage(message, 'visitor');
|
||||||
button.addEventListener('click', () => window.print());
|
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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
<?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
560
index.php
@ -1,432 +1,150 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
@ini_set('display_errors', '1');
|
||||||
|
@error_reporting(E_ALL);
|
||||||
|
@date_default_timezone_set('UTC');
|
||||||
|
|
||||||
require_once __DIR__ . '/archive_bootstrap.php';
|
$phpVersion = PHP_VERSION;
|
||||||
ensure_archive_table();
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
[$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>
|
<!doctype html>
|
||||||
<html lang="id">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title><?= h($projectName) ?> — Arsip Internal</title>
|
<title>New Style</title>
|
||||||
<meta name="description" content="<?= h($projectDescription) ?>">
|
<?php
|
||||||
<meta property="og:title" content="<?= h($projectName) ?>">
|
// Read project preview data from environment
|
||||||
<meta property="og:description" content="<?= h($projectDescription) ?>">
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
<meta property="twitter:title" content="<?= h($projectName) ?>">
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
<meta property="twitter:description" content="<?= h($projectDescription) ?>">
|
?>
|
||||||
<?php if ($projectImageUrl): ?>
|
<?php if ($projectDescription): ?>
|
||||||
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
|
<!-- Meta description -->
|
||||||
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>">
|
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||||
<?php endif; ?>
|
<!-- Open Graph meta tags -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
<!-- 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>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<?php if (!$user): ?>
|
<main>
|
||||||
<main class="auth-shell">
|
<div class="card">
|
||||||
<section class="container py-4 py-lg-5">
|
<h1>Analyzing your requirements and generating your website…</h1>
|
||||||
<div class="row justify-content-center">
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<div class="col-12 col-xl-10">
|
<span class="sr-only">Loading…</span>
|
||||||
<div class="row g-4 align-items-stretch">
|
</div>
|
||||||
<div class="col-lg-7">
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
<div class="panel panel-hero h-100">
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
<div class="eyebrow mb-3">Portal Arsip Internal KBRI Harare</div>
|
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||||
<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>
|
</div>
|
||||||
<?php endif; ?>
|
</main>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
<footer>
|
||||||
<script src="assets/js/main.js?v=<?= time() ?>"></script>
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
10
logout.php
10
logout.php
@ -1,10 +0,0 @@
|
|||||||
<?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');
|
|
||||||
Loading…
x
Reference in New Issue
Block a user