From 17a5ba8e7b50d8468cbb1bd73117a700a6357266 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 25 Mar 2026 07:58:17 +0000 Subject: [PATCH] =?UTF-8?q?arsip=E2=80=91kbri=E2=80=91final?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- archive_bootstrap.php | 679 ++++++++++++++++++++++++++ assets/css/custom.css | 1078 +++++++++++++++++++++++++++-------------- assets/js/main.js | 143 ++++-- document.php | 251 ++++++++++ file.php | 38 ++ index.php | 583 ++++++++++++++++------ 6 files changed, 2242 insertions(+), 530 deletions(-) create mode 100644 archive_bootstrap.php create mode 100644 document.php create mode 100644 file.php diff --git a/archive_bootstrap.php b/archive_bootstrap.php new file mode 100644 index 0000000..ebe4460 --- /dev/null +++ b/archive_bootstrap.php @@ -0,0 +1,679 @@ + $title, + 'description' => $projectDescription, + 'image' => $projectImageUrl, + ]; +} + +function csrf_token(): string +{ + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + + return $_SESSION['csrf_token']; +} + +function verify_csrf(?string $token): bool +{ + $sessionToken = $_SESSION['csrf_token'] ?? ''; + return is_string($token) && $sessionToken !== '' && hash_equals($sessionToken, $token); +} + +function set_flash(string $type, string $message): void +{ + $_SESSION['flashes'][] = [ + 'type' => $type, + 'message' => $message, + ]; +} + +function get_flashes(): array +{ + $flashes = $_SESSION['flashes'] ?? []; + unset($_SESSION['flashes']); + return is_array($flashes) ? $flashes : []; +} + +function demo_users(): array +{ + return [ + 'superadmin@kbriharare.go.id' => [ + 'email' => 'superadmin@kbriharare.go.id', + 'name' => 'Super Admin KBRI', + 'role' => 'super_admin', + 'role_label' => 'Super Admin', + 'department' => 'Semua Departemen', + 'seed_password' => 'Harare2026!', + 'avatar' => 'SA', + ], + 'staf.politik@kbriharare.go.id' => [ + 'email' => 'staf.politik@kbriharare.go.id', + 'name' => 'Staf Politik', + 'role' => 'staff', + 'role_label' => 'Staf', + 'department' => 'Politik', + 'seed_password' => 'Harare2026!', + 'avatar' => 'SP', + ], + 'staf.konsuler@kbriharare.go.id' => [ + 'email' => 'staf.konsuler@kbriharare.go.id', + 'name' => 'Staf Konsuler', + 'role' => 'staff', + 'role_label' => 'Staf', + 'department' => 'Kekonsuleran', + 'seed_password' => 'Harare2026!', + 'avatar' => 'SK', + ], + ]; +} + +function authenticate_demo_user(string $email, string $password): ?array +{ + $key = strtolower(trim($email)); + $users = demo_users(); + + if (!isset($users[$key])) { + return null; + } + + $user = $users[$key]; + $hash = password_hash($user['seed_password'], PASSWORD_BCRYPT); + if (!password_verify($password, $hash)) { + return null; + } + + unset($user['seed_password']); + return $user; +} + +function mask_email(string $email): string +{ + if (!str_contains($email, '@')) { + return $email; + } + + [$local, $domain] = explode('@', $email, 2); + $prefix = substr($local, 0, 2); + return $prefix . str_repeat('•', max(2, strlen($local) - 2)) . '@' . $domain; +} + +function login_locked_until(): int +{ + return (int)($_SESSION['login_locked_until'] ?? 0); +} + +function current_user(): ?array +{ + return isset($_SESSION['auth_user']) && is_array($_SESSION['auth_user']) ? $_SESSION['auth_user'] : null; +} + +function is_authenticated(): bool +{ + return current_user() !== null; +} + +function is_super_admin(): bool +{ + return (current_user()['role'] ?? '') === 'super_admin'; +} + +function process_login_request(): void +{ + if (!verify_csrf($_POST['csrf_token'] ?? null)) { + set_flash('danger', 'Permintaan login tidak valid. Silakan muat ulang halaman.'); + return; + } + + if (time() < login_locked_until()) { + $seconds = login_locked_until() - time(); + set_flash('danger', 'Terlalu banyak percobaan login. Coba lagi dalam ' . max(1, $seconds) . ' detik.'); + return; + } + + $email = trim((string)($_POST['email'] ?? '')); + $password = (string)($_POST['password'] ?? ''); + + if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $password === '') { + set_flash('danger', 'Masukkan email dan password yang valid.'); + return; + } + + $user = authenticate_demo_user($email, $password); + if ($user === null) { + $_SESSION['login_failures'] = (int)($_SESSION['login_failures'] ?? 0) + 1; + if ((int)$_SESSION['login_failures'] >= 5) { + $_SESSION['login_locked_until'] = time() + 120; + $_SESSION['login_failures'] = 0; + } + set_flash('danger', 'Email atau password tidak cocok.'); + return; + } + + unset($_SESSION['login_failures'], $_SESSION['login_locked_until']); + + $otp = (string)random_int(100000, 999999); + $pending = [ + 'user' => $user, + 'otp' => $otp, + 'expires_at' => time() + 300, + 'masked_email' => mask_email($user['email']), + 'mail_sent' => false, + ]; + + $mailSent = false; + try { + $subject = 'Kode OTP Arsip Digital KBRI Harare'; + $html = '

Kode OTP Anda:

' . h($otp) . '

Kode berlaku selama 5 menit.

'; + $result = MailService::sendMail($user['email'], $subject, $html, 'Kode OTP Anda: ' . $otp); + $mailSent = !empty($result['success']); + } catch (Throwable $exception) { + $mailSent = false; + } + + $pending['mail_sent'] = $mailSent; + $_SESSION['pending_auth'] = $pending; + + if ($mailSent) { + set_flash('success', 'Kode OTP telah dikirim ke ' . $pending['masked_email'] . '.'); + } else { + set_flash('warning', 'SMTP belum terpasang untuk pengujian. Gunakan OTP demo ' . $otp . ' untuk melanjutkan.'); + } +} + +function process_otp_verification(): void +{ + if (!verify_csrf($_POST['csrf_token'] ?? null)) { + set_flash('danger', 'Permintaan MFA tidak valid.'); + return; + } + + $pending = $_SESSION['pending_auth'] ?? null; + if (!is_array($pending)) { + set_flash('danger', 'Sesi OTP tidak ditemukan. Silakan login ulang.'); + return; + } + + if ((int)($pending['expires_at'] ?? 0) < time()) { + unset($_SESSION['pending_auth']); + set_flash('danger', 'Kode OTP telah kedaluwarsa. Silakan login ulang.'); + return; + } + + $otp = preg_replace('/\D+/', '', (string)($_POST['otp'] ?? '')); + if ($otp === '' || !hash_equals((string)$pending['otp'], $otp)) { + set_flash('danger', 'Kode OTP tidak valid.'); + return; + } + + $_SESSION['auth_user'] = $pending['user']; + $_SESSION['login_audit'] = [ + 'timestamp' => date('Y-m-d H:i:s'), + 'mfa_method' => 'Email OTP', + ]; + unset($_SESSION['pending_auth']); + session_regenerate_id(true); + set_flash('success', 'Autentikasi berhasil. Selamat datang di brankas digital KBRI Harare.'); +} + +function logout_user(): void +{ + $token = $_SESSION['csrf_token'] ?? null; + $_SESSION = []; + if ($token) { + $_SESSION['csrf_token'] = $token; + } + session_regenerate_id(true); + set_flash('info', 'Sesi Anda telah diakhiri.'); +} + +function require_auth(): void +{ + if (!is_authenticated()) { + header('Location: index.php'); + exit; + } +} + +function archive_folder_tree(): array +{ + return [ + [ + 'group' => 'Informasi Negara', + 'items' => [ + ['folder' => 'Informasi Negara / Zimbabwe', 'label' => 'Zimbabwe', 'hint' => 'Keterangan dasar, informasi penting, catatan peristiwa penting.'], + ['folder' => 'Informasi Negara / Zambia', 'label' => 'Zambia', 'hint' => 'Keterangan dasar, informasi penting, catatan peristiwa penting.'], + ], + ], + [ + 'group' => 'Politik', + 'items' => [ + ['folder' => 'Politik / Nota Diplomatik', 'label' => 'Nota Diplomatik', 'hint' => 'Permintaan dukungan, pernyataan resmi, laporan pertemuan.'], + ['folder' => 'Politik / Perjanjian & Kesepakatan', 'label' => 'Perjanjian & Kesepakatan', 'hint' => 'MoU, agreement bilateral, multilateral.'], + ['folder' => 'Politik / Hubungan Bilateral', 'label' => 'Hubungan Bilateral', 'hint' => 'Laporan menteri, pertukaran delegasi, pernyataan bersama.'], + ['folder' => 'Politik / Kebijakan Strategis', 'label' => 'Kebijakan Strategis', 'hint' => 'Instruksi pusat, analisis geopolitik, rekomendasi.'], + ['folder' => 'Politik / Keamanan & Pertahanan', 'label' => 'Keamanan & Pertahanan', 'hint' => 'MoU pertahanan, contingency plan, intelijen terbatas.'], + ], + ], + [ + 'group' => 'Pensosbud', + 'items' => [ + ['folder' => 'Pensosbud / Program Kerja Sama', 'label' => 'Program Kerja Sama', 'hint' => 'Dokumen kolaborasi dan agenda kerja.'], + ['folder' => 'Pensosbud / Scholarship', 'label' => 'Scholarship', 'hint' => 'Permohonan, persetujuan, monitoring & evaluasi.'], + ['folder' => 'Pensosbud / Event Budaya', 'label' => 'Event Budaya', 'hint' => 'Undangan dan laporan kegiatan budaya.'], + ], + ], + [ + 'group' => 'Ekonomi & Perdagangan', + 'items' => [ + ['folder' => 'Ekonomi & Perdagangan / Nota Kesepahaman Dagang', 'label' => 'Nota Kesepahaman Dagang', 'hint' => 'Zimbabwe, Zambia, Indonesia.'], + ['folder' => 'Ekonomi & Perdagangan / Agreement Investasi', 'label' => 'Agreement Investasi', 'hint' => 'Dokumen investasi lintas negara.'], + ['folder' => 'Ekonomi & Perdagangan / Dukungan Perusahaan', 'label' => 'Dukungan Perusahaan', 'hint' => 'Dukungan perusahaan Indonesia, Zimbabwe, Zambia.'], + ], + ], + [ + 'group' => 'Kekonsuleran', + 'items' => [ + ['folder' => 'Kekonsuleran / Dokumen Perjalanan & Identitas', 'label' => 'Dokumen Perjalanan & Identitas', 'hint' => 'Visa, izin tinggal, SPLP, legalisasi, apostille, pencatatan sipil.'], + ['folder' => 'Kekonsuleran / Legalisasi & Administrasi Dokumen', 'label' => 'Legalisasi & Administrasi Dokumen', 'hint' => 'Administrasi dokumen konsuler.'], + ['folder' => 'Kekonsuleran / Perlindungan & Bantuan WNI', 'label' => 'Perlindungan & Bantuan WNI', 'hint' => 'Kasus perlindungan dan bantuan warga negara.'], + ['folder' => 'Kekonsuleran / Fasilitas Diplomatik & Transportasi', 'label' => 'Fasilitas Diplomatik & Transportasi', 'hint' => 'Fasilitas diplomatik dan logistik perjalanan.'], + ], + ], + [ + 'group' => 'Kanselerai/HOC', + 'items' => [ + ['folder' => 'Kanselerai/HOC / SOP', 'label' => 'SOP', 'hint' => 'Standar operasional prosedur.'], + ['folder' => 'Kanselerai/HOC / Kepegawaian', 'label' => 'Kepegawaian', 'hint' => 'Dokumen personalia dan administrasi SDM.'], + ['folder' => 'Kanselerai/HOC / Perkantoran', 'label' => 'Perkantoran', 'hint' => 'Operasional perkantoran dan fasilitas kerja.'], + ['folder' => 'Kanselerai/HOC / Wisma Dubes', 'label' => 'Wisma Dubes', 'hint' => 'Dokumen rumah dinas dan operasional.'], + ], + ], + [ + 'group' => 'PID', + 'items' => [ + ['folder' => 'PID / Contingency Plan', 'label' => 'Contingency Plan', 'hint' => 'Rencana kontingensi dan respons insiden.'], + ['folder' => 'PID / Komputerisasi', 'label' => 'Komputerisasi', 'hint' => 'Sistem, inventaris TI, dan konfigurasi.'], + ['folder' => 'PID / Pengamanan Terpadu', 'label' => 'Pengamanan Terpadu', 'hint' => 'Pengamanan terpadu dan SOP keamanan.'], + ], + ], + [ + 'group' => 'Administrasi & Internal', + 'items' => [ + ['folder' => 'Administrasi & Internal / Surat Edaran', 'label' => 'Surat Edaran', 'hint' => 'Edaran dan komunikasi internal.'], + ['folder' => 'Administrasi & Internal / Laporan Internal', 'label' => 'Laporan Internal', 'hint' => 'Laporan manajemen dan operasional.'], + ['folder' => 'Administrasi & Internal / Arsip Kepegawaian', 'label' => 'Arsip Kepegawaian', 'hint' => 'Arsip kepegawaian dan administrasi personal.'], + ['folder' => 'Administrasi & Internal / Inventaris & Logistik', 'label' => 'Inventaris & Logistik', 'hint' => 'Persediaan dan logistik internal.'], + ], + ], + [ + 'group' => 'ARSIP', + 'items' => [ + ['folder' => 'ARSIP / File Penting', 'label' => 'File Penting', 'hint' => 'Dokumen prioritas dan referensi inti.'], + ['folder' => 'ARSIP / Photo', 'label' => 'Photo', 'hint' => 'Dokumentasi foto kegiatan dan arsip visual.'], + ['folder' => 'ARSIP / Video', 'label' => 'Video', 'hint' => 'Video resmi dan dokumentasi audiovisual.'], + ], + ], + ]; +} + +function folder_options(): array +{ + $options = []; + foreach (archive_folder_tree() as $section) { + foreach ($section['items'] as $item) { + $options[] = $item['folder']; + } + } + return $options; +} + +function category_options(): array +{ + return [ + 'Nota Diplomatik', + 'Laporan Pertemuan', + 'Perjanjian', + 'Instruksi', + 'Scholarship', + 'Event Budaya', + 'Dokumen Konsuler', + 'SOP', + 'Kepegawaian', + 'Inventaris', + 'File Penting', + 'Photo', + 'Video', + ]; +} + +function upload_root(): string +{ + return __DIR__ . '/uploads/archives'; +} + +function ensure_upload_root(): void +{ + $root = upload_root(); + if (!is_dir($root)) { + mkdir($root, 0775, true); + } +} + +function ensure_archive_schema(): void +{ + ensure_upload_root(); + + db()->exec( + "CREATE TABLE IF NOT EXISTS archive_documents ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(190) NOT NULL, + document_date DATE NOT NULL, + category VARCHAR(120) NOT NULL, + folder_path VARCHAR(255) NOT NULL, + department VARCHAR(120) NOT NULL, + notes TEXT NULL, + attachment_name VARCHAR(255) NOT NULL, + attachment_path VARCHAR(255) NOT NULL, + attachment_ext VARCHAR(16) NOT NULL, + attachment_size INT UNSIGNED NOT NULL, + status ENUM('pending', 'validated') NOT NULL DEFAULT 'pending', + created_by VARCHAR(190) NOT NULL, + created_role VARCHAR(50) NOT NULL, + validated_by VARCHAR(190) DEFAULT NULL, + validation_notes TEXT DEFAULT NULL, + activity_log LONGTEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); +} + +function department_from_folder(string $folder): string +{ + $parts = explode('/', $folder); + return trim($parts[0] ?? 'Umum'); +} + +function append_activity(array $entries, string $action, string $actor, string $notes = ''): array +{ + $entries[] = [ + 'action' => $action, + 'actor' => $actor, + 'notes' => $notes, + 'timestamp' => date('Y-m-d H:i:s'), + ]; + return $entries; +} + +function create_document(array $post, array $files, array $user): bool +{ + if (!verify_csrf($post['csrf_token'] ?? null)) { + set_flash('danger', 'Token formulir unggah tidak valid.'); + return false; + } + + $title = trim((string)($post['title'] ?? '')); + $documentDate = trim((string)($post['document_date'] ?? '')); + $category = trim((string)($post['category'] ?? '')); + $folderPath = trim((string)($post['folder_path'] ?? '')); + $notes = trim((string)($post['notes'] ?? '')); + + if ($title === '' || strlen($title) < 4) { + set_flash('danger', 'Judul dokumen minimal 4 karakter.'); + return false; + } + + if ($documentDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $documentDate)) { + set_flash('danger', 'Tanggal dokumen wajib diisi.'); + return false; + } + + if ($category === '') { + set_flash('danger', 'Kategori dokumen wajib diisi.'); + return false; + } + + if (!in_array($folderPath, folder_options(), true)) { + set_flash('danger', 'Folder tujuan tidak tersedia.'); + return false; + } + + if (!isset($files['attachment']) || !is_array($files['attachment']) || (int)($files['attachment']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + set_flash('danger', 'Lampiran wajib diunggah.'); + return false; + } + + $file = $files['attachment']; + $allowedExtensions = ['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png', 'mp4']; + $originalName = (string)($file['name'] ?? ''); + $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); + + if (!in_array($extension, $allowedExtensions, true)) { + set_flash('danger', 'Format file harus PDF, DOC, DOCX, JPG, PNG, atau MP4.'); + return false; + } + + $size = (int)($file['size'] ?? 0); + if ($size <= 0 || $size > 10 * 1024 * 1024) { + set_flash('danger', 'Ukuran file maksimum 10 MB.'); + return false; + } + + ensure_upload_root(); + $storedName = bin2hex(random_bytes(16)) . '.' . $extension; + $relativePath = 'uploads/archives/' . $storedName; + $targetPath = __DIR__ . '/' . $relativePath; + + if (!move_uploaded_file((string)$file['tmp_name'], $targetPath)) { + set_flash('danger', 'Lampiran gagal disimpan ke server.'); + return false; + } + + $department = department_from_folder($folderPath); + $activity = append_activity([], 'upload', $user['name'], 'Dokumen ditambahkan ke folder ' . $folderPath); + + $sql = 'INSERT INTO archive_documents + (title, document_date, category, folder_path, department, notes, attachment_name, attachment_path, attachment_ext, attachment_size, created_by, created_role, activity_log) + VALUES + (:title, :document_date, :category, :folder_path, :department, :notes, :attachment_name, :attachment_path, :attachment_ext, :attachment_size, :created_by, :created_role, :activity_log)'; + + $stmt = db()->prepare($sql); + $stmt->bindValue(':title', $title); + $stmt->bindValue(':document_date', $documentDate); + $stmt->bindValue(':category', $category); + $stmt->bindValue(':folder_path', $folderPath); + $stmt->bindValue(':department', $department); + $stmt->bindValue(':notes', $notes !== '' ? $notes : null, $notes !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); + $stmt->bindValue(':attachment_name', $originalName); + $stmt->bindValue(':attachment_path', $relativePath); + $stmt->bindValue(':attachment_ext', $extension); + $stmt->bindValue(':attachment_size', $size, PDO::PARAM_INT); + $stmt->bindValue(':created_by', $user['name']); + $stmt->bindValue(':created_role', $user['role']); + $stmt->bindValue(':activity_log', json_encode($activity, JSON_UNESCAPED_UNICODE)); + $stmt->execute(); + + set_flash('success', 'Dokumen berhasil ditambahkan. Menunggu validasi Super Admin.'); + return true; +} + +function get_documents(array $filters = []): array +{ + $where = []; + $params = []; + + if (!empty($filters['status'])) { + $where[] = 'status = :status'; + $params[':status'] = $filters['status']; + } + + if (!empty($filters['folder'])) { + $where[] = 'folder_path = :folder'; + $params[':folder'] = $filters['folder']; + } + + if (!empty($filters['department'])) { + $where[] = 'department = :department'; + $params[':department'] = $filters['department']; + } + + $sql = 'SELECT * FROM archive_documents'; + if ($where) { + $sql .= ' WHERE ' . implode(' AND ', $where); + } + $sql .= ' ORDER BY updated_at DESC'; + + $limit = isset($filters['limit']) ? max(1, min(100, (int)$filters['limit'])) : 20; + $sql .= ' LIMIT ' . $limit; + + $stmt = db()->prepare($sql); + foreach ($params as $key => $value) { + $stmt->bindValue($key, $value); + } + $stmt->execute(); + return $stmt->fetchAll(); +} + +function get_document(int $id): ?array +{ + $stmt = db()->prepare('SELECT * FROM archive_documents WHERE id = :id LIMIT 1'); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + $row = $stmt->fetch(); + return $row ?: null; +} + +function validate_document(int $id, array $user, string $notes = ''): bool +{ + if (!is_super_admin()) { + set_flash('danger', 'Hanya Super Admin yang dapat melakukan validasi dokumen.'); + return false; + } + + if (!verify_csrf($_POST['csrf_token'] ?? null)) { + set_flash('danger', 'Token validasi tidak valid.'); + return false; + } + + $document = get_document($id); + if (!$document) { + set_flash('danger', 'Dokumen tidak ditemukan.'); + return false; + } + + if ($document['status'] === 'validated') { + set_flash('info', 'Dokumen ini sudah tervalidasi.'); + return false; + } + + $activity = json_decode((string)$document['activity_log'], true); + if (!is_array($activity)) { + $activity = []; + } + $activity = append_activity($activity, 'validation', $user['name'], $notes !== '' ? $notes : 'Dokumen disetujui untuk akses staf.'); + + $stmt = db()->prepare('UPDATE archive_documents SET status = :status, validated_by = :validated_by, validation_notes = :validation_notes, activity_log = :activity_log WHERE id = :id'); + $stmt->bindValue(':status', 'validated'); + $stmt->bindValue(':validated_by', $user['name']); + $stmt->bindValue(':validation_notes', $notes !== '' ? $notes : null, $notes !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); + $stmt->bindValue(':activity_log', json_encode($activity, JSON_UNESCAPED_UNICODE)); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + + set_flash('success', 'Dokumen berhasil divalidasi dan dapat diakses staf terkait.'); + return true; +} + +function archive_dashboard_metrics(): array +{ + $totals = db()->query("SELECT + COUNT(*) AS total, + SUM(status = 'pending') AS pending, + SUM(status = 'validated') AS validated, + SUM(DATE(created_at) = CURDATE()) AS today_uploads + FROM archive_documents")->fetch() ?: []; + + return [ + 'total' => (int)($totals['total'] ?? 0), + 'pending' => (int)($totals['pending'] ?? 0), + 'validated' => (int)($totals['validated'] ?? 0), + 'today_uploads' => (int)($totals['today_uploads'] ?? 0), + ]; +} + +function format_filesize(int $bytes): string +{ + if ($bytes >= 1048576) { + return number_format($bytes / 1048576, 1) . ' MB'; + } + if ($bytes >= 1024) { + return number_format($bytes / 1024, 1) . ' KB'; + } + return $bytes . ' B'; +} + +function document_activity(array $document): array +{ + $activity = json_decode((string)($document['activity_log'] ?? '[]'), true); + return is_array($activity) ? $activity : []; +} + +function can_access_document_file(array $document): bool +{ + if (is_super_admin()) { + return true; + } + return ($document['status'] ?? '') === 'validated'; +} + +function attachment_mime(string $extension): string +{ + return match (strtolower($extension)) { + 'pdf' => 'application/pdf', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'jpg', 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'mp4' => 'video/mp4', + default => 'application/octet-stream', + }; +} + +function can_preview_inline(array $document): bool +{ + return in_array(strtolower((string)($document['attachment_ext'] ?? '')), ['pdf', 'jpg', 'jpeg', 'png', 'mp4'], true); +} diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..fa2a173 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,763 @@ +:root { + --bg: #f3f5f7; + --bg-elevated: #ffffff; + --bg-alt: #e9edf1; + --surface: #ffffff; + --surface-strong: #f8fafc; + --text: #111827; + --muted: #667085; + --border: #d7dde5; + --border-strong: #c2ccd7; + --primary: #1f3b57; + --primary-strong: #13283d; + --accent: #5f6f82; + --success: #166534; + --warning: #b45309; + --danger: #b42318; + --shadow: 0 18px 40px rgba(15, 23, 42, 0.08); + --shadow-soft: 0 10px 24px rgba(15, 23, 42, 0.06); + --radius-sm: 10px; + --radius-md: 14px; + --radius-lg: 18px; + --sidebar-width: 340px; +} + +html[data-bs-theme='dark'] { + --bg: #0f141b; + --bg-elevated: #121a23; + --bg-alt: #1a2430; + --surface: #151d27; + --surface-strong: #101720; + --text: #e7edf4; + --muted: #99a7b8; + --border: #243140; + --border-strong: #324457; + --primary: #cbd5e1; + --primary-strong: #f8fafc; + --accent: #7f8ea3; + --shadow: 0 18px 40px rgba(0, 0, 0, 0.35); + --shadow-soft: 0 10px 24px rgba(0, 0, 0, 0.28); +} + +* { + box-sizing: border-box; +} + +html, 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; + min-height: 100%; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +body.archive-app { + margin: 0; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +img, +video, +iframe { + max-width: 100%; } -.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; +a { + color: inherit; } -.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; +.btn, +.form-control, +.form-select, +.dropdown-menu, +.card, +.toast, +.badge, +.alert { + border-radius: var(--radius-sm); } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.btn-primary { + --bs-btn-bg: var(--primary); + --bs-btn-border-color: var(--primary); + --bs-btn-hover-bg: var(--primary-strong); + --bs-btn-hover-border-color: var(--primary-strong); + --bs-btn-active-bg: var(--primary-strong); + --bs-btn-active-border-color: var(--primary-strong); } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +.btn-outline-secondary { + --bs-btn-color: var(--text); + --bs-btn-border-color: var(--border-strong); + --bs-btn-hover-bg: var(--surface-strong); + --bs-btn-hover-border-color: var(--border-strong); + --bs-btn-hover-color: var(--text); } -::-webkit-scrollbar-track { - background: transparent; +.form-control, +.form-select { + border-color: var(--border); + background: var(--surface); + color: var(--text); + min-height: 46px; } -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; +.form-control:focus, +.form-select:focus { + border-color: var(--primary); + box-shadow: 0 0 0 0.2rem rgba(31, 59, 87, 0.15); } -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); +.card, +.dropdown-menu, +.toast { + background: var(--surface); + border: 1px solid var(--border); + box-shadow: var(--shadow-soft); } -.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); +.section-kicker, +.eyebrow { + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.74rem; + font-weight: 700; } -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } +.text-secondary, +.small, +small, +.form-text { + color: var(--muted) !important; } -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; +code { + background: var(--surface-strong); + color: var(--text); + padding: 0.18rem 0.4rem; + border-radius: 8px; + border: 1px solid var(--border); } -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; +.login-shell { + min-height: 100vh; + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(340px, 520px); } -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); +.login-splash { + position: relative; + overflow: hidden; + padding: 48px 56px; + background: #0d141b; + color: #eef4fa; + display: flex; + align-items: flex-end; } -.chat-input-area form { - display: flex; - gap: 0.75rem; +.glass-city { + position: absolute; + inset: 0; + overflow: hidden; + opacity: 0.92; } -.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; +.tower, +.grid-line { + position: absolute; + border: 1px solid rgba(233, 239, 246, 0.18); + background: rgba(220, 230, 239, 0.04); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03), 0 20px 50px rgba(0,0,0,0.28); } -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); +.tower::before { + content: ''; + position: absolute; + inset: 14px; + border: 1px solid rgba(255,255,255,0.06); + opacity: 0.45; } -.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; +.tower-a { + width: 28%; + height: 76%; + bottom: -6%; + left: 6%; + transform: skewY(-8deg); } -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); +.tower-b { + width: 26%; + height: 64%; + bottom: 2%; + left: 30%; + transform: skewY(-6deg); } -/* Background Animations */ -.bg-animations { +.tower-c { + width: 30%; + height: 82%; + right: 10%; + bottom: -8%; + transform: skewY(7deg); +} + +.grid-line { + border: 0; + background: rgba(255,255,255,0.08); +} + +.grid-a { + width: 1px; + top: 0; + bottom: 0; + left: 62%; +} + +.grid-b { + height: 1px; + left: 0; + right: 0; + bottom: 24%; +} + +.login-copy { + position: relative; + z-index: 1; + max-width: 620px; +} + +.login-copy h1 { + font-size: clamp(2.2rem, 4vw, 4rem); + line-height: 1.05; + letter-spacing: -0.04em; + margin: 0 0 1rem; + max-width: 14ch; +} + +.login-copy .lead { + font-size: 1.12rem; + margin-bottom: 0.5rem; + color: #f8fbff; +} + +.muted-copy { + max-width: 58ch; + color: rgba(238, 244, 250, 0.78); +} + +.security-badges { + margin-top: 1.75rem; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.security-badges span { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.65rem 0.85rem; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.12); + border-radius: 999px; + font-size: 0.92rem; +} + +.login-panel { + display: flex; + align-items: center; + justify-content: center; + padding: 32px; + background: var(--bg); +} + +.login-panel .card { + width: 100%; + max-width: 460px; + box-shadow: var(--shadow); +} + +.demo-credentials { + border-top: 1px solid var(--border); + padding-top: 1rem; +} + +.app-shell { + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar-panel { + position: sticky; + top: 0; + height: 100vh; + padding: 20px; + border-right: 1px solid var(--border); + background: var(--bg-elevated); + overflow-y: auto; +} + +.sidebar-top { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + margin-bottom: 1rem; +} + +.folder-tree { + display: grid; + gap: 0.9rem; +} + +.folder-group { + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + overflow: hidden; +} + +.folder-group summary { + list-style: none; + cursor: pointer; + padding: 0.9rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 700; +} + +.folder-group summary::-webkit-details-marker { + display: none; +} + +.folder-group[open] summary i { + transform: rotate(180deg); +} + +.folder-group summary i { + transition: transform 0.2s ease; +} + +.folder-links { + padding: 0 0.85rem 0.85rem; + display: grid; + gap: 0.65rem; +} + +.folder-item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.65rem; + align-items: center; +} + +.folder-select { + text-align: left; + padding: 0.85rem 0.9rem; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface-strong); + color: var(--text); +} + +.folder-select:hover, +.folder-select.active { + border-color: var(--primary); + background: rgba(31, 59, 87, 0.06); +} + +.folder-select small, +.folder-name { + display: block; +} + +.folder-name { + font-weight: 600; + margin-bottom: 0.2rem; +} + +.folder-add { + width: 40px; + height: 40px; + padding: 0; +} + +.app-main { + min-width: 0; +} + +.topbar { + position: sticky; + top: 0; + z-index: 20; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 18px 24px; + border-bottom: 1px solid var(--border); + background: rgba(243, 245, 247, 0.94); + backdrop-filter: blur(12px); +} + +html[data-bs-theme='dark'] .topbar { + background: rgba(15, 20, 27, 0.92); +} + +.topbar-static { + position: sticky; +} + +.profile-chip { + display: inline-flex; + align-items: center; + gap: 0.75rem; + border: 1px solid var(--border); + background: var(--surface); + padding: 0.35rem 0.5rem 0.35rem 0.35rem; + color: var(--text); +} + +.profile-chip.static-chip { + cursor: default; +} + +.avatar { + width: 38px; + height: 38px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + background: var(--primary); + color: #fff; +} + +html[data-bs-theme='dark'] .avatar { + background: var(--primary-strong); + color: #111827; +} + +.content-grid { + padding: 24px; + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(300px, 0.8fr); + gap: 1.25rem; +} + +.content-grid > .hero-panel, +.content-grid > .table-card { + grid-column: 1 / -1; +} + +.display-title { + margin: 0; + font-size: clamp(1.7rem, 2vw, 2.45rem); + line-height: 1.08; + letter-spacing: -0.04em; + max-width: 18ch; +} + +.session-panel { + display: grid; + grid-template-columns: repeat(2, minmax(120px, 1fr)); + gap: 0.85rem; + min-width: min(100%, 340px); +} + +.metric-box { + padding: 1rem; + background: var(--surface-strong); + border: 1px solid var(--border); + border-radius: var(--radius-md); +} + +.metric-box span { + display: block; + color: var(--muted); + font-size: 0.86rem; + margin-bottom: 0.35rem; +} + +.metric-box strong { + font-size: 1.45rem; +} + +.dropzone { + border: 1px dashed var(--border-strong); + border-radius: var(--radius-md); + background: var(--surface-strong); + min-height: 148px; + display: grid; + place-items: center; + text-align: center; + padding: 1.2rem; + cursor: pointer; + transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease; +} + +.dropzone i { + font-size: 1.4rem; + display: inline-flex; + margin-bottom: 0.55rem; + color: var(--primary); +} + +.dropzone.dragover { + border-color: var(--primary); + background: rgba(31, 59, 87, 0.08); + transform: translateY(-1px); +} + +.queue-list { + display: grid; + gap: 0.75rem; +} + +.queue-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + text-decoration: none; + padding: 0.95rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface-strong); +} + +.queue-item:hover { + border-color: var(--primary); +} + +.queue-item strong, +.queue-item span { + display: block; +} + +.search-box { + position: relative; + max-width: 320px; + width: 100%; +} + +.search-box i { + position: absolute; + left: 0.85rem; + top: 50%; + transform: translateY(-50%); + color: var(--muted); +} + +.search-box input { + padding-left: 2.6rem; +} + +.archive-table th { + font-size: 0.82rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); +} + +.archive-table td, +.archive-table th { + padding: 1rem 0.75rem; + border-color: var(--border); + background: transparent; +} + +.status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 112px; + padding: 0.42rem 0.72rem; + border-radius: 999px; + font-size: 0.82rem; + font-weight: 700; + border: 1px solid transparent; +} + +.status-pill.pending { + color: var(--warning); + background: rgba(180, 83, 9, 0.1); + border-color: rgba(180, 83, 9, 0.18); +} + +.status-pill.validated { + color: var(--success); + background: rgba(22, 101, 52, 0.1); + border-color: rgba(22, 101, 52, 0.18); +} + +.security-list { + display: grid; + gap: 0.85rem; +} + +.security-list li { + display: flex; + gap: 0.7rem; + align-items: flex-start; +} + +.security-list i { + color: var(--primary); + margin-top: 0.15rem; +} + +.empty-panel { + min-height: 148px; + border: 1px dashed var(--border-strong); + border-radius: var(--radius-md); + background: var(--surface-strong); + display: grid; + place-items: center; + text-align: center; + padding: 1.5rem; +} + +.empty-panel.tall { + min-height: 240px; +} + +.empty-panel i { + font-size: 1.6rem; + color: var(--muted); + margin-bottom: 0.7rem; +} + +.detail-layout { + max-width: 1460px; +} + +.detail-meta strong, +.meta-label { + display: block; +} + +.meta-label { + color: var(--muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 0.35rem; +} + +.annotation-box { + padding: 1rem 1.1rem; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface-strong); +} + +.preview-frame { + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; + background: var(--surface-strong); +} + +.preview-iframe { + width: 100%; + min-height: 640px; + border: 0; + background: #fff; +} + +.timeline { + display: grid; + gap: 1rem; +} + +.timeline-item { + display: grid; + grid-template-columns: 18px minmax(0, 1fr); + gap: 0.9rem; + align-items: start; +} + +.timeline-dot { + width: 12px; + height: 12px; + border-radius: 50%; + margin-top: 0.35rem; + background: var(--primary); + box-shadow: 0 0 0 4px rgba(31, 59, 87, 0.12); +} + +.theme-toggle { + min-width: 42px; +} + +.sidebar-open .sidebar-panel { + transform: translateX(0); +} + +@media (max-width: 1199.98px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar-panel { position: fixed; - top: 0; - left: 0; + z-index: 40; + width: min(92vw, 360px); + transform: translateX(-102%); + transition: transform 0.24s ease; + box-shadow: var(--shadow); + } + + .content-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 991.98px) { + .login-shell { + grid-template-columns: 1fr; + } + + .login-splash { + min-height: 50vh; + padding: 32px 24px; + } + + .login-panel { + padding: 20px; + } + + .topbar { + padding: 16px; + } + + .content-grid { + padding: 16px; + } +} + +@media (max-width: 575.98px) { + .login-copy h1 { + max-width: none; + } + + .session-panel { + grid-template-columns: 1fr 1fr; + } + + .hero-actions, + .detail-actions { width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; + } + + .hero-actions .btn, + .detail-actions .btn { + flex: 1 1 auto; + } } - -.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; -} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..824b31c 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,110 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); - - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - 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'); - } + const root = document.documentElement; + const storedTheme = localStorage.getItem('archive-theme'); + const applyTheme = (theme) => { + root.setAttribute('data-bs-theme', theme); + document.querySelectorAll('[data-theme-toggle]').forEach((button) => { + const icon = button.querySelector('i'); + if (icon) { + icon.className = theme === 'dark' ? 'bi bi-sun' : 'bi bi-moon-stars'; + } }); + }; + + applyTheme(storedTheme === 'dark' ? 'dark' : 'light'); + + document.querySelectorAll('[data-theme-toggle]').forEach((button) => { + button.addEventListener('click', () => { + const nextTheme = root.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark'; + localStorage.setItem('archive-theme', nextTheme); + applyTheme(nextTheme); + }); + }); + + document.querySelectorAll('.toast').forEach((toastElement) => { + const toast = new bootstrap.Toast(toastElement); + toast.show(); + }); + + const sidebarToggles = document.querySelectorAll('[data-sidebar-toggle]'); + sidebarToggles.forEach((button) => { + button.addEventListener('click', () => { + document.body.classList.toggle('sidebar-open'); + }); + }); + + const folderSelect = document.getElementById('folder_path'); + const categoryInput = document.getElementById('category'); + document.querySelectorAll('[data-folder-select]').forEach((button) => { + button.addEventListener('click', () => { + const folder = button.getAttribute('data-folder') || ''; + const category = button.getAttribute('data-category') || ''; + if (folderSelect) { + folderSelect.value = folder; + folderSelect.dispatchEvent(new Event('change')); + folderSelect.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + if (categoryInput && categoryInput.value.trim() === '') { + categoryInput.value = category; + } + document.querySelectorAll('.folder-select').forEach((node) => node.classList.remove('active')); + const cardButton = button.classList.contains('folder-add') ? button.previousElementSibling : button; + if (cardButton) { + cardButton.classList.add('active'); + } + document.getElementById('uploadCard')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + document.body.classList.remove('sidebar-open'); + }); + }); + + const dropzone = document.querySelector('[data-dropzone]'); + const fileInput = document.getElementById('attachment'); + const fileLabel = document.querySelector('[data-file-label]'); + + const updateFileLabel = (files) => { + if (!fileLabel) return; + if (!files || !files.length) { + fileLabel.textContent = 'PDF, DOC, DOCX, JPG, PNG, MP4 · maksimum 10 MB'; + return; + } + const file = files[0]; + fileLabel.textContent = `${file.name} · ${Math.max(1, Math.round(file.size / 1024))} KB`; + }; + + if (dropzone && fileInput) { + dropzone.addEventListener('click', () => fileInput.click()); + fileInput.addEventListener('change', () => updateFileLabel(fileInput.files)); + + ['dragenter', 'dragover'].forEach((eventName) => { + dropzone.addEventListener(eventName, (event) => { + event.preventDefault(); + dropzone.classList.add('dragover'); + }); + }); + + ['dragleave', 'drop'].forEach((eventName) => { + dropzone.addEventListener(eventName, (event) => { + event.preventDefault(); + dropzone.classList.remove('dragover'); + }); + }); + + dropzone.addEventListener('drop', (event) => { + const files = event.dataTransfer?.files; + if (!files || !files.length) return; + fileInput.files = files; + updateFileLabel(files); + }); + } + + const searchInput = document.getElementById('documentSearch'); + if (searchInput) { + searchInput.addEventListener('input', () => { + const query = searchInput.value.trim().toLowerCase(); + document.querySelectorAll('[data-search-row]').forEach((row) => { + const haystack = row.getAttribute('data-search-row') || ''; + row.style.display = haystack.includes(query) ? '' : 'none'; + }); + }); + } }); diff --git a/document.php b/document.php new file mode 100644 index 0000000..7247545 --- /dev/null +++ b/document.php @@ -0,0 +1,251 @@ + 0 ? get_document($documentId) : null; + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'validate_document' && $document) { + validate_document($documentId, current_user(), trim((string)($_POST['validation_notes'] ?? ''))); + header('Location: document.php?id=' . $documentId); + exit; +} + +$document = $documentId > 0 ? get_document($documentId) : null; +if (!$document) { + http_response_code(404); +} + +$meta = page_meta('Detail Arsip – KBRI Harare', 'Detail arsip digital, validasi, dan akses pratinjau aman KBRI Harare.'); +$flashes = get_flashes(); +$user = current_user(); +$activity = $document ? document_activity($document) : []; +$canAccessFile = $document ? can_access_document_file($document) : false; +$canPreview = $document ? ($canAccessFile && can_preview_inline($document)) : false; +?> + + + + + + <?= h($meta['title']) ?> + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+ Kembali +
+

Detail arsip

+

Pusat validasi & pratinjau dokumen

+
+
+
+ +
+ + + + + +
+
+
+ +
+ +
+
+ +

Dokumen tidak ditemukan

+

ID arsip tidak tersedia atau telah berubah.

+ Kembali ke dashboard +
+
+ +
+
+
+
+
+
+
+ + +
+

+

+
+
+ + Unduh + + Pratinjau + + + +
+
+ +
+
+ Tanggal dokumen + +
+
+ Departemen + +
+
+ Diunggah oleh + +
+
+ Lampiran + · +
+
+ + +
+ Catatan dokumen +

+
+ +
+
+ +
+
+
+
+

Pratinjau aman

+

Akses file sesuai status validasi

+
+ +
+ + +
+ + Pratinjau <?= h($document['title']) ?> + + + + + +
+ +
+ +

Jenis file tidak mendukung pratinjau inline.

+

Unduh lampiran untuk membuka dokumen ini.

+
+ +
+ +

Pratinjau dikunci sampai validasi selesai.

+

Setelah disetujui Super Admin, staf dapat pratinjau, unduh, dan cetak dari halaman ini.

+
+ +
+
+
+ +
+ +
+
+

Aksi Super Admin

+

Validasi dokumen

+

Setelah disetujui, dokumen terbuka untuk pratinjau, unduh, dan cetak oleh staf terkait.

+
+ + +
+ + +
+ +
+
+
+ + +
+
+

Status akses

+

Ringkasan kepatuhan

+
    +
  • Password user demo diproses dengan verifikasi Bcrypt.
  • +
  • Form terlindungi CSRF token dan prepared statements PDO.
  • +
  • File hanya di-stream melalui gerbang aplikasi, bukan tautan publik langsung.
  • +
  • +
+
+
+ +
+
+

Audit trail

+

Riwayat aktivitas

+
+ +
+ +
+ + · + +

+ +
+
+ +
+
+
+
+
+ +
+ + + + + diff --git a/file.php b/file.php new file mode 100644 index 0000000..33a7234 --- /dev/null +++ b/file.php @@ -0,0 +1,38 @@ + 0 ? get_document($documentId) : null; + +if (!$document) { + http_response_code(404); + echo 'Dokumen tidak ditemukan.'; + exit; +} + +if (!can_access_document_file($document)) { + http_response_code(403); + echo 'Dokumen belum tersedia untuk diakses.'; + exit; +} + +$filePath = __DIR__ . '/' . ltrim((string)$document['attachment_path'], '/'); +if (!is_file($filePath)) { + http_response_code(404); + echo 'Lampiran tidak ditemukan di server.'; + exit; +} + +header('Content-Type: ' . attachment_mime((string)$document['attachment_ext'])); +header('Content-Length: ' . filesize($filePath)); +header('Content-Disposition: ' . $disposition . '; filename="' . rawurlencode((string)$document['attachment_name']) . '"'); +header('X-Content-Type-Options: nosniff'); +readfile($filePath); +exit; diff --git a/index.php b/index.php index 7205f3d..4c5de22 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,463 @@ 0, 'pending' => 0, 'validated' => 0, 'today_uploads' => 0]; +$recentDocuments = is_authenticated() ? get_documents(['limit' => 8]) : []; +$pendingDocuments = is_authenticated() ? get_documents(['status' => 'pending', 'limit' => 5]) : []; +$loginAudit = $_SESSION['login_audit'] ?? null; ?> - + - New Style - - - - - - - - - - - - - - - + <?= h($meta['title']) ?> + + + + + + + + + + + - - + + + + - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… + +
+ + + +
+ + +
+ + + +
+ +
+ + +
+
+
+ +
+

Sistem Informasi Arsip Digital

+

KBRI Harare Secure Vault

+
+
+
+ + +
+
+ +
+
+
+
+
+

Workflow MVP siap dipakai

+

Unggah → Validasi → Pratinjau aman → Riwayat audit

+

Staf dapat menempatkan dokumen pada folder yang tepat, sementara Super Admin memvalidasi sebelum dokumen dapat dipratinjau, diunduh, atau dicetak.

+ +
+
+
+ Total arsip + +
+
+ Menunggu validasi + +
+
+ Tervalidasi + +
+
+ Upload hari ini + +
+
+
+
+
+ +
+
+
+
+

Input dokumen

+

Tambah dokumen ke folder terpilih

+

Gunakan drag-and-drop, isi metadata inti, lalu sistem menyimpan arsip ke folder terkait secara otomatis.

+
+ CSRF aktif +
+
+ + +
+ + +
+
+ + +
+
+ + + + + + + +
+
+ + +
Klik folder di sidebar untuk mengisi otomatis.
+
+
+ +
+ +
+ +

Tarik file ke sini atau klik untuk memilih

+ PDF, DOC, DOCX, JPG, PNG, MP4 · maksimum 10 MB +
+
+
+
+ + +
+
+
Akses staf untuk pratinjau/unduh/cetak akan terbuka setelah validasi Super Admin.
+ +
+
+
+
+ +
+
+
+
+

Kontrol akses

+

Status sesi & keamanan

+
+ +
+
    +
  • Login terakhir:
  • +
  • MFA:
  • +
  • Hak akses:
  • +
  • Proteksi aktif: prepared statements, CSRF token, dan rate limiting sesi.
  • +
+
+
+ +
+
+
+
+

Validasi dokumen

+

Queue persetujuan

+
+ pending +
+ + + +
+ +

Belum ada dokumen yang menunggu validasi.

+
+ +
+
+ +
+
+
+
+

Daftar arsip

+

Dokumen terbaru

+

Setiap arsip memiliki detail metadata, status validasi, dan jejak audit.

+
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
DokumenFolderUploadStatusAksi
+ + · + + + + Buka detail
+
+ +
+ +

Belum ada arsip.

+

Mulai dengan memilih folder di sidebar lalu unggah dokumen pertama Anda.

+
+ +
+
+
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
- + + + +