From 7b3bc9178d089eb73f394334dde77dc802e0fe89 Mon Sep 17 00:00:00 2001 From: alifaisyahh Date: Tue, 9 Jun 2026 19:36:57 +0700 Subject: [PATCH] first commit --- Web GIS Layer/api_jalan.php | 118 + Web GIS Layer/api_parsil.php | 118 + Web GIS Layer/api_spbu.php | 120 + Web GIS Layer/config.php | 45 + Web GIS Layer/database.sql | 138 + Web GIS Layer/index.html | 1228 +++++++ .../PENJELASAN_SISTEM_SRS_MANUAL.md | 312 ++ Web GIS Rumah Ibadah/api_auth.php | 111 + Web GIS Rumah Ibadah/api_dashboard.php | 148 + Web GIS Rumah Ibadah/api_histori_bantuan.php | 214 ++ Web GIS Rumah Ibadah/api_keluarga_miskin.php | 285 ++ Web GIS Rumah Ibadah/api_laporan.php | 166 + Web GIS Rumah Ibadah/api_rumah_ibadah.php | 234 ++ Web GIS Rumah Ibadah/config.php | 267 ++ Web GIS Rumah Ibadah/database.sql | 289 ++ Web GIS Rumah Ibadah/index.html | 2884 +++++++++++++++++ Web GIS Rumah Ibadah/index.php | 3 + index.php | 622 ++++ 18 files changed, 7302 insertions(+) create mode 100644 Web GIS Layer/api_jalan.php create mode 100644 Web GIS Layer/api_parsil.php create mode 100644 Web GIS Layer/api_spbu.php create mode 100644 Web GIS Layer/config.php create mode 100644 Web GIS Layer/database.sql create mode 100644 Web GIS Layer/index.html create mode 100644 Web GIS Rumah Ibadah/PENJELASAN_SISTEM_SRS_MANUAL.md create mode 100644 Web GIS Rumah Ibadah/api_auth.php create mode 100644 Web GIS Rumah Ibadah/api_dashboard.php create mode 100644 Web GIS Rumah Ibadah/api_histori_bantuan.php create mode 100644 Web GIS Rumah Ibadah/api_keluarga_miskin.php create mode 100644 Web GIS Rumah Ibadah/api_laporan.php create mode 100644 Web GIS Rumah Ibadah/api_rumah_ibadah.php create mode 100644 Web GIS Rumah Ibadah/config.php create mode 100644 Web GIS Rumah Ibadah/database.sql create mode 100644 Web GIS Rumah Ibadah/index.html create mode 100644 Web GIS Rumah Ibadah/index.php create mode 100644 index.php diff --git a/Web GIS Layer/api_jalan.php b/Web GIS Layer/api_jalan.php new file mode 100644 index 0000000..2f771df --- /dev/null +++ b/Web GIS Layer/api_jalan.php @@ -0,0 +1,118 @@ +prepare($sql); + $stmt->bind_param('i', $id); + $stmt->execute(); + $row = $stmt->get_result()->fetch_assoc(); + if ($row) { + $row['geojson'] = json_decode($row['geojson']); + jsonResponse(['status' => 'success', 'data' => $row]); + } else { + jsonResponse(['status' => 'error', 'message' => 'Data tidak ditemukan'], 404); + } + } else { + $sql = "SELECT * FROM data_jalan ORDER BY id DESC"; + $rows = $conn->query($sql)->fetch_all(MYSQLI_ASSOC); + foreach ($rows as &$r) { + $r['geojson'] = json_decode($r['geojson']); + } + jsonResponse(['status' => 'success', 'data' => $rows]); + } + break; + + // ── CREATE ─────────────────────────────────────────────── + case 'POST': + $nama_jalan = clean($input['nama_jalan'] ?? ''); + $status_jalan = clean($input['status_jalan'] ?? ''); + $panjang_m = (float) ($input['panjang_m'] ?? 0); + $keterangan = clean($input['keterangan'] ?? ''); + $geojson = $input['geojson'] ?? null; + + if (!$nama_jalan || !$status_jalan || !$geojson) { + jsonResponse(['status' => 'error', 'message' => 'Field nama_jalan, status_jalan, dan geojson wajib diisi'], 400); + } + + $geojsonStr = is_string($geojson) ? $geojson : json_encode($geojson); + + $sql = "INSERT INTO data_jalan (nama_jalan, status_jalan, panjang_m, geojson, keterangan) VALUES (?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($sql); + $stmt->bind_param('ssdss', $nama_jalan, $status_jalan, $panjang_m, $geojsonStr, $keterangan); + + if ($stmt->execute()) { + jsonResponse(['status' => 'success', 'message' => 'Data jalan berhasil ditambahkan', 'id' => $conn->insert_id], 201); + } else { + jsonResponse(['status' => 'error', 'message' => $conn->error], 500); + } + break; + + // ── UPDATE ─────────────────────────────────────────────── + case 'PUT': + $id = (int) ($input['id'] ?? 0); + $nama_jalan = clean($input['nama_jalan'] ?? ''); + $status_jalan = clean($input['status_jalan'] ?? ''); + $panjang_m = (float) ($input['panjang_m'] ?? 0); + $keterangan = clean($input['keterangan'] ?? ''); + $geojson = $input['geojson'] ?? null; + + if (!$id || !$nama_jalan || !$status_jalan || !$geojson) { + jsonResponse(['status' => 'error', 'message' => 'Field id, nama_jalan, status_jalan, dan geojson wajib diisi'], 400); + } + + $geojsonStr = is_string($geojson) ? $geojson : json_encode($geojson); + + $sql = "UPDATE data_jalan SET nama_jalan=?, status_jalan=?, panjang_m=?, geojson=?, keterangan=? WHERE id=?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param('ssdssi', $nama_jalan, $status_jalan, $panjang_m, $geojsonStr, $keterangan, $id); + + if ($stmt->execute()) { + jsonResponse(['status' => 'success', 'message' => 'Data jalan berhasil diperbarui']); + } else { + jsonResponse(['status' => 'error', 'message' => $conn->error], 500); + } + break; + + // ── DELETE ─────────────────────────────────────────────── + case 'DELETE': + $id = (int) ($_GET['id'] ?? ($input['id'] ?? 0)); + + if (!$id) { + jsonResponse(['status' => 'error', 'message' => 'Parameter id wajib diisi'], 400); + } + + $stmt = $conn->prepare("DELETE FROM data_jalan WHERE id = ?"); + $stmt->bind_param('i', $id); + + if ($stmt->execute() && $stmt->affected_rows > 0) { + jsonResponse(['status' => 'success', 'message' => 'Data jalan berhasil dihapus']); + } else { + jsonResponse(['status' => 'error', 'message' => 'Data tidak ditemukan atau gagal dihapus'], 404); + } + break; + + default: + jsonResponse(['status' => 'error', 'message' => 'Method tidak diizinkan'], 405); +} diff --git a/Web GIS Layer/api_parsil.php b/Web GIS Layer/api_parsil.php new file mode 100644 index 0000000..be434b5 --- /dev/null +++ b/Web GIS Layer/api_parsil.php @@ -0,0 +1,118 @@ +prepare("SELECT * FROM data_parsil WHERE id = ?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $row = $stmt->get_result()->fetch_assoc(); + if ($row) { + $row['geojson'] = json_decode($row['geojson']); + jsonResponse(['status' => 'success', 'data' => $row]); + } else { + jsonResponse(['status' => 'error', 'message' => 'Data tidak ditemukan'], 404); + } + } else { + $rows = $conn->query("SELECT * FROM data_parsil ORDER BY id DESC")->fetch_all(MYSQLI_ASSOC); + foreach ($rows as &$r) { + $r['geojson'] = json_decode($r['geojson']); + } + jsonResponse(['status' => 'success', 'data' => $rows]); + } + break; + + // ── CREATE ─────────────────────────────────────────────── + case 'POST': + $nama_parsil = clean($input['nama_parsil'] ?? ''); + $status_hak = clean($input['status_hak'] ?? ''); + $luas_m2 = (float) ($input['luas_m2'] ?? 0); + $pemilik = clean($input['pemilik'] ?? ''); + $keterangan = clean($input['keterangan'] ?? ''); + $geojson = $input['geojson'] ?? null; + + if (!$nama_parsil || !$status_hak || !$geojson) { + jsonResponse(['status' => 'error', 'message' => 'Field nama_parsil, status_hak, dan geojson wajib diisi'], 400); + } + + $geojsonStr = is_string($geojson) ? $geojson : json_encode($geojson); + + $sql = "INSERT INTO data_parsil (nama_parsil, status_hak, luas_m2, geojson, pemilik, keterangan) VALUES (?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($sql); + $stmt->bind_param('ssdsss', $nama_parsil, $status_hak, $luas_m2, $geojsonStr, $pemilik, $keterangan); + + if ($stmt->execute()) { + jsonResponse(['status' => 'success', 'message' => 'Data parsil berhasil ditambahkan', 'id' => $conn->insert_id], 201); + } else { + jsonResponse(['status' => 'error', 'message' => $conn->error], 500); + } + break; + + // ── UPDATE ─────────────────────────────────────────────── + case 'PUT': + $id = (int) ($input['id'] ?? 0); + $nama_parsil = clean($input['nama_parsil'] ?? ''); + $status_hak = clean($input['status_hak'] ?? ''); + $luas_m2 = (float) ($input['luas_m2'] ?? 0); + $pemilik = clean($input['pemilik'] ?? ''); + $keterangan = clean($input['keterangan'] ?? ''); + $geojson = $input['geojson'] ?? null; + + if (!$id || !$nama_parsil || !$status_hak || !$geojson) { + jsonResponse(['status' => 'error', 'message' => 'Field id, nama_parsil, status_hak, dan geojson wajib diisi'], 400); + } + + $geojsonStr = is_string($geojson) ? $geojson : json_encode($geojson); + + $sql = "UPDATE data_parsil SET nama_parsil=?, status_hak=?, luas_m2=?, geojson=?, pemilik=?, keterangan=? WHERE id=?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param('ssdsssi', $nama_parsil, $status_hak, $luas_m2, $geojsonStr, $pemilik, $keterangan, $id); + + if ($stmt->execute()) { + jsonResponse(['status' => 'success', 'message' => 'Data parsil berhasil diperbarui']); + } else { + jsonResponse(['status' => 'error', 'message' => $conn->error], 500); + } + break; + + // ── DELETE ─────────────────────────────────────────────── + case 'DELETE': + $id = (int) ($_GET['id'] ?? ($input['id'] ?? 0)); + + if (!$id) { + jsonResponse(['status' => 'error', 'message' => 'Parameter id wajib diisi'], 400); + } + + $stmt = $conn->prepare("DELETE FROM data_parsil WHERE id = ?"); + $stmt->bind_param('i', $id); + + if ($stmt->execute() && $stmt->affected_rows > 0) { + jsonResponse(['status' => 'success', 'message' => 'Data parsil berhasil dihapus']); + } else { + jsonResponse(['status' => 'error', 'message' => 'Data tidak ditemukan atau gagal dihapus'], 404); + } + break; + + default: + jsonResponse(['status' => 'error', 'message' => 'Method tidak diizinkan'], 405); +} diff --git a/Web GIS Layer/api_spbu.php b/Web GIS Layer/api_spbu.php new file mode 100644 index 0000000..8656e4f --- /dev/null +++ b/Web GIS Layer/api_spbu.php @@ -0,0 +1,120 @@ +prepare("SELECT * FROM data_spbu WHERE id = ?"); + $stmt->bind_param('i', $id); + $stmt->execute(); + $row = $stmt->get_result()->fetch_assoc(); + if ($row) { + $row['geojson'] = json_decode($row['geojson']); + $row['buka_24jam'] = (bool) $row['buka_24jam']; + jsonResponse(['status' => 'success', 'data' => $row]); + } else { + jsonResponse(['status' => 'error', 'message' => 'Data tidak ditemukan'], 404); + } + } else { + $rows = $conn->query("SELECT * FROM data_spbu ORDER BY id DESC")->fetch_all(MYSQLI_ASSOC); + foreach ($rows as &$r) { + $r['geojson'] = json_decode($r['geojson']); + $r['buka_24jam'] = (bool) $r['buka_24jam']; + } + jsonResponse(['status' => 'success', 'data' => $rows]); + } + break; + + // ── CREATE ─────────────────────────────────────────────── + case 'POST': + $nama_spbu = clean($input['nama_spbu'] ?? ''); + $telepon = clean($input['telepon'] ?? ''); + $buka_24jam = isset($input['buka_24jam']) ? (int)(bool)$input['buka_24jam'] : 0; + $lat = (float) ($input['lat'] ?? 0); + $lng = (float) ($input['lng'] ?? 0); + $geojson = $input['geojson'] ?? null; + + if (!$nama_spbu || !$geojson) { + jsonResponse(['status' => 'error', 'message' => 'Field nama_spbu dan geojson wajib diisi'], 400); + } + + $geojsonStr = is_string($geojson) ? $geojson : json_encode($geojson); + + $sql = "INSERT INTO data_spbu (nama_spbu, telepon, buka_24jam, lat, lng, geojson) VALUES (?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($sql); + $stmt->bind_param('ssidds', $nama_spbu, $telepon, $buka_24jam, $lat, $lng, $geojsonStr); + + if ($stmt->execute()) { + jsonResponse(['status' => 'success', 'message' => 'Data SPBU berhasil ditambahkan', 'id' => $conn->insert_id], 201); + } else { + jsonResponse(['status' => 'error', 'message' => $conn->error], 500); + } + break; + + // ── UPDATE ─────────────────────────────────────────────── + case 'PUT': + $id = (int) ($input['id'] ?? 0); + $nama_spbu = clean($input['nama_spbu'] ?? ''); + $telepon = clean($input['telepon'] ?? ''); + $buka_24jam = isset($input['buka_24jam']) ? (int)(bool)$input['buka_24jam'] : 0; + $lat = (float) ($input['lat'] ?? 0); + $lng = (float) ($input['lng'] ?? 0); + $geojson = $input['geojson'] ?? null; + + if (!$id || !$nama_spbu || !$geojson) { + jsonResponse(['status' => 'error', 'message' => 'Field id, nama_spbu dan geojson wajib diisi'], 400); + } + + $geojsonStr = is_string($geojson) ? $geojson : json_encode($geojson); + + $sql = "UPDATE data_spbu SET nama_spbu=?, telepon=?, buka_24jam=?, lat=?, lng=?, geojson=? WHERE id=?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param('ssiddsi', $nama_spbu, $telepon, $buka_24jam, $lat, $lng, $geojsonStr, $id); + + if ($stmt->execute()) { + jsonResponse(['status' => 'success', 'message' => 'Data SPBU berhasil diperbarui']); + } else { + jsonResponse(['status' => 'error', 'message' => $conn->error], 500); + } + break; + + // ── DELETE ─────────────────────────────────────────────── + case 'DELETE': + $id = (int) ($_GET['id'] ?? ($input['id'] ?? 0)); + + if (!$id) { + jsonResponse(['status' => 'error', 'message' => 'Parameter id wajib diisi'], 400); + } + + $stmt = $conn->prepare("DELETE FROM data_spbu WHERE id = ?"); + $stmt->bind_param('i', $id); + + if ($stmt->execute() && $stmt->affected_rows > 0) { + jsonResponse(['status' => 'success', 'message' => 'Data SPBU berhasil dihapus']); + } else { + jsonResponse(['status' => 'error', 'message' => 'Data tidak ditemukan atau gagal dihapus'], 404); + } + break; + + default: + jsonResponse(['status' => 'error', 'message' => 'Method tidak diizinkan'], 405); +} diff --git a/Web GIS Layer/config.php b/Web GIS Layer/config.php new file mode 100644 index 0000000..ac7821e --- /dev/null +++ b/Web GIS Layer/config.php @@ -0,0 +1,45 @@ +connect_error) { + http_response_code(500); + die(json_encode([ + 'status' => 'error', + 'message' => 'Koneksi database gagal: ' . $conn->connect_error + ])); +} + +$conn->set_charset(DB_CHARSET); + +// ── Fungsi utilitas ────────────────────────────────────────── + +/** + * Kembalikan response JSON dan hentikan eksekusi. + */ +function jsonResponse(array $data, int $httpCode = 200): void +{ + http_response_code($httpCode); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +/** + * Sanitasi input string. + */ +function clean(string $value): string +{ + return htmlspecialchars(strip_tags(trim($value))); +} diff --git a/Web GIS Layer/database.sql b/Web GIS Layer/database.sql new file mode 100644 index 0000000..35a8379 --- /dev/null +++ b/Web GIS Layer/database.sql @@ -0,0 +1,138 @@ +-- ============================================================ +-- DATABASE: webgis_sig +-- Sistem Manajemen Data Jalan, Parsil Tanah & SPBU +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS webgis_sig CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE webgis_sig; + +-- ============================================================ +-- TABEL: data_jalan (Line / Polyline) +-- Menyimpan data ruas jalan berdasarkan status jalan +-- Geometri disimpan dalam format GeoJSON (LineString) +-- ============================================================ +CREATE TABLE IF NOT EXISTS data_jalan ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + nama_jalan VARCHAR(150) NOT NULL COMMENT 'Nama ruas jalan', + status_jalan ENUM( + 'Jalan Nasional', + 'Jalan Provinsi', + 'Jalan Kabupaten' + ) NOT NULL COMMENT 'Status / klasifikasi jalan', + panjang_m DECIMAL(12,2) DEFAULT 0.00 COMMENT 'Panjang jalan dalam meter (dihitung otomatis oleh LeafletJS)', + geojson LONGTEXT NOT NULL COMMENT 'Geometri LineString dalam format GeoJSON', + keterangan TEXT DEFAULT NULL COMMENT 'Keterangan tambahan', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- TABEL: data_parsil (Polygon) +-- Menyimpan data parsil tanah / kavling berdasarkan status kepemilikan +-- Geometri disimpan dalam format GeoJSON (Polygon) +-- ============================================================ +CREATE TABLE IF NOT EXISTS data_parsil ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + nama_parsil VARCHAR(150) NOT NULL COMMENT 'Nama / nomor parsil', + status_hak ENUM( + 'Sertifikat Hak Milik', + 'Sertifikat Hak Guna Bangunan', + 'Sertifikat Hak Guna Usaha', + 'Sertifikat Hak Pakai' + ) NOT NULL COMMENT 'Status kepemilikan / hak atas tanah', + luas_m2 DECIMAL(15,2) DEFAULT 0.00 COMMENT 'Luas tanah dalam meter persegi (dihitung otomatis oleh LeafletJS)', + geojson LONGTEXT NOT NULL COMMENT 'Geometri Polygon dalam format GeoJSON', + pemilik VARCHAR(150) DEFAULT NULL COMMENT 'Nama pemilik (opsional)', + keterangan TEXT DEFAULT NULL COMMENT 'Keterangan tambahan', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- DATA CONTOH – Jalan +-- ============================================================ +INSERT INTO data_jalan (nama_jalan, status_jalan, panjang_m, geojson, keterangan) VALUES +( + 'Jl. Sudirman', + 'Jalan Nasional', + 1250.50, + '{"type":"LineString","coordinates":[[106.8272,-6.2088],[106.8290,-6.2075],[106.8310,-6.2060],[106.8330,-6.2045]]}', + 'Jalan protokol pusat kota' +), +( + 'Jl. Gatot Subroto', + 'Jalan Provinsi', + 980.00, + '{"type":"LineString","coordinates":[[106.8200,-6.2100],[106.8220,-6.2095],[106.8240,-6.2090],[106.8260,-6.2085]]}', + 'Penghubung antar kecamatan' +), +( + 'Jl. Pramuka', + 'Jalan Kabupaten', + 620.75, + '{"type":"LineString","coordinates":[[106.8150,-6.2180],[106.8160,-6.2170],[106.8175,-6.2160],[106.8185,-6.2150]]}', + 'Jalan lingkungan kabupaten' +); + +-- ============================================================ +-- DATA CONTOH – Parsil Tanah +-- ============================================================ +INSERT INTO data_parsil (nama_parsil, status_hak, luas_m2, geojson, pemilik, keterangan) VALUES +( + 'Parsil A-001', + 'Sertifikat Hak Milik', + 1200.00, + '{"type":"Polygon","coordinates":[[[106.8200,-6.2000],[106.8210,-6.2000],[106.8210,-6.2010],[106.8200,-6.2010],[106.8200,-6.2000]]]}', + 'Budi Santoso', + 'Tanah kavling perumahan' +), +( + 'Parsil B-002', + 'Sertifikat Hak Guna Bangunan', + 850.50, + '{"type":"Polygon","coordinates":[[[106.8220,-6.2000],[106.8230,-6.2000],[106.8230,-6.2008],[106.8220,-6.2008],[106.8220,-6.2000]]]}', + 'PT. Maju Bersama', + 'Kavling komersial' +), +( + 'Parsil C-003', + 'Sertifikat Hak Guna Usaha', + 2500.00, + '{"type":"Polygon","coordinates":[[[106.8240,-6.1990],[106.8260,-6.1990],[106.8260,-6.2010],[106.8240,-6.2010],[106.8240,-6.1990]]]}', + 'CV. Tani Makmur', + 'Lahan pertanian/perkebunan' +), +( + 'Parsil D-004', + 'Sertifikat Hak Pakai', + 4000.00, + '{"type":"Polygon","coordinates":[[[106.8270,-6.1985],[106.8295,-6.1985],[106.8295,-6.2005],[106.8270,-6.2005],[106.8270,-6.1985]]]}', + 'Pemerintah Kota', + 'Lahan fasilitas umum' +); + +-- ============================================================ +-- TABEL: data_spbu (Point) +-- Menyimpan data SPBU (Stasiun Pengisian Bahan Bakar Umum) +-- Geometri disimpan dalam format GeoJSON (Point) +-- ============================================================ +CREATE TABLE IF NOT EXISTS data_spbu ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + nama_spbu VARCHAR(150) NOT NULL COMMENT 'Nama SPBU / nama cabang', + telepon VARCHAR(20) DEFAULT NULL COMMENT 'Nomor telepon SPBU', + buka_24jam TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = buka 24 jam (marker hijau), 0 = tidak (marker merah)', + lat DECIMAL(10,7) NOT NULL COMMENT 'Latitude titik SPBU', + lng DECIMAL(10,7) NOT NULL COMMENT 'Longitude titik SPBU', + geojson LONGTEXT NOT NULL COMMENT 'Geometri Point dalam format GeoJSON', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- DATA CONTOH – SPBU di Pontianak +-- ============================================================ +INSERT INTO data_spbu (nama_spbu, telepon, buka_24jam, lat, lng, geojson) VALUES +('SPBU Jl. Ahmad Yani', '0561-123456', 1, -0.0200, 109.3300, '{"type":"Point","coordinates":[109.3300,-0.0200]}'), +('SPBU Jl. Gajah Mada', '0561-654321', 0, -0.0350, 109.3200, '{"type":"Point","coordinates":[109.3200,-0.0350]}'), +('SPBU Jl. Tanjungpura', '0561-789012', 1, -0.0100, 109.3450, '{"type":"Point","coordinates":[109.3450,-0.0100]}'), +('SPBU Jl. Sutan Syahrir','0561-345678', 0, -0.0480, 109.3380, '{"type":"Point","coordinates":[109.3380,-0.0480]}'); diff --git a/Web GIS Layer/index.html b/Web GIS Layer/index.html new file mode 100644 index 0000000..fbd3f0e --- /dev/null +++ b/Web GIS Layer/index.html @@ -0,0 +1,1228 @@ + + + + + + WebGIS – Jalan, Parsil Tanah & SPBU Pontianak + + + + + + + + + + + + + +
+
+
+ WebGIS Pontianak – Jalan · Parsil · SPBU +
+
+ + + +
+
+ + Arahkan kursor ke peta +
+
+ + +
+
+
+
+   + +
+
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/Web GIS Rumah Ibadah/PENJELASAN_SISTEM_SRS_MANUAL.md b/Web GIS Rumah Ibadah/PENJELASAN_SISTEM_SRS_MANUAL.md new file mode 100644 index 0000000..80b4241 --- /dev/null +++ b/Web GIS Rumah Ibadah/PENJELASAN_SISTEM_SRS_MANUAL.md @@ -0,0 +1,312 @@ +# Penjelasan WebGIS — Bahan SRS & Manual Book + +**WebGIS Pontianak — Pengentasan Kemiskinan Berbasis Rumah Ibadah** +*(Poverty Mapping — WebGIS Kota Pontianak)* + +--- + +## 1. Gambaran Umum Sistem + +### Nama & Tujuan +Sistem informasi geografis (WebGIS) untuk **pemetaan keluarga miskin** di **Kota Pontianak** dengan pendekatan **berbasis rumah ibadah** sebagai unit pembina sosial. Rumah ibadah (masjid, gereja, pura, vihara, klenteng, dll.) memiliki **radius cakupan wilayah**; keluarga miskin di dalam radius tersebut **otomatis ditugaskan** sebagai KK binaan rumah ibadah terdekat. + +### Masalah yang Diselesaikan +- Data kemiskinan tersebar dan sulit divisualisasikan secara spasial +- Koordinasi bantuan sosial (PKH, BPNT, bansos, dll.) perlu dilacak per keluarga +- Masyarakat perlu saluran laporan tanpa harus login +- Pemkot (walikota) butuh dashboard agregat untuk pengambilan keputusan +- Pengurus rumah ibadah butuh akses terbatas hanya untuk KK binaannya + +### Cakupan Geografis +6 kecamatan di Kota Pontianak: +- Pontianak Utara, Selatan, Timur, Barat, Tenggara, Kota + +--- + +## 2. Arsitektur Teknis + +### Stack Teknologi + +| Komponen | Teknologi | +|----------|-----------| +| Frontend | HTML5, CSS3, JavaScript (vanilla), Leaflet 1.9.4 | +| Backend | PHP dengan PDO | +| Database | MySQL (`webgis_rumah_ibadah`) | +| Peta | CARTO Voyager tiles + OpenStreetMap Nominatim (reverse geocoding) | +| Ekspor | SheetJS (Excel), jsPDF + autoTable (PDF) | +| Autentikasi | PHP Session | +| Server dev | Laragon (Apache + MySQL) | + +### Struktur File Utama +- `index.html` — seluruh antarmuka & logika frontend +- `index.php` — redirect ke `index.html` +- `config.php` — koneksi DB, utilitas (Haversine, auto-assign, rate limit, upload) +- `api_auth.php` — autentikasi +- `api_rumah_ibadah.php` — CRUD rumah ibadah +- `api_keluarga_miskin.php` — CRUD keluarga miskin +- `api_laporan.php` — laporan masyarakat +- `api_histori_bantuan.php` — histori bantuan +- `api_dashboard.php` — dashboard walikota +- `database.sql` — skema & data contoh +- `uploads/` — foto laporan masyarakat + +### Alur Arsitektur +``` +Browser (index.html) + ↓ fetch JSON +PHP API (api_*.php) + ↓ PDO +MySQL (webgis_rumah_ibadah) +``` + +Peta juga memanggil **Nominatim OpenStreetMap** untuk reverse geocoding (alamat otomatis dari koordinat). + +--- + +## 3. Aktor / Pengguna Sistem + +| Role | Deskripsi | Login | +|------|-----------|-------| +| **Masyarakat (publik)** | Melihat peta & data (read-only), mengirim laporan warga miskin | Tidak perlu login | +| **Admin** | Kelola penuh: rumah ibadah, KK, laporan, bantuan, reset password pengurus | `admin` / `admin123` | +| **Pengurus Rumah Ibadah** | Kelola KK binaan & histori bantuan di wilayahnya | Kode `RI-XXX-9999` / password pengurus | +| **Walikota** | Dashboard agregat, lihat data, ekspor; **tidak bisa** menambah/mengubah/hapus | `walikota` / `admin123` | + +**Format login pengurus:** `RI-[JENIS 3 HURUF]-[4 DIGIT]` +Contoh: `RI-MSJ-4821` (Masjid), `RI-GRJ-7153` (Gereja) + +--- + +## 4. Modul Fungsional (untuk SRS) + +### 4.1 Modul Peta Interaktif (WebGIS) +- Peta interaktif berpusat di Pontianak (lat: -0.0263, lng: 109.3425, zoom 14) +- Layer marker: + - **Rumah ibadah** — warna per jenis (Masjid hijau, Gereja biru, dll.) + - **Keluarga miskin** — warna per tingkat (Sangat Miskin merah, Miskin oranye, Hampir Miskin kuning) + - **Radius cakupan** rumah ibadah (lingkaran saat zoom ke RI) +- **Pick lokasi di peta** — klik peta untuk set koordinat + auto-fill alamat via Nominatim +- Popup detail saat klik marker +- Legenda dinamis per tab aktif + +### 4.2 Modul Rumah Ibadah +**Data:** nama, jenis, alamat, koordinat, radius (0.5–5 km), kecamatan, kode pengurus, password pengurus + +| Aksi | Admin | Pengurus | Walikota | Publik | +|------|-------|----------|----------|--------| +| Lihat daftar & peta | ✓ | ✓ (hanya RI sendiri) | ✓ | ✓ | +| Tambah | ✓ | ✗ | ✗ | ✗ | +| Edit | ✓ | ✓ (RI sendiri) | ✗ | ✗ | +| Hapus | ✓ | ✗ | ✗ | ✗ | +| Reset password pengurus | ✓ | ✗ | ✗ | ✗ | + +**Fitur khusus:** +- Saat tambah RI, sistem generate **kode pengurus** & **password acak** (8 karakter) +- Perubahan/hapus RI memicu **re-assign otomatis** semua KK ke RI terdekat + +### 4.3 Modul Keluarga Miskin +**Data:** NIK, nama KK, tanggal lahir, pendidikan, jumlah anggota, alamat, kecamatan, koordinat, tingkat kemiskinan, status DTKS, riwayat penyakit (JSON), rumah ibadah binaan, jarak ke RI + +**Tingkat kemiskinan:** Sangat Miskin, Miskin, Hampir Miskin +**Status DTKS:** Terdaftar DTKS, Belum DTKS, Sedang Diusulkan +**Riwayat penyakit:** Stunting, TBC, Hipertensi, Diabetes, Jantung, ODGJ, DBD, Gizi Buruk, Lainnya + +**Auto-assign:** Algoritma **Haversine** — KK ditugaskan ke RI terdekat yang radiusnya mencakup lokasi KK. + +**Filter:** kecamatan, tingkat kemiskinan, status DTKS + +**Flag bisnis:** KK dengan status `Belum DTKS` ditandai **Eligible Bansos Tunai Pemkot** + +| Aksi | Admin | Pengurus | Walikota | +|------|-------|----------|----------| +| Lihat | ✓ | ✓ (KK binaan) | ✓ | +| Tambah/Edit | ✓ | ✓ (KK binaan) | ✗ | +| Hapus | ✓ | ✗ | ✗ | +| Ekspor Excel/PDF | ✓ | ✗ | ✓ | + +### 4.4 Modul Laporan Masyarakat +- **Tanpa login** — tombol floating *"Laporkan Warga Miskin"* +- Input: identitas warga (opsional), NIK (opsional, 16 digit), lokasi, alamat, deskripsi, foto (maks 5MB: JPG/PNG/GIF/WebP), identitas pelapor (boleh anonim) +- **Rate limit:** maks **5 laporan per jam per IP** +- Status: **Baru → Diverifikasi / Ditolak** +- Admin dapat: kelola status, isi alasan tolak, konversi ke data KK, buat KK baru dari laporan + +| Aksi | Admin | Walikota | Publik | +|------|-------|----------|--------| +| Kirim laporan | ✓ | ✓ | ✓ | +| Lihat daftar | ✓ | ✓ | ✗ | +| Kelola/verifikasi | ✓ | ✗ | ✗ | + +### 4.5 Modul Histori Bantuan +**Jenis bantuan:** PKH, BPNT/Sembako, Beras Bansos, KIS-PBI, Bansos Tunai Pemkot, Rutilahu, PMT Stunting, CSR Swasta, Lainnya +**Sumber dana:** APBN, APBD, CSR, Lainnya + +- Pencatatan per KK: jenis, nominal (Rp), keterangan, tanggal penyaluran, sumber dana, penanggung jawab +- **Peringatan duplikasi** jika KK sudah punya PKH/BPNT/Beras Bansos +- Total bantuan per KK dihitung otomatis + +### 4.6 Modul Dashboard Walikota +Tampil di sidebar saat walikota login (tab Rumah Ibadah & Keluarga Miskin). Menampilkan: +- Total KK, KK sudah/belum dapat bantuan (tahun berjalan) +- Distribusi per kecamatan, penyakit, DTKS, tingkat kemiskinan +- Bantuan per jenis & sumber dana + total nominal +- Jumlah laporan belum ditangani +- **Drill-down** klikabel → detail tabel + ekspor Excel + +Admin juga dapat mengakses API dashboard yang sama. + +### 4.7 Modul Autentikasi +- Login via username/password (JSON POST) +- Session PHP (`$_SESSION['user']`) +- Endpoint: `check`, `login`, `logout` +- Pengurus login via kode RI, bukan tabel `users` + +--- + +## 5. Model Data + +### Tabel Database (6 tabel) + +1. **rumah_ibadah** — titik lokasi + radius + kredensial pengurus +2. **users** — admin & walikota +3. **keluarga_miskin** — data KK + auto-assign +4. **laporan_masyarakat** — laporan warga +5. **histori_bantuan** — riwayat penyaluran +6. **rate_limit** — pembatasan IP laporan + +### Relasi Utama +- `rumah_ibadah` → `keluarga_miskin` (satu RI membina banyak KK) +- `keluarga_miskin` → `histori_bantuan` (satu KK punya banyak bantuan) +- `users` → `histori_bantuan` (penanggung jawab) +- `laporan_masyarakat` → `keluarga_miskin` (konversi laporan ke KK) + +--- + +## 6. Alur Bisnis Utama (untuk Manual Book) + +### Alur A — Pendaftaran Rumah Ibadah (Admin) +1. Login sebagai admin +2. Tab **Rumah Ibadah** → **Tambah** +3. Isi form / klik peta untuk koordinat +4. Atur radius cakupan (slider 0.5–5 km) +5. Simpan → sistem tampilkan **kode pengurus & password** (diserahkan ke pengurus RI) +6. Semua KK di wilayah otomatis di-assign ulang + +### Alur B — Pengurus Mencatat KK Binaan +1. Login dengan kode `RI-XXX-9999` +2. Tab **Keluarga Miskin** → **Tambah** +3. Isi NIK, data keluarga, pilih penyakit, tentukan lokasi di peta +4. Simpan → KK terhubung ke RI pengurus +5. Klik **Bantuan** pada kartu KK → catat penyaluran bantuan + +### Alur C — Laporan Masyarakat +1. Tanpa login, klik **Laporkan Warga Miskin** +2. Isi lokasi & deskripsi (identitas opsional) +3. Kirim → status **Baru** +4. Admin buka tab **Laporan** → **Kelola** +5. Verifikasi (bisa buat KK dari laporan) atau tolak dengan alasan + +### Alur D — Monitoring Walikota +1. Login `walikota` +2. Lihat dashboard di sidebar (statistik & grafik bar) +3. Klik kartu untuk drill-down detail +4. Tab **Keluarga Miskin** → filter & **Ekspor Excel/PDF** +5. Tab **Laporan** → pantau laporan masuk (read-only untuk kelola) + +--- + +## 7. Antarmuka Pengguna + +| Area | Fungsi | +|------|--------| +| **Topbar** | Brand, navigasi tab (RI / KK / Laporan), info user, login/logout | +| **Peta (kiri)** | Visualisasi spasial, pick lokasi, zoom ke entitas | +| **Sidebar (kanan)** | Daftar data, filter, legenda, statistik, tombol tambah/ekspor | +| **Modal** | Form CRUD, login, laporan, bantuan, konfirmasi hapus, dashboard drill-down | +| **FAB** | Tombol mengambang laporan masyarakat | +| **Toast** | Notifikasi sukses/error | + +**Responsif:** Di layar ≤768px, sidebar menjadi panel bawah setengah layar. + +--- + +## 8. Kebutuhan Non-Fungsional (untuk SRS) + +| Aspek | Spesifikasi | +|-------|-------------| +| **Keamanan** | Password bcrypt, session-based auth, role-based access control, sanitasi input, prepared statements PDO | +| **Ketersediaan data** | UTF-8 (`utf8mb4`), format JSON standar `{success, data, message}` | +| **Performa** | Single-page, data dimuat via AJAX paralel | +| **Batasan upload** | Foto laporan maks 5MB | +| **Rate limiting** | 5 laporan/jam/IP | +| **Kompatibilitas** | Browser modern dengan JavaScript; butuh koneksi internet (CDN peta & library) | +| **Deployment** | PHP 7.4+, MySQL 5.7+, web server Apache/Nginx | + +--- + +## 9. Kerangka Dokumen SRS + +1. **Pendahuluan** — latar belakang, tujuan, ruang lingkup, definisi (DTKS, KK, RI, WebGIS) +2. **Deskripsi Umum** — perspektif produk, fungsi produk, karakteristik pengguna +3. **Kebutuhan Fungsional** — per modul (FR-01 s/d FR-XX) +4. **Kebutuhan Data** — entitas & atribut dari database +5. **Kebutuhan Antarmuka** — layar peta, sidebar, modal, dashboard +6. **Kebutuhan Non-Fungsional** — keamanan, performa, reliabilitas +7. **Aturan Bisnis** — auto-assign Haversine, eligible bansos tunai, duplikasi bantuan, rate limit +8. **Matriks Hak Akses** — tabel role +9. **Integrasi Eksternal** — OpenStreetMap, Nominatim, CDN library +10. **Lampiran** — ERD, diagram alur, mockup UI + +--- + +## 10. Kerangka Manual Book + +**Bab 1 — Pengenalan** +Tentang sistem, siapa penggunanya, persyaratan (browser, Laragon/URL) + +**Bab 2 — Akses & Login** +Login admin, walikota, pengurus (contoh kredensial demo) + +**Bab 3 — Panduan Masyarakat** +Cara melihat peta, mengirim laporan, batas 5 laporan/jam + +**Bab 4 — Panduan Admin** +Kelola rumah ibadah, KK, laporan, verifikasi & konversi ke KK + +**Bab 5 — Panduan Pengurus Rumah Ibadah** +Login kode RI, input KK binaan, pencatatan bantuan + +**Bab 6 — Panduan Walikota** +Dashboard, drill-down, ekspor data, tab laporan + +**Bab 7 — Fitur Peta** +Navigasi, legenda, zoom, pick lokasi + +**Bab 8 — Ekspor Laporan** +Excel & PDF (kolom yang diekspor) + +**Bab 9 — Troubleshooting** +Gagal login, rate limit, KK di luar radius, koneksi database + +--- + +## 11. Data Demo (untuk Testing) + +| Akun | Username | Password | +|------|----------|----------| +| Admin | `admin` | `admin123` | +| Walikota | `walikota` | `admin123` | +| Pengurus Masjid Raya | `RI-MSJ-4821` | `pengurus123` | +| Pengurus Gereja | `RI-GRJ-7153` | `pengurus123` | + +Database seed berisi: 2 rumah ibadah, 5 keluarga miskin, 1 laporan, 2 histori bantuan. + +--- + +## 12. Ringkasan Nilai Sistem + +Sistem ini menggabungkan **GIS**, **manajemen data sosial**, **partisipasi masyarakat**, dan **dashboard kebijakan** dalam satu aplikasi web. Konsep intinya: **rumah ibadah sebagai node pembina** dengan cakupan geografis terukur, sehingga program pengentasan kemiskinan dapat dipantau secara spasial dan berbasis data bantuan per keluarga. + +--- + +*Dokumen ini dibuat sebagai bahan penyusunan SRS (Software Requirements Specification) dan Manual Book untuk WebGIS Pontianak.* diff --git a/Web GIS Rumah Ibadah/api_auth.php b/Web GIS Rumah Ibadah/api_auth.php new file mode 100644 index 0000000..1185400 --- /dev/null +++ b/Web GIS Rumah Ibadah/api_auth.php @@ -0,0 +1,111 @@ +prepare(" + SELECT id, kode_pengurus, password_pengurus, nama, jenis + FROM rumah_ibadah + WHERE kode_pengurus = ? + LIMIT 1 + "); + $stmt->execute([$username]); + $ri = $stmt->fetch(); + + if (!$ri) { + jsonResponse(null, 'Kode pengurus tidak ditemukan.', false, 401); + } + + if (empty($password)) { + jsonResponse(null, 'Password pengurus wajib diisi.', false, 400); + } + + if (!password_verify($password, $ri['password_pengurus'])) { + jsonResponse(null, 'Password pengurus salah.', false, 401); + } + + // Set session sebagai pengurus + $_SESSION['user'] = [ + 'id' => 0, // Pengurus tidak punya ID di tabel users + 'nama' => 'Pengurus ' . $ri['nama'], + 'username' => $ri['kode_pengurus'], + 'role' => 'pengurus', + 'rumah_ibadah_id' => (int)$ri['id'], + ]; + + jsonResponse($_SESSION['user'], 'Login berhasil sebagai pengurus.'); + + } else { + // ── Login admin / walikota via tabel users ── + $stmt = $pdo->prepare(" + SELECT id, nama, username, password, role, rumah_ibadah_id + FROM users + WHERE username = ? + LIMIT 1 + "); + $stmt->execute([$username]); + $user = $stmt->fetch(); + + if (!$user) { + jsonResponse(null, 'Username tidak ditemukan.', false, 401); + } + + if (empty($password)) { + jsonResponse(null, 'Password wajib diisi.', false, 400); + } + + if (!password_verify($password, $user['password'])) { + jsonResponse(null, 'Password salah.', false, 401); + } + + // Set session + $_SESSION['user'] = [ + 'id' => (int)$user['id'], + 'nama' => $user['nama'], + 'username' => $user['username'], + 'role' => $user['role'], + 'rumah_ibadah_id' => $user['rumah_ibadah_id'] ? (int)$user['rumah_ibadah_id'] : null, + ]; + + jsonResponse($_SESSION['user'], 'Login berhasil sebagai ' . $user['role'] . '.'); + } +} + +// ── POST: Logout ───────────────────────────────────────────── +if ($method === 'POST' && $action === 'logout') { + session_destroy(); + jsonResponse(null, 'Berhasil logout.'); +} + +// ── Endpoint tidak valid ───────────────────────────────────── +jsonResponse(null, 'Endpoint tidak valid.', false, 404); diff --git a/Web GIS Rumah Ibadah/api_dashboard.php b/Web GIS Rumah Ibadah/api_dashboard.php new file mode 100644 index 0000000..8c094cd --- /dev/null +++ b/Web GIS Rumah Ibadah/api_dashboard.php @@ -0,0 +1,148 @@ +query(" + SELECT kecamatan, COUNT(*) AS jumlah + FROM keluarga_miskin + GROUP BY kecamatan + ORDER BY FIELD(kecamatan, + 'Pontianak Utara','Pontianak Selatan','Pontianak Timur', + 'Pontianak Barat','Pontianak Tenggara','Pontianak Kota') +"); +$kkPerKecamatan = $stmt->fetchAll(); + +// ── 2. KK yang SUDAH dapat bantuan tahun ini (dari histori_bantuan) ── +$stmt = $pdo->prepare(" + SELECT COUNT(DISTINCT kk_id) AS jumlah + FROM histori_bantuan + WHERE YEAR(tanggal_penyaluran) = ? +"); +$stmt->execute([$tahun]); +$kkSudahBantuan = (int)$stmt->fetch()['jumlah']; + +// ── 3. Total KK ─────────────────────────────────────────────── +$stmt = $pdo->query("SELECT COUNT(*) AS jumlah FROM keluarga_miskin"); +$totalKK = (int)$stmt->fetch()['jumlah']; + +$kkBelumBantuan = $totalKK - $kkSudahBantuan; + +// ── 4. Distribusi status DTKS ──────────────────────────────── +$stmt = $pdo->query(" + SELECT status_dtks, COUNT(*) AS jumlah + FROM keluarga_miskin + GROUP BY status_dtks +"); +$distribusiDtks = $stmt->fetchAll(); + +// ── 5. Distribusi riwayat penyakit ─────────────────────────── +$stmt = $pdo->query("SELECT riwayat_penyakit FROM keluarga_miskin WHERE riwayat_penyakit IS NOT NULL AND riwayat_penyakit != '[]'"); +$penyakitRows = $stmt->fetchAll(); +$penyakitCount = []; +foreach ($penyakitRows as $row) { + $arr = json_decode($row['riwayat_penyakit'], true) ?? []; + foreach ($arr as $p) { + $penyakitCount[$p] = ($penyakitCount[$p] ?? 0) + 1; + } +} +arsort($penyakitCount); +// Format jadi array [{ penyakit, jumlah }] +$distribusiPenyakit = array_map( + fn($k, $v) => ['penyakit' => $k, 'jumlah' => $v], + array_keys($penyakitCount), + array_values($penyakitCount) +); + +// ── 6. Laporan belum ditangani ──────────────────────────────── +$stmt = $pdo->query("SELECT COUNT(*) AS jumlah FROM laporan_masyarakat WHERE status = 'Baru'"); +$laporanBaru = (int)$stmt->fetch()['jumlah']; + +// ── 7. Bantuan tersalurkan tahun ini per sumber dana ───────── +$stmt = $pdo->prepare(" + SELECT sumber_dana, + COUNT(*) AS jumlah_entri, + COUNT(DISTINCT kk_id) AS jumlah_kk, + COALESCE(SUM(nominal), 0) AS total_nominal + FROM histori_bantuan + WHERE YEAR(tanggal_penyaluran) = ? + GROUP BY sumber_dana + ORDER BY total_nominal DESC +"); +$stmt->execute([$tahun]); +$bantuanPerSumber = $stmt->fetchAll(); + +// Format nominal di setiap baris +foreach ($bantuanPerSumber as &$b) { + $b['total_nominal_formatted'] = 'Rp ' . number_format((int)$b['total_nominal'], 0, ',', '.'); +} +unset($b); + +// ── 8. Bantuan tersalurkan tahun ini per jenis ─────────────── +$stmt = $pdo->prepare(" + SELECT jenis_bantuan, + COUNT(*) AS jumlah_entri, + COUNT(DISTINCT kk_id) AS jumlah_kk, + COALESCE(SUM(nominal), 0) AS total_nominal + FROM histori_bantuan + WHERE YEAR(tanggal_penyaluran) = ? + GROUP BY jenis_bantuan + ORDER BY total_nominal DESC +"); +$stmt->execute([$tahun]); +$bantuanPerJenis = $stmt->fetchAll(); + +foreach ($bantuanPerJenis as &$b) { + $b['total_nominal_formatted'] = 'Rp ' . number_format((int)$b['total_nominal'], 0, ',', '.'); +} +unset($b); + +// ── 9. Total entri bantuan + total nominal tahun ini ───────── +$stmt = $pdo->prepare(" + SELECT COUNT(*) AS jumlah, COALESCE(SUM(nominal), 0) AS total_nominal + FROM histori_bantuan WHERE YEAR(tanggal_penyaluran) = ? +"); +$stmt->execute([$tahun]); +$totalRow = $stmt->fetch(); +$totalEntri = (int)$totalRow['jumlah']; +$totalNominal = (int)$totalRow['total_nominal']; +$totalNominalFormatted = 'Rp ' . number_format($totalNominal, 0, ',', '.'); + +// ── 10. Distribusi tingkat kemiskinan ──────────────────────── +$stmt = $pdo->query(" + SELECT tingkat_kemiskinan, COUNT(*) AS jumlah + FROM keluarga_miskin + GROUP BY tingkat_kemiskinan +"); +$distribusiKemiskinan = $stmt->fetchAll(); + +// ── Kirim semua data ────────────────────────────────────────── +jsonResponse([ + 'tahun' => $tahun, + 'total_kk' => $totalKK, + 'kk_per_kecamatan' => $kkPerKecamatan, + 'kk_sudah_bantuan' => $kkSudahBantuan, + 'kk_belum_bantuan' => $kkBelumBantuan, + 'distribusi_dtks' => $distribusiDtks, + 'distribusi_penyakit' => $distribusiPenyakit, + 'distribusi_kemiskinan' => $distribusiKemiskinan, + 'laporan_baru' => $laporanBaru, + 'bantuan_per_sumber' => $bantuanPerSumber, + 'bantuan_per_jenis' => $bantuanPerJenis, + 'total_entri_bantuan' => $totalEntri, + 'total_nominal_bantuan' => $totalNominal, + 'total_nominal_formatted'=> $totalNominalFormatted, +], "Data dashboard tahun $tahun."); diff --git a/Web GIS Rumah Ibadah/api_histori_bantuan.php b/Web GIS Rumah Ibadah/api_histori_bantuan.php new file mode 100644 index 0000000..38494a1 --- /dev/null +++ b/Web GIS Rumah Ibadah/api_histori_bantuan.php @@ -0,0 +1,214 @@ +prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + $totalNominal = 0; + foreach ($rows as &$r) { + $r['nominal_formatted'] = formatRupiah((int)$r['nominal']); + $totalNominal += (int)$r['nominal']; + } + unset($r); + + jsonResponse($rows, count($rows) . ' data bantuan ditemukan.', true, 200, [ + 'total_nominal' => $totalNominal, + 'total_nominal_formatted' => formatRupiah($totalNominal), + ]); + } + + // ── Mode PER-KK (existing) ────────────────────────────── + if (!$kk_id) { + jsonResponse(null, 'Parameter kk_id wajib diisi.', false, 400); + } + + $stmt = $pdo->prepare(" + SELECT hb.*, + u.nama AS nama_penanggung_jawab + FROM histori_bantuan hb + LEFT JOIN users u ON hb.penanggung_jawab = u.id + WHERE hb.kk_id = ? + ORDER BY hb.tanggal_penyaluran DESC + "); + $stmt->execute([$kk_id]); + $rows = $stmt->fetchAll(); + + foreach ($rows as &$r) { + $r['nominal_formatted'] = formatRupiah((int)$r['nominal']); + } + unset($r); + + $stmtTotal = $pdo->prepare("SELECT COALESCE(SUM(nominal), 0) AS total FROM histori_bantuan WHERE kk_id = ?"); + $stmtTotal->execute([$kk_id]); + $totalNominal = (int)$stmtTotal->fetch()['total']; + + jsonResponse($rows, 'Histori bantuan untuk KK ID ' . $kk_id . '.', true, 200, [ + 'total_bantuan' => $totalNominal, + 'total_bantuan_formatted' => formatRupiah($totalNominal), + ]); +} + +// ── POST: Tambah histori bantuan (admin + pengurus) ────────── +if ($method === 'POST') { + $user = requireRole(['admin', 'pengurus']); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + + $kk_id = (int)($body['kk_id'] ?? 0); + $jenis_bantuan = clean($body['jenis_bantuan'] ?? ''); + $nominal = (int)($body['nominal'] ?? 0); + $keterangan = clean($body['keterangan'] ?? ''); + $tanggal_penyaluran = clean($body['tanggal_penyaluran'] ?? ''); + $sumber_dana = clean($body['sumber_dana'] ?? 'APBD'); + + if (!$kk_id || !$jenis_bantuan || !$tanggal_penyaluran) { + jsonResponse(null, 'KK ID, jenis bantuan, dan tanggal penyaluran wajib diisi.', false, 400); + } + + if ($nominal < 0) { + jsonResponse(null, 'Nominal tidak boleh negatif.', false, 400); + } + + // Validasi jenis_bantuan + $validJenis = ['PKH', 'BPNT/Sembako', 'Beras Bansos', 'KIS-PBI', 'Bansos Tunai Pemkot', 'Rutilahu', 'PMT Stunting', 'CSR Swasta', 'Lainnya']; + if (!in_array($jenis_bantuan, $validJenis)) { + jsonResponse(null, 'Jenis bantuan tidak valid.', false, 400); + } + + // Validasi sumber_dana + $validSumber = ['APBN', 'APBD', 'CSR', 'Lainnya']; + if (!in_array($sumber_dana, $validSumber)) { + jsonResponse(null, 'Sumber dana tidak valid.', false, 400); + } + + // Cek apakah KK ada + $stmt = $pdo->prepare("SELECT id, rumah_ibadah_id FROM keluarga_miskin WHERE id = ? LIMIT 1"); + $stmt->execute([$kk_id]); + $kk = $stmt->fetch(); + + if (!$kk) { + jsonResponse(null, 'Data keluarga miskin tidak ditemukan.', false, 404); + } + + // Pengurus hanya bisa tambah bantuan untuk KK binaan miliknya + if ($user['role'] === 'pengurus') { + if ((int)($kk['rumah_ibadah_id'] ?? 0) !== (int)$user['rumah_ibadah_id']) { + jsonResponse(null, 'Anda hanya dapat menambah bantuan untuk KK binaan rumah ibadah Anda.', false, 403); + } + } + + // Cek duplikasi bantuan: PKH, BPNT/Sembako, Beras Bansos + $duplikasiWarning = false; + $bantuanPrioritas = ['PKH', 'BPNT/Sembako', 'Beras Bansos']; + + if (in_array($jenis_bantuan, $bantuanPrioritas)) { + $placeholders = implode(',', array_fill(0, count($bantuanPrioritas), '?')); + $stmt = $pdo->prepare(" + SELECT COUNT(*) AS cnt + FROM histori_bantuan + WHERE kk_id = ? AND jenis_bantuan IN ({$placeholders}) + "); + $stmt->execute(array_merge([$kk_id], $bantuanPrioritas)); + $cnt = (int)$stmt->fetch()['cnt']; + if ($cnt > 0) { + $duplikasiWarning = true; + } + } + + // Tentukan penanggung_jawab + $penanggungJawab = $user['id']; + if ($penanggungJawab === 0) { + $stAdmin = $pdo->query("SELECT id FROM users WHERE role = 'admin' LIMIT 1"); + $admin = $stAdmin->fetch(); + $penanggungJawab = $admin ? (int)$admin['id'] : 1; + } + + // Simpan bantuan + $stmt = $pdo->prepare(" + INSERT INTO histori_bantuan + (kk_id, jenis_bantuan, nominal, keterangan, tanggal_penyaluran, sumber_dana, penanggung_jawab) + VALUES (?, ?, ?, ?, ?, ?, ?) + "); + $stmt->execute([ + $kk_id, $jenis_bantuan, $nominal, + $keterangan ?: null, + $tanggal_penyaluran, $sumber_dana, $penanggungJawab, + ]); + + $newId = (int)$pdo->lastInsertId(); + + $message = 'Histori bantuan berhasil ditambahkan.'; + if ($duplikasiWarning) { + $message .= ' PERHATIAN: KK ini sudah memiliki bantuan serupa (PKH/BPNT/Beras Bansos).'; + } + + jsonResponse( + ['id' => $newId], + $message, + true, + 200, + ['duplikasi_warning' => $duplikasiWarning] + ); +} + +// ── DELETE: Hapus histori bantuan (admin only) ─────────────── +if ($method === 'DELETE') { + $user = requireRole(['admin']); + $id = (int)($_GET['id'] ?? 0); + + if (!$id) { + jsonResponse(null, 'ID histori bantuan wajib diisi.', false, 400); + } + + $stmt = $pdo->prepare("DELETE FROM histori_bantuan WHERE id = ?"); + $stmt->execute([$id]); + + if ($stmt->rowCount() === 0) { + jsonResponse(null, 'Data histori bantuan tidak ditemukan.', false, 404); + } + + jsonResponse(null, 'Histori bantuan berhasil dihapus.'); +} + +// ── Method tidak didukung ──────────────────────────────────── +jsonResponse(null, 'Method tidak didukung.', false, 405); diff --git a/Web GIS Rumah Ibadah/api_keluarga_miskin.php b/Web GIS Rumah Ibadah/api_keluarga_miskin.php new file mode 100644 index 0000000..6713b0b --- /dev/null +++ b/Web GIS Rumah Ibadah/api_keluarga_miskin.php @@ -0,0 +1,285 @@ +prepare(" + SELECT km.*, ri.nama AS nama_ri, ri.jenis AS jenis_ri, ri.kode_pengurus, + COALESCE((SELECT SUM(hb.nominal) FROM histori_bantuan hb WHERE hb.kk_id = km.id), 0) AS total_bantuan + FROM keluarga_miskin km + LEFT JOIN rumah_ibadah ri ON km.rumah_ibadah_id = ri.id + WHERE km.id = ? + "); + $stmt->execute([$id]); + $row = $stmt->fetch(); + + if (!$row) { + jsonResponse(null, 'Data keluarga miskin tidak ditemukan.', false, 404); + } + + // Cek akses pengurus — hanya boleh lihat KK binaan miliknya + $sess = $_SESSION['user'] ?? null; + if ($sess && $sess['role'] === 'pengurus') { + if ((int)($row['rumah_ibadah_id'] ?? 0) !== (int)$sess['rumah_ibadah_id']) { + jsonResponse(null, 'Akses ditolak. KK ini bukan binaan Anda.', false, 403); + } + } + + // Decode riwayat_penyakit JSON + $row['riwayat_penyakit'] = json_decode($row['riwayat_penyakit'] ?? '[]', true); + + // Flag eligible_bansos_tunai: hanya untuk KK dengan status_dtks = 'Belum DTKS' + $row['eligible_bansos_tunai'] = ($row['status_dtks'] === 'Belum DTKS'); + + // Format total_bantuan + $row['total_bantuan_formatted'] = 'Rp ' . number_format((int)$row['total_bantuan'], 0, ',', '.'); + + jsonResponse($row, 'Data keluarga miskin ditemukan.'); + } + + // ── Ambil semua dengan filter ── + $where = []; + $params = []; + + // Filter berdasarkan role pengurus + $sess = $_SESSION['user'] ?? null; + if ($sess && $sess['role'] === 'pengurus' && !empty($sess['rumah_ibadah_id'])) { + $where[] = 'km.rumah_ibadah_id = ?'; + $params[] = (int)$sess['rumah_ibadah_id']; + } + + // Filter kecamatan + if (!empty($_GET['kecamatan'])) { + $where[] = 'km.kecamatan = ?'; + $params[] = clean($_GET['kecamatan']); + } + + // Filter tingkat_kemiskinan + if (!empty($_GET['tingkat_kemiskinan'])) { + $where[] = 'km.tingkat_kemiskinan = ?'; + $params[] = clean($_GET['tingkat_kemiskinan']); + } + + // Filter status_dtks + if (!empty($_GET['status_dtks'])) { + $where[] = 'km.status_dtks = ?'; + $params[] = clean($_GET['status_dtks']); + } + + // Filter rumah_ibadah_id + if (isset($_GET['rumah_ibadah_id']) && $_GET['rumah_ibadah_id'] !== '') { + $where[] = 'km.rumah_ibadah_id = ?'; + $params[] = (int)$_GET['rumah_ibadah_id']; + } + + $sql = " + SELECT km.*, ri.nama AS nama_ri, ri.jenis AS jenis_ri, + COALESCE((SELECT SUM(hb.nominal) FROM histori_bantuan hb WHERE hb.kk_id = km.id), 0) AS total_bantuan + FROM keluarga_miskin km + LEFT JOIN rumah_ibadah ri ON km.rumah_ibadah_id = ri.id + "; + if (!empty($where)) { + $sql .= ' WHERE ' . implode(' AND ', $where); + } + $sql .= ' ORDER BY km.nama_kepala_keluarga ASC'; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + // Proses setiap baris: decode JSON + flag eligible + format total + foreach ($rows as &$r) { + $r['riwayat_penyakit'] = json_decode($r['riwayat_penyakit'] ?? '[]', true); + $r['eligible_bansos_tunai'] = ($r['status_dtks'] === 'Belum DTKS'); + $r['total_bantuan_formatted'] = 'Rp ' . number_format((int)$r['total_bantuan'], 0, ',', '.'); + } + + jsonResponse($rows, 'Daftar keluarga miskin.', true, 200); +} + +// ── POST: Tambah KK (admin + pengurus) ─────────────────────── +if ($method === 'POST') { + $user = requireRole(['admin', 'pengurus']); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + + // Validasi input + $nik = clean($body['nik'] ?? ''); + $nama_kepala_keluarga = clean($body['nama_kepala_keluarga'] ?? ''); + $tanggal_lahir = clean($body['tanggal_lahir'] ?? ''); + $pendidikan = clean($body['pendidikan'] ?? 'Tidak Sekolah'); + $jumlah_anggota = (int)($body['jumlah_anggota'] ?? 1); + $alamat = clean($body['alamat'] ?? ''); + $kecamatan = clean($body['kecamatan'] ?? ''); + $lat = (float)($body['lat'] ?? 0); + $lng = (float)($body['lng'] ?? 0); + $tingkat_kemiskinan = clean($body['tingkat_kemiskinan'] ?? 'Miskin'); + $status_dtks = clean($body['status_dtks'] ?? 'Belum DTKS'); + $riwayat_penyakit = $body['riwayat_penyakit'] ?? []; + + if (!$nik || !$nama_kepala_keluarga || !$alamat || !$kecamatan || !$lat || !$lng) { + jsonResponse(null, 'NIK, nama KK, alamat, kecamatan, dan koordinat wajib diisi.', false, 400); + } + + // Cek NIK unik + $stmt = $pdo->prepare("SELECT id FROM keluarga_miskin WHERE nik = ? LIMIT 1"); + $stmt->execute([$nik]); + if ($stmt->fetch()) { + jsonResponse(null, 'NIK sudah terdaftar dalam sistem.', false, 400); + } + + // Auto-assign ke rumah ibadah terdekat dalam radius + $assign = autoAssign($pdo, $lat, $lng); + $riId = $assign['rumah_ibadah_id']; + $jarakKm = $assign['jarak_km']; + $riwayatJson = json_encode(is_array($riwayat_penyakit) ? $riwayat_penyakit : []); + + // Pengurus: paksa assign ke RI miliknya (jika dalam radius) + if ($user['role'] === 'pengurus') { + $riId = (int)$user['rumah_ibadah_id']; + // Hitung jarak ke RI miliknya + $stRi = $pdo->prepare("SELECT lat, lng FROM rumah_ibadah WHERE id = ?"); + $stRi->execute([$riId]); + $riData = $stRi->fetch(); + $jarakKm = $riData ? haversine($lat, $lng, (float)$riData['lat'], (float)$riData['lng']) : null; + } + + $stmt = $pdo->prepare(" + INSERT INTO keluarga_miskin + (nik, nama_kepala_keluarga, tanggal_lahir, pendidikan, jumlah_anggota, + alamat, kecamatan, lat, lng, tingkat_kemiskinan, status_dtks, + riwayat_penyakit, rumah_ibadah_id, jarak_ke_ibadah) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "); + $stmt->execute([ + $nik, $nama_kepala_keluarga, + $tanggal_lahir ?: null, $pendidikan, $jumlah_anggota, + $alamat, $kecamatan, $lat, $lng, + $tingkat_kemiskinan, $status_dtks, + $riwayatJson, $riId, $jarakKm + ]); + + $newId = (int)$pdo->lastInsertId(); + + $assignMsg = $riId + ? "Auto-assign ke rumah ibadah (jarak: {$jarakKm} km)." + : "Tidak ada rumah ibadah dalam radius cakupan."; + + jsonResponse([ + 'id' => $newId, + 'rumah_ibadah_id' => $riId, + 'jarak_km' => $jarakKm, + ], 'Data keluarga miskin berhasil ditambahkan. ' . $assignMsg); +} + +// ── PUT: Update KK ─────────────────────────────────────────── +if ($method === 'PUT') { + $user = requireRole(['admin', 'pengurus']); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + $id = (int)($body['id'] ?? 0); + + if (!$id) { + jsonResponse(null, 'ID keluarga miskin wajib diisi.', false, 400); + } + + // Cek data ada + $stmt = $pdo->prepare("SELECT rumah_ibadah_id FROM keluarga_miskin WHERE id = ? LIMIT 1"); + $stmt->execute([$id]); + $existing = $stmt->fetch(); + + if (!$existing) { + jsonResponse(null, 'Data keluarga miskin tidak ditemukan.', false, 404); + } + + // Pengurus hanya bisa edit KK dalam RI-nya + if ($user['role'] === 'pengurus') { + if ((int)($existing['rumah_ibadah_id'] ?? 0) !== (int)$user['rumah_ibadah_id']) { + jsonResponse(null, 'Anda hanya dapat mengedit KK binaan rumah ibadah Anda.', false, 403); + } + } + + $nik = clean($body['nik'] ?? ''); + $nama_kepala_keluarga = clean($body['nama_kepala_keluarga'] ?? ''); + $tanggal_lahir = clean($body['tanggal_lahir'] ?? ''); + $pendidikan = clean($body['pendidikan'] ?? 'Tidak Sekolah'); + $jumlah_anggota = (int)($body['jumlah_anggota'] ?? 1); + $alamat = clean($body['alamat'] ?? ''); + $kecamatan = clean($body['kecamatan'] ?? ''); + $lat = (float)($body['lat'] ?? 0); + $lng = (float)($body['lng'] ?? 0); + $tingkat_kemiskinan = clean($body['tingkat_kemiskinan'] ?? 'Miskin'); + $status_dtks = clean($body['status_dtks'] ?? 'Belum DTKS'); + $riwayat_penyakit = $body['riwayat_penyakit'] ?? []; + + if (!$nik || !$nama_kepala_keluarga || !$alamat || !$kecamatan || !$lat || !$lng) { + jsonResponse(null, 'Semua field wajib diisi.', false, 400); + } + + // Cek NIK unik (kecuali milik sendiri) + $stmt = $pdo->prepare("SELECT id FROM keluarga_miskin WHERE nik = ? AND id != ? LIMIT 1"); + $stmt->execute([$nik, $id]); + if ($stmt->fetch()) { + jsonResponse(null, 'NIK sudah digunakan oleh KK lain.', false, 400); + } + + // Re-calculate auto-assign + $assign = autoAssign($pdo, $lat, $lng); + $riId = $assign['rumah_ibadah_id']; + $jarakKm = $assign['jarak_km']; + $riwayatJson = json_encode(is_array($riwayat_penyakit) ? $riwayat_penyakit : []); + + $stmt = $pdo->prepare(" + UPDATE keluarga_miskin SET + nik = ?, nama_kepala_keluarga = ?, tanggal_lahir = ?, pendidikan = ?, + jumlah_anggota = ?, alamat = ?, kecamatan = ?, lat = ?, lng = ?, + tingkat_kemiskinan = ?, status_dtks = ?, riwayat_penyakit = ?, + rumah_ibadah_id = ?, jarak_ke_ibadah = ? + WHERE id = ? + "); + $stmt->execute([ + $nik, $nama_kepala_keluarga, + $tanggal_lahir ?: null, $pendidikan, $jumlah_anggota, + $alamat, $kecamatan, $lat, $lng, + $tingkat_kemiskinan, $status_dtks, $riwayatJson, + $riId, $jarakKm, $id + ]); + + jsonResponse([ + 'rumah_ibadah_id' => $riId, + 'jarak_km' => $jarakKm, + ], 'Data keluarga miskin berhasil diperbarui.'); +} + +// ── DELETE: Hapus KK (admin only) ──────────────────────────── +if ($method === 'DELETE') { + $user = requireRole(['admin']); + $id = (int)($_GET['id'] ?? 0); + + if (!$id) { + jsonResponse(null, 'ID keluarga miskin wajib diisi.', false, 400); + } + + $stmt = $pdo->prepare("DELETE FROM keluarga_miskin WHERE id = ?"); + $stmt->execute([$id]); + + if ($stmt->rowCount() === 0) { + jsonResponse(null, 'Data tidak ditemukan.', false, 404); + } + + jsonResponse(null, 'Data keluarga miskin berhasil dihapus.'); +} + +// ── Method tidak didukung ──────────────────────────────────── +jsonResponse(null, 'Method tidak didukung.', false, 405); diff --git a/Web GIS Rumah Ibadah/api_laporan.php b/Web GIS Rumah Ibadah/api_laporan.php new file mode 100644 index 0000000..976b5e8 --- /dev/null +++ b/Web GIS Rumah Ibadah/api_laporan.php @@ -0,0 +1,166 @@ +prepare(" + INSERT INTO laporan_masyarakat + (nama_terlapor, nik_terlapor, nama_pelapor, no_hp, alamat_terlapor, lat, lng, deskripsi, foto, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'Baru') + "); + $stmt->execute([ + $nama_terlapor ?: null, + $nik_terlapor ?: null, + $nama_pelapor ?: null, + $no_hp ?: null, + $alamat_terlapor, + $lat, $lng, + $deskripsi, + $fotoPath + ]); + + $newId = (int)$pdo->lastInsertId(); + + jsonResponse(['id' => $newId], 'Laporan berhasil dikirim. Terima kasih atas partisipasi Anda.'); +} + +// ── GET: Daftar laporan (admin & walikota) ─────────────────── +if ($method === 'GET') { + $user = requireRole(['admin', 'walikota']); + + $id = isset($_GET['id']) ? (int)$_GET['id'] : null; + + if ($id) { + $stmt = $pdo->prepare(" + SELECT lm.*, + u.nama AS nama_penangan, + kk.nama_kepala_keluarga AS nama_kk_converted + FROM laporan_masyarakat lm + LEFT JOIN users u ON lm.ditangani_oleh = u.id + LEFT JOIN keluarga_miskin kk ON lm.converted_to_kk_id = kk.id + WHERE lm.id = ? + "); + $stmt->execute([$id]); + $row = $stmt->fetch(); + + if (!$row) { + jsonResponse(null, 'Laporan tidak ditemukan.', false, 404); + } + + jsonResponse($row, 'Detail laporan.'); + } + + // Filter status + $where = []; + $params = []; + + if (!empty($_GET['status'])) { + $where[] = 'lm.status = ?'; + $params[] = clean($_GET['status']); + } + + $sql = " + SELECT lm.*, + u.nama AS nama_penangan, + kk.nama_kepala_keluarga AS nama_kk_converted + FROM laporan_masyarakat lm + LEFT JOIN users u ON lm.ditangani_oleh = u.id + LEFT JOIN keluarga_miskin kk ON lm.converted_to_kk_id = kk.id + "; + if (!empty($where)) { + $sql .= ' WHERE ' . implode(' AND ', $where); + } + $sql .= ' ORDER BY lm.created_at DESC'; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + jsonResponse($rows, 'Daftar laporan masyarakat.'); +} + +// ── PUT: Update status laporan (admin only) ────────────────── +if ($method === 'PUT') { + $user = requireRole(['admin']); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + + $id = (int)($body['id'] ?? 0); + $status = clean($body['status'] ?? ''); + $alasan_tolak = clean($body['alasan_tolak'] ?? ''); + $converted_to_kk = isset($body['converted_to_kk_id']) ? (int)$body['converted_to_kk_id'] : null; + + if (!$id || !$status) { + jsonResponse(null, 'ID dan status wajib diisi.', false, 400); + } + + $validStatus = ['Baru', 'Diverifikasi', 'Ditolak']; + if (!in_array($status, $validStatus)) { + jsonResponse(null, 'Status tidak valid. Pilih: Baru, Diverifikasi, atau Ditolak.', false, 400); + } + + // Jika ditolak, wajib ada alasan + if ($status === 'Ditolak' && empty($alasan_tolak)) { + jsonResponse(null, 'Alasan penolakan wajib diisi.', false, 400); + } + + // Update laporan + $stmt = $pdo->prepare(" + UPDATE laporan_masyarakat SET + status = ?, + alasan_tolak = ?, + ditangani_oleh = ?, + converted_to_kk_id = ? + WHERE id = ? + "); + $stmt->execute([ + $status, + $status === 'Ditolak' ? $alasan_tolak : null, + $user['id'], + $status === 'Diverifikasi' ? $converted_to_kk : null, + $id + ]); + + if ($stmt->rowCount() === 0) { + jsonResponse(null, 'Laporan tidak ditemukan.', false, 404); + } + + jsonResponse(null, 'Status laporan berhasil diperbarui ke: ' . $status); +} + +// ── Method tidak didukung ──────────────────────────────────── +jsonResponse(null, 'Method tidak didukung.', false, 405); diff --git a/Web GIS Rumah Ibadah/api_rumah_ibadah.php b/Web GIS Rumah Ibadah/api_rumah_ibadah.php new file mode 100644 index 0000000..1289e65 --- /dev/null +++ b/Web GIS Rumah Ibadah/api_rumah_ibadah.php @@ -0,0 +1,234 @@ +prepare(" + SELECT ri.*, + (SELECT COUNT(*) FROM keluarga_miskin km WHERE km.rumah_ibadah_id = ri.id) AS jumlah_kk_binaan + FROM rumah_ibadah ri + WHERE ri.id = ? + "); + $stmt->execute([$id]); + $row = $stmt->fetch(); + + if (!$row) { + jsonResponse(null, 'Rumah ibadah tidak ditemukan.', false, 404); + } + + // Sembunyikan password_pengurus hash dari response + unset($row['password_pengurus']); + + jsonResponse($row, 'Data rumah ibadah ditemukan.'); + } + + // Ambil semua — pengurus hanya melihat miliknya + $sess = $_SESSION['user'] ?? null; + if ($sess && $sess['role'] === 'pengurus' && !empty($sess['rumah_ibadah_id'])) { + $stmt = $pdo->prepare(" + SELECT ri.*, + (SELECT COUNT(*) FROM keluarga_miskin km WHERE km.rumah_ibadah_id = ri.id) AS jumlah_kk_binaan + FROM rumah_ibadah ri + WHERE ri.id = ? + ORDER BY ri.nama ASC + "); + $stmt->execute([$sess['rumah_ibadah_id']]); + } else { + $stmt = $pdo->query(" + SELECT ri.*, + (SELECT COUNT(*) FROM keluarga_miskin km WHERE km.rumah_ibadah_id = ri.id) AS jumlah_kk_binaan + FROM rumah_ibadah ri + ORDER BY ri.nama ASC + "); + } + + $rows = $stmt->fetchAll(); + + // Sembunyikan password hash, tapi tampilkan kode_pengurus + foreach ($rows as &$r) { + unset($r['password_pengurus']); + } + + jsonResponse($rows, 'Daftar rumah ibadah.', true, 200); +} + +// ── POST: Tambah rumah ibadah (admin only) ─────────────────── +if ($method === 'POST') { + $user = requireRole(['admin']); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + + // Validasi input wajib + $nama = clean($body['nama'] ?? ''); + $jenis = clean($body['jenis'] ?? ''); + $alamat = clean($body['alamat'] ?? ''); + $lat = (float)($body['lat'] ?? 0); + $lng = (float)($body['lng'] ?? 0); + $radius_km = (float)($body['radius_km'] ?? 2.0); + $kecamatan = clean($body['kecamatan'] ?? ''); + + if (!$nama || !$jenis || !$alamat || !$lat || !$lng || !$kecamatan) { + jsonResponse(null, 'Nama, jenis, alamat, koordinat, dan kecamatan wajib diisi.', false, 400); + } + + // Validasi jenis + $validJenis = ['Masjid', 'Gereja', 'Pura', 'Vihara', 'Klenteng', 'Lainnya']; + if (!in_array($jenis, $validJenis)) { + jsonResponse(null, 'Jenis rumah ibadah tidak valid.', false, 400); + } + + // Validasi kecamatan + $validKecamatan = ['Pontianak Utara', 'Pontianak Selatan', 'Pontianak Timur', 'Pontianak Barat', 'Pontianak Tenggara', 'Pontianak Kota']; + if (!in_array($kecamatan, $validKecamatan)) { + jsonResponse(null, 'Kecamatan tidak valid.', false, 400); + } + + // Generate kode pengurus dan password + $kodePengurus = generateKodePengurus($pdo, $jenis); + $passPlain = substr(bin2hex(random_bytes(4)), 0, 8); // 8 karakter acak + $passHash = password_hash($passPlain, PASSWORD_DEFAULT); + + // Simpan ke database + $stmt = $pdo->prepare(" + INSERT INTO rumah_ibadah (nama, jenis, alamat, lat, lng, radius_km, kode_pengurus, password_pengurus, kecamatan) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + "); + $stmt->execute([$nama, $jenis, $alamat, $lat, $lng, $radius_km, $kodePengurus, $passHash, $kecamatan]); + $newId = (int)$pdo->lastInsertId(); + + // Re-assign semua KK setelah RI baru ditambahkan + reassignAllKK($pdo); + + jsonResponse([ + 'id' => $newId, + 'kode_pengurus' => $kodePengurus, + 'password_pengurus' => $passPlain, + ], 'Rumah ibadah berhasil ditambahkan. Kode pengurus: ' . $kodePengurus); +} + +// ── PUT: Update rumah ibadah ───────────────────────────────── +if ($method === 'PUT') { + $user = requireRole(['admin', 'pengurus']); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + $id = (int)($body['id'] ?? 0); + + if (!$id) { + jsonResponse(null, 'ID rumah ibadah wajib diisi.', false, 400); + } + + // Pengurus hanya bisa edit RI miliknya sendiri + if ($user['role'] === 'pengurus' && (int)$user['rumah_ibadah_id'] !== $id) { + jsonResponse(null, 'Anda hanya dapat mengedit rumah ibadah Anda sendiri.', false, 403); + } + + $nama = clean($body['nama'] ?? ''); + $jenis = clean($body['jenis'] ?? ''); + $alamat = clean($body['alamat'] ?? ''); + $lat = (float)($body['lat'] ?? 0); + $lng = (float)($body['lng'] ?? 0); + $radius_km = (float)($body['radius_km'] ?? 2.0); + $kecamatan = clean($body['kecamatan'] ?? ''); + + if (!$nama || !$jenis || !$alamat || !$lat || !$lng || !$kecamatan) { + jsonResponse(null, 'Semua field wajib diisi.', false, 400); + } + + $stmt = $pdo->prepare(" + UPDATE rumah_ibadah + SET nama = ?, jenis = ?, alamat = ?, lat = ?, lng = ?, radius_km = ?, kecamatan = ? + WHERE id = ? + "); + $stmt->execute([$nama, $jenis, $alamat, $lat, $lng, $radius_km, $kecamatan, $id]); + + // Re-assign semua KK setelah radius/posisi berubah + reassignAllKK($pdo); + + jsonResponse(null, 'Rumah ibadah berhasil diperbarui.'); +} + +// ── DELETE: Hapus rumah ibadah (admin only) ────────────────── +if ($method === 'DELETE') { + $user = requireRole(['admin']); + $id = (int)($_GET['id'] ?? 0); + + if (!$id) { + jsonResponse(null, 'ID rumah ibadah wajib diisi.', false, 400); + } + + $stmt = $pdo->prepare("DELETE FROM rumah_ibadah WHERE id = ?"); + $stmt->execute([$id]); + + if ($stmt->rowCount() === 0) { + jsonResponse(null, 'Rumah ibadah tidak ditemukan.', false, 404); + } + + // Re-assign semua KK setelah RI dihapus + reassignAllKK($pdo); + + jsonResponse(null, 'Rumah ibadah berhasil dihapus.'); +} + +// ── Helper: Re-assign semua KK ke RI terdekat ──────────────── +function reassignAllKK(PDO $pdo): void { + $rows = $pdo->query("SELECT id, lat, lng FROM keluarga_miskin")->fetchAll(); + + foreach ($rows as $kk) { + $assign = autoAssign($pdo, (float)$kk['lat'], (float)$kk['lng']); + + if ($assign['rumah_ibadah_id'] !== null) { + $stmt = $pdo->prepare("UPDATE keluarga_miskin SET rumah_ibadah_id = ?, jarak_ke_ibadah = ? WHERE id = ?"); + $stmt->execute([$assign['rumah_ibadah_id'], $assign['jarak_km'], $kk['id']]); + } else { + $stmt = $pdo->prepare("UPDATE keluarga_miskin SET rumah_ibadah_id = NULL, jarak_ke_ibadah = NULL WHERE id = ?"); + $stmt->execute([$kk['id']]); + } + } +} + +// ── PATCH: Reset password pengurus (admin only) ────────────── +if ($method === 'PATCH') { + $user = requireRole(['admin']); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + $id = (int)($body['id'] ?? 0); + + if (!$id) { + jsonResponse(null, 'ID rumah ibadah wajib diisi.', false, 400); + } + + // Pastikan RI ada + $stmt = $pdo->prepare("SELECT id, nama, kode_pengurus FROM rumah_ibadah WHERE id = ? LIMIT 1"); + $stmt->execute([$id]); + $ri = $stmt->fetch(); + + if (!$ri) { + jsonResponse(null, 'Rumah ibadah tidak ditemukan.', false, 404); + } + + // Generate password baru + $passPlain = substr(bin2hex(random_bytes(4)), 0, 8); // 8 karakter acak + $passHash = password_hash($passPlain, PASSWORD_DEFAULT); + + $stmt = $pdo->prepare("UPDATE rumah_ibadah SET password_pengurus = ? WHERE id = ?"); + $stmt->execute([$passHash, $id]); + + jsonResponse([ + 'id' => $id, + 'kode_pengurus' => $ri['kode_pengurus'], + 'password_pengurus' => $passPlain, + ], 'Password pengurus berhasil direset untuk: ' . $ri['nama']); +} + +// ── Method tidak didukung ──────────────────────────────────── +jsonResponse(null, 'Method tidak didukung.', false, 405); diff --git a/Web GIS Rumah Ibadah/config.php b/Web GIS Rumah Ibadah/config.php new file mode 100644 index 0000000..66eadaa --- /dev/null +++ b/Web GIS Rumah Ibadah/config.php @@ -0,0 +1,267 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ] + ); +} catch (PDOException $e) { + http_response_code(500); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'success' => false, + 'data' => null, + 'message' => 'Koneksi database gagal: ' . $e->getMessage() + ]); + exit; +} + +// ── CORS Headers ───────────────────────────────────────────── +header('Access-Control-Allow-Origin: *'); +header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); +header('Access-Control-Allow-Headers: Content-Type'); + +// Handle preflight request +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(200); + exit; +} + +// ============================================================ +// FUNGSI UTILITAS +// ============================================================ + +/** + * Kirim respon JSON standar lalu hentikan eksekusi. + * Format: {"success": true/false, "data": ..., "message": "..."} + */ +function jsonResponse($data = null, string $message = '', bool $success = true, int $httpCode = 200, array $extra = []): void { + http_response_code($httpCode); + header('Content-Type: application/json; charset=utf-8'); + $response = [ + 'success' => $success, + 'data' => $data, + 'message' => $message + ]; + // Gabungkan field tambahan (misal: duplikasi_warning) + if (!empty($extra)) { + $response = array_merge($response, $extra); + } + echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +/** + * Bersihkan input string dari tag HTML. + */ +function clean(string $value): string { + return htmlspecialchars(strip_tags(trim($value)), ENT_QUOTES, 'UTF-8'); +} + +/** + * Hitung jarak Haversine antara dua koordinat dalam kilometer. + */ +function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float { + $R = 6371; // Radius bumi dalam km + $dLat = deg2rad($lat2 - $lat1); + $dLng = deg2rad($lng2 - $lng1); + $a = sin($dLat / 2) * sin($dLat / 2) + + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * + sin($dLng / 2) * sin($dLng / 2); + $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); + return round($R * $c, 4); +} + +/** + * Auto-assign: cari rumah ibadah terdekat yang radius-nya mencakup lokasi KK. + * Mengembalikan ['rumah_ibadah_id' => int|null, 'jarak_km' => float|null] + */ +function autoAssign(PDO $pdo, float $lat, float $lng): array { + $result = ['rumah_ibadah_id' => null, 'jarak_km' => null]; + + $stmt = $pdo->query("SELECT id, lat, lng, radius_km FROM rumah_ibadah"); + $rows = $stmt->fetchAll(); + + $bestDist = PHP_FLOAT_MAX; + $bestId = null; + + foreach ($rows as $row) { + $dist = haversine($lat, $lng, (float)$row['lat'], (float)$row['lng']); + // Hanya assign jika KK berada dalam radius RI dan merupakan yang terdekat + if ($dist <= (float)$row['radius_km'] && $dist < $bestDist) { + $bestDist = $dist; + $bestId = (int)$row['id']; + } + } + + if ($bestId !== null) { + $result['rumah_ibadah_id'] = $bestId; + $result['jarak_km'] = $bestDist; + } + + return $result; +} + +/** + * Cek apakah user sudah login. Jika belum, kirim 401. + * Mengembalikan data session user. + */ +function requireLogin(): array { + if (empty($_SESSION['user'])) { + jsonResponse(null, 'Tidak diizinkan. Silakan login terlebih dahulu.', false, 401); + } + return $_SESSION['user']; +} + +/** + * Cek apakah role user termasuk dalam daftar yang diizinkan. + * Jika tidak, kirim 403. + */ +function requireRole(array $allowedRoles): array { + $user = requireLogin(); + if (!in_array($user['role'], $allowedRoles)) { + jsonResponse(null, 'Akses ditolak. Role Anda tidak memiliki izin.', false, 403); + } + return $user; +} + +/** + * Blokir aksi tulis (POST/PUT/DELETE) untuk role walikota. + * Walikota hanya boleh GET. + */ +function blockWalikotaWrite(): void { + if (!empty($_SESSION['user']) && $_SESSION['user']['role'] === 'walikota') { + $method = $_SERVER['REQUEST_METHOD']; + if (in_array($method, ['POST', 'PUT', 'DELETE'])) { + jsonResponse(null, 'Walikota hanya memiliki akses baca (read-only).', false, 403); + } + } +} + +/** + * Generate kode pengurus dengan format: RI-[JENIS 3 HURUF]-[RANDOM 4 DIGIT] + * Contoh: RI-MSJ-4821, RI-GRJ-7153 + */ +function generateKodePengurus(PDO $pdo, string $jenis): string { + // Mapping jenis ke 3 huruf + $map = [ + 'Masjid' => 'MSJ', + 'Gereja' => 'GRJ', + 'Pura' => 'PRA', + 'Vihara' => 'VHR', + 'Klenteng' => 'KLT', + 'Lainnya' => 'LNY', + ]; + $prefix = $map[$jenis] ?? 'LNY'; + + // Coba generate kode unik (maks 100 percobaan) + for ($i = 0; $i < 100; $i++) { + $angka = str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT); + $kode = "RI-{$prefix}-{$angka}"; + + $stmt = $pdo->prepare("SELECT id FROM rumah_ibadah WHERE kode_pengurus = ? LIMIT 1"); + $stmt->execute([$kode]); + if ($stmt->rowCount() === 0) { + return $kode; + } + } + // Fallback: tambahkan timestamp + return "RI-{$prefix}-" . substr(time(), -4); +} + +/** + * Cek rate limit untuk IP tertentu. Maks 5 submit per jam. + * Mengembalikan true jika masih diizinkan, false jika melebihi batas. + */ +function checkRateLimit(PDO $pdo, string $ip): bool { + // Hapus record yang lebih dari 1 jam + $pdo->prepare("DELETE FROM rate_limit WHERE created_at < NOW() - INTERVAL 1 HOUR")->execute(); + + // Hitung jumlah submit dalam 1 jam terakhir + $stmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM rate_limit WHERE ip_address = ? AND created_at >= NOW() - INTERVAL 1 HOUR"); + $stmt->execute([$ip]); + $count = (int)$stmt->fetch()['cnt']; + + if ($count >= 5) { + return false; // Sudah melebihi batas + } + + // Catat submit baru + $stmt = $pdo->prepare("INSERT INTO rate_limit (ip_address) VALUES (?)"); + $stmt->execute([$ip]); + + return true; +} + +/** + * Handle upload file foto. Mengembalikan path relatif file atau null. + */ +function handleUpload(string $fieldName): ?string { + if (!isset($_FILES[$fieldName]) || $_FILES[$fieldName]['error'] !== UPLOAD_ERR_OK) { + return null; + } + + $file = $_FILES[$fieldName]; + $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + $maxSize = 5 * 1024 * 1024; // 5MB + + if (!in_array($file['type'], $allowedTypes)) { + jsonResponse(null, 'Tipe file tidak diizinkan. Gunakan JPG, PNG, GIF, atau WebP.', false, 400); + } + + if ($file['size'] > $maxSize) { + jsonResponse(null, 'Ukuran file melebihi batas 5MB.', false, 400); + } + + // Buat direktori upload jika belum ada + if (!is_dir(UPLOAD_DIR)) { + mkdir(UPLOAD_DIR, 0755, true); + } + + // Generate nama file unik + $ext = pathinfo($file['name'], PATHINFO_EXTENSION); + $filename = 'laporan_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext; + $dest = UPLOAD_DIR . $filename; + + if (!move_uploaded_file($file['tmp_name'], $dest)) { + jsonResponse(null, 'Gagal menyimpan file foto.', false, 500); + } + + return 'uploads/' . $filename; +} + +/** + * Dapatkan IP address client (mendukung proxy). + */ +function getClientIP(): string { + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; + } + return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; +} diff --git a/Web GIS Rumah Ibadah/database.sql b/Web GIS Rumah Ibadah/database.sql new file mode 100644 index 0000000..df8e1c6 --- /dev/null +++ b/Web GIS Rumah Ibadah/database.sql @@ -0,0 +1,289 @@ +-- ============================================================ +-- DATABASE: webgis_rumah_ibadah +-- WebGIS Pontianak — Pengentasan Kemiskinan Berbasis Rumah Ibadah +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS webgis_rumah_ibadah + CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE webgis_rumah_ibadah; + +-- Hapus tabel lama (urutan sesuai dependensi FK) +DROP TABLE IF EXISTS histori_bantuan; +DROP TABLE IF EXISTS laporan_masyarakat; +DROP TABLE IF EXISTS keluarga_miskin; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS rumah_ibadah; +DROP TABLE IF EXISTS rate_limit; + +-- ============================================================ +-- TABEL: rumah_ibadah +-- Menyimpan data titik lokasi rumah ibadah beserta radius cakupan +-- ============================================================ +CREATE TABLE rumah_ibadah ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(200) NOT NULL COMMENT 'Nama rumah ibadah', + jenis ENUM('Masjid','Gereja','Pura','Vihara','Klenteng','Lainnya') + NOT NULL COMMENT 'Jenis rumah ibadah', + alamat TEXT NOT NULL COMMENT 'Alamat lengkap', + lat DECIMAL(10,7) NOT NULL COMMENT 'Latitude', + lng DECIMAL(10,7) NOT NULL COMMENT 'Longitude', + radius_km FLOAT NOT NULL DEFAULT 2.0 COMMENT 'Radius cakupan (km)', + kode_pengurus VARCHAR(20) NULL UNIQUE COMMENT 'Kode unik login pengurus, format RI-XXX-9999', + password_pengurus VARCHAR(255) NULL COMMENT 'Password pengurus (hashed)', + kecamatan ENUM('Pontianak Utara','Pontianak Selatan','Pontianak Timur', + 'Pontianak Barat','Pontianak Tenggara','Pontianak Kota') + NOT NULL COMMENT 'Kecamatan lokasi', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- TABEL: users +-- Manajemen pengguna dengan 3 role: admin, pengurus, walikota +-- ============================================================ +CREATE TABLE users ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL COMMENT 'Nama lengkap pengguna', + username VARCHAR(100) NOT NULL UNIQUE COMMENT 'Username login', + password VARCHAR(255) NOT NULL COMMENT 'Password ter-hash (bcrypt)', + role ENUM('admin','pengurus','walikota') + NOT NULL DEFAULT 'pengurus' COMMENT 'Role pengguna', + rumah_ibadah_id INT DEFAULT NULL COMMENT 'FK ke rumah_ibadah (hanya untuk pengurus)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- TABEL: keluarga_miskin +-- Data keluarga miskin dengan auto-assign ke rumah ibadah terdekat +-- ============================================================ +CREATE TABLE keluarga_miskin ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + nik VARCHAR(20) NOT NULL UNIQUE COMMENT 'NIK Kepala Keluarga', + nama_kepala_keluarga VARCHAR(200) NOT NULL COMMENT 'Nama Kepala Keluarga', + tanggal_lahir DATE NULL COMMENT 'Tanggal lahir KK', + pendidikan ENUM('Tidak Sekolah','SD','SMP','SMA','D3','S1','S2/S3') + NOT NULL DEFAULT 'Tidak Sekolah', + jumlah_anggota INT NOT NULL DEFAULT 1 COMMENT 'Jumlah anggota keluarga', + alamat TEXT NOT NULL COMMENT 'Alamat lengkap', + kecamatan ENUM('Pontianak Utara','Pontianak Selatan','Pontianak Timur', + 'Pontianak Barat','Pontianak Tenggara','Pontianak Kota') + NOT NULL, + lat DECIMAL(10,7) NOT NULL, + lng DECIMAL(10,7) NOT NULL, + tingkat_kemiskinan ENUM('Sangat Miskin','Miskin','Hampir Miskin') + NOT NULL DEFAULT 'Miskin', + status_dtks ENUM('Terdaftar DTKS','Belum DTKS','Sedang Diusulkan') + NOT NULL DEFAULT 'Belum DTKS', + riwayat_penyakit JSON NULL COMMENT 'Array penyakit: Stunting, TBC, Hipertensi, dll', + rumah_ibadah_id INT DEFAULT NULL COMMENT 'Auto-assign ke RI terdekat', + jarak_ke_ibadah FLOAT DEFAULT NULL COMMENT 'Jarak ke RI terdekat (km)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- TABEL: laporan_masyarakat +-- Laporan warga tentang kemiskinan (bisa anonim, tanpa login) +-- ============================================================ +CREATE TABLE laporan_masyarakat ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + nama_terlapor VARCHAR(150) NULL COMMENT 'Nama warga yang dilaporkan (boleh null)', + nik_terlapor VARCHAR(16) NULL COMMENT 'NIK warga yang dilaporkan (boleh null)', + nama_pelapor VARCHAR(150) NULL COMMENT 'Boleh anonim (null)', + no_hp VARCHAR(20) NULL, + alamat_terlapor TEXT NOT NULL COMMENT 'Alamat yang dilaporkan', + lat DECIMAL(10,7) NOT NULL, + lng DECIMAL(10,7) NOT NULL, + deskripsi TEXT NOT NULL COMMENT 'Deskripsi laporan', + foto VARCHAR(255) NULL COMMENT 'Path file foto', + status ENUM('Baru','Diverifikasi','Ditolak') + NOT NULL DEFAULT 'Baru', + alasan_tolak TEXT NULL COMMENT 'Alasan jika ditolak', + ditangani_oleh INT NULL COMMENT 'FK ke users.id', + converted_to_kk_id INT NULL COMMENT 'FK ke keluarga_miskin.id jika dikonversi', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (ditangani_oleh) REFERENCES users(id) ON DELETE SET NULL, + FOREIGN KEY (converted_to_kk_id) REFERENCES keluarga_miskin(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- TABEL: histori_bantuan +-- Riwayat bantuan yang diterima keluarga miskin +-- ============================================================ +CREATE TABLE histori_bantuan ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + kk_id INT NOT NULL COMMENT 'FK ke keluarga_miskin', + jenis_bantuan ENUM('PKH','BPNT/Sembako','Beras Bansos','KIS-PBI', + 'Bansos Tunai Pemkot','Rutilahu','PMT Stunting', + 'CSR Swasta','Lainnya') + NOT NULL, + nominal BIGINT NOT NULL DEFAULT 0 COMMENT 'Nominal dalam rupiah (0 jika non-uang)', + keterangan TEXT NULL COMMENT 'Catatan bebas: 10 kg beras, 3 karung, dll', + tanggal_penyaluran DATE NOT NULL, + sumber_dana ENUM('APBN','APBD','CSR','Lainnya') + NOT NULL DEFAULT 'APBD', + penanggung_jawab INT NOT NULL COMMENT 'FK ke users.id', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (kk_id) REFERENCES keluarga_miskin(id) ON DELETE CASCADE, + FOREIGN KEY (penanggung_jawab) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- TABEL: rate_limit +-- Pembatasan submit laporan masyarakat per IP (maks 5/jam) +-- ============================================================ +CREATE TABLE rate_limit ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + ip_address VARCHAR(45) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_ip_time (ip_address, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================================ +-- DATA SEED: Rumah Ibadah (2 data) +-- ============================================================ +INSERT INTO rumah_ibadah (nama, jenis, alamat, lat, lng, radius_km, kode_pengurus, password_pengurus, kecamatan) VALUES +( + 'Masjid Raya Mujahidin', + 'Masjid', + 'Jl. Ahmad Yani No. 1, Pontianak Tenggara', + -0.0263330, 109.3425200, + 2.5, + 'RI-MSJ-4821', + -- password: pengurus123 + '$2y$10$EQKGMwtGqPsn3kxOGkUf3eh3cnXOCEYqVNfCKDVk.Q7WYTCKvi5iG', + 'Pontianak Tenggara' +), +( + 'Gereja Katedral Santo Yosef', + 'Gereja', + 'Jl. Diponegoro No. 120, Pontianak Kota', + -0.0380000, 109.3422000, + 2.0, + 'RI-GRJ-7153', + -- password: pengurus123 + '$2y$10$EQKGMwtGqPsn3kxOGkUf3eh3cnXOCEYqVNfCKDVk.Q7WYTCKvi5iG', + 'Pontianak Kota' +); + +-- ============================================================ +-- DATA SEED: Users (1 admin, 1 walikota) +-- Password semua: admin123 +-- ============================================================ +INSERT INTO users (nama, username, password, role, rumah_ibadah_id) VALUES +( + 'Administrator Sistem', + 'admin', + '$2y$10$AIO3pPEflVaouN/G.rvgWeZmSjr0Nnjsd2PTna4lSHEoDTWr9KQpi', + 'admin', + NULL +), +( + 'Walikota Pontianak', + 'walikota', + '$2y$10$AIO3pPEflVaouN/G.rvgWeZmSjr0Nnjsd2PTna4lSHEoDTWr9KQpi', + 'walikota', + NULL +); + +-- ============================================================ +-- DATA SEED: Keluarga Miskin (5 data) +-- Auto-assign rumah_ibadah_id dilakukan oleh aplikasi saat disimpan +-- ============================================================ +INSERT INTO keluarga_miskin (nik, nama_kepala_keluarga, tanggal_lahir, pendidikan, jumlah_anggota, alamat, kecamatan, lat, lng, tingkat_kemiskinan, status_dtks, riwayat_penyakit, rumah_ibadah_id, jarak_ke_ibadah) VALUES +( + '6171010101010001', + 'Budi Santoso', + '1978-03-15', + 'SD', + 4, + 'Gg. Mawar No. 3, Benua Melayu Darat', + 'Pontianak Tenggara', + -0.0283000, 109.3440000, + 'Sangat Miskin', + 'Terdaftar DTKS', + '["Stunting","Gizi Buruk"]', + 1, + 0.2500 +), +( + '6171010101010002', + 'Siti Rahayu', + '1985-07-22', + 'SMP', + 6, + 'Jl. Merdeka Gg. 5 No. 7, Mariana', + 'Pontianak Kota', + -0.0370000, 109.3430000, + 'Miskin', + 'Terdaftar DTKS', + '["TBC","Hipertensi"]', + 2, + 0.1200 +), +( + '6171010101010003', + 'Wayan Dharma', + '1972-11-08', + 'Tidak Sekolah', + 3, + 'Gg. Bali No. 2, Bansir Laut', + 'Pontianak Tenggara', + -0.0290000, 109.3410000, + 'Sangat Miskin', + 'Belum DTKS', + '["Diabetes","Hipertensi","ODGJ"]', + 1, + 0.3400 +), +( + '6171010101010004', + 'Ahmad Fauzan', + '1990-01-30', + 'SMA', + 5, + 'Jl. Tanjungpura Gg. 12, Parit Tokaya', + 'Pontianak Selatan', + -0.0310000, 109.3350000, + 'Miskin', + 'Sedang Diusulkan', + '["Stunting"]', + NULL, + NULL +), +( + '6171010101010005', + 'Ruslan Effendi', + '1968-09-05', + 'SD', + 7, + 'Jl. Sutan Syahrir Gg. Melati, Batu Layang', + 'Pontianak Utara', + -0.0200000, 109.3460000, + 'Hampir Miskin', + 'Terdaftar DTKS', + '["Jantung","DBD"]', + 1, + 0.7800 +); + +-- ============================================================ +-- DATA SEED: Laporan Masyarakat (1 contoh) +-- ============================================================ +INSERT INTO laporan_masyarakat (nama_pelapor, no_hp, alamat_terlapor, lat, lng, deskripsi, status) VALUES +( + 'Pak RT 03', + '081234567890', + 'Gg. Flamboyan No. 5, Pontianak Tenggara', + -0.0275000, 109.3435000, + 'Terdapat keluarga dengan 5 anak yang sangat membutuhkan bantuan. Rumah tidak layak huni dan anak-anak putus sekolah.', + 'Baru' +); + +-- ============================================================ +-- DATA SEED: Histori Bantuan (2 contoh) +-- ============================================================ +INSERT INTO histori_bantuan (kk_id, jenis_bantuan, nominal, keterangan, tanggal_penyaluran, sumber_dana, penanggung_jawab) VALUES +(1, 'PKH', 3000000, 'Bantuan tahap I tahun 2026', '2026-01-15', 'APBN', 1), +(2, 'BPNT/Sembako', 0, '10 kg beras + minyak goreng', '2026-02-20', 'APBN', 1); diff --git a/Web GIS Rumah Ibadah/index.html b/Web GIS Rumah Ibadah/index.html new file mode 100644 index 0000000..0e65e62 --- /dev/null +++ b/Web GIS Rumah Ibadah/index.html @@ -0,0 +1,2884 @@ + + + + + + WebGIS Pontianak — Pengentasan Kemiskinan Berbasis Rumah Ibadah + + + + + + + + + + + + +
+
+ +
+
Poverty Mapping
+
WebGIS Kota Pontianak
+
+
+ +
+ () + +
+
+ + +
+
+
+
+   + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + \ No newline at end of file diff --git a/Web GIS Rumah Ibadah/index.php b/Web GIS Rumah Ibadah/index.php new file mode 100644 index 0000000..a985c22 --- /dev/null +++ b/Web GIS Rumah Ibadah/index.php @@ -0,0 +1,3 @@ + + + + + + + WebGIS Portal — Kota Pontianak + + + + + + + + + +
+ +
+ + + + + +
+
+
+ + Sistem Informasi Geografis +
+

