= 6) $skor += 20; elseif ($tanggungan >= 4) $skor += 15; elseif ($tanggungan >= 2) $skor += 10; else $skor += 4; // 3. Kondisi rumah (15 poin) $skor += match($warga['kondisi_rumah'] ?? '') { 'Sangat Tidak Layak' => 15, 'Tidak Layak' => 10, 'Cukup Layak' => 5, 'Layak' => 1, default => 5 }; // 4. Pendidikan (10 poin) $skor += match($warga['pendidikan_terakhir'] ?? '') { 'Tidak Sekolah' => 10, 'SD' => 8, 'SMP' => 6, 'SMA' => 3, default => 1 }; // 5. Riwayat penyakit (10 poin) if (!empty($warga['riwayat_penyakit'])) { $penyakit = strtolower($warga['riwayat_penyakit']); if (str_contains($penyakit, 'kanker') || str_contains($penyakit, 'gagal ginjal') || str_contains($penyakit, 'stroke')) { $skor += 10; } elseif (str_contains($penyakit, 'diabetes') || str_contains($penyakit, 'jantung') || str_contains($penyakit, 'tbc')) { $skor += 8; } else { $skor += 5; } } // 6. Lansia (8 poin) if (!empty($warga['is_lansia'])) $skor += 8; // 7. Disabilitas (7 poin) if (!empty($warga['is_disabilitas'])) $skor += 7; $skor = min(100, $skor); $level = match(true) { $skor >= 70 => 'Tinggi', $skor >= 40 => 'Sedang', default => 'Rendah' }; return ['skor' => $skor, 'level' => $level]; } /** * Tentukan status kemiskinan berdasarkan penghasilan dan tanggungan */ function tentukanStatusKemiskinan(int $penghasilan, int $tanggungan): string { $kapita = $tanggungan > 0 ? $penghasilan / $tanggungan : $penghasilan; if ($kapita <= 350000) return 'Miskin Ekstrem'; if ($kapita <= 550000) return 'Miskin'; return 'Rentan Miskin'; } /** * Generate kode laporan: LPR-YYYYMMDD-XXXX */ function generateKodeLaporan(): string { $db = Database::getInstance(); $today = date('Ymd'); $count = $db->fetchOne("SELECT COUNT(*)+1 as n FROM laporan_masyarakat WHERE DATE(created_at) = CURDATE()")['n']; return 'LPR-' . $today . '-' . str_pad($count, 4, '0', STR_PAD_LEFT); } /** * Generate kode bantuan: BNT-YYYYMMDD-XXXX */ function generateKodeBantuan(): string { $db = Database::getInstance(); $today = date('Ymd'); $count = $db->fetchOne("SELECT COUNT(*)+1 as n FROM histori_bantuan WHERE DATE(created_at) = CURDATE()")['n']; return 'BNT-' . $today . '-' . str_pad($count, 4, '0', STR_PAD_LEFT); } /** * Generate password awal/reset yang mudah diketik tetapi tetap acak. */ function generatePasswordAkun(): string { return 'Sinergi@' . random_int(100000, 999999); } /** * Kirim kredensial akun via email jika mail server tersedia. * Mengembalikan false di instalasi lokal yang belum punya konfigurasi SMTP/sendmail. */ function kirimEmailAkun(string $email, string $nama, string $username, string $password, string $aksi = 'dibuat'): bool { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return false; } $subject = 'Informasi Akun SINERGI'; $message = "Yth. {$nama},\n\n" . "Akun SINERGI Anda telah {$aksi}.\n\n" . "URL Login: " . APP_URL . "/login\n" . "Username: {$username}\n" . "Password: {$password}\n\n" . "Silakan masuk ke sistem dan simpan kredensial ini dengan aman.\n\n" . "Salam,\nAdmin SINERGI"; $headers = [ 'From: SINERGI ', 'Content-Type: text/plain; charset=UTF-8' ]; return @mail($email, $subject, $message, implode("\r\n", $headers)); } /** * Temukan rumah ibadah terdekat berdasarkan Haversine */ function findRumahIbadahTerdekat(float $lat, float $lng): ?array { $db = Database::getInstance(); $rumahIbadahs = $db->fetchAll( "SELECT ri.*, k.nama as kelurahan FROM rumah_ibadah ri JOIN kelurahan k ON ri.id_kelurahan = k.id WHERE ri.is_active = 1" ); $nearest = null; $minDist = PHP_FLOAT_MAX; foreach ($rumahIbadahs as $ri) { $dist = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']); if ($dist < $minDist) { $minDist = $dist; $nearest = $ri; $nearest['jarak_meter'] = round($dist); } } return $nearest; } /** * Cek apakah titik berada dalam radius salah satu rumah ibadah */ function isInBlindSpot(float $lat, float $lng): bool { $db = Database::getInstance(); $rumahIbadahs = $db->fetchAll("SELECT lat, lng, radius_meter FROM rumah_ibadah WHERE is_active = 1"); foreach ($rumahIbadahs as $ri) { $dist = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']); if ($dist <= (float)$ri['radius_meter']) { return false; } } return true; } /** * Format rupiah */ function formatRupiah(int $amount): string { return 'Rp ' . number_format($amount, 0, ',', '.'); } /** * Format tanggal Indonesia */ function formatTanggal(string $date, bool $withTime = false): string { if (empty($date) || $date === '0000-00-00') return '-'; $bulan = ['','Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember']; $d = new DateTime($date); $str = $d->format('j') . ' ' . $bulan[(int)$d->format('n')] . ' ' . $d->format('Y'); if ($withTime) $str .= ' ' . $d->format('H:i'); return $str; } /** * Hitung umur */ function hitungUmur(string $tanggalLahir): int { if (empty($tanggalLahir)) return 0; return (new DateTime($tanggalLahir))->diff(new DateTime())->y; } /** * Warna marker berdasarkan status kemiskinan */ function warnaMiskin(string $status): string { return match($status) { 'Miskin Ekstrem' => '#dc2626', 'Miskin' => '#ea580c', 'Rentan Miskin' => '#ca8a04', default => '#6b7280' }; } /** * Badge HTML status */ function statusBadge(string $status): string { $map = [ 'menunggu' => ['bg-yellow-100 text-yellow-800', 'Menunggu'], 'diproses' => ['bg-blue-100 text-blue-800', 'Diproses'], 'terverifikasi'=> ['bg-green-100 text-green-800', 'Terverifikasi'], 'ditolak' => ['bg-red-100 text-red-800', 'Ditolak'], 'aktif' => ['bg-green-100 text-green-800', 'Aktif'], 'nonaktif' => ['bg-gray-100 text-gray-600', 'Nonaktif'], 'Tinggi' => ['bg-red-100 text-red-800', 'Tinggi'], 'Sedang' => ['bg-yellow-100 text-yellow-800', 'Sedang'], 'Rendah' => ['bg-green-100 text-green-800', 'Rendah'], 'disalurkan' => ['bg-green-100 text-green-800', 'Disalurkan'], 'rencana' => ['bg-blue-100 text-blue-800', 'Rencana'], 'dibatalkan' => ['bg-red-100 text-red-800', 'Dibatalkan'], ]; [$cls, $label] = $map[$status] ?? ['bg-gray-100 text-gray-600', $status]; return "$label"; } /** * CSRF hidden input */ function csrfField(): string { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return ''; } /** * Set flash message ke session. * Gunakan fungsi ini (bukan tulis langsung ke $_SESSION['flash']) agar * kode_laporan / kode_bantuan disimpan sebagai key terpisah — bukan * ditempel ke dalam string pesan — sehingga tidak ikut terhapus saat * flashMessage() dipanggil. * * Contoh penggunaan: * setFlash('success', 'Laporan berhasil disimpan.', ['kode_laporan' => $kode]); * setFlash('error', 'Terjadi kesalahan.'); */ function setFlash(string $type, string $message, array $extra = []): void { $_SESSION['flash'] = array_merge(['type' => $type, 'message' => $message], $extra); } /** * Flash message display. * * FIX Bug 1 — unset dilakukan SETELAH semua data flash dibaca, bukan di * tengah-tengah, sehingga kode_laporan/kode_bantuan yang * mungkin dipakai lapor.php masih tersedia selama request ini. * * FIX Bug 2 — pesan flash boleh mengandung dan
(supaya kode * laporan bisa ditebalkan), tapi semua tag lain tetap di-escape * agar aman dari XSS. * * Jika flash mengandung 'kode_laporan' atau 'kode_bantuan', nilainya * otomatis disisipkan ke baris kedua pesan dalam tag . */ function flashMessage(): string { if (empty($_SESSION['flash'])) return ''; // Baca dulu seluruh data — JANGAN unset sebelum semua field diambil $flash = $_SESSION['flash']; unset($_SESSION['flash']); // hapus sekarang, setelah semua field tersalin ke $flash $type = $flash['type'] ?? 'info'; $message = $flash['message'] ?? ''; // Sisipkan kode laporan / bantuan sebagai baris ke-2 jika ada if (!empty($flash['kode_laporan'])) { $kode = htmlspecialchars($flash['kode_laporan'], ENT_QUOTES, 'UTF-8'); $message .= '
Kode Laporan: ' . $kode . ''; } elseif (!empty($flash['kode_bantuan'])) { $kode = htmlspecialchars($flash['kode_bantuan'], ENT_QUOTES, 'UTF-8'); $message .= '
Kode Bantuan: ' . $kode . ''; } // Escape pesan utama — lalu kembalikan dan
yang sudah kita sisipkan // Alur: escape semua → unescape hanya tag yang kita izinkan $safe = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); $safe = str_replace( ['<strong>', '</strong>', '<br>'], ['', '', '
'], $safe ); $icon = match($type) { 'success' => '✓', 'error' => '✕', 'warning' => '⚠', default => 'ℹ', }; $cls = match($type) { 'success' => 'flash-success', 'error' => 'flash-error', 'warning' => 'flash-warning', default => 'flash-info', }; return "
$icon$safe
"; } /** * Escape HTML */ function e(string $str): string { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } /** * Asset URL */ function asset(string $path): string { return APP_URL . '/public/' . ltrim($path, '/'); } /** * Upload URL */ function uploadUrl(string $filename): string { if (empty($filename)) return asset('images/placeholder.png'); return APP_URL . '/public/images/uploads/' . $filename; }