diff --git a/archive_bootstrap.php b/archive_bootstrap.php new file mode 100644 index 0000000..7e574eb --- /dev/null +++ b/archive_bootstrap.php @@ -0,0 +1,527 @@ + 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 = ''; + 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", + }; +} diff --git a/archive_delete.php b/archive_delete.php new file mode 100644 index 0000000..fd66a71 --- /dev/null +++ b/archive_delete.php @@ -0,0 +1,26 @@ +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'); diff --git a/archive_detail.php b/archive_detail.php new file mode 100644 index 0000000..b3eae38 --- /dev/null +++ b/archive_detail.php @@ -0,0 +1,139 @@ + + + + + + + <?= h($record['title']) ?> — <?= h($projectName) ?> + + + + + + + + + + + + + +
+
+
+
+
Detail arsip
+

+
+
+ + Kembali ke dashboard + + Edit + + +
+
+
+ +
+
+
+ +
+
+
+
+
Metadata
+

Informasi dokumen

+
+
Kata kunci
+
Penginput
·
+
Dibuat
+
Diperbarui
+
+
+
+
Lampiran
+

Aksi cepat

+ +
+ +

Lampiran arsip siap diunduh sesuai hak akses.

+
+ + +
+ Belum ada lampiran. +

Arsip ini masih tersimpan sebagai entri metadata dan ringkasan dokumen.

