Files
D1041231027-WebGIS/03/api/redistribusi.php
T
2026-06-10 18:54:25 +07:00

401 lines
18 KiB
PHP

<?php
// api/redistribusi.php
// Fitur 1: Redistribusi Bantuan antar Rumah Ibadah
// Method: GET (analisis), POST (buat saran manual), PUT (update status), DELETE
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../config/db.php';
require_once 'middleware.php';
/* ─────────────────────────────────────────────
HELPER: ambil konfigurasi dari DB
───────────────────────────────────────────── */
function getKonfig(PDO $pdo, string $kunci, $default = null) {
$stmt = $pdo->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()]);
}