Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17a5ba8e7b |
679
archive_bootstrap.php
Normal file
679
archive_bootstrap.php
Normal file
@ -0,0 +1,679 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
@date_default_timezone_set('Africa/Harare');
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/mail/MailService.php';
|
||||
|
||||
function h(?string $value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function app_env(string $key, string $default = ''): string
|
||||
{
|
||||
$serverValue = $_SERVER[$key] ?? null;
|
||||
if (is_string($serverValue) && $serverValue !== '') {
|
||||
return $serverValue;
|
||||
}
|
||||
|
||||
$envValue = getenv($key);
|
||||
return is_string($envValue) && $envValue !== '' ? $envValue : $default;
|
||||
}
|
||||
|
||||
function page_meta(string $title, string $description = ''): array
|
||||
{
|
||||
$projectDescription = app_env('PROJECT_DESCRIPTION', $description);
|
||||
$projectImageUrl = app_env('PROJECT_IMAGE_URL', '');
|
||||
|
||||
return [
|
||||
'title' => $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 = '<p>Kode OTP Anda:</p><p style="font-size:24px;font-weight:700;letter-spacing:4px;">' . h($otp) . '</p><p>Kode berlaku selama 5 menit.</p>';
|
||||
$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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
251
document.php
Normal file
251
document.php
Normal file
@ -0,0 +1,251 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/archive_bootstrap.php';
|
||||
|
||||
ensure_archive_schema();
|
||||
require_auth();
|
||||
|
||||
$documentId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$document = $documentId > 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;
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="id" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title><?= h($meta['title']) ?></title>
|
||||
<?php if ($meta['description'] !== ''): ?>
|
||||
<meta name="description" content="<?= h($meta['description']) ?>" />
|
||||
<meta property="og:description" content="<?= h($meta['description']) ?>" />
|
||||
<meta property="twitter:description" content="<?= h($meta['description']) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($meta['image'] !== ''): ?>
|
||||
<meta property="og:image" content="<?= h($meta['image']) ?>" />
|
||||
<meta property="twitter:image" content="<?= h($meta['image']) ?>" />
|
||||
<?php endif; ?>
|
||||
<meta property="og:title" content="<?= h($meta['title']) ?>" />
|
||||
<meta property="twitter:title" content="<?= h($meta['title']) ?>" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= urlencode((string)filemtime(__DIR__ . '/assets/css/custom.css')) ?>">
|
||||
</head>
|
||||
<body class="archive-app app-authenticated detail-page">
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3">
|
||||
<?php foreach ($flashes as $flash): ?>
|
||||
<div class="toast align-items-center text-bg-<?= h($flash['type']) ?> border-0 mb-2" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="4500">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body"><?= h($flash['message']) ?></div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<header class="topbar topbar-static">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<a href="index.php" class="btn btn-outline-secondary btn-sm"><i class="bi bi-arrow-left me-2"></i>Kembali</a>
|
||||
<div>
|
||||
<p class="section-kicker mb-1">Detail arsip</p>
|
||||
<h1 class="h4 mb-0">Pusat validasi & pratinjau dokumen</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 gap-lg-3">
|
||||
<button class="btn btn-outline-secondary btn-sm theme-toggle" type="button" data-theme-toggle>
|
||||
<i class="bi bi-moon-stars"></i>
|
||||
</button>
|
||||
<div class="profile-chip static-chip">
|
||||
<span class="avatar"><?= h($user['avatar'] ?? 'U') ?></span>
|
||||
<span class="text-start d-none d-md-inline-block">
|
||||
<strong class="d-block"><?= h($user['name']) ?></strong>
|
||||
<small class="text-secondary"><?= h($user['role_label'] ?? '') ?></small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="detail-layout container-fluid px-3 px-lg-4 py-4">
|
||||
<?php if (!$document): ?>
|
||||
<section class="card border-0 shadow-sm">
|
||||
<div class="card-body p-5 text-center">
|
||||
<i class="bi bi-file-earmark-x display-5 text-secondary"></i>
|
||||
<h2 class="h4 mt-3">Dokumen tidak ditemukan</h2>
|
||||
<p class="text-secondary">ID arsip tidak tersedia atau telah berubah.</p>
|
||||
<a href="index.php" class="btn btn-primary">Kembali ke dashboard</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-12 col-xl-8">
|
||||
<section class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||
<span class="status-pill <?= $document['status'] === 'validated' ? 'validated' : 'pending' ?>"><?= $document['status'] === 'validated' ? 'Tervalidasi' : 'Menunggu Validasi' ?></span>
|
||||
<span class="badge text-bg-secondary-subtle text-secondary-emphasis border"><?= h($document['category']) ?></span>
|
||||
</div>
|
||||
<h2 class="h3 mb-1"><?= h($document['title']) ?></h2>
|
||||
<p class="text-secondary mb-0"><?= h($document['folder_path']) ?></p>
|
||||
</div>
|
||||
<div class="detail-actions d-flex flex-wrap gap-2">
|
||||
<?php if ($canAccessFile): ?>
|
||||
<a href="file.php?id=<?= (int)$document['id'] ?>&disposition=download" class="btn btn-outline-secondary"><i class="bi bi-download me-2"></i>Unduh</a>
|
||||
<?php if ($canPreview): ?>
|
||||
<a href="file.php?id=<?= (int)$document['id'] ?>&disposition=inline" class="btn btn-outline-secondary" target="_blank" rel="noopener"><i class="bi bi-eye me-2"></i>Pratinjau</a>
|
||||
<button class="btn btn-primary" type="button" onclick="window.print()"><i class="bi bi-printer me-2"></i>Cetak</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 detail-meta">
|
||||
<div class="col-6 col-lg-3">
|
||||
<span class="meta-label">Tanggal dokumen</span>
|
||||
<strong><?= h(date('d M Y', strtotime((string)$document['document_date']))) ?></strong>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<span class="meta-label">Departemen</span>
|
||||
<strong><?= h($document['department']) ?></strong>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<span class="meta-label">Diunggah oleh</span>
|
||||
<strong><?= h($document['created_by']) ?></strong>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<span class="meta-label">Lampiran</span>
|
||||
<strong><?= h(strtoupper((string)$document['attachment_ext'])) ?> · <?= h(format_filesize((int)$document['attachment_size'])) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($document['notes']): ?>
|
||||
<div class="annotation-box mt-4">
|
||||
<span class="meta-label">Catatan dokumen</span>
|
||||
<p class="mb-0"><?= nl2br(h($document['notes'])) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||||
<div>
|
||||
<p class="section-kicker mb-2">Pratinjau aman</p>
|
||||
<h2 class="h5 mb-1">Akses file sesuai status validasi</h2>
|
||||
</div>
|
||||
<span class="badge text-bg-dark"><?= $canAccessFile ? 'Akses dibuka' : 'Terkunci' ?></span>
|
||||
</div>
|
||||
|
||||
<?php if ($canPreview): ?>
|
||||
<div class="preview-frame">
|
||||
<?php if (in_array(strtolower((string)$document['attachment_ext']), ['jpg', 'jpeg', 'png'], true)): ?>
|
||||
<img src="file.php?id=<?= (int)$document['id'] ?>&disposition=inline" alt="Pratinjau <?= h($document['title']) ?>" class="img-fluid rounded-3 w-100">
|
||||
<?php elseif (strtolower((string)$document['attachment_ext']) === 'mp4'): ?>
|
||||
<video controls class="w-100 rounded-3" preload="metadata">
|
||||
<source src="file.php?id=<?= (int)$document['id'] ?>&disposition=inline" type="video/mp4">
|
||||
</video>
|
||||
<?php else: ?>
|
||||
<iframe src="file.php?id=<?= (int)$document['id'] ?>&disposition=inline" title="Pratinjau dokumen" class="preview-iframe"></iframe>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php elseif ($canAccessFile): ?>
|
||||
<div class="empty-panel tall">
|
||||
<i class="bi bi-file-earmark-lock2"></i>
|
||||
<p class="mb-1 fw-semibold">Jenis file tidak mendukung pratinjau inline.</p>
|
||||
<p class="mb-0 text-secondary small">Unduh lampiran untuk membuka dokumen ini.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty-panel tall">
|
||||
<i class="bi bi-shield-lock"></i>
|
||||
<p class="mb-1 fw-semibold">Pratinjau dikunci sampai validasi selesai.</p>
|
||||
<p class="mb-0 text-secondary small">Setelah disetujui Super Admin, staf dapat pratinjau, unduh, dan cetak dari halaman ini.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-4">
|
||||
<?php if (is_super_admin() && $document['status'] !== 'validated'): ?>
|
||||
<section class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<p class="section-kicker mb-2">Aksi Super Admin</p>
|
||||
<h2 class="h5 mb-1">Validasi dokumen</h2>
|
||||
<p class="text-secondary small">Setelah disetujui, dokumen terbuka untuk pratinjau, unduh, dan cetak oleh staf terkait.</p>
|
||||
<form method="post" class="vstack gap-3 mt-3">
|
||||
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="validate_document">
|
||||
<div>
|
||||
<label class="form-label" for="validation_notes">Catatan validasi</label>
|
||||
<textarea id="validation_notes" class="form-control" name="validation_notes" rows="4" placeholder="Contoh: Metadata sesuai, siap diakses unit kerja."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-patch-check me-2"></i>Validasi dokumen</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<p class="section-kicker mb-2">Status akses</p>
|
||||
<h2 class="h5 mb-1">Ringkasan kepatuhan</h2>
|
||||
<ul class="list-unstyled security-list mb-0">
|
||||
<li><i class="bi bi-check2-circle"></i><span>Password user demo diproses dengan verifikasi Bcrypt.</span></li>
|
||||
<li><i class="bi bi-check2-circle"></i><span>Form terlindungi CSRF token dan prepared statements PDO.</span></li>
|
||||
<li><i class="bi bi-check2-circle"></i><span>File hanya di-stream melalui gerbang aplikasi, bukan tautan publik langsung.</span></li>
|
||||
<li><i class="bi bi-check2-circle"></i><span><?= $document['status'] === 'validated' ? 'Dokumen siap diakses sesuai hak role.' : 'Dokumen menunggu otorisasi Super Admin.' ?></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<p class="section-kicker mb-2">Audit trail</p>
|
||||
<h2 class="h5 mb-1">Riwayat aktivitas</h2>
|
||||
<div class="timeline mt-3">
|
||||
<?php foreach ($activity as $entry): ?>
|
||||
<article class="timeline-item">
|
||||
<span class="timeline-dot"></span>
|
||||
<div>
|
||||
<strong class="d-block text-capitalize"><?= h($entry['action'] ?? '') ?></strong>
|
||||
<small class="text-secondary d-block"><?= h($entry['actor'] ?? '') ?> · <?= h($entry['timestamp'] ?? '') ?></small>
|
||||
<?php if (!empty($entry['notes'])): ?>
|
||||
<p class="mb-0 mt-2 text-secondary small"><?= h($entry['notes']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?= urlencode((string)filemtime(__DIR__ . '/assets/js/main.js')) ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
file.php
Normal file
38
file.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/archive_bootstrap.php';
|
||||
|
||||
ensure_archive_schema();
|
||||
require_auth();
|
||||
|
||||
$documentId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$disposition = (string)($_GET['disposition'] ?? 'inline');
|
||||
$disposition = $disposition === 'download' ? 'attachment' : 'inline';
|
||||
$document = $documentId > 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;
|
||||
583
index.php
583
index.php
@ -1,150 +1,463 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
require_once __DIR__ . '/archive_bootstrap.php';
|
||||
|
||||
ensure_archive_schema();
|
||||
|
||||
if (isset($_GET['reset_otp'])) {
|
||||
unset($_SESSION['pending_auth']);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
|
||||
if ($action === 'login_request') {
|
||||
process_login_request();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'verify_otp') {
|
||||
process_otp_verification();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'logout') {
|
||||
if (verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||
logout_user();
|
||||
} else {
|
||||
set_flash('danger', 'Permintaan logout tidak valid.');
|
||||
}
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'upload_document' && is_authenticated()) {
|
||||
create_document($_POST, $_FILES, current_user());
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$meta = page_meta('Sistem Informasi Arsip Digital – KBRI Harare', 'Sistem arsip digital ultra-aman untuk pengelolaan dokumen diplomatik, konsuler, dan internal KBRI Harare.');
|
||||
$flashes = get_flashes();
|
||||
$user = current_user();
|
||||
$pendingAuth = $_SESSION['pending_auth'] ?? null;
|
||||
$folderTree = archive_folder_tree();
|
||||
$folderOptions = folder_options();
|
||||
$categoryOptions = category_options();
|
||||
$selectedFolder = isset($_GET['folder']) && in_array($_GET['folder'], $folderOptions, true) ? (string)$_GET['folder'] : ($folderOptions[0] ?? '');
|
||||
$dashboardMetrics = is_authenticated() ? archive_dashboard_metrics() : ['total' => 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;
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="id" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<title><?= h($meta['title']) ?></title>
|
||||
<?php if ($meta['description'] !== ''): ?>
|
||||
<meta name="description" content="<?= h($meta['description']) ?>" />
|
||||
<meta property="og:description" content="<?= h($meta['description']) ?>" />
|
||||
<meta property="twitter:description" content="<?= h($meta['description']) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($meta['image'] !== ''): ?>
|
||||
<meta property="og:image" content="<?= h($meta['image']) ?>" />
|
||||
<meta property="twitter:image" content="<?= h($meta['image']) ?>" />
|
||||
<?php endif; ?>
|
||||
<meta property="og:title" content="<?= h($meta['title']) ?>" />
|
||||
<meta property="twitter:title" content="<?= h($meta['title']) ?>" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= urlencode((string)filemtime(__DIR__ . '/assets/css/custom.css')) ?>">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<body class="archive-app <?= $user ? 'app-authenticated' : 'app-guest' ?>">
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3">
|
||||
<?php foreach ($flashes as $flash): ?>
|
||||
<div class="toast align-items-center text-bg-<?= h($flash['type']) ?> border-0 mb-2" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="4500">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body"><?= h($flash['message']) ?></div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!$user): ?>
|
||||
<main class="login-shell">
|
||||
<section class="login-splash">
|
||||
<div class="glass-city" aria-hidden="true">
|
||||
<span class="tower tower-a"></span>
|
||||
<span class="tower tower-b"></span>
|
||||
<span class="tower tower-c"></span>
|
||||
<span class="grid-line grid-a"></span>
|
||||
<span class="grid-line grid-b"></span>
|
||||
</div>
|
||||
<div class="login-copy">
|
||||
<span class="eyebrow">Sistem Informasi Arsip Digital</span>
|
||||
<h1>KEDUTAAN BESAR REPUBLIK INDONESIA HARARE</h1>
|
||||
<p class="lead">Selamat datang di Sistem Informasi Arsip Digital KBRI Harare.</p>
|
||||
<p class="muted-copy">Mewujudkan tata kelola administrasi yang tertib, transparan, dan akuntabel untuk pelayanan publik prima.</p>
|
||||
<div class="security-badges">
|
||||
<span><i class="bi bi-shield-lock"></i> Bcrypt + CSRF</span>
|
||||
<span><i class="bi bi-envelope-check"></i> MFA via Email OTP</span>
|
||||
<span><i class="bi bi-journal-text"></i> Audit trail dokumen</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="login-panel card border-0 shadow-sm">
|
||||
<div class="card-body p-4 p-lg-5">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-4">
|
||||
<div>
|
||||
<p class="section-kicker mb-2">Akses aman staf internal</p>
|
||||
<h2 class="h4 mb-1"><?= is_array($pendingAuth) ? 'Verifikasi MFA' : 'Masuk ke brankas digital' ?></h2>
|
||||
<p class="text-secondary mb-0 small"><?= is_array($pendingAuth) ? 'Masukkan kode OTP yang berlaku 5 menit.' : 'Gunakan email kedinasan, password, dan verifikasi OTP.' ?></p>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary btn-sm theme-toggle" type="button" data-theme-toggle>
|
||||
<i class="bi bi-moon-stars"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (is_array($pendingAuth)): ?>
|
||||
<div class="alert alert-secondary border mb-4" role="status">
|
||||
Kode OTP dikirim ke <strong><?= h($pendingAuth['masked_email'] ?? '') ?></strong>.
|
||||
<?php if (empty($pendingAuth['mail_sent'])): ?>
|
||||
<span class="d-block small mt-2">Mode uji aktif karena SMTP belum dikonfigurasi.</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<form method="post" class="vstack gap-3">
|
||||
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="verify_otp">
|
||||
<div>
|
||||
<label for="otp" class="form-label">Kode OTP</label>
|
||||
<input id="otp" name="otp" inputmode="numeric" maxlength="6" class="form-control form-control-lg" placeholder="Masukkan 6 digit kode" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">Verifikasi & Masuk</button>
|
||||
<a href="index.php?reset_otp=1" class="btn btn-link text-decoration-none p-0">Kembali ke form login</a>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<form method="post" class="vstack gap-3">
|
||||
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="login_request">
|
||||
<div>
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input id="email" name="email" type="email" class="form-control form-control-lg" placeholder="nama@kbriharare.go.id" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input id="password" name="password" type="password" class="form-control form-control-lg" placeholder="Masukkan password" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mfa" class="form-label">MFA</label>
|
||||
<input id="mfa" class="form-control" value="Email OTP" readonly aria-readonly="true">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">Lanjutkan ke OTP</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="demo-credentials mt-4">
|
||||
<p class="mb-2 fw-semibold small text-uppercase text-secondary">Akun demo internal</p>
|
||||
<div class="small text-secondary">Super Admin: <code>superadmin@kbriharare.go.id</code></div>
|
||||
<div class="small text-secondary">Staf: <code>staf.politik@kbriharare.go.id</code></div>
|
||||
<div class="small text-secondary">Password: <code>Harare2026!</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<?php else: ?>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar-panel" id="sidebarPanel">
|
||||
<div class="sidebar-top">
|
||||
<div>
|
||||
<p class="section-kicker mb-1">Navigasi arsip</p>
|
||||
<h2 class="h6 mb-0">Pohon folder KBRI Harare</h2>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary btn-sm d-lg-none" type="button" data-sidebar-toggle>
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="folder-tree">
|
||||
<?php foreach ($folderTree as $section): ?>
|
||||
<details class="folder-group" open>
|
||||
<summary>
|
||||
<span><?= h($section['group']) ?></span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</summary>
|
||||
<div class="folder-links">
|
||||
<?php foreach ($section['items'] as $item): ?>
|
||||
<div class="folder-item">
|
||||
<button
|
||||
type="button"
|
||||
class="folder-select"
|
||||
data-folder-select
|
||||
data-folder="<?= h($item['folder']) ?>"
|
||||
data-category="<?= h($item['label']) ?>"
|
||||
>
|
||||
<span class="folder-name"><?= h($item['label']) ?></span>
|
||||
<small><?= h($item['hint']) ?></small>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary folder-add" data-folder-select data-folder="<?= h($item['folder']) ?>" data-category="<?= h($item['label']) ?>" aria-label="Tambah dokumen ke <?= h($item['folder']) ?>">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="app-main">
|
||||
<header class="topbar">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<button class="btn btn-outline-secondary d-lg-none" type="button" data-sidebar-toggle>
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
<div>
|
||||
<p class="section-kicker mb-1">Sistem Informasi Arsip Digital</p>
|
||||
<h1 class="h4 mb-0">KBRI Harare Secure Vault</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 gap-lg-3">
|
||||
<button class="btn btn-outline-secondary btn-sm theme-toggle" type="button" data-theme-toggle>
|
||||
<i class="bi bi-moon-stars"></i>
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<button class="btn profile-chip dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="avatar"><?= h($user['avatar'] ?? 'U') ?></span>
|
||||
<span class="text-start d-none d-md-inline-block">
|
||||
<strong class="d-block"><?= h($user['name']) ?></strong>
|
||||
<small class="text-secondary"><?= h($user['role_label'] ?? '') ?> · <?= h($user['department'] ?? '') ?></small>
|
||||
</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||
<li><span class="dropdown-item-text small text-secondary">Foto profil dapat diaktifkan pada iterasi berikutnya.</span></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="logout">
|
||||
<button type="submit" class="dropdown-item text-danger">Keluar</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content-grid">
|
||||
<section class="hero-panel card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex flex-column flex-xl-row justify-content-between gap-4">
|
||||
<div>
|
||||
<p class="section-kicker mb-2">Workflow MVP siap dipakai</p>
|
||||
<h2 class="display-title">Unggah → Validasi → Pratinjau aman → Riwayat audit</h2>
|
||||
<p class="text-secondary mb-4">Staf dapat menempatkan dokumen pada folder yang tepat, sementara Super Admin memvalidasi sebelum dokumen dapat dipratinjau, diunduh, atau dicetak.</p>
|
||||
<div class="hero-actions d-flex flex-wrap gap-2">
|
||||
<a href="#uploadCard" class="btn btn-primary"><i class="bi bi-cloud-arrow-up me-2"></i>Tambah Dokumen</a>
|
||||
<a href="#documentTable" class="btn btn-outline-secondary"><i class="bi bi-journal-text me-2"></i>Lihat Arsip</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-panel">
|
||||
<div class="metric-box">
|
||||
<span>Total arsip</span>
|
||||
<strong><?= number_format($dashboardMetrics['total']) ?></strong>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span>Menunggu validasi</span>
|
||||
<strong><?= number_format($dashboardMetrics['pending']) ?></strong>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span>Tervalidasi</span>
|
||||
<strong><?= number_format($dashboardMetrics['validated']) ?></strong>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span>Upload hari ini</span>
|
||||
<strong><?= number_format($dashboardMetrics['today_uploads']) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card border-0 shadow-sm" id="uploadCard">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-4">
|
||||
<div>
|
||||
<p class="section-kicker mb-2">Input dokumen</p>
|
||||
<h2 class="h5 mb-1">Tambah dokumen ke folder terpilih</h2>
|
||||
<p class="text-secondary mb-0 small">Gunakan drag-and-drop, isi metadata inti, lalu sistem menyimpan arsip ke folder terkait secara otomatis.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary-subtle text-secondary-emphasis border">CSRF aktif</span>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" class="row g-3" id="uploadForm">
|
||||
<input type="hidden" name="csrf_token" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" value="upload_document">
|
||||
<div class="col-12 col-xl-7">
|
||||
<label class="form-label" for="title">Judul dokumen</label>
|
||||
<input class="form-control" id="title" name="title" placeholder="Contoh: Nota Diplomatik Dukungan Kegiatan RI" required>
|
||||
</div>
|
||||
<div class="col-6 col-xl-2">
|
||||
<label class="form-label" for="document_date">Tanggal</label>
|
||||
<input class="form-control" id="document_date" name="document_date" type="date" value="<?= h(date('Y-m-d')) ?>" required>
|
||||
</div>
|
||||
<div class="col-6 col-xl-3">
|
||||
<label class="form-label" for="category">Kategori</label>
|
||||
<input class="form-control" id="category" name="category" list="categoryList" placeholder="Pilih / ketik kategori" required>
|
||||
<datalist id="categoryList">
|
||||
<?php foreach ($categoryOptions as $category): ?>
|
||||
<option value="<?= h($category) ?>"></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="col-12 col-lg-7">
|
||||
<label class="form-label" for="folder_path">Folder tujuan</label>
|
||||
<select class="form-select" id="folder_path" name="folder_path" required>
|
||||
<?php foreach ($folderOptions as $folder): ?>
|
||||
<option value="<?= h($folder) ?>" <?= $selectedFolder === $folder ? 'selected' : '' ?>><?= h($folder) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Klik folder di sidebar untuk mengisi otomatis.</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5">
|
||||
<label class="form-label">Lampiran</label>
|
||||
<div class="dropzone" data-dropzone>
|
||||
<input class="visually-hidden" type="file" id="attachment" name="attachment" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.mp4" required>
|
||||
<div>
|
||||
<i class="bi bi-file-earmark-arrow-up"></i>
|
||||
<p class="mb-1 fw-semibold">Tarik file ke sini atau klik untuk memilih</p>
|
||||
<small class="text-secondary" data-file-label>PDF, DOC, DOCX, JPG, PNG, MP4 · maksimum 10 MB</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="notes">Catatan</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="4" placeholder="Isi ringkasan, status, atau konteks diplomatik singkat."></textarea>
|
||||
</div>
|
||||
<div class="col-12 d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||
<div class="small text-secondary">Akses staf untuk pratinjau/unduh/cetak akan terbuka setelah validasi Super Admin.</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Simpan dokumen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card border-0 shadow-sm compact-card">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||||
<div>
|
||||
<p class="section-kicker mb-2">Kontrol akses</p>
|
||||
<h2 class="h5 mb-1">Status sesi & keamanan</h2>
|
||||
</div>
|
||||
<span class="badge text-bg-dark"><?= h($user['role_label'] ?? '') ?></span>
|
||||
</div>
|
||||
<ul class="list-unstyled security-list mb-0">
|
||||
<li><i class="bi bi-check2-circle"></i><span>Login terakhir: <?= h($loginAudit['timestamp'] ?? date('Y-m-d H:i:s')) ?></span></li>
|
||||
<li><i class="bi bi-check2-circle"></i><span>MFA: <?= h($loginAudit['mfa_method'] ?? 'Email OTP') ?></span></li>
|
||||
<li><i class="bi bi-check2-circle"></i><span>Hak akses: <?= is_super_admin() ? 'validasi, unggah, dan akses semua dokumen' : 'unggah dan akses dokumen setelah validasi' ?></span></li>
|
||||
<li><i class="bi bi-check2-circle"></i><span>Proteksi aktif: prepared statements, CSRF token, dan rate limiting sesi.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card border-0 shadow-sm compact-card">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||||
<div>
|
||||
<p class="section-kicker mb-2">Validasi dokumen</p>
|
||||
<h2 class="h5 mb-1">Queue persetujuan</h2>
|
||||
</div>
|
||||
<span class="badge text-bg-warning"><?= number_format($dashboardMetrics['pending']) ?> pending</span>
|
||||
</div>
|
||||
<?php if ($pendingDocuments): ?>
|
||||
<div class="queue-list">
|
||||
<?php foreach ($pendingDocuments as $document): ?>
|
||||
<a class="queue-item" href="document.php?id=<?= (int)$document['id'] ?>">
|
||||
<div>
|
||||
<strong><?= h($document['title']) ?></strong>
|
||||
<span><?= h($document['folder_path']) ?></span>
|
||||
</div>
|
||||
<span class="status-pill pending">Menunggu</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty-panel">
|
||||
<i class="bi bi-inbox"></i>
|
||||
<p class="mb-0">Belum ada dokumen yang menunggu validasi.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card border-0 shadow-sm table-card" id="documentTable">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-3">
|
||||
<div>
|
||||
<p class="section-kicker mb-2">Daftar arsip</p>
|
||||
<h2 class="h5 mb-1">Dokumen terbaru</h2>
|
||||
<p class="text-secondary mb-0 small">Setiap arsip memiliki detail metadata, status validasi, dan jejak audit.</p>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="search" class="form-control" id="documentSearch" placeholder="Cari judul, folder, atau pembuat">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($recentDocuments): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle archive-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Dokumen</th>
|
||||
<th>Folder</th>
|
||||
<th>Upload</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recentDocuments as $document): ?>
|
||||
<tr data-search-row="<?= h(strtolower($document['title'] . ' ' . $document['folder_path'] . ' ' . $document['created_by'])) ?>">
|
||||
<td>
|
||||
<strong class="d-block"><?= h($document['title']) ?></strong>
|
||||
<small class="text-secondary"><?= h($document['category']) ?> · <?= h(format_filesize((int)$document['attachment_size'])) ?></small>
|
||||
</td>
|
||||
<td><?= h($document['folder_path']) ?></td>
|
||||
<td>
|
||||
<span class="d-block"><?= h($document['created_by']) ?></span>
|
||||
<small class="text-secondary"><?= h(date('d M Y H:i', strtotime((string)$document['created_at']))) ?></small>
|
||||
</td>
|
||||
<td><span class="status-pill <?= $document['status'] === 'validated' ? 'validated' : 'pending' ?>"><?= $document['status'] === 'validated' ? 'Tervalidasi' : 'Menunggu' ?></span></td>
|
||||
<td class="text-end"><a class="btn btn-sm btn-outline-secondary" href="document.php?id=<?= (int)$document['id'] ?>">Buka detail</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="empty-panel tall">
|
||||
<i class="bi bi-folder2-open"></i>
|
||||
<p class="mb-1 fw-semibold">Belum ada arsip.</p>
|
||||
<p class="mb-0 text-secondary small">Mulai dengan memilih folder di sidebar lalu unggah dokumen pertama Anda.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?= urlencode((string)filemtime(__DIR__ . '/assets/js/main.js')) ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user