Files
D1041231092-webgis-povertymap/core/helpers.php
T
2026-06-11 12:30:23 +07:00

357 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// core/helpers.php
/**
* Haversine Formula - menghitung jarak antara dua koordinat (meter)
*/
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371000; // radius bumi meter
$phi1 = deg2rad($lat1);
$phi2 = deg2rad($lat2);
$dphi = deg2rad($lat2 - $lat1);
$dlam = deg2rad($lng2 - $lng1);
$a = sin($dphi / 2) ** 2 + cos($phi1) * cos($phi2) * sin($dlam / 2) ** 2;
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $R * $c;
}
/**
* Algoritma Skor Prioritas Bantuan (0-100)
* Semakin tinggi skor = semakin prioritas
*/
function hitungSkorPrioritas(array $warga): array {
$skor = 0;
// 1. Penghasilan (30 poin)
$penghasilan = (int)($warga['penghasilan_bulanan'] ?? 0);
if ($penghasilan === 0) $skor += 30;
elseif ($penghasilan <= 300000) $skor += 25;
elseif ($penghasilan <= 600000) $skor += 18;
elseif ($penghasilan <= 1000000) $skor += 10;
else $skor += 3;
// 2. Jumlah tanggungan (20 poin)
$tanggungan = (int)($warga['jumlah_tanggungan'] ?? 0);
if ($tanggungan >= 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 <noreply@sinergi.local>',
'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 "<span class=\"badge $cls\">$label</span>";
}
/**
* CSRF hidden input
*/
function csrfField(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return '<input type="hidden" name="_token" value="' . htmlspecialchars($_SESSION['csrf_token']) . '">';
}
/**
* 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 <strong> dan <br> (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 <strong>.
*/
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 .= '<br>Kode Laporan: <strong>' . $kode . '</strong>';
} elseif (!empty($flash['kode_bantuan'])) {
$kode = htmlspecialchars($flash['kode_bantuan'], ENT_QUOTES, 'UTF-8');
$message .= '<br>Kode Bantuan: <strong>' . $kode . '</strong>';
}
// Escape pesan utama — lalu kembalikan <strong> dan <br> yang sudah kita sisipkan
// Alur: escape semua → unescape hanya tag yang kita izinkan
$safe = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
$safe = str_replace(
['&lt;strong&gt;', '&lt;/strong&gt;', '&lt;br&gt;'],
['<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 "<div class=\"flash-message $cls\"><span>$icon</span><span>$safe</span></div>";
}
/**
* 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;
}