401 lines
18 KiB
PHP
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()]);
|
|
} |