prepare("SELECT nilai, tipe FROM konfigurasi_sistem WHERE kunci = ?"); $stmt->execute([$kunci]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) return $default; return match($row['tipe']) { 'integer' => (int)$row['nilai'], 'decimal' => (float)$row['nilai'], 'boolean' => filter_var($row['nilai'], FILTER_VALIDATE_BOOLEAN), default => $row['nilai'], }; } /* ───────────────────────────────────────────── CORE: Hitung beban tiap rumah ibadah berdasarkan radius dan jumlah warga miskin ───────────────────────────────────────────── */ function hitungBebanIbadah(PDO $pdo, float $radius): array { // Ambil semua rumah ibadah $ibadahList = $pdo->query("SELECT * FROM rumah_ibadah ORDER BY id")->fetchAll(PDO::FETCH_ASSOC); // Ambil semua rumah warga dengan status relevan $wargaList = $pdo->query(" SELECT id, latitude, longitude, kategori_kemiskinan, status_bantuan, rekomendasi_bantuan, jumlah_tanggungan, penghasilan_bulanan, status_kemandirian, skor_kemandirian, dikelola_oleh_ibadah_id FROM rumah_warga WHERE status_kemandirian NOT IN ('mandiri') AND kategori_kemiskinan IN ('sangat_miskin','miskin','rentan_miskin') ")->fetchAll(PDO::FETCH_ASSOC); $result = []; foreach ($ibadahList as $ibadah) { $lat1 = (float)$ibadah['latitude']; $lng1 = (float)$ibadah['longitude']; $rad = $ibadah['radius_primer'] ?? $radius; $kap = (int)($ibadah['kapasitas_kk'] ?? 20); $wargaDalam = []; foreach ($wargaList as $warga) { $kelolaId = $warga['dikelola_oleh_ibadah_id']; if ($kelolaId !== null) { if ((int)$kelolaId === (int)$ibadah['id']) { // Explicitly assigned to this ibadah $d = haversine($lat1, $lng1, (float)$warga['latitude'], (float)$warga['longitude']); $wargaDalam[] = array_merge($warga, ['jarak_meter' => round($d)]); } } else { // Not assigned explicitly, check radius $d = haversine($lat1, $lng1, (float)$warga['latitude'], (float)$warga['longitude']); if ($d <= $rad) { $wargaDalam[] = array_merge($warga, ['jarak_meter' => round($d)]); } } } // Hitung kategori $sangat = count(array_filter($wargaDalam, fn($w) => $w['kategori_kemiskinan'] === 'sangat_miskin')); $miskin = count(array_filter($wargaDalam, fn($w) => $w['kategori_kemiskinan'] === 'miskin')); $rentan = count(array_filter($wargaDalam, fn($w) => $w['kategori_kemiskinan'] === 'rentan_miskin')); $total = count($wargaDalam); // Indeks beban: (sangat_miskin*3 + miskin*2 + rentan*1) / kapasitas // Skor 0-100, > 100 = overload $bobotBeban = ($sangat * 3 + $miskin * 2 + $rentan * 1); $maxBobot = $kap * 3; // kalau semua sangat miskin $pctBeban = $maxBobot > 0 ? round(min($bobotBeban / $maxBobot * 100, 200)) : 0; // Rasio kemiskinan: berapa % warga dalam radius yang masuk kategori miskin $rasioMiskin = $total > 0 ? round(($sangat + $miskin) / $total * 100) : 0; // Status beban $statusBeban = match(true) { $total === 0 => 'kosong', $total < $kap * 0.5 => 'ringan', $total <= $kap => 'normal', $total <= $kap * 1.3 => 'padat', default => 'overload', }; $result[$ibadah['id']] = [ 'ibadah' => $ibadah, 'radius_dipakai' => $rad, 'kapasitas' => $kap, 'total_warga' => $total, 'sangat_miskin' => $sangat, 'miskin' => $miskin, 'rentan_miskin' => $rentan, 'rasio_miskin' => $rasioMiskin, 'bobot_beban' => $bobotBeban, 'pct_beban' => $pctBeban, 'status_beban' => $statusBeban, 'surplus_kapasitas' => max(0, $kap - $total), 'defisit_kapasitas' => max(0, $total - $kap), 'warga_list' => $wargaDalam, ]; } return $result; } /* ───────────────────────────────────────────── CORE: Generate saran redistribusi otomatis ───────────────────────────────────────────── */ function generateSaranRedistribusi(array $bebanData): array { $saranList = []; // Pisahkan ibadah surplus vs defisit $surplus = array_filter($bebanData, fn($d) => $d['status_beban'] === 'ringan' || $d['status_beban'] === 'kosong'); $defisit = array_filter($bebanData, fn($d) => in_array($d['status_beban'], ['padat','overload'])); // Sort: defisit paling parah dulu, surplus paling longgar dulu uasort($surplus, fn($a,$b) => $a['pct_beban'] <=> $b['pct_beban']); uasort($defisit, fn($a,$b) => $b['pct_beban'] <=> $a['pct_beban']); foreach ($defisit as $def) { foreach ($surplus as &$sur) { if ($sur['surplus_kapasitas'] <= 0) continue; $ibadahDef = $def['ibadah']; $ibadahSur = $sur['ibadah']; // Hitung jarak antar kedua ibadah $jarakKm = round(haversine( (float)$ibadahDef['latitude'], (float)$ibadahDef['longitude'], (float)$ibadahSur['latitude'], (float)$ibadahSur['longitude'] ) / 1000, 2); // Hanya sarankan jika jarak tidak terlalu jauh (< 5km) if ($jarakKm > 5) continue; $jumlahPindah = min($def['defisit_kapasitas'], $sur['surplus_kapasitas']); if ($jumlahPindah <= 0) continue; // Estimasi dana: rata-rata penghasilan warga * faktor bantuan $estDana = $jumlahPindah * 200000; // estimasi Rp200rb/KK/bulan // Tingkat prioritas $prioritas = match(true) { $def['rasio_miskin'] >= 80 => 'tinggi', $def['rasio_miskin'] >= 50 => 'sedang', default => 'rendah', }; $saranList[] = [ 'pengirim_id' => (int)$ibadahSur['id'], 'pengirim_nama' => $ibadahSur['nama'], 'penerima_id' => (int)$ibadahDef['id'], 'penerima_nama' => $ibadahDef['nama'], 'jumlah_kk' => $jumlahPindah, 'estimasi_dana' => $estDana, 'jarak_km' => $jarakKm, 'prioritas' => $prioritas, 'alasan' => sprintf( '%s memiliki surplus %d kapasitas (beban %d%%), sementara %s kelebihan %d KK (beban %d%%) dengan rasio kemiskinan %d%%.', $ibadahSur['nama'], $sur['surplus_kapasitas'], $sur['pct_beban'], $ibadahDef['nama'], $def['defisit_kapasitas'], $def['pct_beban'], $def['rasio_miskin'] ), ]; // Kurangi surplus yang sudah dipakai $sur['surplus_kapasitas'] -= $jumlahPindah; break; } } // Sort hasil saran berdasarkan prioritas usort($saranList, function($a, $b) { $p = ['tinggi' => 0, 'sedang' => 1, 'rendah' => 2]; return ($p[$a['prioritas']] ?? 9) <=> ($p[$b['prioritas']] ?? 9); }); return $saranList; } function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float { $R = 6371000; $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 * atan2(sqrt($a), sqrt(1-$a)); } /* ───────────────────────────────────────────── ROUTING ───────────────────────────────────────────── */ try { // Hanya Admin yang bisa mengakses fitur redistribusi bantuan / analisis spasial requireAuth(['admin']); // Guard: PDO connection must be available if (!$pdo) { throw new RuntimeException('Koneksi database (PDO) tidak tersedia. Pastikan XAMPP MySQL aktif.'); } $method = $_SERVER['REQUEST_METHOD']; if ($method === 'GET') { $action = $_GET['action'] ?? 'analisis'; if ($action === 'analisis') { // Analisis beban semua rumah ibadah $radius = (float)($_GET['radius'] ?? getKonfig($pdo, 'kapasitas_default_ibadah') ?? 500); $beban = hitungBebanIbadah($pdo, $radius); // Hapus warga_list dari response utama (terlalu besar) $ringkasan = array_map(function($d) { $r = $d; unset($r['warga_list']); return $r; }, $beban); echo json_encode([ 'success' => true, 'data' => array_values($ringkasan), 'saran' => generateSaranRedistribusi($beban), 'summary' => [ 'total_ibadah' => count($beban), 'overload' => count(array_filter($beban, fn($d) => $d['status_beban'] === 'overload')), 'padat' => count(array_filter($beban, fn($d) => $d['status_beban'] === 'padat')), 'normal' => count(array_filter($beban, fn($d) => $d['status_beban'] === 'normal')), 'ringan' => count(array_filter($beban, fn($d) => $d['status_beban'] === 'ringan')), 'kosong' => count(array_filter($beban, fn($d) => $d['status_beban'] === 'kosong')), ], ]); } elseif ($action === 'riwayat') { // Riwayat keputusan redistribusi $limit = (int)($_GET['limit'] ?? 20); $status = $_GET['status'] ?? null; $sql = "SELECT r.*, ip.nama as pengirim_nama, ip.jenis as pengirim_jenis, ir.nama as penerima_nama, ir.jenis as penerima_jenis FROM redistribusi_bantuan r JOIN rumah_ibadah ip ON r.ibadah_pengirim_id = ip.id JOIN rumah_ibadah ir ON r.ibadah_penerima_id = ir.id"; if ($status) $sql .= " WHERE r.status = " . $pdo->quote($status); $sql .= " ORDER BY r.tanggal_saran DESC LIMIT $limit"; $rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC); echo json_encode(['success' => true, 'data' => $rows]); } elseif ($action === 'detail') { // Detail beban 1 ibadah $id = (int)($_GET['id'] ?? 0); $radius = (float)($_GET['radius'] ?? 500); $beban = hitungBebanIbadah($pdo, $radius); if (!isset($beban[$id])) { echo json_encode(['success' => false, 'message' => 'Rumah ibadah tidak ditemukan']); exit; } echo json_encode(['success' => true, 'data' => $beban[$id]]); } } elseif ($method === 'POST') { // Simpan saran redistribusi (dari sistem atau manual admin) $body = json_decode(file_get_contents('php://input'), true); $stmt = $pdo->prepare(" INSERT INTO redistribusi_bantuan (ibadah_pengirim_id, ibadah_penerima_id, jumlah_kk, estimasi_dana, status, catatan_sistem, catatan_admin, periode_bulan) VALUES (?, ?, ?, ?, ?, ?, ?, ?) "); $stmt->execute([ (int)$body['pengirim_id'], (int)$body['penerima_id'], (int)($body['jumlah_kk'] ?? 0), (float)($body['estimasi_dana'] ?? 0), $body['status'] ?? 'saran_sistem', $body['catatan_sistem'] ?? null, $body['catatan_admin'] ?? null, $body['periode_bulan'] ?? date('Y-m'), ]); $redistId = (int)$pdo->lastInsertId(); // Jika disetujui, alihkan warga dari penerima (defisit) ke pengirim (surplus) if (($body['status'] ?? '') === 'disetujui') { $penerimaId = (int)$body['penerima_id']; $pengirimId = (int)$body['pengirim_id']; $limit = (int)($body['jumlah_kk'] ?? 0); if (isset($body['warga_ids']) && is_array($body['warga_ids']) && count($body['warga_ids']) > 0) { $ids = array_map('intval', $body['warga_ids']); $placeholders = str_repeat('?,', count($ids) - 1) . '?'; $updateStmt = $pdo->prepare("UPDATE rumah_warga SET dikelola_oleh_ibadah_id = ? WHERE id IN ($placeholders)"); $params = array_merge([$pengirimId], $ids); $updateStmt->execute($params); } elseif ($limit > 0) { // Ambil radius penerima (untuk mencari warga yang saat ini masuk radiusnya) $stmtPen = $pdo->prepare("SELECT latitude, longitude, radius_primer FROM rumah_ibadah WHERE id = ?"); $stmtPen->execute([$penerimaId]); $penerima = $stmtPen->fetch(PDO::FETCH_ASSOC); // Ambil koordinat pengirim (untuk mencari warga terdekat ke pengirim) $stmtPeng = $pdo->prepare("SELECT latitude, longitude FROM rumah_ibadah WHERE id = ?"); $stmtPeng->execute([$pengirimId]); $pengirim = $stmtPeng->fetch(PDO::FETCH_ASSOC); if ($penerima && $pengirim) { $radPenerima = (float)($penerima['radius_primer'] ?? 500); // Ambil warga yang dikelola oleh penerima atau belum dikelola siapapun $stmtWarga = $pdo->prepare("SELECT id, latitude, longitude, dikelola_oleh_ibadah_id FROM rumah_warga WHERE (dikelola_oleh_ibadah_id = ? OR dikelola_oleh_ibadah_id IS NULL) AND kategori_kemiskinan IN ('sangat_miskin', 'miskin', 'rentan_miskin') AND status_kemandirian != 'mandiri'"); $stmtWarga->execute([$penerimaId]); $wargaList = $stmtWarga->fetchAll(PDO::FETCH_ASSOC); $eligible = []; foreach ($wargaList as $w) { // Jika sudah secara eksplisit dikelola oleh penerima, atau jika NULL dan masih dalam radius penerima $isManaged = ($w['dikelola_oleh_ibadah_id'] == $penerimaId); $distPenerima = haversine((float)$w['latitude'], (float)$w['longitude'], (float)$penerima['latitude'], (float)$penerima['longitude']); if ($isManaged || $distPenerima <= $radPenerima) { $distPengirim = haversine((float)$w['latitude'], (float)$w['longitude'], (float)$pengirim['latitude'], (float)$pengirim['longitude']); $w['dist_pengirim'] = $distPengirim; $eligible[] = $w; } } // Urutkan warga berdasarkan jarak terdekat ke rumah ibadah pengirim (surplus) usort($eligible, fn($a, $b) => $a['dist_pengirim'] <=> $b['dist_pengirim']); // Ambil sejumlah $limit $toTransfer = array_slice($eligible, 0, $limit); if (!empty($toTransfer)) { $ids = array_column($toTransfer, 'id'); $placeholders = str_repeat('?,', count($ids) - 1) . '?'; $updateStmt = $pdo->prepare("UPDATE rumah_warga SET dikelola_oleh_ibadah_id = ? WHERE id IN ($placeholders)"); $params = array_merge([$pengirimId], $ids); $updateStmt->execute($params); } } } } echo json_encode([ 'success' => true, 'message' => 'Saran redistribusi berhasil disimpan', 'id' => $redistId, ]); } elseif ($method === 'PUT') { // Update status keputusan admin $body = json_decode(file_get_contents('php://input'), true); $id = (int)($body['id'] ?? 0); $stmt = $pdo->prepare(" UPDATE redistribusi_bantuan SET status = ?, catatan_admin = ?, disetujui_oleh = ?, tanggal_keputusan = NOW() WHERE id = ? "); $stmt->execute([ $body['status'], $body['catatan_admin'] ?? null, $body['disetujui_oleh'] ?? null, $id, ]); echo json_encode(['success' => true, 'message' => 'Status redistribusi diperbarui']); } elseif ($method === 'DELETE') { $id = (int)($_GET['id'] ?? 0); $stmt = $pdo->prepare("DELETE FROM redistribusi_bantuan WHERE id = ?"); $stmt->execute([$id]); echo json_encode(['success' => true, 'message' => 'Data redistribusi dihapus']); } } catch (Throwable $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => $e->getMessage()]); }