commit df18a7a99a635de41bc0f8ed381da6f683e19266 Author: L4zyL4mb Date: Wed Jun 10 21:46:56 2026 +0700 UAS SIG Proyek WebGis diff --git a/SIG_Tanah_Jalan/api/jalan.php b/SIG_Tanah_Jalan/api/jalan.php new file mode 100644 index 0000000..109dbe5 --- /dev/null +++ b/SIG_Tanah_Jalan/api/jalan.php @@ -0,0 +1,141 @@ +prepare("SELECT * FROM jalan WHERE id = ?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + if ($row) { + $row['geometry'] = json_decode($row['geometry']); + echo json_encode(['success' => true, 'data' => $row]); + } else { + http_response_code(404); + echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']); + } + } else { + $result = $db->query("SELECT * FROM jalan ORDER BY created_at DESC"); + $data = []; + while ($row = $result->fetch_assoc()) { + $row['geometry'] = json_decode($row['geometry']); + $data[] = $row; + } + echo json_encode(['success' => true, 'data' => $data]); + } + break; + + // ── CREATE: Tambah jalan baru ─────────────────────────────────── + case 'POST': + $input = json_decode(file_get_contents('php://input'), true); + + if ( + empty($input['nama_jalan']) || + empty($input['status_jalan']) || + empty($input['geometry']) || + !isset($input['panjang']) + ) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']); + break; + } + + $nama_jalan = trim($input['nama_jalan']); + $status_jalan = $input['status_jalan']; + $panjang = floatval($input['panjang']); + $geometry = json_encode($input['geometry']); + + $valid_status = ['Nasional', 'Provinsi', 'Kabupaten']; + if (!in_array($status_jalan, $valid_status)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Status jalan tidak valid']); + break; + } + + $stmt = $db->prepare( + "INSERT INTO jalan (nama_jalan, status_jalan, panjang, geometry) VALUES (?, ?, ?, ?)" + ); + $stmt->bind_param('ssds', $nama_jalan, $status_jalan, $panjang, $geometry); + + if ($stmt->execute()) { + echo json_encode([ + 'success' => true, + 'message' => 'Data jalan berhasil disimpan', + 'id' => $db->insert_id + ]); + } else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data']); + } + break; + + // ── UPDATE: Edit jalan ────────────────────────────────────────── + case 'PUT': + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID diperlukan']); + break; + } + + $id = intval($input['id']); + $nama_jalan = trim($input['nama_jalan']); + $status_jalan = $input['status_jalan']; + $panjang = floatval($input['panjang']); + $geometry = json_encode($input['geometry']); + + $stmt = $db->prepare( + "UPDATE jalan SET nama_jalan=?, status_jalan=?, panjang=?, geometry=? WHERE id=?" + ); + $stmt->bind_param('ssdsi', $nama_jalan, $status_jalan, $panjang, $geometry, $id); + + if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Data jalan berhasil diupdate']); + } else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal mengupdate data']); + } + break; + + // ── DELETE: Hapus jalan ───────────────────────────────────────── + case 'DELETE': + $input = json_decode(file_get_contents('php://input'), true); + $id = intval($input['id'] ?? ($_GET['id'] ?? 0)); + + if (!$id) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID diperlukan']); + break; + } + + $stmt = $db->prepare("DELETE FROM jalan WHERE id = ?"); + $stmt->bind_param('i', $id); + + if ($stmt->execute() && $stmt->affected_rows > 0) { + echo json_encode(['success' => true, 'message' => 'Data jalan berhasil dihapus']); + } else { + http_response_code(404); + echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']); + } + break; + + default: + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']); +} + +$db->close(); +?> \ No newline at end of file diff --git a/SIG_Tanah_Jalan/api/kerusakan.php b/SIG_Tanah_Jalan/api/kerusakan.php new file mode 100644 index 0000000..a694685 --- /dev/null +++ b/SIG_Tanah_Jalan/api/kerusakan.php @@ -0,0 +1,194 @@ +false,'message'=>'ID diperlukan']); exit; } + $stmt = $db->prepare("SELECT * FROM histori_kerusakan WHERE kerusakan_id=? ORDER BY tgl_perubahan DESC"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $result = $stmt->get_result(); + $data = []; + while ($row = $result->fetch_assoc()) $data[] = $row; + echo json_encode(['success'=>true, 'data'=>$data]); + $db->close(); exit; +} + +if ($action === 'update_status') { + // POST update status + catat histori + $input = json_decode(file_get_contents('php://input'), true); + $id = intval($input['id'] ?? 0); + $status_baru = $input['status_baru'] ?? ''; + $catatan = $input['catatan'] ?? ''; + if (!$id || !$status_baru) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID dan status_baru diperlukan']); exit; } + + // Ambil status lama + $stmt = $db->prepare("SELECT status_laporan FROM kerusakan_jalan WHERE id=?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $row = $stmt->get_result()->fetch_assoc(); + if (!$row) { http_response_code(404); echo json_encode(['success'=>false,'message'=>'Data tidak ditemukan']); exit; } + + $status_lama = $row['status_laporan']; + + // Update status + $stmt2 = $db->prepare("UPDATE kerusakan_jalan SET status_laporan=? WHERE id=?"); + $stmt2->bind_param('si', $status_baru, $id); + $stmt2->execute(); + + // Catat histori + $stmt3 = $db->prepare("INSERT INTO histori_kerusakan (kerusakan_id, status_lama, status_baru, catatan) VALUES (?,?,?,?)"); + $stmt3->bind_param('isss', $id, $status_lama, $status_baru, $catatan); + $stmt3->execute(); + + echo json_encode(['success'=>true, 'message'=>'Status kerusakan diupdate']); + $db->close(); exit; +} + +switch ($method) { + + // ── READ ──────────────────────────────────────────────────────── + case 'GET': + if (isset($_GET['id'])) { + $id = intval($_GET['id']); + $stmt = $db->prepare("SELECT * FROM kerusakan_jalan WHERE id=?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $row = $stmt->get_result()->fetch_assoc(); + if ($row) { + echo json_encode(['success'=>true, 'data'=>$row]); + } else { + http_response_code(404); + echo json_encode(['success'=>false, 'message'=>'Data tidak ditemukan']); + } + } else { + // Filter opsional: ?jalan_id=X atau ?status=X + $where = []; + $params = ''; + $vals = []; + if (!empty($_GET['jalan_id'])) { + $where[] = 'jalan_id=?'; $params .= 'i'; $vals[] = intval($_GET['jalan_id']); + } + if (!empty($_GET['status'])) { + $where[] = 'status_laporan=?'; $params .= 's'; $vals[] = $_GET['status']; + } + $sql = "SELECT k.*, j.nama_jalan as jalan_nama FROM kerusakan_jalan k LEFT JOIN jalan j ON k.jalan_id=j.id"; + if ($where) $sql .= ' WHERE '.implode(' AND ', $where); + $sql .= ' ORDER BY k.tgl_laporan DESC'; + $stmt = $db->prepare($sql); + if ($params) $stmt->bind_param($params, ...$vals); + $stmt->execute(); + $result = $stmt->get_result(); + $data = []; + while ($row = $result->fetch_assoc()) $data[] = $row; + + // Hitung kluster per lokasi (radius ~50m) + foreach ($data as &$item) { + $cnt = 0; + foreach ($data as $other) { + $dlat = ($item['lat'] - $other['lat']) * 111000; + $dlng = ($item['lng'] - $other['lng']) * 111000 * cos(deg2rad($item['lat'])); + if (sqrt($dlat*$dlat + $dlng*$dlng) < 50) $cnt++; + } + $item['laporan_terdekat'] = $cnt; + } + echo json_encode(['success'=>true, 'data'=>$data]); + } + break; + + // ── CREATE ────────────────────────────────────────────────────── + case 'POST': + $input = json_decode(file_get_contents('php://input'), true); + + $required = ['deskripsi','tingkat_urgensi','lat','lng']; + foreach ($required as $f) { + if (empty($input[$f]) && $input[$f] !== 0) { + http_response_code(400); + echo json_encode(['success'=>false,'message'=>"Field $f diperlukan"]); + exit; + } + } + + $jalan_id = !empty($input['jalan_id']) ? intval($input['jalan_id']) : null; + $nama_jalan_ref = trim($input['nama_jalan_ref'] ?? ''); + $deskripsi = trim($input['deskripsi']); + $tingkat_urgensi = $input['tingkat_urgensi']; + $lat = floatval($input['lat']); + $lng = floatval($input['lng']); + $foto_base64 = $input['foto_base64'] ?? null; + + $valid_urgensi = ['Rendah','Sedang','Tinggi','Kritis']; + if (!in_array($tingkat_urgensi, $valid_urgensi)) { + http_response_code(400); + echo json_encode(['success'=>false,'message'=>'Tingkat urgensi tidak valid']); + break; + } + + $stmt = $db->prepare( + "INSERT INTO kerusakan_jalan (jalan_id, nama_jalan_ref, deskripsi, tingkat_urgensi, lat, lng, foto_base64) VALUES (?,?,?,?,?,?,?)" + ); + $stmt->bind_param('isssdds', $jalan_id, $nama_jalan_ref, $deskripsi, $tingkat_urgensi, $lat, $lng, $foto_base64); + + if ($stmt->execute()) { + echo json_encode(['success'=>true,'message'=>'Laporan kerusakan berhasil disimpan','id'=>$db->insert_id]); + } else { + http_response_code(500); + echo json_encode(['success'=>false,'message'=>'Gagal menyimpan data']); + } + break; + + // ── UPDATE ────────────────────────────────────────────────────── + case 'PUT': + $input = json_decode(file_get_contents('php://input'), true); + $id = intval($input['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID diperlukan']); break; } + + $deskripsi = trim($input['deskripsi'] ?? ''); + $tingkat_urgensi = $input['tingkat_urgensi'] ?? ''; + $nama_jalan_ref = trim($input['nama_jalan_ref'] ?? ''); + + $stmt = $db->prepare("UPDATE kerusakan_jalan SET deskripsi=?, tingkat_urgensi=?, nama_jalan_ref=? WHERE id=?"); + $stmt->bind_param('sssi', $deskripsi, $tingkat_urgensi, $nama_jalan_ref, $id); + + if ($stmt->execute()) { + echo json_encode(['success'=>true,'message'=>'Data kerusakan diupdate']); + } else { + http_response_code(500); + echo json_encode(['success'=>false,'message'=>'Gagal mengupdate data']); + } + break; + + // ── DELETE ────────────────────────────────────────────────────── + case 'DELETE': + $input = json_decode(file_get_contents('php://input'), true); + $id = intval($input['id'] ?? ($_GET['id'] ?? 0)); + if (!$id) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID diperlukan']); break; } + + $stmt = $db->prepare("DELETE FROM kerusakan_jalan WHERE id=?"); + $stmt->bind_param('i', $id); + if ($stmt->execute() && $stmt->affected_rows > 0) { + echo json_encode(['success'=>true,'message'=>'Laporan kerusakan dihapus']); + } else { + http_response_code(404); + echo json_encode(['success'=>false,'message'=>'Data tidak ditemukan']); + } + break; + + default: + http_response_code(405); + echo json_encode(['success'=>false,'message'=>'Method tidak diizinkan']); +} + +$db->close(); +?> \ No newline at end of file diff --git a/SIG_Tanah_Jalan/api/tanah.php b/SIG_Tanah_Jalan/api/tanah.php new file mode 100644 index 0000000..4ae7f5e --- /dev/null +++ b/SIG_Tanah_Jalan/api/tanah.php @@ -0,0 +1,144 @@ +prepare("SELECT * FROM tanah WHERE id = ?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + if ($row) { + $row['geometry'] = json_decode($row['geometry']); + echo json_encode(['success' => true, 'data' => $row]); + } else { + http_response_code(404); + echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']); + } + } else { + $result = $db->query("SELECT * FROM tanah ORDER BY created_at DESC"); + $data = []; + while ($row = $result->fetch_assoc()) { + $row['geometry'] = json_decode($row['geometry']); + $data[] = $row; + } + echo json_encode(['success' => true, 'data' => $data]); + } + break; + + // ── CREATE ────────────────────────────────────────────────────── + case 'POST': + $input = json_decode(file_get_contents('php://input'), true); + + if ( + empty($input['kode_kavling']) || + empty($input['nama_pemilik']) || + empty($input['status_tanah']) || + empty($input['geometry']) || + !isset($input['luas']) + ) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']); + break; + } + + $kode_kavling = trim($input['kode_kavling']); + $nama_pemilik = trim($input['nama_pemilik']); + $status_tanah = $input['status_tanah']; + $luas = floatval($input['luas']); + $geometry = json_encode($input['geometry']); + + $valid_status = ['SHM', 'HGB', 'HGU', 'HP']; + if (!in_array($status_tanah, $valid_status)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Status tanah tidak valid']); + break; + } + + $stmt = $db->prepare( + "INSERT INTO tanah (kode_kavling, nama_pemilik, status_tanah, luas, geometry) VALUES (?, ?, ?, ?, ?)" + ); + $stmt->bind_param('sssds', $kode_kavling, $nama_pemilik, $status_tanah, $luas, $geometry); + + if ($stmt->execute()) { + echo json_encode([ + 'success' => true, + 'message' => 'Data tanah berhasil disimpan', + 'id' => $db->insert_id + ]); + } else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data']); + } + break; + + // ── UPDATE ────────────────────────────────────────────────────── + case 'PUT': + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID diperlukan']); + break; + } + + $id = intval($input['id']); + $kode_kavling = trim($input['kode_kavling']); + $nama_pemilik = trim($input['nama_pemilik']); + $status_tanah = $input['status_tanah']; + $luas = floatval($input['luas']); + $geometry = json_encode($input['geometry']); + + $stmt = $db->prepare( + "UPDATE tanah SET kode_kavling=?, nama_pemilik=?, status_tanah=?, luas=?, geometry=? WHERE id=?" + ); + $stmt->bind_param('sssdsi', $kode_kavling, $nama_pemilik, $status_tanah, $luas, $geometry, $id); + + if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Data tanah berhasil diupdate']); + } else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal mengupdate data']); + } + break; + + // ── DELETE ────────────────────────────────────────────────────── + case 'DELETE': + $input = json_decode(file_get_contents('php://input'), true); + $id = intval($input['id'] ?? ($_GET['id'] ?? 0)); + + if (!$id) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID diperlukan']); + break; + } + + $stmt = $db->prepare("DELETE FROM tanah WHERE id = ?"); + $stmt->bind_param('i', $id); + + if ($stmt->execute() && $stmt->affected_rows > 0) { + echo json_encode(['success' => true, 'message' => 'Data tanah berhasil dihapus']); + } else { + http_response_code(404); + echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']); + } + break; + + default: + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']); +} + +$db->close(); +?> \ No newline at end of file diff --git a/SIG_Tanah_Jalan/config.php b/SIG_Tanah_Jalan/config.php new file mode 100644 index 0000000..1b1f624 --- /dev/null +++ b/SIG_Tanah_Jalan/config.php @@ -0,0 +1,31 @@ +connect_error) { + http_response_code(500); + die(json_encode(['error' => 'Koneksi database gagal: ' . $conn->connect_error])); + } + $conn->set_charset('utf8mb4'); + return $conn; +} + +// Header CORS & JSON untuk semua API +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(); +} +?> \ No newline at end of file diff --git a/SIG_Tanah_Jalan/database.sql b/SIG_Tanah_Jalan/database.sql new file mode 100644 index 0000000..7ba88d4 --- /dev/null +++ b/SIG_Tanah_Jalan/database.sql @@ -0,0 +1,80 @@ +-- ============================================ +-- DATABASE SETUP - SIG Tugas Kuliah +-- Manajemen Jalan & Parsil Tanah - Pontianak +-- ============================================ + +CREATE DATABASE IF NOT EXISTS sig_tugas CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE sig_tugas; + +-- Tabel Data Jalan (Polyline) +CREATE TABLE IF NOT EXISTS jalan ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_jalan VARCHAR(100) NOT NULL, + status_jalan ENUM('Nasional','Provinsi','Kabupaten') NOT NULL, + panjang DOUBLE NOT NULL DEFAULT 0 COMMENT 'Panjang dalam meter, dihitung otomatis Leaflet', + geometry LONGTEXT NOT NULL COMMENT 'GeoJSON LineString', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Tabel Data Parsil Tanah (Polygon) +CREATE TABLE IF NOT EXISTS tanah ( + id INT AUTO_INCREMENT PRIMARY KEY, + kode_kavling VARCHAR(50) NOT NULL, + nama_pemilik VARCHAR(100) NOT NULL, + status_tanah ENUM('SHM','HGB','HGU','HP') NOT NULL, + luas DOUBLE NOT NULL DEFAULT 0 COMMENT 'Luas dalam meter persegi, dihitung otomatis Leaflet', + geometry LONGTEXT NOT NULL COMMENT 'GeoJSON Polygon', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- ============================================ +-- Sample Data - Jalan di Pontianak +-- ============================================ +INSERT INTO jalan (nama_jalan, status_jalan, panjang, geometry) VALUES +( + 'Jl. Ahmad Yani', + 'Nasional', + 3250.5, + '{"type":"LineString","coordinates":[[109.3425,-0.0222],[109.3480,-0.0198],[109.3540,-0.0175],[109.3600,-0.0155]]}' +), +( + 'Jl. Gajah Mada', + 'Provinsi', + 1820.3, + '{"type":"LineString","coordinates":[[109.3310,-0.0290],[109.3355,-0.0265],[109.3400,-0.0242]]}' +), +( + 'Jl. Tanjungpura', + 'Kabupaten', + 1100.7, + '{"type":"LineString","coordinates":[[109.3250,-0.0350],[109.3285,-0.0330],[109.3320,-0.0310]]}' +); + +-- ============================================ +-- Sample Data - Parsil Tanah di Pontianak +-- ============================================ +INSERT INTO tanah (kode_kavling, nama_pemilik, status_tanah, luas, geometry) VALUES +( + 'KAV-001', + 'Budi Santoso', + 'SHM', + 1250.75, + '{"type":"Polygon","coordinates":[[[109.3350,-0.0400],[109.3370,-0.0400],[109.3370,-0.0420],[109.3350,-0.0420],[109.3350,-0.0400]]]}' +), +( + 'KAV-002', + 'PT. Maju Bersama', + 'HGB', + 3400.00, + '{"type":"Polygon","coordinates":[[[109.3280,-0.0450],[109.3310,-0.0450],[109.3310,-0.0480],[109.3280,-0.0480],[109.3280,-0.0450]]]}' +), +( + 'KAV-003', + 'CV. Agro Kalimantan', + 'HGU', + 5800.50, + '{"type":"Polygon","coordinates":[[[109.3150,-0.0500],[109.3200,-0.0500],[109.3200,-0.0540],[109.3150,-0.0540],[109.3150,-0.0500]]]}' +); \ No newline at end of file diff --git a/SIG_Tanah_Jalan/db_jalan_update.sql b/SIG_Tanah_Jalan/db_jalan_update.sql new file mode 100644 index 0000000..bda2c2a --- /dev/null +++ b/SIG_Tanah_Jalan/db_jalan_update.sql @@ -0,0 +1,45 @@ +-- ============================================ +-- DATABASE UPDATE - Tambah Tabel Kerusakan Jalan +-- Jalankan query ini setelah database.sql +-- ============================================ + +-- Tabel Laporan Kerusakan Jalan (Point) +CREATE TABLE IF NOT EXISTS kerusakan_jalan ( + id INT AUTO_INCREMENT PRIMARY KEY, + jalan_id INT NULL COMMENT 'Referensi ke tabel jalan (opsional)', + nama_jalan_ref VARCHAR(100) NULL COMMENT 'Nama jalan jika tidak ada di db', + deskripsi TEXT NOT NULL, + tingkat_urgensi ENUM('Rendah','Sedang','Tinggi','Kritis') NOT NULL DEFAULT 'Sedang', + status_laporan ENUM('Dilaporkan','Diverifikasi','Dalam Perbaikan','Sudah Diperbaiki') NOT NULL DEFAULT 'Dilaporkan', + foto_path VARCHAR(255) NULL COMMENT 'Path relatif foto', + foto_base64 LONGTEXT NULL COMMENT 'Base64 foto (opsional, jika tidak pakai file system)', + lat DOUBLE NOT NULL, + lng DOUBLE NOT NULL, + tgl_laporan TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + tgl_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Tabel Histori Kerusakan Jalan +-- Dipakai untuk merekam perubahan status kerusakan +CREATE TABLE IF NOT EXISTS histori_kerusakan ( + id INT AUTO_INCREMENT PRIMARY KEY, + kerusakan_id INT NOT NULL, + status_lama ENUM('Dilaporkan','Diverifikasi','Dalam Perbaikan','Sudah Diperbaiki') NOT NULL, + status_baru ENUM('Dilaporkan','Diverifikasi','Dalam Perbaikan','Sudah Diperbaiki') NOT NULL, + catatan TEXT NULL, + tgl_perubahan TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (kerusakan_id) REFERENCES kerusakan_jalan(id) ON DELETE CASCADE +); + +-- Sample Data Kerusakan +INSERT INTO kerusakan_jalan (jalan_id, nama_jalan_ref, deskripsi, tingkat_urgensi, status_laporan, lat, lng) VALUES +(1, 'Jl. Ahmad Yani', 'Lubang besar di tengah jalan, diameter sekitar 50cm', 'Tinggi', 'Diverifikasi', -0.0210, 109.3460), +(1, 'Jl. Ahmad Yani', 'Aspal terkelupas dan berlubang', 'Sedang', 'Dilaporkan', -0.0215, 109.3465), +(1, 'Jl. Ahmad Yani', 'Lubang di pinggir jalan dekat got', 'Sedang', 'Dilaporkan', -0.0212, 109.3462), +(2, 'Jl. Gajah Mada', 'Retak memanjang sepanjang 3 meter', 'Rendah', 'Sudah Diperbaiki', -0.0275, 109.3340); + +-- Sample Histori +INSERT INTO histori_kerusakan (kerusakan_id, status_lama, status_baru, catatan) VALUES +(1, 'Dilaporkan', 'Diverifikasi', 'Tim survei sudah turun ke lapangan, kerusakan terkonfirmasi'), +(4, 'Dilaporkan', 'Dalam Perbaikan', 'Jadwal perbaikan: minggu ini'), +(4, 'Dalam Perbaikan', 'Sudah Diperbaiki', 'Perbaikan selesai tanggal 10 April 2026'); \ No newline at end of file diff --git a/SIG_Tanah_Jalan/index.php b/SIG_Tanah_Jalan/index.php new file mode 100644 index 0000000..d66db76 --- /dev/null +++ b/SIG_Tanah_Jalan/index.php @@ -0,0 +1,1466 @@ + + + + + +WebGIS SIG – Jalan & Parsil Tanah Pontianak + + + + + + + + + +
+ +
+
+ + +
+ Mode Edit Shape Aktif + + + +
+ + + + + + + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/SPBU_Pontianak/database.sql b/SPBU_Pontianak/database.sql new file mode 100644 index 0000000..7a10afa --- /dev/null +++ b/SPBU_Pontianak/database.sql @@ -0,0 +1,28 @@ +-- ===================================================== +-- WebGIS SPBU Pontianak — Database Schema +-- Jalankan di phpMyAdmin atau MySQL CLI +-- ===================================================== + +CREATE DATABASE IF NOT EXISTS webgis_spbu + CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE webgis_spbu; + +CREATE TABLE IF NOT EXISTS spbu ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL COMMENT 'Nama SPBU', + no VARCHAR(50) NOT NULL COMMENT 'Nomor SPBU (mis: 64.751.01)', + status TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=Buka 24 Jam, 0=Terbatas', + latitude DECIMAL(10,8) NOT NULL COMMENT 'Koordinat Latitude', + longitude DECIMAL(11,8) NOT NULL COMMENT 'Koordinat Longitude', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ── Contoh Data SPBU Pontianak ────────────────────── +INSERT INTO spbu (nama, no, status, latitude, longitude) VALUES +('SPBU Jl. Ahmad Yani', '64.751.01', 1, -0.020500, 109.341200), +('SPBU Jl. Gajahmada', '64.751.02', 0, -0.031800, 109.325600), +('SPBU Jl. Tanjungpura', '64.751.03', 1, -0.025300, 109.332400), +('SPBU Jl. Sutan Syahrir', '64.751.04', 0, -0.018200, 109.348700), +('SPBU Jl. Adisucipto', '64.751.05', 1, -0.040100, 109.358900); \ No newline at end of file diff --git a/SPBU_Pontianak/db_connect.php b/SPBU_Pontianak/db_connect.php new file mode 100644 index 0000000..dcc52a9 --- /dev/null +++ b/SPBU_Pontianak/db_connect.php @@ -0,0 +1,14 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +} catch (PDOException $e) { + die(json_encode(["error" => "Koneksi gagal: " . $e->getMessage()])); +} +?> \ No newline at end of file diff --git a/SPBU_Pontianak/index.html b/SPBU_Pontianak/index.html new file mode 100644 index 0000000..faf761c --- /dev/null +++ b/SPBU_Pontianak/index.html @@ -0,0 +1,14 @@ + + + + + + WebGIS SPBU Pontianak + + + +

Mengalihkan ke halaman utama... Jika tidak otomatis, klik di sini.

+ + diff --git a/SPBU_Pontianak/index.php b/SPBU_Pontianak/index.php new file mode 100644 index 0000000..bbc6e26 --- /dev/null +++ b/SPBU_Pontianak/index.php @@ -0,0 +1,397 @@ + 0){ + $pdo->prepare("UPDATE spbu SET latitude=:lat,longitude=:lng WHERE id=:id") + ->execute([':lat'=>$lat,':lng'=>$lng,':id'=>$id]); + echo json_encode(['ok'=>true,'lat'=>$lat,'lng'=>$lng]); + } else { + echo json_encode(['ok'=>false]); + } + exit; +} + +// AJAX: hapus dari index.php +if (isset($_GET['delete']) && $_SERVER['REQUEST_METHOD']==='POST') { + header('Content-Type: application/json'); + $id = intval($_POST['id']); + $pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>$id]); + echo json_encode(['ok'=>true]); + exit; +} + +$spbu_list = $pdo->query("SELECT * FROM spbu ORDER BY nama")->fetchAll(PDO::FETCH_ASSOC); +$total = count($spbu_list); +$total_24 = count(array_filter($spbu_list, fn($s) => $s['status']==1)); +?> + + + + + +Peta SPBU — WebGIS Pontianak + + + + + + + + +
+
Total SPBU
+
Buka 24 Jam:
+
Terbatas:
+
+ +
+ + + + + +
+
+
+ +
+
+ ⛽ Informasi SPBU +
+ Edit + + +
+
+
+
Nama
+
Nomor SPBU
+
Status
+
Koordinat
+
+
⟳ Drag marker di peta untuk memperbarui lokasi secara langsung
+
+
+ +
+ + + + + diff --git a/SPBU_Pontianak/insert.php b/SPBU_Pontianak/insert.php new file mode 100644 index 0000000..debf099 --- /dev/null +++ b/SPBU_Pontianak/insert.php @@ -0,0 +1,293 @@ +prepare("INSERT INTO spbu (nama, no, status, latitude, longitude) VALUES (:nama,:no,:status,:lat,:lng)"); + $stmt->execute([':nama'=>trim($_POST['nama']),':no'=>trim($_POST['no']),':status'=>isset($_POST['status'])?1:0,':lat'=>floatval($_POST['latitude']),':lng'=>floatval($_POST['longitude'])]); + $message = ['type'=>'success','text'=>'Data SPBU berhasil ditambahkan!']; + } + if ($_POST['action'] === 'update') { + $stmt = $pdo->prepare("UPDATE spbu SET nama=:nama,no=:no,status=:status,latitude=:lat,longitude=:lng WHERE id=:id"); + $stmt->execute([':nama'=>trim($_POST['nama']),':no'=>trim($_POST['no']),':status'=>isset($_POST['status'])?1:0,':lat'=>floatval($_POST['latitude']),':lng'=>floatval($_POST['longitude']),':id'=>intval($_POST['id'])]); + $message = ['type'=>'success','text'=>'Data berhasil diperbarui!']; + } + if ($_POST['action'] === 'delete') { + $pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>intval($_POST['id'])]); + $message = ['type'=>'danger','text'=>'Data berhasil dihapus.']; + } +} + +if (isset($_GET['edit'])) { + $stmt = $pdo->prepare("SELECT * FROM spbu WHERE id=:id"); + $stmt->execute([':id'=>intval($_GET['edit'])]); + $edit_data = $stmt->fetch(PDO::FETCH_ASSOC); +} + +$all = $pdo->query("SELECT * FROM spbu ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC); +?> + + + + + +Input SPBU — WebGIS Pontianak + + + + + + + + +
+ + +
+ +
+ + +
+
+
📍

Klik Peta — Tentukan Lokasi

+
+
+
Klik titik mana saja untuk mengisi koordinat otomatis
+
+
+
+ +
+
+
+

+
+
+
+ + + + + +
+ + +
+
+ + +
+
+ +
+ + Buka 24 Jam +
+
+
+ +
+ + +
+
+ +
+ + Batal +
+
+
+
+
+ + +
+
+
📋
+

Daftar SPBU data

+
+
+ +
Belum ada data. Tambahkan SPBU lewat form di atas.
+ + + + + $r): ?> + + + + + + + + + + + +
#NamaNoStatusLatitudeLongitudeAksi
+
+ Edit +
+ + + +
+
+
+ +
+
+ +
+ + + + + \ No newline at end of file diff --git a/SPBU_Pontianak/show.php b/SPBU_Pontianak/show.php new file mode 100644 index 0000000..83114b3 --- /dev/null +++ b/SPBU_Pontianak/show.php @@ -0,0 +1,414 @@ + 0){ + $pdo->prepare("UPDATE spbu SET latitude=:lat,longitude=:lng WHERE id=:id") + ->execute([':lat'=>$lat,':lng'=>$lng,':id'=>$id]); + echo json_encode(['ok'=>true,'lat'=>$lat,'lng'=>$lng]); + } else { + echo json_encode(['ok'=>false]); + } + exit; +} + +// AJAX: hapus dari show.php +if (isset($_GET['delete']) && $_SERVER['REQUEST_METHOD']==='POST') { + header('Content-Type: application/json'); + $id = intval($_POST['id']); + $pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>$id]); + echo json_encode(['ok'=>true]); + exit; +} + +$spbu_list = $pdo->query("SELECT * FROM spbu ORDER BY nama")->fetchAll(PDO::FETCH_ASSOC); +$total = count($spbu_list); +$total_24 = count(array_filter($spbu_list, fn($s) => $s['status']==1)); +?> + + + + + +Peta SPBU — WebGIS Pontianak + + + + + + + + +
+
Total SPBU
+
Buka 24 Jam:
+
Terbatas:
+
+ +
+ + + + + +
+
+
+ +
+
+ ⛽ Informasi SPBU +
+ Edit + + +
+
+
+
Nama
+
Nomor SPBU
+
Status
+
Koordinat
+
+
⟳ Drag marker di peta untuk memperbarui lokasi secara langsung
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..c297b63 --- /dev/null +++ b/index.html @@ -0,0 +1,614 @@ + + + + + + Portal WebGIS Pontianak — SIG Hub + + + + + + + + + + + + + + +
+
+
+
+
+
+ + +
+
+
+ +
+
+

