PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (PDOException $e) { jsonErr('Koneksi database gagal: ' . $e->getMessage(), 500); } } return $pdo; } function startSession(): void { if (session_status() === PHP_SESSION_NONE) { session_name('webgis_v3'); session_set_cookie_params(['lifetime'=>28800,'path'=>'/','httponly'=>true,'samesite'=>'Strict']); session_start(); } } function getCurrentUser(): ?array { startSession(); return $_SESSION['user'] ?? null; } function requireLogin(): array { $u = getCurrentUser(); if (!$u) jsonErr('Silakan login terlebih dahulu.', 401); return $u; } function requireRole(array $roles): array { $u = requireLogin(); if (!in_array($u['role'], $roles, true)) jsonErr('Akses ditolak untuk role: '.$u['role'], 403); return $u; } function canWrite(): bool { $u = getCurrentUser(); return $u && in_array($u['role'], ['superadmin','pengurus','relawan'], true); } function jsonOk($data = null, string $msg = 'Berhasil'): void { header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success'=>true,'message'=>$msg,'data'=>$data], JSON_UNESCAPED_UNICODE); exit; } function jsonErr(string $msg, int $code = 400): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success'=>false,'message'=>$msg], JSON_UNESCAPED_UNICODE); exit; } function log_aksi(int $userId, string $aksi, string $tabel, int $recId, string $ket = ''): void { try { getDB()->prepare("INSERT INTO log_aktivitas (user_id,aksi,tabel,record_id,keterangan,ip_address) VALUES (?,?,?,?,?,?)") ->execute([$userId, $aksi, $tabel, $recId, $ket, $_SERVER['REMOTE_ADDR'] ?? '']); } catch (Exception $e) {} } function sanitize(string $s): string { return htmlspecialchars(strip_tags(trim($s)), ENT_QUOTES, 'UTF-8'); } // Hitung umur dari tanggal lahir function hitungUmur(string $tgl): int { return (int) date_diff(date_create($tgl), date_create('today'))->y; } // Validasi pendidikan vs umur function validasiPendidikan(int $umur, string $pendidikan, string $statusPendidikan): ?string { if ($umur >= 7 && $umur <= 15 && $statusPendidikan === 'tidak_sekolah' && in_array($pendidikan, ['tidak_sekolah','sd'])) { return 'Peringatan: Usia 7-15 tahun seharusnya masih sekolah SD/SMP.'; } if ($umur >= 16 && $umur <= 18 && $statusPendidikan === 'tidak_sekolah') { return 'Peringatan: Usia 16-18 tahun seharusnya masih sekolah SMA/SMK.'; } return null; } header('Content-Type: application/json; charset=utf-8'); if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);