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'], }; } function hitungUsia(?string $tanggalLahir): ?int { if (!$tanggalLahir) return null; $lahir = new DateTime($tanggalLahir); $today = new DateTime(); return (int)$today->diff($lahir)->y; } /* ───────────────────────────────────────────── CORE: Hitung Skor Kemandirian Komponen: A) Skor Penghasilan (bobot configurable, default 40%) - 0% : penghasilan = 0 - 50% : penghasilan = 50% threshold - 100%: penghasilan >= threshold UMR B) Skor Status Kerja (default 35%) - Dihitung dari anggota usia produktif (18-55) - Poin per anggota: bekerja_tetap=100, tidak_tetap=60, tidak_bekerja=0, pelajar=30 - Bonus: sudah bekerja tetap setelah ikut pelatihan C) Skor Tanggungan (default 25%) - Tanggungan sedikit (1-2) = skor tinggi - Tanggungan banyak (> 6) = skor rendah Status kemandirian: - 0-20 : sangat_bergantung - 21-40 : bergantung - 41-60 : berkembang - 61-79 : berpotensi_mandiri - 80+ : mandiri (siap exit) ───────────────────────────────────────────── */ function hitungSkorKemandirian( PDO $pdo, array $warga, array $anggotaList, float $thresholdPenghasilan, int $bobotPenghasilan, int $bobotStatusKerja, int $bobotTanggungan ): array { $penghasilan = (float)$warga['penghasilan_bulanan']; $tanggungan = (int)$warga['jumlah_tanggungan']; $statusKerja = $warga['status_pekerjaan']; /* ── A: Skor Penghasilan ── */ $skorPenghasilan = 0; if ($thresholdPenghasilan > 0) { $skorPenghasilan = (int)min(100, round($penghasilan / $thresholdPenghasilan * 100)); } /* ── B: Skor Status Kerja ── */ $bobotStatusKerjaMap = [ 'tetap' => 100, 'tidak_tetap' => 60, 'pensiun' => 50, 'tidak_bekerja'=> 0, ]; // Skor kepala keluarga $poinKK = $bobotStatusKerjaMap[$statusKerja] ?? 0; $anggotaProd = array_filter($anggotaList, function($a) { $usia = hitungUsia($a['tanggal_lahir']); return $usia !== null && $usia >= 18 && $usia <= 55; }); if (count($anggotaProd) > 0) { $totalPoin = $poinKK; foreach ($anggotaProd as $a) { $stsBekMap = [ 'bekerja' => 100, 'pensiunan' => 50, 'pelajar' => 30, 'tidak_bekerja' => 0, ]; $poin = $stsBekMap[$a['status_bekerja']] ?? 0; // Diskon: tidak bisa bekerja / disabilitas if (!$a['dapat_bekerja'] || in_array($a['kondisi_kesehatan'], ['sakit_berat','disabilitas'])) { $poin = (int)($poin * 0.3); } $totalPoin += $poin; } $jumlahDitimbang = count($anggotaProd) + 1; // +1 untuk KK $skorStatusKerja = (int)round($totalPoin / ($jumlahDitimbang * 100) * 100); } else { // Tidak ada anggota produktif, skor bergantung murni dari KK $skorStatusKerja = $poinKK; } /* ── C: Skor Tanggungan ── */ // Makin sedikit tanggungan relatif terhadap yang bekerja = makin mandiri $anggotaBekerja = count(array_filter($anggotaList, fn($a) => $a['status_bekerja'] === 'bekerja')); $pencariNafkah = $anggotaBekerja + ($statusKerja !== 'tidak_bekerja' ? 1 : 0); if ($tanggungan <= 0) { $skorTanggungan = 100; } elseif ($pencariNafkah >= $tanggungan) { $skorTanggungan = 100; // semua tanggungan punya pemasukan } else { $rasio = $pencariNafkah / $tanggungan; $skorTanggungan = (int)min(100, round($rasio * 100)); } /* ── Skor Total (weighted) ── */ $totalBobot = $bobotPenghasilan + $bobotStatusKerja + $bobotTanggungan; if ($totalBobot <= 0) $totalBobot = 100; $skorTotal = (int)round( ($skorPenghasilan * $bobotPenghasilan + $skorStatusKerja * $bobotStatusKerja + $skorTanggungan * $bobotTanggungan) / $totalBobot ); /* ── Status ── */ $status = match(true) { $skorTotal >= 80 => 'mandiri', $skorTotal >= 61 => 'berpotensi_mandiri', $skorTotal >= 41 => 'berkembang', $skorTotal >= 21 => 'bergantung', default => 'sangat_bergantung', }; /* ── Estimasi waktu menuju mandiri ── */ $estimasiKenaikan = 5; // asumsi naik 5 poin/bulan dengan pelatihan $bulanMenujuMandiri = $skorTotal >= 80 ? 0 : (int)ceil((80 - $skorTotal) / $estimasiKenaikan); /* ── Rekomendasi aksi ── */ $rekomendasi = []; if ($skorPenghasilan < 50) $rekomendasi[] = 'Tingkatkan penghasilan melalui pelatihan vokasional'; if ($skorStatusKerja < 50) $rekomendasi[] = 'Dukung anggota produktif untuk mendapat pekerjaan tetap'; if ($skorTanggungan < 50) $rekomendasi[] = 'Pertimbangkan program KB/pemberdayaan tanggungan'; if (count($rekomendasi) === 0) $rekomendasi[] = 'Pertahankan perkembangan yang ada, pantau secara berkala'; return [ 'skor_total' => $skorTotal, 'skor_penghasilan' => $skorPenghasilan, 'skor_status_kerja' => $skorStatusKerja, 'skor_tanggungan' => $skorTanggungan, 'status' => $status, 'estimasi_bulan_mandiri' => $bulanMenujuMandiri, 'rekomendasi' => $rekomendasi, 'detail' => [ 'penghasilan' => $penghasilan, 'threshold' => $thresholdPenghasilan, 'jumlah_tanggungan' => $tanggungan, 'pencari_nafkah' => $pencariNafkah, 'anggota_produktif' => count($anggotaProd), ], ]; } /* ───────────────────────────────────────────── UPDATE skor kemandirian batch + simpan riwayat ───────────────────────────────────────────── */ function updateKemandirianBatch(PDO $pdo, bool $simpanRiwayat = false): int { $thres = getKonfig($pdo, 'threshold_penghasilan_mandiri', 3500000); $bPeng = getKonfig($pdo, 'bobot_penghasilan', 40); $bKerja = getKonfig($pdo, 'bobot_status_kerja', 35); $bTangg = getKonfig($pdo, 'bobot_tanggungan', 25); $wargaList = $pdo->query(" SELECT id, penghasilan_bulanan, jumlah_tanggungan, status_pekerjaan, skor_kemandirian as skor_lama FROM rumah_warga ")->fetchAll(PDO::FETCH_ASSOC); $stmtAnggota = $pdo->prepare(" SELECT nama, tanggal_lahir, status_bekerja, kondisi_kesehatan, dapat_bekerja FROM anggota_keluarga WHERE rumah_warga_id = ? "); $stmtUpdate = $pdo->prepare(" UPDATE rumah_warga SET skor_kemandirian = ?, status_kemandirian = ? WHERE id = ? "); $stmtRiwayat = $pdo->prepare(" INSERT INTO riwayat_kemandirian (rumah_warga_id, tanggal, skor, status, penghasilan_saat_ini) VALUES (?, CURDATE(), ?, ?, ?) "); $updated = 0; foreach ($wargaList as $w) { $stmtAnggota->execute([(int)$w['id']]); $anggota = $stmtAnggota->fetchAll(PDO::FETCH_ASSOC); $calc = hitungSkorKemandirian($pdo, $w, $anggota, $thres, $bPeng, $bKerja, $bTangg); $stmtUpdate->execute([$calc['skor_total'], $calc['status'], (int)$w['id']]); if ($simpanRiwayat && $calc['skor_total'] !== (int)$w['skor_lama']) { $stmtRiwayat->execute([ (int)$w['id'], $calc['skor_total'], $calc['status'], (float)$w['penghasilan_bulanan'], ]); } $updated++; } return $updated; } /* ───────────────────────────────────────────── ROUTING ───────────────────────────────────────────── */ try { // Hanya Admin dan Petugas Lapangan yang memiliki akses ke modul kemandirian warga requireAuth(['admin', 'petugas']); $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? 'list'; if ($method === 'GET') { if ($action === 'list') { // Recalculate dulu updateKemandirianBatch($pdo, true); $statusFilter = $_GET['status'] ?? null; $minSkor = (int)($_GET['min_skor'] ?? 0); $limit = (int)($_GET['limit'] ?? 50); $thresholdSk = getKonfig($pdo, 'threshold_skor_mandiri', 70); $where = []; if ($statusFilter) $where[] = "rw.status_kemandirian = " . $pdo->quote($statusFilter); if ($minSkor > 0) $where[] = "rw.skor_kemandirian >= $minSkor"; $whereStr = $where ? "WHERE " . implode(" AND ", $where) : ""; $rows = $pdo->query(" SELECT rw.id, rw.nama_kepala_keluarga, rw.no_kk, rw.penghasilan_bulanan, rw.jumlah_tanggungan, rw.status_pekerjaan, rw.kategori_kemiskinan, rw.skor_kemandirian, rw.status_kemandirian, rw.catatan_kemandirian, rw.tanggal_exit_plan, rw.rekomendasi_bantuan, rw.status_bantuan, rw.kelurahan, rw.kecamatan, rw.latitude, rw.longitude FROM rumah_warga rw $whereStr ORDER BY rw.skor_kemandirian DESC LIMIT $limit ")->fetchAll(PDO::FETCH_ASSOC); // Tambah detail skor per row $thres = getKonfig($pdo, 'threshold_penghasilan_mandiri', 3500000); $bPeng = getKonfig($pdo, 'bobot_penghasilan', 40); $bKerja = getKonfig($pdo, 'bobot_status_kerja', 35); $bTangg = getKonfig($pdo, 'bobot_tanggungan', 25); $stmtA = $pdo->prepare("SELECT * FROM anggota_keluarga WHERE rumah_warga_id = ?"); foreach ($rows as &$r) { $stmtA->execute([(int)$r['id']]); $anggota = $stmtA->fetchAll(PDO::FETCH_ASSOC); $r['detail_skor'] = hitungSkorKemandirian($pdo, $r, $anggota, $thres, $bPeng, $bKerja, $bTangg); } $summary = $pdo->query(" SELECT status_kemandirian, COUNT(*) as jumlah, ROUND(AVG(skor_kemandirian)) as avg_skor FROM rumah_warga GROUP BY status_kemandirian ")->fetchAll(PDO::FETCH_ASSOC); $siapExit = $pdo->query(" SELECT COUNT(*) FROM rumah_warga WHERE skor_kemandirian >= $thresholdSk AND status_bantuan != 'sudah_terima' ")->fetchColumn(); echo json_encode([ 'success' => true, 'data' => $rows, 'summary' => $summary, 'siap_exit' => (int)$siapExit, ]); } elseif ($action === 'detail') { $wargaId = (int)($_GET['id'] ?? 0); $warga = $pdo->prepare("SELECT * FROM rumah_warga WHERE id = ?"); $warga->execute([$wargaId]); $w = $warga->fetch(PDO::FETCH_ASSOC); if (!$w) { echo json_encode(['success' => false, 'message' => 'Warga tidak ditemukan']); exit; } $stmtA = $pdo->prepare("SELECT * FROM anggota_keluarga WHERE rumah_warga_id = ?"); $stmtA->execute([$wargaId]); $anggota = $stmtA->fetchAll(PDO::FETCH_ASSOC); $thres = getKonfig($pdo, 'threshold_penghasilan_mandiri', 3500000); $bPeng = getKonfig($pdo, 'bobot_penghasilan', 40); $bKerja = getKonfig($pdo, 'bobot_status_kerja', 35); $bTangg = getKonfig($pdo, 'bobot_tanggungan', 25); $skor = hitungSkorKemandirian($pdo, $w, $anggota, $thres, $bPeng, $bKerja, $bTangg); // Riwayat $riwayat = $pdo->prepare(" SELECT * FROM riwayat_kemandirian WHERE rumah_warga_id = ? ORDER BY tanggal DESC LIMIT 12 "); $riwayat->execute([$wargaId]); echo json_encode([ 'success' => true, 'data' => $w, 'skor' => $skor, 'anggota' => $anggota, 'riwayat' => $riwayat->fetchAll(PDO::FETCH_ASSOC), ]); } elseif ($action === 'statistik') { updateKemandirianBatch($pdo, false); $thresholdSk = getKonfig($pdo, 'threshold_skor_mandiri', 70); echo json_encode([ 'success' => true, 'data' => [ 'distribusi_status' => $pdo->query(" SELECT status_kemandirian as status, COUNT(*) as jumlah, ROUND(AVG(skor_kemandirian)) as avg_skor, ROUND(AVG(penghasilan_bulanan)) as avg_penghasilan FROM rumah_warga GROUP BY status_kemandirian ORDER BY avg_skor DESC ")->fetchAll(PDO::FETCH_ASSOC), 'siap_exit_plan' => (int)$pdo->query(" SELECT COUNT(*) FROM rumah_warga WHERE skor_kemandirian >= $thresholdSk ")->fetchColumn(), 'sudah_punya_exit_plan' => (int)$pdo->query(" SELECT COUNT(*) FROM rumah_warga WHERE tanggal_exit_plan IS NOT NULL ")->fetchColumn(), 'avg_skor_global' => (float)$pdo->query(" SELECT ROUND(AVG(skor_kemandirian),1) FROM rumah_warga ")->fetchColumn(), 'trend_bulan_ini' => $pdo->query(" SELECT DATE(tanggal) as tgl, ROUND(AVG(skor)) as avg_skor, COUNT(*) as jumlah FROM riwayat_kemandirian WHERE tanggal >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY DATE(tanggal) ORDER BY tgl ")->fetchAll(PDO::FETCH_ASSOC), ], ]); } } elseif ($method === 'POST') { // Catat perkembangan/catatan manual dari petugas $body = json_decode(file_get_contents('php://input'), true); $wargaId = (int)($body['warga_id'] ?? 0); if (!$wargaId) { echo json_encode(['success' => false, 'message' => 'warga_id wajib']); exit; } $stmt = $pdo->prepare(" UPDATE rumah_warga SET catatan_kemandirian = ?, tanggal_exit_plan = ?, updated_at = NOW() WHERE id = ? "); $stmt->execute([ $body['catatan'] ?? null, $body['tanggal_exit_plan'] ?? null, $wargaId, ]); // Hitung ulang dan simpan ke riwayat $warga = $pdo->prepare("SELECT * FROM rumah_warga WHERE id = ?"); $warga->execute([$wargaId]); $w = $warga->fetch(PDO::FETCH_ASSOC); $stmtA = $pdo->prepare("SELECT * FROM anggota_keluarga WHERE rumah_warga_id = ?"); $stmtA->execute([$wargaId]); $anggota = $stmtA->fetchAll(PDO::FETCH_ASSOC); $thres = getKonfig($pdo, 'threshold_penghasilan_mandiri', 3500000); $bPeng = getKonfig($pdo, 'bobot_penghasilan', 40); $bKerja = getKonfig($pdo, 'bobot_status_kerja', 35); $bTangg = getKonfig($pdo, 'bobot_tanggungan', 25); $calc = hitungSkorKemandirian($pdo, $w, $anggota, $thres, $bPeng, $bKerja, $bTangg); $pdo->prepare(" INSERT INTO riwayat_kemandirian (rumah_warga_id, tanggal, skor, status, penghasilan_saat_ini, catatan) VALUES (?, CURDATE(), ?, ?, ?, ?) ")->execute([ $wargaId, $calc['skor_total'], $calc['status'], (float)$w['penghasilan_bulanan'], $body['catatan'] ?? null ]); echo json_encode([ 'success' => true, 'message' => 'Catatan kemandirian berhasil disimpan', 'skor' => $calc, ]); } elseif ($method === 'PUT') { // Update status kemandirian manual oleh admin (override sistem) $body = json_decode(file_get_contents('php://input'), true); $wargaId = (int)($body['warga_id'] ?? 0); $stmt = $pdo->prepare(" UPDATE rumah_warga SET status_kemandirian = ?, catatan_kemandirian = ?, tanggal_exit_plan = ?, updated_at = NOW() WHERE id = ? "); $stmt->execute([ $body['status_kemandirian'] ?? null, $body['catatan'] ?? null, $body['tanggal_exit_plan'] ?? null, $wargaId, ]); echo json_encode(['success' => true, 'message' => 'Status kemandirian diperbarui']); } } catch (Exception $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => $e->getMessage()]); }