true, 'message' => $message, 'data' => $data], JSON_UNESCAPED_UNICODE); exit; } function jsonError(string $message = 'Error', int $code = 400, array $errors = []): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['success' => false, 'message' => $message, 'errors' => $errors], JSON_UNESCAPED_UNICODE); exit; } /* ============================================================ Input sanitization ============================================================ */ function sanitize(string $val): string { return htmlspecialchars(strip_tags(trim($val)), ENT_QUOTES, 'UTF-8'); } function getBody(): array { $raw = file_get_contents('php://input'); $data = json_decode($raw, true); return is_array($data) ? $data : []; } function required(array $data, array $fields): array { $errors = []; foreach ($fields as $f) { if (!isset($data[$f]) || trim((string)$data[$f]) === '') { $errors[] = "Field '$f' wajib diisi"; } } return $errors; } /* ============================================================ Session / Auth ============================================================ */ function startSession(): void { if (session_status() === PHP_SESSION_NONE) { session_name(SESSION_NAME); session_start(); } } function currentUser(): ?array { startSession(); return $_SESSION['user'] ?? null; } function requireAuth(array $allowedRoles = []): array { $user = currentUser(); if (!$user) jsonError('Unauthorized - silakan login', 401); if (!empty($allowedRoles) && !in_array($user['id_role'], $allowedRoles)) { jsonError('Forbidden - hak akses tidak cukup', 403); } return $user; } function login(string $username, string $password): array|false { $db = getDB(); $stmt = $db->prepare( "SELECT u.*, r.nama_role FROM user u JOIN role r ON u.id_role = r.id WHERE u.username = ? AND u.aktif = 1 LIMIT 1" ); $stmt->execute([$username]); $user = $stmt->fetch(); if (!$user || !password_verify($password, $user['password_hash'])) return false; // Update last_login $db->prepare("UPDATE user SET last_login = NOW() WHERE id = ?")->execute([$user['id']]); unset($user['password_hash']); return $user; } /* ============================================================ Activity Log ============================================================ */ function logActivity(string $aksi, string $tabel = '', string $idRecord = '', array $dataLama = [], array $dataBaru = []): void { try { $user = currentUser(); $db = getDB(); $db->prepare( "INSERT INTO log_aktivitas (id_user, aksi, tabel, id_record, data_lama, data_baru, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?)" )->execute([ $user['id'] ?? null, $aksi, $tabel, $idRecord, $dataLama ? json_encode($dataLama, JSON_UNESCAPED_UNICODE) : null, $dataBaru ? json_encode($dataBaru, JSON_UNESCAPED_UNICODE) : null, $_SERVER['REMOTE_ADDR'] ?? null, ]); } catch (Throwable $e) { /* silent */ } } /* ============================================================ Haversine distance (km) ============================================================ */ function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float { $R = 6371; $dLat = deg2rad($lat2 - $lat1); $dLng = deg2rad($lng2 - $lng1); $a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2; return $R * 2 * asin(sqrt($a)); } /* ============================================================ Pagination helper ============================================================ */ function paginate(array $data, int $page, int $perPage = 20): array { $total = count($data); $pages = (int)ceil($total / $perPage); $offset = ($page - 1) * $perPage; return [ 'items' => array_slice($data, $offset, $perPage), 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'pages' => $pages, ]; } /* ============================================================ Validasi umur vs pendidikan ============================================================ */ function validasiPendidikan(int $umur, string $pendidikanTerakhir, string $statusPendidikan): array { $warnings = []; if ($umur >= 7 && $umur <= 15) { if ($statusPendidikan === 'tidak_sekolah') { $warnings[] = "Anak usia $umur tahun seharusnya masih sekolah (wajib belajar 9 tahun)"; } if (!in_array($pendidikanTerakhir, ['SD','SMP','tidak_sekolah'])) { $warnings[] = "Pendidikan terakhir tidak sesuai untuk usia $umur tahun"; } } elseif ($umur >= 16 && $umur <= 18) { if ($statusPendidikan === 'tidak_sekolah' && in_array($pendidikanTerakhir, ['tidak_sekolah','SD'])) { $warnings[] = "Remaja usia $umur tahun disarankan melanjutkan ke SMA/SMK"; } } return $warnings; } /* ============================================================ Upload foto helper ============================================================ */ function uploadFoto(array $file, string $subfolder = 'foto_laporan'): string|false { $allowed = ['image/jpeg','image/png','image/webp']; if (!in_array($file['type'], $allowed)) return false; if ($file['size'] > 5 * 1024 * 1024) return false; $dir = UPLOAD_DIR . $subfolder . '/'; if (!is_dir($dir)) mkdir($dir, 0755, true); $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $filename = uniqid('img_', true) . '.' . strtolower($ext); $dest = $dir . $filename; if (!move_uploaded_file($file['tmp_name'], $dest)) return false; return $subfolder . '/' . $filename; }