+
+ +
+ +
+ + + +
+ +
+
+
+
+
+ + + + diff --git a/archive_download.php b/archive_download.php new file mode 100644 index 0000000..f6a4f8b --- /dev/null +++ b/archive_download.php @@ -0,0 +1,26 @@ + 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); diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..ecbcece 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,336 @@ -body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; +/* KBRI Harare Archive Desk */ +:root { + --bg: #f4f5f7; + --surface: #ffffff; + --surface-alt: #f8f9fb; + --border: #d7dce3; + --border-strong: #c3cad4; + --text: #101828; + --text-muted: #667085; + --primary: #1f2937; + --primary-soft: #eef1f4; + --accent: #4b5563; + --success: #0f766e; + --warning: #92400e; + --danger: #b42318; + --shadow-sm: 0 10px 30px rgba(16, 24, 40, 0.04); + --shadow-lg: 0 22px 50px rgba(16, 24, 40, 0.08); + --radius-sm: 10px; + --radius-md: 14px; + --radius-lg: 18px; + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2rem; } -.main-wrapper { +html { scroll-behavior: smooth; } +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.5; +} + +a { color: inherit; text-decoration: none; } +a:hover { color: inherit; } + +.auth-shell, +.app-shell { + min-height: 100vh; + background: #f4f5f7; +} + +.topbar { + background: rgba(255,255,255,0.92); + backdrop-filter: blur(10px); +} + +.panel, +.metric-card, +.record-card, +.file-card, +.empty-state { + background: rgba(255,255,255,0.98); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.panel { padding: var(--space-5); } +.panel-hero { + position: relative; + overflow: hidden; + box-shadow: var(--shadow-lg); +} +.panel-hero::after { + content: ""; + position: absolute; + inset: auto -10% -40% auto; + width: 220px; + height: 220px; + border-radius: 50%; + background: rgba(31, 41, 55, 0.05); + box-shadow: inset -24px -24px 40px rgba(255,255,255,0.7), 20px 20px 40px rgba(31,41,55,0.05); +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.72rem; + font-weight: 700; + color: var(--text-muted); +} + +.display-title, +.app-title, +.section-title { + letter-spacing: -0.02em; + font-weight: 700; + color: var(--text); +} +.display-title { font-size: clamp(2rem, 4vw, 3.35rem); max-width: 12ch; } +.app-title { font-size: 1.35rem; } +.section-title { font-size: 1.15rem; } +.lead-copy, +.text-secondary, +.form-text, +.inline-note, +.record-meta, +.record-path, +.policy-list li, +.empty-state p, +.callout, +small { color: var(--text-muted) !important; } + +.badge-soft { + background: var(--primary-soft); + color: var(--primary); + border: 1px solid var(--border); + font-weight: 600; +} +.badge-outline { + background: transparent; + color: var(--text); + border: 1px solid var(--border-strong); + font-weight: 600; +} + +.metric-card { + padding: 1rem 1.1rem; + position: relative; +} +.metric-card::before { + content: ""; + position: absolute; + inset: 12px 12px auto auto; + width: 52px; + height: 52px; + border-radius: 14px; + border: 1px solid rgba(31,41,55,0.08); + background: rgba(255,255,255,0.72); + box-shadow: 12px 12px 25px rgba(31,41,55,0.04), inset -10px -10px 15px rgba(255,255,255,0.9); +} +.metric-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + margin-bottom: 0.4rem; + position: relative; + z-index: 1; +} +.metric-value { + font-size: 1.9rem; + font-weight: 700; + letter-spacing: -0.03em; + position: relative; + z-index: 1; +} +.small-value { font-size: 1.3rem; } + +.callout, +.inline-note, +.file-card, +.empty-state { + padding: 0.9rem 1rem; + border-radius: var(--radius-md); + background: var(--surface-alt); + border: 1px solid var(--border); +} + +.form-control, +.form-select { + border-radius: var(--radius-sm); + border-color: var(--border-strong); + padding: 0.8rem 0.9rem; + color: var(--text); + background-color: #fff; +} +.form-control:focus, +.form-select:focus { + border-color: #98a2b3; + box-shadow: 0 0 0 0.2rem rgba(17,24,39,0.08); +} +.form-label { + font-size: 0.88rem; + font-weight: 600; + color: var(--text); +} + +.btn { + border-radius: 12px; + font-weight: 600; + padding: 0.72rem 1rem; +} +.btn-dark { + background: #111827; + border-color: #111827; + box-shadow: 0 10px 25px rgba(17, 24, 39, 0.18); +} +.btn-dark:hover, +.btn-dark:focus { background: #0b1220; border-color: #0b1220; } +.btn-outline-secondary, +.btn-outline-danger { + border-color: var(--border-strong); +} + +.tree-nav { + max-height: 70vh; + overflow: auto; + padding-right: 0.35rem; +} +.tree-level { padding-left: 0; } +.tree-item + .tree-item { margin-top: 0.35rem; } +.tree-toggle, +.tree-leaf { + width: 100%; display: flex; align-items: center; + gap: 0.7rem; + border: 0; + background: transparent; + padding: 0.65rem 0.7rem; + border-radius: 12px; + text-align: left; +} +.tree-toggle:hover, +.tree-leaf:hover, +.tree-leaf.active { + background: var(--primary-soft); +} +.tree-icon { + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; -} - -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + border: 1px solid var(--border-strong); + background: #fff; font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; + flex: 0 0 22px; } - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); +.tree-toggle.open .tree-icon { content: "−"; } +.tree-toggle.open .tree-icon, +.tree-leaf.active .tree-dot { + background: #111827; color: #fff; - border-bottom-right-radius: 4px; + border-color: #111827; +} +.tree-label { font-size: 0.94rem; color: var(--text); } +.tree-children { + display: none; + margin-left: 0.9rem; + border-left: 1px dashed var(--border); + padding-left: 0.75rem; +} +.tree-children.show { display: block; } +.tree-dot { + width: 10px; + height: 10px; + border-radius: 999px; + border: 1px solid var(--border-strong); + background: #fff; + flex: 0 0 10px; } -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; +.record-card { + padding: 1rem; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; } - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; +.record-card + .record-card { margin-top: 0.85rem; } +.record-card:hover, +.record-card.active { transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); + box-shadow: var(--shadow-lg); + border-color: #b6bec9; } +.record-title { font-size: 1rem; margin-bottom: 0.35rem; } +.record-meta, +.record-path { font-size: 0.84rem; } +.record-path { min-height: 2.4rem; } -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; +.credential-table th, +.credential-table td, +.meta-grid dt, +.meta-grid dd { font-size: 0.9rem; } +.meta-grid div + div { border-top: 1px solid var(--border); } +.meta-grid dt { + color: var(--text-muted); + margin-bottom: 0.25rem; + padding-top: 0.9rem; } - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; +.meta-grid dd { + margin-bottom: 0; font-weight: 600; + padding-bottom: 0.9rem; +} + +.policy-list { + padding-left: 1rem; + display: grid; + gap: 0.55rem; +} +.small-heading { + font-size: 0.82rem; + letter-spacing: 0.08em; text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; - gap: 1rem; -} - -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; font-weight: 700; + color: var(--text-muted); +} +.detail-body { + border-top: 1px solid var(--border); + padding-top: 1rem; } -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; +.toast { + border-radius: 14px; + box-shadow: var(--shadow-sm); } -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; +.no-print { display: block; } + +@media (max-width: 991.98px) { + .panel { padding: 1rem; } + .tree-nav { max-height: none; } + .display-title { max-width: none; } } -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; +@media print { + body { background: #fff; } + .no-print, + .btn, + .topbar { display: none !important; } + .panel, + .metric-card, + .file-card { + box-shadow: none; + border-color: #d1d5db; + background: #fff; + } + .print-panel { padding: 0; border: 0; } } - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..0fde0a0 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,46 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + document.querySelectorAll('.tree-toggle').forEach((toggle) => { + const targetSelector = toggle.getAttribute('data-tree-target'); + const target = targetSelector ? document.querySelector(targetSelector) : null; + const icon = toggle.querySelector('.tree-icon'); + const syncState = () => { + const open = target && target.classList.contains('show'); + toggle.classList.toggle('open', !!open); + toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); + if (icon) { + icon.textContent = open ? '−' : '+'; + } + }; + syncState(); + toggle.addEventListener('click', () => { + if (target) { + target.classList.toggle('show'); + } + syncState(); + }); + }); - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; + document.querySelectorAll('.toast').forEach((el) => { + const toast = new bootstrap.Toast(el, { delay: 4500 }); + toast.show(); + }); - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + const folderSelect = document.getElementById('folder_path'); + const mainMenuSelect = document.getElementById('main_menu'); + if (folderSelect && mainMenuSelect) { + const syncMenuFromFolder = () => { + const selectedOption = folderSelect.options[folderSelect.selectedIndex]; + if (!selectedOption || !selectedOption.value) return; + const mainMenu = selectedOption.value.split(' / ')[0]; + if ([...mainMenuSelect.options].some((option) => option.value === mainMenu)) { + mainMenuSelect.value = mainMenu; + } + }; + folderSelect.addEventListener('change', syncMenuFromFolder); + syncMenuFromFolder(); + } - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } + document.querySelectorAll('[data-print-trigger]').forEach((button) => { + button.addEventListener('click', () => window.print()); }); }); diff --git a/auth_login.php b/auth_login.php new file mode 100644 index 0000000..28920f7 --- /dev/null +++ b/auth_login.php @@ -0,0 +1,21 @@ + '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(); ?> - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + <?= h($projectName) ?> — Arsip Internal + + + + + + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+
+
+
+
+
+
+
Portal Arsip Internal KBRI Harare
+

Arsip terstruktur, aman, dan siap dipakai untuk alur kerja harian.

+

Versi awal ini sudah menyiapkan login internal, struktur menu tree lengkap, input dokumen bertanggal, daftar arsip, detail, edit, print, dan unduh lampiran.

+
+
+
+
Peran awal
+
14 akun
+ 2 super admin, 7 kepala bagian, 5 staf +
+
+
+
+
Kontrol kerja
+
Tree + RBAC
+ Akses folder sesuai unit kerja +
+
+
+
+ Default keamanan tahap awal: semua akun demo memakai password Kbri2026!. Setelah alur disetujui, langkah paling penting berikutnya adalah password individual + audit trail lanjutan. +
+
+
+
+
+
+
+
Masuk
+

Akun internal

+

Gunakan salah satu username resmi di bawah.

+
+ High security MVP +
+ + + +
+ +
+ + +
+
+ + +
+ +
+
+
+

Akun demo siap pakai

+ password sama untuk semua +
+
+ + + + + + + + + $account): ?> + + + + + + +
UsernameRole
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
Internal archive workspace
+