+ Portal WebGIS
Kota Pontianak +

+

+ Pusat akses seluruh project Sistem Informasi Geografis berbasis web untuk pengelolaan data spasial wilayah Kota Pontianak. +

+
+
+
2
+
Project Aktif
+
+
+
6+
+
Layer Data
+
+
+
PHP
+
+
+
+
GIS
+
LeafletJS
+
+
+
+
+ + +
+
+
+ +

Semua WebGIS Project

+
+ +
+ + +
+ 01 +
+ +
+ +
+ + Jalan + + + Parsil + + + SPBU + +
+ +

WebGIS Layer — Jalan, Parsil & SPBU

+

+ Sistem informasi geografis untuk manajemen data jalan (nasional, provinsi, kabupaten), parsil tanah beserta status kepemilikan, dan sebaran SPBU di Kota Pontianak. +

+ +
+
+ + Gambar polyline & polygon langsung di peta +
+
+ + CRUD data jalan, parsil tanah & SPBU +
+
+ + Kalkulasi panjang & luas otomatis (LeafletJS) +
+
+ + Layer switcher: Street & Satellite view +
+
+ + +
+ + +
+ 02 +
+ +
+ +
+ + Kemiskinan + + + Rumah Ibadah + + + Bantuan + +
+ +

WebGIS Poverty Mapping & Rumah Ibadah

+

+ Pemetaan keluarga miskin, histori penyaluran bantuan sosial, dan lokasi rumah ibadah berbasis peta interaktif. Dilengkapi dashboard analitik dan laporan spasial. +

+ +
+
+ + Pemetaan & manajemen data keluarga miskin +
+
+ + Histori & tracking bantuan sosial +
+
+ + Lokasi rumah ibadah (masjid, gereja, dll.) +
+
+ + Dashboard statistik & laporan lengkap +
+
+ + +
+ +
+ + +
+ +

Tech Stack yang Digunakan

+
+
HTML5 & CSS3
+
JavaScript
+
PHP
+
MySQL / MariaDB
+
Leaflet.js 1.9.4
+
Leaflet.Draw
+
Leaflet.GeometryUtil
+
OpenStreetMap
+
Esri Satellite
+
Font Awesome 6
+
Laragon
+
GeoJSON Format
+
+
+ +
+
+ + + + + + + \ No newline at end of file