445 lines
18 KiB
PHP
445 lines
18 KiB
PHP
<?php
|
|
// api/kemandirian.php
|
|
// Fitur 3: Potensi Kemandirian Warga
|
|
// Hitung skor kemandirian berdasarkan: penghasilan, status kerja anggota, jumlah tanggungan
|
|
// Method: GET (list/detail/statistik), POST (catat perkembangan manual), PUT (set exit plan)
|
|
|
|
header('Content-Type: application/json');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: GET, POST, PUT, 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
|
|
───────────────────────────────────────────── */
|
|
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'],
|
|
};
|
|
}
|
|
|
|
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()]);
|
|
} |