680 lines
24 KiB
PHP
680 lines
24 KiB
PHP
<?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);
|
|
}
|