KBRI Harare Archive Desk

+
+
+ + · + Sinkron UTC +
+ + +
+
+
+
+
+ +
+
+
+
+
+
+
Navigasi pohon
+

Menu utama

+
+ + +
+

Klik tanda + untuk membuka sub-menu dan pilih folder terdalam sebagai lokasi arsip.

+ +
+
+ +
+
+
+
+
First delivery
+

Workflow arsip harian dari input sampai detail.

+

Form di bawah terhubung ke database, mengikuti folder tree, menyimpan lampiran, dan menampilkan daftar arsip sesuai hak akses akun yang aktif.

+
+ +
+
+ +
+
+
+
Total arsip
+
+ +
+
+
+
+
Lampiran aktif
+
+ Siap diunduh dari detail arsip +
+
+
+
+
Masuk hari ini
+
+ Dokumen baru per +
+
+
+ +
+
+
+
Form arsip
+

+

Tanggal dipecah menjadi hari, bulan, dan tahun. Folder harus mengikuti menu kerja akun aktif.

+
+ + Batal edit + +
+ + +
+
+
+ +
+
+ + +
+ + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Format umum dokumen, gambar, video, dan ZIP didukung. Maksimal 8 MB.
+ +
Lampiran aktif:
+ +
+
+ + + Lihat folder ini + + Akun aktif: · unit +
+
+
+
+ +
+
+
+
+
+
Daftar arsip
+

+
+ + Reset + +
+ +
Filter:
+ + +
+ Belum ada arsip. +

Pilih folder dari tree lalu input dokumen pertama untuk mulai membangun database arsip.

+
+ +
+ + + +
+ +
+ +
+
Hak akses aktif
+

Kontrol akun

+
    + +
  • Melihat, input, edit, unduh, dan hapus semua arsip.
  • +
  • Akses ke seluruh menu utama dan nested folder.
  • + +
  • Melihat seluruh database yang tersedia di dashboard.
  • +
  • Edit hanya arsip dari unit , tanpa fitur hapus.
  • + +
  • Input, edit, hapus, dan unduh arsip milik akun sendiri.
  • +
  • Folder input dibatasi ke unit .
  • + +
+
+
+
+
+
-
- + + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..3794e6c --- /dev/null +++ b/logout.php @@ -0,0 +1,10 @@ +