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 skor kepercayaan data Formula: mulai 100, kurangi seiring waktu Decay rate: linear -100/interval_bulan per bulan ───────────────────────────────────────────── */ function hitungSkorKepercayaan( ?string $tanggalVerifikasi, int $intervalBulan, int $minSkor = 40 ): array { if (!$tanggalVerifikasi) { return ['skor' => 0, 'hari_sejak' => null, 'status' => 'kadaluarsa']; } $tglVerif = new DateTime($tanggalVerifikasi); $today = new DateTime(); // Jika tanggal verifikasi di masa depan (karena beda timezone / lag server), anggap 0 hari if ($tglVerif > $today) { $hariSejak = 0; } else { $diff = $today->diff($tglVerif); $hariSejak = $diff->days; } $intervalHari = $intervalBulan * 30; // Skor linear: 100 di hari 0, turun ke 0 di hari intervalHari $skor = max(0, (int)round(100 - ($hariSejak / $intervalHari * 100))); $status = match(true) { $skor >= 80 => 'terverifikasi', $skor >= $minSkor => 'perlu_update', default => 'kadaluarsa', }; // Berapa hari lagi sampai perlu_update dan kadaluarsa $hariMenujuPerluUpdate = max(0, (int)($intervalHari * 0.2) - $hariSejak); $hariMenujuKadaluarsa = max(0, (int)($intervalHari * ((100 - $minSkor) / 100)) - $hariSejak); return [ 'skor' => $skor, 'hari_sejak_verifikasi' => $hariSejak, 'interval_hari' => $intervalHari, 'status' => $status, 'hari_menuju_perlu_update' => $hariMenujuPerluUpdate, 'hari_menuju_kadaluarsa' => $hariMenujuKadaluarsa, 'tanggal_kadaluarsa' => (clone $tglVerif)->modify("+{$intervalHari} days")->format('Y-m-d'), ]; } /* ───────────────────────────────────────────── UPDATE skor semua warga secara batch Formula: mulai 100, kurangi seiring waktu ───────────────────────────────────────────── */ function updateSkopBatch(PDO $pdo): int { $interval = getKonfig($pdo, 'interval_verifikasi_bulan', 6); $minSkor = getKonfig($pdo, 'skor_data_kadaluarsa', 40); $wargaList = $pdo->query("SELECT id, tanggal_verifikasi FROM rumah_warga")->fetchAll(PDO::FETCH_ASSOC); $updated = 0; $stmt = $pdo->prepare(" UPDATE rumah_warga SET skor_kepercayaan = ?, status_verifikasi = ? WHERE id = ? "); foreach ($wargaList as $w) { $calc = hitungSkorKepercayaan($w['tanggal_verifikasi'], $interval, $minSkor); $stmt->execute([$calc['skor'], $calc['status'], (int)$w['id']]); $updated++; } return $updated; } /* ───────────────────────────────────────────── ROUTING ───────────────────────────────────────────── */ try { $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? 'list'; if ($method === 'GET') { // Hanya Admin dan Petugas Lapangan yang boleh melihat data verifikasi requireAuth(['admin', 'petugas']); if ($action === 'list') { // Daftar warga yang perlu diverifikasi $statusFilter = $_GET['status'] ?? null; // terverifikasi|perlu_update|kadaluarsa $limit = (int)($_GET['limit'] ?? 50); $offset = (int)($_GET['offset'] ?? 0); // Update skor dulu secara batch updateSkopBatch($pdo); if ($statusFilter === 'semua') { $where = ""; } elseif ($statusFilter === 'terverifikasi' || $statusFilter === 'perlu_update' || $statusFilter === 'kadaluarsa') { $where = "WHERE rw.status_verifikasi = " . $pdo->quote($statusFilter); } else { // Default: Hanya warga yang perlu verifikasi (perlu_update atau kadaluarsa) $where = "WHERE rw.status_verifikasi IN ('perlu_update', 'kadaluarsa')"; } $sql = " SELECT rw.id, rw.nama_kepala_keluarga, rw.no_kk, rw.kelurahan, rw.kecamatan, rw.tanggal_verifikasi, rw.petugas_verifikasi, rw.skor_kepercayaan, rw.status_verifikasi, rw.kategori_kemiskinan, rw.status_bantuan, rw.latitude, rw.longitude FROM rumah_warga rw $where ORDER BY rw.skor_kepercayaan ASC, rw.tanggal_verifikasi ASC LIMIT $limit OFFSET $offset "; $rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC); // Enrichment: hitung detail decay per row $interval = getKonfig($pdo, 'interval_verifikasi_bulan', 6); $minSkor = getKonfig($pdo, 'skor_data_kadaluarsa', 40); foreach ($rows as &$r) { $r['detail_skor'] = hitungSkorKepercayaan( $r['tanggal_verifikasi'], $interval, $minSkor ); } // Summary count $summary = $pdo->query(" SELECT status_verifikasi, COUNT(*) as jumlah FROM rumah_warga GROUP BY status_verifikasi ")->fetchAll(PDO::FETCH_KEY_PAIR); echo json_encode([ 'success' => true, 'data' => $rows, 'summary' => $summary, 'total' => count($rows), ]); } elseif ($action === 'config') { // Ambil semua konfigurasi $rows = $pdo->query("SELECT * FROM konfigurasi_sistem ORDER BY id")->fetchAll(PDO::FETCH_ASSOC); echo json_encode(['success' => true, 'data' => $rows]); } elseif ($action === 'riwayat') { // Riwayat verifikasi untuk 1 warga $wargaId = (int)($_GET['warga_id'] ?? 0); $rows = $pdo->prepare(" SELECT * FROM riwayat_verifikasi WHERE rumah_warga_id = ? ORDER BY tanggal DESC LIMIT 20 "); $rows->execute([$wargaId]); echo json_encode(['success' => true, 'data' => $rows->fetchAll(PDO::FETCH_ASSOC)]); } elseif ($action === 'statistik') { updateSkopBatch($pdo); $data = [ 'per_status' => $pdo->query(" SELECT status_verifikasi, COUNT(*) as jumlah, ROUND(AVG(skor_kepercayaan)) as avg_skor FROM rumah_warga GROUP BY status_verifikasi ")->fetchAll(PDO::FETCH_ASSOC), 'avg_skor_global' => (float)$pdo->query("SELECT ROUND(AVG(skor_kepercayaan),1) FROM rumah_warga")->fetchColumn(), 'paling_lama' => $pdo->query(" SELECT nama_kepala_keluarga, tanggal_verifikasi, skor_kepercayaan FROM rumah_warga ORDER BY skor_kepercayaan ASC LIMIT 5 ")->fetchAll(PDO::FETCH_ASSOC), ]; echo json_encode(['success' => true, 'data' => $data]); } } elseif ($method === 'POST') { // Catat verifikasi baru untuk 1 warga // Hanya Admin, Petugas Lapangan, dan Koordinator $currentUser = requireAuth(['admin', 'petugas', 'koordinator']); $body = json_decode(file_get_contents('php://input'), true); $wargaId = (int)($body['warga_id'] ?? 0); $petugas = trim($body['petugas'] ?? ''); $catatan = $body['catatan'] ?? null; $tanggal = $body['tanggal'] ?? date('Y-m-d'); if (!$wargaId) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'warga_id wajib diisi']); exit; } // Jika koordinator, lakukan pengecekan wilayah if ($currentUser['role'] === 'koordinator') { $koorIbadahId = (int)$currentUser['rumah_ibadah_id']; $wilayahRes = $pdo->prepare("SELECT kelurahan, kecamatan FROM rumah_ibadah WHERE id = ?"); $wilayahRes->execute([$koorIbadahId]); if ($wil = $wilayahRes->fetch(PDO::FETCH_ASSOC)) { $kel = $wil['kelurahan']; $kec = $wil['kecamatan']; $wargaRes = $pdo->prepare("SELECT kelurahan, kecamatan FROM rumah_warga WHERE id = ?"); $wargaRes->execute([$wargaId]); if ($wargaRow = $wargaRes->fetch(PDO::FETCH_ASSOC)) { $wKel = strtolower(trim($wargaRow['kelurahan'] ?? '')); $wKec = strtolower(trim($wargaRow['kecamatan'] ?? '')); if (strpos($wKel, strtolower($kel)) === false && strpos($wKec, strtolower($kec)) === false) { http_response_code(403); echo json_encode(['success' => false, 'message' => 'Forbidden: Warga di luar wilayah tugas Anda.']); exit; } } else { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Warga tidak ditemukan.']); exit; } } else { http_response_code(403); echo json_encode(['success' => false, 'message' => 'Forbidden: Wilayah tugas tidak terkonfigurasi.']); exit; } } // Ambil skor lama $oldRow = $pdo->prepare("SELECT skor_kepercayaan, status_verifikasi FROM rumah_warga WHERE id = ?"); $oldRow->execute([$wargaId]); $old = $oldRow->fetch(PDO::FETCH_ASSOC); $skorLama = $old ? (int)$old['skor_kepercayaan'] : null; // Reset skor ke 100 & update tanggal verifikasi $stmt = $pdo->prepare(" UPDATE rumah_warga SET tanggal_verifikasi = ?, petugas_verifikasi = ?, skor_kepercayaan = 100, status_verifikasi = 'terverifikasi', updated_at = NOW() WHERE id = ? "); $stmt->execute([$tanggal, $petugas ?: null, $wargaId]); // Simpan riwayat $riwayat = $pdo->prepare(" INSERT INTO riwayat_verifikasi (rumah_warga_id, tanggal, petugas, skor_sebelum, skor_sesudah, catatan) VALUES (?, ?, ?, ?, 100, ?) "); $riwayat->execute([$wargaId, $tanggal, $petugas ?: null, $skorLama, $catatan]); echo json_encode([ 'success' => true, 'message' => 'Verifikasi berhasil dicatat. Skor kepercayaan data dikembalikan ke 100.', 'skor_lama' => $skorLama, 'skor_baru' => 100, ]); } elseif ($method === 'PUT') { // Update konfigurasi sistem // HANYA Admin yang boleh merubah konfigurasi sistem requireAuth(['admin']); $body = json_decode(file_get_contents('php://input'), true); if (!is_array($body)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Body tidak valid']); exit; } $stmt = $pdo->prepare("UPDATE konfigurasi_sistem SET nilai = ? WHERE kunci = ?"); $updated = 0; foreach ($body as $kunci => $nilai) { $stmt->execute([(string)$nilai, $kunci]); $updated += $stmt->rowCount(); } // Setelah konfigurasi diupdate, recalculate semua skor updateSkopBatch($pdo); echo json_encode([ 'success' => true, 'message' => "$updated konfigurasi berhasil diperbarui. Skor kepercayaan seluruh data telah dihitung ulang.", ]); } } catch (Exception $e) { http_response_code(500); echo json_encode(['success' => false, 'message' => $e->getMessage()]); }