GIS HUB PONTIANAK

+

Sistem Informasi Geografis Terintegrasi

+
+
+ +
+
+
+ Server Online +
+
+
+ + +
+
+ Geographic Information System +

Portal Aplikasi WebGIS

+

Akses cepat ke berbagai modul pemetaan interaktif Kota Pontianak. Dibuat menggunakan teknologi web modern dan visualisasi data spasial secara real-time.

+
+ + + +
+ + + + + + diff --git a/sig-kemiskinan/README.md b/sig-kemiskinan/README.md new file mode 100644 index 0000000..8cd7bbb --- /dev/null +++ b/sig-kemiskinan/README.md @@ -0,0 +1,132 @@ +# SIG Penduduk Miskin – Berbasis Rumah Ibadah + +Aplikasi Sistem Informasi Geografis (SIG) untuk pendataan penduduk miskin berdasarkan wilayah radius rumah ibadah terdekat. + +--- + +## 📁 Struktur Folder + +``` +sig-kemiskinan/ +├── index.html ← Halaman utama (Leaflet + JS) +├── database.sql ← Schema + data contoh +├── includes/ +│ └── config.php ← Koneksi DB + fungsi utilitas +├── api/ +│ ├── rumah_ibadah.php ← CRUD Rumah Ibadah +│ ├── penduduk.php ← CRUD Penduduk Miskin +│ ├── geocode.php ← Reverse Geocoding (Nominatim) +│ └── update_radius.php ← Update radius + reassign +└── README.md +``` + +--- + +## ⚙️ Setup + +### 1. Persyaratan +- PHP 7.4+ (atau 8.x) +- MySQL 5.7+ / MariaDB +- Web server (Apache/Nginx) atau PHP built-in server + +### 2. Database +```sql +-- Import file database.sql ke MySQL: +mysql -u root -p < database.sql +``` +Atau lewat phpMyAdmin: Import → pilih `database.sql` + +### 3. Konfigurasi DB +Edit `includes/config.php`: +```php +define('DB_HOST', 'localhost'); +define('DB_USER', 'root'); // ganti sesuai user MySQL Anda +define('DB_PASS', ''); // ganti dengan password MySQL +define('DB_NAME', 'sig_kemiskinan'); +``` + +### 4. Jalankan +```bash +# Opsi 1: PHP built-in server (development) +cd sig-kemiskinan +php -S localhost:8080 + +# Opsi 2: Taruh di folder htdocs (XAMPP/WAMP) +# Copy folder ke C:/xampp/htdocs/sig-kemiskinan +# Akses: http://localhost/sig-kemiskinan +``` + +--- + +## 🗺️ Cara Penggunaan + +### Tambah Rumah Ibadah +1. Klik tombol **"+ Rumah Ibadah"** (pojok kanan atas) +2. Kursor berubah → **klik lokasi di peta** +3. Isi form: Nama, Jenis, PIC, No. WA, Radius +4. Alamat **otomatis terisi** dari reverse geocoding (Nominatim/OSM) +5. Klik **Simpan** + +### Tambah Penduduk Miskin +1. Klik tombol **"+ Penduduk"** +2. **Klik lokasi rumah** di peta +3. Isi: Nama KK, Jumlah anggota +4. Sistem otomatis menentukan rumah ibadah terdekat dalam radius +5. **Titik hijau** = dalam radius | **Titik merah** = luar radius + +### Ubah Radius +1. Klik marker rumah ibadah → popup → tombol **📐 Radius** +2. Atau klik ikon 📐 di list sidebar +3. **Drag slider** untuk mengubah radius (preview langsung) +4. Klik **Simpan** → semua penduduk di-reassign otomatis + +### Logika Overlap +Jika penduduk masuk dalam radius **>1 rumah ibadah**: +→ Diassign ke **rumah ibadah yang paling dekat** (Haversine distance) + +--- + +## 🔌 API Endpoints + +| Method | URL | Fungsi | +|--------|-----|--------| +| GET | `api/rumah_ibadah.php` | Semua rumah ibadah + stats | +| POST | `api/rumah_ibadah.php` | Tambah rumah ibadah | +| PUT | `api/rumah_ibadah.php` | Update rumah ibadah | +| DELETE | `api/rumah_ibadah.php?id=X` | Hapus | +| GET | `api/penduduk.php` | Semua penduduk | +| POST | `api/penduduk.php` | Tambah penduduk | +| PUT | `api/penduduk.php` | Update penduduk | +| DELETE | `api/penduduk.php?id=X` | Hapus | +| GET | `api/geocode.php?lat=X&lng=Y` | Reverse geocoding | +| POST | `api/update_radius.php` | Update radius + reassign | + +--- + +## 🧮 Algoritma Radius + +``` +Jarak = Haversine(lat1, lng1, lat2, lng2) // dalam meter + +Untuk setiap penduduk P: + candidates = [ibadah | jarak(P, ibadah) <= ibadah.radius] + if candidates: + assign P → min(candidates, key=jarak) + else: + P.rumah_ibadah_id = NULL // luar radius semua +``` + +--- + +## 🖥️ Fitur UI + +- Peta Leaflet dengan tile OpenStreetMap +- Sidebar panel: list Rumah Ibadah & Penduduk +- Stats bar: total ibadah, KK, jiwa, tercakup/tidak +- Radius fleksibel via slider (100–3000m, default 500m) +- Lingkaran radius real-time di peta +- Reverse geocoding otomatis saat klik peta +- Warna marker penduduk: hijau (dalam radius) / merah (luar) +- Popup informatif + tombol CRUD langsung dari peta +- Responsive mobile-friendly +- Dark theme modern diff --git a/sig-kemiskinan/api/auth.php b/sig-kemiskinan/api/auth.php new file mode 100644 index 0000000..457f42b --- /dev/null +++ b/sig-kemiskinan/api/auth.php @@ -0,0 +1,75 @@ + false, 'error' => 'Username dan password wajib diisi'], 400); + } + + $stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND aktif = 1 LIMIT 1"); + $stmt->execute([$username]); + $user = $stmt->fetch(); + + if (!$user || !password_verify($password, $user['password'])) { + jsonResponse(['success' => false, 'error' => 'Username atau password salah'], 401); + } + + // Simpan ke session + $_SESSION['user_id'] = $user['id']; + $_SESSION['user'] = [ + 'id' => $user['id'], + 'nama' => $user['nama'], + 'username' => $user['username'], + 'role' => $user['role'], + ]; + + // Update last_login + $db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?") + ->execute([$user['id']]); + + jsonResponse([ + 'success' => true, + 'message' => 'Login berhasil', + 'user' => $_SESSION['user'], + ]); + break; + + // ── DELETE /api/auth.php → Logout ───────────────────── + case 'DELETE': + session_destroy(); + jsonResponse(['success' => true, 'message' => 'Logout berhasil']); + break; + + // ── GET /api/auth.php → Cek session ─────────────────── + case 'GET': + if (!isset($_SESSION['user_id'])) { + jsonResponse(['success' => false, 'loggedIn' => false], 200); + } + jsonResponse(['success' => true, 'loggedIn' => true, 'user' => $_SESSION['user']]); + break; + + default: + jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405); +} diff --git a/sig-kemiskinan/api/bantuan.php b/sig-kemiskinan/api/bantuan.php new file mode 100644 index 0000000..87a040a --- /dev/null +++ b/sig-kemiskinan/api/bantuan.php @@ -0,0 +1,167 @@ +query("SELECT * FROM jenis_bantuan ORDER BY kode")->fetchAll(); + jsonResponse(['success' => true, 'data' => $rows]); + } + + // GET bantuan per penduduk + $pendudukId = $_GET['penduduk_id'] ?? null; + if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400); + + $stmt = $db->prepare(" + SELECT bp.*, jb.kode, jb.nama AS nama_bantuan, jb.warna_badge, jb.deskripsi + FROM bantuan_penduduk bp + JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id + WHERE bp.penduduk_id = ? + ORDER BY jb.kode + "); + $stmt->execute([$pendudukId]); + jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]); + break; + + // ════════════════════════════════════════════════════════ + // POST – Tambah/set bantuan untuk penduduk + // ════════════════════════════════════════════════════════ + case 'POST': + $in = getInput(); + + // Batch upsert – terima array bantuans[] + if (isset($in['bantuans']) && is_array($in['bantuans'])) { + $pendudukId = (int)($in['penduduk_id'] ?? 0); + if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400); + + // Nonaktifkan semua bantuan lama dulu + $db->prepare("UPDATE bantuan_penduduk SET status = 'tidak_aktif' WHERE penduduk_id = ?") + ->execute([$pendudukId]); + + foreach ($in['bantuans'] as $b) { + $bantuanId = (int)($b['bantuan_id'] ?? 0); + if (!$bantuanId) continue; + + // Cek sudah ada? + $cek = $db->prepare("SELECT id FROM bantuan_penduduk WHERE penduduk_id = ? AND bantuan_id = ?"); + $cek->execute([$pendudukId, $bantuanId]); + $existing = $cek->fetch(); + + if ($existing) { + $db->prepare(" + UPDATE bantuan_penduduk + SET status = 'aktif', tanggal_mulai = ?, nominal_bulanan = ?, + nomor_kpj = ?, keterangan = ?, diinput_oleh = ? + WHERE penduduk_id = ? AND bantuan_id = ? + ")->execute([ + $b['tanggal_mulai'] ?? null, + $b['nominal_bulanan'] ?? null, + $b['nomor_kpj'] ?? null, + $b['keterangan'] ?? null, + $b['diinput_oleh'] ?? 1, + $pendudukId, $bantuanId, + ]); + } else { + $db->prepare(" + INSERT INTO bantuan_penduduk + (penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, nomor_kpj, keterangan, diinput_oleh) + VALUES (?,?,'aktif',?,?,?,?,?) + ")->execute([ + $pendudukId, $bantuanId, + $b['tanggal_mulai'] ?? null, + $b['nominal_bulanan'] ?? null, + $b['nomor_kpj'] ?? null, + $b['keterangan'] ?? null, + $b['diinput_oleh'] ?? 1, + ]); + } + } + jsonResponse(['success' => true, 'message' => 'Data bantuan berhasil disimpan']); + } + + // Single insert + $required = ['penduduk_id','bantuan_id']; + foreach ($required as $f) { + if (empty($in[$f])) jsonResponse(['success' => false, 'error' => "Field '$f' wajib diisi"], 400); + } + + // Upsert + $cek = $db->prepare("SELECT id FROM bantuan_penduduk WHERE penduduk_id = ? AND bantuan_id = ?"); + $cek->execute([(int)$in['penduduk_id'], (int)$in['bantuan_id']]); + if ($cek->fetch()) { + jsonResponse(['success' => false, 'error' => 'Bantuan ini sudah tercatat untuk penduduk tersebut. Gunakan PUT untuk update.'], 409); + } + + $db->prepare(" + INSERT INTO bantuan_penduduk (penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, nomor_kpj, keterangan, diinput_oleh) + VALUES (?,?,?,?,?,?,?,?) + ")->execute([ + (int)$in['penduduk_id'], + (int)$in['bantuan_id'], + $in['status'] ?? 'aktif', + $in['tanggal_mulai'] ?? null, + $in['nominal_bulanan'] ?? null, + $in['nomor_kpj'] ?? null, + $in['keterangan'] ?? null, + $in['diinput_oleh'] ?? 1, + ]); + jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Bantuan berhasil ditambahkan']); + break; + + // ════════════════════════════════════════════════════════ + // PUT – Update status bantuan + // ════════════════════════════════════════════════════════ + case 'PUT': + $in = getInput(); + $id = (int)($in['id'] ?? $_GET['id'] ?? 0); + if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400); + + $db->prepare(" + UPDATE bantuan_penduduk + SET status = ?, tanggal_mulai = ?, tanggal_selesai = ?, + nominal_bulanan = ?, nomor_kpj = ?, keterangan = ? + WHERE id = ? + ")->execute([ + $in['status'] ?? 'aktif', + $in['tanggal_mulai'] ?? null, + $in['tanggal_selesai'] ?? null, + $in['nominal_bulanan'] ?? null, + $in['nomor_kpj'] ?? null, + $in['keterangan'] ?? null, + $id, + ]); + jsonResponse(['success' => true, 'message' => 'Status bantuan diperbarui']); + break; + + // ════════════════════════════════════════════════════════ + // DELETE – Hapus relasi bantuan + // ════════════════════════════════════════════════════════ + case 'DELETE': + $id = (int)($_GET['id'] ?? getInput()['id'] ?? 0); + if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400); + $db->prepare("DELETE FROM bantuan_penduduk WHERE id = ?")->execute([$id]); + jsonResponse(['success' => true, 'message' => 'Bantuan dihapus']); + break; + + default: + jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405); +} diff --git a/sig-kemiskinan/api/dashboard_stats.php b/sig-kemiskinan/api/dashboard_stats.php new file mode 100644 index 0000000..e17f1ec --- /dev/null +++ b/sig-kemiskinan/api/dashboard_stats.php @@ -0,0 +1,189 @@ +false,'error'=>'Method tidak diizinkan'],405); + +$db = getDB(); + +// ── Filter opsional ──────────────────────────────────────── +$kelurahan = $_GET['kelurahan'] ?? null; +$kecamatan = $_GET['kecamatan'] ?? null; +$whereBase = "WHERE 1=1"; +$params = []; +if ($kelurahan) { $whereBase .= " AND w.kelurahan = ?"; $params[] = $kelurahan; } +if ($kecamatan) { $whereBase .= " AND w.kecamatan = ?"; $params[] = $kecamatan; } + +$joinW = "LEFT JOIN wilayah w ON w.id = pm.wilayah_id"; + +// ═══════════════════════════════════════════════════════════ +// 1. RINGKASAN UTAMA (KPI Cards) +// ═══════════════════════════════════════════════════════════ +$kpi = $db->prepare(" + SELECT + COUNT(pm.id) AS total_kk, + SUM(pm.jumlah_anggota) AS total_jiwa, + SUM(pm.jumlah_anak_sekolah) AS total_anak_sekolah, + SUM(CASE WHEN pm.status_verifikasi='terverifikasi' THEN 1 ELSE 0 END) AS terverifikasi, + SUM(CASE WHEN pm.status_verifikasi='belum_diverifikasi' THEN 1 ELSE 0 END) AS belum_verifikasi, + SUM(CASE WHEN pm.punya_bpjs=1 THEN 1 ELSE 0 END) AS punya_bpjs, + SUM(CASE WHEN pm.kategori_kemiskinan='sangat_miskin' THEN 1 ELSE 0 END) AS sangat_miskin, + SUM(CASE WHEN pm.kategori_kemiskinan='miskin' THEN 1 ELSE 0 END) AS miskin, + SUM(CASE WHEN pm.kategori_kemiskinan='hampir_miskin' THEN 1 ELSE 0 END) AS hampir_miskin, + SUM(CASE WHEN pm.kategori_kemiskinan='mampu' THEN 1 ELSE 0 END) AS mampu, + ROUND(AVG(pm.penghasilan_bulanan),0) AS avg_penghasilan, + SUM(CASE WHEN bp_count.cnt > 0 THEN 1 ELSE 0 END) AS penerima_bantuan, + SUM(CASE WHEN bp_count.cnt = 0 THEN 1 ELSE 0 END) AS belum_bantuan + FROM penduduk_miskin pm + $joinW + LEFT JOIN ( + SELECT penduduk_id, COUNT(*) AS cnt + FROM bantuan_penduduk + WHERE status='aktif' + GROUP BY penduduk_id + ) bp_count ON bp_count.penduduk_id = pm.id + $whereBase +"); +$kpi->execute($params); +$kpiData = $kpi->fetch(); + +// ═══════════════════════════════════════════════════════════ +// 2. DISTRIBUSI KATEGORI KEMISKINAN (Donut Chart) +// ═══════════════════════════════════════════════════════════ +$katStmt = $db->prepare(" + SELECT pm.kategori_kemiskinan AS label, COUNT(*) AS value + FROM penduduk_miskin pm $joinW $whereBase + GROUP BY pm.kategori_kemiskinan + ORDER BY FIELD(pm.kategori_kemiskinan,'sangat_miskin','miskin','hampir_miskin','mampu') +"); +$katStmt->execute($params); +$kategoriChart = $katStmt->fetchAll(); + +// ═══════════════════════════════════════════════════════════ +// 3. KK PER KELURAHAN (Bar Chart) +// ═══════════════════════════════════════════════════════════ +$kelStmt = $db->prepare(" + SELECT + COALESCE(w.kelurahan,'Tidak Diketahui') AS label, + COUNT(pm.id) AS total_kk, + SUM(pm.jumlah_anggota) AS total_jiwa, + SUM(CASE WHEN bp_c.cnt>0 THEN 1 ELSE 0 END) AS penerima + FROM penduduk_miskin pm $joinW + LEFT JOIN ( + SELECT penduduk_id, COUNT(*) AS cnt FROM bantuan_penduduk WHERE status='aktif' GROUP BY penduduk_id + ) bp_c ON bp_c.penduduk_id = pm.id + $whereBase + GROUP BY w.kelurahan + ORDER BY total_kk DESC + LIMIT 15 +"); +$kelStmt->execute($params); +$kelurahanChart = $kelStmt->fetchAll(); + +// ═══════════════════════════════════════════════════════════ +// 4. DISTRIBUSI JENIS BANTUAN (Pie Chart) +// ═══════════════════════════════════════════════════════════ +$bantuanStmt = $db->prepare(" + SELECT jb.kode, jb.nama AS label, jb.warna_badge AS color, COUNT(bp.id) AS value + FROM bantuan_penduduk bp + JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id + JOIN penduduk_miskin pm ON pm.id = bp.penduduk_id + $joinW + " . str_replace('WHERE 1=1', 'WHERE bp.status=\'aktif\'', $whereBase) . " + GROUP BY jb.id + ORDER BY value DESC +"); +$bantuanStmt->execute($params); +$bantuanChart = $bantuanStmt->fetchAll(); + +// ═══════════════════════════════════════════════════════════ +// 5. TREN PENDATAAN PER BULAN (Line Chart) – 12 bulan terakhir +// ═══════════════════════════════════════════════════════════ +$trenStmt = $db->prepare(" + SELECT + DATE_FORMAT(pm.created_at,'%Y-%m') AS bulan, + COUNT(pm.id) AS total + FROM penduduk_miskin pm $joinW $whereBase + AND pm.created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH) + GROUP BY DATE_FORMAT(pm.created_at,'%Y-%m') + ORDER BY bulan ASC +"); +$trenStmt->execute($params); +$trenChart = $trenStmt->fetchAll(); + +// ═══════════════════════════════════════════════════════════ +// 6. DISTRIBUSI PEKERJAAN (Bar horizontal) +// ═══════════════════════════════════════════════════════════ +$pekerjaanStmt = $db->prepare(" + SELECT pm.status_pekerjaan AS label, COUNT(*) AS value + FROM penduduk_miskin pm $joinW $whereBase + GROUP BY pm.status_pekerjaan + ORDER BY value DESC +"); +$pekerjaanStmt->execute($params); +$pekerjaanChart = $pekerjaanStmt->fetchAll(); + +// ═══════════════════════════════════════════════════════════ +// 7. DATA HEATMAP (lat/lng + bobot untuk Leaflet.heat) +// ═══════════════════════════════════════════════════════════ +$heatStmt = $db->prepare(" + SELECT pm.lat, pm.lng, + CASE pm.kategori_kemiskinan + WHEN 'sangat_miskin' THEN 1.0 + WHEN 'miskin' THEN 0.6 + WHEN 'hampir_miskin' THEN 0.3 + WHEN 'mampu' THEN 0.1 + ELSE 0.1 + END AS intensity + FROM penduduk_miskin pm $joinW $whereBase +"); +$heatStmt->execute($params); +$heatmap = $heatStmt->fetchAll(); + +// ═══════════════════════════════════════════════════════════ +// 8. TOP 10 KK BELUM MENERIMA BANTUAN APAPUN +// ═══════════════════════════════════════════════════════════ +$belumBantuanStmt = $db->prepare(" + SELECT pm.id, pm.nama_kk, pm.nik_kk, pm.alamat, + pm.kategori_kemiskinan, pm.jumlah_anggota, + pm.status_verifikasi, + COALESCE(w.kelurahan,'–') AS kelurahan, + DATEDIFF(NOW(), pm.created_at) AS hari_terdata + FROM penduduk_miskin pm $joinW + $whereBase + AND NOT EXISTS ( + SELECT 1 FROM bantuan_penduduk bp + WHERE bp.penduduk_id = pm.id AND bp.status='aktif' + ) + ORDER BY pm.kategori_kemiskinan ASC, hari_terdata DESC + LIMIT 10 +"); +$belumBantuanStmt->execute($params); +$belumBantuan = $belumBantuanStmt->fetchAll(); + +// ── Statistik Rumah Ibadah (tetap dari v1.1) ────────────── +$ibadahStats = $db->query(" + SELECT COUNT(*) AS total_ibadah FROM rumah_ibadah +")->fetch(); + +jsonResponse([ + 'success' => true, + 'kpi' => $kpiData, + 'chart_kategori' => $kategoriChart, + 'chart_kelurahan' => $kelurahanChart, + 'chart_bantuan' => $bantuanChart, + 'chart_tren' => $trenChart, + 'chart_pekerjaan' => $pekerjaanChart, + 'heatmap' => $heatmap, + 'belum_bantuan' => $belumBantuan, + 'total_ibadah' => (int)$ibadahStats['total_ibadah'], + 'generated_at' => date('Y-m-d H:i:s'), +]); diff --git a/sig-kemiskinan/api/export_excel.php b/sig-kemiskinan/api/export_excel.php new file mode 100644 index 0000000..418d13b --- /dev/null +++ b/sig-kemiskinan/api/export_excel.php @@ -0,0 +1,118 @@ + 0"; +} + +$stmt = $db->prepare(" + SELECT + pm.nik_kk, pm.nama_kk, pm.no_hp, + pm.alamat, pm.rt, pm.rw, + COALESCE(w.kelurahan,'–') AS kelurahan, + COALESCE(w.kecamatan,'–') AS kecamatan, + pm.jumlah_anggota, pm.jumlah_anak_sekolah, + pm.penghasilan_bulanan, pm.sumber_penghasilan, pm.status_pekerjaan, + pm.kategori_kemiskinan, pm.kondisi_rumah, pm.kepemilikan_rumah, + CASE pm.punya_bpjs WHEN 1 THEN 'Ya' ELSE 'Tidak' END AS punya_bpjs, + pm.jenis_bpjs, + pm.lat, pm.lng, + pm.status_verifikasi, + COALESCE(u.nama,'–') AS nama_surveyor, + DATE_FORMAT(pm.created_at,'%d/%m/%Y %H:%i') AS tgl_input, + (SELECT GROUP_CONCAT(jb.kode ORDER BY jb.kode SEPARATOR ', ') + FROM bantuan_penduduk bp JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id + WHERE bp.penduduk_id = pm.id AND bp.status='aktif') AS bantuan_diterima + FROM penduduk_miskin pm + LEFT JOIN wilayah w ON w.id = pm.wilayah_id + LEFT JOIN users u ON u.id = pm.surveyor_id + $where + ORDER BY w.kelurahan, pm.nama_kk +"); +$stmt->execute($params); +$rows = $stmt->fetchAll(); + +// ── Nama file ───────────────────────────────────────────── +$filter = $kelurahan ? "_" . preg_replace('/\s+/', '-', $kelurahan) : ''; +$filename = "data_kemiskinan{$filter}_" . date('Ymd_His') . ".csv"; + +// ── Output header HTTP ───────────────────────────────────── +header('Content-Type: text/csv; charset=UTF-8'); +header('Content-Disposition: attachment; filename="' . $filename . '"'); +header('Cache-Control: no-cache, no-store, must-revalidate'); +header('Pragma: no-cache'); + +// BOM UTF-8 agar Excel baca karakter Indonesia dengan benar +echo "\xEF\xBB\xBF"; + +$out = fopen('php://output', 'w'); + +// ── Header kolom ────────────────────────────────────────── +fputcsv($out, [ + 'No','NIK KK','Nama KK','No HP', + 'Alamat','RT','RW','Kelurahan','Kecamatan', + 'Jml Anggota','Anak Sekolah', + 'Penghasilan/Bln','Sumber Penghasilan','Status Pekerjaan', + 'Kategori Kemiskinan','Kondisi Rumah','Kepemilikan Rumah', + 'Punya BPJS','Jenis BPJS', + 'Latitude','Longitude', + 'Status Verifikasi','Bantuan Diterima', + 'Petugas Input','Tgl Input', +], ';'); + +// ── Data rows ───────────────────────────────────────────── +$no = 1; +foreach ($rows as $r) { + fputcsv($out, [ + $no++, + $r['nik_kk'], + $r['nama_kk'], + $r['no_hp'] ?? '–', + $r['alamat'] ?? '–', + $r['rt'] ?? '–', + $r['rw'] ?? '–', + $r['kelurahan'], + $r['kecamatan'], + $r['jumlah_anggota'], + $r['jumlah_anak_sekolah'], + $r['penghasilan_bulanan'] ? 'Rp ' . number_format($r['penghasilan_bulanan'],0,',','.') : '–', + $r['sumber_penghasilan'] ?? '–', + ucwords(str_replace('_',' ', $r['status_pekerjaan'])), + $r['kategori_kemiskinan'] === 'hampir_miskin' ? 'Nyaris Miskin' : ucwords(str_replace('_',' ', $r['kategori_kemiskinan'])), + ucwords(str_replace('_',' ', $r['kondisi_rumah'])), + ucwords(str_replace('_',' ', $r['kepemilikan_rumah'])), + $r['punya_bpjs'], + strtoupper(str_replace('_',' ', $r['jenis_bpjs'])), + $r['lat'], + $r['lng'], + ucwords(str_replace('_',' ', $r['status_verifikasi'])), + $r['bantuan_diterima'] ?? 'Belum Ada', + $r['nama_surveyor'], + $r['tgl_input'], + ], ';'); +} + +fclose($out); +exit; diff --git a/sig-kemiskinan/api/export_pdf.php b/sig-kemiskinan/api/export_pdf.php new file mode 100644 index 0000000..435d3ab --- /dev/null +++ b/sig-kemiskinan/api/export_pdf.php @@ -0,0 +1,246 @@ +prepare(" + SELECT pm.nik_kk, pm.nama_kk, pm.no_hp, pm.alamat, pm.rt, pm.rw, + COALESCE(w.kelurahan,'–') AS kelurahan, + pm.jumlah_anggota, pm.jumlah_anak_sekolah, + pm.penghasilan_bulanan, pm.status_pekerjaan, + pm.kategori_kemiskinan, pm.kondisi_rumah, + CASE pm.punya_bpjs WHEN 1 THEN 'Ya' ELSE 'Tidak' END AS punya_bpjs, + pm.status_verifikasi, + (SELECT GROUP_CONCAT(jb.kode ORDER BY jb.kode SEPARATOR ', ') + FROM bantuan_penduduk bp JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id + WHERE bp.penduduk_id = pm.id AND bp.status='aktif') AS bantuan + FROM penduduk_miskin pm + LEFT JOIN wilayah w ON w.id = pm.wilayah_id + $where + ORDER BY w.kelurahan, pm.kategori_kemiskinan, pm.nama_kk +"); +$stmt->execute($params); +$rows = $stmt->fetchAll(); + +// Statistik ringkas +$statsStmt = $db->prepare(" + SELECT COUNT(*) AS total, SUM(pm.jumlah_anggota) AS jiwa, + SUM(CASE WHEN pm.kategori_kemiskinan='sangat_miskin' THEN 1 ELSE 0 END) AS sangat_miskin, + SUM(CASE WHEN pm.kategori_kemiskinan='miskin' THEN 1 ELSE 0 END) AS miskin, + SUM(CASE WHEN pm.kategori_kemiskinan='hampir_miskin' THEN 1 ELSE 0 END) AS hampir_miskin, + SUM(CASE WHEN pm.kategori_kemiskinan='mampu' THEN 1 ELSE 0 END) AS mampu, + SUM(CASE WHEN EXISTS(SELECT 1 FROM bantuan_penduduk bp WHERE bp.penduduk_id=pm.id AND bp.status='aktif') THEN 1 ELSE 0 END) AS penerima + FROM penduduk_miskin pm LEFT JOIN wilayah w ON w.id = pm.wilayah_id $where +"); +$statsStmt->execute($params); +$stats = $statsStmt->fetch(); + +$judul = "LAPORAN DATA PENDUDUK MISKIN"; +$filter = $kelurahan ? " – Kelurahan $kelurahan" : ($kecamatan ? " – Kec. $kecamatan" : " – Seluruh Wilayah"); +$tgl = date('d F Y'); + +// Warna kategori +function warnaBg(string $k): string { + return match($k) { + 'sangat_miskin' => '#fee2e2', + 'miskin' => '#fef3c7', + 'hampir_miskin' => '#dcfce7', + 'mampu' => '#dbeafe', + default => '#f3f4f6', + }; +} +function labelKat(string $k): string { + return match($k) { + 'sangat_miskin' => 'Sangat Miskin', + 'miskin' => 'Miskin', + 'hampir_miskin' => 'Nyaris Miskin', + 'mampu' => 'Mampu', + default => $k, + }; +} +?> + + + + +<?= htmlspecialchars($judul . $filter) ?> + + + + + +
+ + +
+ + +
+

