idKelurahan = (int)($_SESSION['id_kelurahan'] ?? 0); } public function dashboard(): void { $stats = $this->db->fetchOne(" SELECT (SELECT COUNT(*) FROM laporan_masyarakat WHERE id_kelurahan=?) as total_laporan, (SELECT COUNT(*) FROM laporan_masyarakat WHERE id_kelurahan=? AND status='menunggu') as laporan_menunggu, (SELECT COUNT(*) FROM laporan_masyarakat WHERE id_kelurahan=? AND status='diproses') as laporan_diproses, (SELECT COUNT(*) FROM warga_miskin WHERE id_kelurahan=? AND status='aktif') as total_warga, (SELECT COUNT(*) FROM warga_miskin WHERE id_kelurahan=? AND level_prioritas='Tinggi' AND status='aktif') as prioritas_tinggi, (SELECT COUNT(*) FROM warga_miskin WHERE id_kelurahan=? AND is_blind_spot=1 AND status='aktif') as blind_spot ", array_fill(0, 6, $this->idKelurahan)); $laporanTerbaru = $this->db->fetchAll(" SELECT l.*, k.nama as kelurahan FROM laporan_masyarakat l JOIN kelurahan k ON l.id_kelurahan = k.id WHERE l.id_kelurahan = ? ORDER BY l.created_at DESC LIMIT 10 ", [$this->idKelurahan]); $this->view('verifikator.dashboard', [ 'title' => 'Dashboard Verifikator', 'stats' => $stats, 'laporanTerbaru'=> $laporanTerbaru, 'user' => Auth::user() ], 'verifikator'); } public function laporan(): void { $status = $_GET['status'] ?? 'semua'; $sql = "SELECT l.*, k.nama as kelurahan, kec.nama as kecamatan FROM laporan_masyarakat l JOIN kelurahan k ON l.id_kelurahan = k.id JOIN kecamatan kec ON k.id_kecamatan = kec.id WHERE l.id_kelurahan = ?"; $params = [$this->idKelurahan]; if ($status !== 'semua') { $sql .= " AND l.status = ?"; $params[] = $status; } $sql .= " ORDER BY l.created_at DESC"; $data = $this->db->fetchAll($sql, $params); $this->view('verifikator.laporan', [ 'title' => 'Daftar Laporan', 'data' => $data, 'status' => $status, 'user' => Auth::user() ], 'verifikator'); } // FIX: type hint string, cast to int internally (router always passes strings) public function detailLaporan(string $id): void { $id = (int) $id; $laporan = $this->db->fetchOne(" SELECT l.*, k.nama as kelurahan, kec.nama as kecamatan FROM laporan_masyarakat l JOIN kelurahan k ON l.id_kelurahan = k.id JOIN kecamatan kec ON k.id_kecamatan = kec.id WHERE l.id = ? AND l.id_kelurahan = ? ", [$id, $this->idKelurahan]); if (!$laporan) { $this->flash('error', 'Laporan tidak ditemukan.'); $this->redirect(APP_URL . '/verifikator/laporan'); } $this->view('verifikator.detail_laporan', [ 'title' => 'Detail Laporan', 'laporan' => $laporan, 'user' => Auth::user() ], 'verifikator'); } // FIX: type hint string, cast to int internally public function prosesLaporan(string $id): void { $id = (int) $id; if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/verifikator/laporan'); $aksi = $_POST['aksi'] ?? ''; $catatan = trim($_POST['catatan'] ?? ''); if ($aksi === 'proses') { $this->db->update('laporan_masyarakat', [ 'status' => 'diproses', 'id_verifikator' => Auth::id(), 'catatan_verifikator' => $catatan ], 'id = ? AND id_kelurahan = ?', [$id, $this->idKelurahan]); $this->flash('success', 'Laporan ditandai sebagai diproses.'); } elseif ($aksi === 'tolak') { $this->db->update('laporan_masyarakat', [ 'status' => 'ditolak', 'id_verifikator' => Auth::id(), 'catatan_verifikator'=> $catatan, 'tanggal_verifikasi' => date('Y-m-d H:i:s') ], 'id = ? AND id_kelurahan = ?', [$id, $this->idKelurahan]); $this->flash('info', 'Laporan ditolak.'); } elseif ($aksi === 'verifikasi') { // Verifikasi = buat warga miskin baru dari laporan $laporan = $this->db->fetchOne("SELECT * FROM laporan_masyarakat WHERE id=?", [$id]); if ($laporan) { $this->db->update('laporan_masyarakat', [ 'status' => 'terverifikasi', 'id_verifikator' => Auth::id(), 'catatan_verifikator'=> $catatan, 'tanggal_verifikasi' => date('Y-m-d H:i:s') ], 'id = ?', [$id]); // Cek apakah warga sudah ada $existing = $this->db->fetchOne("SELECT id FROM warga_miskin WHERE id_laporan = ?", [$id]); if (!$existing) { // Buat entry warga miskin baru $isBlindSpot = isInBlindSpot((float)$laporan['lat'], (float)$laporan['lng']) ? 1 : 0; $ri = findRumahIbadahTerdekat((float)$laporan['lat'], (float)$laporan['lng']); $idRi = ($ri && !$isBlindSpot) ? $ri['id'] : null; // Jika dalam radius, gunakan yang terdekat dan dalam radius if ($ri && !$isBlindSpot) { $dist = haversine((float)$laporan['lat'], (float)$laporan['lng'], (float)$ri['lat'], (float)$ri['lng']); if ($dist > $ri['radius_meter']) { $idRi = null; $isBlindSpot = 1; } } $wargaData = [ 'id_laporan' => $id, 'id_kelurahan' => $laporan['id_kelurahan'], 'id_rumah_ibadah' => $idRi, 'id_verifikator' => Auth::id(), 'nama' => $laporan['nama_warga'], 'alamat' => $laporan['alamat_warga'], 'lat' => $laporan['lat'], 'lng' => $laporan['lng'], 'is_blind_spot' => $isBlindSpot, 'status' => 'aktif', 'tanggal_verifikasi' => date('Y-m-d') ]; // Hitung skor prioritas awal $skor = hitungSkorPrioritas($wargaData); $wargaData['skor_prioritas'] = $skor['skor']; $wargaData['level_prioritas'] = $skor['level']; $this->db->insert('warga_miskin', $wargaData); } } Auth::logActivity(Auth::id(), Auth::user()['nama'], 'verifikator', 'Verifikasi Laporan', 'verifikasi', "Verifikasi laporan ID $id"); $this->flash('success', 'Laporan berhasil diverifikasi. Data warga telah ditambahkan.'); } $this->redirect(APP_URL . '/verifikator/laporan/' . $id); } public function warga(): void { $level = $_GET['level'] ?? 'semua'; $sql = "SELECT w.*, k.nama as kelurahan, ri.nama as rumah_ibadah FROM warga_miskin w JOIN kelurahan k ON w.id_kelurahan = k.id LEFT JOIN rumah_ibadah ri ON w.id_rumah_ibadah = ri.id WHERE w.id_kelurahan = ? AND w.status = 'aktif'"; $params = [$this->idKelurahan]; if ($level !== 'semua') { $sql .= " AND w.level_prioritas = ?"; $params[] = $level; } $sql .= " ORDER BY w.skor_prioritas DESC, w.nama"; $data = $this->db->fetchAll($sql, $params); $this->view('verifikator.warga', [ 'title' => 'Data Warga Miskin', 'data' => $data, 'level' => $level, 'user' => Auth::user() ], 'verifikator'); } // FIX: type hint string, cast to int internally public function detailWarga(string $id): void { $id = (int) $id; $warga = $this->db->fetchOne(" SELECT w.*, k.nama as kelurahan, kec.nama as kecamatan, ri.nama as rumah_ibadah_nama, ri.lat as ri_lat, ri.lng as ri_lng FROM warga_miskin w JOIN kelurahan k ON w.id_kelurahan = k.id JOIN kecamatan kec ON k.id_kecamatan = kec.id LEFT JOIN rumah_ibadah ri ON w.id_rumah_ibadah = ri.id WHERE w.id = ? AND w.id_kelurahan = ? ", [$id, $this->idKelurahan]); if (!$warga) { $this->flash('error', 'Data tidak ditemukan.'); $this->redirect(APP_URL . '/verifikator/warga'); } $bantuan = $this->db->fetchAll(" SELECT hb.*, kb.nama as kategori, ri.nama as rumah_ibadah FROM histori_bantuan hb JOIN kategori_bantuan kb ON hb.id_kategori_bantuan = kb.id JOIN rumah_ibadah ri ON hb.id_rumah_ibadah = ri.id WHERE hb.id_warga = ? ORDER BY hb.tanggal_penyaluran DESC ", [$id]); $this->view('verifikator.detail_warga', [ 'title' => 'Detail Warga', 'warga' => $warga, 'bantuan' => $bantuan, 'user' => Auth::user() ], 'verifikator'); } public function formTambahWarga(): void { $kelurahan = $this->db->fetchOne("SELECT k.id, k.nama, kec.nama as kecamatan FROM kelurahan k JOIN kecamatan kec ON k.id_kecamatan=kec.id WHERE k.id=?", [$this->idKelurahan]); $this->view('verifikator.form_warga', [ 'title' => 'Tambah Data Warga', 'kelurahan' => $kelurahan, 'user' => Auth::user() ], 'verifikator'); } public function storeWarga(): void { if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/verifikator/warga'); $lat = (float)$_POST['lat']; $lng = (float)$_POST['lng']; // Blind spot check $isBlindSpot = isInBlindSpot($lat, $lng) ? 1 : 0; // Assignment rumah ibadah $ri = findRumahIbadahTerdekat($lat, $lng); $idRi = null; if ($ri && !$isBlindSpot) { $dist = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']); $idRi = ($dist <= $ri['radius_meter']) ? $ri['id'] : null; if (!$idRi) $isBlindSpot = 1; } $penghasilan = (int)$_POST['penghasilan_bulanan']; $tanggungan = (int)$_POST['jumlah_tanggungan']; $statusKemiskinan = tentukanStatusKemiskinan($penghasilan, $tanggungan); $tanggalLahir = $_POST['tanggal_lahir'] ?? ''; $isLansia = 0; if ($tanggalLahir) { $umur = hitungUmur($tanggalLahir); if ($umur >= 60) $isLansia = 1; } $data = [ 'id_kelurahan' => $this->idKelurahan, 'id_rumah_ibadah' => $idRi, 'id_verifikator' => Auth::id(), 'nik' => trim($_POST['nik'] ?? '') ?: null, 'nama' => trim($_POST['nama']), 'tempat_lahir' => trim($_POST['tempat_lahir'] ?? ''), 'tanggal_lahir' => $tanggalLahir ?: null, 'jenis_kelamin' => $_POST['jenis_kelamin'] ?? 'L', 'agama' => $_POST['agama'] ?? 'Islam', 'alamat' => trim($_POST['alamat']), 'lat' => $lat, 'lng' => $lng, 'no_hp' => trim($_POST['no_hp'] ?? ''), 'pendidikan_terakhir'=> $_POST['pendidikan_terakhir'] ?? 'SD', 'pekerjaan' => trim($_POST['pekerjaan'] ?? ''), 'penghasilan_bulanan'=> $penghasilan, 'jumlah_tanggungan' => $tanggungan, 'kondisi_rumah' => $_POST['kondisi_rumah'] ?? 'Tidak Layak', 'riwayat_penyakit' => trim($_POST['riwayat_penyakit'] ?? ''), 'is_lansia' => $isLansia, 'is_disabilitas' => (int)($_POST['is_disabilitas'] ?? 0), 'disabilitas_jenis' => trim($_POST['disabilitas_jenis'] ?? ''), 'status_kemiskinan' => $statusKemiskinan, 'is_blind_spot' => $isBlindSpot, 'status' => 'aktif', 'tanggal_verifikasi' => date('Y-m-d') ]; $foto = null; if (!empty($_FILES['foto']['name'])) { $foto = $this->uploadFile($_FILES['foto'], 'warga'); } $data['foto'] = $foto; $skor = hitungSkorPrioritas($data); $data['skor_prioritas'] = $skor['skor']; $data['level_prioritas'] = $skor['level']; try { $this->db->insert('warga_miskin', $data); Auth::logActivity(Auth::id(), Auth::user()['nama'], 'verifikator', 'Tambah Warga', 'warga_miskin', 'Tambah: ' . $data['nama']); $this->flash('success', 'Data warga berhasil ditambahkan.' . ($isBlindSpot ? ' PERHATIAN: Warga ini terdeteksi sebagai Blind Spot.' : '')); } catch (Exception $e) { $this->flash('error', 'Gagal menyimpan. NIK mungkin sudah terdaftar.'); } $this->redirect(APP_URL . '/verifikator/warga'); } // FIX: type hint string, cast to int internally public function updateWarga(string $id): void { $id = (int) $id; if (!$this->verifyCsrf()) $this->redirect(APP_URL . '/verifikator/warga'); $lat = (float)$_POST['lat']; $lng = (float)$_POST['lng']; $isBlindSpot = isInBlindSpot($lat, $lng) ? 1 : 0; $ri = findRumahIbadahTerdekat($lat, $lng); $idRi = null; if ($ri && !$isBlindSpot) { $dist = haversine($lat, $lng, (float)$ri['lat'], (float)$ri['lng']); $idRi = ($dist <= $ri['radius_meter']) ? $ri['id'] : null; if (!$idRi) $isBlindSpot = 1; } $penghasilan = (int)$_POST['penghasilan_bulanan']; $tanggungan = (int)$_POST['jumlah_tanggungan']; $tanggalLahir = $_POST['tanggal_lahir'] ?? ''; $isLansia = ($tanggalLahir && hitungUmur($tanggalLahir) >= 60) ? 1 : 0; $data = [ 'id_rumah_ibadah' => $idRi, 'nik' => trim($_POST['nik'] ?? '') ?: null, 'nama' => trim($_POST['nama']), 'tempat_lahir' => trim($_POST['tempat_lahir'] ?? ''), 'tanggal_lahir' => $tanggalLahir ?: null, 'jenis_kelamin' => $_POST['jenis_kelamin'] ?? 'L', 'agama' => $_POST['agama'] ?? 'Islam', 'alamat' => trim($_POST['alamat']), 'lat' => $lat, 'lng' => $lng, 'pendidikan_terakhir'=> $_POST['pendidikan_terakhir'] ?? 'SD', 'pekerjaan' => trim($_POST['pekerjaan'] ?? ''), 'penghasilan_bulanan'=> $penghasilan, 'jumlah_tanggungan' => $tanggungan, 'kondisi_rumah' => $_POST['kondisi_rumah'] ?? 'Tidak Layak', 'riwayat_penyakit' => trim($_POST['riwayat_penyakit'] ?? ''), 'is_lansia' => $isLansia, 'is_disabilitas' => (int)($_POST['is_disabilitas'] ?? 0), 'disabilitas_jenis' => trim($_POST['disabilitas_jenis'] ?? ''), 'status_kemiskinan' => tentukanStatusKemiskinan($penghasilan, $tanggungan), 'is_blind_spot' => $isBlindSpot ]; $skor = hitungSkorPrioritas($data); $data['skor_prioritas'] = $skor['skor']; $data['level_prioritas'] = $skor['level']; if (!empty($_FILES['foto']['name'])) { $data['foto'] = $this->uploadFile($_FILES['foto'], 'warga'); } $this->db->update('warga_miskin', $data, 'id = ? AND id_kelurahan = ?', [$id, $this->idKelurahan]); Auth::logActivity(Auth::id(), Auth::user()['nama'], 'verifikator', 'Update Warga', 'warga_miskin', "Update warga ID $id"); $this->flash('success', 'Data warga berhasil diperbarui.'); $this->redirect(APP_URL . '/verifikator/warga/' . $id); } public function apiStats(): void { $data = $this->db->fetchOne(" SELECT COUNT(CASE WHEN status='menunggu' THEN 1 END) as menunggu, COUNT(CASE WHEN status='diproses' THEN 1 END) as diproses, COUNT(CASE WHEN status='terverifikasi' THEN 1 END) as terverifikasi, COUNT(CASE WHEN status='ditolak' THEN 1 END) as ditolak FROM laporan_masyarakat WHERE id_kelurahan = ? ", [$this->idKelurahan]); $this->json(['success' => true, 'data' => $data]); } }