+

SISTEM INFORMASI GEOGRAFIS KEMISKINAN

+

Dicetak pada:  |  Total: KK

+
+ + +
+
Total KK
+
Total Jiwa
+
Sangat Miskin
+
Miskin
+
Nyaris Miskin
+
Mampu
+
Penerima Bantuan
+
Belum Bantuan
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NoNIK / Nama KKKelurahanRT/RWAlamatAngg.AnakPenghasilanPekerjaanKategoriKondisi RumahBPJSBantuan DiterimaStatus Verifikasi
+
+
+ +
+ + + + + + + + + + Belum Ada + + + '#dcfce7', 'belum_diverifikasi'=>'#fef3c7', default=>'#fee2e2' }; + ?> + + + +
+ + + + + + diff --git a/sig-kemiskinan/api/foto.php b/sig-kemiskinan/api/foto.php new file mode 100644 index 0000000..d5db212 --- /dev/null +++ b/sig-kemiskinan/api/foto.php @@ -0,0 +1,171 @@ + false, 'error' => 'penduduk_id diperlukan'], 400); + } + $stmt = $db->prepare(" + SELECT f.*, u.nama AS nama_petugas + FROM foto_rumah f + LEFT JOIN users u ON u.id = f.diambil_oleh + WHERE f.penduduk_id = ? + ORDER BY f.is_primary DESC, f.created_at ASC + "); + $stmt->execute([$pendudukId]); + header('Content-Type: application/json'); + jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]); + break; + + // ════════════════════════════════════════════════════════ + // POST – Upload foto baru + // ════════════════════════════════════════════════════════ + case 'POST': + header('Content-Type: application/json'); + $pendudukId = (int)($_POST['penduduk_id'] ?? 0); + if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400); + + // Cek penduduk ada + $cek = $db->prepare("SELECT id FROM penduduk_miskin WHERE id = ?"); + $cek->execute([$pendudukId]); + if (!$cek->fetch()) jsonResponse(['success' => false, 'error' => 'Data penduduk tidak ditemukan'], 404); + + if (!isset($_FILES['foto']) || $_FILES['foto']['error'] !== UPLOAD_ERR_OK) { + $errCode = $_FILES['foto']['error'] ?? 'no_file'; + jsonResponse(['success' => false, 'error' => "Upload gagal (kode: $errCode)"], 400); + } + + $file = $_FILES['foto']; + $tmpPath = $file['tmp_name']; + $origName = $file['name']; + $fileSize = $file['size']; + + // Validasi ukuran + if ($fileSize > MAX_FOTO_SIZE) { + jsonResponse(['success' => false, 'error' => 'Ukuran file maksimal 5 MB'], 413); + } + + // Validasi ekstensi & MIME type + $ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION)); + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $tmpPath); + finfo_close($finfo); + $allowedMime = ['image/jpeg','image/png','image/webp']; + if (!in_array($ext, ALLOWED_EXT) || !in_array($mime, $allowedMime)) { + jsonResponse(['success' => false, 'error' => 'Format file tidak didukung (jpg/png/webp)'], 415); + } + + // Generate nama file unik + $newName = 'foto_' . $pendudukId . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext; + $destPath = UPLOAD_DIR . $newName; + $relPath = UPLOAD_URL . $newName; + + if (!move_uploaded_file($tmpPath, $destPath)) { + jsonResponse(['success' => false, 'error' => 'Gagal menyimpan file'], 500); + } + + // Cek apakah ini foto pertama → jadikan primary + $countStmt = $db->prepare("SELECT COUNT(*) FROM foto_rumah WHERE penduduk_id = ?"); + $countStmt->execute([$pendudukId]); + $isPrimary = ($countStmt->fetchColumn() == 0) ? 1 : 0; + + // Koordinat GPS dari POST (dari browser Geolocation atau EXIF) + $latFoto = isset($_POST['lat_foto']) && $_POST['lat_foto'] !== '' ? (float)$_POST['lat_foto'] : null; + $lngFoto = isset($_POST['lng_foto']) && $_POST['lng_foto'] !== '' ? (float)$_POST['lng_foto'] : null; + $accuracy = isset($_POST['accuracy_meter']) && $_POST['accuracy_meter'] !== '' ? (float)$_POST['accuracy_meter'] + : (isset($_POST['accuracy']) && $_POST['accuracy'] !== '' ? (float)$_POST['accuracy'] : null); + + // Jika user set is_primary = 1, nonaktifkan primary lain dulu + $isPrimaryReq = isset($_POST['is_primary']) && (int)$_POST['is_primary'] === 1; + if ($isPrimaryReq) { + $db->prepare("UPDATE foto_rumah SET is_primary = 0 WHERE penduduk_id = ?") + ->execute([$pendudukId]); + $isPrimary = 1; + } + + // diambil_oleh: support field name lama (user_id) dan baru (diambil_oleh) + $diambilOleh = $_POST['diambil_oleh'] ?? $_POST['user_id'] ?? null; + + $db->prepare(" + INSERT INTO foto_rumah (penduduk_id, file_path, lat_foto, lng_foto, accuracy_meter, is_primary, ukuran_kb, keterangan, diambil_oleh) + VALUES (?,?,?,?,?,?,?,?,?) + ")->execute([ + $pendudukId, + $relPath, + $latFoto, + $lngFoto, + $accuracy, + $isPrimary, + (int)round($fileSize / 1024), + $_POST['keterangan'] ?? null, + $diambilOleh ? (int)$diambilOleh : null, + ]); + + $newId = (int)$db->lastInsertId(); + jsonResponse([ + 'success' => true, + 'id' => $newId, + 'file_path' => $relPath, + 'is_primary'=> $isPrimary, + 'message' => 'Foto berhasil diupload', + ]); + break; + + // ════════════════════════════════════════════════════════ + // DELETE – Hapus foto + // ════════════════════════════════════════════════════════ + case 'DELETE': + header('Content-Type: application/json'); + $id = (int)($_GET['id'] ?? 0); + if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400); + + $stmt = $db->prepare("SELECT * FROM foto_rumah WHERE id = ?"); + $stmt->execute([$id]); + $foto = $stmt->fetch(); + if (!$foto) jsonResponse(['success' => false, 'error' => 'Foto tidak ditemukan'], 404); + + // Hapus file fisik + $filePath = __DIR__ . '/../' . $foto['file_path']; + if (file_exists($filePath)) @unlink($filePath); + + $db->prepare("DELETE FROM foto_rumah WHERE id = ?")->execute([$id]); + + // Jika foto yang dihapus adalah primary, set foto lain jadi primary + if ($foto['is_primary']) { + $next = $db->prepare("SELECT id FROM foto_rumah WHERE penduduk_id = ? ORDER BY created_at ASC LIMIT 1"); + $next->execute([$foto['penduduk_id']]); + $nextFoto = $next->fetch(); + if ($nextFoto) { + $db->prepare("UPDATE foto_rumah SET is_primary = 1 WHERE id = ?")->execute([$nextFoto['id']]); + } + } + + jsonResponse(['success' => true, 'message' => 'Foto berhasil dihapus']); + break; + + default: + header('Content-Type: application/json'); + jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405); +} \ No newline at end of file diff --git a/sig-kemiskinan/api/geocode.php b/sig-kemiskinan/api/geocode.php new file mode 100644 index 0000000..26c65d4 --- /dev/null +++ b/sig-kemiskinan/api/geocode.php @@ -0,0 +1,71 @@ + false, 'error' => 'lat dan lng diperlukan']); + exit; +} + +$lat = (float)$lat; +$lng = (float)$lng; + +$url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={$lat}&lon={$lng}&zoom=18&addressdetails=1"; + +$context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => "User-Agent: SIG-Kemiskinan/1.0 (sig@example.com)\r\n", + 'timeout' => 8, + ] +]); + +$response = @file_get_contents($url, false, $context); + +if ($response === false) { + // Fallback: koordinat saja + echo json_encode([ + 'success' => true, + 'alamat' => "Koordinat: {$lat}, {$lng}", + 'detail' => null + ]); + exit; +} + +$data = json_decode($response, true); + +if (!$data || isset($data['error'])) { + echo json_encode([ + 'success' => true, + 'alamat' => "Koordinat: {$lat}, {$lng}", + 'detail' => null + ]); + exit; +} + +// Susun alamat yang rapi +$addr = $data['address'] ?? []; +$parts = array_filter([ + $addr['road'] ?? $addr['pedestrian'] ?? $addr['path'] ?? null, + $addr['house_number'] ?? null, + $addr['neighbourhood'] ?? $addr['suburb'] ?? null, + $addr['city'] ?? $addr['town'] ?? $addr['village'] ?? $addr['county'] ?? null, +]); + +$alamat = !empty($parts) + ? implode(', ', $parts) + : ($data['display_name'] ?? "Koordinat: {$lat}, {$lng}"); + +echo json_encode([ + 'success' => true, + 'alamat' => $alamat, + 'display_name' => $data['display_name'] ?? '', + 'detail' => $addr +]); diff --git a/sig-kemiskinan/api/notifikasi.php b/sig-kemiskinan/api/notifikasi.php new file mode 100644 index 0000000..7acc373 --- /dev/null +++ b/sig-kemiskinan/api/notifikasi.php @@ -0,0 +1,161 @@ +prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + // Hitung unread + $unreadStmt = $db->prepare("SELECT COUNT(*) FROM notifikasi WHERE sudah_dibaca = 0 AND (user_id = ? OR user_id IS NULL)"); + $unreadStmt->execute([$userId ?? 0]); + $unreadCount = (int)$unreadStmt->fetchColumn(); + + jsonResponse(['success' => true, 'data' => $rows, 'unread_count' => $unreadCount]); + break; + + // ════════════════════════════════════════════════════════ + // PUT – Tandai sudah dibaca + // ════════════════════════════════════════════════════════ + case 'PUT': + $in = getInput(); + $id = $in['id'] ?? 'all'; + + if ($id === 'all') { + $userId = (int)($in['user_id'] ?? 0); + $db->prepare("UPDATE notifikasi SET sudah_dibaca = 1 WHERE user_id = ? OR user_id IS NULL") + ->execute([$userId]); + jsonResponse(['success' => true, 'message' => 'Semua notifikasi ditandai dibaca']); + } else { + $db->prepare("UPDATE notifikasi SET sudah_dibaca = 1 WHERE id = ?") + ->execute([(int)$id]); + jsonResponse(['success' => true, 'message' => 'Notifikasi ditandai dibaca']); + } + break; + + // ════════════════════════════════════════════════════════ + // POST – Buat notifikasi manual + // ════════════════════════════════════════════════════════ + case 'POST': + $in = getInput(); + if (empty($in['judul']) || empty($in['pesan'])) { + jsonResponse(['success' => false, 'error' => 'Judul dan pesan wajib diisi'], 400); + } + $db->prepare(" + INSERT INTO notifikasi (user_id, jenis, judul, pesan, penduduk_id) + VALUES (?,?,?,?,?) + ")->execute([ + $in['user_id'] ?? null, + $in['jenis'] ?? 'info', + $in['judul'], + $in['pesan'], + $in['penduduk_id'] ?? null, + ]); + jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Notifikasi dibuat']); + break; + + default: + jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405); +} + +// ============================================================ +// FUNGSI: Generate notifikasi otomatis +// ============================================================ +function generateNotifikasi(PDO $db): void { + + // 1. KK belum terverifikasi + $belumVerif = $db->query(" + SELECT id, nama_kk FROM penduduk_miskin + WHERE status_verifikasi = 'belum_diverifikasi' + AND id NOT IN ( + SELECT DISTINCT penduduk_id FROM notifikasi + WHERE jenis = 'belum_verifikasi' AND sudah_dibaca = 0 + ) + LIMIT 20 + ")->fetchAll(); + + foreach ($belumVerif as $p) { + $db->prepare(" + INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id) + VALUES ('belum_verifikasi','Data Menunggu Verifikasi',?,?) + ")->execute(["KK {$p['nama_kk']} belum diverifikasi.", $p['id']]); + } + + // 2. KK belum menerima bantuan apapun (sangat miskin) + $belumBantuan = $db->query(" + SELECT id, nama_kk FROM penduduk_miskin + WHERE kategori_kemiskinan = 'sangat_miskin' + AND id NOT IN ( + SELECT penduduk_id FROM bantuan_penduduk WHERE status = 'aktif' + ) + AND id NOT IN ( + SELECT DISTINCT penduduk_id FROM notifikasi + WHERE jenis = 'belum_bantuan' AND sudah_dibaca = 0 + ) + LIMIT 20 + ")->fetchAll(); + + foreach ($belumBantuan as $p) { + $db->prepare(" + INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id) + VALUES ('belum_bantuan','KK Sangat Miskin Tanpa Bantuan',?,?) + ")->execute(["KK {$p['nama_kk']} berkategori sangat miskin namun belum menerima bantuan apapun.", $p['id']]); + } + + // 3. Data usang (tidak diupdate > DATA_USANG_HARI hari) + $usang = $db->prepare(" + SELECT id, nama_kk, DATEDIFF(NOW(), updated_at) AS hari_usang + FROM penduduk_miskin + WHERE updated_at < DATE_SUB(NOW(), INTERVAL ? DAY) + AND id NOT IN ( + SELECT DISTINCT penduduk_id FROM notifikasi + WHERE jenis = 'data_usang' AND sudah_dibaca = 0 + ) + LIMIT 10 + "); + $usang->execute([DATA_USANG_HARI]); + + foreach ($usang->fetchAll() as $p) { + $db->prepare(" + INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id) + VALUES ('data_usang','Data Tidak Diperbarui',?,?) + ")->execute(["Data KK {$p['nama_kk']} belum diperbarui selama {$p['hari_usang']} hari.", $p['id']]); + } +} diff --git a/sig-kemiskinan/api/penduduk.php b/sig-kemiskinan/api/penduduk.php new file mode 100644 index 0000000..94b2196 --- /dev/null +++ b/sig-kemiskinan/api/penduduk.php @@ -0,0 +1,308 @@ +prepare($SELECT_SQL . " WHERE pm.id = ?"); + $stmt->execute([$id]); + $row = $stmt->fetch(); + if (!$row) jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404); + + // Ambil semua foto + $fotoStmt = $db->prepare("SELECT * FROM foto_rumah WHERE penduduk_id = ? ORDER BY is_primary DESC, created_at ASC"); + $fotoStmt->execute([$id]); + $row['fotos'] = $fotoStmt->fetchAll(); + + // Ambil semua bantuan + $bantuanStmt = $db->prepare(" + SELECT bp.*, jb.kode, jb.nama AS nama_bantuan, jb.warna_badge + FROM bantuan_penduduk bp + JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id + WHERE bp.penduduk_id = ? + ORDER BY jb.kode + "); + $bantuanStmt->execute([$id]); + $row['bantuans'] = $bantuanStmt->fetchAll(); + + jsonResponse(['success' => true, 'data' => $row]); + } + + // GET list dengan filter + $sql = $SELECT_SQL . " WHERE 1=1"; + $params = []; + + if ($kelurahan) { $sql .= " AND w.kelurahan = ?"; $params[] = $kelurahan; } + if ($kecamatan) { $sql .= " AND w.kecamatan = ?"; $params[] = $kecamatan; } + if ($kategori) { $sql .= " AND pm.kategori_kemiskinan = ?"; $params[] = $kategori; } + if ($verifikasi) { $sql .= " AND pm.status_verifikasi = ?"; $params[] = $verifikasi; } + if ($surveyor) { $sql .= " AND pm.surveyor_id = ?"; $params[] = $surveyor; } + if ($search) { $sql .= " AND (pm.nama_kk LIKE ? OR pm.nik_kk LIKE ? OR pm.alamat LIKE ?)"; + $params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%"; } + if ($bantuan === 'tidak') { + $sql .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') = 0"; + } elseif ($bantuan === 'ada') { + $sql .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') > 0"; + } + + $sql .= " ORDER BY pm.nama_kk ASC"; + + $stmt = $db->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + // Statistik ringkas + $stats = [ + 'total' => count($rows), + 'total_jiwa' => array_sum(array_column($rows, 'jumlah_anggota')), + 'penerima_bantuan' => count(array_filter($rows, fn($r) => $r['jumlah_bantuan'] > 0)), + 'belum_verifikasi' => count(array_filter($rows, fn($r) => $r['status_verifikasi'] === 'belum_diverifikasi')), + ]; + + jsonResponse(['success' => true, 'data' => $rows, 'stats' => $stats]); + break; + + // ════════════════════════════════════════════════════════ + // POST – Tambah penduduk baru + // ════════════════════════════════════════════════════════ + case 'POST': + $in = getInput(); + + // Validasi wajib + $required = ['nik_kk', 'nama_kk', 'lat', 'lng']; + foreach ($required as $f) { + if (empty($in[$f])) jsonResponse(['success' => false, 'error' => "Field '$f' wajib diisi"], 400); + } + + // Cek NIK duplikat + $cek = $db->prepare("SELECT id FROM penduduk_miskin WHERE nik_kk = ?"); + $cek->execute([trim($in['nik_kk'])]); + if ($cek->fetch()) jsonResponse(['success' => false, 'error' => 'NIK sudah terdaftar di sistem'], 409); + + // Hitung kategori otomatis + $penghasilan = isset($in['penghasilan_bulanan']) && $in['penghasilan_bulanan'] !== '' + ? (float)$in['penghasilan_bulanan'] : null; + $kategori = hitungKategori($penghasilan); + + $wilayahId = isset($in['wilayah_id']) && $in['wilayah_id'] !== '' ? (int)$in['wilayah_id'] : null; + $surveyorId = isset($in['surveyor_id']) && $in['surveyor_id'] !== '' ? (int)$in['surveyor_id'] : null; + + $statusPekerjaan = $in['status_pekerjaan'] ?? 'tidak_bekerja'; + $allowedPekerjaan = ['tidak_bekerja','buruh_harian','petani','nelayan','pedagang','pegawai_swasta','wiraswasta','lainnya']; + if (!in_array($statusPekerjaan, $allowedPekerjaan)) { + $statusPekerjaan = 'lainnya'; + } + + $db->prepare(" + INSERT INTO penduduk_miskin ( + nik_kk, nama_kk, no_hp, alamat, wilayah_id, rt, rw, lat, lng, + jumlah_anggota, jumlah_anak_sekolah, + penghasilan_bulanan, sumber_penghasilan, status_pekerjaan, + kategori_kemiskinan, kondisi_rumah, kepemilikan_rumah, + punya_bpjs, jenis_bpjs, status_verifikasi, surveyor_id + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ")->execute([ + trim($in['nik_kk']), + trim($in['nama_kk']), + $in['no_hp'] ?? null, + $in['alamat'] ?? null, + $wilayahId, + $in['rt'] ?? null, + $in['rw'] ?? null, + (float)$in['lat'], + (float)$in['lng'], + max(1, (int)($in['jumlah_anggota'] ?? 1)), + max(0, (int)($in['jumlah_anak_sekolah'] ?? 0)), + $penghasilan, + $in['sumber_penghasilan'] ?? null, + $statusPekerjaan, + $kategori, + $in['kondisi_rumah'] ?? 'tidak_layak', + $in['kepemilikan_rumah'] ?? 'milik_sendiri', + (int)($in['punya_bpjs'] ?? 0), + $in['jenis_bpjs'] ?? 'tidak_punya', + 'belum_diverifikasi', + $surveyorId, + ]); + + $newId = (int)$db->lastInsertId(); + assignRumahIbadah($newId); + + // Catat riwayat & notifikasi – tidak block jika gagal (misal user_id tidak ada) + try { + catatRiwayat($newId, (int)($surveyorId ?? 1), 'submit', 'Data baru dari survei'); + } catch(\Exception $e) { /* abaikan error audit trail */ } + + try { + $db->prepare(" + INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id) + VALUES ('belum_verifikasi', 'Data Baru Belum Diverifikasi', ?, ?) + ")->execute(["Data KK {$in['nama_kk']} (NIK: {$in['nik_kk']}) menunggu verifikasi admin.", $newId]); + } catch(\Exception $e) { /* abaikan error notifikasi */ } + + $stmt = $db->prepare($SELECT_SQL . " WHERE pm.id = ?"); + $stmt->execute([$newId]); + jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Data penduduk berhasil ditambahkan']); + break; + + // ════════════════════════════════════════════════════════ + // PUT – Update data penduduk + // ════════════════════════════════════════════════════════ + case 'PUT': + $in = getInput(); + $id = (int)($in['id'] ?? $_GET['id'] ?? 0); + if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400); + + // Ambil data lama untuk audit + $old = $db->prepare("SELECT * FROM penduduk_miskin WHERE id = ?"); + $old->execute([$id]); + $dataLama = $old->fetch(); + if (!$dataLama) jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404); + + // Hitung kategori ulang jika penghasilan berubah + $penghasilan = array_key_exists('penghasilan_bulanan', $in) + ? (($in['penghasilan_bulanan'] !== '') ? (float)$in['penghasilan_bulanan'] : null) + : (float)$dataLama['penghasilan_bulanan']; + $kategori = hitungKategori($penghasilan); + + $allowed = [ + 'nama_kk','no_hp','alamat','wilayah_id','rt','rw','lat','lng', + 'jumlah_anggota','jumlah_anak_sekolah','penghasilan_bulanan', + 'sumber_penghasilan','status_pekerjaan','kondisi_rumah', + 'kepemilikan_rumah','punya_bpjs','jenis_bpjs', + 'status_verifikasi','catatan_verifikasi' + ]; + + $fields = []; $values = []; $posChanged = false; + foreach ($allowed as $f) { + if (!array_key_exists($f, $in)) continue; + $fields[] = "$f = ?"; + if ($f === 'lat' || $f === 'lng') { $values[] = (float)$in[$f]; $posChanged = true; } + elseif ($f === 'wilayah_id') { $values[] = (isset($in[$f]) && $in[$f] !== '') ? (int)$in[$f] : null; } + elseif ($f === 'status_pekerjaan') { + $statusPekerjaan = $in[$f] ?? 'tidak_bekerja'; + $allowedPekerjaan = ['tidak_bekerja','buruh_harian','petani','nelayan','pedagang','pegawai_swasta','wiraswasta','lainnya']; + $values[] = in_array($statusPekerjaan, $allowedPekerjaan) ? $statusPekerjaan : 'lainnya'; + } + elseif (in_array($f, ['jumlah_anggota','jumlah_anak_sekolah','punya_bpjs'])) $values[] = (int)$in[$f]; + elseif ($f === 'penghasilan_bulanan') $values[] = $penghasilan; + else $values[] = trim((string)$in[$f]); + } + + // Update kategori otomatis + $fields[] = "kategori_kemiskinan = ?"; + $values[] = $kategori; + + // Jika diverifikasi, catat verifikator + if (isset($in['status_verifikasi']) && $in['status_verifikasi'] === 'terverifikasi') { + $fields[] = "verified_by = ?"; $values[] = $in['verified_by'] ?? 1; + $fields[] = "verified_at = NOW()"; + } + + if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data untuk diupdate'], 400); + + $values[] = $id; + $db->prepare("UPDATE penduduk_miskin SET " . implode(', ', $fields) . " WHERE id = ?") + ->execute($values); + + if ($posChanged) assignRumahIbadah($id); + + $aksi = 'update'; + if (isset($in['status_verifikasi'])) { + if ($in['status_verifikasi'] === 'terverifikasi') { + $aksi = 'verifikasi'; + } elseif ($in['status_verifikasi'] === 'ditolak') { + $aksi = 'tolak'; + } + } + try { + catatRiwayat($id, (int)($in['updated_by'] ?? 1), $aksi, $in['catatan_verifikasi'] ?? null, $dataLama); + } catch(\Exception $e) { /* abaikan error audit trail */ } + + $stmt = $db->prepare($SELECT_SQL . " WHERE pm.id = ?"); + $stmt->execute([$id]); + jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Data berhasil diperbarui']); + break; + + // ════════════════════════════════════════════════════════ + // DELETE – Hapus penduduk + // ════════════════════════════════════════════════════════ + case 'DELETE': + $id = (int)($_GET['id'] ?? getInput()['id'] ?? 0); + if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400); + + // Hapus foto fisik terlebih dahulu + $fotos = $db->prepare("SELECT file_path FROM foto_rumah WHERE penduduk_id = ?"); + $fotos->execute([$id]); + foreach ($fotos->fetchAll() as $foto) { + $path = __DIR__ . '/../' . $foto['file_path']; + if (file_exists($path)) @unlink($path); + } + + catatRiwayat($id, 1, 'hapus', 'Data dihapus oleh admin'); + $db->prepare("DELETE FROM penduduk_miskin WHERE id = ?")->execute([$id]); + jsonResponse(['success' => true, 'message' => 'Data penduduk berhasil dihapus']); + break; + + default: + jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405); + } +} catch (\Throwable $e) { + jsonResponse(['success' => false, 'error' => 'Database error: ' . $e->getMessage()], 500); +} \ No newline at end of file diff --git a/sig-kemiskinan/api/rumah_ibadah.php b/sig-kemiskinan/api/rumah_ibadah.php new file mode 100644 index 0000000..1624c84 --- /dev/null +++ b/sig-kemiskinan/api/rumah_ibadah.php @@ -0,0 +1,131 @@ +prepare(" + SELECT ri.*, + COUNT(pm.id) as jumlah_kk, + COALESCE(SUM(pm.jumlah_anggota), 0) as total_jiwa + FROM rumah_ibadah ri + LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id = ri.id + WHERE ri.id = ? + GROUP BY ri.id + "); + $stmt->execute([$id]); + $row = $stmt->fetch(); + if ($row) jsonResponse(['success' => true, 'data' => $row]); + else jsonResponse(['success' => false, 'error' => 'Not found'], 404); + } + + $rows = $db->query(" + SELECT ri.*, + COUNT(pm.id) as jumlah_kk, + COALESCE(SUM(pm.jumlah_anggota), 0) as total_jiwa + FROM rumah_ibadah ri + LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id = ri.id + GROUP BY ri.id + ORDER BY ri.nama ASC + ")->fetchAll(); + jsonResponse(['success' => true, 'data' => $rows]); + break; + + // ── POST buat rumah ibadah baru + case 'POST': + $input = getInput(); + $required = ['nama', 'lat', 'lng']; + foreach ($required as $field) { + if (empty($input[$field])) { + jsonResponse(['success' => false, 'error' => "Field '$field' wajib diisi"], 400); + } + } + + $stmt = $db->prepare(" + INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, lat, lng, radius) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + "); + $stmt->execute([ + trim($input['nama']), + $input['jenis'] ?? 'masjid', + $input['alamat'] ?? '', + $input['pic'] ?? '', + $input['no_wa'] ?? '', + (float)$input['lat'], + (float)$input['lng'], + (int)($input['radius'] ?? 500), + ]); + + $newId = (int)$db->lastInsertId(); + reassignAll(); + + $stmt2 = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?"); + $stmt2->execute([$newId]); + jsonResponse(['success' => true, 'data' => $stmt2->fetch(), 'message' => 'Rumah ibadah berhasil ditambahkan']); + break; + + // ── PUT update rumah ibadah + case 'PUT': + $input = getInput(); + $id = $input['id'] ?? $_GET['id'] ?? null; + if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400); + + $fields = []; + $values = []; + $allowed = ['nama', 'jenis', 'alamat', 'pic', 'no_wa', 'lat', 'lng', 'radius']; + foreach ($allowed as $f) { + if (isset($input[$f])) { + $fields[] = "$f = ?"; + $values[] = ($f === 'lat' || $f === 'lng') ? (float)$input[$f] : + ($f === 'radius' ? (int)$input[$f] : trim($input[$f])); + } + } + if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data yang diupdate'], 400); + + $values[] = $id; + $db->prepare("UPDATE rumah_ibadah SET " . implode(', ', $fields) . " WHERE id = ?")->execute($values); + + // Radius berubah → reassign semua + if (isset($input['radius']) || isset($input['lat']) || isset($input['lng'])) { + reassignAll(); + } + + $stmt = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?"); + $stmt->execute([$id]); + jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Berhasil diupdate']); + break; + + // ── DELETE hapus rumah ibadah + case 'DELETE': + $id = $_GET['id'] ?? getInput()['id'] ?? null; + if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400); + + // Lepas referensi penduduk + $db->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id = NULL, jarak_ke_ibadah = NULL WHERE rumah_ibadah_id = ?") + ->execute([$id]); + $db->prepare("DELETE FROM rumah_ibadah WHERE id = ?")->execute([$id]); + + reassignAll(); + jsonResponse(['success' => true, 'message' => 'Berhasil dihapus']); + break; + + default: + jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405); +} diff --git a/sig-kemiskinan/api/update_radius.php b/sig-kemiskinan/api/update_radius.php new file mode 100644 index 0000000..177fb95 --- /dev/null +++ b/sig-kemiskinan/api/update_radius.php @@ -0,0 +1,29 @@ + false, 'error' => 'id dan radius diperlukan'], 400); +} + +$radius = max(100, min(5000, (int)$radius)); // clamp 100–5000m + +$db = getDB(); +$db->prepare("UPDATE rumah_ibadah SET radius = ? WHERE id = ?")->execute([$radius, $id]); + +reassignAll(); + +jsonResponse(['success' => true, 'radius' => $radius, 'message' => "Radius diubah ke {$radius}m, penduduk di-reassign"]); diff --git a/sig-kemiskinan/api/wilayah.php b/sig-kemiskinan/api/wilayah.php new file mode 100644 index 0000000..f02bae1 --- /dev/null +++ b/sig-kemiskinan/api/wilayah.php @@ -0,0 +1,68 @@ +query("SELECT DISTINCT kelurahan, kecamatan FROM wilayah ORDER BY kelurahan")->fetchAll(); + jsonResponse(['success' => true, 'data' => $rows]); + } + if ($type === 'kecamatan') { + $rows = $db->query("SELECT DISTINCT kecamatan FROM wilayah ORDER BY kecamatan")->fetchAll(); + jsonResponse(['success' => true, 'data' => array_column($rows, 'kecamatan')]); + } + } + + $sql = "SELECT * FROM wilayah WHERE 1=1"; + $params = []; + if ($kelurahan) { $sql .= " AND kelurahan = ?"; $params[] = $kelurahan; } + if ($kecamatan) { $sql .= " AND kecamatan = ?"; $params[] = $kecamatan; } + $sql .= " ORDER BY kelurahan, rt, rw"; + + $stmt = $db->prepare($sql); + $stmt->execute($params); + jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]); + break; + + // ── POST tambah wilayah ─────────────────────────────── + case 'POST': + $in = getInput(); + if (empty($in['kelurahan']) || empty($in['kecamatan'])) { + jsonResponse(['success' => false, 'error' => 'Kelurahan dan kecamatan wajib diisi'], 400); + } + $db->prepare("INSERT INTO wilayah (kelurahan, kecamatan, kota, rt, rw) VALUES (?,?,?,?,?)") + ->execute([ + trim($in['kelurahan']), + trim($in['kecamatan']), + trim($in['kota'] ?? 'Pontianak'), + $in['rt'] ?? null, + $in['rw'] ?? null, + ]); + jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Wilayah ditambahkan']); + break; + + default: + jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405); +} diff --git a/sig-kemiskinan/assets/img/uploads/foto_14_1781095115_a7fa167f.jpeg b/sig-kemiskinan/assets/img/uploads/foto_14_1781095115_a7fa167f.jpeg new file mode 100644 index 0000000..588efd0 Binary files /dev/null and b/sig-kemiskinan/assets/img/uploads/foto_14_1781095115_a7fa167f.jpeg differ diff --git a/sig-kemiskinan/assets/img/uploads/foto_15_1781099424_25542d1c.jpg b/sig-kemiskinan/assets/img/uploads/foto_15_1781099424_25542d1c.jpg new file mode 100644 index 0000000..758621c Binary files /dev/null and b/sig-kemiskinan/assets/img/uploads/foto_15_1781099424_25542d1c.jpg differ diff --git a/sig-kemiskinan/assets/img/uploads/foto_16_1781099576_96e58d0d.jpeg b/sig-kemiskinan/assets/img/uploads/foto_16_1781099576_96e58d0d.jpeg new file mode 100644 index 0000000..63231d2 Binary files /dev/null and b/sig-kemiskinan/assets/img/uploads/foto_16_1781099576_96e58d0d.jpeg differ diff --git a/sig-kemiskinan/database.sql b/sig-kemiskinan/database.sql new file mode 100644 index 0000000..f6ed1c4 --- /dev/null +++ b/sig-kemiskinan/database.sql @@ -0,0 +1,363 @@ +-- ============================================================ +-- SIG Penduduk Miskin v2.0 – Database Schema +-- WebGIS Pendataan & Monitoring Bantuan Sosial +-- Update: 2026 | Versi: 2.0 +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS sig_kemiskinan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE sig_kemiskinan; + +SET FOREIGN_KEY_CHECKS = 0; + +-- ============================================================ +-- TABEL 1: users +-- ============================================================ +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(100) NOT NULL, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL COMMENT 'bcrypt hash', + role ENUM('admin','petugas','pimpinan') NOT NULL DEFAULT 'petugas', + wilayah_tugas VARCHAR(100) NULL COMMENT 'Kelurahan/RT wilayah tugas petugas', + aktif TINYINT(1) DEFAULT 1, + last_login TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_username (username), + INDEX idx_role (role) +); + +-- ============================================================ +-- TABEL 2: wilayah +-- ============================================================ +CREATE TABLE IF NOT EXISTS wilayah ( + id INT AUTO_INCREMENT PRIMARY KEY, + kelurahan VARCHAR(100) NOT NULL, + kecamatan VARCHAR(100) NOT NULL, + kota VARCHAR(100) NOT NULL DEFAULT 'Pontianak', + rt VARCHAR(10) NULL, + rw VARCHAR(10) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_kelurahan (kelurahan), + INDEX idx_kecamatan (kecamatan) +); + +-- ============================================================ +-- TABEL 3: rumah_ibadah (Tidak berubah) +-- ============================================================ +CREATE TABLE IF NOT EXISTS rumah_ibadah ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(255) NOT NULL, + jenis ENUM('masjid','musholla','gereja','pura','vihara','klenteng','lainnya') DEFAULT 'masjid', + alamat TEXT, + pic VARCHAR(255) COMMENT 'Person In Charge / Penanggung Jawab', + no_wa VARCHAR(20), + lat DECIMAL(10, 8) NOT NULL, + lng DECIMAL(11, 8) NOT NULL, + radius INT DEFAULT 500 COMMENT 'Radius dalam meter', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- ============================================================ +-- TABEL 4: penduduk_miskin (DIPERLUAS dari v1.1) +-- ============================================================ +CREATE TABLE IF NOT EXISTS penduduk_miskin ( + id INT AUTO_INCREMENT PRIMARY KEY, + + -- Identitas Kepala Keluarga + nik_kk VARCHAR(16) NOT NULL UNIQUE COMMENT 'NIK KK 16 digit', + nama_kk VARCHAR(255) NOT NULL COMMENT 'Nama Kepala Keluarga', + no_hp VARCHAR(20) NULL COMMENT 'No HP/WhatsApp', + + -- Lokasi + alamat TEXT NULL, + wilayah_id INT NULL, + rt VARCHAR(10) NULL, + rw VARCHAR(10) NULL, + lat DECIMAL(10,8) NOT NULL, + lng DECIMAL(11,8) NOT NULL, + + -- Data Keluarga + jumlah_anggota INT NOT NULL DEFAULT 1, + jumlah_anak_sekolah INT NOT NULL DEFAULT 0, + + -- Data Ekonomi + penghasilan_bulanan DECIMAL(12,2) NULL COMMENT 'Penghasilan per bulan (Rp)', + sumber_penghasilan VARCHAR(255) NULL, + status_pekerjaan ENUM( + 'tidak_bekerja','buruh_harian','petani','nelayan', + 'pedagang','pegawai_swasta','wiraswasta','lainnya' + ) DEFAULT 'tidak_bekerja', + + -- Kategori Kemiskinan (dihitung otomatis) + kategori_kemiskinan ENUM('sangat_miskin','miskin','hampir_miskin','mampu') DEFAULT 'sangat_miskin', + + -- Kondisi Rumah + kondisi_rumah ENUM('sangat_tidak_layak','tidak_layak','layak') DEFAULT 'tidak_layak', + kepemilikan_rumah ENUM('milik_sendiri','kontrak','menumpang','lainnya') DEFAULT 'milik_sendiri', + + -- Status BPJS + punya_bpjs TINYINT(1) DEFAULT 0, + jenis_bpjs ENUM('PBI','mandiri','tidak_punya') DEFAULT 'tidak_punya', + + -- Relasi ke Rumah Ibadah (kompatibel v1.1) + rumah_ibadah_id INT NULL, + jarak_ke_ibadah DECIMAL(10,2) NULL COMMENT 'Jarak dalam meter', + + -- Status Verifikasi + status_verifikasi ENUM('belum_diverifikasi','terverifikasi','ditolak') DEFAULT 'belum_diverifikasi', + catatan_verifikasi TEXT NULL, + + -- Audit Trail + surveyor_id INT NULL COMMENT 'FK users - petugas input', + verified_by INT NULL COMMENT 'FK users - admin verifikator', + verified_at TIMESTAMP NULL, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Foreign Keys + CONSTRAINT fk_pm_wilayah FOREIGN KEY (wilayah_id) REFERENCES wilayah(id) ON DELETE SET NULL, + CONSTRAINT fk_pm_ibadah FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL, + CONSTRAINT fk_pm_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_pm_verif FOREIGN KEY (verified_by) REFERENCES users(id) ON DELETE SET NULL, + + INDEX idx_nik (nik_kk), + INDEX idx_lat_lng (lat, lng), + INDEX idx_kategori (kategori_kemiskinan), + INDEX idx_wilayah (wilayah_id), + INDEX idx_status_v (status_verifikasi), + INDEX idx_surveyor (surveyor_id) +); + +-- ============================================================ +-- TABEL 5: foto_rumah +-- ============================================================ +CREATE TABLE IF NOT EXISTS foto_rumah ( + id INT AUTO_INCREMENT PRIMARY KEY, + penduduk_id INT NOT NULL, + file_path VARCHAR(500) NOT NULL COMMENT 'Path relatif dari root web', + lat_foto DECIMAL(10,8) NULL COMMENT 'Koordinat GPS saat foto diambil', + lng_foto DECIMAL(11,8) NULL, + accuracy_meter DECIMAL(8,2) NULL COMMENT 'Akurasi GPS dalam meter', + is_primary TINYINT(1) DEFAULT 0 COMMENT '1 = foto utama (thumbnail peta)', + ukuran_kb INT NULL, + keterangan VARCHAR(255) NULL, + diambil_oleh INT NULL COMMENT 'FK users', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_fr_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE, + CONSTRAINT fk_fr_user FOREIGN KEY (diambil_oleh) REFERENCES users(id) ON DELETE SET NULL, + + INDEX idx_penduduk (penduduk_id), + INDEX idx_primary (penduduk_id, is_primary) +); + +-- ============================================================ +-- TABEL 6: jenis_bantuan +-- ============================================================ +CREATE TABLE IF NOT EXISTS jenis_bantuan ( + id INT AUTO_INCREMENT PRIMARY KEY, + kode VARCHAR(20) NOT NULL UNIQUE, + nama VARCHAR(100) NOT NULL, + deskripsi TEXT NULL, + warna_badge VARCHAR(7) DEFAULT '#4f8ef7' COMMENT 'Hex color badge', + aktif TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================ +-- TABEL 7: bantuan_penduduk (Many-to-Many pivot) +-- ============================================================ +CREATE TABLE IF NOT EXISTS bantuan_penduduk ( + id INT AUTO_INCREMENT PRIMARY KEY, + penduduk_id INT NOT NULL, + bantuan_id INT NOT NULL, + status ENUM('aktif','tidak_aktif','dicabut') DEFAULT 'aktif', + tanggal_mulai DATE NULL, + tanggal_selesai DATE NULL, + nominal_bulanan DECIMAL(12,2) NULL, + nomor_kpj VARCHAR(50) NULL COMMENT 'Nomor kartu/KPJ bantuan', + keterangan TEXT NULL, + diinput_oleh INT NULL COMMENT 'FK users', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + CONSTRAINT fk_bp_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE, + CONSTRAINT fk_bp_bantuan FOREIGN KEY (bantuan_id) REFERENCES jenis_bantuan(id) ON DELETE RESTRICT, + CONSTRAINT fk_bp_user FOREIGN KEY (diinput_oleh) REFERENCES users(id) ON DELETE SET NULL, + + UNIQUE KEY uq_penduduk_bantuan (penduduk_id, bantuan_id), + INDEX idx_penduduk (penduduk_id), + INDEX idx_bantuan (bantuan_id), + INDEX idx_status (status) +); + +-- ============================================================ +-- TABEL 8: riwayat_verifikasi (Audit Trail) +-- ============================================================ +CREATE TABLE IF NOT EXISTS riwayat_verifikasi ( + id INT AUTO_INCREMENT PRIMARY KEY, + penduduk_id INT NOT NULL, + user_id INT NOT NULL, + aksi ENUM('submit','verifikasi','tolak','update','hapus') NOT NULL, + catatan TEXT NULL, + data_lama JSON NULL COMMENT 'Snapshot data sebelum perubahan', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_rv_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE, + CONSTRAINT fk_rv_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT, + + INDEX idx_penduduk (penduduk_id), + INDEX idx_user (user_id), + INDEX idx_tanggal (created_at) +); + +-- ============================================================ +-- TABEL 9: notifikasi +-- ============================================================ +CREATE TABLE IF NOT EXISTS notifikasi ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NULL COMMENT 'NULL = broadcast semua admin', + jenis ENUM('belum_bantuan','belum_verifikasi','data_usang','info') DEFAULT 'info', + judul VARCHAR(255) NOT NULL, + pesan TEXT NOT NULL, + penduduk_id INT NULL COMMENT 'Referensi penduduk terkait', + sudah_dibaca TINYINT(1) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_notif_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_notif_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE SET NULL, + + INDEX idx_user_baca (user_id, sudah_dibaca), + INDEX idx_jenis (jenis) +); + +-- ============================================================ +-- INDEX SPASIAL +-- ============================================================ +CREATE INDEX idx_ri_lat_lng ON rumah_ibadah(lat, lng); + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================ +-- DATA AWAL: users +-- ============================================================ +INSERT INTO users (nama, username, password, role) VALUES +('Administrator', 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin'), +('Petugas Survei', 'petugas', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'petugas'), +('Kepala Dinas', 'pimpinan', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'pimpinan'); +-- Password semua akun: "password" (bcrypt) + +-- ============================================================ +-- DATA AWAL: wilayah +-- ============================================================ +INSERT INTO wilayah (kelurahan, kecamatan, kota, rt, rw) VALUES +('Siantan Tengah', 'Pontianak Utara', 'Pontianak', '01', '01'), +('Siantan Tengah', 'Pontianak Utara', 'Pontianak', '02', '01'), +('Siantan Hilir', 'Pontianak Utara', 'Pontianak', '01', '02'), +('Batu Layang', 'Pontianak Utara', 'Pontianak', '01', '01'), +('Batu Layang', 'Pontianak Utara', 'Pontianak', '02', '02'), +('Saigon', 'Pontianak Timur', 'Pontianak', '01', '01'), +('Saigon', 'Pontianak Timur', 'Pontianak', '02', '01'), +('Tanjung Hulu', 'Pontianak Timur', 'Pontianak', '01', '01'), +('Parit Mayor', 'Pontianak Timur', 'Pontianak', '01', '01'), +('Pontianak Kota', 'Pontianak Kota', 'Pontianak', '01', '01'); + +-- ============================================================ +-- DATA AWAL: rumah_ibadah +-- ============================================================ +INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, lat, lng, radius) VALUES +('Masjid Al-Ikhlas', 'masjid', 'Jl. Merdeka No. 1, Pontianak', 'Ahmad Fauzi', '081234567890', -0.0263303, 109.3425094, 500), +('Masjid Ar-Rahmat', 'masjid', 'Jl. Gajah Mada No. 45, Pontianak', 'Budi Santoso', '082345678901', -0.0320000, 109.3380000, 500), +('Gereja Bethel', 'gereja', 'Jl. Diponegoro No. 12, Pontianak', 'Pendeta Yusuf', '083456789012', -0.0290000, 109.3450000, 500); + +-- ============================================================ +-- DATA AWAL: jenis_bantuan +-- ============================================================ +INSERT INTO jenis_bantuan (kode, nama, deskripsi, warna_badge) VALUES +('PKH', 'Program Keluarga Harapan', 'Bantuan tunai bersyarat untuk KK miskin', '#22c55e'), +('BPNT', 'Bantuan Pangan Non Tunai', 'Sembako via e-warong setiap 2 bulan', '#3b82f6'), +('KIS', 'Kartu Indonesia Sehat', 'Akses layanan kesehatan gratis (BPJS PBI)', '#ec4899'), +('BLT', 'Bantuan Langsung Tunai', 'Bantuan tunai darurat/sementara dari desa/kel', '#f59e0b'), +('BST', 'Bantuan Sosial Tunai', 'Bantuan sosial tunai dari Kemensos', '#8b5cf6'), +('PIP', 'Program Indonesia Pintar', 'Bantuan pendidikan untuk anak sekolah', '#06b6d4'); + +-- ============================================================ +-- DATA AWAL: penduduk_miskin (contoh) +-- ============================================================ +INSERT INTO penduduk_miskin ( + nik_kk, nama_kk, no_hp, alamat, wilayah_id, rt, rw, + lat, lng, jumlah_anggota, jumlah_anak_sekolah, + penghasilan_bulanan, sumber_penghasilan, status_pekerjaan, + kategori_kemiskinan, kondisi_rumah, kepemilikan_rumah, + punya_bpjs, jenis_bpjs, status_verifikasi, surveyor_id +) VALUES +('6171010101010001', 'Suyatno', '081200000001', 'Jl. Merdeka Gg. III No. 5', 1, '01', '01', -0.0270000, 109.3430000, 4, 2, 850000, 'Buruh bangunan', 'buruh_harian', 'miskin', 'tidak_layak', 'menumpang', 1, 'PBI', 'terverifikasi', 1), +('6171010101010002', 'Rohani', '081200000002', 'Jl. Merdeka Gg. IV No. 2', 1, '02', '01', -0.0255000, 109.3420000, 3, 1, 450000, 'Jualan kecil', 'pedagang', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'terverifikasi', 1), +('6171010101010003', 'Hamid Husin', '081200000003', 'Jl. Gajah Mada Gg. II No. 8', 2, '01', '02', -0.0315000, 109.3375000, 5, 3, 1200000, 'Ojek online', 'buruh_harian', 'mampu', 'tidak_layak', 'kontrak', 0, 'tidak_punya','belum_diverifikasi',2), +('6171010101010004', 'Yanti Susanti', '081200000004', 'Jl. Veteran No. 99', 3, '01', '01', -0.0500000, 109.3600000, 2, 0, 300000, 'Tidak ada', 'tidak_bekerja', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'terverifikasi', 1), +('6171010101010005', 'Supriyadi', '081200000005', 'Jl. Budi Utomo No. 12', 4, '02', '01', -0.0340000, 109.3510000, 6, 4, 600000, 'Petani kecil', 'petani', 'miskin', 'tidak_layak', 'milik_sendiri', 1, 'PBI', 'terverifikasi', 1), +('6171010101010006', 'Marlina', '081200000006', 'Gg. Damai No. 3', 5, '01', '02', -0.0410000, 109.3480000, 3, 1, 200000, 'Buruh cuci', 'buruh_harian', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 0, 'tidak_punya','belum_diverifikasi',2), +('6171010101010007', 'Junaidi', '081200000007', 'Jl. Khatulistiwa No. 45', 6, '01', '01', -0.0380000, 109.3550000, 4, 2, 950000, 'Nelayan', 'nelayan', 'miskin', 'tidak_layak', 'milik_sendiri', 1, 'mandiri', 'terverifikasi', 1), +('6171010101010008', 'Siti Aminah', '081200000008', 'Gg. Melati No. 7', 7, '02', '02', -0.0290000, 109.3395000, 5, 3, 400000, 'Pembantu RT', 'buruh_harian', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'belum_diverifikasi',2), +('6171010101010009', 'Rahman', '081200000009', 'Jl. Tanjung Pura No. 88', 8, '01', '01', -0.0220000, 109.3460000, 3, 1, 750000, 'Pedagang kaki lima','pedagang', 'miskin', 'tidak_layak', 'kontrak', 0, 'tidak_punya','terverifikasi', 1), +('6171010101010010', 'Dewi Kartika', '081200000010', 'Gg. Bahagia No. 2', 9, '01', '01', -0.0450000, 109.3530000, 4, 2, 500000, 'Buruh pabrik', 'buruh_harian', 'sangat_miskin', 'tidak_layak', 'kontrak', 1, 'PBI', 'terverifikasi', 1); + +-- ============================================================ +-- DATA AWAL: bantuan_penduduk +-- ============================================================ +INSERT INTO bantuan_penduduk (penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, diinput_oleh) VALUES +(1, 1, 'aktif', '2024-01-01', 500000, 1), -- Suyatno: PKH +(1, 2, 'aktif', '2024-01-01', 200000, 1), -- Suyatno: BPNT +(2, 1, 'aktif', '2023-07-01', 500000, 1), -- Rohani: PKH +(2, 3, 'aktif', '2023-07-01', 0, 1), -- Rohani: KIS +(4, 1, 'aktif', '2024-03-01', 500000, 1), -- Yanti: PKH +(4, 3, 'aktif', '2024-03-01', 0, 1), -- Yanti: KIS +(5, 2, 'aktif', '2024-02-01', 200000, 1), -- Supriyadi: BPNT +(7, 3, 'aktif', '2023-09-01', 0, 1), -- Junaidi: KIS +(9, 2, 'aktif', '2024-01-01', 200000, 1), -- Rahman: BPNT +(10, 1, 'aktif','2024-04-01', 500000, 1), -- Dewi: PKH +(10, 3, 'aktif','2024-04-01', 0, 1); -- Dewi: KIS + +-- ============================================================ +-- Update rumah_ibadah assignment (kompatibel v1.1) +-- ============================================================ +UPDATE penduduk_miskin pm +SET + rumah_ibadah_id = ( + SELECT ri.id FROM rumah_ibadah ri + WHERE ( + 6371000 * ACOS(GREATEST(-1, LEAST(1, + COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) * + COS(RADIANS(ri.lng) - RADIANS(pm.lng)) + + SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat)) + ))) + ) <= ri.radius + ORDER BY ( + 6371000 * ACOS(GREATEST(-1, LEAST(1, + COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) * + COS(RADIANS(ri.lng) - RADIANS(pm.lng)) + + SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat)) + ))) + ) ASC + LIMIT 1 + ), + jarak_ke_ibadah = ( + SELECT ROUND( + 6371000 * ACOS(GREATEST(-1, LEAST(1, + COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) * + COS(RADIANS(ri.lng) - RADIANS(pm.lng)) + + SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat)) + ))), 2 + ) FROM rumah_ibadah ri + WHERE ( + 6371000 * ACOS(GREATEST(-1, LEAST(1, + COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) * + COS(RADIANS(ri.lng) - RADIANS(pm.lng)) + + SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat)) + ))) + ) <= ri.radius + LIMIT 1 + ); diff --git a/sig-kemiskinan/includes/auth.php b/sig-kemiskinan/includes/auth.php new file mode 100644 index 0000000..faa8b26 --- /dev/null +++ b/sig-kemiskinan/includes/auth.php @@ -0,0 +1,30 @@ + false, 'error' => 'Unauthorized – silakan login', 'redirect' => '/index.html'], 401); + } + $user = $_SESSION['user']; + if (!empty($allowedRoles) && !in_array($user['role'], $allowedRoles)) { + jsonResponse(['success' => false, 'error' => 'Akses ditolak untuk role: ' . $user['role']], 403); + } + return $user; +} + +// ── Ambil user saat ini (null jika belum login) ─────────── +function currentUser(): ?array { + return $_SESSION['user'] ?? null; +} + +// ── Cek apakah sudah login (tanpa redirect) ─────────────── +function isLoggedIn(): bool { + return isset($_SESSION['user_id']); +} diff --git a/sig-kemiskinan/includes/config.php b/sig-kemiskinan/includes/config.php new file mode 100644 index 0000000..3a8face --- /dev/null +++ b/sig-kemiskinan/includes/config.php @@ -0,0 +1,144 @@ += 5jt → mampu +// ── Notifikasi ──────────────────────────────────────────── +define('DATA_USANG_HARI', 30); // data > 30 hari belum update = notif usang + +// ============================================================ +// FUNGSI: Koneksi Database (Singleton PDO) +// ============================================================ +function getDB(): PDO { + static $pdo = null; + if ($pdo === null) { + $dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET; + $options = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + try { + $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); + } catch (PDOException $e) { + http_response_code(500); + die(json_encode(['success' => false, 'error' => 'Database connection failed: ' . $e->getMessage()])); + } + } + return $pdo; +} + +// ============================================================ +// FUNGSI: Response JSON +// ============================================================ +function jsonResponse(mixed $data, int $code = 200): void { + http_response_code($code); + header('Content-Type: application/json; charset=utf-8'); + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); + header('Access-Control-Allow-Headers: Content-Type, Authorization'); + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +// ============================================================ +// FUNGSI: Baca Input (JSON body atau $_POST) +// ============================================================ +function getInput(): array { + $raw = file_get_contents('php://input'); + $data = json_decode($raw, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $data = $_POST; + } + return $data ?? []; +} + +// ============================================================ +// FUNGSI: Hitung Jarak Haversine (meter) +// ============================================================ +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)); +} + +// ============================================================ +// FUNGSI: Hitung Kategori Kemiskinan Otomatis +// ============================================================ +function hitungKategori(?float $penghasilan): string { + if ($penghasilan === null || $penghasilan < BATAS_SANGAT_MISKIN) return 'sangat_miskin'; + if ($penghasilan < BATAS_MISKIN) return 'miskin'; + if ($penghasilan < BATAS_HAMPIR_MISKIN) return 'hampir_miskin'; + return 'mampu'; +} + +// ============================================================ +// FUNGSI: Assign Rumah Ibadah (kompatibel v1.1) +// ============================================================ +function assignRumahIbadah(int $pendudukId): void { + try { + $db = getDB(); + $p = $db->prepare("SELECT lat, lng FROM penduduk_miskin WHERE id = ?"); + $p->execute([$pendudukId]); + $row = $p->fetch(); + if (!$row) return; + + $ibadahList = $db->query("SELECT id, lat, lng, radius FROM rumah_ibadah")->fetchAll(); + $closest = null; + $closestDist = PHP_FLOAT_MAX; + + foreach ($ibadahList as $ri) { + $dist = haversine((float)$row['lat'], (float)$row['lng'], (float)$ri['lat'], (float)$ri['lng']); + if ($dist <= $ri['radius'] && $dist < $closestDist) { + $closest = $ri['id']; + $closestDist = $dist; + } + } + + $db->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id = ?, jarak_ke_ibadah = ? WHERE id = ?") + ->execute([$closest, $closest ? round($closestDist, 2) : null, $pendudukId]); + } catch (\Exception $e) { /* abaikan error assign ibadah */ } +} + +function reassignAll(): void { + $db = getDB(); + $all = $db->query("SELECT id FROM penduduk_miskin")->fetchAll(); + foreach ($all as $row) assignRumahIbadah((int)$row['id']); +} + +// ============================================================ +// FUNGSI: Catat Riwayat Verifikasi +// ============================================================ +function catatRiwayat(int $pendudukId, int $userId, string $aksi, ?string $catatan = null, ?array $dataLama = null): void { + $db = getDB(); + $db->prepare(" + INSERT INTO riwayat_verifikasi (penduduk_id, user_id, aksi, catatan, data_lama) + VALUES (?, ?, ?, ?, ?) + ")->execute([$pendudukId, $userId, $aksi, $catatan, $dataLama ? json_encode($dataLama, JSON_UNESCAPED_UNICODE) : null]); +} \ No newline at end of file diff --git a/sig-kemiskinan/index.html b/sig-kemiskinan/index.html new file mode 100644 index 0000000..5d341bd --- /dev/null +++ b/sig-kemiskinan/index.html @@ -0,0 +1,2661 @@ + + + + + +WebGIS PovertyMap – WebGIS Monitoring Bantuan Sosial v3.0 + + + + + + + + + + +
+ +
+ + +
+ +
+
+ + 📊 Dashboard +
+
+ + +
+ +
+ 📊 Export Excel (.xlsx) + 📄 Export PDF +
+ +
+
+ + 🛠 Administrator + +
+
+ + +
+
+ 🏠 +
+
+
KK Terdata
+
+
+
+ 👥 +
+
+
Total Jiwa
+
+
+
+ +
+
+
Penerima Bantuan
+
+
+
+ +
+
+
Belum Bantuan
+
+
+ +
+ 🕌 +
+
+
Rumah Ibadah
+
+
+
+ + +
+ + + + + +
+
+ + + + + +
+ Klik peta untuk menentukan lokasi + +
+ + +
+
+
Legenda Peta
+ +
+
+ +
Di Dalam Radius
+
Di Luar Radius
+
+
Masjid (Islam)
+
Gereja (Kristen)
+
Pura (Hindu)
+
Vihara (Buddha)
+
Klenteng (Konghucu)
+
+
+ + + + +
+ +
+ + + +
+
+ + + + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/sig-kemiskinan/update_kategori.php b/sig-kemiskinan/update_kategori.php new file mode 100644 index 0000000..56fa552 --- /dev/null +++ b/sig-kemiskinan/update_kategori.php @@ -0,0 +1,38 @@ +exec("ALTER TABLE penduduk_miskin MODIFY kategori_kemiskinan ENUM('sangat_miskin','miskin','hampir_miskin','mampu') DEFAULT 'sangat_miskin'"); + echo "ENUM updated successfully!\n\n"; + + // 2. Re-calculate all existing records + echo "Re-calculating categories for existing records...\n"; + $stmt = $db->query("SELECT id, penghasilan_bulanan FROM penduduk_miskin"); + $rows = $stmt->fetchAll(); + + $update = $db->prepare("UPDATE penduduk_miskin SET kategori_kemiskinan = ? WHERE id = ?"); + $counts = ['sangat_miskin' => 0, 'miskin' => 0, 'hampir_miskin' => 0, 'mampu' => 0]; + + foreach ($rows as $row) { + $p = $row['penghasilan_bulanan'] !== null ? (float)$row['penghasilan_bulanan'] : null; + if ($p === null || $p < BATAS_SANGAT_MISKIN) $kat = 'sangat_miskin'; + elseif ($p < BATAS_MISKIN) $kat = 'miskin'; + elseif ($p < BATAS_HAMPIR_MISKIN) $kat = 'hampir_miskin'; + else $kat = 'mampu'; + $update->execute([$kat, $row['id']]); + $counts[$kat]++; + } + + echo "Done! Updated " . count($rows) . " records:\n"; + foreach ($counts as $kat => $c) { + echo " $kat: $c\n"; + } + +} catch (\Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; +}