From 61f4fb2e71d98b832fe6895a81efcedc77b82b97 Mon Sep 17 00:00:00 2001 From: mhmmadbayuu Date: Wed, 10 Jun 2026 17:28:32 +0700 Subject: [PATCH] add files and folders --- index.html | 293 ++ manual.html | 199 + webgis-kemiskinan/Dockerfile | 12 + webgis-kemiskinan/README.md | 185 + webgis-kemiskinan/admin/_layout.php | 121 + webgis-kemiskinan/admin/bantuan.php | 155 + webgis-kemiskinan/admin/belum_dibantu.php | 71 + webgis-kemiskinan/admin/forgot.php | 92 + webgis-kemiskinan/admin/histori_bantuan.php | 48 + webgis-kemiskinan/admin/index.php | 128 + webgis-kemiskinan/admin/init_admin.php | 32 + webgis-kemiskinan/admin/keluarga.php | 159 + webgis-kemiskinan/admin/laporan.php | 167 + webgis-kemiskinan/admin/log.php | 33 + webgis-kemiskinan/admin/login.php | 89 + webgis-kemiskinan/admin/logout.php | 6 + webgis-kemiskinan/admin/penduduk.php | 297 ++ webgis-kemiskinan/admin/rumah_ibadah.php | 141 + webgis-kemiskinan/admin/users.php | 163 + webgis-kemiskinan/index.html | 1849 +++++++ webgis-kemiskinan/php/api/auth.php | 54 + webgis-kemiskinan/php/api/bantuan.php | 239 + webgis-kemiskinan/php/api/keluarga.php | 183 + webgis-kemiskinan/php/api/laporan.php | 255 + webgis-kemiskinan/php/api/penduduk.php | 328 ++ webgis-kemiskinan/php/api/pesan.php | 127 + webgis-kemiskinan/php/api/rumah_ibadah.php | 111 + webgis-kemiskinan/php/koneksi.php | 177 + webgis-kemiskinan/php/middleware/auth.php | 125 + webgis-kemiskinan/sql/schema.sql | 384 ++ webgis-kemiskinan/update_db.php | 11 + webgis-kemiskinan/uploads/bukti/.gitkeep | 0 .../uploads/foto_laporan/.gitkeep | 0 .../uploads/foto_profil/.gitkeep | 0 webgis-spbu/.codex | 0 webgis-spbu/Dockerfile | 10 + webgis-spbu/README.md | 32 + webgis-spbu/admin/_auth.php | 18 + webgis-spbu/admin/_layout.php | 39 + webgis-spbu/admin/index.php | 53 + webgis-spbu/admin/init_admin.php | 34 + webgis-spbu/admin/login.php | 62 + webgis-spbu/admin/logout.php | 6 + webgis-spbu/admin/penduduk_miskin.php | 286 ++ webgis-spbu/admin/rumah_ibadah.php | 166 + webgis-spbu/css/style.css | 779 +++ webgis-spbu/fetch_geojson.js | 61 + webgis-spbu/fetch_osm.js | 29 + webgis-spbu/index.html | 359 ++ webgis-spbu/js/app.js | 2874 +++++++++++ webgis-spbu/js/pontianak_kecamatan.geojson | 4341 +++++++++++++++++ webgis-spbu/migrate_now.php | 10 + webgis-spbu/osm_pontianak.json | 9 + webgis-spbu/osm_to_geojson_draft.js | 40 + webgis-spbu/php/create_jalan.php | 49 + webgis-spbu/php/create_parsil.php | 65 + webgis-spbu/php/create_pemukiman_miskin.php | 192 + webgis-spbu/php/create_point.php | 124 + webgis-spbu/php/create_rumah_ibadah.php | 60 + webgis-spbu/php/delete_jalan.php | 16 + webgis-spbu/php/delete_parsil.php | 16 + webgis-spbu/php/delete_pemukiman_miskin.php | 33 + webgis-spbu/php/delete_point.php | 16 + webgis-spbu/php/delete_rumah_ibadah.php | 19 + webgis-spbu/php/koneksi.php | 27 + webgis-spbu/php/read_anggota_keluarga.php | 24 + webgis-spbu/php/read_jalan.php | 51 + webgis-spbu/php/read_parsil.php | 49 + webgis-spbu/php/read_pemukiman_miskin.php | 127 + webgis-spbu/php/read_point.php | 96 + webgis-spbu/php/read_rumah_ibadah.php | 17 + webgis-spbu/php/reverse_geocode.php | 42 + webgis-spbu/php/update_jalan.php | 63 + webgis-spbu/php/update_parsil.php | 58 + webgis-spbu/php/update_pemukiman_miskin.php | 213 + .../php/update_pemukiman_miskin_location.php | 95 + webgis-spbu/php/update_point.php | 115 + webgis-spbu/php/update_rumah_ibadah.php | 75 + webgis-spbu/sql/schema_webgis_spbu.sql | 466 ++ webgis-spbu/uploads/bukti/.gitkeep | 1 + 80 files changed, 17551 insertions(+) create mode 100644 index.html create mode 100644 manual.html create mode 100644 webgis-kemiskinan/Dockerfile create mode 100644 webgis-kemiskinan/README.md create mode 100644 webgis-kemiskinan/admin/_layout.php create mode 100644 webgis-kemiskinan/admin/bantuan.php create mode 100644 webgis-kemiskinan/admin/belum_dibantu.php create mode 100644 webgis-kemiskinan/admin/forgot.php create mode 100644 webgis-kemiskinan/admin/histori_bantuan.php create mode 100644 webgis-kemiskinan/admin/index.php create mode 100644 webgis-kemiskinan/admin/init_admin.php create mode 100644 webgis-kemiskinan/admin/keluarga.php create mode 100644 webgis-kemiskinan/admin/laporan.php create mode 100644 webgis-kemiskinan/admin/log.php create mode 100644 webgis-kemiskinan/admin/login.php create mode 100644 webgis-kemiskinan/admin/logout.php create mode 100644 webgis-kemiskinan/admin/penduduk.php create mode 100644 webgis-kemiskinan/admin/rumah_ibadah.php create mode 100644 webgis-kemiskinan/admin/users.php create mode 100644 webgis-kemiskinan/index.html create mode 100644 webgis-kemiskinan/php/api/auth.php create mode 100644 webgis-kemiskinan/php/api/bantuan.php create mode 100644 webgis-kemiskinan/php/api/keluarga.php create mode 100644 webgis-kemiskinan/php/api/laporan.php create mode 100644 webgis-kemiskinan/php/api/penduduk.php create mode 100644 webgis-kemiskinan/php/api/pesan.php create mode 100644 webgis-kemiskinan/php/api/rumah_ibadah.php create mode 100644 webgis-kemiskinan/php/koneksi.php create mode 100644 webgis-kemiskinan/php/middleware/auth.php create mode 100644 webgis-kemiskinan/sql/schema.sql create mode 100644 webgis-kemiskinan/update_db.php create mode 100644 webgis-kemiskinan/uploads/bukti/.gitkeep create mode 100644 webgis-kemiskinan/uploads/foto_laporan/.gitkeep create mode 100644 webgis-kemiskinan/uploads/foto_profil/.gitkeep create mode 100644 webgis-spbu/.codex create mode 100644 webgis-spbu/Dockerfile create mode 100644 webgis-spbu/README.md create mode 100644 webgis-spbu/admin/_auth.php create mode 100644 webgis-spbu/admin/_layout.php create mode 100644 webgis-spbu/admin/index.php create mode 100644 webgis-spbu/admin/init_admin.php create mode 100644 webgis-spbu/admin/login.php create mode 100644 webgis-spbu/admin/logout.php create mode 100644 webgis-spbu/admin/penduduk_miskin.php create mode 100644 webgis-spbu/admin/rumah_ibadah.php create mode 100644 webgis-spbu/css/style.css create mode 100644 webgis-spbu/fetch_geojson.js create mode 100644 webgis-spbu/fetch_osm.js create mode 100644 webgis-spbu/index.html create mode 100644 webgis-spbu/js/app.js create mode 100644 webgis-spbu/js/pontianak_kecamatan.geojson create mode 100644 webgis-spbu/migrate_now.php create mode 100644 webgis-spbu/osm_pontianak.json create mode 100644 webgis-spbu/osm_to_geojson_draft.js create mode 100644 webgis-spbu/php/create_jalan.php create mode 100644 webgis-spbu/php/create_parsil.php create mode 100644 webgis-spbu/php/create_pemukiman_miskin.php create mode 100644 webgis-spbu/php/create_point.php create mode 100644 webgis-spbu/php/create_rumah_ibadah.php create mode 100644 webgis-spbu/php/delete_jalan.php create mode 100644 webgis-spbu/php/delete_parsil.php create mode 100644 webgis-spbu/php/delete_pemukiman_miskin.php create mode 100644 webgis-spbu/php/delete_point.php create mode 100644 webgis-spbu/php/delete_rumah_ibadah.php create mode 100644 webgis-spbu/php/koneksi.php create mode 100644 webgis-spbu/php/read_anggota_keluarga.php create mode 100644 webgis-spbu/php/read_jalan.php create mode 100644 webgis-spbu/php/read_parsil.php create mode 100644 webgis-spbu/php/read_pemukiman_miskin.php create mode 100644 webgis-spbu/php/read_point.php create mode 100644 webgis-spbu/php/read_rumah_ibadah.php create mode 100644 webgis-spbu/php/reverse_geocode.php create mode 100644 webgis-spbu/php/update_jalan.php create mode 100644 webgis-spbu/php/update_parsil.php create mode 100644 webgis-spbu/php/update_pemukiman_miskin.php create mode 100644 webgis-spbu/php/update_pemukiman_miskin_location.php create mode 100644 webgis-spbu/php/update_point.php create mode 100644 webgis-spbu/php/update_rumah_ibadah.php create mode 100644 webgis-spbu/sql/schema_webgis_spbu.sql create mode 100644 webgis-spbu/uploads/bukti/.gitkeep diff --git a/index.html b/index.html new file mode 100644 index 0000000..53beb15 --- /dev/null +++ b/index.html @@ -0,0 +1,293 @@ + + + + + + Portal WebGIS - UAS Project + + + + + + +
+
+ +
+

WebGIS Project UAS

+

Silakan pilih proyek yang ingin Anda akses

+
+ +
+ + +
Akses Publik
+
+ +
+

Project 1: WebGIS SPBU

+

Jelajahi peta interaktif untuk SPBU, Jalan, Parsil, dan lainnya secara langsung tanpa perlu login.

+ +
+ + + +
Akses Login
+
+ +
+

Project 2: WebGIS Kemiskinan

+

Sistem manajemen dan analisis pengentasan kemiskinan berbasis masyarakat. Memerlukan login untuk akses.

+ +
+
+ +
+ + + Baca Manual Book + +
+ + diff --git a/manual.html b/manual.html new file mode 100644 index 0000000..ce08a5a --- /dev/null +++ b/manual.html @@ -0,0 +1,199 @@ + + + + + + Manual Book - WebGIS Project UAS + + + + + + +
+
+ +
+ + + Kembali ke Landing Page + + +
+

Manual Book

+

Panduan Lengkap Portal WebGIS Project UAS

+
+ +
+

Selamat datang di Panduan Penggunaan Sistem Portal WebGIS. Sistem ini terdiri dari dua proyek utama yang terintegrasi dalam satu landing page. Panduan ini dirancang untuk memudahkan Anda memahami dan mengoperasikan seluruh fitur yang tersedia sesuai dengan hak akses (role) Anda.

+ +

1. Navigasi Awal (Landing Page)

+

Saat pertama kali mengakses sistem, Anda akan disambut oleh halaman Portal Utama.

+
    +
  • Kartu Pertama (WebGIS SPBU): Digunakan untuk mengakses peta fasilitas umum secara publik (tanpa login).
  • +
  • Kartu Kedua (WebGIS Kemiskinan): Digunakan untuk mengakses sistem manajemen kemiskinan. Anda akan diarahkan ke halaman login karena sistem ini berbasis otorisasi.
  • +
+ +
+ πŸ’‘ Tip: Jika Anda sedang berada di dalam salah satu peta, Anda selalu bisa menekan tombol "Kembali ke Landing Page" (di sidebar kiri atau atas) untuk kembali ke menu awal. +
+ +

2. Project 1: WebGIS SPBU (Akses Publik)

+

Proyek ini dirancang sebagai sistem informasi geografis interaktif yang bersifat terbuka untuk masyarakat luas.

+ +

A. Melihat dan Mencari Lokasi

+
    +
  • Peta Interaktif: Anda dapat menggeser (drag) dan memperbesar (scroll/zoom) peta.
  • +
  • Filter & Layer: Di panel sebelah kiri, terdapat checkbox untuk menyalakan atau mematikan tampilan layer tertentu (contoh: menyembunyikan Jalan atau Rumah Ibadah).
  • +
  • Filter Cepat: Gunakan tombol di Toolbar peta untuk langsung menyorot "SPBU 24 Jam".
  • +
+ +

B. Menambah Data Spasial (Simulasi)

+

Sistem ini menyediakan alat untuk menggambar objek langsung di peta:

+
    +
  • Point: Klik tombol Tambah Point, lalu klik area pada peta.
  • +
  • Jalan: Klik Tambah Jalan, klik titik-titik untuk membuat jalur, klik ganda untuk selesai.
  • +
  • Parsil: Buat area poligon dan satukan kembali ke titik awal.
  • +
+ +

3. Project 2: WebGIS Kemiskinan (Berbasis Otorisasi)

+

Proyek ini adalah sistem tata kelola data kemiskinan yang terstruktur secara ketat berdasarkan Role-Based Access Control (RBAC).

+ +
+ ⚠️ Penting: Kebijakan Akses Peta sangat ketat. Peta ini tidak bisa diakses secara anonim. Jika Anda memaksa masuk melalui URL, Anda akan otomatis dialihkan ke halaman Login. +
+ +

A. Hak Akses (Role)

+
    +
  • Admin (Administrator): Akses absolut. Bisa mengelola semua penduduk, semua rumah ibadah, user, dan histori log.
  • +
  • Pengurus Masjid/Gereja: Dibatasi hanya mengelola warga dan bantuan di dalam radius wilayah rumah ibadahnya.
  • +
  • Pimpinan Daerah: Akses Read-Only. Dirancang untuk Bupati/Camat yang perlu melihat statistik dan Heatmap secara luas tanpa bisa mengubah data.
  • +
  • Warga Masyarakat: Dapat digunakan warga untuk sekadar login dan memantau transparansi (jika fitur diekspos).
  • +
+ +

B. Fitur Peta Kemiskinan

+
    +
  • Layer Ekonomi: Titik berwarna Hijau (Mampu), Kuning (Rentan), dan Merah (Miskin).
  • +
  • Lapor Darurat 🚨: Tombol segitiga peringatan di atas peta digunakan warga untuk melaporkan tetangga yang kritis. Form bisa diisi anonim.
  • +
  • Blind Spot: Di tab Statistik, pimpinan dapat menekan "Cari Titik Blind Spot" untuk menemukan warga miskin yang belum pernah mendapat bantuan apa pun.
  • +
+ +
+ ⚑ Peringatan Keamanan: Segala aktivitas seperti Edit, Hapus, dan Verifikasi akan direkam secara permanen ke dalam tabel Log Aktivitas bersama dengan Alamat IP dan waktu (Timestamp) Anda. Harap berhati-hati saat memanipulasi data! +
+ +
+
+ + diff --git a/webgis-kemiskinan/Dockerfile b/webgis-kemiskinan/Dockerfile new file mode 100644 index 0000000..f2a69f4 --- /dev/null +++ b/webgis-kemiskinan/Dockerfile @@ -0,0 +1,12 @@ +FROM php:8.2-apache + +RUN docker-php-ext-install mysqli pdo pdo_mysql \ + && a2enmod rewrite + +COPY . /var/www/html/ + +RUN mkdir -p /var/www/html/uploads/bukti \ + && mkdir -p /var/www/html/uploads/foto_laporan \ + && mkdir -p /var/www/html/uploads/foto_profil \ + && chown -R www-data:www-data /var/www/html/uploads \ + && chown -R www-data:www-data /var/www/html \ No newline at end of file diff --git a/webgis-kemiskinan/README.md b/webgis-kemiskinan/README.md new file mode 100644 index 0000000..95db997 --- /dev/null +++ b/webgis-kemiskinan/README.md @@ -0,0 +1,185 @@ +# WebGIS Pengentasan Kemiskinan v2.0 +### Berbasis Masyarakat & Rumah Ibadah + +> Pengembangan dari sistem webgis-spbu menjadi sistem manajemen sosial yang lengkap, real-time, dan berbasis relasi database. + +--- + +## πŸš€ Fitur Lengkap + +| Fitur | Status | +|---|---| +| Peta Leaflet interaktif + MarkerCluster | βœ… | +| Layer: penduduk, keluarga, rumah ibadah, laporan, jalan | βœ… | +| Warna marker berdasar status ekonomi (merah/kuning/hijau) | βœ… | +| CRUD penduduk lengkap (NIK, umur otomatis, kesehatan, dll) | βœ… | +| Sistem keluarga (KK β†’ anggota) | βœ… | +| Validasi logika pendidikan vs umur | βœ… | +| Laporan masyarakat (siapapun bisa lapor, tanpa login) | βœ… | +| Manajemen bantuan + histori per penduduk | βœ… | +| Deteksi otomatis warga belum pernah dibantu | βœ… | +| Prioritas bantuan (KRITIS = miskin + sakit parah) | βœ… | +| Multi-role: admin, pengurus, pimpinan, masyarakat | βœ… | +| Chat/diskusi antar pengurus per rumah ibadah | βœ… | +| Notifikasi real-time (polling) | βœ… | +| Audit log semua aktivitas | βœ… | +| Dashboard monitoring statistik | βœ… | +| Data dummy siap pakai (Pontianak) | βœ… | + +--- + +## πŸ“ Struktur Direktori + +``` +webgis-kemiskinan/ +β”œβ”€β”€ index.html ← Peta WebGIS utama (publik) +β”œβ”€β”€ admin/ +β”‚ β”œβ”€β”€ login.php ← Halaman login +β”‚ β”œβ”€β”€ logout.php +β”‚ β”œβ”€β”€ init_admin.php ← Inisialisasi user (jalankan sekali!) +β”‚ β”œβ”€β”€ _layout.php ← Layout admin (header + nav) +β”‚ β”œβ”€β”€ index.php ← Dashboard +β”‚ β”œβ”€β”€ penduduk.php ← CRUD penduduk +β”‚ β”œβ”€β”€ bantuan.php ← Catat penyaluran bantuan +β”‚ β”œβ”€β”€ histori_bantuan.php ← Riwayat bantuan +β”‚ β”œβ”€β”€ belum_dibantu.php ← Warga belum tersentuh bantuan +β”‚ β”œβ”€β”€ laporan.php ← Manajemen laporan masyarakat +β”‚ └── log.php ← Audit log (admin only) +β”œβ”€β”€ php/ +β”‚ β”œβ”€β”€ koneksi.php ← Config DB + helper functions +β”‚ β”œβ”€β”€ middleware/ +β”‚ β”‚ └── auth.php ← Autentikasi & otorisasi +β”‚ └── api/ +β”‚ β”œβ”€β”€ auth.php ← Login/logout API +β”‚ β”œβ”€β”€ penduduk.php ← CRUD penduduk (RESTful) +β”‚ β”œβ”€β”€ keluarga.php ← CRUD keluarga +β”‚ β”œβ”€β”€ bantuan.php ← Bantuan + histori + stats +β”‚ β”œβ”€β”€ laporan.php ← Laporan masyarakat +β”‚ β”œβ”€β”€ rumah_ibadah.php ← CRUD rumah ibadah +β”‚ └── pesan.php ← Chat + notifikasi +β”œβ”€β”€ sql/ +β”‚ └── schema.sql ← Schema + data dummy +└── uploads/ + β”œβ”€β”€ bukti/ ← Bukti penyaluran bantuan + └── foto_laporan/ ← Foto dari laporan masyarakat +``` + +--- + +## βš™οΈ Instalasi (XAMPP) + +### 1. Salin Folder +``` +C:\xampp\htdocs\webgis-kemiskinan\ +``` + +### 2. Buat Database +- Buka **phpMyAdmin** β†’ `http://localhost/phpmyadmin` +- Buat database baru: `webgis_kemiskinan` +- Import: `sql/schema.sql` + +### 3. Konfigurasi DB +Edit `php/koneksi.php`: +```php +define('DB_NAME', 'webgis_kemiskinan'); +define('DB_USER', 'root'); +define('DB_PASS', ''); // sesuaikan password MySQL Anda +``` + +### 4. Inisialisasi User +Buka sekali di browser: +``` +http://localhost/webgis-kemiskinan/admin/init_admin.php +``` +> **Hapus file ini setelah dijalankan!** + +### 5. Akses Sistem +| URL | Keterangan | +|---|---| +| `http://localhost/webgis-kemiskinan/` | Peta WebGIS (publik) | +| `http://localhost/webgis-kemiskinan/admin/login.php` | Panel Admin | + +--- + +## πŸ‘€ Akun Demo + +| Username | Password | Role | +|---|---|---| +| `admin` | `password` | Administrator | +| `pengurus1` | `password` | Pengurus Masjid Al-Falah | +| `pimpinan1` | `password` | Pimpinan Daerah | +| `warga1` | `password` | Masyarakat | + +> Hash yang digunakan adalah hash Laravel default untuk string `"password"`. +> Untuk produksi, ganti dengan: `password_hash('PasswordBaru', PASSWORD_BCRYPT)` + +--- + +## πŸ—„οΈ Tabel Database + +| Tabel | Fungsi | +|---|---| +| `roles` | Definisi role sistem | +| `users` | Akun pengguna | +| `rumah_ibadah` | Data rumah ibadah + radius | +| `keluarga` | Data KK | +| `penduduk` | Data individu (NIK sebagai PK) | +| `riwayat_pelatihan` | Pelatihan kerja per penduduk | +| `jenis_bantuan` | Master jenis bantuan | +| `histori_bantuan` | Riwayat penyaluran bantuan | +| `laporan` | Laporan kondisi dari masyarakat | +| `pesan` | Chat antar pengurus | +| `notifikasi` | Notifikasi per user | +| `log_aktivitas` | Audit trail semua perubahan data | +| `jalan_lines` | Layer jalan (GeoJSON) | +| `parsil_tanah` | Layer parsil tanah (GeoJSON) | + +--- + +## 🧠 Logika Khusus + +### Validasi Pendidikan vs Umur +- Umur 7–12 tahun β†’ wajib masih sekolah (SD) +- Umur 13–15 tahun β†’ wajib masih sekolah (SMP) +- Umur 16–18 tahun β†’ wajib masih sekolah (SMA/SMK) +- Umur >18 tahun β†’ bebas (bisa tidak sekolah / ikut pelatihan) + +### Prioritas Bantuan (otomatis) +| Kondisi | Prioritas | +|---|---| +| Miskin + Sakit Parah/Disabilitas | πŸ”΄ KRITIS | +| Miskin + Belum pernah dibantu | 🟠 TINGGI | +| Miskin (umum) | 🟑 SEDANG | +| Rentan + Sakit | 🟑 SEDANG | +| Lainnya | 🟒 RENDAH | + +### Umur Otomatis +Kolom `umur` di tabel `penduduk` adalah **GENERATED COLUMN** β€” dihitung langsung dari `tanggal_lahir` setiap kali data dibaca. Tidak perlu update manual. + +--- + +## πŸ”’ Keamanan +- PDO dengan prepared statements (anti SQL injection) +- `password_hash()` / `password_verify()` untuk password +- Session dengan `session_regenerate_id()` saat login +- Validasi input di sisi server untuk semua endpoint +- Otorisasi per-role di setiap API endpoint +- Upload file: validasi ekstensi + ukuran + nama acak + +--- + +## πŸ“± Cara Buat Laporan (Masyarakat) +1. Buka `http://localhost/webgis-kemiskinan/` +2. Klik tombol **"Buat Laporan"** di pojok kanan atas +3. Isi deskripsi kondisi (nama pelapor opsional) +4. Klik peta untuk memilih lokasi otomatis +5. Kirim β†’ pengurus akan mendapat notifikasi otomatis + +--- + +## πŸ”„ Pengembangan Lanjutan (Saran) +- [ ] Integrasi SMS/WhatsApp notifikasi +- [ ] Export PDF laporan distribusi bantuan +- [ ] Login via Google / warga menggunakan NIK + tanggal lahir +- [ ] Mobile app (React Native / Flutter) +- [ ] Sinkronisasi data dengan Dukcapil diff --git a/webgis-kemiskinan/admin/_layout.php b/webgis-kemiskinan/admin/_layout.php new file mode 100644 index 0000000..e019363 --- /dev/null +++ b/webgis-kemiskinan/admin/_layout.php @@ -0,0 +1,121 @@ +prepare("SELECT COUNT(*) FROM notifikasi WHERE user_id=:u AND dibaca=0"); + $stmtN->execute([':u' => $_SESSION['user_id']]); + $unread = (int)$stmtN->fetchColumn(); + } catch(Exception $e){} + } +?> + + + + + + <?= h($title) ?> β€” WebGIS Kemiskinan + + + + +
+ +
+ πŸ‘€ () + Buka Peta + Logout +
+
+ +
+
'; +} + +function require_login(array $roles = []): void { + requireLogin($roles); +} diff --git a/webgis-kemiskinan/admin/bantuan.php b/webgis-kemiskinan/admin/bantuan.php new file mode 100644 index 0000000..d81e8ef --- /dev/null +++ b/webgis-kemiskinan/admin/bantuan.php @@ -0,0 +1,155 @@ +prepare("SELECT nik,id_keluarga FROM penduduk WHERE nik=:nik AND status_hidup='hidup'"); + $pddk->execute([':nik'=>$nik]); + $pddk = $pddk->fetch(); + if (!$pddk) throw new Exception("Penduduk dengan NIK $nik tidak ditemukan / meninggal."); + + $bukti = null; + if (!empty($_FILES['bukti']['name'])) { + $bukti = uploadFile($_FILES['bukti'], UPLOAD_BUKTI); + } + $stmt = $pdo->prepare(" + INSERT INTO histori_bantuan (nik,id_jenis_bantuan,id_keluarga,rumah_ibadah_id,tanggal,jumlah,nilai_rupiah,status_penyaluran,catatan,bukti_file,disalurkan_oleh) + VALUES(:nik,:jb,:kk,:ri,:tgl,:jml,:rp,:status,:cat,:bukti,:user) + "); + $stmt->execute([ + ':nik'=>$nik, ':jb'=>$idJb, ':kk'=>$pddk['id_keluarga'], + ':ri'=>$_SESSION['rumah_ibadah_id'] ?: null, + ':tgl'=>$tgl, ':jml'=>$_POST['jumlah']??'' ?: null, + ':rp'=>$_POST['nilai_rupiah']??'' ?: null, + ':status'=>$_POST['status_penyaluran']??'disalurkan', + ':cat'=>$_POST['catatan']??'' ?: null, + ':bukti'=>$bukti, ':user'=>$_SESSION['user_id'], + ]); + logAktivitas($pdo, (int)$_SESSION['user_id'], 'CREATE', 'histori_bantuan', (string)$pdo->lastInsertId()); + $msg = "Bantuan untuk NIK $nik berhasil dicatat."; + } catch(Exception $e) { $err = $e->getMessage(); } + } +} + +$jenisList = $pdo->query("SELECT * FROM jenis_bantuan WHERE aktif=1 ORDER BY kategori,nama")->fetchAll(); + +render_header('Catat Bantuan'); +?> +
🎁 Catat Penyaluran Bantuan
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+ πŸ“‹ Bantuan Terakhir Disalurkan + + + + query(" + SELECT p.nama_lengkap, jb.nama AS nama_bantuan, hb.tanggal + FROM histori_bantuan hb + JOIN penduduk p ON p.nik=hb.nik + JOIN jenis_bantuan jb ON jb.id=hb.id_jenis_bantuan + WHERE hb.status_penyaluran='disalurkan' + ORDER BY hb.created_at DESC LIMIT 10 + ")->fetchAll(); + foreach ($recent as $r): ?> + + + + + + + +
PenerimaBantuanTanggal
+
+
+
+ + + + diff --git a/webgis-kemiskinan/admin/belum_dibantu.php b/webgis-kemiskinan/admin/belum_dibantu.php new file mode 100644 index 0000000..136e493 --- /dev/null +++ b/webgis-kemiskinan/admin/belum_dibantu.php @@ -0,0 +1,71 @@ +query(" + SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan, + p.umur, p.pekerjaan, p.penghasilan, p.alamat, + k.no_kk, k.nama_kk, ri.nama AS nama_ri, ri.jenis AS jenis_ri + FROM penduduk p + LEFT JOIN keluarga k ON k.id=p.id_keluarga + LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id + WHERE p.status_hidup='hidup' + AND p.status_ekonomi IN ('miskin','rentan') + AND NOT EXISTS( + SELECT 1 FROM histori_bantuan hb + WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan' + ) + ORDER BY + CASE p.status_ekonomi WHEN 'miskin' THEN 1 ELSE 2 END, + CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 WHEN 'disabilitas' THEN 2 WHEN 'sakit_ringan' THEN 3 ELSE 4 END, + p.nama_lengkap + ")->fetchAll(); +} catch(Exception $e) {} + +render_header('Belum Pernah Dibantu'); +?> +
⚠️ Warga Belum Pernah Menerima Bantuan
+ +
+ Ditemukan jiwa dengan status ekonomi miskin/rentan yang belum pernah menerima bantuan apapun. Prioritaskan yang kondisi kesehatannya kritis. +
+ +
+ + + + + + + + > + + + + + + + + + + + + + + + + +
NamaNIKStatusKesehatanUmurPekerjaanRumah IbadahAksi
+ + KRITIS' : '' ?> + th + + 🎁 Beri Bantuan + +
πŸŽ‰ Semua warga miskin/rentan sudah pernah dibantu!
+
+ diff --git a/webgis-kemiskinan/admin/forgot.php b/webgis-kemiskinan/admin/forgot.php new file mode 100644 index 0000000..e6c582e --- /dev/null +++ b/webgis-kemiskinan/admin/forgot.php @@ -0,0 +1,92 @@ + + + + + + Lupa Password β€” WebGIS Pengentasan Kemiskinan + + + +prepare("SELECT id FROM users WHERE username = :u AND email = :e LIMIT 1"); + $stmt->execute([':u' => $username, ':e' => $email]); + $user = $stmt->fetch(); + + if ($user) { + $hash = password_hash($new_password, PASSWORD_DEFAULT); + $update = $pdo->prepare("UPDATE users SET password_hash = :p WHERE id = :id"); + $update->execute([':p' => $hash, ':id' => $user['id']]); + $success = 'Password berhasil direset! Silakan login dengan password baru Anda.'; + } else { + $error = 'Username dan Email tidak cocok atau tidak ditemukan.'; + } + } +} +?> +
+ + + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + diff --git a/webgis-kemiskinan/admin/histori_bantuan.php b/webgis-kemiskinan/admin/histori_bantuan.php new file mode 100644 index 0000000..e64fe3c --- /dev/null +++ b/webgis-kemiskinan/admin/histori_bantuan.php @@ -0,0 +1,48 @@ +query(" + SELECT hb.id, hb.nik, p.nama_lengkap, hb.tanggal, + jb.nama AS nama_bantuan, jb.kategori, + hb.jumlah, hb.nilai_rupiah, hb.status_penyaluran, + ri.nama AS nama_ri, u.nama AS disalurkan_oleh + FROM histori_bantuan hb + JOIN penduduk p ON p.nik=hb.nik + JOIN jenis_bantuan jb ON jb.id=hb.id_jenis_bantuan + LEFT JOIN rumah_ibadah ri ON ri.id=hb.rumah_ibadah_id + LEFT JOIN users u ON u.id=hb.disalurkan_oleh + ORDER BY hb.tanggal DESC + LIMIT 500 + ")->fetchAll(); +} catch(Exception $e) {} + +render_header('Histori Bantuan'); +?> +
πŸ“œ Histori Penyaluran Bantuan
+
+ + + + + + + + + + + + + + + + + + + + +
#Nama PenerimaJenis BantuanJumlahTanggalRumah IbadahDisalurkan OlehStatus

()Rp '.number_format($r['nilai_rupiah'],0,',','.').'' : '' ?>
Belum ada data
+
+ diff --git a/webgis-kemiskinan/admin/index.php b/webgis-kemiskinan/admin/index.php new file mode 100644 index 0000000..df9ca0e --- /dev/null +++ b/webgis-kemiskinan/admin/index.php @@ -0,0 +1,128 @@ +query(" + SELECT + (SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='miskin' AND status_hidup='hidup') AS miskin, + (SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='rentan' AND status_hidup='hidup') AS rentan, + (SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup') AS total_pddk, + (SELECT COUNT(*) FROM keluarga) AS total_kk, + (SELECT COUNT(*) FROM rumah_ibadah) AS total_ri, + (SELECT COUNT(*) FROM laporan WHERE status='pending') AS laporan_pending, + (SELECT COUNT(*) FROM laporan WHERE tingkat_urgensi='darurat' AND status NOT IN ('selesai','ditolak')) AS laporan_darurat, + (SELECT COUNT(*) FROM histori_bantuan WHERE MONTH(tanggal)=MONTH(CURDATE()) AND YEAR(tanggal)=YEAR(CURDATE()) AND status_penyaluran='disalurkan') AS bantuan_bulan, + (SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup' AND status_ekonomi IN ('miskin','rentan') + AND NOT EXISTS(SELECT 1 FROM histori_bantuan hb WHERE hb.nik=penduduk.nik AND hb.status_penyaluran='disalurkan') + ) AS belum_dibantu + ")->fetch(); +} catch(Exception $e) {} + +// Laporan terbaru +$laporan_terbaru = []; +try { + $laporan_terbaru = $pdo->query(" + SELECT l.id, l.tingkat_urgensi, l.kategori, l.status, l.created_at, + l.nama_pelapor, p.nama_lengkap AS nama_terdampak + FROM laporan l + LEFT JOIN penduduk p ON p.nik = l.nik_terdampak + ORDER BY + CASE l.tingkat_urgensi WHEN 'darurat' THEN 1 WHEN 'tinggi' THEN 2 ELSE 3 END, + l.created_at DESC + LIMIT 8 + ")->fetchAll(); +} catch(Exception $e) {} + +// Prioritas belum dibantu +$prioritas = []; +try { + $prioritas = $pdo->query(" + SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan, + p.alamat, k.no_kk, ri.nama AS nama_ri + FROM penduduk p + LEFT JOIN keluarga k ON k.id=p.id_keluarga + LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id + WHERE p.status_hidup='hidup' + AND p.status_ekonomi='miskin' + AND p.kondisi_kesehatan IN ('sakit_parah','disabilitas') + AND NOT EXISTS(SELECT 1 FROM histori_bantuan hb WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan') + ORDER BY CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 ELSE 2 END + LIMIT 5 + ")->fetchAll(); +} catch(Exception $e) {} + +render_header('Dashboard'); +?> + +
πŸ“Š Dashboard Monitoring
+ + +
+
Warga Miskin
+
Warga Rentan
+
Total KK
+
Bantuan Bulan Ini
+
Laporan Pending
+
Belum Pernah Dibantu
+
+ + 0): ?> +
+ 🚨 +
+
LAPORAN DARURAT belum ditangani!
+
Segera tindak lanjuti laporan kondisi darurat dari masyarakat.
+
+ Lihat β†’ +
+ + +
+ + +
+
+ πŸ“‹ Laporan Terbaru + Semua β†’ +
+ + + + + + + + + + + + +
Pelapor/TerdampakUrgensiStatus
Belum ada laporan
+
+ + +
+
+ πŸ”₯ Prioritas KRITIS (Belum Dibantu) + Semua β†’ +
+ + + + + + + + + + + + +
NamaStatusKesehatan
πŸŽ‰ Tidak ada kasus kritis saat ini
+
+ +
+ + diff --git a/webgis-kemiskinan/admin/init_admin.php b/webgis-kemiskinan/admin/init_admin.php new file mode 100644 index 0000000..3d9b7e6 --- /dev/null +++ b/webgis-kemiskinan/admin/init_admin.php @@ -0,0 +1,32 @@ +prepare("SELECT id FROM users WHERE username=:u"); + $cek->execute([':u'=>$uname]); + if (!$cek->fetch()) { + $pdo->prepare("INSERT INTO users (role_id,nama,username,password_hash,rumah_ibadah_id) VALUES(:r,:n,:u,:p,:ri)") + ->execute([':r'=>$role,':n'=>$nama,':u'=>$uname,':p'=>$hash,':ri'=>$ri]); + $created++; + echo "βœ… User '$uname' dibuat.
"; + } else { + echo "ℹ️ User '$uname' sudah ada β€” dilewati.
"; + } +} + +echo "
$created user baru dibuat.
"; +echo "
Password semua akun demo: password
"; +echo "
Segera hapus file ini setelah digunakan!
"; +echo "
β†’ Ke halaman login"; diff --git a/webgis-kemiskinan/admin/keluarga.php b/webgis-kemiskinan/admin/keluarga.php new file mode 100644 index 0000000..357ac70 --- /dev/null +++ b/webgis-kemiskinan/admin/keluarga.php @@ -0,0 +1,159 @@ +prepare("DELETE FROM keluarga WHERE id=:id")->execute([':id'=>$id]); + logAktivitas($pdo, (int)$_SESSION['user_id'], 'DELETE', 'keluarga', (string)$id); + $msg = "Data keluarga #$id dihapus."; + } catch(Exception $e) { $err = 'Gagal hapus: '.$e->getMessage(); } +} + +// SAVE +if ($canEdit && isset($_POST['save'])) { + $nokk = trim($_POST['no_kk'] ?? ''); + $nama = trim($_POST['nama_kk'] ?? ''); + $idEdit = (int)($_POST['id_edit'] ?? 0); + if (!preg_match('/^\d{16}$/', $nokk)) $err = 'No KK harus 16 digit.'; + elseif (strlen($nama) < 3) $err = 'Nama KK terlalu pendek.'; + else { + try { + $data = [ + ':nokk' => $nokk, ':nama' => $nama, + ':almt' => $_POST['alamat']??'' ?: null, + ':kel' => $_POST['kelurahan']??'' ?: null, + ':kec' => $_POST['kecamatan']??'' ?: null, + ':kota' => $_POST['kota']??'' ?: null, + ':lat' => $_POST['latitude']??'' ?: null, + ':lng' => $_POST['longitude']??'' ?: null, + ':se' => $_POST['status_ekonomi']??'rentan', + ':ri' => $_POST['rumah_ibadah_id']??'' ?: null, + ]; + if ($idEdit) { + $data[':id'] = $idEdit; + $pdo->prepare("UPDATE keluarga SET no_kk=:nokk,nama_kk=:nama,alamat=:almt,kelurahan=:kel, + kecamatan=:kec,kota=:kota,latitude=:lat,longitude=:lng,status_ekonomi=:se, + rumah_ibadah_id=:ri WHERE id=:id")->execute($data); + $msg = "Data KK $nama diperbarui."; + logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'keluarga', (string)$idEdit); + } else { + $pdo->prepare("INSERT INTO keluarga (no_kk,nama_kk,alamat,kelurahan,kecamatan,kota,latitude,longitude,status_ekonomi,rumah_ibadah_id) + VALUES(:nokk,:nama,:almt,:kel,:kec,:kota,:lat,:lng,:se,:ri)")->execute($data); + $msg = "Keluarga $nama berhasil ditambahkan."; + logAktivitas($pdo, (int)$_SESSION['user_id'], 'CREATE', 'keluarga', (string)$pdo->lastInsertId()); + } + } catch(Exception $e) { $err = $e->getMessage(); } + } +} + +$edit = null; +if (isset($_GET['edit'])) { + $s = $pdo->prepare("SELECT * FROM keluarga WHERE id=:id"); + $s->execute([':id'=>(int)$_GET['edit']]); + $edit = $s->fetch() ?: null; +} + +$search = trim($_GET['s'] ?? ''); +$where = []; $params = []; +if ($search) { $where[] = '(k.nama_kk LIKE :q OR k.no_kk LIKE :q2)'; $params[':q']='%'.$search.'%'; $params[':q2']='%'.$search.'%'; } +$whereStr = $where ? 'WHERE '.implode(' AND ',$where) : ''; +$rows = $pdo->prepare(" + SELECT k.*, ri.nama AS nama_ri, + (SELECT COUNT(*) FROM penduduk p WHERE p.id_keluarga=k.id) AS jml_anggota + FROM keluarga k + LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id + $whereStr ORDER BY k.nama_kk LIMIT 300 +"); +$rows->execute($params); +$rows = $rows->fetchAll(); +$riList = $pdo->query("SELECT id,nama,jenis FROM rumah_ibadah ORDER BY nama")->fetchAll(); + +render_header('Keluarga (KK)'); +?> +
🏠 Data Keluarga (Kartu Keluarga)
+ +
+
+ +
+ +
+

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Batal +
+
+ + +
+
+
+ + +
+
+
+ + Aksi':'' ?> + + + + + + + + + + + + + + + +
No KKNama KKStatusAnggotaRumah Ibadah

jiwa + ✏️ + πŸ—‘οΈ +
Belum ada data
+
+
+
+ diff --git a/webgis-kemiskinan/admin/laporan.php b/webgis-kemiskinan/admin/laporan.php new file mode 100644 index 0000000..bc0581a --- /dev/null +++ b/webgis-kemiskinan/admin/laporan.php @@ -0,0 +1,167 @@ + (int)$_SESSION['user_id'], 'role' => $_SESSION['role']]; +$filter = $_GET['filter'] ?? 'semua'; +$msg = ''; $err = ''; + +// Update status laporan +if (isset($_POST['update_status']) && in_array($currentUser['role'], ['admin','pengurus'])) { + $id = (int)($_POST['lap_id'] ?? 0); + $status = $_POST['status'] ?? ''; + $catatan = trim($_POST['catatan'] ?? ''); + $valid = ['diverifikasi','diproses','selesai','ditolak']; + if ($id > 0 && in_array($status, $valid)) { + try { + $sets = ['status=:s','updated_at=NOW()']; + $params = [':s'=>$status,':id'=>$id]; + if ($status === 'diverifikasi') { $sets[] = 'diverifikasi_oleh=:v'; $params[':v'] = $currentUser['id']; } + if ($status === 'diproses') { $sets[] = 'ditangani_oleh=:t'; $params[':t'] = $currentUser['id']; } + if ($status === 'selesai') { $sets[] = 'tanggal_selesai=NOW()'; } + if ($catatan) { $sets[] = 'catatan_verifikasi=:c'; $params[':c'] = $catatan; } + $pdo->prepare("UPDATE laporan SET ".implode(',',$sets)." WHERE id=:id")->execute($params); + logAktivitas($pdo, $currentUser['id'], 'UPDATE', 'laporan', (string)$id, null, ['status'=>$status]); + $msg = "Status laporan #$id diperbarui ke '$status'."; + } catch(Exception $e) { $err = $e->getMessage(); } + } +} + +// Ambil laporan +$where = []; +$params = []; +if ($filter === 'pending') { $where[] = "status='pending'"; } +if ($filter === 'darurat') { $where[] = "tingkat_urgensi IN ('darurat','tinggi')"; } +if ($filter === 'diproses') { $where[] = "status IN ('diverifikasi','diproses')"; } +if ($filter === 'selesai') { $where[] = "status='selesai'"; } +$whereStr = $where ? 'WHERE '.implode(' AND ',$where) : ''; + +$rows = []; +try { + $rows = $pdo->query(" + SELECT l.*, p.nama_lengkap AS nama_terdampak, u.nama AS nama_verifikator + FROM laporan l + LEFT JOIN penduduk p ON p.nik=l.nik_terdampak + LEFT JOIN users u ON u.id=l.diverifikasi_oleh + $whereStr + ORDER BY CASE l.tingkat_urgensi WHEN 'darurat' THEN 1 WHEN 'tinggi' THEN 2 ELSE 3 END, + CASE l.status WHEN 'pending' THEN 1 WHEN 'diverifikasi' THEN 2 ELSE 3 END, + l.created_at DESC + ")->fetchAll(); +} catch(Exception $e) { $err = $e->getMessage(); } + +render_header('Manajemen Laporan'); +?> + +
🚩 Laporan Masyarakat
+ +
+
+ +
+ 'Semua','pending'=>'Pending','darurat'=>'πŸ”΄ Darurat','diproses'=>'Diproses','selesai'=>'Selesai'] as $k=>$v): ?> + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
#PelaporTerdampakKategoriUrgensiStatusTanggalAksi
+ + + + +
Tidak ada laporan
+
+ + + + + + + + + + diff --git a/webgis-kemiskinan/admin/log.php b/webgis-kemiskinan/admin/log.php new file mode 100644 index 0000000..acca980 --- /dev/null +++ b/webgis-kemiskinan/admin/log.php @@ -0,0 +1,33 @@ +query(" + SELECT l.*, u.nama AS nama_user + FROM log_aktivitas l + LEFT JOIN users u ON u.id=l.user_id + ORDER BY l.created_at DESC + LIMIT 200 +")->fetchAll(); + +render_header('Log Aktivitas'); +?> +
πŸ“œ Log Aktivitas Sistem
+
+ + + + + + + + + + + + + + +
WaktuUserAksiTabelRecordIP
+
+ diff --git a/webgis-kemiskinan/admin/login.php b/webgis-kemiskinan/admin/login.php new file mode 100644 index 0000000..fd4eaa1 --- /dev/null +++ b/webgis-kemiskinan/admin/login.php @@ -0,0 +1,89 @@ + + + + + + Login β€” WebGIS Pengentasan Kemiskinan + + + + 'index.php', + 'pengurus' => 'index.php', + 'pimpinan' => 'index.php', + 'masyarakat' => '../index.html', + ]; + header('Location: ' . ($redirect[$result['role']] ?? 'index.php')); + exit; + } + $error = $result['message']; +} +?> +
+ + + +
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+

Akun Demo (password: password)

+
adminAdministrator
+
pengurus1Pengurus Masjid
+
pimpinan1Pimpinan Daerah
+
warga1Masyarakat
+
+ + +
+ + diff --git a/webgis-kemiskinan/admin/logout.php b/webgis-kemiskinan/admin/logout.php new file mode 100644 index 0000000..8e0c539 --- /dev/null +++ b/webgis-kemiskinan/admin/logout.php @@ -0,0 +1,6 @@ +prepare("DELETE FROM penduduk WHERE nik=:nik")->execute([':nik'=>$nik]); + logAktivitas($pdo, (int)$_SESSION['user_id'], 'DELETE', 'penduduk', $nik); + $msg = "Penduduk NIK $nik dihapus."; + } catch(Exception $e) { $err = $e->getMessage(); } +} + +// SAVE +if ($canEdit && isset($_POST['save'])) { + $nik = trim($_POST['nik'] ?? ''); + $nama = trim($_POST['nama_lengkap'] ?? ''); + $tgl = $_POST['tanggal_lahir'] ?? ''; + $nikEdit = $_POST['nik_edit'] ?? ''; + + if (!preg_match('/^\d{16}$/', $nik)) $err = 'NIK harus 16 digit.'; + elseif (strlen($nama) < 3) $err = 'Nama terlalu pendek.'; + else { + try { + $data = [ + ':kk'=>$_POST['id_keluarga']?:(null), + ':nama'=>$nama, ':jk'=>$_POST['jenis_kelamin']??'L', + ':tgl'=>$tgl, ':sk'=>$_POST['status_keluarga']??'anggota_lain', + ':sp'=>$_POST['status_perkawinan']??'belum_kawin', + ':sh'=>$_POST['status_hidup']??'hidup', + ':ptk'=>$_POST['pendidikan_terakhir']??'tidak_sekolah', + ':spd'=>$_POST['status_pendidikan']??'tidak_sekolah', + ':pkr'=>$_POST['pekerjaan']??'' ?: null, + ':pgn'=>(float)($_POST['penghasilan']??0), + ':se'=>$_POST['status_ekonomi']??'rentan', + ':kkh'=>$_POST['kondisi_kesehatan']??'sehat', + ':ckkh'=>$_POST['catatan_kesehatan']??'' ?: null, + ':tlp'=>$_POST['no_telp']??'' ?: null, + ':almt'=>$_POST['alamat']??'' ?: null, + ':lat'=>$_POST['latitude']??'' ?: null, + ':lng'=>$_POST['longitude']??'' ?: null, + ]; + if ($nikEdit) { + $data[':nik'] = $nikEdit; + $pdo->prepare("UPDATE penduduk SET id_keluarga=:kk,nama_lengkap=:nama,jenis_kelamin=:jk,tanggal_lahir=:tgl, + status_keluarga=:sk,status_perkawinan=:sp,status_hidup=:sh,pendidikan_terakhir=:ptk,status_pendidikan=:spd, + pekerjaan=:pkr,penghasilan=:pgn,status_ekonomi=:se,kondisi_kesehatan=:kkh,catatan_kesehatan=:ckkh, + no_telp=:tlp,alamat=:almt,latitude=:lat,longitude=:lng WHERE nik=:nik")->execute($data); + $msg = "Data penduduk $nama diperbarui."; + } else { + $data[':nik'] = $nik; + $pdo->prepare("INSERT INTO penduduk (nik,id_keluarga,nama_lengkap,jenis_kelamin,tanggal_lahir, + status_keluarga,status_perkawinan,status_hidup,pendidikan_terakhir,status_pendidikan, + pekerjaan,penghasilan,status_ekonomi,kondisi_kesehatan,catatan_kesehatan,no_telp,alamat,latitude,longitude) + VALUES(:nik,:kk,:nama,:jk,:tgl,:sk,:sp,:sh,:ptk,:spd,:pkr,:pgn,:se,:kkh,:ckkh,:tlp,:almt,:lat,:lng)")->execute($data); + $msg = "Penduduk $nama berhasil ditambahkan."; + } + logAktivitas($pdo, (int)$_SESSION['user_id'], $nikEdit?'UPDATE':'CREATE', 'penduduk', $nik); + } catch(Exception $e) { $err = $e->getMessage(); } + } +} + +$edit = null; +if (isset($_GET['edit'])) { + $stmt = $pdo->prepare("SELECT * FROM penduduk WHERE nik=:nik"); + $stmt->execute([':nik'=>$_GET['edit']]); + $edit = $stmt->fetch() ?: null; +} + +// List penduduk +$search = trim($_GET['s'] ?? ''); +$se = $_GET['se'] ?? ''; +$where = []; $params = []; +if ($search) { $where[] = '(p.nama_lengkap LIKE :q OR p.nik LIKE :q2)'; $params[':q']='%'.$search.'%'; $params[':q2']='%'.$search.'%'; } +if ($se) { $where[] = 'p.status_ekonomi=:se'; $params[':se']=$se; } +$whereStr = $where ? 'WHERE '.implode(' AND ',$where) : ''; + +$rows = $pdo->prepare(" + SELECT p.nik,p.nama_lengkap,p.umur,p.jenis_kelamin,p.status_ekonomi,p.kondisi_kesehatan, + p.pekerjaan,p.status_hidup,k.no_kk,ri.nama AS nama_ri, + (SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan') AS jml_bantuan + FROM penduduk p + LEFT JOIN keluarga k ON k.id=p.id_keluarga + LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id + $whereStr + ORDER BY CASE p.status_ekonomi WHEN 'miskin' THEN 1 WHEN 'rentan' THEN 2 ELSE 3 END, p.nama_lengkap + LIMIT 300 +"); +$rows->execute($params); +$rows = $rows->fetchAll(); + +// Keluarga untuk dropdown +$kkList = $pdo->query("SELECT id, no_kk, nama_kk FROM keluarga ORDER BY nama_kk")->fetchAll(); + +render_header('Manajemen Penduduk'); +?> +
πŸ‘₯ Manajemen Penduduk
+ +
+
+ + + +
+ +
+ +
+
+ + > +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + Batal +
+
+
+ + + +
+
+
+ + + + Reset +
+ data +
+ + + + + + + + + + + + + + + + + + + +
NIKNamaUmur/JKStatus EkonomiKesehatanBantuanRumah IbadahAksi
†meninggal':'' ?>th / 0 ? "βœ… {$r['jml_bantuan']}x" : "⚠️ Belum" ?> + Edit + Bantu + + Hapus + +
Tidak ada data
+
+ + + + diff --git a/webgis-kemiskinan/admin/rumah_ibadah.php b/webgis-kemiskinan/admin/rumah_ibadah.php new file mode 100644 index 0000000..ccbe5dc --- /dev/null +++ b/webgis-kemiskinan/admin/rumah_ibadah.php @@ -0,0 +1,141 @@ +prepare("DELETE FROM rumah_ibadah WHERE id=:id")->execute([':id'=>$id]); + logAktivitas($pdo, (int)$_SESSION['user_id'], 'DELETE', 'rumah_ibadah', (string)$id); + $msg = "Rumah ibadah #$id dihapus."; + } catch(Exception $e) { $err = 'Gagal hapus: '.$e->getMessage(); } +} + +// SAVE +if ($canEdit && isset($_POST['save'])) { + $nama = trim($_POST['nama'] ?? ''); + $lat = $_POST['latitude'] ?? ''; + $lng = $_POST['longitude'] ?? ''; + $idEdit = (int)($_POST['id_edit'] ?? 0); + if (strlen($nama) < 3) $err = 'Nama terlalu pendek.'; + elseif (!is_numeric($lat) || !is_numeric($lng)) $err = 'Latitude/Longitude tidak valid.'; + else { + try { + $data = [ + ':nama' => $nama, + ':jenis' => $_POST['jenis']??'Lainnya', + ':kontak'=> $_POST['kontak']??'' ?: null, + ':rad' => (float)($_POST['radius_meter']??300), + ':lat' => (float)$lat, ':lng' => (float)$lng, + ':almt' => $_POST['alamat']??'' ?: null, + ':kel' => $_POST['kelurahan']??'' ?: null, + ':kec' => $_POST['kecamatan']??'' ?: null, + ':kota' => $_POST['kota']??'' ?: null, + ':desk' => $_POST['deskripsi']??'' ?: null, + ]; + if ($idEdit) { + $data[':id'] = $idEdit; + $pdo->prepare("UPDATE rumah_ibadah SET nama=:nama,jenis=:jenis,kontak=:kontak,radius_meter=:rad, + latitude=:lat,longitude=:lng,alamat=:almt,kelurahan=:kel,kecamatan=:kec,kota=:kota,deskripsi=:desk + WHERE id=:id")->execute($data); + $msg = "$nama diperbarui."; + logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'rumah_ibadah', (string)$idEdit); + } else { + $pdo->prepare("INSERT INTO rumah_ibadah (nama,jenis,kontak,radius_meter,latitude,longitude,alamat,kelurahan,kecamatan,kota,deskripsi) + VALUES(:nama,:jenis,:kontak,:rad,:lat,:lng,:almt,:kel,:kec,:kota,:desk)")->execute($data); + $msg = "$nama berhasil ditambahkan."; + logAktivitas($pdo, (int)$_SESSION['user_id'], 'CREATE', 'rumah_ibadah', (string)$pdo->lastInsertId()); + } + } catch(Exception $e) { $err = $e->getMessage(); } + } +} + +$edit = null; +if (isset($_GET['edit'])) { + $s = $pdo->prepare("SELECT * FROM rumah_ibadah WHERE id=:id"); + $s->execute([':id'=>(int)$_GET['edit']]); + $edit = $s->fetch() ?: null; +} + +$rows = $pdo->query(" + SELECT ri.*, + (SELECT COUNT(*) FROM keluarga k WHERE k.rumah_ibadah_id=ri.id) AS jml_kk + FROM rumah_ibadah ri ORDER BY ri.nama +")->fetchAll(); + +$jenisOpts = ['Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya']; + +render_header('Rumah Ibadah'); +?> +
πŸ•Œ Data Rumah Ibadah
+ +
+
+ +
+ +
+

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Batal +
+
+ + +
+ + Aksi':'' ?> + + + + + + + + + + + + + + + +
NamaJenisAlamatRadiusKK

m KK + ✏️ + πŸ—‘οΈ +
Belum ada data
+
+
+ diff --git a/webgis-kemiskinan/admin/users.php b/webgis-kemiskinan/admin/users.php new file mode 100644 index 0000000..66539aa --- /dev/null +++ b/webgis-kemiskinan/admin/users.php @@ -0,0 +1,163 @@ +prepare("UPDATE users SET aktif = 1 - aktif WHERE id=:id")->execute([':id'=>$id]); + logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'users', (string)$id); + $msg = "Status user #$id diperbarui."; + } catch(Exception $e) { $err = $e->getMessage(); } + } else { $err = 'Tidak dapat mengubah status akun sendiri.'; } +} + +// RESET PASSWORD +if (isset($_POST['reset_pw'])) { + $id = (int)($_POST['user_id'] ?? 0); + $pw = $_POST['new_password'] ?? ''; + if (strlen($pw) < 6) $err = 'Password minimal 6 karakter.'; + else { + $hash = password_hash($pw, PASSWORD_BCRYPT); + $pdo->prepare("UPDATE users SET password_hash=:h WHERE id=:id")->execute([':h'=>$hash,':id'=>$id]); + logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'users', (string)$id, null, ['action'=>'reset_password']); + $msg = "Password user #$id berhasil direset."; + } +} + +// SAVE USER BARU +if (isset($_POST['save'])) { + $uname = trim($_POST['username'] ?? ''); + $nama = trim($_POST['nama'] ?? ''); + $pw = $_POST['password'] ?? ''; + $idEdit = (int)($_POST['id_edit'] ?? 0); + if (strlen($uname) < 3) $err = 'Username minimal 3 karakter.'; + elseif (strlen($nama) < 3) $err = 'Nama minimal 3 karakter.'; + elseif (!$idEdit && strlen($pw) < 6) $err = 'Password minimal 6 karakter.'; + else { + try { + if ($idEdit) { + $pdo->prepare("UPDATE users SET nama=:nama,role_id=:rid,email=:em,no_telp=:tlp,rumah_ibadah_id=:ri WHERE id=:id") + ->execute([':nama'=>$nama,':rid'=>(int)$_POST['role_id'],':em'=>$_POST['email']??'' ?: null, + ':tlp'=>$_POST['no_telp']??'' ?: null,':ri'=>$_POST['rumah_ibadah_id']??'' ?: null,':id'=>$idEdit]); + $msg = "User $nama diperbarui."; + logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'users', (string)$idEdit); + } else { + $hash = password_hash($pw, PASSWORD_BCRYPT); + $pdo->prepare("INSERT INTO users (role_id,nama,username,email,password_hash,no_telp,rumah_ibadah_id) + VALUES(:rid,:nama,:uname,:em,:hash,:tlp,:ri)") + ->execute([':rid'=>(int)$_POST['role_id'],':nama'=>$nama,':uname'=>$uname, + ':em'=>$_POST['email']??'' ?: null,':hash'=>$hash, + ':tlp'=>$_POST['no_telp']??'' ?: null,':ri'=>$_POST['rumah_ibadah_id']??'' ?: null]); + $msg = "User $uname berhasil dibuat."; + logAktivitas($pdo, (int)$_SESSION['user_id'], 'CREATE', 'users', (string)$pdo->lastInsertId()); + } + } catch(Exception $e) { $err = $e->getMessage(); } + } +} + +$edit = null; +if (isset($_GET['edit'])) { + $s = $pdo->prepare("SELECT * FROM users WHERE id=:id"); + $s->execute([':id'=>(int)$_GET['edit']]); + $edit = $s->fetch() ?: null; +} + +$rows = $pdo->query(" + SELECT u.*, r.nama AS role_nama, ri.nama AS nama_ri + FROM users u + JOIN roles r ON r.id=u.role_id + LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id + ORDER BY u.role_id, u.nama +")->fetchAll(); + +$roles = $pdo->query("SELECT * FROM roles ORDER BY id")->fetchAll(); +$riList = $pdo->query("SELECT id,nama FROM rumah_ibadah ORDER BY nama")->fetchAll(); + +render_header('Manajemen User'); +?> +
πŸ‘₯ Manajemen User
+ +
+
+ +
+
+
+

+
+ +
+ >
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + Batal +
+
+ + +
+ πŸ”‘ Reset Password

+
+ +
+
+ +
+
+ +
+ +
+ + + + + > + + + + + + + + + +
NamaUsernameRoleRumah IbadahStatusAksi

Aktif' : 'Nonaktif' ?> + ✏️ + + + + + +
+
+
+ diff --git a/webgis-kemiskinan/index.html b/webgis-kemiskinan/index.html new file mode 100644 index 0000000..f6f7313 --- /dev/null +++ b/webgis-kemiskinan/index.html @@ -0,0 +1,1849 @@ + + + + + + WebGIS Pengentasan Kemiskinan β€” Berbasis Masyarakat & Rumah Ibadah + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ + + + + + + + + + + + + +
+ + + + + + + + + + diff --git a/webgis-kemiskinan/php/api/auth.php b/webgis-kemiskinan/php/api/auth.php new file mode 100644 index 0000000..1a5c719 --- /dev/null +++ b/webgis-kemiskinan/php/api/auth.php @@ -0,0 +1,54 @@ + $result['role'], + 'user_id' => $_SESSION['user_id'], + 'nama' => $_SESSION['user_nama'], + ]); +} + +if ($method === 'POST' && $action === 'logout') { + doLogout($pdo); + jsonResponse(true, 'Logout berhasil.'); +} + +if ($method === 'GET' && $action === 'me') { + $user = getApiUser($pdo); + if (!$user) jsonResponse(false, 'Belum login.', [], 401); + $detail = $pdo->prepare("SELECT u.id, u.nama, u.username, u.email, r.nama AS role, u.rumah_ibadah_id, ri.nama AS nama_ri FROM users u JOIN roles r ON r.id=u.role_id LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id WHERE u.id=:id"); + $detail->execute([':id' => $user['id']]); + jsonResponse(true, 'OK', ['data' => $detail->fetch()]); +} + +jsonResponse(false, 'Action tidak dikenal.', [], 400); diff --git a/webgis-kemiskinan/php/api/bantuan.php b/webgis-kemiskinan/php/api/bantuan.php new file mode 100644 index 0000000..edc00bd --- /dev/null +++ b/webgis-kemiskinan/php/api/bantuan.php @@ -0,0 +1,239 @@ +query("SELECT * FROM jenis_bantuan WHERE aktif=1 ORDER BY kategori, nama")->fetchAll(); + jsonResponse(true, 'OK', ['data' => $rows]); + } + + if ($action === 'histori') { + $nik = sanitizeStr($_GET['nik'] ?? ''); + + if ($nik && validateNIK($nik)) { + // Histori per penduduk + $stmt = $pdo->prepare(" + SELECT hb.*, jb.nama AS nama_bantuan, jb.kategori, jb.satuan, + ri.nama AS nama_rumah_ibadah, u.nama AS disalurkan_oleh_nama + FROM histori_bantuan hb + JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan + LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id + LEFT JOIN users u ON u.id = hb.disalurkan_oleh + WHERE hb.nik = :nik + ORDER BY hb.tanggal DESC + "); + $stmt->execute([':nik' => $nik]); + $data = $stmt->fetchAll(); + + // Ringkasan + $summary = [ + 'total_bantuan' => count($data), + 'kategori' => [], + ]; + foreach ($data as $row) { + $summary['kategori'][$row['kategori']] = ($summary['kategori'][$row['kategori']] ?? 0) + 1; + } + + jsonResponse(true, 'OK', ['data' => $data, 'summary' => $summary]); + } + + // List semua histori (admin/pengurus) + if (!canViewAll($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + + $where = []; + $params = []; + if (!empty($_GET['bulan'])) { $where[] = 'MONTH(hb.tanggal) = :bln'; $params[':bln'] = (int)$_GET['bulan']; } + if (!empty($_GET['tahun'])) { $where[] = 'YEAR(hb.tanggal) = :thn'; $params[':thn'] = (int)$_GET['tahun']; } + if (!empty($_GET['kategori'])) { $where[] = 'jb.kategori = :kat'; $params[':kat'] = $_GET['kategori']; } + + $whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : ''; + + $stmt = $pdo->prepare(" + SELECT hb.id, hb.nik, p.nama_lengkap, hb.tanggal, + jb.nama AS nama_bantuan, jb.kategori, + hb.jumlah, hb.nilai_rupiah, hb.status_penyaluran, + ri.nama AS nama_ri + FROM histori_bantuan hb + JOIN penduduk p ON p.nik = hb.nik + JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan + LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id + $whereStr + ORDER BY hb.tanggal DESC + LIMIT 1000 + "); + $stmt->execute($params); + jsonResponse(true, 'OK', ['data' => $stmt->fetchAll()]); + } + + if ($action === 'belum_dibantu') { + // Warga miskin yang BELUM pernah menerima bantuan + $rows = $pdo->query(" + SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan, + p.pekerjaan, p.alamat, p.latitude, p.longitude, + k.nama_kk, k.no_kk, ri.nama AS nama_ri + FROM penduduk p + LEFT JOIN keluarga k ON k.id = p.id_keluarga + LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id + WHERE p.status_hidup = 'hidup' + AND p.status_ekonomi IN ('miskin', 'rentan') + AND NOT EXISTS ( + SELECT 1 FROM histori_bantuan hb + WHERE hb.nik = p.nik AND hb.status_penyaluran = 'disalurkan' + ) + ORDER BY + CASE p.status_ekonomi WHEN 'miskin' THEN 1 ELSE 2 END, + CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 WHEN 'disabilitas' THEN 2 ELSE 3 END + ")->fetchAll(); + + jsonResponse(true, 'OK', ['data' => $rows, 'total' => count($rows)]); + } + + if ($action === 'dashboard_stats') { + // Statistik untuk dashboard + $stats = $pdo->query(" + SELECT + (SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='miskin' AND status_hidup='hidup') AS total_miskin, + (SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='rentan' AND status_hidup='hidup') AS total_rentan, + (SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='mampu' AND status_hidup='hidup') AS total_mampu, + (SELECT COUNT(*) FROM keluarga) AS total_keluarga, + (SELECT COUNT(*) FROM rumah_ibadah) AS total_rumah_ibadah, + (SELECT COUNT(*) FROM laporan WHERE status='pending') AS laporan_pending, + (SELECT COUNT(*) FROM laporan WHERE status='diproses') AS laporan_diproses, + (SELECT COUNT(*) FROM histori_bantuan WHERE MONTH(tanggal)=MONTH(CURDATE()) AND status_penyaluran='disalurkan') AS bantuan_bulan_ini, + (SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup' AND status_ekonomi IN ('miskin','rentan') + AND NOT EXISTS (SELECT 1 FROM histori_bantuan hb WHERE hb.nik=penduduk.nik AND hb.status_penyaluran='disalurkan') + ) AS belum_pernah_dibantu + ")->fetch(); + + // Bantuan per kategori bulan ini + $per_kategori = $pdo->query(" + SELECT jb.kategori, COUNT(*) AS jumlah + FROM histori_bantuan hb + JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan + WHERE MONTH(hb.tanggal) = MONTH(CURDATE()) + AND hb.status_penyaluran = 'disalurkan' + GROUP BY jb.kategori + ")->fetchAll(); + + jsonResponse(true, 'OK', ['data' => $stats, 'per_kategori' => $per_kategori]); + } + + jsonResponse(false, 'Action tidak dikenal.', [], 400); +} + +// ============================================================ +// POST: salurkan bantuan +// ============================================================ +if ($method === 'POST' && $action === 'salurkan') { + if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + + $body = json_decode(file_get_contents('php://input'), true) ?? $_POST; + + $nik = sanitizeStr($body['nik'] ?? ''); + $idJenis = (int)($body['id_jenis_bantuan'] ?? 0); + $tanggal = sanitizeStr($body['tanggal'] ?? date('Y-m-d')); + $jumlah = sanitizeStr($body['jumlah'] ?? ''); + $status = sanitizeStr($body['status_penyaluran'] ?? 'dijadwalkan'); + + if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400); + if ($idJenis <= 0) jsonResponse(false, 'Jenis bantuan harus dipilih.', [], 400); + if (!strtotime($tanggal)) jsonResponse(false, 'Format tanggal tidak valid.', [], 400); + + // Cek penduduk ada + $cek = $pdo->prepare("SELECT nik, id_keluarga FROM penduduk WHERE nik = :nik AND status_hidup='hidup'"); + $cek->execute([':nik' => $nik]); + $pddk = $cek->fetch(); + if (!$pddk) jsonResponse(false, 'Penduduk tidak ditemukan atau sudah meninggal.', [], 404); + + // Cek jenis bantuan ada + $cekJb = $pdo->prepare("SELECT id FROM jenis_bantuan WHERE id = :id AND aktif=1"); + $cekJb->execute([':id' => $idJenis]); + if (!$cekJb->fetch()) jsonResponse(false, 'Jenis bantuan tidak ditemukan.', [], 404); + + $buktiNama = null; + if (!empty($_FILES['bukti']['name'])) { + try { $buktiNama = uploadFile($_FILES['bukti'], UPLOAD_BUKTI); } + catch (RuntimeException $e) { jsonResponse(false, $e->getMessage(), [], 400); } + } + + $stmt = $pdo->prepare(" + INSERT INTO histori_bantuan + (nik, id_jenis_bantuan, id_keluarga, rumah_ibadah_id, tanggal, + jumlah, nilai_rupiah, status_penyaluran, catatan, bukti_file, disalurkan_oleh) + VALUES + (:nik,:jb,:kk,:ri,:tgl,:jml,:rp,:status,:cat,:bukti,:user) + "); + $stmt->execute([ + ':nik' => $nik, + ':jb' => $idJenis, + ':kk' => $pddk['id_keluarga'], + ':ri' => $currentUser['rumah_ibadah_id'], + ':tgl' => $tanggal, + ':jml' => $jumlah ?: null, + ':rp' => !empty($body['nilai_rupiah']) ? (float)$body['nilai_rupiah'] : null, + ':status' => in_array($status, ['dijadwalkan','disalurkan','dibatalkan']) ? $status : 'dijadwalkan', + ':cat' => sanitizeStr($body['catatan'] ?? '') ?: null, + ':bukti' => $buktiNama, + ':user' => $currentUser['id'], + ]); + + $hbId = (int)$pdo->lastInsertId(); + logAktivitas($pdo, $currentUser['id'], 'CREATE', 'histori_bantuan', (string)$hbId, null, $body); + + // Notifikasi ke pimpinan + $pimpinan = $pdo->query("SELECT id FROM users WHERE role_id=3")->fetchAll(); + foreach ($pimpinan as $pm) { + $pdo->prepare("INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id) VALUES (:u,:j,:i,'bantuan',:r)") + ->execute([':u' => $pm['id'], ':j' => 'Bantuan Disalurkan', ':i' => "Bantuan untuk NIK $nik telah dicatat.", ':r' => $hbId]); + } + + jsonResponse(true, 'Bantuan berhasil dicatat.', ['id' => $hbId], 201); +} + +// ============================================================ +// PUT: update status penyaluran +// ============================================================ +if ($method === 'PUT') { + if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + + $id = (int)($_GET['id'] ?? 0); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + $status = sanitizeStr($body['status_penyaluran'] ?? ''); + + if (!in_array($status, ['dijadwalkan','disalurkan','dibatalkan'])) { + jsonResponse(false, 'Status tidak valid.', [], 400); + } + + $old = $pdo->prepare("SELECT * FROM histori_bantuan WHERE id = :id"); + $old->execute([':id' => $id]); + if (!$old->fetch()) jsonResponse(false, 'Data tidak ditemukan.', [], 404); + + $pdo->prepare("UPDATE histori_bantuan SET status_penyaluran=:s WHERE id=:id") + ->execute([':s' => $status, ':id' => $id]); + + logAktivitas($pdo, $currentUser['id'], 'UPDATE', 'histori_bantuan', (string)$id); + jsonResponse(true, 'Status penyaluran diperbarui.', ['id' => $id]); +} + +jsonResponse(false, 'Method tidak didukung.', [], 405); diff --git a/webgis-kemiskinan/php/api/keluarga.php b/webgis-kemiskinan/php/api/keluarga.php new file mode 100644 index 0000000..d77038c --- /dev/null +++ b/webgis-kemiskinan/php/api/keluarga.php @@ -0,0 +1,183 @@ +prepare(" + SELECT k.*, + ri.nama AS nama_ri, ri.jenis AS jenis_ri, + COUNT(p.nik) AS jumlah_anggota_db, + SUM(CASE WHEN p.kondisi_kesehatan IN ('sakit_parah','disabilitas') THEN 1 ELSE 0 END) AS anggota_sakit + FROM keluarga k + LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id + LEFT JOIN penduduk p ON p.id_keluarga = k.id AND p.status_hidup='hidup' + $whereStr + GROUP BY k.id + ORDER BY CASE k.status_ekonomi WHEN 'miskin' THEN 1 WHEN 'rentan' THEN 2 ELSE 3 END ASC + "); + $rows->execute($params); + jsonResponse(true, 'OK', ['data' => $rows->fetchAll()]); + } + + if ($action === 'detail') { + $id = (int)($_GET['id'] ?? 0); + $stmt = $pdo->prepare("SELECT k.*, ri.nama AS nama_ri FROM keluarga k LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id WHERE k.id=:id"); + $stmt->execute([':id' => $id]); + $kk = $stmt->fetch(); + if (!$kk) jsonResponse(false, 'Keluarga tidak ditemukan.', [], 404); + + $anggota = $pdo->prepare("SELECT * FROM penduduk WHERE id_keluarga=:id ORDER BY CASE status_keluarga WHEN 'kepala_keluarga' THEN 1 ELSE 2 END, tanggal_lahir"); + $anggota->execute([':id' => $id]); + + $histori = $pdo->prepare(" + SELECT hb.tanggal, jb.nama AS bantuan, hb.jumlah, hb.status_penyaluran, p.nama_lengkap + FROM histori_bantuan hb + JOIN penduduk p ON p.nik=hb.nik + JOIN jenis_bantuan jb ON jb.id=hb.id_jenis_bantuan + WHERE hb.id_keluarga=:id + ORDER BY hb.tanggal DESC + LIMIT 50 + "); + $histori->execute([':id' => $id]); + + jsonResponse(true, 'OK', ['data' => $kk, 'anggota' => $anggota->fetchAll(), 'histori' => $histori->fetchAll()]); + } + + if ($action === 'geojson') { + $stmt = $pdo->query(" + SELECT k.id, k.no_kk, k.nama_kk, k.status_ekonomi, k.latitude, k.longitude, + COUNT(p.nik) AS jml_anggota, ri.nama AS nama_ri + FROM keluarga k + LEFT JOIN penduduk p ON p.id_keluarga=k.id AND p.status_hidup='hidup' + LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id + WHERE k.latitude IS NOT NULL AND k.longitude IS NOT NULL + GROUP BY k.id + "); + $rows = $stmt->fetchAll(); + $features = []; + foreach ($rows as $r) { + $features[] = [ + 'type' => 'Feature', + 'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['longitude'],(float)$r['latitude']]], + 'properties' => array_merge($r, [ + 'color' => match($r['status_ekonomi']){'miskin'=>'#ef4444','rentan'=>'#f59e0b',default=>'#22c55e'}, + 'layer' => 'keluarga' + ]) + ]; + } + header('Content-Type: application/json'); + echo json_encode(['type'=>'FeatureCollection','features'=>$features], JSON_UNESCAPED_UNICODE); + exit; + } + + jsonResponse(false, 'Action tidak dikenal.', [], 400); +} + +if ($method === 'POST') { + if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + $body = json_decode(file_get_contents('php://input'), true) ?? $_POST; + + $noKk = sanitizeStr($body['no_kk'] ?? ''); + $namaKk = sanitizeStr($body['nama_kk'] ?? ''); + if (strlen($noKk) !== 16) jsonResponse(false, 'No KK harus 16 digit.', [], 400); + if (strlen($namaKk) < 3) jsonResponse(false, 'Nama KK terlalu pendek.', [], 400); + + $cek = $pdo->prepare("SELECT id FROM keluarga WHERE no_kk=:nkk"); + $cek->execute([':nkk' => $noKk]); + if ($cek->fetch()) jsonResponse(false, 'No KK sudah terdaftar.', [], 409); + + $stmt = $pdo->prepare(" + INSERT INTO keluarga (no_kk, nama_kk, alamat, kelurahan, kecamatan, kota, latitude, longitude, status_ekonomi, rumah_ibadah_id) + VALUES (:nkk,:nkk_nama,:almt,:kel,:kec,:kota,:lat,:lng,:se,:ri) + "); + $stmt->execute([ + ':nkk' => $noKk, + ':nkk_nama' => $namaKk, + ':almt' => sanitizeStr($body['alamat'] ?? '') ?: null, + ':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null, + ':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null, + ':kota' => sanitizeStr($body['kota'] ?? '') ?: null, + ':lat' => !empty($body['latitude']) ? (float)$body['latitude'] : null, + ':lng' => !empty($body['longitude']) ? (float)$body['longitude'] : null, + ':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'), + ':ri' => !empty($body['rumah_ibadah_id']) ? (int)$body['rumah_ibadah_id'] : null, + ]); + $newId = (int)$pdo->lastInsertId(); + logAktivitas($pdo, $currentUser['id']??null, 'CREATE', 'keluarga', (string)$newId, null, $body); + jsonResponse(true, 'Keluarga berhasil ditambahkan.', ['id' => $newId], 201); +} + +if ($method === 'PUT') { + if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + $id = (int)($_GET['id'] ?? 0); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + if ($id <= 0) jsonResponse(false, 'ID tidak valid.', [], 400); + + if ($action === 'update_koordinat') { + $lat = (float)($body['latitude'] ?? 0); + $lng = (float)($body['longitude'] ?? 0); + if (!$lat || !$lng) jsonResponse(false, 'Koordinat tidak valid.', [], 400); + + $pdo->prepare("UPDATE keluarga SET latitude=:lat, longitude=:lng WHERE id=:id") + ->execute([':lat'=>$lat, ':lng'=>$lng, ':id'=>$id]); + logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'keluarga', (string)$id, null, ['lat'=>$lat,'lng'=>$lng]); + jsonResponse(true, 'Koordinat diperbarui.', ['id' => $id]); + } + + $pdo->prepare(" + UPDATE keluarga SET + nama_kk=:nkk, alamat=:almt, kelurahan=:kel, kecamatan=:kec, + kota=:kota, latitude=:lat, longitude=:lng, + status_ekonomi=:se, rumah_ibadah_id=:ri + WHERE id=:id + ")->execute([ + ':nkk' => sanitizeStr($body['nama_kk'] ?? ''), + ':almt' => sanitizeStr($body['alamat'] ?? '') ?: null, + ':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null, + ':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null, + ':kota' => sanitizeStr($body['kota'] ?? '') ?: null, + ':lat' => !empty($body['latitude']) ? (float)$body['latitude'] : null, + ':lng' => !empty($body['longitude']) ? (float)$body['longitude'] : null, + ':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'), + ':ri' => !empty($body['rumah_ibadah_id']) ? (int)$body['rumah_ibadah_id'] : null, + ':id' => $id, + ]); + logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'keluarga', (string)$id); + jsonResponse(true, 'Keluarga diperbarui.', ['id' => $id]); +} + +if ($method === 'DELETE') { + if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin yang bisa menghapus keluarga.', [], 403); + $id = (int)($_GET['id'] ?? 0); + // Cek apakah masih ada anggota + $jml = (int)$pdo->prepare("SELECT COUNT(*) FROM penduduk WHERE id_keluarga=:id")->execute([':id'=>$id]); + if ($jml > 0) jsonResponse(false, 'Tidak bisa dihapus: masih ada anggota keluarga.', [], 409); + $pdo->prepare("DELETE FROM keluarga WHERE id=:id")->execute([':id' => $id]); + logAktivitas($pdo, $currentUser['id']??null, 'DELETE', 'keluarga', (string)$id); + jsonResponse(true, 'Keluarga dihapus.'); +} + +jsonResponse(false, 'Method tidak didukung.', [], 405); diff --git a/webgis-kemiskinan/php/api/laporan.php b/webgis-kemiskinan/php/api/laporan.php new file mode 100644 index 0000000..a9092a0 --- /dev/null +++ b/webgis-kemiskinan/php/api/laporan.php @@ -0,0 +1,255 @@ +prepare(" + SELECT l.*, + p.nama_lengkap AS nama_terdampak, + u.nama AS nama_verifikator + FROM laporan l + LEFT JOIN penduduk p ON p.nik = l.nik_terdampak + LEFT JOIN users u ON u.id = l.diverifikasi_oleh + $whereStr + ORDER BY + CASE l.tingkat_urgensi WHEN 'darurat' THEN 1 WHEN 'tinggi' THEN 2 WHEN 'sedang' THEN 3 ELSE 4 END ASC, + CASE l.status WHEN 'pending' THEN 1 WHEN 'diverifikasi' THEN 2 ELSE 3 END ASC, + l.created_at DESC + LIMIT 500 + "); + $rows->execute($params); + jsonResponse(true, 'OK', ['data' => $rows->fetchAll(), 'total' => $rows->rowCount()]); + } + + if ($action === 'detail') { + $id = (int)($_GET['id'] ?? 0); + $stmt = $pdo->prepare(" + SELECT l.*, p.nama_lengkap AS nama_terdampak, p.status_ekonomi, + p.kondisi_kesehatan, p.alamat AS alamat_penduduk + FROM laporan l + LEFT JOIN penduduk p ON p.nik = l.nik_terdampak + WHERE l.id = :id + "); + $stmt->execute([':id' => $id]); + $data = $stmt->fetch(); + if (!$data) jsonResponse(false, 'Laporan tidak ditemukan.', [], 404); + jsonResponse(true, 'OK', ['data' => $data]); + } + + if ($action === 'geojson') { + // Untuk layer marker laporan di peta + $stmt = $pdo->query(" + SELECT id, nama_pelapor, pelapor_id, kategori, tingkat_urgensi, status, + latitude, longitude, alamat, created_at + FROM laporan + WHERE latitude IS NOT NULL AND longitude IS NOT NULL + AND status NOT IN ('selesai','ditolak') + "); + $rows = $stmt->fetchAll(); + + $features = []; + foreach ($rows as $r) { + $color = match($r['tingkat_urgensi']) { + 'darurat' => '#dc2626', + 'tinggi' => '#ea580c', + 'sedang' => '#ca8a04', + default => '#2563eb', + }; + $features[] = [ + 'type' => 'Feature', + 'geometry' => ['type' => 'Point', 'coordinates' => [(float)$r['longitude'], (float)$r['latitude']]], + 'properties' => array_merge($r, ['color' => $color, 'layer' => 'laporan']) + ]; + } + header('Content-Type: application/json'); + echo json_encode(['type' => 'FeatureCollection', 'features' => $features], JSON_UNESCAPED_UNICODE); + exit; + } + + if ($action === 'stats') { + $stats = $pdo->query(" + SELECT + SUM(status='pending') AS pending, + SUM(status='diverifikasi') AS diverifikasi, + SUM(status='diproses') AS diproses, + SUM(status='selesai') AS selesai, + SUM(tingkat_urgensi='darurat') AS darurat, + COUNT(*) AS total + FROM laporan + ")->fetch(); + jsonResponse(true, 'OK', ['data' => $stats]); + } + + jsonResponse(false, 'Action tidak dikenal.', [], 400); +} + +// ============================================================ +// POST: buat laporan baru (bisa dilakukan masyarakat umum) +// ============================================================ +if ($method === 'POST') { + $deskripsi = sanitizeStr($_POST['deskripsi'] ?? '', 2000); + $kategori = sanitizeStr($_POST['kategori'] ?? 'butuh_bantuan_lain'); + $urgensi = sanitizeStr($_POST['tingkat_urgensi'] ?? 'sedang'); + $lat = !empty($_POST['latitude']) ? (float)$_POST['latitude'] : null; + $lng = !empty($_POST['longitude']) ? (float)$_POST['longitude'] : null; + + if (strlen($deskripsi) < 10) jsonResponse(false, 'Deskripsi laporan terlalu singkat.', [], 400); + + $fotoNama = null; + if (!empty($_FILES['foto']['name'])) { + try { + $fotoNama = uploadFile($_FILES['foto'], UPLOAD_LAPORAN, ALLOWED_IMG_EXT); + } catch (RuntimeException $e) { + jsonResponse(false, $e->getMessage(), [], 400); + } + } + + $stmt = $pdo->prepare(" + INSERT INTO laporan + (nama_pelapor, pelapor_id, nik_terdampak, deskripsi, kategori, tingkat_urgensi, + latitude, longitude, alamat, foto, status) + VALUES + (:np, :pid, :nik, :dsk, :kat, :urg, :lat, :lng, :almt, :foto, 'pending') + "); + $stmt->execute([ + ':np' => sanitizeStr($_POST['nama_pelapor'] ?? '') ?: null, + ':pid' => $currentUser['id'] ?? null, + ':nik' => sanitizeStr($_POST['nik_terdampak'] ?? '') ?: null, + ':dsk' => $deskripsi, + ':kat' => $kategori, + ':urg' => $urgensi, + ':lat' => $lat, + ':lng' => $lng, + ':almt' => sanitizeStr($_POST['alamat'] ?? '') ?: null, + ':foto' => $fotoNama, + ]); + + $laporanId = (int)$pdo->lastInsertId(); + + // Kirim notifikasi ke pengurus jika darurat + if (in_array($urgensi, ['darurat', 'tinggi'])) { + $pengurus = $pdo->query("SELECT id FROM users WHERE role_id = 2")->fetchAll(); + foreach ($pengurus as $p) { + $pdo->prepare(" + INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id) + VALUES (:uid, :judul, :isi, 'laporan', :ref) + ")->execute([ + ':uid' => $p['id'], + ':judul' => '🚨 Laporan ' . strtoupper($urgensi) . ' masuk!', + ':isi' => "Ada laporan baru kategori '$kategori'. Segera ditangani.", + ':ref' => $laporanId, + ]); + } + } + + logAktivitas($pdo, $currentUser['id'] ?? null, 'LAPORAN', 'laporan', (string)$laporanId); + + jsonResponse(true, 'Laporan berhasil dikirim. Terima kasih!', ['id' => $laporanId], 201); +} + +// ============================================================ +// PUT: Update status laporan (hanya pengurus/admin) +// ============================================================ +if ($method === 'PUT') { + if (!canVerify($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + + $id = (int)($_GET['id'] ?? 0); + if ($id <= 0) jsonResponse(false, 'ID tidak valid.', [], 400); + + $body = json_decode(file_get_contents('php://input'), true) ?? []; + $status = sanitizeStr($body['status'] ?? ''); + $valid = ['diverifikasi', 'diproses', 'selesai', 'ditolak']; + + if (!in_array($status, $valid)) jsonResponse(false, 'Status tidak valid.', [], 400); + + $old = $pdo->prepare("SELECT * FROM laporan WHERE id = :id"); + $old->execute([':id' => $id]); + $dataLama = $old->fetch(); + if (!$dataLama) jsonResponse(false, 'Laporan tidak ditemukan.', [], 404); + + $sets = ['status = :status', 'updated_at = NOW()']; + $params = [':id' => $id, ':status' => $status]; + + if ($status === 'diverifikasi' && $currentUser) { + $sets[] = 'diverifikasi_oleh = :verif'; + $params[':verif'] = $currentUser['id']; + } + if ($status === 'diproses' && $currentUser) { + $sets[] = 'ditangani_oleh = :tangani'; + $params[':tangani'] = $currentUser['id']; + } + if ($status === 'selesai') { + $sets[] = 'tanggal_selesai = NOW()'; + } + if (!empty($body['catatan_verifikasi'])) { + $sets[] = 'catatan_verifikasi = :catatan'; + $params[':catatan'] = sanitizeStr($body['catatan_verifikasi'], 1000); + } + + $pdo->prepare("UPDATE laporan SET " . implode(', ', $sets) . " WHERE id = :id")->execute($params); + logAktivitas($pdo, $currentUser['id'], 'UPDATE', 'laporan', (string)$id, $dataLama, $body); + + jsonResponse(true, "Status laporan diperbarui menjadi '$status'.", ['id' => $id]); +} + +// ============================================================ +// PUT: Update koordinat (via drag map) +// ============================================================ +if ($method === 'PUT' && $action === 'update_koordinat') { + if (!canVerify($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + + $id = (int)($_GET['id'] ?? 0); + $body = json_decode(file_get_contents('php://input'), true); + + $lat = isset($body['latitude']) ? (float)$body['latitude'] : null; + $lng = isset($body['longitude']) ? (float)$body['longitude'] : null; + + if (!$lat || !$lng) jsonResponse(false, 'Latitude dan Longitude wajib diisi.', [], 400); + + $pdo->prepare("UPDATE laporan SET latitude = :lat, longitude = :lng, updated_at = NOW() WHERE id = :id")->execute([ + ':lat' => $lat, + ':lng' => $lng, + ':id' => $id + ]); + + logAktivitas($pdo, $currentUser['id'], 'UPDATE_KOORDINAT', 'laporan', (string)$id); + jsonResponse(true, 'Koordinat laporan berhasil diperbarui.', ['id' => $id]); +} + +jsonResponse(false, 'Method tidak didukung.', [], 405); diff --git a/webgis-kemiskinan/php/api/penduduk.php b/webgis-kemiskinan/php/api/penduduk.php new file mode 100644 index 0000000..18bdf90 --- /dev/null +++ b/webgis-kemiskinan/php/api/penduduk.php @@ -0,0 +1,328 @@ +prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + + // Hitung prioritas untuk setiap penduduk + foreach ($rows as &$r) { + $belumDibantu = (int)$r['jumlah_bantuan_diterima'] === 0; + $r['prioritas_bantuan'] = hitungPrioritasBantuan($r['status_ekonomi'], $r['kondisi_kesehatan'], $belumDibantu); + } + unset($r); + + jsonResponse(true, 'OK', ['data' => $rows, 'total' => count($rows)]); + } + + if ($action === 'detail') { + $nik = sanitizeStr($_GET['nik'] ?? ''); + if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400); + + $stmt = $pdo->prepare(" + SELECT p.*, k.no_kk, k.nama_kk, ri.nama AS nama_ri + FROM penduduk p + LEFT JOIN keluarga k ON k.id = p.id_keluarga + LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id + WHERE p.nik = :nik + "); + $stmt->execute([':nik' => $nik]); + $penduduk = $stmt->fetch(); + if (!$penduduk) jsonResponse(false, 'Data tidak ditemukan.', [], 404); + + // Histori bantuan + $stmtHB = $pdo->prepare(" + SELECT hb.*, jb.nama AS nama_bantuan, jb.kategori, ri.nama AS nama_ri + FROM histori_bantuan hb + JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan + LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id + WHERE hb.nik = :nik + ORDER BY hb.tanggal DESC + "); + $stmtHB->execute([':nik' => $nik]); + $histori = $stmtHB->fetchAll(); + + // Pelatihan + $stmtPl = $pdo->prepare("SELECT * FROM riwayat_pelatihan WHERE nik = :nik ORDER BY tanggal_mulai DESC"); + $stmtPl->execute([':nik' => $nik]); + $pelatihan = $stmtPl->fetchAll(); + + // Validasi pendidikan + $validasi = validasiPendidikanUmur( + (int)$penduduk['umur'], + $penduduk['pendidikan_terakhir'], + $penduduk['status_pendidikan'] + ); + + $belumDibantu = empty($histori); + $penduduk['prioritas_bantuan'] = hitungPrioritasBantuan($penduduk['status_ekonomi'], $penduduk['kondisi_kesehatan'], $belumDibantu); + + jsonResponse(true, 'OK', [ + 'data' => $penduduk, + 'histori' => $histori, + 'pelatihan' => $pelatihan, + 'validasi' => $validasi, + ]); + } + + if ($action === 'geojson') { + // Untuk layer Leaflet + $stmt = $pdo->query(" + SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan, + p.latitude, p.longitude, + k.nama_kk, + (SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan') AS jml_bantuan + FROM penduduk p + LEFT JOIN keluarga k ON k.id = p.id_keluarga + WHERE p.latitude IS NOT NULL AND p.longitude IS NOT NULL + AND p.status_hidup = 'hidup' + "); + $rows = $stmt->fetchAll(); + + $features = []; + foreach ($rows as $r) { + $color = match($r['status_ekonomi']) { + 'miskin' => '#ef4444', + 'rentan' => '#f59e0b', + default => '#22c55e' + }; + $features[] = [ + 'type' => 'Feature', + 'geometry' => [ + 'type' => 'Point', + 'coordinates' => [(float)$r['longitude'], (float)$r['latitude']] + ], + 'properties' => array_merge($r, ['color' => $color]) + ]; + } + + header('Content-Type: application/json'); + echo json_encode(['type' => 'FeatureCollection', 'features' => $features], JSON_UNESCAPED_UNICODE); + exit; + } + + jsonResponse(false, 'Action tidak dikenal.', [], 400); +} + +// ============================================================ +// POST - Create +// ============================================================ +if ($method === 'POST') { + if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + + $body = json_decode(file_get_contents('php://input'), true) ?? $_POST; + + $nik = sanitizeStr($body['nik'] ?? ''); + $nama = sanitizeStr($body['nama_lengkap'] ?? ''); + $jk = sanitizeStr($body['jenis_kelamin'] ?? ''); + $tgl = sanitizeStr($body['tanggal_lahir'] ?? ''); + $idKk = !empty($body['id_keluarga']) ? (int)$body['id_keluarga'] : null; + + if (!validateNIK($nik)) jsonResponse(false, 'Format NIK harus 16 digit angka.', [], 400); + if (strlen($nama) < 3) jsonResponse(false, 'Nama terlalu pendek.', [], 400); + if (!in_array($jk, ['L','P'])) jsonResponse(false, 'Jenis kelamin tidak valid.', [], 400); + if (!strtotime($tgl)) jsonResponse(false, 'Format tanggal lahir tidak valid.', [], 400); + + // Cek duplikat NIK + $cek = $pdo->prepare("SELECT nik FROM penduduk WHERE nik = :nik"); + $cek->execute([':nik' => $nik]); + if ($cek->fetch()) jsonResponse(false, 'NIK sudah terdaftar.', [], 409); + + // Hitung umur untuk validasi pendidikan + $umur = (int)(new DateTime())->diff(new DateTime($tgl))->y; + $statusPendidikan = sanitizeStr($body['status_pendidikan'] ?? 'tidak_sekolah'); + $pendidikan = sanitizeStr($body['pendidikan_terakhir'] ?? 'tidak_sekolah'); + $validasi = validasiPendidikanUmur($umur, $pendidikan, $statusPendidikan); + + $lat = !empty($body['latitude']) ? (float)$body['latitude'] : null; + $lng = !empty($body['longitude']) ? (float)$body['longitude'] : null; + if ($lat !== null && !validateCoord((string)$lat, 'lat')) jsonResponse(false, 'Latitude tidak valid.', [], 400); + if ($lng !== null && !validateCoord((string)$lng, 'lng')) jsonResponse(false, 'Longitude tidak valid.', [], 400); + + $stmt = $pdo->prepare(" + INSERT INTO penduduk + (nik, id_keluarga, nama_lengkap, jenis_kelamin, tanggal_lahir, + status_keluarga, status_perkawinan, status_hidup, + pendidikan_terakhir, status_pendidikan, + pekerjaan, penghasilan, status_ekonomi, + kondisi_kesehatan, catatan_kesehatan, + no_telp, alamat, latitude, longitude) + VALUES + (:nik,:kk,:nama,:jk,:tgl, + :sk,:sp,:sh, + :ptk,:spd, + :pkr,:pgn,:se, + :kkh,:ckkh, + :tlp,:almt,:lat,:lng) + "); + $stmt->execute([ + ':nik' => $nik, + ':kk' => $idKk, + ':nama' => $nama, + ':jk' => $jk, + ':tgl' => $tgl, + ':sk' => sanitizeStr($body['status_keluarga'] ?? 'anggota_lain'), + ':sp' => sanitizeStr($body['status_perkawinan'] ?? 'belum_kawin'), + ':sh' => sanitizeStr($body['status_hidup'] ?? 'hidup'), + ':ptk' => $pendidikan, + ':spd' => $statusPendidikan, + ':pkr' => sanitizeStr($body['pekerjaan'] ?? '') ?: null, + ':pgn' => !empty($body['penghasilan']) ? (float)$body['penghasilan'] : 0, + ':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'), + ':kkh' => sanitizeStr($body['kondisi_kesehatan'] ?? 'sehat'), + ':ckkh' => sanitizeStr($body['catatan_kesehatan'] ?? '') ?: null, + ':tlp' => sanitizeStr($body['no_telp'] ?? '') ?: null, + ':almt' => sanitizeStr($body['alamat'] ?? '') ?: null, + ':lat' => $lat, + ':lng' => $lng, + ]); + + logAktivitas($pdo, $currentUser['id'] ?? null, 'CREATE', 'penduduk', $nik, null, $body); + + jsonResponse(true, 'Penduduk berhasil ditambahkan.', [ + 'nik' => $nik, + 'validasi' => $validasi + ], 201); +} + +// ============================================================ +// PUT - Update +// ============================================================ +if ($method === 'PUT') { + if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + + $nik = sanitizeStr($_GET['nik'] ?? ''); + if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400); + + $body = json_decode(file_get_contents('php://input'), true) ?? []; + + // Ambil data lama + $old = $pdo->prepare("SELECT * FROM penduduk WHERE nik = :nik"); + $old->execute([':nik' => $nik]); + $dataLama = $old->fetch(); + if (!$dataLama) jsonResponse(false, 'Data tidak ditemukan.', [], 404); + + $sets = []; + $params = [':nik' => $nik]; + + $fieldMap = [ + 'id_keluarga' => ':kk', 'nama_lengkap' => ':nama', 'jenis_kelamin' => ':jk', + 'tanggal_lahir' => ':tgl', 'status_keluarga' => ':sk', 'status_perkawinan' => ':sp', + 'status_hidup' => ':sh', 'pendidikan_terakhir' => ':ptk', 'status_pendidikan' => ':spd', + 'pekerjaan' => ':pkr', 'penghasilan' => ':pgn', 'status_ekonomi' => ':se', + 'kondisi_kesehatan' => ':kkh', 'catatan_kesehatan' => ':ckkh', + 'no_telp' => ':tlp', 'alamat' => ':almt', 'latitude' => ':lat', 'longitude' => ':lng', + ]; + + foreach ($fieldMap as $field => $param) { + if (array_key_exists($field, $body)) { + $sets[] = "$field = $param"; + $params[$param] = $body[$field] === '' ? null : $body[$field]; + } + } + + if (empty($sets)) jsonResponse(false, 'Tidak ada data yang diubah.', [], 400); + + $sql = "UPDATE penduduk SET " . implode(', ', $sets) . " WHERE nik = :nik"; + $pdo->prepare($sql)->execute($params); + + logAktivitas($pdo, $currentUser['id'] ?? null, 'UPDATE', 'penduduk', $nik, $dataLama, $body); + + jsonResponse(true, 'Data penduduk berhasil diperbarui.', ['nik' => $nik]); +} + +// ============================================================ +// DELETE +// ============================================================ +if ($method === 'DELETE') { + if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin yang bisa menghapus data.', [], 403); + + $nik = sanitizeStr($_GET['nik'] ?? ''); + if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400); + + $old = $pdo->prepare("SELECT * FROM penduduk WHERE nik = :nik"); + $old->execute([':nik' => $nik]); + $dataLama = $old->fetch(); + if (!$dataLama) jsonResponse(false, 'Data tidak ditemukan.', [], 404); + + $pdo->prepare("DELETE FROM penduduk WHERE nik = :nik")->execute([':nik' => $nik]); + logAktivitas($pdo, $currentUser['id'] ?? null, 'DELETE', 'penduduk', $nik, $dataLama, null); + + jsonResponse(true, 'Data penduduk dihapus.'); +} + +jsonResponse(false, 'Method tidak didukung.', [], 405); diff --git a/webgis-kemiskinan/php/api/pesan.php b/webgis-kemiskinan/php/api/pesan.php new file mode 100644 index 0000000..6f9af42 --- /dev/null +++ b/webgis-kemiskinan/php/api/pesan.php @@ -0,0 +1,127 @@ +prepare(" + SELECT p.*, u.nama AS pengirim, u.role_id, + ri.nama AS nama_ri + FROM pesan p + JOIN users u ON u.id = p.dari_user_id + LEFT JOIN rumah_ibadah ri ON ri.id = p.ke_rumah_ibadah_id + WHERE p.ke_rumah_ibadah_id = :ri + ORDER BY p.created_at ASC + LIMIT 200 + "); + $stmt->execute([':ri' => $riId]); + } else { + // Pesan langsung ke user + $stmt = $pdo->prepare(" + SELECT p.*, u.nama AS pengirim + FROM pesan p + JOIN users u ON u.id = p.dari_user_id + WHERE p.ke_user_id = :uid OR p.dari_user_id = :uid2 + ORDER BY p.created_at ASC + LIMIT 200 + "); + $stmt->execute([':uid' => $currentUser['id'], ':uid2' => $currentUser['id']]); + } + $pesan = $stmt->fetchAll(); + + // Tandai sudah dibaca + if ($riId) { + $pdo->prepare("UPDATE pesan SET dibaca=1 WHERE ke_rumah_ibadah_id=:ri AND dari_user_id != :uid") + ->execute([':ri' => $riId, ':uid' => $currentUser['id']]); + } else { + $pdo->prepare("UPDATE pesan SET dibaca=1 WHERE ke_user_id=:uid") + ->execute([':uid' => $currentUser['id']]); + } + + jsonResponse(true, 'OK', ['data' => $pesan]); + } + + if ($action === 'notifikasi') { + $stmt = $pdo->prepare(" + SELECT * FROM notifikasi + WHERE user_id = :uid + ORDER BY created_at DESC + LIMIT 30 + "); + $stmt->execute([':uid' => $currentUser['id']]); + $notif = $stmt->fetchAll(); + $unread = (int)$pdo->prepare("SELECT COUNT(*) FROM notifikasi WHERE user_id=:uid AND dibaca=0") + ->execute([':uid' => $currentUser['id']]); + jsonResponse(true, 'OK', ['data' => $notif, 'unread' => array_sum(array_column($notif, 'dibaca') === array_map(fn($r) => $r['dibaca'] == 0, $notif))]); + } + + if ($action === 'pengurus_list') { + // Daftar pengurus untuk pilihan chat + $rows = $pdo->query(" + SELECT u.id, u.nama, u.email, ri.nama AS nama_ri, ri.jenis AS jenis_ri + FROM users u + LEFT JOIN rumah_ibadah ri ON ri.id = u.rumah_ibadah_id + WHERE u.role_id IN (1,2) AND u.aktif=1 + ORDER BY u.nama + ")->fetchAll(); + jsonResponse(true, 'OK', ['data' => $rows]); + } + + jsonResponse(false, 'Action tidak dikenal.', [], 400); +} + +if ($method === 'POST') { + // Kirim pesan + $body = json_decode(file_get_contents('php://input'), true) ?? $_POST; + $isi = sanitizeStr($body['isi'] ?? '', 2000); + if (strlen(trim($isi)) < 1) jsonResponse(false, 'Pesan tidak boleh kosong.', [], 400); + + $keUserId = !empty($body['ke_user_id']) ? (int)$body['ke_user_id'] : null; + $keRiId = !empty($body['ke_rumah_ibadah_id']) ? (int)$body['ke_rumah_ibadah_id'] : null; + + if (!$keUserId && !$keRiId) jsonResponse(false, 'Tujuan pesan harus diisi.', [], 400); + + $stmt = $pdo->prepare(" + INSERT INTO pesan (dari_user_id, ke_user_id, ke_rumah_ibadah_id, isi) + VALUES (:dari, :ke_user, :ke_ri, :isi) + "); + $stmt->execute([ + ':dari' => $currentUser['id'], + ':ke_user' => $keUserId, + ':ke_ri' => $keRiId, + ':isi' => $isi, + ]); + + $newId = (int)$pdo->lastInsertId(); + + // Notifikasi ke penerima + if ($keUserId) { + $pdo->prepare("INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id) VALUES (:u,:j,:i,'pesan',:r)") + ->execute([':u'=>$keUserId, ':j'=>"Pesan dari {$currentUser['role']}", ':i'=>substr($isi,0,100), ':r'=>$newId]); + } + + jsonResponse(true, 'Pesan terkirim.', ['id' => $newId], 201); +} + +jsonResponse(false, 'Method tidak didukung.', [], 405); diff --git a/webgis-kemiskinan/php/api/rumah_ibadah.php b/webgis-kemiskinan/php/api/rumah_ibadah.php new file mode 100644 index 0000000..6ddb5d8 --- /dev/null +++ b/webgis-kemiskinan/php/api/rumah_ibadah.php @@ -0,0 +1,111 @@ +query(" + SELECT ri.*, + COUNT(k.id) AS jumlah_kk, + SUM(CASE WHEN k.status_ekonomi='miskin' THEN 1 ELSE 0 END) AS kk_miskin + FROM rumah_ibadah ri + LEFT JOIN keluarga k ON k.rumah_ibadah_id = ri.id + GROUP BY ri.id + ORDER BY ri.nama + ")->fetchAll(); + jsonResponse(true, 'OK', ['data' => $rows]); + } + if ($action === 'geojson') { + $rows = $pdo->query("SELECT * FROM rumah_ibadah")->fetchAll(); + $features = []; + foreach ($rows as $r) { + $features[] = [ + 'type' => 'Feature', + 'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['longitude'],(float)$r['latitude']]], + 'properties' => $r + ]; + } + header('Content-Type: application/json'); + echo json_encode(['type'=>'FeatureCollection','features'=>$features], JSON_UNESCAPED_UNICODE); + exit; + } + jsonResponse(false, 'Action tidak dikenal.', [], 400); +} + +if ($method === 'POST') { + if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + $body = json_decode(file_get_contents('php://input'), true) ?? $_POST; + $nama = sanitizeStr($body['nama'] ?? ''); + $lat = (float)($body['latitude'] ?? 0); + $lng = (float)($body['longitude'] ?? 0); + if (strlen($nama) < 3) jsonResponse(false, 'Nama terlalu pendek.', [], 400); + if (!$lat || !$lng) jsonResponse(false, 'Koordinat wajib diisi.', [], 400); + + $stmt = $pdo->prepare(" + INSERT INTO rumah_ibadah (nama, jenis, kontak, radius_meter, latitude, longitude, alamat, kelurahan, kecamatan) + VALUES (:nama,:jenis,:kontak,:radius,:lat,:lng,:alamat,:kel,:kec) + "); + $stmt->execute([ + ':nama' => $nama, + ':jenis' => sanitizeStr($body['jenis'] ?? 'Lainnya'), + ':kontak' => sanitizeStr($body['kontak'] ?? '') ?: null, + ':radius' => (float)($body['radius_meter'] ?? 300), + ':lat' => $lat, ':lng' => $lng, + ':alamat' => sanitizeStr($body['alamat'] ?? '') ?: null, + ':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null, + ':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null, + ]); + $newId = (int)$pdo->lastInsertId(); + logAktivitas($pdo, $currentUser['id']??null, 'CREATE', 'rumah_ibadah', (string)$newId, null, $body); + jsonResponse(true, 'Rumah ibadah ditambahkan.', ['id' => $newId], 201); +} + +if ($method === 'PUT') { + if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403); + $id = (int)($_GET['id'] ?? 0); + $body = json_decode(file_get_contents('php://input'), true) ?? []; + if ($action === 'update_koordinat') { + $lat = (float)($body['latitude'] ?? 0); + $lng = (float)($body['longitude'] ?? 0); + if (!$lat || !$lng) jsonResponse(false, 'Koordinat tidak valid.', [], 400); + + $pdo->prepare("UPDATE rumah_ibadah SET latitude=:lat, longitude=:lng WHERE id=:id") + ->execute([':lat'=>$lat, ':lng'=>$lng, ':id'=>$id]); + logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'rumah_ibadah', (string)$id, null, ['lat'=>$lat,'lng'=>$lng]); + jsonResponse(true, 'Koordinat diperbarui.', ['id' => $id]); + } + + $pdo->prepare(" + UPDATE rumah_ibadah SET nama=:nama,jenis=:jenis,kontak=:kontak,radius_meter=:radius, + latitude=:lat,longitude=:lng,alamat=:alamat,kelurahan=:kel,kecamatan=:kec WHERE id=:id + ")->execute([ + ':nama'=>sanitizeStr($body['nama']??''), ':jenis'=>sanitizeStr($body['jenis']??'Lainnya'), + ':kontak'=>sanitizeStr($body['kontak']??'')?: null, ':radius'=>(float)($body['radius_meter']??300), + ':lat'=>(float)($body['latitude']??0), ':lng'=>(float)($body['longitude']??0), + ':alamat'=>sanitizeStr($body['alamat']??'')?: null, ':kel'=>sanitizeStr($body['kelurahan']??'')?: null, + ':kec'=>sanitizeStr($body['kecamatan']??'')?: null, ':id'=>$id, + ]); + logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'rumah_ibadah', (string)$id); + jsonResponse(true, 'Rumah ibadah diperbarui.', ['id' => $id]); +} + +if ($method === 'DELETE') { + if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin.', [], 403); + $id = (int)($_GET['id'] ?? 0); + $pdo->prepare("DELETE FROM rumah_ibadah WHERE id=:id")->execute([':id'=>$id]); + logAktivitas($pdo, $currentUser['id']??null, 'DELETE', 'rumah_ibadah', (string)$id); + jsonResponse(true, 'Dihapus.'); +} + +jsonResponse(false, 'Method tidak didukung.', [], 405); diff --git a/webgis-kemiskinan/php/koneksi.php b/webgis-kemiskinan/php/koneksi.php new file mode 100644 index 0000000..1a729a5 --- /dev/null +++ b/webgis-kemiskinan/php/koneksi.php @@ -0,0 +1,177 @@ + 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, $pdoOptions); +} catch (PDOException $e) { + http_response_code(500); + header('Content-Type: application/json'); + // Jangan expose detail error di produksi + $errMsg = (defined('APP_ENV') && APP_ENV === 'development') ? $e->getMessage() : 'Hubungi administrator sistem.'; + echo json_encode([ + 'success' => false, + 'message' => 'Koneksi database gagal: ' . DB_NAME . '@' . DB_HOST . ' tidak ditemukan. Pastikan database sudah dibuat dan nama database benar.', + 'error' => $errMsg + ]); + exit; +} + +// ============================================================ +// Helper: Response JSON +// ============================================================ + +function jsonResponse(bool $success, string $message, array $data = [], int $httpCode = 200): void { + http_response_code($httpCode); + 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(array_merge(['success' => $success, 'message' => $message], $data), JSON_UNESCAPED_UNICODE); + exit; +} + +// ============================================================ +// Helper: Sanitasi & Validasi +// ============================================================ + +function h(string $s): string { + return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); +} + +function sanitizeStr(?string $v, int $max = 255): string { + return mb_substr(trim($v ?? ''), 0, $max); +} + +function validateNIK(string $nik): bool { + return preg_match('/^\d{16}$/', $nik) === 1; +} + +function validateCoord(string $val, string $type = 'lat'): bool { + $f = (float)$val; + return $type === 'lat' ? ($f >= -90 && $f <= 90) : ($f >= -180 && $f <= 180); +} + +// ============================================================ +// Helper: Upload File +// ============================================================ + +function uploadFile(array $file, string $destDir, array $allowedExt = null): ?string { + if ($allowedExt === null) $allowedExt = ALLOWED_FILE_EXT; + if ($file['error'] !== UPLOAD_ERR_OK) return null; + if ($file['size'] > MAX_FILE_SIZE) throw new RuntimeException('Ukuran file melebihi 5MB.'); + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if (!in_array($ext, $allowedExt, true)) throw new RuntimeException("Format file .$ext tidak diizinkan."); + $filename = uniqid('img_', true) . '.' . $ext; + $dest = rtrim($destDir, '/') . '/' . $filename; + if (!move_uploaded_file($file['tmp_name'], $dest)) throw new RuntimeException('Gagal menyimpan file.'); + return $filename; +} + +// ============================================================ +// Helper: Log Aktivitas +// ============================================================ + +function logAktivitas(PDO $pdo, ?int $userId, string $aksi, string $tabel = '', string $recordId = '', $dataLama = null, $dataBaru = null): void { + try { + $stmt = $pdo->prepare(" + INSERT INTO log_aktivitas (user_id, aksi, tabel, record_id, data_lama, data_baru, ip_address, user_agent) + VALUES (:u, :a, :t, :r, :dl, :db, :ip, :ua) + "); + $stmt->execute([ + ':u' => $userId, + ':a' => $aksi, + ':t' => $tabel, + ':r' => $recordId, + ':dl' => $dataLama ? json_encode($dataLama, JSON_UNESCAPED_UNICODE) : null, + ':db' => $dataBaru ? json_encode($dataBaru, JSON_UNESCAPED_UNICODE) : null, + ':ip' => $_SERVER['REMOTE_ADDR'] ?? null, + ':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255), + ]); + } catch (Exception) { /* log gagal tidak menghentikan proses */ } +} + +// ============================================================ +// Validasi Logika Pendidikan vs Umur +// ============================================================ + +function validasiPendidikanUmur(int $umur, string $pendidikan, string $statusPendidikan): array { + $warnings = []; + $errors = []; + + if ($umur >= 7 && $umur <= 12) { + if ($statusPendidikan !== 'sekolah') { + $warnings[] = "Anak usia $umur tahun semestinya masih sekolah SD."; + } + } elseif ($umur >= 13 && $umur <= 15) { + if ($statusPendidikan !== 'sekolah') { + $warnings[] = "Anak usia $umur tahun semestinya masih sekolah SMP."; + } + } elseif ($umur >= 16 && $umur <= 18) { + if ($statusPendidikan !== 'sekolah') { + $warnings[] = "Anak usia $umur tahun semestinya masih sekolah SMA/SMK."; + } + } + // Umur > 18: bebas, boleh tidak sekolah, boleh pelatihan + + return ['warnings' => $warnings, 'errors' => $errors]; +} + +// ============================================================ +// Prioritas Bantuan +// ============================================================ + +function hitungPrioritasBantuan(string $statusEkonomi, string $kondisiKesehatan, bool $belumPernahDibantu): string { + if ($statusEkonomi === 'miskin' && in_array($kondisiKesehatan, ['sakit_parah', 'disabilitas'])) { + return 'KRITIS'; + } + if ($statusEkonomi === 'miskin' && $belumPernahDibantu) { + return 'TINGGI'; + } + if ($statusEkonomi === 'miskin') { + return 'SEDANG'; + } + if ($statusEkonomi === 'rentan' && in_array($kondisiKesehatan, ['sakit_parah', 'disabilitas'])) { + return 'SEDANG'; + } + return 'RENDAH'; +} diff --git a/webgis-kemiskinan/php/middleware/auth.php b/webgis-kemiskinan/php/middleware/auth.php new file mode 100644 index 0000000..895d014 --- /dev/null +++ b/webgis-kemiskinan/php/middleware/auth.php @@ -0,0 +1,125 @@ + true, + 'cookie_samesite' => 'Strict', + 'use_strict_mode' => true, + ]); +} + +// ============================================================ +// Fungsi login / logout +// ============================================================ + +function doLogin(PDO $pdo, string $username, string $password): array { + $stmt = $pdo->prepare(" + SELECT u.*, r.nama AS role_nama + FROM users u + JOIN roles r ON r.id = u.role_id + WHERE u.username = :u AND u.aktif = 1 + LIMIT 1 + "); + $stmt->execute([':u' => $username]); + $user = $stmt->fetch(); + + if (!$user || !password_verify($password, $user['password_hash'])) { + return ['success' => false, 'message' => 'Username atau password salah.']; + } + + // Update last_login + $pdo->prepare("UPDATE users SET last_login = NOW() WHERE id = :id") + ->execute([':id' => $user['id']]); + + $_SESSION['user_id'] = $user['id']; + $_SESSION['user_nama'] = $user['nama']; + $_SESSION['role'] = $user['role_nama']; + $_SESSION['role_id'] = $user['role_id']; + $_SESSION['rumah_ibadah_id'] = $user['rumah_ibadah_id']; + session_regenerate_id(true); + + logAktivitas($pdo, $user['id'], 'LOGIN', 'users', (string)$user['id']); + + return ['success' => true, 'role' => $user['role_nama']]; +} + +function doLogout(PDO $pdo): void { + if (isset($_SESSION['user_id'])) { + logAktivitas($pdo, $_SESSION['user_id'], 'LOGOUT', 'users', (string)$_SESSION['user_id']); + } + session_destroy(); +} + +// ============================================================ +// Guard: wajib login +// ============================================================ + +function requireLogin(array $allowedRoles = []): array { + if (empty($_SESSION['user_id'])) { + // Jika request API + if (!empty($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + jsonResponse(false, 'Sesi habis. Silakan login kembali.', [], 401); + } + header('Location: ' . APP_URL . '/admin/login.php'); + exit; + } + if (!empty($allowedRoles) && !in_array($_SESSION['role'], $allowedRoles, true)) { + if (!empty($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + jsonResponse(false, 'Tidak punya akses.', [], 403); + } + header('Location: ' . APP_URL . '/admin/forbidden.php'); + exit; + } + return [ + 'id' => (int)$_SESSION['user_id'], + 'nama' => $_SESSION['user_nama'], + 'role' => $_SESSION['role'], + 'role_id' => (int)$_SESSION['role_id'], + 'rumah_ibadah_id' => $_SESSION['rumah_ibadah_id'] ? (int)$_SESSION['rumah_ibadah_id'] : null, + ]; +} + +// ============================================================ +// Guard API: token-based (opsional, untuk mobile app nanti) +// ============================================================ + +function getApiUser(PDO $pdo): ?array { + // Cek session dulu + if (!empty($_SESSION['user_id'])) { + return [ + 'id' => (int)$_SESSION['user_id'], + 'role' => $_SESSION['role'], + 'role_id' => (int)$_SESSION['role_id'], + 'rumah_ibadah_id' => $_SESSION['rumah_ibadah_id'] ? (int)$_SESSION['rumah_ibadah_id'] : null, + ]; + } + return null; // belum login = null +} + +// ============================================================ +// Cek izin per fitur +// ============================================================ + +function canEdit(?array $user): bool { + if (!$user) return false; + return in_array($user['role'], ['admin', 'pengurus'], true); +} + +function canVerify(?array $user): bool { + if (!$user) return false; + return in_array($user['role'], ['admin', 'pengurus'], true); +} + +function canViewAll(?array $user): bool { + if (!$user) return false; + return in_array($user['role'], ['admin', 'pengurus', 'pimpinan'], true); +} + +function isAdmin(?array $user): bool { + return $user && $user['role'] === 'admin'; +} diff --git a/webgis-kemiskinan/sql/schema.sql b/webgis-kemiskinan/sql/schema.sql new file mode 100644 index 0000000..8009642 --- /dev/null +++ b/webgis-kemiskinan/sql/schema.sql @@ -0,0 +1,384 @@ +-- ============================================================ +-- WebGIS Pengentasan Kemiskinan Berbasis Masyarakat & Rumah Ibadah +-- Schema Lengkap v2.0 β€” FIXED VERSION +-- Engine: MySQL / MariaDB | Charset: utf8mb4 +-- ============================================================ + +-- PENTING: Buat database terlebih dahulu jika belum ada +CREATE DATABASE IF NOT EXISTS webgis_kemiskinan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE webgis_kemiskinan; + +SET FOREIGN_KEY_CHECKS = 0; +SET NAMES utf8mb4; + +-- ============================================================ +-- DROP TABLES (urutan terbalik dari dependensi FK) +-- ============================================================ +DROP TABLE IF EXISTS notifikasi; +DROP TABLE IF EXISTS log_aktivitas; +DROP TABLE IF EXISTS pesan; +DROP TABLE IF EXISTS laporan; +DROP TABLE IF EXISTS histori_bantuan; +DROP TABLE IF EXISTS jenis_bantuan; +DROP TABLE IF EXISTS riwayat_pelatihan; +DROP TABLE IF EXISTS penduduk; +DROP TABLE IF EXISTS keluarga; +DROP TABLE IF EXISTS rumah_ibadah; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS roles; +DROP TABLE IF EXISTS jalan_lines; +DROP TABLE IF EXISTS parsil_tanah; + +-- ============================================================ +-- 1. ROLES & USERS +-- ============================================================ + +CREATE TABLE roles ( + id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(50) NOT NULL UNIQUE, + label VARCHAR(80) NOT NULL, + deskripsi TEXT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO roles (nama, label) VALUES + ('admin', 'Administrator Sistem'), + ('pengurus', 'Pengurus Rumah Ibadah'), + ('pimpinan', 'Pimpinan Daerah'), + ('masyarakat', 'Masyarakat Umum'); + +-- ============================================================ +-- 2. RUMAH IBADAH (dibuat sebelum users karena users FK ke sini) +-- ============================================================ + +CREATE TABLE rumah_ibadah ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(200) NOT NULL, + jenis ENUM('Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya') NOT NULL DEFAULT 'Lainnya', + kontak VARCHAR(120) NULL, + radius_meter DOUBLE NOT NULL DEFAULT 300, + latitude DECIMAL(10,7) NOT NULL, + longitude DECIMAL(10,7) NOT NULL, + alamat TEXT NULL, + kelurahan VARCHAR(120) NULL, + kecamatan VARCHAR(120) NULL, + kota VARCHAR(120) NULL, + deskripsi TEXT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_ri_jenis (jenis), + INDEX idx_ri_latlng (latitude, longitude) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 3. USERS (setelah roles & rumah_ibadah) +-- ============================================================ + +CREATE TABLE users ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + role_id TINYINT UNSIGNED NOT NULL DEFAULT 4, + nama VARCHAR(150) NOT NULL, + username VARCHAR(60) NOT NULL UNIQUE, + email VARCHAR(150) NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + no_telp VARCHAR(20) NULL, + foto VARCHAR(255) NULL, + aktif TINYINT(1) NOT NULL DEFAULT 1, + rumah_ibadah_id INT UNSIGNED NULL, + last_login DATETIME NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_users_role (role_id), + CONSTRAINT fk_users_role FOREIGN KEY (role_id) REFERENCES roles(id), + CONSTRAINT fk_users_ri FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 4. KELUARGA (KK) +-- ============================================================ + +CREATE TABLE keluarga ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + no_kk VARCHAR(20) NOT NULL UNIQUE, + nama_kk VARCHAR(200) NOT NULL, + alamat TEXT NULL, + kelurahan VARCHAR(120) NULL, + kecamatan VARCHAR(120) NULL, + kota VARCHAR(120) NULL, + latitude DECIMAL(10,7) NULL, + longitude DECIMAL(10,7) NULL, + status_ekonomi ENUM('miskin','rentan','mampu') NOT NULL DEFAULT 'rentan', + rumah_ibadah_id INT UNSIGNED NULL, + jarak_ri_meter DOUBLE NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_kk_status (status_ekonomi), + INDEX idx_kk_latlng (latitude, longitude), + INDEX idx_kk_ri (rumah_ibadah_id), + CONSTRAINT fk_kk_ri FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 5. PENDUDUK +-- ============================================================ + +CREATE TABLE penduduk ( + nik VARCHAR(16) NOT NULL PRIMARY KEY, + id_keluarga INT UNSIGNED NULL, + nama_lengkap VARCHAR(200) NOT NULL, + jenis_kelamin ENUM('L','P') NOT NULL, + tanggal_lahir DATE NOT NULL, + umur TINYINT UNSIGNED GENERATED ALWAYS AS ( + TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE()) + ) VIRTUAL, + status_keluarga ENUM('kepala_keluarga','istri','anak','menantu','cucu','orang_tua','mertua','anggota_lain') NOT NULL DEFAULT 'anggota_lain', + status_perkawinan ENUM('belum_kawin','kawin','cerai_hidup','cerai_mati') NOT NULL DEFAULT 'belum_kawin', + status_hidup ENUM('hidup','meninggal') NOT NULL DEFAULT 'hidup', + pendidikan_terakhir ENUM('tidak_sekolah','SD','SMP','SMA','SMK','D1','D2','D3','S1','S2','S3') NOT NULL DEFAULT 'tidak_sekolah', + status_pendidikan ENUM('sekolah','tidak_sekolah','lulus') NOT NULL DEFAULT 'tidak_sekolah', + pekerjaan VARCHAR(150) NULL, + penghasilan DECIMAL(15,2) NULL DEFAULT 0, + status_ekonomi ENUM('miskin','rentan','mampu') NOT NULL DEFAULT 'rentan', + kondisi_kesehatan ENUM('sehat','sakit_ringan','sakit_parah','disabilitas') NOT NULL DEFAULT 'sehat', + catatan_kesehatan TEXT NULL, + no_telp VARCHAR(20) NULL, + alamat TEXT NULL, + latitude DECIMAL(10,7) NULL, + longitude DECIMAL(10,7) NULL, + foto VARCHAR(255) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_pddk_keluarga (id_keluarga), + INDEX idx_pddk_status_ek (status_ekonomi), + INDEX idx_pddk_status_hid (status_hidup), + INDEX idx_pddk_latlng (latitude, longitude), + CONSTRAINT fk_pddk_keluarga FOREIGN KEY (id_keluarga) REFERENCES keluarga(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 6. RIWAYAT PELATIHAN +-- ============================================================ + +CREATE TABLE riwayat_pelatihan ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nik VARCHAR(16) NOT NULL, + nama_pelatihan VARCHAR(200) NOT NULL, + penyelenggara VARCHAR(150) NULL, + tanggal_mulai DATE NULL, + tanggal_selesai DATE NULL, + sertifikat VARCHAR(255) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_platihan_nik (nik), + CONSTRAINT fk_platihan_nik FOREIGN KEY (nik) REFERENCES penduduk(nik) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 7. JENIS BANTUAN (master) +-- ============================================================ + +CREATE TABLE jenis_bantuan ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL, + kategori ENUM('sembako','pendidikan','kesehatan','ekonomi','perumahan','lainnya') NOT NULL DEFAULT 'lainnya', + sumber VARCHAR(150) NULL, + satuan VARCHAR(50) NULL, + deskripsi TEXT NULL, + aktif TINYINT(1) NOT NULL DEFAULT 1, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO jenis_bantuan (nama, kategori, sumber, satuan) VALUES + ('Sembako Bulanan', 'sembako', 'Rumah Ibadah', '1 paket/bulan'), + ('Beasiswa Pendidikan', 'pendidikan','BAZNAS', 'Rp/semester'), + ('Bantuan Kesehatan', 'kesehatan', 'Puskesmas', '1 kali/kunjungan'), + ('Modal Usaha Mikro', 'ekonomi', 'Baznas', 'Rp/program'), + ('Renovasi Rumah', 'perumahan', 'Pemerintah', 'paket/unit'); + +-- ============================================================ +-- 8. HISTORI BANTUAN +-- ============================================================ + +CREATE TABLE histori_bantuan ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nik VARCHAR(16) NOT NULL, + id_jenis_bantuan INT UNSIGNED NOT NULL, + id_keluarga INT UNSIGNED NULL, + rumah_ibadah_id INT UNSIGNED NULL, + tanggal DATE NOT NULL, + jumlah VARCHAR(100) NULL, + nilai_rupiah DECIMAL(15,2) NULL, + status_penyaluran ENUM('dijadwalkan','disalurkan','dibatalkan') NOT NULL DEFAULT 'dijadwalkan', + catatan TEXT NULL, + bukti_file VARCHAR(255) NULL, + disalurkan_oleh INT UNSIGNED NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_hb_nik (nik), + INDEX idx_hb_tanggal (tanggal), + INDEX idx_hb_status (status_penyaluran), + CONSTRAINT fk_hb_nik FOREIGN KEY (nik) REFERENCES penduduk(nik) ON DELETE CASCADE, + CONSTRAINT fk_hb_jenis FOREIGN KEY (id_jenis_bantuan) REFERENCES jenis_bantuan(id), + CONSTRAINT fk_hb_kk FOREIGN KEY (id_keluarga) REFERENCES keluarga(id) ON DELETE SET NULL, + CONSTRAINT fk_hb_ri FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL, + CONSTRAINT fk_hb_user FOREIGN KEY (disalurkan_oleh) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 9. LAPORAN MASYARAKAT +-- ============================================================ + +CREATE TABLE laporan ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nama_pelapor VARCHAR(150) NULL, + pelapor_id INT UNSIGNED NULL, + nik_terdampak VARCHAR(16) NULL, + deskripsi TEXT NOT NULL, + kategori ENUM('darurat_pangan','darurat_kesehatan','darurat_ekonomi','butuh_bantuan_lain') NOT NULL DEFAULT 'butuh_bantuan_lain', + tingkat_urgensi ENUM('rendah','sedang','tinggi','darurat') NOT NULL DEFAULT 'sedang', + latitude DECIMAL(10,7) NULL, + longitude DECIMAL(10,7) NULL, + alamat TEXT NULL, + foto VARCHAR(255) NULL, + status ENUM('pending','diverifikasi','diproses','selesai','ditolak') NOT NULL DEFAULT 'pending', + catatan_verifikasi TEXT NULL, + diverifikasi_oleh INT UNSIGNED NULL, + ditangani_oleh INT UNSIGNED NULL, + tanggal_selesai DATETIME NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_lap_status (status), + INDEX idx_lap_urgensi (tingkat_urgensi), + INDEX idx_lap_latlng (latitude, longitude), + CONSTRAINT fk_lap_nik FOREIGN KEY (nik_terdampak) REFERENCES penduduk(nik) ON DELETE SET NULL, + CONSTRAINT fk_lap_pelapor FOREIGN KEY (pelapor_id) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_lap_verif FOREIGN KEY (diverifikasi_oleh) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_lap_tangan FOREIGN KEY (ditangani_oleh) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 10. PESAN / CHAT ANTAR PENGURUS +-- ============================================================ + +CREATE TABLE pesan ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + dari_user_id INT UNSIGNED NOT NULL, + ke_user_id INT UNSIGNED NULL, + ke_rumah_ibadah_id INT UNSIGNED NULL, + isi TEXT NOT NULL, + dibaca TINYINT(1) NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_pesan_dari (dari_user_id), + INDEX idx_pesan_ke (ke_user_id), + CONSTRAINT fk_pesan_dari FOREIGN KEY (dari_user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_pesan_ke FOREIGN KEY (ke_user_id) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_pesan_ri FOREIGN KEY (ke_rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 11. LOG AKTIVITAS (audit trail) +-- ============================================================ + +CREATE TABLE log_aktivitas ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id INT UNSIGNED NULL, + aksi VARCHAR(50) NOT NULL, + tabel VARCHAR(60) NULL, + record_id VARCHAR(32) NULL, + data_lama JSON NULL, + data_baru JSON NULL, + ip_address VARCHAR(45) NULL, + user_agent VARCHAR(255) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_log_user (user_id), + INDEX idx_log_tabel (tabel), + INDEX idx_log_aksi (aksi), + INDEX idx_log_time (created_at), + CONSTRAINT fk_log_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 12. LAYER SPASIAL TAMBAHAN +-- ============================================================ + +CREATE TABLE jalan_lines ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nama_jalan VARCHAR(200) NOT NULL, + status_jalan ENUM('Nasional','Provinsi','Kabupaten','Desa') NOT NULL DEFAULT 'Kabupaten', + panjang_meter DOUBLE NOT NULL DEFAULT 0, + geojson LONGTEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_jalan_status (status_jalan) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE parsil_tanah ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + status_kepemilikan ENUM('SHM','HGB','HGU','HP','Wakaf','Lainnya') NOT NULL DEFAULT 'SHM', + luas_m2 DOUBLE NOT NULL DEFAULT 0, + geojson LONGTEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_parsil_status (status_kepemilikan) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ============================================================ +-- 13. NOTIFIKASI +-- ============================================================ + +CREATE TABLE notifikasi ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id INT UNSIGNED NOT NULL, + judul VARCHAR(200) NOT NULL, + isi TEXT NOT NULL, + tipe ENUM('laporan','bantuan','sistem','pesan') NOT NULL DEFAULT 'sistem', + ref_id INT UNSIGNED NULL, + dibaca TINYINT(1) NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_notif_user (user_id), + INDEX idx_notif_dibaca (dibaca), + CONSTRAINT fk_notif_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================ +-- DATA DUMMY +-- ============================================================ + +INSERT IGNORE INTO users (role_id, nama, username, email, password_hash) VALUES + (1, 'Administrator', 'admin', 'admin@webgis.local', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'), + (2, 'H. Ahmad Fauzi', 'pengurus1', 'pengurus1@webgis.local', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'), + (3, 'Camat Pontianak Selatan','pimpinan1', 'pimpinan@webgis.local', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'), + (4, 'Budi Santoso', 'warga1', 'warga1@webgis.local', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'); + +INSERT IGNORE INTO rumah_ibadah (nama, jenis, kontak, radius_meter, latitude, longitude, alamat, kelurahan, kecamatan) VALUES + ('Masjid Al-Falah', 'Masjid', '08113001234', 400, -0.0200, 109.3410, 'Jl. Gajah Mada No. 1', 'Benua Melayu Darat', 'Pontianak Selatan'), + ('Masjid Raya Mujahidin', 'Masjid', '08113005678', 600, -0.0250, 109.3430, 'Jl. Ahmad Yani No. 10', 'Akcaya', 'Pontianak Selatan'), + ('GKE Pontianak', 'Gereja Protestan', '08113009999', 300, -0.0280, 109.3480, 'Jl. Nusa Indah', 'Sungai Bangkong', 'Pontianak Kota'), + ('Vihara Bodhicitta', 'Vihara', '08113004321', 250, -0.0310, 109.3520, 'Jl. Tanjungpura No. 5', 'Mariana', 'Pontianak Kota'); + +INSERT IGNORE INTO keluarga (no_kk, nama_kk, alamat, kelurahan, kecamatan, latitude, longitude, status_ekonomi, rumah_ibadah_id) VALUES + ('6171010101010001', 'Ahmad Firdaus', 'Jl. Melati No. 3', 'Benua Melayu Darat', 'Pontianak Selatan', -0.0210, 109.3415, 'miskin', 1), + ('6171010101010002', 'Siti Rahma', 'Jl. Kenanga No. 5', 'Benua Melayu Darat', 'Pontianak Selatan', -0.0220, 109.3420, 'miskin', 1), + ('6171010101010003', 'Budi Hermawan','Jl. Mawar No. 7', 'Akcaya', 'Pontianak Selatan', -0.0240, 109.3435, 'rentan', 2), + ('6171010101010004', 'Maria Goretti','Jl. Nusa Indah No.2', 'Sungai Bangkong', 'Pontianak Kota', -0.0275, 109.3475, 'miskin', 3), + ('6171010101010005', 'Tan Ah Kow', 'Jl. Tanjungpura 12', 'Mariana', 'Pontianak Kota', -0.0305, 109.3515, 'rentan', 4); + +INSERT IGNORE INTO penduduk (nik, id_keluarga, nama_lengkap, jenis_kelamin, tanggal_lahir, status_keluarga, status_perkawinan, status_ekonomi, pekerjaan, penghasilan, kondisi_kesehatan, alamat, latitude, longitude) VALUES + ('6171010101010001', 1, 'Ahmad Firdaus', 'L', '1980-03-15', 'kepala_keluarga', 'kawin', 'miskin', 'Buruh Harian', 800000, 'sehat', 'Jl. Melati No. 3', -0.0210, 109.3415), + ('6171010101010002', 1, 'Siti Aisyah', 'P', '1983-07-22', 'istri', 'kawin', 'miskin', 'Ibu Rumah Tangga', 0, 'sehat', 'Jl. Melati No. 3', -0.0210, 109.3415), + ('6171010101010003', 1, 'Reza Firdaus', 'L', '2010-11-05', 'anak', 'belum_kawin', 'miskin', 'Pelajar', 0, 'sehat', 'Jl. Melati No. 3', -0.0210, 109.3415), + ('6171020202020001', 2, 'Siti Rahma', 'P', '1975-01-10', 'kepala_keluarga', 'cerai_mati', 'miskin', 'Pedagang Kecil', 500000, 'sakit_ringan','Jl. Kenanga No. 5', -0.0220, 109.3420), + ('6171020202020002', 2, 'Dewi Rahma', 'P', '2008-05-30', 'anak', 'belum_kawin', 'miskin', 'Pelajar', 0, 'sehat', 'Jl. Kenanga No. 5', -0.0220, 109.3420), + ('6171030303030001', 3, 'Budi Hermawan', 'L', '1990-09-20', 'kepala_keluarga', 'kawin', 'rentan', 'Ojek Online', 1500000, 'sehat', 'Jl. Mawar No. 7', -0.0240, 109.3435), + ('6171040404040001', 4, 'Yohanes Goretti', 'L', '1970-12-01', 'kepala_keluarga', 'kawin', 'miskin', 'Tidak bekerja', 0, 'sakit_parah', 'Jl. Nusa Indah No.2', -0.0275, 109.3475), + ('6171050505050001', 5, 'Tan Ah Kow', 'L', '1968-06-18', 'kepala_keluarga', 'kawin', 'rentan', 'Pedagang', 1200000, 'sehat', 'Jl. Tanjungpura 12', -0.0305, 109.3515); + +INSERT IGNORE INTO histori_bantuan (nik, id_jenis_bantuan, id_keluarga, rumah_ibadah_id, tanggal, jumlah, status_penyaluran, catatan) VALUES + ('6171010101010001', 1, 1, 1, CURDATE() - INTERVAL 30 DAY, '1 paket sembako', 'disalurkan', 'Penyaluran rutin bulan lalu'), + ('6171020202020001', 2, 2, 1, CURDATE() - INTERVAL 15 DAY, 'Rp 500.000', 'disalurkan', 'Bantuan pendidikan anak'), + ('6171040404040001', 3, 4, 3, CURDATE() - INTERVAL 7 DAY, '1 kali pemeriksaan', 'disalurkan', 'Kunjungan kesehatan darurat'); + +INSERT IGNORE INTO laporan (nama_pelapor, nik_terdampak, deskripsi, kategori, tingkat_urgensi, latitude, longitude, alamat, status) VALUES + ('Anonim', '6171020202020001', 'Ibu Siti sudah 3 hari tidak makan, anaknya juga sakit', 'darurat_pangan', 'darurat', -0.0220, 109.3420, 'Jl. Kenanga No. 5', 'pending'), + ('Pak RT 03', NULL, 'Ada lansia sebatang kara tidak ada yang merawat di RT 03','butuh_bantuan_lain','tinggi', -0.0265, 109.3460, 'RT 03 Sungai Bangkong','diverifikasi'), + ('Maria Magdalena', '6171040404040001', 'Pak Yohanes sakit parah, tidak ada biaya berobat', 'darurat_kesehatan', 'darurat', -0.0275, 109.3475, 'Jl. Nusa Indah No.2', 'diproses'); diff --git a/webgis-kemiskinan/update_db.php b/webgis-kemiskinan/update_db.php new file mode 100644 index 0000000..ce2aa3c --- /dev/null +++ b/webgis-kemiskinan/update_db.php @@ -0,0 +1,11 @@ +exec("ALTER TABLE laporan ADD COLUMN pelapor_id INT UNSIGNED NULL AFTER nama_pelapor"); + $pdo->exec("ALTER TABLE laporan ADD CONSTRAINT fk_lap_pelapor FOREIGN KEY (pelapor_id) REFERENCES users(id) ON DELETE SET NULL"); + echo "Kolom pelapor_id berhasil ditambahkan.\n"; +} catch (PDOException $e) { + echo "Error atau kolom sudah ada: " . $e->getMessage() . "\n"; +} diff --git a/webgis-kemiskinan/uploads/bukti/.gitkeep b/webgis-kemiskinan/uploads/bukti/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/webgis-kemiskinan/uploads/foto_laporan/.gitkeep b/webgis-kemiskinan/uploads/foto_laporan/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/webgis-kemiskinan/uploads/foto_profil/.gitkeep b/webgis-kemiskinan/uploads/foto_profil/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/webgis-spbu/.codex b/webgis-spbu/.codex new file mode 100644 index 0000000..e69de29 diff --git a/webgis-spbu/Dockerfile b/webgis-spbu/Dockerfile new file mode 100644 index 0000000..6d60883 --- /dev/null +++ b/webgis-spbu/Dockerfile @@ -0,0 +1,10 @@ +FROM php:8.2-apache + +RUN docker-php-ext-install mysqli pdo pdo_mysql \ + && a2enmod rewrite + +COPY . /var/www/html/ + +RUN mkdir -p /var/www/html/uploads \ + && chown -R www-data:www-data /var/www/html/uploads \ + && chown -R www-data:www-data /var/www/html \ No newline at end of file diff --git a/webgis-spbu/README.md b/webgis-spbu/README.md new file mode 100644 index 0000000..d133633 --- /dev/null +++ b/webgis-spbu/README.md @@ -0,0 +1,32 @@ +# WebGIS Sebaran Penduduk Miskin + Rumah Ibadah + +Stack: PHP (XAMPP), MySQL/MariaDB, LeafletJS + Leaflet.draw (tanpa framework). + +## Fitur +- Peta interaktif (fullscreen) + layer: rumah ibadah, radius, penduduk miskin. +- CRUD lewat aksi di peta (klik untuk point, popup untuk edit/hapus, edit lokasi + drag). +- Reverse geocoding (server proxy `php/reverse_geocode.php` ke Nominatim). +- Auto-relasi penduduk miskin β†’ rumah ibadah terdekat **dalam radius** (jarak dihitung otomatis). +- Marker penduduk miskin: + - Merah: Belum dibantu + - Kuning: Menunggu verifikasi + - Hijau: Sudah dibantu + - Outline: menunjukkan inside/outside radius rumah ibadah +- Form penduduk miskin mendukung anggota keluarga (dinamis) + upload bukti (jpg/png/webp/pdf). +- Admin panel (login) + CRUD dasar + import/export CSV. + +## Instalasi (XAMPP) +1. Pastikan folder project berada di `htdocs`, misalnya: `C:\\xampp\\htdocs\\webgis-spbu`. +2. Buat database `webgis_spbu` di phpMyAdmin. +3. Import schema: `sql/schema_webgis_spbu.sql`. +4. Sesuaikan konfigurasi DB di `php/koneksi.php`. +5. Jalankan Apache + MySQL di XAMPP. +6. Buka aplikasi: + - WebGIS: `http://localhost/webgis-spbu/` + - Admin: `http://localhost/webgis-spbu/admin/login.php` + - Jika belum ada user, jalankan sekali: `http://localhost/webgis-spbu/admin/init_admin.php` + +## Catatan Upload Bukti +- File disimpan ke `uploads/bukti/`. +- Pastikan folder ini writable oleh Apache. + diff --git a/webgis-spbu/admin/_auth.php b/webgis-spbu/admin/_auth.php new file mode 100644 index 0000000..4a22ccb --- /dev/null +++ b/webgis-spbu/admin/_auth.php @@ -0,0 +1,18 @@ +'; + echo '' . h($title) . ''; + echo ''; + echo ''; + echo '
'; + echo '
Admin Panel
' . $name . ' Logout
'; +} + +function render_footer() { + echo '
'; +} + diff --git a/webgis-spbu/admin/index.php b/webgis-spbu/admin/index.php new file mode 100644 index 0000000..6570ce4 --- /dev/null +++ b/webgis-spbu/admin/index.php @@ -0,0 +1,53 @@ + 0, + 'penduduk' => 0, + 'dibantu' => 0, + 'belum' => 0, + 'verifikasi' => 0 +]; + +try { + $counts['rumah_ibadah'] = (int)$pdo->query("SELECT COUNT(*) FROM rumah_ibadah_points")->fetchColumn(); + $cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasNew = in_array('status_bantuan', $cols, true); + $counts['penduduk'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points")->fetchColumn(); + if ($hasNew) { + $counts['dibantu'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Sudah dibantu'")->fetchColumn(); + $counts['belum'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Belum dibantu'")->fetchColumn(); + $counts['verifikasi'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Menunggu verifikasi'")->fetchColumn(); + } +} catch (Exception $e) { +} + +render_header('Admin Dashboard'); +?> +
+
+
+
+
Rumah Ibadah
+
+
+
+
Penduduk Miskin
+
+
+
+
Sudah dibantu
+
+
+
+
Belum dibantu
+
+
+
+ Menu admin ini melengkapi WebGIS utama: `index.html`. +
+
+ + diff --git a/webgis-spbu/admin/init_admin.php b/webgis-spbu/admin/init_admin.php new file mode 100644 index 0000000..08b8c8a --- /dev/null +++ b/webgis-spbu/admin/init_admin.php @@ -0,0 +1,34 @@ +exec(" + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + username VARCHAR(60) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'operator', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + "); + + $count = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); + if ($count > 0) { + echo "Users sudah ada. Silakan login di login.php."; + exit; + } + + $username = 'admin'; + $password = 'admin123'; + $hash = password_hash($password, PASSWORD_DEFAULT); + $stmt = $pdo->prepare("INSERT INTO users (name, username, password_hash, role) VALUES (:n,:u,:p,'admin')"); + $stmt->execute([':n' => 'Administrator', ':u' => $username, ':p' => $hash]); + + echo "Admin dibuat. Username: admin, Password: admin123. Lanjut login: login.php"; +} catch (Exception $e) { + echo "Gagal init admin: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); +} + diff --git a/webgis-spbu/admin/login.php b/webgis-spbu/admin/login.php new file mode 100644 index 0000000..c5870e1 --- /dev/null +++ b/webgis-spbu/admin/login.php @@ -0,0 +1,62 @@ +prepare("SELECT id, name, username, password_hash, role FROM users WHERE username = :u LIMIT 1"); + $stmt->execute([':u' => $username]); + $u = $stmt->fetch(); + if ($u && password_verify($password, $u['password_hash'])) { + $_SESSION['user'] = ['id' => (int)$u['id'], 'name' => $u['name'], 'username' => $u['username'], 'role' => $u['role']]; + header('Location: index.php'); + exit; + } + $error = 'Username atau password salah.'; + } catch (Exception $e) { + $error = $e->getMessage(); + } +} + +?> + + + + + + Login Admin + + + + +
+
Login Admin
+
Jika belum ada user, jalankan init_admin.php.
+
+
+ + + + + +
+
+ + + diff --git a/webgis-spbu/admin/logout.php b/webgis-spbu/admin/logout.php new file mode 100644 index 0000000..c8f7435 --- /dev/null +++ b/webgis-spbu/admin/logout.php @@ -0,0 +1,6 @@ +query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasNew = in_array('kk_nama', $cols, true); + $out = fopen('php://output', 'w'); + if ($hasNew) { + fputcsv($out, ['id','kk_nama','nik','jumlah_anggota','lat','lng','alamat','kelurahan','kecamatan','status_bantuan','jenis_bantuan','tanggal_bantuan','rumah_ibadah_id','jarak_meter']); + $rows = $pdo->query("SELECT id, kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan, rumah_ibadah_id, jarak_meter FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll(); + foreach ($rows as $r) { + fputcsv($out, [ + $r['id'], $r['kk_nama'], $r['nik'], $r['jumlah_anggota'], + $r['latitude'], $r['longitude'], $r['address'], $r['kelurahan'], $r['kecamatan'], + $r['status_bantuan'], $r['jenis_bantuan'], $r['tanggal_bantuan'], + $r['rumah_ibadah_id'], $r['jarak_meter'] + ]); + } + } else { + fputcsv($out, ['id','nama','lat','lng','alamat','rumah_ibadah_id']); + $rows = $pdo->query("SELECT id, nama, latitude, longitude, address, rumah_ibadah_id FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll(); + foreach ($rows as $r) fputcsv($out, [$r['id'],$r['nama'],$r['latitude'],$r['longitude'],$r['address'],$r['rumah_ibadah_id']]); + } + fclose($out); + exit; +} + +$msg = ''; +$err = ''; + +// Delete +if (isset($_GET['delete'])) { + $id = (int)$_GET['delete']; + try { + $stmt = $pdo->prepare("DELETE FROM pemukiman_miskin_points WHERE id=:id"); + $stmt->execute([':id' => $id]); + $msg = 'Berhasil dihapus.'; + } catch (Exception $e) { + $err = $e->getMessage(); + } +} + +// Import CSV (minimal) +if (isset($_POST['import_csv'])) { + if (!isset($_FILES['csv_file']) || ($_FILES['csv_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + $err = 'CSV tidak ditemukan.'; + } else { + try { + $cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasNew = in_array('kk_nama', $cols, true); + $fp = fopen($_FILES['csv_file']['tmp_name'], 'r'); + $header = fgetcsv($fp); + if (!$header) throw new Exception('Header CSV kosong.'); + $map = []; + foreach ($header as $i => $hcol) $map[strtolower(trim($hcol))] = $i; + $pdo->beginTransaction(); + $ins = $hasNew + ? $pdo->prepare("INSERT INTO pemukiman_miskin_points (kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan) VALUES (:kk,:nik,:j,:lat,:lng,:a,:kel,:kec,:s,:jb,:tb)") + : $pdo->prepare("INSERT INTO pemukiman_miskin_points (nama, latitude, longitude, address) VALUES (:kk,:lat,:lng,:a)"); + $n = 0; + while (($row = fgetcsv($fp)) !== false) { + $kk = $row[$map['kk_nama'] ?? $map['nama'] ?? -1] ?? ''; + $lat = $row[$map['lat'] ?? $map['latitude'] ?? -1] ?? ''; + $lng = $row[$map['lng'] ?? $map['longitude'] ?? -1] ?? ''; + if (trim($kk) === '' || trim($lat) === '' || trim($lng) === '') continue; + if ($hasNew) { + $ins->execute([ + ':kk' => $kk, + ':nik' => ($row[$map['nik'] ?? -1] ?? '') ?: null, + ':j' => (int)($row[$map['jumlah_anggota'] ?? -1] ?? 0), + ':lat' => $lat, + ':lng' => $lng, + ':a' => ($row[$map['alamat'] ?? $map['address'] ?? -1] ?? '') ?: null, + ':kel' => ($row[$map['kelurahan'] ?? -1] ?? '') ?: null, + ':kec' => ($row[$map['kecamatan'] ?? -1] ?? '') ?: null, + ':s' => ($row[$map['status_bantuan'] ?? -1] ?? 'Belum dibantu') ?: 'Belum dibantu', + ':jb' => ($row[$map['jenis_bantuan'] ?? -1] ?? '') ?: null, + ':tb' => ($row[$map['tanggal_bantuan'] ?? -1] ?? '') ?: null + ]); + } else { + $ins->execute([ + ':kk' => $kk, + ':lat' => $lat, + ':lng' => $lng, + ':a' => ($row[$map['alamat'] ?? $map['address'] ?? -1] ?? '') ?: null + ]); + } + $n++; + } + $pdo->commit(); + fclose($fp); + $msg = "Import selesai: $n baris."; + } catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + $err = $e->getMessage(); + } + } +} + +// Simple edit/create (without anggota details) +if (isset($_POST['save_penduduk'])) { + $id = (int)($_POST['id'] ?? 0); + $kk = trim($_POST['kk_nama'] ?? ''); + $nik = trim($_POST['nik'] ?? ''); + $jml = (int)($_POST['jumlah_anggota'] ?? 0); + $lat = $_POST['latitude'] ?? null; + $lng = $_POST['longitude'] ?? null; + $addr = trim($_POST['address'] ?? ''); + $kel = trim($_POST['kelurahan'] ?? ''); + $kec = trim($_POST['kecamatan'] ?? ''); + $status = trim($_POST['status_bantuan'] ?? 'Belum dibantu'); + $jb = trim($_POST['jenis_bantuan'] ?? ''); + $tb = trim($_POST['tanggal_bantuan'] ?? ''); + + if ($kk === '' || $lat === null || $lng === null) $err = 'Data belum lengkap (nama/lat/lng).'; + else { + try { + $cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasNew = in_array('kk_nama', $cols, true); + if (!$hasNew) throw new Exception('Schema belum mendukung fitur penduduk detail. Jalankan schema terbaru.'); + + if ($id > 0) { + $stmt = $pdo->prepare("UPDATE pemukiman_miskin_points SET kk_nama=:kk, nik=:nik, jumlah_anggota=:j, latitude=:lat, longitude=:lng, address=:a, kelurahan=:kel, kecamatan=:kec, status_bantuan=:s, jenis_bantuan=:jb, tanggal_bantuan=:tb WHERE id=:id"); + $stmt->execute([':kk'=>$kk,':nik'=>$nik ?: null,':j'=>$jml,':lat'=>$lat,':lng'=>$lng,':a'=>$addr ?: null,':kel'=>$kel ?: null,':kec'=>$kec ?: null,':s'=>$status ?: 'Belum dibantu',':jb'=>$jb ?: null,':tb'=>$tb ?: null,':id'=>$id]); + $msg = 'Berhasil diupdate.'; + } else { + $stmt = $pdo->prepare("INSERT INTO pemukiman_miskin_points (kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan) VALUES (:kk,:nik,:j,:lat,:lng,:a,:kel,:kec,:s,:jb,:tb)"); + $stmt->execute([':kk'=>$kk,':nik'=>$nik ?: null,':j'=>$jml,':lat'=>$lat,':lng'=>$lng,':a'=>$addr ?: null,':kel'=>$kel ?: null,':kec'=>$kec ?: null,':s'=>$status ?: 'Belum dibantu',':jb'=>$jb ?: null,':tb'=>$tb ?: null]); + $msg = 'Berhasil ditambahkan.'; + } + } catch (Exception $e) { $err = $e->getMessage(); } + } +} + +$edit = null; +if (isset($_GET['edit'])) { + $id = (int)$_GET['edit']; + $stmt = $pdo->prepare("SELECT * FROM pemukiman_miskin_points WHERE id=:id"); + $stmt->execute([':id' => $id]); + $edit = $stmt->fetch() ?: null; +} + +$rows = []; +try { + $cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasNew = in_array('kk_nama', $cols, true); + if ($hasNew) { + $rows = $pdo->query(" + SELECT p.*, r.nama AS rumah_ibadah_nama + FROM pemukiman_miskin_points p + LEFT JOIN rumah_ibadah_points r ON r.id=p.rumah_ibadah_id + ORDER BY p.id ASC + ")->fetchAll(); + } else { + $rows = $pdo->query("SELECT * FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll(); + } +} catch (Exception $e) {} + +render_header('Admin Penduduk Miskin'); +?> +
+
+
Penduduk Miskin
+ +
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
Import CSV
+
+ + +
Kolom minimal: `kk_nama`/`nama`, `lat`, `lng`.
+
+
+ +
+
Daftar Penduduk Miskin
+ + + + + + + + + + + + + + + + + + +
IDKepala KeluargaStatusRumah IbadahKoordinatAksi
+ Edit + Hapus +
+
+ + diff --git a/webgis-spbu/admin/rumah_ibadah.php b/webgis-spbu/admin/rumah_ibadah.php new file mode 100644 index 0000000..c5606c7 --- /dev/null +++ b/webgis-spbu/admin/rumah_ibadah.php @@ -0,0 +1,166 @@ +prepare("DELETE FROM rumah_ibadah_points WHERE id=:id"); + $stmt->execute([':id' => $id]); + $msg = 'Berhasil dihapus.'; + } catch (Exception $e) { + $err = $e->getMessage(); + } +} + +// Save (create/update) +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $id = (int)($_POST['id'] ?? 0); + $nama = trim($_POST['nama'] ?? ''); + $jenis = trim($_POST['jenis'] ?? 'Lainnya'); + $kontak = trim($_POST['kontak'] ?? ''); + $radius = (float)($_POST['radius_meter'] ?? 300); + $lat = $_POST['latitude'] ?? null; + $lng = $_POST['longitude'] ?? null; + $address = trim($_POST['address'] ?? ''); + + if ($nama === '' || $radius <= 0 || $lat === null || $lng === null) { + $err = 'Data belum lengkap (nama/radius/lat/lng).'; + } else { + try { + $cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasJenis = in_array('jenis', $cols, true); + $hasKontak = in_array('kontak', $cols, true); + + if ($id > 0) { + if ($hasJenis && $hasKontak) { + $stmt = $pdo->prepare("UPDATE rumah_ibadah_points SET nama=:n, jenis=:j, kontak=:k, radius_meter=:r, latitude=:lat, longitude=:lng, address=:a WHERE id=:id"); + $stmt->execute([':n'=>$nama,':j'=>$jenis ?: 'Lainnya',':k'=>$kontak ?: null,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null,':id'=>$id]); + } else { + $stmt = $pdo->prepare("UPDATE rumah_ibadah_points SET nama=:n, radius_meter=:r, latitude=:lat, longitude=:lng, address=:a WHERE id=:id"); + $stmt->execute([':n'=>$nama,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null,':id'=>$id]); + } + $msg = 'Berhasil diupdate.'; + } else { + if ($hasJenis && $hasKontak) { + $stmt = $pdo->prepare("INSERT INTO rumah_ibadah_points (nama, jenis, kontak, radius_meter, latitude, longitude, address) VALUES (:n,:j,:k,:r,:lat,:lng,:a)"); + $stmt->execute([':n'=>$nama,':j'=>$jenis ?: 'Lainnya',':k'=>$kontak ?: null,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null]); + } else { + $stmt = $pdo->prepare("INSERT INTO rumah_ibadah_points (nama, radius_meter, latitude, longitude, address) VALUES (:n,:r,:lat,:lng,:a)"); + $stmt->execute([':n'=>$nama,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null]); + } + $msg = 'Berhasil ditambahkan.'; + } + } catch (Exception $e) { + $err = $e->getMessage(); + } + } +} + +$edit = null; +if (isset($_GET['edit'])) { + $id = (int)$_GET['edit']; + $stmt = $pdo->prepare("SELECT * FROM rumah_ibadah_points WHERE id=:id"); + $stmt->execute([':id' => $id]); + $edit = $stmt->fetch() ?: null; +} + +$rows = []; +try { + $rows = $pdo->query(" + SELECT r.*, + (SELECT COUNT(*) FROM pemukiman_miskin_points p WHERE p.rumah_ibadah_id=r.id) AS binaan_count + FROM rumah_ibadah_points r + ORDER BY r.id ASC + ")->fetchAll(); +} catch (Exception $e) {} + +render_header('Admin Rumah Ibadah'); +?> +
+
+
Rumah Ibadah
+ Reset +
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
Daftar Rumah Ibadah
+ + + + + + + + + + + + + + + + + + + +
IDNamaJenisRadiusBinaanKoordinatAksi
m + Edit + Hapus +
+
+ + diff --git a/webgis-spbu/css/style.css b/webgis-spbu/css/style.css new file mode 100644 index 0000000..58913d7 --- /dev/null +++ b/webgis-spbu/css/style.css @@ -0,0 +1,779 @@ +:root { + --bg: #0d1520; + --bg-soft: #10182c; + --bg-card: #141e2e; + --card: #ffffff; + --text: #e5e7eb; + --text-secondary: #cbd5e1; + --muted: #8b94a5; + --line: #e2e8f0; + --primary: #3b82f6; + --primary-dark: #1d4ed8; + --primary-light: #60a5fa; + --secondary: #6b7280; + --danger: #ef4444; + --success: #10b981; + --warning: #f59e0b; + --shadow: 0 20px 50px rgba(0, 0, 0, 0.25); + --shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 25px 60px rgba(0, 0, 0, 0.3); + --radius: 16px; +} + +* { + box-sizing: border-box; +} + +html, body { + height: 100%; + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: #0f172a; + color: #0f172a; +} + +body.crosshair #map { + cursor: crosshair; +} + +.hidden { + display: none !important; +} + +.app-shell { + display: grid; + grid-template-columns: 320px 1fr; + height: 100vh; +} + +.sidebar { + background: var(--bg); + color: var(--text); + padding: 20px; + overflow-y: auto; + border-right: 1px solid rgba(255,255,255,0.05); + box-shadow: inset 0 0 40px rgba(0,0,0,0.2); +} +.sidebar::-webkit-scrollbar { width: 6px; } +.sidebar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 6px; } + +.btn-back { + display: block; + text-align: center; + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.1); + color: var(--text-secondary); + border-radius: 12px; + padding: 10px; + font-size: 13px; + margin-bottom: 20px; + text-decoration: none; + font-weight: 600; + transition: all 0.2s; +} +.btn-back:hover { background: rgba(255,255,255,0.1); color: #fff; } + +.brand { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 24px; + padding-bottom: 18px; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +.brand-badge { + width: 56px; + height: 56px; + border-radius: 14px; + display: grid; + place-items: center; + background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); + color: #fff; + font-weight: 900; + font-size: 16px; + box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3); + letter-spacing: -0.5px; +} + +.brand h1 { + font-family: 'Outfit', sans-serif; + font-size: 20px; + margin: 0; + font-weight: 800; + background: linear-gradient(to right, #60a5fa, #34d399); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + letter-spacing: -0.5px; +} + +.brand p { + margin: 4px 0 0; + color: var(--muted); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.3px; +} + +.mode-info { + padding: 14px 16px; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(139, 92, 246, 0.1) 100%); + border: 1px solid rgba(147, 197, 253, 0.3); + border-radius: 12px; + margin-bottom: 22px; + font-size: 14px; + color: #e0f2fe; + font-weight: 600; +} + +.action-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 24px; +} + +.btn { + appearance: none; + border: 0; + padding: 12px 10px; + border-radius: 12px; + font-weight: 600; + font-size: 12px; + cursor: pointer; + transition: all .2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + text-align: center; +} +.btn i { font-size: 16px; margin-bottom: 2px; } + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0,0,0,0.25); +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.btn-primary { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: #fff; +} + +.btn-secondary { + background: rgba(255,255,255,0.03); + color: var(--text-secondary); + border: 1px solid rgba(255,255,255,0.08); +} +.btn-secondary:hover { + background: rgba(255,255,255,0.08); + color: #fff; + border-color: rgba(255,255,255,0.15); +} + +.btn-danger { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + color: #fff; +} + +.btn.is-active { + outline: 2px solid rgba(147, 197, 253, 0.6); + outline-offset: 2px; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), 0 4px 12px rgba(0,0,0,0.15); +} + +.stats { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 24px; +} + +.stat-card { + background: linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%); + border: 1px solid rgba(255,255,255,0.12); + border-radius: 14px; + padding: 16px 18px; + display: grid; + gap: 4px; + backdrop-filter: blur(8px); + transition: all .2s ease; +} + +.stat-value { + font-size: 28px; + font-weight: 900; + color: #fff; + letter-spacing: -0.5px; +} + +.stat-label { + color: var(--muted); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.2px; +} + +.panel { + background: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.06); + border-radius: 14px; + padding: 16px 18px; + margin-bottom: 16px; + backdrop-filter: blur(8px); +} + +.panel h2 { + font-size: 14px; + margin: 0 0 12px; + color: #fff; + font-weight: 800; + letter-spacing: -0.3px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 10px; + margin: 8px 0; + font-size: 13px; + color: var(--muted); + font-weight: 500; +} + +.legend-sep { + margin-top: 12px; +} + +.swatch { + width: 16px; + height: 16px; + border-radius: 5px; + display: inline-block; +} + +.nasional { background: #ef4444; } +.provinsi { background: #3b82f6; } +.kabupaten { background: #22c55e; } +.spbu24 { background: #22c55e; } +.spbuNon { background: #ef4444; } +.shm { background: #f59e0b; } +.hgb { background: #8b5cf6; } +.hgu { background: #14b8a6; } +.hp { background: #94a3b8; } +.ri { background: #7c3aed; } +.pmGreen { background: #16a34a; } +.pmRed { background: #dc2626; } + +.help-box { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.05) 100%); + border: 1px solid rgba(147, 197, 253, 0.3); + color: #e0f2fe; + padding: 16px 18px; + border-radius: 12px; + font-size: 13px; + font-weight: 500; + line-height: 1.6; + backdrop-filter: blur(8px); +} + +.list { + display: grid; + gap: 8px; + max-height: 220px; + overflow: auto; + padding-right: 6px; +} + +.list::-webkit-scrollbar { + width: 10px; +} + +.list::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.35); + border-radius: 999px; + border: 2px solid rgba(15, 23, 42, 0.3); +} + +.list-item { + width: 100%; + text-align: left; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.05); + color: #e5e7eb; + border-radius: 12px; + padding: 11px 13px; + cursor: pointer; + transition: all .2s ease; +} + +.list-item:hover { + transform: translateX(4px); + border-color: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.08); +} + +.list-item.is-active { + border-color: rgba(147, 197, 253, 0.55); + background: rgba(37, 99, 235, 0.16); +} + +.list-title { + font-weight: 800; + font-size: 13px; + color: #fff; + letter-spacing: -0.2px; +} + +.list-meta { + margin-top: 4px; + font-size: 11px; + color: var(--muted); + font-weight: 500; +} + +.list-empty { + font-size: 13px; + color: #cbd5e1; + padding: 10px 12px; + border-radius: 14px; + border: 1px dashed rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.04); +} + +.spbu-icon { + background: transparent; + border: 0; +} + +.spbu-dot { + width: 14px; + height: 14px; + border-radius: 999px; + border: 2px solid #ffffff; + box-shadow: 0 10px 18px rgba(15, 23, 42, 0.22); +} + +.spbu-dot--green { background: #22c55e; } +.spbu-dot--red { background: #ef4444; } + +.pm-icon { + background: transparent; + border: 0; +} + +.pm-hit { + width: var(--pm-size, 30px); + height: var(--pm-size, 30px); + border-radius: 999px; + background: #0f172a; + border: 2px solid #ffffff; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 14px 22px rgba(15, 23, 42, 0.22); + cursor: grab; + touch-action: none; +} + +.pm-hit--green { + background: #16a34a; +} + +.pm-hit--red { + background: #dc2626; +} + +.pm-hit:active { + cursor: grabbing; +} + +.pm-svg { + display: block; +} + +.ri-icon { + background: transparent; + border: 0; +} + +.ri-hit { + width: 24px; + height: 24px; + border-radius: 10px; + background: #7c3aed; + border: 2px solid #ffffff; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 12px 18px rgba(15, 23, 42, 0.18); +} + +.ri-svg { + display: block; +} + +.map-area { + position: relative; + background: #e2e8f0; +} + +.map-toolbar { + position: absolute; + top: 14px; + right: 14px; + z-index: 1200; + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(226, 232, 240, 0.9); + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.12); + backdrop-filter: blur(8px); +} + +.map-toolbar-row { + display: flex; + gap: 8px; +} + +.map-tool { + border: 0; + background: #0f172a; + color: #fff; + padding: 9px 12px; + border-radius: 14px; + cursor: pointer; + font-weight: 800; + font-size: 13px; + transition: transform .15s ease, opacity .15s ease; +} + +.map-tool:hover { + transform: translateY(-1px); + opacity: 0.96; +} + +.map-tool:disabled { + opacity: 0.55; + cursor: not-allowed; + transform: none; +} + +.map-tool.is-active { + background: linear-gradient(135deg, #0f172a 0%, #1f2a44 100%); + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.12); +} + +.edit-bar { + position: absolute; + bottom: 14px; + right: 14px; + z-index: 800; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 10px 10px 12px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.94); + border: 1px solid rgba(226, 232, 240, 0.95); + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.14); + backdrop-filter: blur(8px); +} + +.edit-bar-title { + font-weight: 900; + font-size: 13px; + color: #0f172a; + margin-right: 2px; +} + +.edit-bar-btn { + border: 0; + background: #e2e8f0; + color: #0f172a; + padding: 9px 12px; + border-radius: 14px; + cursor: pointer; + font-weight: 900; + font-size: 13px; + transition: transform .15s ease, opacity .15s ease; +} + +.edit-bar-btn.primary { + background: linear-gradient(135deg, var(--primary), var(--primary-dark)); + color: #fff; +} + +.edit-bar-btn.danger { + background: linear-gradient(135deg, var(--danger), #b91c1c); + color: #fff; +} + +.edit-bar-btn.is-active { + outline: 2px solid rgba(15, 23, 42, 0.22); + box-shadow: 0 0 0 5px rgba(239, 68, 68, 0.12), 0 14px 30px rgba(15, 23, 42, 0.12); +} + +.edit-bar-btn:hover { + transform: translateY(-1px); + opacity: 0.96; +} + +.edit-bar-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +#map { + width: 100%; + height: 100%; + position: relative; + z-index: 0; +} + +.leaflet-control-container .leaflet-draw { + display: none !important; +} + +.modal { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.54); + display: grid; + place-items: center; + z-index: 9999; + padding: 20px; +} + +.modal-card { + width: 100%; + max-width: 540px; + background: #fff; + border-radius: 22px; + box-shadow: var(--shadow); + overflow: hidden; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 18px 20px; + border-bottom: 1px solid var(--line); + background: linear-gradient(180deg, #f8fafc 0%, #fff 100%); +} + +.modal-header h3 { + margin: 0; + font-size: 18px; +} + +.close-btn { + border: 0; + background: transparent; + font-size: 30px; + line-height: 1; + cursor: pointer; + color: #475569; +} + +#featureForm { + padding: 18px 20px 20px; +} + +.form-section { + display: grid; + gap: 10px; +} + +.form-section h4 { + margin: 0 0 6px; + font-size: 15px; + color: #0f172a; +} + +label { + font-size: 13px; + font-weight: 700; + color: #334155; +} + +input, textarea, select { + width: 100%; + border: 1px solid #cbd5e1; + background: #fff; + border-radius: 14px; + padding: 11px 12px; + font: inherit; + color: #0f172a; +} + +input[readonly] { + background: #f8fafc; + color: #475569; +} + +.checkbox-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + font-weight: 600; + color: var(--text-secondary); + background: rgba(255,255,255,0.03); + border-radius: 10px; + margin-bottom: 6px; + font-size: 13px; + cursor: pointer; + transition: all 0.2s; +} +.checkbox-row:hover { background: rgba(255,255,255,0.06); color: #fff; } + +.checkbox-row input { + accent-color: var(--primary); + width: 16px; + height: 16px; + cursor: pointer; +} + +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.notice { + margin: 0 0 14px; + padding: 12px 14px; + border-radius: 14px; + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + font-size: 14px; + line-height: 1.45; +} + +.modal-actions { + display: flex; + gap: 10px; + margin-top: 18px; +} + +.modal-actions .btn { + flex: 1; + box-shadow: none; +} + +.leaflet-popup-content-wrapper { + border-radius: 14px; +} + +.leaflet-popup-content { + margin: 12px 14px; + font-size: 13px; + line-height: 1.5; +} + +.popup-actions { + display: flex; + gap: 8px; + margin-top: 10px; + flex-wrap: wrap; +} + +.popup-btn { + border: 0; + padding: 7px 10px; + border-radius: 10px; + cursor: pointer; + font-weight: 700; + color: #fff; +} + +.popup-btn.edit { background: #2563eb; } +.popup-btn.spatial { background: #7c3aed; } +.popup-btn.delete { background: #dc2626; } + +.toast { + position: fixed; + left: calc(320px + 16px); + bottom: 16px; + z-index: 10000; + max-width: min(520px, calc(100vw - 32px)); + padding: 12px 14px; + border-radius: 16px; + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.22); + border: 1px solid rgba(226, 232, 240, 0.9); + background: rgba(255, 255, 255, 0.96); + color: #0f172a; + font-weight: 700; + line-height: 1.35; + cursor: pointer; + word-break: break-word; +} + +.toast.toast-success { + border-color: rgba(34, 197, 94, 0.35); +} + +.toast.toast-error { + border-color: rgba(239, 68, 68, 0.35); +} + +.toast.toast-info { + border-color: rgba(59, 130, 246, 0.35); +} + +@media (max-width: 960px) { + .app-shell { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid rgba(255,255,255,0.05); + padding: 20px; + } + + .action-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .stats { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .map-area { + min-height: 60vh; + } + + .map-toolbar { + top: 10px; + right: 10px; + left: 10px; + justify-content: space-between; + } + + .edit-bar { + left: 10px; + right: 10px; + bottom: 10px; + justify-content: space-between; + } + + .toast { + left: 16px; + } +} diff --git a/webgis-spbu/fetch_geojson.js b/webgis-spbu/fetch_geojson.js new file mode 100644 index 0000000..0dde7e3 --- /dev/null +++ b/webgis-spbu/fetch_geojson.js @@ -0,0 +1,61 @@ +const https = require('https'); +const fs = require('fs'); + +const kecamatans = [ + "Kecamatan Pontianak Barat", + "Kecamatan Pontianak Kota", + "Kecamatan Pontianak Selatan", + "Kecamatan Pontianak Tenggara", + "Kecamatan Pontianak Timur", + "Kecamatan Pontianak Utara" +]; + +const featureCollection = { + type: "FeatureCollection", + features: [] +}; + +let completed = 0; + +function fetchGeojson(name) { + const url = `https://nominatim.openstreetmap.org/search.php?q=${encodeURIComponent(name)}&polygon_geojson=1&format=jsonv2`; + const options = { + headers: { 'User-Agent': 'WebGIS-SPBU-App/1.0' } + }; + + https.get(url, options, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + const results = JSON.parse(data); + const geomResult = results.find(r => r.geojson && r.category === 'boundary' && r.type === 'administrative'); + if (geomResult) { + featureCollection.features.push({ + type: "Feature", + properties: { + Kecamatan: name.replace('Kecamatan ', ''), + Name: name + }, + geometry: geomResult.geojson + }); + console.log("Fetched " + name); + } else { + console.log("Not found for " + name); + } + completed++; + if (completed === kecamatans.length) { + fs.writeFileSync('js/pontianak_kecamatan.geojson', JSON.stringify(featureCollection, null, 2)); + console.log("All done! Saved to js/pontianak_kecamatan.geojson"); + } + }); + }).on('error', (e) => { + console.error(e); + }); +} + +// Nominatim has a usage policy of 1 request per second. We will stagger the requests. +kecamatans.forEach((kec, i) => { + setTimeout(() => { + fetchGeojson(kec); + }, i * 1500); // 1.5 seconds apart +}); diff --git a/webgis-spbu/fetch_osm.js b/webgis-spbu/fetch_osm.js new file mode 100644 index 0000000..b8dbbd1 --- /dev/null +++ b/webgis-spbu/fetch_osm.js @@ -0,0 +1,29 @@ +const https = require('https'); +const fs = require('fs'); + +const query = `[out:json]; +area["name"="Pontianak"]->.searchArea; +( + relation["admin_level"="6"](area.searchArea); +); +out geom;`; + +const options = { + hostname: 'overpass-api.de', + path: '/api/interpreter', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } +}; + +const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + fs.writeFileSync('osm_pontianak.json', data); + console.log('Done downloading OSM data.'); + }); +}); +req.write(query); +req.end(); diff --git a/webgis-spbu/index.html b/webgis-spbu/index.html new file mode 100644 index 0000000..2184e29 --- /dev/null +++ b/webgis-spbu/index.html @@ -0,0 +1,359 @@ + + + + + + WebGIS SPBU, Jalan, Parsil, Rumah Ibadah, dan Pemukiman + + + + + + + + + + + + + +
+ + + + + +
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + diff --git a/webgis-spbu/js/app.js b/webgis-spbu/js/app.js new file mode 100644 index 0000000..6dd6e0a --- /dev/null +++ b/webgis-spbu/js/app.js @@ -0,0 +1,2874 @@ +// ================= CONFIG ================= +const INITIAL_CENTER = [-0.0263, 109.3425]; +const INITIAL_ZOOM = 13; + +// ================= MAP ================= +const map = L.map('map').setView(INITIAL_CENTER, INITIAL_ZOOM); +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 20, + attribution: '© OpenStreetMap contributors' +}).addTo(map); + +// ================= GROUPS ================= +const editableGroup = L.featureGroup().addTo(map); // pending draw layer (unsaved) +const points24Layer = L.layerGroup().addTo(map); +const pointsNon24Layer = L.layerGroup().addTo(map); +const jalanLayer = L.layerGroup().addTo(map); +const parsilLayer = L.layerGroup().addTo(map); +const pemukimanLayer = L.layerGroup().addTo(map); +const rumahIbadahLayer = L.layerGroup().addTo(map); +const rumahIbadahRadiusLayer = L.layerGroup().addTo(map); +const sektorLayer = L.layerGroup().addTo(map); + +// ================= STATE ================= +const state = { + featureType: null, // point | jalan | parsil + currentLayer: null, // LatLng | L.Layer + pendingLayer: null, // L.Layer (polyline/polygon) before saved + activeDrawer: null, + pointFilter: 'all', // all | 24 | non24 + pointsData: [], + pemukimanData: [], + rumahIbadahData: [], + counts: { points: 0, jalan: 0, parsil: 0, pemukiman: 0, rumahIbadah: 0, sektor: 0 }, + indices: { + points: new Map(), + jalan: new Map(), + parsil: new Map(), + pemukiman: new Map(), + rumahIbadah: new Map(), + sektor: new Map() + }, + selected: { + points: null, + jalan: null, + parsil: null, + pemukiman: null, + rumahIbadah: null + }, + editing: null, // { type, id, layer, backup } for Edit Data (modal) + locationEditing: null // { type, id, layer, backup, attrs, translate, vertexDeleteMode } +}; + +const warned = new Set(); +function warnOnce(key, message) { + if (warned.has(key)) return; + warned.add(key); + showToast(message, 'error', 6500); +} + +// ================= DRAW (kept, but toolbar is hidden via CSS) ================= +const drawControl = new L.Control.Draw({ + draw: { + polyline: true, + polygon: true, + marker: false, + rectangle: false, + circle: false, + circlemarker: false + }, + edit: { featureGroup: editableGroup } +}); +map.addControl(drawControl); + +// ================= DOM HELPERS ================= +const el = { + modal: document.getElementById('featureModal'), + modalTitle: document.getElementById('modalTitle'), + modeInfo: document.getElementById('modeInfo'), + toast: document.getElementById('toast'), + countPoints: document.getElementById('countPoints'), + countJalan: document.getElementById('countJalan'), + countParsil: document.getElementById('countParsil'), + countRumahIbadah: document.getElementById('countRumahIbadah'), + countPemukiman: document.getElementById('countPemukiman'), + countSektor: document.getElementById('countSektor'), + togglePoints: document.getElementById('togglePoints'), + toggleJalan: document.getElementById('toggleJalan'), + toggleParsil: document.getElementById('toggleParsil'), + toggleRumahIbadah: document.getElementById('toggleRumahIbadah'), + togglePemukiman: document.getElementById('togglePemukiman'), + toggleSektor: document.getElementById('toggleSektor'), + pointsList: document.getElementById('pointsList'), + jalanList: document.getElementById('jalanList'), + parsilList: document.getElementById('parsilList'), + rumahIbadahList: document.getElementById('rumahIbadahList'), + pemukimanList: document.getElementById('pemukimanList'), + sektorList: document.getElementById('sektorList'), + btnModePoint: document.getElementById('btnModePoint'), + btnModeJalan: document.getElementById('btnModeJalan'), + btnModeParsil: document.getElementById('btnModeParsil'), + btnModeRumahIbadah: document.getElementById('btnModeRumahIbadah'), + btnModePemukiman: document.getElementById('btnModePemukiman'), + btnResetMode: document.getElementById('btnResetMode'), + btnResetView: document.getElementById('btnResetView'), + btnMyLocation: document.getElementById('btnMyLocation'), + btnFilter24: document.getElementById('btnFilter24'), + btnFilterNon24: document.getElementById('btnFilterNon24'), + editBar: document.getElementById('editBar'), + editBarTitle: document.getElementById('editBarTitle'), + btnSaveLocation: document.getElementById('btnSaveLocation'), + btnCancelLocation: document.getElementById('btnCancelLocation'), + btnDeleteVertex: document.getElementById('btnDeleteVertex'), + pointFields: document.getElementById('pointFields'), + roadFields: document.getElementById('roadFields'), + parsilFields: document.getElementById('parsilFields'), + rumahIbadahFields: document.getElementById('rumahIbadahFields'), + pemukimanFields: document.getElementById('pemukimanFields'), + featureForm: document.getElementById('featureForm'), + formNotice: document.getElementById('formNotice'), + btnSubmitFeature: document.getElementById('btnSubmitFeature'), + statusJalan: document.getElementById('statusJalan'), + statusParsil: document.getElementById('statusParsil'), + namaPoint: document.getElementById('namaPoint'), + noPoint: document.getElementById('noPoint'), + deskripsiPoint: document.getElementById('deskripsiPoint'), + status24Jam: document.getElementById('status24Jam'), + latPoint: document.getElementById('latPoint'), + lngPoint: document.getElementById('lngPoint'), + namaJalan: document.getElementById('namaJalan'), + + // Rumah Ibadah + namaRumahIbadah: document.getElementById('namaRumahIbadah'), + jenisRumahIbadah: document.getElementById('jenisRumahIbadah'), + kontakRumahIbadah: document.getElementById('kontakRumahIbadah'), + radiusRumahIbadah: document.getElementById('radiusRumahIbadah'), + alamatRumahIbadah: document.getElementById('alamatRumahIbadah'), + latRumahIbadah: document.getElementById('latRumahIbadah'), + lngRumahIbadah: document.getElementById('lngRumahIbadah'), + + // Pemukiman Miskin + kkNamaPemukiman: document.getElementById('kkNamaPemukiman'), + nikPemukiman: document.getElementById('nikPemukiman'), + jumlahAnggotaPemukiman: document.getElementById('jumlahAnggotaPemukiman'), + statusBantuanPemukiman: document.getElementById('statusBantuanPemukiman'), + jenisBantuanPemukiman: document.getElementById('jenisBantuanPemukiman'), + tanggalBantuanPemukiman: document.getElementById('tanggalBantuanPemukiman'), + alamatPemukiman: document.getElementById('alamatPemukiman'), + kelurahanPemukiman: document.getElementById('kelurahanPemukiman'), + kecamatanPemukiman: document.getElementById('kecamatanPemukiman'), + latPemukiman: document.getElementById('latPemukiman'), + lngPemukiman: document.getElementById('lngPemukiman') +}; + +let toastTimer = null; +function showToast(message, type = 'info', timeoutMs = 3500) { + if (!el.toast) return; + if (!el.toast.__dismissBound) { + el.toast.__dismissBound = true; + el.toast.title = 'Klik untuk menutup'; + el.toast.addEventListener('click', () => { + el.toast.classList.add('hidden'); + }); + } + if (toastTimer) window.clearTimeout(toastTimer); + + el.toast.classList.remove('hidden', 'toast-success', 'toast-error', 'toast-info'); + el.toast.classList.add(`toast-${type}`); + el.toast.textContent = message; + + toastTimer = window.setTimeout(() => { + el.toast.classList.add('hidden'); + }, timeoutMs); +} + +function setFormNotice(message, type = 'info') { + if (!el.formNotice) return; + el.formNotice.textContent = message || ''; + el.formNotice.classList.remove('hidden'); + el.formNotice.style.borderColor = + type === 'error' ? '#fecaca' : + type === 'success' ? '#bbf7d0' : + '#bfdbfe'; + el.formNotice.style.background = + type === 'error' ? '#fef2f2' : + type === 'success' ? '#f0fdf4' : + '#eff6ff'; + el.formNotice.style.color = + type === 'error' ? '#991b1b' : + type === 'success' ? '#166534' : + '#1d4ed8'; +} + +function clearFormNotice() { + if (!el.formNotice) return; + el.formNotice.textContent = ''; + el.formNotice.classList.add('hidden'); +} + +function setSubmitting(isSubmitting) { + if (!el.btnSubmitFeature) return; + el.btnSubmitFeature.disabled = Boolean(isSubmitting); + el.btnSubmitFeature.textContent = isSubmitting ? 'Menyimpan...' : 'Simpan Data'; +} + +function escapeHtml(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function parseApiJson(response) { + // Support both {success:true} and {status:"success"} styles. + const success = Boolean(response?.success) || response?.status === 'success'; + const message = + response?.message || + (success ? 'Berhasil' : 'Gagal memproses permintaan'); + return { success, message }; +} + +async function fetchMaybeJson(url, options) { + const res = await fetch(url, options); + const text = await res.text(); + try { + return { res, json: JSON.parse(text), text }; + } catch { + return { res, json: null, text }; + } +} + +function getJalanColor(status) { + if (status === 'Nasional') return '#ef4444'; + if (status === 'Provinsi') return '#3b82f6'; + if (status === 'Kabupaten') return '#22c55e'; + return '#64748b'; +} + +function getParsilColor(status) { + if (status === 'SHM') return '#f59e0b'; + if (status === 'HGB') return '#8b5cf6'; + if (status === 'HGU') return '#14b8a6'; + if (status === 'HP') return '#94a3b8'; + return '#cbd5e1'; +} + +function makeSpbuIcon(is24) { + const dotClass = is24 ? 'spbu-dot spbu-dot--green' : 'spbu-dot spbu-dot--red'; + return L.divIcon({ + className: 'spbu-icon', + html: `
`, + iconSize: [18, 18], + iconAnchor: [9, 9], + popupAnchor: [0, -8] + }); +} + +function makePemukimanIcon({ isEdit = false, isCovered = false } = {}) { + // Keep it distinct from SPBU but not too big/flashy. + const size = isEdit ? 34 : 26; + const inner = isEdit ? 20 : 16; + const hitClass = isCovered ? 'pm-hit pm-hit--green' : 'pm-hit pm-hit--red'; + const html = ` + + `; + + return L.divIcon({ + className: 'pm-icon', + html, + iconSize: [size, size], + iconAnchor: [size / 2, size / 2], + popupAnchor: [0, -size / 2] + }); +} + +function makeRumahIbadahIcon() { + // Simple + recognizable icon (not too "perfect"). + const size = 24; + const html = ` + + `; + + return L.divIcon({ + className: 'ri-icon', + html, + iconSize: [size, size], + iconAnchor: [size / 2, size / 2], + popupAnchor: [0, -size / 2] + }); +} + +function computePemukimanCoverage(latlng) { + if (!latlng || !Array.isArray(state.rumahIbadahData) || state.rumahIbadahData.length === 0) { + return { covered: null, rumahIbadahId: null, jarakMeter: null }; + } + + let best = null; + for (const row of state.rumahIbadahData) { + const lat = Number(row?.latitude); + const lng = Number(row?.longitude); + const radius = Number(row?.radius_meter); + if (!Number.isFinite(lat) || !Number.isFinite(lng) || !Number.isFinite(radius) || radius <= 0) continue; + + const d = map.distance(latlng, L.latLng(lat, lng)); + if (d <= radius) { + if (!best || d < best.jarakMeter) { + best = { rumahIbadahId: row?.id ?? null, jarakMeter: d }; + } + } + } + + if (!best) return { covered: false, rumahIbadahId: null, jarakMeter: null }; + return { covered: true, rumahIbadahId: best.rumahIbadahId, jarakMeter: best.jarakMeter }; +} + + + +function updateStats() { + if (el.countPoints) el.countPoints.textContent = state.counts.points; + if (el.countJalan) el.countJalan.textContent = state.counts.jalan; + if (el.countParsil) el.countParsil.textContent = state.counts.parsil; + if (el.countRumahIbadah) el.countRumahIbadah.textContent = state.counts.rumahIbadah; + if (el.countPemukiman) el.countPemukiman.textContent = state.counts.pemukiman; + if (el.countSektor) el.countSektor.textContent = state.counts.sektor; + + // Robust update for any additional stat cards (e.g., Rumah Ibadah, Pemukiman Miskin) + try { + const cards = document.querySelectorAll('.stat-card'); + for (const card of cards) { + const labelEl = card.querySelector('.stat-label'); + const valueEl = card.querySelector('.stat-value'); + const label = String(labelEl?.textContent ?? '').trim().toLowerCase(); + if (!label || !valueEl) continue; + + if (label.includes('rumah') && label.includes('ibadah')) { + valueEl.textContent = String(state.counts.rumahIbadah ?? 0); + } + if (label.includes('pemukiman')) { + valueEl.textContent = String(state.counts.pemukiman ?? 0); + } + } + } catch { + // ignore + } +} + +function isSpbu24(row) { + const v = row?.status_24jam; + if (v === true) return true; + if (typeof v === 'number') return v === 1; + + const s = String(v ?? '').trim().toLowerCase(); + if (!s) return false; + if (s === '1' || s === 'ya' || s === 'yes') return true; + if (s === '0' || s === 'tidak' || s === 'no') return false; + + const has24 = s.includes('24'); + const isNeg = s.includes('tidak') || s.includes('non'); + if (has24 && isNeg) return false; + if (has24) return true; + return false; +} + +function applyPointFilter() { + const mode = state.pointFilter || 'all'; + if (el.btnFilter24) el.btnFilter24.classList.toggle('is-active', mode === '24'); + if (el.btnFilterNon24) el.btnFilterNon24.classList.toggle('is-active', mode === 'non24'); + + const shouldShow = (row) => { + if (mode === '24') return isSpbu24(row); + if (mode === 'non24') return !isSpbu24(row); + return true; + }; + + // Map visibility is handled by applyLayerVisibility based on state.pointFilter + applyLayerVisibility(); + + const filteredRows = Array.isArray(state.pointsData) + ? state.pointsData.filter((row) => shouldShow(row)) + : []; + renderPointsList(filteredRows); +} + +function togglePointFilter(next) { + state.pointFilter = state.pointFilter === next ? 'all' : next; + if (el.togglePoints) el.togglePoints.checked = true; + applyLayerVisibility(); + applyPointFilter(); + + const rows = Array.isArray(state.pointsData) ? state.pointsData : []; + const n24 = rows.filter((r) => isSpbu24(r)).length; + const nNon = rows.length - n24; + if (state.pointFilter === '24') showToast(`Filter: SPBU 24 Jam (${n24})`, 'info', 2200); + else if (state.pointFilter === 'non24') showToast(`Filter: SPBU Tidak 24 Jam (${nNon})`, 'info', 2200); + else showToast(`Filter: Semua SPBU (${rows.length})`, 'info', 1800); +} + +function applyLayerVisibility() { + if (el.togglePoints) { + if (!el.togglePoints.checked) { + map.removeLayer(points24Layer); + map.removeLayer(pointsNon24Layer); + } else { + const mode = state.pointFilter || 'all'; + if (mode === '24') { + points24Layer.addTo(map); + map.removeLayer(pointsNon24Layer); + } else if (mode === 'non24') { + pointsNon24Layer.addTo(map); + map.removeLayer(points24Layer); + } else { + points24Layer.addTo(map); + pointsNon24Layer.addTo(map); + } + } + } + + if (el.toggleJalan) { + if (el.toggleJalan.checked) jalanLayer.addTo(map); + else map.removeLayer(jalanLayer); + } + + if (el.toggleParsil) { + if (el.toggleParsil.checked) parsilLayer.addTo(map); + else map.removeLayer(parsilLayer); + } + + if (el.toggleRumahIbadah) { + if (el.toggleRumahIbadah.checked) { + rumahIbadahLayer.addTo(map); + rumahIbadahRadiusLayer.addTo(map); + } else { + map.removeLayer(rumahIbadahLayer); + map.removeLayer(rumahIbadahRadiusLayer); + } + } + + if (el.togglePemukiman) { + if (el.togglePemukiman.checked) pemukimanLayer.addTo(map); + else map.removeLayer(pemukimanLayer); + } + + if (el.toggleSektor) { + if (el.toggleSektor.checked) sektorLayer.addTo(map); + else map.removeLayer(sektorLayer); + } + + updateStats(); +} + +function setActiveModeButton(type) { + const buttons = [ + el.btnModePoint, + el.btnModeJalan, + el.btnModeParsil, + el.btnModeRumahIbadah, + el.btnModePemukiman + ].filter(Boolean); + for (const btn of buttons) btn.classList.remove('is-active'); + if (type === 'point' && el.btnModePoint) el.btnModePoint.classList.add('is-active'); + if (type === 'jalan' && el.btnModeJalan) el.btnModeJalan.classList.add('is-active'); + if (type === 'parsil' && el.btnModeParsil) el.btnModeParsil.classList.add('is-active'); + if (type === 'rumahIbadah' && el.btnModeRumahIbadah) el.btnModeRumahIbadah.classList.add('is-active'); + if (type === 'pemukiman' && el.btnModePemukiman) el.btnModePemukiman.classList.add('is-active'); +} + +function updateModeInfo() { + if (!el.modeInfo) return; + + if (state.locationEditing?.type === 'point') { + el.modeInfo.textContent = 'Mode: Edit Lokasi Point (geser marker)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.locationEditing?.type === 'jalan') { + el.modeInfo.textContent = 'Mode: Edit Lokasi Jalan (drag garis / geser vertex)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.locationEditing?.type === 'parsil') { + el.modeInfo.textContent = 'Mode: Edit Lokasi Parsil (drag area / geser vertex)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.locationEditing?.type === 'pemukiman') { + el.modeInfo.textContent = 'Mode: Edit Lokasi Pemukiman Miskin (geser marker)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.locationEditing?.type === 'rumahIbadah') { + el.modeInfo.textContent = 'Mode: Edit Lokasi Rumah Ibadah (geser marker)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.editing?.type === 'point') { + el.modeInfo.textContent = 'Mode: Edit Point (geser marker / ubah atribut)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.editing?.type === 'jalan') { + el.modeInfo.textContent = 'Mode: Edit Jalan (geser titik garis)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.editing?.type === 'parsil') { + el.modeInfo.textContent = 'Mode: Edit Parsil (geser titik poligon)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.editing?.type === 'rumahIbadah') { + el.modeInfo.textContent = 'Mode: Edit Rumah Ibadah (geser marker / ubah atribut)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.editing?.type === 'pemukiman') { + el.modeInfo.textContent = 'Mode: Edit Pemukiman Miskin (geser marker / ubah atribut)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.featureType === 'point') { + el.modeInfo.textContent = 'Mode: Tambah Point (klik peta)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.featureType === 'jalan') { + el.modeInfo.textContent = 'Mode: Tambah Jalan (gambar garis)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.featureType === 'parsil') { + el.modeInfo.textContent = 'Mode: Tambah Parsil (gambar poligon)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.featureType === 'rumahIbadah') { + el.modeInfo.textContent = 'Mode: Tambah Rumah Ibadah (klik peta)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + if (state.featureType === 'pemukiman') { + el.modeInfo.textContent = 'Mode: Tambah Pemukiman Miskin (klik peta)'; + if (el.btnResetMode) el.btnResetMode.disabled = false; + return; + } + + el.modeInfo.textContent = 'Mode: Tidak aktif'; + if (el.btnResetMode) el.btnResetMode.disabled = true; +} + +function clearPendingLayer() { + if (state.pendingLayer) { + editableGroup.removeLayer(state.pendingLayer); + state.pendingLayer = null; + } + state.currentLayer = null; +} + +function showSection(type) { + const setEnabled = (section, enabled) => { + if (!section) return; + section.querySelectorAll('input, textarea, select, button').forEach((node) => { + node.disabled = !enabled; + }); + }; + + el.pointFields?.classList.add('hidden'); + el.roadFields?.classList.add('hidden'); + el.parsilFields?.classList.add('hidden'); + el.rumahIbadahFields?.classList.add('hidden'); + el.pemukimanFields?.classList.add('hidden'); + setEnabled(el.pointFields, false); + setEnabled(el.roadFields, false); + setEnabled(el.parsilFields, false); + setEnabled(el.rumahIbadahFields, false); + setEnabled(el.pemukimanFields, false); + + if (type === 'point') { + el.pointFields?.classList.remove('hidden'); + setEnabled(el.pointFields, true); + } + if (type === 'jalan') { + el.roadFields?.classList.remove('hidden'); + setEnabled(el.roadFields, true); + } + if (type === 'parsil') { + el.parsilFields?.classList.remove('hidden'); + setEnabled(el.parsilFields, true); + } + + if (type === 'rumahIbadah') { + el.rumahIbadahFields?.classList.remove('hidden'); + setEnabled(el.rumahIbadahFields, true); + } + + if (type === 'pemukiman') { + el.pemukimanFields?.classList.remove('hidden'); + setEnabled(el.pemukimanFields, true); + } +} + +function openModal() { + if (!el.modal) return; + + if (state.featureType === 'point') el.modalTitle.textContent = 'Input Data Point SPBU'; + if (state.featureType === 'jalan') el.modalTitle.textContent = 'Input Data Jalan'; + if (state.featureType === 'parsil') el.modalTitle.textContent = 'Input Data Parsil'; + if (state.featureType === 'rumahIbadah') el.modalTitle.textContent = 'Input Data Rumah Ibadah'; + if (state.featureType === 'pemukiman') el.modalTitle.textContent = 'Input Data Pemukiman Miskin'; + + clearFormNotice(); + setSubmitting(false); + if (el.btnSubmitFeature) { + el.btnSubmitFeature.textContent = state.editing ? 'Update Data' : 'Simpan Data'; + } + showSection(state.featureType); + el.modal.classList.remove('hidden'); + + // focus first input of the active section + if (state.featureType === 'point') el.namaPoint?.focus(); + if (state.featureType === 'jalan') el.namaJalan?.focus(); + if (state.featureType === 'parsil') el.statusParsil?.focus(); + if (state.featureType === 'rumahIbadah') el.namaRumahIbadah?.focus(); + if (state.featureType === 'pemukiman') el.kkNamaPemukiman?.focus(); +} + +function backupLayerGeometry(layer) { + if (layer?.getLatLng && layer?.setLatLng) { + const ll = layer.getLatLng(); + return { kind: 'marker', latlng: [ll.lat, ll.lng] }; + } + + if (layer?.getLatLngs && layer?.setLatLngs) { + const toTuple = (latlng) => [latlng.lat, latlng.lng]; + const clone = (latlngs) => + Array.isArray(latlngs) + ? latlngs.map((v) => (Array.isArray(v) ? clone(v) : toTuple(v))) + : latlngs; + return { kind: 'path', latlngs: clone(layer.getLatLngs()) }; + } + + return { kind: 'unknown' }; +} + +function restoreLayerGeometry(layer, backup) { + if (!layer || !backup) return; + + if (backup.kind === 'marker' && Array.isArray(backup.latlng)) { + layer.setLatLng(L.latLng(backup.latlng[0], backup.latlng[1])); + return; + } + + if (backup.kind === 'path' && backup.latlngs && layer.setLatLngs) { + const toLatLng = (pair) => L.latLng(pair[0], pair[1]); + const rebuild = (latlngs) => + Array.isArray(latlngs) + ? latlngs.map((v) => (Array.isArray(v) && typeof v[0] === 'number' ? toLatLng(v) : rebuild(v))) + : latlngs; + layer.setLatLngs(rebuild(backup.latlngs)); + } +} + +function enableLayerEditing(layer) { + if (!layer) return; + if (layer.dragging && layer.dragging.enable) { + layer.dragging.enable(); + } + if (layer.editing && layer.editing.enable) { + layer.editing.enable(); + } +} + +function disableLayerEditing(layer) { + if (!layer) return; + if (layer.dragging && layer.dragging.disable) { + layer.dragging.disable(); + } + if (layer.editing && layer.editing.disable) { + layer.editing.disable(); + } +} + +function startEditPoint(id, marker, row) { + resetMode(); + const backup = backupLayerGeometry(marker); + backup.is24 = Number(row?.status_24jam) === 1; + state.editing = { type: 'point', id: Number(id), layer: marker, backup }; + state.featureType = 'point'; + state.currentLayer = null; + + el.namaPoint.value = row?.nama ?? ''; + el.noPoint.value = row?.no ?? ''; + el.deskripsiPoint.value = row?.deskripsi ?? ''; + el.status24Jam.checked = Number(row?.status_24jam) === 1; + + const ll = marker.getLatLng(); + el.latPoint.value = ll.lat; + el.lngPoint.value = ll.lng; + + marker.setIcon(makeSpbuIcon(el.status24Jam.checked)); + if (marker.__spbuDragHandler) { + marker.off('drag', marker.__spbuDragHandler); + marker.off('dragend', marker.__spbuDragHandler); + } + marker.__spbuDragHandler = () => { + const current = marker.getLatLng(); + el.latPoint.value = current.lat; + el.lngPoint.value = current.lng; + }; + marker.on('drag', marker.__spbuDragHandler); + marker.on('dragend', marker.__spbuDragHandler); + + enableLayerEditing(marker); + updateModeInfo(); + setActiveListItem('points', id); + setFormNotice('Edit: geser marker jika perlu, lalu klik "Update Data".', 'info'); + openModal(); +} + +function startEditJalan(id, layer, props) { + resetMode(); + const backup = backupLayerGeometry(layer); + backup.status = props?.status_jalan ?? 'Nasional'; + state.editing = { type: 'jalan', id: Number(id), layer, backup }; + state.featureType = 'jalan'; + state.currentLayer = null; + + el.namaJalan.value = props?.nama_jalan ?? ''; + el.statusJalan.value = props?.status_jalan ?? 'Nasional'; + + enableLayerEditing(layer); + updateModeInfo(); + setActiveListItem('jalan', id); + setFormNotice('Edit: geser titik garis, panjang akan dihitung otomatis saat update.', 'info'); + openModal(); +} + +function startEditParsil(id, layer, props) { + resetMode(); + const backup = backupLayerGeometry(layer); + backup.status = props?.status_kepemilikan ?? 'SHM'; + state.editing = { type: 'parsil', id: Number(id), layer, backup }; + state.featureType = 'parsil'; + state.currentLayer = null; + + el.statusParsil.value = props?.status_kepemilikan ?? 'SHM'; + + enableLayerEditing(layer); + updateModeInfo(); + setActiveListItem('parsil', id); + setFormNotice('Edit: geser titik poligon, luas akan dihitung otomatis saat update.', 'info'); + openModal(); +} + +function startEditRumahIbadah(id, marker, row) { + resetMode(); + const backup = backupLayerGeometry(marker); + state.editing = { type: 'rumahIbadah', id: Number(id), layer: marker, backup }; + state.featureType = 'rumahIbadah'; + state.currentLayer = null; + + el.namaRumahIbadah.value = row?.nama ?? ''; + el.jenisRumahIbadah.value = row?.jenis ?? 'Lainnya'; + el.kontakRumahIbadah.value = row?.kontak ?? ''; + el.radiusRumahIbadah.value = row?.radius_meter ?? ''; + el.alamatRumahIbadah.value = row?.address ?? ''; + + const ll = marker.getLatLng(); + el.latRumahIbadah.value = ll.lat; + el.lngRumahIbadah.value = ll.lng; + + if (marker.__riDragHandler) { + marker.off('drag', marker.__riDragHandler); + marker.off('dragend', marker.__riDragHandler); + } + marker.__riDragHandler = () => { + const current = marker.getLatLng(); + el.latRumahIbadah.value = current.lat; + el.lngRumahIbadah.value = current.lng; + }; + marker.on('drag', marker.__riDragHandler); + marker.on('dragend', marker.__riDragHandler); + + enableLayerEditing(marker); + updateModeInfo(); + setActiveListItem('rumahIbadah', id); + setFormNotice('Edit: geser marker jika perlu, lalu klik "Update Data".', 'info'); + openModal(); +} + +function startEditPemukiman(id, marker, row) { + resetMode(); + const backup = backupLayerGeometry(marker); + const ibadahId = row?.rumah_ibadah_id; + backup.covered = ibadahId !== null && ibadahId !== undefined && String(ibadahId).trim() !== '' && String(ibadahId) !== '0'; + state.editing = { type: 'pemukiman', id: Number(id), layer: marker, backup, dragEase: null }; + state.featureType = 'pemukiman'; + state.currentLayer = null; + + el.kkNamaPemukiman.value = row?.kk_nama ?? row?.nama ?? ''; + el.nikPemukiman.value = row?.nik ?? ''; + el.jumlahAnggotaPemukiman.value = row?.jumlah_anggota ?? ''; + el.statusBantuanPemukiman.value = row?.status_bantuan ?? 'Belum dibantu'; + el.jenisBantuanPemukiman.value = row?.jenis_bantuan ?? ''; + el.tanggalBantuanPemukiman.value = row?.tanggal_bantuan ?? ''; + el.alamatPemukiman.value = row?.address ?? ''; + el.kelurahanPemukiman.value = row?.kelurahan ?? ''; + el.kecamatanPemukiman.value = row?.kecamatan ?? ''; + + const ll = marker.getLatLng(); + el.latPemukiman.value = ll.lat; + el.lngPemukiman.value = ll.lng; + + marker.setIcon(makePemukimanIcon({ isEdit: true, isCovered: Boolean(backup.covered) })); + if (marker.__pmDragHandler) { + marker.off('drag', marker.__pmDragHandler); + marker.off('dragend', marker.__pmDragHandler); + } + marker.__currentCovered = Boolean(backup.covered); + marker.__pmDragHandler = () => { + const current = marker.getLatLng(); + el.latPemukiman.value = current.lat; + el.lngPemukiman.value = current.lng; + + const cov = computePemukimanCoverage(current); + if (cov.covered !== null && marker.__currentCovered !== cov.covered) { + marker.__currentCovered = cov.covered; + marker.setIcon(makePemukimanIcon({ isEdit: true, isCovered: cov.covered === true })); + } + }; + marker.on('drag', marker.__pmDragHandler); + marker.on('dragend', marker.__pmDragHandler); + + enableLayerEditing(marker); + updateModeInfo(); + setActiveListItem('pemukiman', id); + setFormNotice('Edit: geser marker jika perlu, lalu klik "Update Data".', 'info'); + openModal(); +} + +async function deleteFeature(type, id) { + const label = + type === 'point' ? 'point SPBU' : + type === 'jalan' ? 'jalan' : + type === 'parsil' ? 'parsil' : + type === 'rumahIbadah' ? 'rumah ibadah' : + 'pemukiman miskin'; + const ok = window.confirm(`Hapus data ${label} ini?`); + if (!ok) return; + + const url = + type === 'point' ? 'php/delete_point.php' : + type === 'jalan' ? 'php/delete_jalan.php' : + type === 'parsil' ? 'php/delete_parsil.php' : + type === 'rumahIbadah' ? 'php/delete_rumah_ibadah.php' : + 'php/delete_pemukiman_miskin.php'; + + try { + const { res, json, text } = await fetchMaybeJson(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: Number(id) }) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat hapus data.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + + showToast('Data berhasil dihapus.', 'success'); + if (type === 'point') await loadPoints(); + if (type === 'jalan') await loadJalan(); + if (type === 'parsil') await loadParsil(); + if (type === 'rumahIbadah') { + await loadPemukimanMiskin(); + await loadRumahIbadah(); + } + if (type === 'pemukiman') { + await loadPemukimanMiskin(); + await loadRumahIbadah(); + } + } catch (e) { + showToast(e?.message || String(e), 'error', 5000); + } +} + +function closeModal({ keepMode = false } = {}) { + if (!el.modal) return; + + // Cancel edit: restore geometry + if (state.editing?.layer && state.editing?.backup) { + const { type, layer, backup } = state.editing; + restoreLayerGeometry(layer, backup); + + if (type === 'point' && layer.setIcon && typeof backup?.is24 === 'boolean') { + layer.setIcon(makeSpbuIcon(backup.is24)); + } + + if (type === 'jalan' && layer.setStyle && backup?.status) { + layer.setStyle({ color: getJalanColor(backup.status), weight: 4, opacity: 0.95 }); + } + + if (type === 'parsil' && layer.setStyle && backup?.status) { + layer.setStyle({ color: '#0f172a', weight: 2, fillColor: getParsilColor(backup.status), fillOpacity: 0.35 }); + } + + if (type === 'pemukiman' && layer.setIcon) { + layer.setIcon(makePemukimanIcon({ isCovered: Boolean(backup?.covered) })); + if (layer.__pmDragHandler) { + layer.off('drag', layer.__pmDragHandler); + layer.off('dragend', layer.__pmDragHandler); + layer.__pmDragHandler = null; + } + if (state.editing?.dragEase?.disable) state.editing.dragEase.disable(); + } + + if (type === 'rumahIbadah' && layer.setIcon) { + layer.setIcon(makeRumahIbadahIcon()); + if (layer.__riDragHandler) { + layer.off('drag', layer.__riDragHandler); + layer.off('dragend', layer.__riDragHandler); + layer.__riDragHandler = null; + } + } + + disableLayerEditing(layer); + state.editing = null; + } + + el.modal.classList.add('hidden'); + el.featureForm?.reset(); + clearFormNotice(); + setSubmitting(false); + clearPendingLayer(); + + if (!keepMode) resetMode({ skipModalClose: true }); +} + +function resetMode({ skipModalClose = false } = {}) { + if (!skipModalClose && el.modal && !el.modal.classList.contains('hidden')) { + // Close modal first, but don't recurse into resetMode again. + closeModal({ keepMode: true }); + } + + if (state.locationEditing) { + cancelLocationEdit({ silent: true }); + } + + if (state.activeDrawer) { + state.activeDrawer.disable(); + state.activeDrawer = null; + } + + state.featureType = null; + document.body.classList.remove('crosshair'); + setActiveModeButton(null); + updateModeInfo(); +} + +function setMode(type) { + resetMode(); + state.editing = null; + state.locationEditing = null; + state.featureType = type; + setActiveModeButton(type); + updateModeInfo(); + + if (type === 'point' || type === 'rumahIbadah' || type === 'pemukiman') { + document.body.classList.add('crosshair'); + return; + } + + if (type === 'jalan') { + state.activeDrawer = new L.Draw.Polyline(map, { + shapeOptions: { color: getJalanColor(el.statusJalan?.value), weight: 4, opacity: 0.95 } + }); + state.activeDrawer.enable(); + return; + } + + if (type === 'parsil') { + state.activeDrawer = new L.Draw.Polygon(map, { + allowIntersection: false, + showArea: true, + shapeOptions: { color: '#0f172a', weight: 2, fillColor: getParsilColor(el.statusParsil?.value), fillOpacity: 0.35 } + }); + state.activeDrawer.enable(); + } +} + +// expose for inline onclick +window.setMode = setMode; +window.resetMode = resetMode; +window.closeModal = closeModal; + +function showEditBar(title) { + if (!el.editBar) return; + if (el.editBarTitle) el.editBarTitle.textContent = title || 'Edit Lokasi'; + el.editBar.classList.remove('hidden'); +} + +function hideEditBar() { + if (!el.editBar) return; + el.editBar.classList.add('hidden'); +} + +function updateDeleteVertexUi() { + if (!el.btnDeleteVertex) return; + const type = state.locationEditing?.type ?? null; + const shouldShow = type === 'jalan' || type === 'parsil'; + el.btnDeleteVertex.classList.toggle('hidden', !shouldShow); + + if (!shouldShow) { + el.btnDeleteVertex.classList.remove('is-active'); + el.btnDeleteVertex.textContent = 'Hapus Vertex'; + } else { + const enabled = Boolean(state.locationEditing?.vertexDeleteMode); + el.btnDeleteVertex.classList.toggle('is-active', enabled); + el.btnDeleteVertex.textContent = enabled ? 'Hapus Vertex: ON' : 'Hapus Vertex'; + } +} + +function setVertexDeleteMode(enabled) { + if (!state.locationEditing) return; + state.locationEditing.vertexDeleteMode = Boolean(enabled); + updateDeleteVertexUi(); + if (state.locationEditing.vertexDeleteMode) { + showToast('Mode hapus vertex aktif: klik vertex (titik putih) untuk menghapus.', 'info', 6000); + } +} + +function refreshEditingHandles(layer) { + if (!layer?.editing?.disable || !layer?.editing?.enable) return; + const wasEnabled = layer.editing.enabled?.(); + if (wasEnabled === false) return; + layer.editing.disable(); + layer.editing.enable(); +} + +function deleteNearestVertex(layer, latlng) { + if (!layer?.getLatLngs || !layer?.setLatLngs) return false; + + const clickPt = map.latLngToContainerPoint(latlng); + const maxDistPx = 18; + const type = state.locationEditing?.type; + + if (type === 'parsil') { + const rings = layer.getLatLngs(); + const ring = Array.isArray(rings?.[0]) ? rings[0] : null; + if (!ring) return { ok: false, reason: 'not_found' }; + if (ring.length <= 3) return { ok: false, reason: 'min' }; + + let bestIdx = -1; + let bestDist = Infinity; + for (let i = 0; i < ring.length; i++) { + const d = clickPt.distanceTo(map.latLngToContainerPoint(ring[i])); + if (d < bestDist) { + bestDist = d; + bestIdx = i; + } + } + if (bestDist > maxDistPx || bestIdx < 0) return { ok: false, reason: 'not_found' }; + + ring.splice(bestIdx, 1); + layer.setLatLngs([ring]); + if (layer.redraw) layer.redraw(); + refreshEditingHandles(layer); + return { ok: true }; + } + + // polyline + let latlngs = layer.getLatLngs(); + if (Array.isArray(latlngs?.[0])) latlngs = latlngs[0]; // guard for MultiLineString + if (!Array.isArray(latlngs)) return { ok: false, reason: 'not_found' }; + if (latlngs.length <= 2) return { ok: false, reason: 'min' }; + + let bestIdx = -1; + let bestDist = Infinity; + for (let i = 0; i < latlngs.length; i++) { + const d = clickPt.distanceTo(map.latLngToContainerPoint(latlngs[i])); + if (d < bestDist) { + bestDist = d; + bestIdx = i; + } + } + if (bestDist > maxDistPx || bestIdx < 0) return { ok: false, reason: 'not_found' }; + + latlngs.splice(bestIdx, 1); + layer.setLatLngs(latlngs); + if (layer.redraw) layer.redraw(); + refreshEditingHandles(layer); + return { ok: true }; +} + +function enableLayerTranslation(layer) { + if (!layer) return null; + + const ctx = { + isDragging: false, + startPoint: null, + originLatLngs: null, + originPoints: null, + raf: null, + pendingDelta: null, + original: { + dragging: map.dragging.enabled(), + boxZoom: map.boxZoom?.enabled?.() ?? false, + doubleClickZoom: map.doubleClickZoom?.enabled?.() ?? false, + touchZoom: map.touchZoom?.enabled?.() ?? false + } + }; + + const cloneLatLngs = (latlngs) => { + if (!Array.isArray(latlngs)) return latlngs; + return latlngs.map((v) => (Array.isArray(v) ? cloneLatLngs(v) : L.latLng(v.lat, v.lng))); + }; + + const toPoints = (latlngs) => { + if (!Array.isArray(latlngs)) return latlngs; + return latlngs.map((v) => (Array.isArray(v) ? toPoints(v) : map.latLngToLayerPoint(v))); + }; + + const setTranslatedFromPoints = (originPoints, delta) => { + const apply = (pts) => { + if (!Array.isArray(pts)) return pts; + return pts.map((v) => { + if (Array.isArray(v)) return apply(v); + return map.layerPointToLatLng(v.add(delta)); + }); + }; + layer.setLatLngs(apply(originPoints)); + }; + + const scheduleUpdate = () => { + if (!ctx.isDragging || !ctx.pendingDelta) return; + if (ctx.raf) return; + ctx.raf = window.requestAnimationFrame(() => { + ctx.raf = null; + if (!ctx.isDragging || !ctx.pendingDelta) return; + setTranslatedFromPoints(ctx.originPoints, ctx.pendingDelta); + if (layer.redraw) layer.redraw(); + }); + }; + + const onDown = (e) => { + // Avoid capturing drag from vertex markers (edit handles) + if (e?.originalEvent?.target && String(e.originalEvent.target.className || '').includes('leaflet-editing-icon')) { + return; + } + ctx.isDragging = true; + + ctx.startPoint = map.latLngToLayerPoint(e.latlng); + ctx.originLatLngs = cloneLatLngs(layer.getLatLngs()); + ctx.originPoints = toPoints(ctx.originLatLngs); + ctx.pendingDelta = L.point(0, 0); + + map.dragging.disable(); + map.boxZoom?.disable?.(); + map.doubleClickZoom?.disable?.(); + map.touchZoom?.disable?.(); + + if (e?.originalEvent) L.DomEvent.stop(e.originalEvent); + }; + + const onMove = (e) => { + if (!ctx.isDragging) return; + const current = map.latLngToLayerPoint(e.latlng); + ctx.pendingDelta = current.subtract(ctx.startPoint); + scheduleUpdate(); + }; + + const onUp = () => { + if (!ctx.isDragging) return; + ctx.isDragging = false; + ctx.startPoint = null; + ctx.originLatLngs = null; + ctx.originPoints = null; + ctx.pendingDelta = null; + if (ctx.raf) { + window.cancelAnimationFrame(ctx.raf); + ctx.raf = null; + } + + if (ctx.original.dragging) map.dragging.enable(); + if (ctx.original.boxZoom) map.boxZoom?.enable?.(); + if (ctx.original.doubleClickZoom) map.doubleClickZoom?.enable?.(); + if (ctx.original.touchZoom) map.touchZoom?.enable?.(); + }; + + layer.on('mousedown touchstart', onDown); + map.on('mousemove touchmove', onMove); + map.on('mouseup touchend touchcancel', onUp); + + return { + disable: () => { + layer.off('mousedown touchstart', onDown); + map.off('mousemove touchmove', onMove); + map.off('mouseup touchend touchcancel', onUp); + if (ctx.raf) { + window.cancelAnimationFrame(ctx.raf); + ctx.raf = null; + } + if (ctx.original.dragging) map.dragging.enable(); + if (ctx.original.boxZoom) map.boxZoom?.enable?.(); + if (ctx.original.doubleClickZoom) map.doubleClickZoom?.enable?.(); + if (ctx.original.touchZoom) map.touchZoom?.enable?.(); + } + }; +} + +async function saveLocationEdit() { + if (!state.locationEditing) return; + + const { type, id, layer, attrs } = state.locationEditing; + try { + if (type === 'point') { + const ll = layer.getLatLng(); + const payload = { + id, + nama: attrs.nama, + no: attrs.no, + deskripsi: attrs.deskripsi, + status_24jam: attrs.status_24jam, + latitude: ll.lat, + longitude: ll.lng + }; + + const { res, json, text } = await fetchMaybeJson('php/update_point.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi point.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + if (type === 'jalan') { + const payload = { + id, + nama_jalan: attrs.nama_jalan, + status_jalan: attrs.status_jalan, + panjang_meter: hitungPanjang(layer), + geometry: layer.toGeoJSON().geometry + }; + + const { res, json, text } = await fetchMaybeJson('php/update_jalan.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi jalan.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + if (type === 'parsil') { + const payload = { + id, + status_kepemilikan: attrs.status_kepemilikan, + luas_m2: hitungLuas(layer), + geometry: layer.toGeoJSON().geometry + }; + + const { res, json, text } = await fetchMaybeJson('php/update_parsil.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi parsil.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + if (type === 'pemukiman') { + const ll = layer.getLatLng(); + const payload = { id, latitude: ll.lat, longitude: ll.lng }; + + const { res, json, text } = await fetchMaybeJson('php/update_pemukiman_miskin_location.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi pemukiman miskin.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + if (type === 'rumahIbadah') { + const ll = layer.getLatLng(); + const payload = { + id, + nama: attrs.nama, + jenis: attrs.jenis, + kontak: attrs.kontak, + radius_meter: attrs.radius_meter, + latitude: ll.lat, + longitude: ll.lng, + address: attrs.address + }; + + const { res, json, text } = await fetchMaybeJson('php/update_rumah_ibadah.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi rumah ibadah.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + showToast('Lokasi berhasil diupdate.', 'success'); + state.locationEditing.vertexDeleteMode = false; + cancelLocationEdit({ silent: true }); + + if (type === 'point') await loadPoints(); + if (type === 'jalan') await loadJalan(); + if (type === 'parsil') await loadParsil(); + if (type === 'pemukiman') { + await loadPemukimanMiskin(); + await loadRumahIbadah(); + } + if (type === 'rumahIbadah') { + await loadPemukimanMiskin(); + await loadRumahIbadah(); + } + } catch (e) { + showToast(e?.message || String(e), 'error', 5000); + } +} + +function cancelLocationEdit({ silent = false } = {}) { + if (!state.locationEditing) return; + const { type, layer, backup, translate, dragEase } = state.locationEditing; + if (translate && translate.disable) translate.disable(); + if (dragEase && dragEase.disable) dragEase.disable(); + state.locationEditing.vertexDeleteMode = false; + updateDeleteVertexUi(); + restoreLayerGeometry(layer, backup); + + if (type === 'jalan' && layer.setStyle && backup?.status) { + layer.setStyle({ color: getJalanColor(backup.status), weight: 4, opacity: 0.95 }); + } + if (type === 'parsil' && layer.setStyle && backup?.status) { + layer.setStyle({ color: '#0f172a', weight: 2, fillColor: getParsilColor(backup.status), fillOpacity: 0.35 }); + } + if (type === 'point' && layer.setIcon && typeof backup?.is24 === 'boolean') { + layer.setIcon(makeSpbuIcon(backup.is24)); + } + + if (type === 'pemukiman' && layer.setIcon) { + layer.setIcon(makePemukimanIcon({ isCovered: Boolean(backup?.covered) })); + if (layer.__pmCoverHandler) { + layer.off('drag', layer.__pmCoverHandler); + layer.off('dragend', layer.__pmCoverHandler); + layer.__pmCoverHandler = null; + } + } + + if (type === 'rumahIbadah' && layer.setIcon) { + layer.setIcon(makeRumahIbadahIcon()); + } + + disableLayerEditing(layer); + hideEditBar(); + state.locationEditing = null; + updateDeleteVertexUi(); + updateModeInfo(); + if (!silent) showToast('Edit lokasi dibatalkan.', 'info'); +} + +function startEditLocationPoint(id, marker, row) { + resetMode(); + const backup = backupLayerGeometry(marker); + backup.is24 = Number(row?.status_24jam) === 1; + state.locationEditing = { + type: 'point', + id: Number(id), + layer: marker, + backup, + vertexDeleteMode: false, + attrs: { + nama: row?.nama ?? '', + no: row?.no ?? '', + deskripsi: row?.deskripsi ?? '', + status_24jam: Number(row?.status_24jam) === 1 ? 1 : 0 + } + }; + + enableLayerEditing(marker); // enables dragging + showEditBar('Edit Lokasi: Point'); + updateDeleteVertexUi(); + updateModeInfo(); + showToast('Geser marker ke posisi baru, lalu klik "Simpan Lokasi".', 'info', 6000); +} + +function startEditLocationPemukiman(id, marker, row) { + resetMode(); + const backup = backupLayerGeometry(marker); + const ibadahId = row?.rumah_ibadah_id; + backup.covered = ibadahId !== null && ibadahId !== undefined && String(ibadahId).trim() !== '' && String(ibadahId) !== '0'; + state.locationEditing = { + type: 'pemukiman', + id: Number(id), + layer: marker, + backup, + vertexDeleteMode: false, + attrs: {} + }; + + marker.setIcon(makePemukimanIcon({ isEdit: true, isCovered: Boolean(backup.covered) })); + enableLayerEditing(marker); + + if (marker.__pmCoverHandler) { + marker.off('drag', marker.__pmCoverHandler); + marker.off('dragend', marker.__pmCoverHandler); + } + marker.__currentCovered = Boolean(backup.covered); + marker.__pmCoverHandler = () => { + const current = marker.getLatLng(); + const cov = computePemukimanCoverage(current); + if (cov.covered !== null && marker.__currentCovered !== cov.covered) { + marker.__currentCovered = cov.covered; + marker.setIcon(makePemukimanIcon({ isEdit: true, isCovered: cov.covered === true })); + } + }; + marker.on('drag', marker.__pmCoverHandler); + marker.on('dragend', marker.__pmCoverHandler); + marker.__pmCoverHandler(); + + showEditBar('Edit Lokasi: Pemukiman Miskin'); + updateDeleteVertexUi(); + updateModeInfo(); + + const label = escapeHtml(row?.kk_nama ?? row?.nama ?? `ID ${id}`); + showToast(`Geser marker pemukiman (${label}) ke posisi baru, lalu klik "Simpan Lokasi".`, 'info', 6500); +} + +function startEditLocationRumahIbadah(id, marker, row) { + resetMode(); + const radiusVal = Number(row?.radius_meter); + if (!Number.isFinite(radiusVal) || radiusVal <= 0) { + showToast('Radius rumah ibadah tidak valid. Silakan Edit Data dulu untuk isi radius.', 'error', 5500); + return; + } + const backup = backupLayerGeometry(marker); + state.locationEditing = { + type: 'rumahIbadah', + id: Number(id), + layer: marker, + backup, + vertexDeleteMode: false, + attrs: { + nama: row?.nama ?? 'Rumah Ibadah', + jenis: row?.jenis ?? 'Lainnya', + kontak: row?.kontak ?? '', + radius_meter: radiusVal, + address: row?.address ?? '' + } + }; + + enableLayerEditing(marker); + showEditBar('Edit Lokasi: Rumah Ibadah'); + updateDeleteVertexUi(); + updateModeInfo(); + + const label = escapeHtml(row?.nama ?? `ID ${id}`); + showToast(`Geser marker rumah ibadah (${label}) ke posisi baru, lalu klik "Simpan Lokasi".`, 'info', 6500); +} + +function startEditLocationJalan(id, layer, props) { + resetMode(); + const backup = backupLayerGeometry(layer); + backup.status = props?.status_jalan ?? 'Nasional'; + state.locationEditing = { + type: 'jalan', + id: Number(id), + layer, + backup, + vertexDeleteMode: false, + attrs: { + nama_jalan: props?.nama_jalan ?? 'Jalan', + status_jalan: props?.status_jalan ?? 'Nasional' + } + }; + + enableLayerEditing(layer); + layer.setStyle?.({ color: getJalanColor(props?.status_jalan), weight: 4, opacity: 0.95 }); + state.locationEditing.translate = enableLayerTranslation(layer); + + showEditBar('Edit Lokasi: Jalan'); + updateDeleteVertexUi(); + updateModeInfo(); + showToast('Geser garis untuk pindah, atau geser vertex untuk ubah bentuk. Lalu simpan.', 'info', 7000); +} + +function startEditLocationParsil(id, layer, props) { + resetMode(); + const backup = backupLayerGeometry(layer); + backup.status = props?.status_kepemilikan ?? 'SHM'; + state.locationEditing = { + type: 'parsil', + id: Number(id), + layer, + backup, + vertexDeleteMode: false, + attrs: { + status_kepemilikan: props?.status_kepemilikan ?? 'SHM' + } + }; + + enableLayerEditing(layer); + layer.setStyle?.({ color: '#0f172a', weight: 2, fillColor: getParsilColor(props?.status_kepemilikan), fillOpacity: 0.35 }); + state.locationEditing.translate = enableLayerTranslation(layer); + + showEditBar('Edit Lokasi: Parsil'); + updateDeleteVertexUi(); + updateModeInfo(); + showToast('Geser area untuk pindah, atau geser vertex untuk ubah bentuk. Lalu simpan.', 'info', 7000); +} + +// ================= DRAW EVENT ================= +map.on(L.Draw.Event.CREATED, function (e) { + state.currentLayer = e.layer; + state.pendingLayer = e.layer; + editableGroup.addLayer(e.layer); + + // Disable drawer while modal is open + if (state.activeDrawer) { + state.activeDrawer.disable(); + state.activeDrawer = null; + } + + if (!state.featureType) { + if (e.layerType === 'polyline') state.featureType = 'jalan'; + if (e.layerType === 'polygon') state.featureType = 'parsil'; + setActiveModeButton(state.featureType); + updateModeInfo(); + } + + // Apply initial style based on selected status + if (state.featureType === 'jalan' && e.layer.setStyle) { + e.layer.setStyle({ color: getJalanColor(el.statusJalan?.value), weight: 4, opacity: 0.95 }); + } + if (state.featureType === 'parsil' && e.layer.setStyle) { + e.layer.setStyle({ color: '#0f172a', weight: 2, fillColor: getParsilColor(el.statusParsil?.value), fillOpacity: 0.35 }); + } + + openModal(); +}); + +// ================= POINT ================= +map.on('click', function (e) { + if (state.locationEditing?.vertexDeleteMode && (state.locationEditing.type === 'jalan' || state.locationEditing.type === 'parsil')) { + const result = deleteNearestVertex(state.locationEditing.layer, e.latlng); + if (result?.ok) { + showToast('Vertex dihapus.', 'success'); + } else if (result?.reason === 'min') { + const min = state.locationEditing.type === 'jalan' ? 2 : 3; + showToast(`Tidak bisa menghapus (minimal ${min} vertex).`, 'error', 2500); + } + return; + } + + if (state.featureType !== 'point' && state.featureType !== 'rumahIbadah' && state.featureType !== 'pemukiman') return; + if (el.modal && !el.modal.classList.contains('hidden')) return; + + state.currentLayer = e.latlng; + if (state.featureType === 'point') { + el.latPoint.value = e.latlng.lat; + el.lngPoint.value = e.latlng.lng; + } + if (state.featureType === 'rumahIbadah') { + el.latRumahIbadah.value = e.latlng.lat; + el.lngRumahIbadah.value = e.latlng.lng; + } + if (state.featureType === 'pemukiman') { + el.latPemukiman.value = e.latlng.lat; + el.lngPemukiman.value = e.latlng.lng; + } + openModal(); +}); + +// ================= SUBMIT ================= +el.featureForm.addEventListener('submit', async function (e) { + e.preventDefault(); + + const submittingType = state.featureType; + + if (!submittingType) { + showToast('Mode belum dipilih.', 'error'); + return; + } + + if (!state.currentLayer && !state.editing?.layer) { + showToast('Layer tidak ada!', 'error'); + return; + } + + try { + clearFormNotice(); + setSubmitting(true); + + // UPDATE mode + if (state.editing && state.editing.type === submittingType) { + if (submittingType === 'point') { + const marker = state.editing.layer; + const ll = marker.getLatLng(); + const payload = { + id: state.editing.id, + nama: el.namaPoint.value, + no: el.noPoint.value, + deskripsi: el.deskripsiPoint.value, + status_24jam: el.status24Jam.checked ? 1 : 0, + latitude: ll.lat, + longitude: ll.lng + }; + + const { res, json, text } = await fetchMaybeJson('php/update_point.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update point.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + if (submittingType === 'jalan') { + const layer = state.editing.layer; + const payload = { + id: state.editing.id, + nama_jalan: el.namaJalan.value, + status_jalan: el.statusJalan.value, + panjang_meter: hitungPanjang(layer), + geometry: layer.toGeoJSON().geometry + }; + + const { res, json, text } = await fetchMaybeJson('php/update_jalan.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update jalan.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + if (submittingType === 'parsil') { + const layer = state.editing.layer; + const payload = { + id: state.editing.id, + status_kepemilikan: el.statusParsil.value, + luas_m2: hitungLuas(layer), + geometry: layer.toGeoJSON().geometry + }; + + const { res, json, text } = await fetchMaybeJson('php/update_parsil.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update parsil.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + if (submittingType === 'rumahIbadah') { + const marker = state.editing.layer; + const ll = marker.getLatLng(); + const payload = { + id: state.editing.id, + nama: el.namaRumahIbadah.value, + jenis: el.jenisRumahIbadah.value, + kontak: el.kontakRumahIbadah.value, + radius_meter: el.radiusRumahIbadah.value, + latitude: ll.lat, + longitude: ll.lng, + address: el.alamatRumahIbadah.value + }; + + const { res, json, text } = await fetchMaybeJson('php/update_rumah_ibadah.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update rumah ibadah.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + if (submittingType === 'pemukiman') { + const marker = state.editing.layer; + const ll = marker.getLatLng(); + const payload = { + id: state.editing.id, + kk_nama: el.kkNamaPemukiman.value, + nik: el.nikPemukiman.value, + jumlah_anggota: el.jumlahAnggotaPemukiman.value, + latitude: ll.lat, + longitude: ll.lng, + address: el.alamatPemukiman.value, + kelurahan: el.kelurahanPemukiman.value, + kecamatan: el.kecamatanPemukiman.value, + status_bantuan: el.statusBantuanPemukiman.value, + jenis_bantuan: el.jenisBantuanPemukiman.value, + tanggal_bantuan: el.tanggalBantuanPemukiman.value, + anggota: [] + }; + + const { res, json, text } = await fetchMaybeJson('php/update_pemukiman_miskin.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat update pemukiman miskin.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + showToast('Data berhasil diupdate.', 'success'); + if (state.editing?.dragEase?.disable) state.editing.dragEase.disable(); + disableLayerEditing(state.editing.layer); + state.editing = null; + closeModal(); + + if (submittingType === 'point') await loadPoints(); + if (submittingType === 'jalan') await loadJalan(); + if (submittingType === 'parsil') await loadParsil(); + if (submittingType === 'rumahIbadah') { + await loadPemukimanMiskin(); + await loadRumahIbadah(); + } + if (submittingType === 'pemukiman') await loadPemukimanMiskin(); + return; + } + + // POINT + if (submittingType === 'point') { + const payload = { + nama: el.namaPoint.value, + no: el.noPoint.value, + deskripsi: el.deskripsiPoint.value, + status_24jam: el.status24Jam.checked ? 1 : 0, + latitude: el.latPoint.value, + longitude: el.lngPoint.value + }; + + const { res, json, text } = await fetchMaybeJson('php/create_point.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan point.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + // JALAN + if (submittingType === 'jalan') { + const layer = state.currentLayer; + + const payload = { + nama_jalan: el.namaJalan.value, + status_jalan: el.statusJalan.value, + panjang_meter: hitungPanjang(layer), + geometry: layer.toGeoJSON().geometry + }; + + const { res, json, text } = await fetchMaybeJson('php/create_jalan.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan jalan.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + // PARSIL + if (submittingType === 'parsil') { + const layer = state.currentLayer; + + const payload = { + status_kepemilikan: el.statusParsil.value, + luas_m2: hitungLuas(layer), + geometry: layer.toGeoJSON().geometry + }; + + const { res, json, text } = await fetchMaybeJson('php/create_parsil.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan parsil.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + // RUMAH IBADAH + if (submittingType === 'rumahIbadah') { + const payload = { + nama: el.namaRumahIbadah.value, + jenis: el.jenisRumahIbadah.value, + kontak: el.kontakRumahIbadah.value, + radius_meter: el.radiusRumahIbadah.value, + latitude: el.latRumahIbadah.value, + longitude: el.lngRumahIbadah.value, + address: el.alamatRumahIbadah.value + }; + + const { res, json, text } = await fetchMaybeJson('php/create_rumah_ibadah.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan rumah ibadah.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + // PEMUKIMAN MISKIN + if (submittingType === 'pemukiman') { + const payload = { + kk_nama: el.kkNamaPemukiman.value, + nik: el.nikPemukiman.value, + jumlah_anggota: el.jumlahAnggotaPemukiman.value, + latitude: el.latPemukiman.value, + longitude: el.lngPemukiman.value, + address: el.alamatPemukiman.value, + kelurahan: el.kelurahanPemukiman.value, + kecamatan: el.kecamatanPemukiman.value, + status_bantuan: el.statusBantuanPemukiman.value, + jenis_bantuan: el.jenisBantuanPemukiman.value, + tanggal_bantuan: el.tanggalBantuanPemukiman.value, + anggota: [] + }; + + const { res, json, text } = await fetchMaybeJson('php/create_pemukiman_miskin.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan pemukiman miskin.'); + const api = parseApiJson(json); + if (!api.success) throw new Error(api.message); + if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`); + } + + showToast('Data berhasil disimpan.', 'success'); + closeModal(); + + if (submittingType === 'point') await loadPoints(); + if (submittingType === 'jalan') await loadJalan(); + if (submittingType === 'parsil') await loadParsil(); + if (submittingType === 'rumahIbadah') { + await loadPemukimanMiskin(); + await loadRumahIbadah(); + } + if (submittingType === 'pemukiman') await loadPemukimanMiskin(); + } catch (err) { + const msg = err?.message || String(err); + setFormNotice(msg, 'error'); + showToast(msg, 'error', 5000); + } finally { + setSubmitting(false); + } +}); + +// live style update for pending layer +el.statusJalan?.addEventListener('change', function () { + const target = + state.pendingLayer && state.featureType === 'jalan' + ? state.pendingLayer + : state.editing?.type === 'jalan' + ? state.editing.layer + : null; + + if (target && target.setStyle) { + target.setStyle({ color: getJalanColor(el.statusJalan.value), weight: 4, opacity: 0.95 }); + } +}); + +el.statusParsil?.addEventListener('change', function () { + const target = + state.pendingLayer && state.featureType === 'parsil' + ? state.pendingLayer + : state.editing?.type === 'parsil' + ? state.editing.layer + : null; + + if (target && target.setStyle) { + target.setStyle({ fillColor: getParsilColor(el.statusParsil.value), fillOpacity: 0.35 }); + } +}); + +el.status24Jam?.addEventListener('change', function () { + if (state.editing?.type === 'point' && state.editing.layer?.setIcon) { + state.editing.layer.setIcon(makeSpbuIcon(el.status24Jam.checked)); + } +}); + +// ================= LOAD LAYERS ================= +function setActiveListItem(kind, id) { + if (kind === 'points' && el.pointsList) { + const items = el.pointsList.querySelectorAll('.list-item'); + items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id))); + state.selected.points = id; + } + if (kind === 'jalan' && el.jalanList) { + const items = el.jalanList.querySelectorAll('.list-item'); + items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id))); + state.selected.jalan = id; + } + if (kind === 'parsil' && el.parsilList) { + const items = el.parsilList.querySelectorAll('.list-item'); + items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id))); + state.selected.parsil = id; + } + + if (kind === 'rumahIbadah' && el.rumahIbadahList) { + const items = el.rumahIbadahList.querySelectorAll('.list-item'); + items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id))); + state.selected.rumahIbadah = id; + } + + if (kind === 'pemukiman' && el.pemukimanList) { + const items = el.pemukimanList.querySelectorAll('.list-item'); + items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id))); + state.selected.pemukiman = id; + } +} + +function renderEmptyList(container, text) { + if (!container) return; + container.innerHTML = ''; + const div = document.createElement('div'); + div.className = 'list-empty'; + div.textContent = text; + container.appendChild(div); +} + +function renderPointsList(rows) { + if (!el.pointsList) return; + if (!Array.isArray(rows) || rows.length === 0) { + renderEmptyList(el.pointsList, 'Belum ada data SPBU.'); + return; + } + + el.pointsList.innerHTML = ''; + for (const row of rows) { + const id = row?.id ?? ''; + const nama = row?.nama ?? 'SPBU'; + const no = row?.no ?? ''; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'list-item'; + btn.dataset.id = String(id); + + const title = document.createElement('div'); + title.className = 'list-title'; + title.textContent = nama; + + const meta = document.createElement('div'); + meta.className = 'list-meta'; + meta.textContent = no ? `No: ${no}` : 'Klik untuk zoom'; + + btn.appendChild(title); + btn.appendChild(meta); + + btn.addEventListener('click', () => { + const lat = parseFloat(row?.latitude); + const lng = parseFloat(row?.longitude); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; + + if (el.togglePoints) el.togglePoints.checked = true; + applyLayerVisibility(); + + const record = state.indices.points.get(String(id)); + map.flyTo([lat, lng], Math.max(map.getZoom(), 17), { duration: 0.9 }); + if (record?.layer?.openPopup) record.layer.openPopup(); + if (record?.layer?.setStyle) { + record.layer.setStyle({ radius: 10 }); + window.setTimeout(() => record.layer.setStyle({ radius: 7 }), 900); + } + setActiveListItem('points', id); + }); + + el.pointsList.appendChild(btn); + } +} + +function renderJalanList(features) { + if (!el.jalanList) return; + if (!Array.isArray(features) || features.length === 0) { + renderEmptyList(el.jalanList, 'Belum ada data jalan.'); + return; + } + + el.jalanList.innerHTML = ''; + for (const f of features) { + const props = f?.properties ?? {}; + const id = props?.id ?? ''; + const nama = props?.nama_jalan ?? 'Jalan'; + const status = props?.status_jalan ?? ''; + const panjang = Number.isFinite(Number(props?.panjang_meter)) + ? `${Number(props.panjang_meter).toFixed(0)} m` + : ''; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'list-item'; + btn.dataset.id = String(id); + + const title = document.createElement('div'); + title.className = 'list-title'; + title.textContent = nama; + + const meta = document.createElement('div'); + meta.className = 'list-meta'; + meta.textContent = [status, panjang].filter(Boolean).join(' β€’ ') || 'Klik untuk zoom'; + + btn.appendChild(title); + btn.appendChild(meta); + + btn.addEventListener('click', () => { + if (el.toggleJalan) el.toggleJalan.checked = true; + applyLayerVisibility(); + + const record = state.indices.jalan.get(String(id)); + const layer = record?.layer; + if (layer?.getBounds) { + map.fitBounds(layer.getBounds(), { padding: [30, 30] }); + } + if (layer?.openPopup) layer.openPopup(); + setActiveListItem('jalan', id); + }); + + el.jalanList.appendChild(btn); + } +} + +function renderParsilList(features) { + if (!el.parsilList) return; + if (!Array.isArray(features) || features.length === 0) { + renderEmptyList(el.parsilList, 'Belum ada data parsil.'); + return; + } + + el.parsilList.innerHTML = ''; + for (const f of features) { + const props = f?.properties ?? {}; + const id = props?.id ?? ''; + const status = props?.status_kepemilikan ?? ''; + const luas = Number.isFinite(Number(props?.luas_m2)) + ? `${Number(props.luas_m2).toFixed(0)} mΒ²` + : ''; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'list-item'; + btn.dataset.id = String(id); + + const title = document.createElement('div'); + title.className = 'list-title'; + title.textContent = `Parsil ${status || ''}`.trim(); + + const meta = document.createElement('div'); + meta.className = 'list-meta'; + meta.textContent = luas ? `Luas: ${luas}` : 'Klik untuk zoom'; + + btn.appendChild(title); + btn.appendChild(meta); + + btn.addEventListener('click', () => { + if (el.toggleParsil) el.toggleParsil.checked = true; + applyLayerVisibility(); + + const record = state.indices.parsil.get(String(id)); + const layer = record?.layer; + if (layer?.getBounds) { + map.fitBounds(layer.getBounds(), { padding: [30, 30] }); + } + if (layer?.openPopup) layer.openPopup(); + setActiveListItem('parsil', id); + }); + + el.parsilList.appendChild(btn); + } +} + +function renderRumahIbadahList(rows) { + if (!el.rumahIbadahList) return; + if (!Array.isArray(rows) || rows.length === 0) { + renderEmptyList(el.rumahIbadahList, 'Belum ada data rumah ibadah.'); + return; + } + + el.rumahIbadahList.innerHTML = ''; + for (const row of rows) { + const lat = parseFloat(row?.latitude); + const lng = parseFloat(row?.longitude); + const id = String(row?.id ?? (Number.isFinite(lat) && Number.isFinite(lng) ? `${lat},${lng}` : '')); + const nama = row?.nama ?? 'Rumah Ibadah'; + const jenis = row?.jenis ?? ''; + const radius = Number(row?.radius_meter); + const metaText = [jenis, Number.isFinite(radius) && radius > 0 ? `Radius: ${radius} m` : ''].filter(Boolean).join(' β€’ ') || 'Klik untuk zoom'; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'list-item'; + btn.dataset.id = String(id); + + const title = document.createElement('div'); + title.className = 'list-title'; + title.textContent = nama; + + const meta = document.createElement('div'); + meta.className = 'list-meta'; + meta.textContent = metaText; + + btn.appendChild(title); + btn.appendChild(meta); + + btn.addEventListener('click', () => { + if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; + + if (el.toggleRumahIbadah) el.toggleRumahIbadah.checked = true; + applyLayerVisibility(); + + const record = state.indices.rumahIbadah.get(String(id)); + map.flyTo([lat, lng], Math.max(map.getZoom(), 17), { duration: 0.9 }); + if (record?.layer?.openPopup) record.layer.openPopup(); + setActiveListItem('rumahIbadah', id); + }); + + el.rumahIbadahList.appendChild(btn); + } +} + +function renderPemukimanList(rows) { + if (!el.pemukimanList) return; + if (!Array.isArray(rows) || rows.length === 0) { + renderEmptyList(el.pemukimanList, 'Belum ada data pemukiman miskin.'); + return; + } + + el.pemukimanList.innerHTML = ''; + for (const row of rows) { + const lat = parseFloat(row?.latitude); + const lng = parseFloat(row?.longitude); + const id = String(row?.id ?? (Number.isFinite(lat) && Number.isFinite(lng) ? `${lat},${lng}` : '')); + const nama = row?.kk_nama ?? row?.nama ?? 'Pemukiman Miskin'; + const status = row?.status_bantuan ?? ''; + const ibadah = row?.rumah_ibadah_nama ?? ''; + const metaText = [status, ibadah ? `Binaan: ${ibadah}` : ''].filter(Boolean).join(' β€’ ') || 'Klik untuk zoom'; + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'list-item'; + btn.dataset.id = String(id); + + const title = document.createElement('div'); + title.className = 'list-title'; + title.textContent = nama; + + const meta = document.createElement('div'); + meta.className = 'list-meta'; + meta.textContent = metaText; + + btn.appendChild(title); + btn.appendChild(meta); + + btn.addEventListener('click', () => { + if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; + + if (el.togglePemukiman) el.togglePemukiman.checked = true; + applyLayerVisibility(); + + const record = state.indices.pemukiman.get(String(id)); + map.flyTo([lat, lng], Math.max(map.getZoom(), 17), { duration: 0.9 }); + if (record?.layer?.openPopup) record.layer.openPopup(); + setActiveListItem('pemukiman', id); + }); + + el.pemukimanList.appendChild(btn); + } +} + +async function loadPoints() { + points24Layer.clearLayers(); + pointsNon24Layer.clearLayers(); + state.counts.points = 0; + state.indices.points.clear(); + + let rows = []; + try { + const res = await fetch('php/read_point.php', { cache: 'no-store' }); + rows = await res.json(); + } catch { + rows = []; + } + + let count = 0; + if (Array.isArray(rows)) { + for (const row of rows) { + const lat = parseFloat(row?.latitude); + const lng = parseFloat(row?.longitude); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue; + + const id = String(row?.id ?? `${lat},${lng}`); + const nama = escapeHtml(row?.nama ?? 'SPBU'); + const no = escapeHtml(row?.no ?? '-'); + const deskripsi = escapeHtml(row?.deskripsi ?? ''); + const is24 = isSpbu24(row); + const status24 = is24 ? 'Ya' : 'Tidak'; + + const marker = L.marker([lat, lng], { icon: makeSpbuIcon(is24), draggable: true }); + marker.on('add', () => { + if (marker.dragging && marker.dragging.disable) { + marker.dragging.disable(); + } + }); + + const popupEl = document.createElement('div'); + popupEl.innerHTML = ` +
${nama}
+
No: ${no}
+
24 Jam: ${status24}
+ ${deskripsi ? `
${deskripsi}
` : ''} + `; + + const actions = document.createElement('div'); + actions.className = 'popup-actions'; + const btnEditData = document.createElement('button'); + btnEditData.type = 'button'; + btnEditData.className = 'popup-btn edit'; + btnEditData.textContent = 'Edit Data'; + const btnEditLoc = document.createElement('button'); + btnEditLoc.type = 'button'; + btnEditLoc.className = 'popup-btn spatial'; + btnEditLoc.textContent = 'Edit Lokasi'; + const btnDelete = document.createElement('button'); + btnDelete.type = 'button'; + btnDelete.className = 'popup-btn delete'; + btnDelete.textContent = 'Hapus'; + actions.appendChild(btnEditData); + actions.appendChild(btnEditLoc); + actions.appendChild(btnDelete); + popupEl.appendChild(actions); + + btnEditData.addEventListener('click', () => startEditPoint(id, marker, row)); + btnEditLoc.addEventListener('click', () => startEditLocationPoint(id, marker, row)); + btnDelete.addEventListener('click', () => deleteFeature('point', id)); + + marker.bindPopup(popupEl); + marker.addTo(is24 ? points24Layer : pointsNon24Layer); + marker.on('click', () => setActiveListItem('points', id)); + state.indices.points.set(String(id), { layer: marker, data: row }); + count += 1; + } + } + + state.counts.points = count; + state.pointsData = Array.isArray(rows) ? rows : []; + updateStats(); + applyPointFilter(); +} + +async function loadJalan() { + jalanLayer.clearLayers(); + state.counts.jalan = 0; + state.indices.jalan.clear(); + + let fc = null; + try { + const res = await fetch('php/read_jalan.php', { cache: 'no-store' }); + fc = await res.json(); + } catch { + fc = null; + } + if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) { + updateStats(); + renderJalanList([]); + return; + } + + const geo = L.geoJSON(fc, { + style: (feature) => { + const status = feature?.properties?.status_jalan; + return { color: getJalanColor(status), weight: 4, opacity: 0.95 }; + }, + onEachFeature: (feature, layer) => { + const props = feature?.properties ?? {}; + const id = String(props?.id ?? ''); + const nama = escapeHtml(props.nama_jalan ?? 'Jalan'); + const status = escapeHtml(props.status_jalan ?? '-'); + const panjang = Number.isFinite(Number(props.panjang_meter)) + ? `${Number(props.panjang_meter).toFixed(1)} m` + : '-'; + + const popupEl = document.createElement('div'); + popupEl.innerHTML = ` +
${nama}
+
Status: ${status}
+
Panjang: ${panjang}
+ `; + + const actions = document.createElement('div'); + actions.className = 'popup-actions'; + const btnEditData = document.createElement('button'); + btnEditData.type = 'button'; + btnEditData.className = 'popup-btn edit'; + btnEditData.textContent = 'Edit Data'; + const btnEditLoc = document.createElement('button'); + btnEditLoc.type = 'button'; + btnEditLoc.className = 'popup-btn spatial'; + btnEditLoc.textContent = 'Edit Lokasi'; + const btnDelete = document.createElement('button'); + btnDelete.type = 'button'; + btnDelete.className = 'popup-btn delete'; + btnDelete.textContent = 'Hapus'; + actions.appendChild(btnEditData); + actions.appendChild(btnEditLoc); + actions.appendChild(btnDelete); + popupEl.appendChild(actions); + + btnEditData.addEventListener('click', () => startEditJalan(id, layer, props)); + btnEditLoc.addEventListener('click', () => startEditLocationJalan(id, layer, props)); + btnDelete.addEventListener('click', () => deleteFeature('jalan', id)); + + layer.bindPopup(popupEl); + + if (id) { + state.indices.jalan.set(id, { layer, data: props }); + layer.on('click', () => setActiveListItem('jalan', id)); + } + } + }); + + geo.addTo(jalanLayer); + state.counts.jalan = fc.features.length; + updateStats(); + renderJalanList(fc.features); +} + +async function loadParsil() { + parsilLayer.clearLayers(); + state.counts.parsil = 0; + state.indices.parsil.clear(); + + let fc = null; + try { + const res = await fetch('php/read_parsil.php', { cache: 'no-store' }); + fc = await res.json(); + } catch { + fc = null; + } + if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) { + updateStats(); + renderParsilList([]); + return; + } + + const geo = L.geoJSON(fc, { + style: (feature) => { + const status = feature?.properties?.status_kepemilikan; + return { + color: '#0f172a', + weight: 2, + fillColor: getParsilColor(status), + fillOpacity: 0.35 + }; + }, + onEachFeature: (feature, layer) => { + const props = feature?.properties ?? {}; + const status = escapeHtml(props.status_kepemilikan ?? '-'); + const luas = Number.isFinite(Number(props.luas_m2)) + ? `${Number(props.luas_m2).toFixed(1)} mΒ²` + : '-'; + + const id = String(props?.id ?? ''); + const popupEl = document.createElement('div'); + popupEl.innerHTML = ` +
Parsil
+
Status: ${status}
+
Luas: ${luas}
+ `; + + const actions = document.createElement('div'); + actions.className = 'popup-actions'; + const btnEditData = document.createElement('button'); + btnEditData.type = 'button'; + btnEditData.className = 'popup-btn edit'; + btnEditData.textContent = 'Edit Data'; + const btnEditLoc = document.createElement('button'); + btnEditLoc.type = 'button'; + btnEditLoc.className = 'popup-btn spatial'; + btnEditLoc.textContent = 'Edit Lokasi'; + const btnDelete = document.createElement('button'); + btnDelete.type = 'button'; + btnDelete.className = 'popup-btn delete'; + btnDelete.textContent = 'Hapus'; + actions.appendChild(btnEditData); + actions.appendChild(btnEditLoc); + actions.appendChild(btnDelete); + popupEl.appendChild(actions); + + btnEditData.addEventListener('click', () => startEditParsil(id, layer, props)); + btnEditLoc.addEventListener('click', () => startEditLocationParsil(id, layer, props)); + btnDelete.addEventListener('click', () => deleteFeature('parsil', id)); + + layer.bindPopup(popupEl); + if (id) { + state.indices.parsil.set(id, { layer, data: props }); + layer.on('click', () => setActiveListItem('parsil', id)); + } + } + }); + + geo.addTo(parsilLayer); + state.counts.parsil = fc.features.length; + updateStats(); + renderParsilList(fc.features); +} + +async function loadPemukimanMiskin() { + pemukimanLayer.clearLayers(); + state.counts.pemukiman = 0; + state.indices.pemukiman.clear(); + + let rows = []; + try { + const { res, json, text } = await fetchMaybeJson('php/read_pemukiman_miskin.php', { cache: 'no-store' }); + if (!res.ok) { + warnOnce('pemukiman_fetch', `Gagal load Pemukiman Miskin (HTTP ${res.status}). Cek koneksi database / endpoint.`); + rows = []; + } else if (!Array.isArray(json)) { + warnOnce('pemukiman_parse', text ? `Respon pemukiman bukan JSON: ${text.slice(0, 120)}` : 'Respon pemukiman bukan JSON.'); + rows = []; + } else { + rows = json; + } + } catch (e) { + warnOnce('pemukiman_err', `Gagal load Pemukiman Miskin: ${e?.message || String(e)}`); + rows = []; + } + + let count = 0; + if (Array.isArray(rows)) { + for (const row of rows) { + const lat = parseFloat(row?.latitude); + const lng = parseFloat(row?.longitude); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue; + + const id = String(row?.id ?? `${lat},${lng}`); + const nama = escapeHtml(row?.kk_nama ?? row?.nama ?? 'Pemukiman Miskin'); + const status = escapeHtml(row?.status_bantuan ?? '-'); + const ibadah = escapeHtml(row?.rumah_ibadah_nama ?? '-'); + const jarakNum = Number(row?.jarak_meter); + const jarak = Number.isFinite(jarakNum) ? `${jarakNum.toFixed(0)} m` : '-'; + const ibadahId = row?.rumah_ibadah_id; + const covered = ibadahId !== null && ibadahId !== undefined && String(ibadahId).trim() !== '' && String(ibadahId) !== '0'; + + const marker = L.marker([lat, lng], { icon: makePemukimanIcon({ isCovered: covered }), draggable: true, autoPan: true, riseOnHover: true }); + marker.on('add', () => { + if (marker.dragging && marker.dragging.disable) marker.dragging.disable(); + }); + + const popupEl = document.createElement('div'); + popupEl.innerHTML = ` +
${nama}
+
Status: ${status}
+
Rumah Ibadah: ${ibadah}
+
Jarak: ${jarak}
+ `; + + const actions = document.createElement('div'); + actions.className = 'popup-actions'; + const btnEditData = document.createElement('button'); + btnEditData.type = 'button'; + btnEditData.className = 'popup-btn edit'; + btnEditData.textContent = 'Edit Data'; + const btnEditLoc = document.createElement('button'); + btnEditLoc.type = 'button'; + btnEditLoc.className = 'popup-btn spatial'; + btnEditLoc.textContent = 'Edit Lokasi'; + const btnDelete = document.createElement('button'); + btnDelete.type = 'button'; + btnDelete.className = 'popup-btn delete'; + btnDelete.textContent = 'Hapus'; + + actions.appendChild(btnEditData); + actions.appendChild(btnEditLoc); + actions.appendChild(btnDelete); + popupEl.appendChild(actions); + + btnEditData.addEventListener('click', () => startEditPemukiman(id, marker, row)); + btnEditLoc.addEventListener('click', () => startEditLocationPemukiman(id, marker, row)); + btnDelete.addEventListener('click', () => deleteFeature('pemukiman', id)); + + marker.bindPopup(popupEl); + marker.addTo(pemukimanLayer); + state.indices.pemukiman.set(String(id), { layer: marker, data: row }); + marker.on('click', () => setActiveListItem('pemukiman', id)); + count += 1; + } + } + + state.counts.pemukiman = count; + state.pemukimanData = Array.isArray(rows) ? rows : []; + updateStats(); + renderPemukimanList(state.pemukimanData); +} + +async function loadRumahIbadah() { + rumahIbadahLayer.clearLayers(); + rumahIbadahRadiusLayer.clearLayers(); + state.indices.rumahIbadah.clear(); + + let rows = []; + try { + const { res, json, text } = await fetchMaybeJson('php/read_rumah_ibadah.php', { cache: 'no-store' }); + if (!res.ok) { + warnOnce('ibadah_fetch', `Gagal load Rumah Ibadah (HTTP ${res.status}). Cek koneksi database / endpoint.`); + rows = []; + } else if (!Array.isArray(json)) { + warnOnce('ibadah_parse', text ? `Respon rumah ibadah bukan JSON: ${text.slice(0, 120)}` : 'Respon rumah ibadah bukan JSON.'); + rows = []; + } else { + rows = json; + } + } catch (e) { + warnOnce('ibadah_err', `Gagal load Rumah Ibadah: ${e?.message || String(e)}`); + rows = []; + } + + if (!Array.isArray(rows)) rows = []; + state.rumahIbadahData = rows; + + let count = 0; + for (const row of rows) { + const lat = parseFloat(row?.latitude); + const lng = parseFloat(row?.longitude); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue; + + const id = String(row?.id ?? `${lat},${lng}`); + const nama = escapeHtml(row?.nama ?? 'Rumah Ibadah'); + const jenis = escapeHtml(row?.jenis ?? '-'); + const kontak = escapeHtml(row?.kontak ?? '-'); + const radius = Number(row?.radius_meter); + const binaan = row?.binaan_count != null ? escapeHtml(String(row.binaan_count)) : null; + + const marker = L.marker([lat, lng], { icon: makeRumahIbadahIcon(), riseOnHover: true }); + const popupEl = document.createElement('div'); + popupEl.innerHTML = ` +
${nama}
+
Jenis: ${jenis}
+
Kontak: ${kontak}
+ ${Number.isFinite(radius) ? `
Radius: ${radius} m
` : ''} + ${binaan !== null ? `
Binaan: ${binaan}
` : ''} + `; + + const actions = document.createElement('div'); + actions.className = 'popup-actions'; + const btnEditData = document.createElement('button'); + btnEditData.type = 'button'; + btnEditData.className = 'popup-btn edit'; + btnEditData.textContent = 'Edit Data'; + const btnEditLoc = document.createElement('button'); + btnEditLoc.type = 'button'; + btnEditLoc.className = 'popup-btn spatial'; + btnEditLoc.textContent = 'Edit Lokasi'; + const btnDelete = document.createElement('button'); + btnDelete.type = 'button'; + btnDelete.className = 'popup-btn delete'; + btnDelete.textContent = 'Hapus'; + + actions.appendChild(btnEditData); + actions.appendChild(btnEditLoc); + actions.appendChild(btnDelete); + popupEl.appendChild(actions); + + btnEditData.addEventListener('click', () => startEditRumahIbadah(id, marker, row)); + btnEditLoc.addEventListener('click', () => startEditLocationRumahIbadah(id, marker, row)); + btnDelete.addEventListener('click', () => deleteFeature('rumahIbadah', id)); + + marker.bindPopup(popupEl); + marker.addTo(rumahIbadahLayer); + + if (Number.isFinite(radius) && radius > 0) { + const circle = L.circle([lat, lng], { + radius, + color: '#2563eb', + weight: 2, + opacity: 0.7, + fillColor: '#60a5fa', + fillOpacity: 0.08 + }); + circle.addTo(rumahIbadahRadiusLayer); + } + + state.indices.rumahIbadah.set(id, { layer: marker, data: row }); + marker.on('click', () => setActiveListItem('rumahIbadah', id)); + count += 1; + } + + state.counts.rumahIbadah = count; + updateStats(); + renderRumahIbadahList(rows); +} + +async function loadSektor() { + sektorLayer.clearLayers(); + state.counts.sektor = 0; + state.indices.sektor.clear(); + + let fc = null; + try { + const res = await fetch('js/pontianak_kecamatan.geojson', { cache: 'no-store' }); + if (!res.ok) throw new Error('Network response was not ok'); + fc = await res.json(); + } catch (e) { + console.error('Failed to load Sektor Kecamatan:', e); + fc = null; + } + + if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) { + updateStats(); + renderSektorList([]); + return; + } + + const colors = ['#f43f5e', '#a855f7', '#3b82f6', '#10b981', '#f59e0b', '#64748b']; + let count = 0; + + const geo = L.geoJSON(fc, { + style: (feature) => { + return { + color: '#1e293b', + weight: 1, + fillColor: colors[count % colors.length], + fillOpacity: 0.25 + }; + }, + onEachFeature: (feature, layer) => { + const name = feature?.properties?.Kecamatan ?? feature?.properties?.Name ?? 'Kecamatan'; + const id = name.replace(/\s+/g, '-').toLowerCase(); + + const popupEl = document.createElement('div'); + popupEl.innerHTML = ` +
${name}
+
Tipe: Sektor Kecamatan
+ `; + + layer.bindPopup(popupEl); + + if (id) { + state.indices.sektor.set(id, { layer, data: feature.properties }); + } + count++; + } + }); + + geo.addTo(sektorLayer); + state.counts.sektor = count; + updateStats(); + renderSektorList(fc.features); +} + +function renderSektorList(features) { + if (!el.sektorList) return; + if (!Array.isArray(features) || features.length === 0) { + renderEmptyList(el.sektorList, 'Belum ada data sektor kecamatan.'); + return; + } + + el.sektorList.innerHTML = ''; + for (const f of features) { + const props = f?.properties ?? {}; + const name = props?.Kecamatan ?? props?.Name ?? 'Kecamatan'; + const id = name.replace(/\s+/g, '-').toLowerCase(); + + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'list-item'; + btn.dataset.id = String(id); + + const title = document.createElement('div'); + title.className = 'list-title'; + title.textContent = name; + + const meta = document.createElement('div'); + meta.className = 'list-meta'; + meta.textContent = 'Klik untuk zoom'; + + btn.appendChild(title); + btn.appendChild(meta); + + btn.addEventListener('click', () => { + if (el.toggleSektor) el.toggleSektor.checked = true; + applyLayerVisibility(); + + const record = state.indices.sektor.get(String(id)); + const layer = record?.layer; + if (layer?.getBounds) { + map.fitBounds(layer.getBounds(), { padding: [30, 30] }); + } + if (layer?.openPopup) layer.openPopup(); + setActiveListItem('sektor', id); + }); + + el.sektorList.appendChild(btn); + } +} + +async function loadAll() { + try { + await Promise.all([loadPoints(), loadJalan(), loadParsil(), loadPemukimanMiskin(), loadRumahIbadah(), loadSektor()]); + } catch { + updateStats(); + } +} + +// ================= MAP TOOLBAR ================= +el.btnResetView?.addEventListener('click', function () { + map.setView(INITIAL_CENTER, INITIAL_ZOOM); +}); + +el.btnMyLocation?.addEventListener('click', function () { + if (!navigator.geolocation) { + showToast('Browser tidak mendukung geolocation.', 'error'); + return; + } + + el.btnMyLocation.disabled = true; + el.btnMyLocation.textContent = 'Mencari...'; + + const restore = () => { + el.btnMyLocation.disabled = false; + el.btnMyLocation.textContent = 'Lokasi Saya'; + }; + + navigator.geolocation.getCurrentPosition( + (pos) => { + const lat = pos.coords.latitude; + const lng = pos.coords.longitude; + map.flyTo([lat, lng], Math.max(map.getZoom(), 15), { duration: 1.2 }); + showToast('Lokasi ditemukan.', 'success'); + restore(); + }, + (err) => { + if (err?.code === 1) { + showToast('Permintaan lokasi dibatalkan.', 'info'); + } else { + showToast(err?.message || 'Gagal mendapatkan lokasi.', 'error'); + } + restore(); + }, + { enableHighAccuracy: true, timeout: 10000 } + ); +}); + +el.btnFilter24?.addEventListener('click', () => togglePointFilter('24')); +el.btnFilterNon24?.addEventListener('click', () => togglePointFilter('non24')); + +// ================= EDIT BAR ================= +el.btnSaveLocation?.addEventListener('click', saveLocationEdit); +el.btnCancelLocation?.addEventListener('click', () => cancelLocationEdit()); +el.btnDeleteVertex?.addEventListener('click', () => { + if (!state.locationEditing) return; + setVertexDeleteMode(!state.locationEditing.vertexDeleteMode); +}); + +// ================= UI EVENTS ================= +el.togglePoints?.addEventListener('change', applyLayerVisibility); +el.toggleJalan?.addEventListener('change', applyLayerVisibility); +el.toggleParsil?.addEventListener('change', applyLayerVisibility); +el.toggleRumahIbadah?.addEventListener('change', applyLayerVisibility); +el.togglePemukiman?.addEventListener('change', applyLayerVisibility); +el.toggleSektor?.addEventListener('change', applyLayerVisibility); + +// Prevent toolbar clicks from being treated as map clicks/drag. +try { + const toolbar = document.querySelector('.map-toolbar'); + if (toolbar && window.L?.DomEvent) { + L.DomEvent.disableClickPropagation(toolbar); + L.DomEvent.disableScrollPropagation(toolbar); + } +} catch { + // ignore +} + +document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') resetMode(); +}); + +// ================= INIT ================= +updateModeInfo(); +applyLayerVisibility(); +loadAll(); + +if (window.location.protocol === 'file:') { + showToast('Buka aplikasi lewat server (contoh: http://localhost/webgis-spbu/) supaya tombol Simpan & Load data berfungsi.', 'error', 7000); +} + +// ================= UTIL ================= +function hitungPanjang(layer) { + const latlngs = layer.getLatLngs(); + let total = 0; + for (let i = 0; i < latlngs.length - 1; i++) { + total += map.distance(latlngs[i], latlngs[i + 1]); + } + return total; +} + +function hitungLuas(layer) { + return L.GeometryUtil.geodesicArea(layer.getLatLngs()[0]); +} diff --git a/webgis-spbu/js/pontianak_kecamatan.geojson b/webgis-spbu/js/pontianak_kecamatan.geojson new file mode 100644 index 0000000..b766024 --- /dev/null +++ b/webgis-spbu/js/pontianak_kecamatan.geojson @@ -0,0 +1,4341 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "Kecamatan": "Pontianak Barat", + "Name": "Kecamatan Pontianak Barat" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 109.2741676, + -0.0298708 + ], + [ + 109.2817778, + -0.0423889 + ], + [ + 109.2820175, + -0.0426144 + ], + [ + 109.2823883, + -0.0432466 + ], + [ + 109.2824075, + -0.0433642 + ], + [ + 109.2837928, + -0.0419721 + ], + [ + 109.2844354, + -0.0414061 + ], + [ + 109.2847768, + -0.0410838 + ], + [ + 109.2855369, + -0.0403664 + ], + [ + 109.2877745, + -0.0381716 + ], + [ + 109.2894039, + -0.0365312 + ], + [ + 109.2901842, + -0.0357804 + ], + [ + 109.291676, + -0.0342671 + ], + [ + 109.293007, + -0.0330657 + ], + [ + 109.2946594, + -0.0317142 + ], + [ + 109.2954512, + -0.0311135 + ], + [ + 109.2969429, + -0.0300161 + ], + [ + 109.2979986, + -0.0293114 + ], + [ + 109.2997771, + -0.0280869 + ], + [ + 109.3012803, + -0.0270588 + ], + [ + 109.3020331, + -0.0266799 + ], + [ + 109.3030245, + -0.0261809 + ], + [ + 109.3049523, + -0.0252914 + ], + [ + 109.3066735, + -0.0245406 + ], + [ + 109.3076718, + -0.0241478 + ], + [ + 109.3093815, + -0.0234663 + ], + [ + 109.3111831, + -0.022727 + ], + [ + 109.3130764, + -0.0221263 + ], + [ + 109.3155091, + -0.0214447 + ], + [ + 109.3179575, + -0.0208518 + ], + [ + 109.3183433, + -0.0207632 + ], + [ + 109.3199613, + -0.0204628 + ], + [ + 109.3208678, + -0.020278 + ], + [ + 109.323484, + -0.0197698 + ], + [ + 109.3247348, + -0.0195849 + ], + [ + 109.3256757, + -0.0193886 + ], + [ + 109.3265613, + -0.0192301 + ], + [ + 109.3265648, + -0.0192132 + ], + [ + 109.3266373, + -0.0192009 + ], + [ + 109.3278158, + -0.0190013 + ], + [ + 109.3298309, + -0.01863 + ], + [ + 109.3305057, + -0.0185213 + ], + [ + 109.3311534, + -0.0183674 + ], + [ + 109.3317202, + -0.0181862 + ], + [ + 109.3322388, + -0.0178681 + ], + [ + 109.332296, + -0.017833 + ], + [ + 109.3328178, + -0.0174255 + ], + [ + 109.3329617, + -0.0171809 + ], + [ + 109.3331866, + -0.0168458 + ], + [ + 109.3335225, + -0.0164315 + ], + [ + 109.3347632, + -0.0147765 + ], + [ + 109.3302446, + -0.0110535 + ], + [ + 109.3233285, + -0.0051419 + ], + [ + 109.3218891, + -0.0042186 + ], + [ + 109.3201467, + -0.0031008 + ], + [ + 109.3159294, + -0.0012995 + ], + [ + 109.3152322, + -0.0011241 + ], + [ + 109.3141567, + -0.0006571 + ], + [ + 109.3127499, + 0 + ], + [ + 109.311816, + 0.0003522 + ], + [ + 109.3102666, + 0.0008174 + ], + [ + 109.3088671, + 0.001147 + ], + [ + 109.3075518, + 0.0014411 + ], + [ + 109.3074637, + 0.0014584 + ], + [ + 109.3062181, + 0.0016332 + ], + [ + 109.3045298, + 0.0018846 + ], + [ + 109.3030103, + 0.0020375 + ], + [ + 109.2990579, + 0.0020382 + ], + [ + 109.2989681, + 0.0020386 + ], + [ + 109.2972853, + 0.0023256 + ], + [ + 109.2962287, + 0.0025397 + ], + [ + 109.2951365, + 0.0029556 + ], + [ + 109.2939708, + 0.0034306 + ], + [ + 109.2920061, + 0.0043241 + ], + [ + 109.2909361, + 0.004795 + ], + [ + 109.2888059, + 0.0055106 + ], + [ + 109.2876731, + 0.0057957 + ], + [ + 109.2872372, + 0.0059046 + ], + [ + 109.2872431, + 0.0058348 + ], + [ + 109.287219, + 0.0052928 + ], + [ + 109.2872127, + 0.0052026 + ], + [ + 109.287164, + 0.0047531 + ], + [ + 109.2869677, + 0.0033198 + ], + [ + 109.2869032, + 0.0026901 + ], + [ + 109.2867996, + 0.0018829 + ], + [ + 109.2867818, + 0.0017943 + ], + [ + 109.2867727, + 0.0017044 + ], + [ + 109.2867781, + 0.0016144 + ], + [ + 109.2867974, + 0.0014346 + ], + [ + 109.2868389, + 0.0012588 + ], + [ + 109.2868574, + 0.001079 + ], + [ + 109.2868597, + 0.0008078 + ], + [ + 109.2868372, + 0.0005375 + ], + [ + 109.2867826, + 0.0001802 + ], + [ + 109.2867711, + 0 + ], + [ + 109.2865973, + -0.0016183 + ], + [ + 109.2865473, + -0.00234 + ], + [ + 109.2865525, + -0.0034251 + ], + [ + 109.2865934, + -0.0040568 + ], + [ + 109.286891, + -0.005932 + ], + [ + 109.2870362, + -0.0061501 + ], + [ + 109.2870704, + -0.0062792 + ], + [ + 109.2870619, + -0.0064341 + ], + [ + 109.2870678, + -0.0067275 + ], + [ + 109.2870394, + -0.0074126 + ], + [ + 109.2869764, + -0.0079056 + ], + [ + 109.2869507, + -0.0083359 + ], + [ + 109.286908, + -0.0085941 + ], + [ + 109.2869251, + -0.008749 + ], + [ + 109.2868994, + -0.0089727 + ], + [ + 109.2867274, + -0.0094967 + ], + [ + 109.2864207, + -0.010255 + ], + [ + 109.286207, + -0.0108832 + ], + [ + 109.2857027, + -0.0120879 + ], + [ + 109.2856343, + -0.0123633 + ], + [ + 109.2856498, + -0.0126656 + ], + [ + 109.2857454, + -0.013112 + ], + [ + 109.2858394, + -0.013439 + ], + [ + 109.2858907, + -0.0136542 + ], + [ + 109.2859078, + -0.0138263 + ], + [ + 109.2859078, + -0.0141361 + ], + [ + 109.2856599, + -0.0152462 + ], + [ + 109.2855317, + -0.0157884 + ], + [ + 109.2854719, + -0.01621 + ], + [ + 109.2853949, + -0.0165112 + ], + [ + 109.2853522, + -0.0167264 + ], + [ + 109.2853607, + -0.0168899 + ], + [ + 109.2853865, + -0.0170974 + ], + [ + 109.2854202, + -0.0172678 + ], + [ + 109.2855062, + -0.0175185 + ], + [ + 109.2856343, + -0.0180681 + ], + [ + 109.2856428, + -0.0182065 + ], + [ + 109.285693, + -0.0184926 + ], + [ + 109.2840229, + -0.0191278 + ], + [ + 109.2831696, + -0.0194969 + ], + [ + 109.2824637, + -0.0198748 + ], + [ + 109.2819788, + -0.0201957 + ], + [ + 109.2816028, + -0.0204718 + ], + [ + 109.2810977, + -0.0209085 + ], + [ + 109.2806164, + -0.021369 + ], + [ + 109.2801262, + -0.0219905 + ], + [ + 109.279986, + -0.0221682 + ], + [ + 109.2792587, + -0.0231284 + ], + [ + 109.2788416, + -0.0237571 + ], + [ + 109.2780275, + -0.0248326 + ], + [ + 109.2774155, + -0.0256573 + ], + [ + 109.2768985, + -0.0263668 + ], + [ + 109.2753548, + -0.0284346 + ], + [ + 109.2746608, + -0.0293029 + ], + [ + 109.2741676, + -0.0298708 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "Kecamatan": "Pontianak Kota", + "Name": "Kecamatan Pontianak Kota" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 109.2847768, + -0.0410838 + ], + [ + 109.2854654, + -0.0418774 + ], + [ + 109.2912339, + -0.0479454 + ], + [ + 109.2928786, + -0.0494856 + ], + [ + 109.2954541, + -0.0519818 + ], + [ + 109.2969562, + -0.0536265 + ], + [ + 109.2967851, + -0.0537501 + ], + [ + 109.2976312, + -0.0546628 + ], + [ + 109.2988322, + -0.0559363 + ], + [ + 109.2993805, + -0.0565177 + ], + [ + 109.3010491, + -0.0583858 + ], + [ + 109.3020901, + -0.0596693 + ], + [ + 109.3022279, + -0.0598262 + ], + [ + 109.3022574, + -0.0599481 + ], + [ + 109.3021762, + -0.0600297 + ], + [ + 109.3021447, + -0.0600409 + ], + [ + 109.3020909, + -0.0601048 + ], + [ + 109.3027708, + -0.0610007 + ], + [ + 109.3028551, + -0.0611401 + ], + [ + 109.3029897, + -0.0612932 + ], + [ + 109.3030176, + -0.0613191 + ], + [ + 109.3031603, + -0.0614871 + ], + [ + 109.3030844, + -0.0615552 + ], + [ + 109.3030177, + -0.0615507 + ], + [ + 109.3028158, + -0.0617142 + ], + [ + 109.3027943, + -0.0617138 + ], + [ + 109.3026567, + -0.0618154 + ], + [ + 109.3027397, + -0.0619222 + ], + [ + 109.3035489, + -0.0629014 + ], + [ + 109.3035057, + -0.062943 + ], + [ + 109.3038028, + -0.063297 + ], + [ + 109.3038142, + -0.0633093 + ], + [ + 109.303901, + -0.0632202 + ], + [ + 109.3039174, + -0.0632362 + ], + [ + 109.3039418, + -0.06326 + ], + [ + 109.304418, + -0.0627711 + ], + [ + 109.3048763, + -0.0623214 + ], + [ + 109.3051618, + -0.0620501 + ], + [ + 109.3054074, + -0.0618017 + ], + [ + 109.3058971, + -0.0612806 + ], + [ + 109.3062625, + -0.060928 + ], + [ + 109.3068222, + -0.0603441 + ], + [ + 109.307052, + -0.0600842 + ], + [ + 109.3073876, + -0.0597801 + ], + [ + 109.3079658, + -0.0592124 + ], + [ + 109.3088212, + -0.0583383 + ], + [ + 109.3090999, + -0.0580588 + ], + [ + 109.3094366, + -0.0577304 + ], + [ + 109.3097031, + -0.0574532 + ], + [ + 109.3099535, + -0.0571801 + ], + [ + 109.3103145, + -0.0568254 + ], + [ + 109.3108064, + -0.0563258 + ], + [ + 109.3112523, + -0.0558642 + ], + [ + 109.311915, + -0.0552312 + ], + [ + 109.3120756, + -0.0550587 + ], + [ + 109.312266, + -0.0548779 + ], + [ + 109.3125087, + -0.0546304 + ], + [ + 109.3128267, + -0.0543274 + ], + [ + 109.3134837, + -0.0536685 + ], + [ + 109.3137662, + -0.0533943 + ], + [ + 109.3141862, + -0.0529743 + ], + [ + 109.3149631, + -0.0522093 + ], + [ + 109.3153546, + -0.0518168 + ], + [ + 109.3157471, + -0.0514348 + ], + [ + 109.3160641, + -0.0511319 + ], + [ + 109.3163242, + -0.050868 + ], + [ + 109.3164851, + -0.0507048 + ], + [ + 109.3170869, + -0.0501227 + ], + [ + 109.3179756, + -0.0492379 + ], + [ + 109.3186224, + -0.0485719 + ], + [ + 109.3189877, + -0.0482141 + ], + [ + 109.3191926, + -0.0479904 + ], + [ + 109.319911, + -0.0473094 + ], + [ + 109.3207602, + -0.0464545 + ], + [ + 109.3211552, + -0.0460768 + ], + [ + 109.3216095, + -0.0456494 + ], + [ + 109.3220834, + -0.0451772 + ], + [ + 109.3228932, + -0.0443521 + ], + [ + 109.3233364, + -0.0439435 + ], + [ + 109.324009, + -0.0432884 + ], + [ + 109.3244139, + -0.0428659 + ], + [ + 109.3246355, + -0.042667 + ], + [ + 109.3256531, + -0.0417228 + ], + [ + 109.3259959, + -0.0414094 + ], + [ + 109.326772, + -0.0407138 + ], + [ + 109.3272589, + -0.0403027 + ], + [ + 109.3278399, + -0.0397916 + ], + [ + 109.3282882, + -0.0393675 + ], + [ + 109.328663, + -0.0390689 + ], + [ + 109.3287735, + -0.0389692 + ], + [ + 109.3291713, + -0.0386371 + ], + [ + 109.3291974, + -0.0386153 + ], + [ + 109.3292121, + -0.038603 + ], + [ + 109.3292679, + -0.0385487 + ], + [ + 109.3296128, + -0.0382584 + ], + [ + 109.3301362, + -0.0377614 + ], + [ + 109.3306744, + -0.0372892 + ], + [ + 109.3314281, + -0.0366439 + ], + [ + 109.3317914, + -0.0363262 + ], + [ + 109.3321746, + -0.0360669 + ], + [ + 109.3328349, + -0.0355404 + ], + [ + 109.3328738, + -0.0354048 + ], + [ + 109.3329821, + -0.0351436 + ], + [ + 109.3368442, + -0.0314183 + ], + [ + 109.3374914, + -0.0308116 + ], + [ + 109.3381077, + -0.0303547 + ], + [ + 109.3388549, + -0.0298384 + ], + [ + 109.3397734, + -0.0291555 + ], + [ + 109.3403635, + -0.0287867 + ], + [ + 109.3410273, + -0.0283631 + ], + [ + 109.342041, + -0.0277706 + ], + [ + 109.3429071, + -0.0271666 + ], + [ + 109.343135, + -0.026948 + ], + [ + 109.3435191, + -0.0267085 + ], + [ + 109.343795, + -0.0265511 + ], + [ + 109.3441519, + -0.0263715 + ], + [ + 109.3444295, + -0.0262517 + ], + [ + 109.3447145, + -0.0260852 + ], + [ + 109.344759, + -0.02608 + ], + [ + 109.3456443, + -0.0256293 + ], + [ + 109.3454961, + -0.0252998 + ], + [ + 109.3454518, + -0.0252213 + ], + [ + 109.3441828, + -0.0231404 + ], + [ + 109.3441358, + -0.0230634 + ], + [ + 109.3440808, + -0.0229924 + ], + [ + 109.3435265, + -0.0223999 + ], + [ + 109.3434614, + -0.0223379 + ], + [ + 109.3428486, + -0.0218527 + ], + [ + 109.3408488, + -0.0202696 + ], + [ + 109.3382402, + -0.0178908 + ], + [ + 109.3376923, + -0.0173912 + ], + [ + 109.3354561, + -0.0153519 + ], + [ + 109.3353888, + -0.015292 + ], + [ + 109.3351802, + -0.0151202 + ], + [ + 109.3347632, + -0.0147765 + ], + [ + 109.3335225, + -0.0164315 + ], + [ + 109.3331866, + -0.0168458 + ], + [ + 109.3329617, + -0.0171809 + ], + [ + 109.3328178, + -0.0174255 + ], + [ + 109.332296, + -0.017833 + ], + [ + 109.3322388, + -0.0178681 + ], + [ + 109.3317202, + -0.0181862 + ], + [ + 109.3311534, + -0.0183674 + ], + [ + 109.3305057, + -0.0185213 + ], + [ + 109.3298309, + -0.01863 + ], + [ + 109.3278158, + -0.0190013 + ], + [ + 109.3266373, + -0.0192009 + ], + [ + 109.3265648, + -0.0192132 + ], + [ + 109.3265613, + -0.0192301 + ], + [ + 109.3256757, + -0.0193886 + ], + [ + 109.3247348, + -0.0195849 + ], + [ + 109.323484, + -0.0197698 + ], + [ + 109.3208678, + -0.020278 + ], + [ + 109.3199613, + -0.0204628 + ], + [ + 109.3183433, + -0.0207632 + ], + [ + 109.3179575, + -0.0208518 + ], + [ + 109.3155091, + -0.0214447 + ], + [ + 109.3130764, + -0.0221263 + ], + [ + 109.3111831, + -0.022727 + ], + [ + 109.3093815, + -0.0234663 + ], + [ + 109.3076718, + -0.0241478 + ], + [ + 109.3066735, + -0.0245406 + ], + [ + 109.3049523, + -0.0252914 + ], + [ + 109.3030245, + -0.0261809 + ], + [ + 109.3020331, + -0.0266799 + ], + [ + 109.3012803, + -0.0270588 + ], + [ + 109.2997771, + -0.0280869 + ], + [ + 109.2979986, + -0.0293114 + ], + [ + 109.2969429, + -0.0300161 + ], + [ + 109.2954512, + -0.0311135 + ], + [ + 109.2946594, + -0.0317142 + ], + [ + 109.293007, + -0.0330657 + ], + [ + 109.291676, + -0.0342671 + ], + [ + 109.2901842, + -0.0357804 + ], + [ + 109.2894039, + -0.0365312 + ], + [ + 109.2877745, + -0.0381716 + ], + [ + 109.2855369, + -0.0403664 + ], + [ + 109.2847768, + -0.0410838 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "Kecamatan": "Pontianak Selatan", + "Name": "Kecamatan Pontianak Selatan" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 109.3039418, + -0.06326 + ], + [ + 109.3040054, + -0.063322 + ], + [ + 109.3044946, + -0.0636438 + ], + [ + 109.3045361, + -0.0637234 + ], + [ + 109.3049351, + -0.0640178 + ], + [ + 109.3053255, + -0.0642953 + ], + [ + 109.3052846, + -0.0643656 + ], + [ + 109.3055087, + -0.0645136 + ], + [ + 109.3052998, + -0.064778 + ], + [ + 109.3061783, + -0.0654232 + ], + [ + 109.3058331, + -0.065886 + ], + [ + 109.3054797, + -0.0663854 + ], + [ + 109.30574, + -0.0665836 + ], + [ + 109.3054372, + -0.0668934 + ], + [ + 109.3053554, + -0.06683 + ], + [ + 109.3052578, + -0.0669674 + ], + [ + 109.3053089, + -0.0670067 + ], + [ + 109.305153, + -0.0671676 + ], + [ + 109.3050995, + -0.0671251 + ], + [ + 109.304986, + -0.0672429 + ], + [ + 109.3050482, + -0.0672933 + ], + [ + 109.304738, + -0.0676115 + ], + [ + 109.3055505, + -0.0686349 + ], + [ + 109.3055934, + -0.0687063 + ], + [ + 109.3052043, + -0.0691588 + ], + [ + 109.3066775, + -0.0701559 + ], + [ + 109.3078647, + -0.0709735 + ], + [ + 109.3072989, + -0.071628 + ], + [ + 109.3078743, + -0.0721731 + ], + [ + 109.3078029, + -0.0723348 + ], + [ + 109.3075147, + -0.0730326 + ], + [ + 109.3071244, + -0.0741664 + ], + [ + 109.3067034, + -0.0752978 + ], + [ + 109.3064932, + -0.0758295 + ], + [ + 109.3065801, + -0.0762906 + ], + [ + 109.3065942, + -0.0767611 + ], + [ + 109.3066128, + -0.0772192 + ], + [ + 109.3078922, + -0.0779615 + ], + [ + 109.3107796, + -0.07969 + ], + [ + 109.3108333, + -0.0797222 + ], + [ + 109.3202778, + -0.0852778 + ], + [ + 109.3206711, + -0.0857694 + ], + [ + 109.3224725, + -0.0880212 + ], + [ + 109.323928, + -0.0854277 + ], + [ + 109.3219873, + -0.0832884 + ], + [ + 109.3233608, + -0.0812877 + ], + [ + 109.3249679, + -0.0790044 + ], + [ + 109.3256936, + -0.0779265 + ], + [ + 109.3265094, + -0.0766657 + ], + [ + 109.327624, + -0.0749794 + ], + [ + 109.3288739, + -0.0731236 + ], + [ + 109.3305545, + -0.0705345 + ], + [ + 109.3310448, + -0.0698008 + ], + [ + 109.3318211, + -0.0688343 + ], + [ + 109.3331777, + -0.0672767 + ], + [ + 109.3338397, + -0.0664927 + ], + [ + 109.3343637, + -0.0658721 + ], + [ + 109.335146, + -0.0649085 + ], + [ + 109.3360536, + -0.0638581 + ], + [ + 109.3365677, + -0.0632845 + ], + [ + 109.3369769, + -0.0627779 + ], + [ + 109.3373884, + -0.062291 + ], + [ + 109.3377636, + -0.0618735 + ], + [ + 109.3379068, + -0.0617045 + ], + [ + 109.3380895, + -0.0615305 + ], + [ + 109.3385002, + -0.0610596 + ], + [ + 109.3394758, + -0.0599561 + ], + [ + 109.3405819, + -0.0586716 + ], + [ + 109.3413518, + -0.0577879 + ], + [ + 109.3418814, + -0.0572063 + ], + [ + 109.3429633, + -0.0559294 + ], + [ + 109.3440063, + -0.0547281 + ], + [ + 109.3449202, + -0.0536878 + ], + [ + 109.3455534, + -0.0529496 + ], + [ + 109.3457278, + -0.0527463 + ], + [ + 109.3464373, + -0.0519604 + ], + [ + 109.3464601, + -0.0519351 + ], + [ + 109.3465392, + -0.0518475 + ], + [ + 109.3469412, + -0.0513682 + ], + [ + 109.3476533, + -0.0504927 + ], + [ + 109.3479601, + -0.0499631 + ], + [ + 109.348686, + -0.0488562 + ], + [ + 109.3492012, + -0.0480329 + ], + [ + 109.3495006, + -0.0475738 + ], + [ + 109.3496942, + -0.0473342 + ], + [ + 109.3513827, + -0.0455163 + ], + [ + 109.3518138, + -0.0450794 + ], + [ + 109.3519166, + -0.0449509 + ], + [ + 109.3519252, + -0.0449124 + ], + [ + 109.3516639, + -0.0446868 + ], + [ + 109.3516454, + -0.044664 + ], + [ + 109.3516482, + -0.0446268 + ], + [ + 109.3516796, + -0.0445926 + ], + [ + 109.3522985, + -0.0439893 + ], + [ + 109.3527954, + -0.0435049 + ], + [ + 109.3542469, + -0.042217 + ], + [ + 109.3543235, + -0.0421357 + ], + [ + 109.3551133, + -0.0414936 + ], + [ + 109.3551747, + -0.0414386 + ], + [ + 109.3541897, + -0.0404159 + ], + [ + 109.3525049, + -0.0384155 + ], + [ + 109.3524501, + -0.038344 + ], + [ + 109.3512119, + -0.0365673 + ], + [ + 109.3499861, + -0.0344603 + ], + [ + 109.3499437, + -0.0343806 + ], + [ + 109.3472764, + -0.0292533 + ], + [ + 109.3467671, + -0.0281247 + ], + [ + 109.3456443, + -0.0256293 + ], + [ + 109.344759, + -0.02608 + ], + [ + 109.3447145, + -0.0260852 + ], + [ + 109.3444295, + -0.0262517 + ], + [ + 109.3441519, + -0.0263715 + ], + [ + 109.343795, + -0.0265511 + ], + [ + 109.3435191, + -0.0267085 + ], + [ + 109.343135, + -0.026948 + ], + [ + 109.3429071, + -0.0271666 + ], + [ + 109.342041, + -0.0277706 + ], + [ + 109.3410273, + -0.0283631 + ], + [ + 109.3403635, + -0.0287867 + ], + [ + 109.3397734, + -0.0291555 + ], + [ + 109.3388549, + -0.0298384 + ], + [ + 109.3381077, + -0.0303547 + ], + [ + 109.3374914, + -0.0308116 + ], + [ + 109.3368442, + -0.0314183 + ], + [ + 109.3329821, + -0.0351436 + ], + [ + 109.3328738, + -0.0354048 + ], + [ + 109.3328349, + -0.0355404 + ], + [ + 109.3321746, + -0.0360669 + ], + [ + 109.3317914, + -0.0363262 + ], + [ + 109.3314281, + -0.0366439 + ], + [ + 109.3306744, + -0.0372892 + ], + [ + 109.3301362, + -0.0377614 + ], + [ + 109.3296128, + -0.0382584 + ], + [ + 109.3292679, + -0.0385487 + ], + [ + 109.3292121, + -0.038603 + ], + [ + 109.3291974, + -0.0386153 + ], + [ + 109.3291713, + -0.0386371 + ], + [ + 109.3287735, + -0.0389692 + ], + [ + 109.328663, + -0.0390689 + ], + [ + 109.3282882, + -0.0393675 + ], + [ + 109.3278399, + -0.0397916 + ], + [ + 109.3272589, + -0.0403027 + ], + [ + 109.326772, + -0.0407138 + ], + [ + 109.3259959, + -0.0414094 + ], + [ + 109.3256531, + -0.0417228 + ], + [ + 109.3246355, + -0.042667 + ], + [ + 109.3244139, + -0.0428659 + ], + [ + 109.324009, + -0.0432884 + ], + [ + 109.3233364, + -0.0439435 + ], + [ + 109.3228932, + -0.0443521 + ], + [ + 109.3220834, + -0.0451772 + ], + [ + 109.3216095, + -0.0456494 + ], + [ + 109.3211552, + -0.0460768 + ], + [ + 109.3207602, + -0.0464545 + ], + [ + 109.319911, + -0.0473094 + ], + [ + 109.3191926, + -0.0479904 + ], + [ + 109.3189877, + -0.0482141 + ], + [ + 109.3186224, + -0.0485719 + ], + [ + 109.3179756, + -0.0492379 + ], + [ + 109.3170869, + -0.0501227 + ], + [ + 109.3164851, + -0.0507048 + ], + [ + 109.3163242, + -0.050868 + ], + [ + 109.3160641, + -0.0511319 + ], + [ + 109.3157471, + -0.0514348 + ], + [ + 109.3153546, + -0.0518168 + ], + [ + 109.3149631, + -0.0522093 + ], + [ + 109.3141862, + -0.0529743 + ], + [ + 109.3137662, + -0.0533943 + ], + [ + 109.3134837, + -0.0536685 + ], + [ + 109.3128267, + -0.0543274 + ], + [ + 109.3125087, + -0.0546304 + ], + [ + 109.312266, + -0.0548779 + ], + [ + 109.3120756, + -0.0550587 + ], + [ + 109.311915, + -0.0552312 + ], + [ + 109.3112523, + -0.0558642 + ], + [ + 109.3108064, + -0.0563258 + ], + [ + 109.3103145, + -0.0568254 + ], + [ + 109.3099535, + -0.0571801 + ], + [ + 109.3097031, + -0.0574532 + ], + [ + 109.3094366, + -0.0577304 + ], + [ + 109.3090999, + -0.0580588 + ], + [ + 109.3088212, + -0.0583383 + ], + [ + 109.3079658, + -0.0592124 + ], + [ + 109.3073876, + -0.0597801 + ], + [ + 109.307052, + -0.0600842 + ], + [ + 109.3068222, + -0.0603441 + ], + [ + 109.3062625, + -0.060928 + ], + [ + 109.3058971, + -0.0612806 + ], + [ + 109.3054074, + -0.0618017 + ], + [ + 109.3051618, + -0.0620501 + ], + [ + 109.3048763, + -0.0623214 + ], + [ + 109.304418, + -0.0627711 + ], + [ + 109.3039418, + -0.06326 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "Kecamatan": "Pontianak Tenggara", + "Name": "Kecamatan Pontianak Tenggara" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 109.3219826, + -0.0941803 + ], + [ + 109.3295691, + -0.0948928 + ], + [ + 109.3336111, + -0.0952778 + ], + [ + 109.3358541, + -0.0950771 + ], + [ + 109.3372231, + -0.0950771 + ], + [ + 109.3380013, + -0.095006 + ], + [ + 109.3429523, + -0.0981948 + ], + [ + 109.3434103, + -0.0976464 + ], + [ + 109.3453547, + -0.0950914 + ], + [ + 109.3457895, + -0.0944185 + ], + [ + 109.3468384, + -0.0926226 + ], + [ + 109.3476632, + -0.0912256 + ], + [ + 109.3480241, + -0.0906143 + ], + [ + 109.3486415, + -0.0896007 + ], + [ + 109.3503423, + -0.0869972 + ], + [ + 109.3523262, + -0.0839272 + ], + [ + 109.3540143, + -0.0814739 + ], + [ + 109.354886, + -0.0802671 + ], + [ + 109.3549645, + -0.0801569 + ], + [ + 109.3550382, + -0.0800433 + ], + [ + 109.3554734, + -0.0793574 + ], + [ + 109.356482, + -0.0775941 + ], + [ + 109.3578695, + -0.0752683 + ], + [ + 109.3593585, + -0.0728458 + ], + [ + 109.3609939, + -0.070362 + ], + [ + 109.3614586, + -0.0695875 + ], + [ + 109.361472, + -0.0695652 + ], + [ + 109.3615186, + -0.0694875 + ], + [ + 109.3615413, + -0.0694463 + ], + [ + 109.3626025, + -0.0675195 + ], + [ + 109.3633345, + -0.0660554 + ], + [ + 109.3642757, + -0.0639162 + ], + [ + 109.3648557, + -0.0626993 + ], + [ + 109.3656768, + -0.0609347 + ], + [ + 109.3660333, + -0.060186 + ], + [ + 109.3668818, + -0.0594587 + ], + [ + 109.3688871, + -0.0577676 + ], + [ + 109.3675808, + -0.0556382 + ], + [ + 109.365504, + -0.0522526 + ], + [ + 109.3646661, + -0.051077 + ], + [ + 109.3646114, + -0.0510054 + ], + [ + 109.3623441, + -0.0484363 + ], + [ + 109.3605651, + -0.0466462 + ], + [ + 109.3604994, + -0.0465847 + ], + [ + 109.3602786, + -0.0463937 + ], + [ + 109.3583193, + -0.0446985 + ], + [ + 109.3551902, + -0.0414546 + ], + [ + 109.3551747, + -0.0414386 + ], + [ + 109.3551133, + -0.0414936 + ], + [ + 109.3543235, + -0.0421357 + ], + [ + 109.3542469, + -0.042217 + ], + [ + 109.3527954, + -0.0435049 + ], + [ + 109.3522985, + -0.0439893 + ], + [ + 109.3516796, + -0.0445926 + ], + [ + 109.3516482, + -0.0446268 + ], + [ + 109.3516454, + -0.044664 + ], + [ + 109.3516639, + -0.0446868 + ], + [ + 109.3519252, + -0.0449124 + ], + [ + 109.3519166, + -0.0449509 + ], + [ + 109.3518138, + -0.0450794 + ], + [ + 109.3513827, + -0.0455163 + ], + [ + 109.3496942, + -0.0473342 + ], + [ + 109.3495006, + -0.0475738 + ], + [ + 109.3492012, + -0.0480329 + ], + [ + 109.348686, + -0.0488562 + ], + [ + 109.3479601, + -0.0499631 + ], + [ + 109.3476533, + -0.0504927 + ], + [ + 109.3469412, + -0.0513682 + ], + [ + 109.3465392, + -0.0518475 + ], + [ + 109.3464601, + -0.0519351 + ], + [ + 109.3464373, + -0.0519604 + ], + [ + 109.3457278, + -0.0527463 + ], + [ + 109.3455534, + -0.0529496 + ], + [ + 109.3449202, + -0.0536878 + ], + [ + 109.3440063, + -0.0547281 + ], + [ + 109.3429633, + -0.0559294 + ], + [ + 109.3418814, + -0.0572063 + ], + [ + 109.3413518, + -0.0577879 + ], + [ + 109.3405819, + -0.0586716 + ], + [ + 109.3394758, + -0.0599561 + ], + [ + 109.3385002, + -0.0610596 + ], + [ + 109.3380895, + -0.0615305 + ], + [ + 109.3379068, + -0.0617045 + ], + [ + 109.3377636, + -0.0618735 + ], + [ + 109.3373884, + -0.062291 + ], + [ + 109.3369769, + -0.0627779 + ], + [ + 109.3365677, + -0.0632845 + ], + [ + 109.3360536, + -0.0638581 + ], + [ + 109.335146, + -0.0649085 + ], + [ + 109.3343637, + -0.0658721 + ], + [ + 109.3338397, + -0.0664927 + ], + [ + 109.3331777, + -0.0672767 + ], + [ + 109.3318211, + -0.0688343 + ], + [ + 109.3310448, + -0.0698008 + ], + [ + 109.3305545, + -0.0705345 + ], + [ + 109.3288739, + -0.0731236 + ], + [ + 109.327624, + -0.0749794 + ], + [ + 109.3265094, + -0.0766657 + ], + [ + 109.3256936, + -0.0779265 + ], + [ + 109.3249679, + -0.0790044 + ], + [ + 109.3233608, + -0.0812877 + ], + [ + 109.3219873, + -0.0832884 + ], + [ + 109.323928, + -0.0854277 + ], + [ + 109.3224725, + -0.0880212 + ], + [ + 109.3225, + -0.0880556 + ], + [ + 109.322213, + -0.090812 + ], + [ + 109.3220872, + -0.0922036 + ], + [ + 109.3219826, + -0.0941803 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "Kecamatan": "Pontianak Timur", + "Name": "Kecamatan Pontianak Timur" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 109.3441358, + -0.0230634 + ], + [ + 109.3441828, + -0.0231404 + ], + [ + 109.3454518, + -0.0252213 + ], + [ + 109.3454961, + -0.0252998 + ], + [ + 109.3456443, + -0.0256293 + ], + [ + 109.3467671, + -0.0281247 + ], + [ + 109.3472764, + -0.0292533 + ], + [ + 109.3499437, + -0.0343806 + ], + [ + 109.3499861, + -0.0344603 + ], + [ + 109.3512119, + -0.0365673 + ], + [ + 109.3524501, + -0.038344 + ], + [ + 109.3525049, + -0.0384155 + ], + [ + 109.3541897, + -0.0404159 + ], + [ + 109.3551747, + -0.0414386 + ], + [ + 109.3551902, + -0.0414546 + ], + [ + 109.3583193, + -0.0446985 + ], + [ + 109.3602786, + -0.0463937 + ], + [ + 109.3604994, + -0.0465847 + ], + [ + 109.3605651, + -0.0466462 + ], + [ + 109.3623441, + -0.0484363 + ], + [ + 109.3646114, + -0.0510054 + ], + [ + 109.3646661, + -0.051077 + ], + [ + 109.365504, + -0.0522526 + ], + [ + 109.3675808, + -0.0556382 + ], + [ + 109.3688871, + -0.0577676 + ], + [ + 109.3690209, + -0.0576547 + ], + [ + 109.3711601, + -0.0605996 + ], + [ + 109.3721512, + -0.0619472 + ], + [ + 109.3727454, + -0.0626365 + ], + [ + 109.3728214, + -0.0627078 + ], + [ + 109.3728927, + -0.0627838 + ], + [ + 109.373014, + -0.0626725 + ], + [ + 109.3730252, + -0.0626828 + ], + [ + 109.3736081, + -0.0621553 + ], + [ + 109.3756858, + -0.0604779 + ], + [ + 109.3771061, + -0.0593951 + ], + [ + 109.3781823, + -0.0586108 + ], + [ + 109.379165, + -0.0579008 + ], + [ + 109.3850615, + -0.0534934 + ], + [ + 109.3849499, + -0.0533736 + ], + [ + 109.385, + -0.0533333 + ], + [ + 109.3827939, + -0.0502926 + ], + [ + 109.3853475, + -0.0483388 + ], + [ + 109.385312, + -0.0482918 + ], + [ + 109.3832415, + -0.0455524 + ], + [ + 109.3826096, + -0.0445867 + ], + [ + 109.3807742, + -0.041782 + ], + [ + 109.379931, + -0.0404682 + ], + [ + 109.3796015, + -0.0398511 + ], + [ + 109.3793912, + -0.0398332 + ], + [ + 109.3792735, + -0.0397085 + ], + [ + 109.3785355, + -0.0389598 + ], + [ + 109.3785278, + -0.0388552 + ], + [ + 109.3785829, + -0.0387652 + ], + [ + 109.3788427, + -0.0383412 + ], + [ + 109.3794268, + -0.0372837 + ], + [ + 109.3793626, + -0.0370342 + ], + [ + 109.3795801, + -0.0366741 + ], + [ + 109.3797405, + -0.036421 + ], + [ + 109.3799877, + -0.0363718 + ], + [ + 109.3798499, + -0.0358204 + ], + [ + 109.3798406, + -0.0356332 + ], + [ + 109.3798019, + -0.0354817 + ], + [ + 109.3796694, + -0.0351452 + ], + [ + 109.3795458, + -0.0346794 + ], + [ + 109.3793739, + -0.0342816 + ], + [ + 109.37906, + -0.0336908 + ], + [ + 109.3789501, + -0.0334502 + ], + [ + 109.3788401, + -0.0331204 + ], + [ + 109.3787213, + -0.0327846 + ], + [ + 109.3790362, + -0.0326955 + ], + [ + 109.379464, + -0.0325886 + ], + [ + 109.3795383, + -0.032541 + ], + [ + 109.379721, + -0.0323912 + ], + [ + 109.3803611, + -0.0318056 + ], + [ + 109.3806111, + -0.0315556 + ], + [ + 109.3810254, + -0.0313242 + ], + [ + 109.3812778, + -0.031 + ], + [ + 109.381584, + -0.0307835 + ], + [ + 109.3818611, + -0.0304167 + ], + [ + 109.3820556, + -0.0299444 + ], + [ + 109.3821187, + -0.0299219 + ], + [ + 109.3821667, + -0.0295556 + ], + [ + 109.3825079, + -0.028604 + ], + [ + 109.3823594, + -0.0285683 + ], + [ + 109.382849, + -0.0267905 + ], + [ + 109.3828966, + -0.0266063 + ], + [ + 109.3831516, + -0.0251057 + ], + [ + 109.3819197, + -0.0248593 + ], + [ + 109.3795667, + -0.0244078 + ], + [ + 109.3779504, + -0.0237898 + ], + [ + 109.3760728, + -0.0231005 + ], + [ + 109.3754204, + -0.0226324 + ], + [ + 109.3752554, + -0.0225587 + ], + [ + 109.3751719, + -0.0225256 + ], + [ + 109.3729755, + -0.021726 + ], + [ + 109.3709272, + -0.0210488 + ], + [ + 109.3688531, + -0.0204563 + ], + [ + 109.3687655, + -0.0204369 + ], + [ + 109.3653059, + -0.0198814 + ], + [ + 109.3638714, + -0.0197918 + ], + [ + 109.362974, + -0.0198291 + ], + [ + 109.3621662, + -0.0198626 + ], + [ + 109.3603708, + -0.0199259 + ], + [ + 109.3574238, + -0.0202494 + ], + [ + 109.3561128, + -0.0205619 + ], + [ + 109.3543387, + -0.0212074 + ], + [ + 109.3525924, + -0.0216327 + ], + [ + 109.3525039, + -0.0216473 + ], + [ + 109.3496218, + -0.0220307 + ], + [ + 109.3483183, + -0.0222042 + ], + [ + 109.3473387, + -0.0223345 + ], + [ + 109.3454627, + -0.0225339 + ], + [ + 109.3453741, + -0.0225466 + ], + [ + 109.3441986, + -0.0229965 + ], + [ + 109.3441358, + -0.0230634 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "Kecamatan": "Pontianak Utara", + "Name": "Kecamatan Pontianak Utara" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 109.2872372, + 0.0059046 + ], + [ + 109.2876731, + 0.0057957 + ], + [ + 109.2888059, + 0.0055106 + ], + [ + 109.2909361, + 0.004795 + ], + [ + 109.2920061, + 0.0043241 + ], + [ + 109.2939708, + 0.0034306 + ], + [ + 109.2951365, + 0.0029556 + ], + [ + 109.2962287, + 0.0025397 + ], + [ + 109.2972853, + 0.0023256 + ], + [ + 109.2989681, + 0.0020386 + ], + [ + 109.2990579, + 0.0020382 + ], + [ + 109.3030103, + 0.0020375 + ], + [ + 109.3045298, + 0.0018846 + ], + [ + 109.3062181, + 0.0016332 + ], + [ + 109.3074637, + 0.0014584 + ], + [ + 109.3075518, + 0.0014411 + ], + [ + 109.3088671, + 0.001147 + ], + [ + 109.3102666, + 0.0008174 + ], + [ + 109.311816, + 0.0003522 + ], + [ + 109.3127499, + 0 + ], + [ + 109.3141567, + -0.0006571 + ], + [ + 109.3152322, + -0.0011241 + ], + [ + 109.3159294, + -0.0012995 + ], + [ + 109.3201467, + -0.0031008 + ], + [ + 109.3218891, + -0.0042186 + ], + [ + 109.3233285, + -0.0051419 + ], + [ + 109.3302446, + -0.0110535 + ], + [ + 109.3347632, + -0.0147765 + ], + [ + 109.3351802, + -0.0151202 + ], + [ + 109.3353888, + -0.015292 + ], + [ + 109.3354561, + -0.0153519 + ], + [ + 109.3376923, + -0.0173912 + ], + [ + 109.3382402, + -0.0178908 + ], + [ + 109.3408488, + -0.0202696 + ], + [ + 109.3428486, + -0.0218527 + ], + [ + 109.3434614, + -0.0223379 + ], + [ + 109.3435265, + -0.0223999 + ], + [ + 109.3440808, + -0.0229924 + ], + [ + 109.3441358, + -0.0230634 + ], + [ + 109.3441986, + -0.0229965 + ], + [ + 109.3453741, + -0.0225466 + ], + [ + 109.3454627, + -0.0225339 + ], + [ + 109.3473387, + -0.0223345 + ], + [ + 109.3483183, + -0.0222042 + ], + [ + 109.3496218, + -0.0220307 + ], + [ + 109.3525039, + -0.0216473 + ], + [ + 109.3525924, + -0.0216327 + ], + [ + 109.3543387, + -0.0212074 + ], + [ + 109.3561128, + -0.0205619 + ], + [ + 109.3574238, + -0.0202494 + ], + [ + 109.3603708, + -0.0199259 + ], + [ + 109.3621662, + -0.0198626 + ], + [ + 109.362974, + -0.0198291 + ], + [ + 109.3638714, + -0.0197918 + ], + [ + 109.3653059, + -0.0198814 + ], + [ + 109.3687655, + -0.0204369 + ], + [ + 109.3688531, + -0.0204563 + ], + [ + 109.3709272, + -0.0210488 + ], + [ + 109.3729755, + -0.021726 + ], + [ + 109.3751719, + -0.0225256 + ], + [ + 109.3752554, + -0.0225587 + ], + [ + 109.3754204, + -0.0226324 + ], + [ + 109.3760728, + -0.0231005 + ], + [ + 109.3767856, + -0.0219637 + ], + [ + 109.3770102, + -0.0210272 + ], + [ + 109.3769805, + -0.0205935 + ], + [ + 109.3770388, + -0.020193 + ], + [ + 109.376996, + -0.0197081 + ], + [ + 109.3771433, + -0.0193611 + ], + [ + 109.3774143, + -0.0187003 + ], + [ + 109.3777233, + -0.0182773 + ], + [ + 109.3779372, + -0.0178589 + ], + [ + 109.377961, + -0.0174692 + ], + [ + 109.3780323, + -0.0169415 + ], + [ + 109.3781368, + -0.0165755 + ], + [ + 109.3787833, + -0.0161381 + ], + [ + 109.3788261, + -0.015948 + ], + [ + 109.3786217, + -0.0156866 + ], + [ + 109.3783745, + -0.0155297 + ], + [ + 109.3779134, + -0.0154726 + ], + [ + 109.3776377, + -0.0153966 + ], + [ + 109.3775569, + -0.0152112 + ], + [ + 109.3778088, + -0.0150115 + ], + [ + 109.3779657, + -0.0148975 + ], + [ + 109.3785789, + -0.0146265 + ], + [ + 109.37875, + -0.0144221 + ], + [ + 109.378964, + -0.0140798 + ], + [ + 109.379002, + -0.0136092 + ], + [ + 109.3788974, + -0.0134571 + ], + [ + 109.378655, + -0.0134286 + ], + [ + 109.378346, + -0.0135142 + ], + [ + 109.3778232, + -0.0138454 + ], + [ + 109.3777277, + -0.0138697 + ], + [ + 109.3776633, + -0.0138402 + ], + [ + 109.3775828, + -0.0136846 + ], + [ + 109.3776044, + -0.0135855 + ], + [ + 109.3780691, + -0.0131897 + ], + [ + 109.3782023, + -0.0130387 + ], + [ + 109.3784541, + -0.0126906 + ], + [ + 109.3784506, + -0.0123305 + ], + [ + 109.3783353, + -0.0121796 + ], + [ + 109.3778077, + -0.0122271 + ], + [ + 109.3774511, + -0.0123174 + ], + [ + 109.3773798, + -0.0120275 + ], + [ + 109.3775842, + -0.0115046 + ], + [ + 109.3777211, + -0.0112821 + ], + [ + 109.3779954, + -0.0110605 + ], + [ + 109.3779208, + -0.010802 + ], + [ + 109.3778685, + -0.0105358 + ], + [ + 109.3778637, + -0.0101508 + ], + [ + 109.3778257, + -0.0095661 + ], + [ + 109.3778115, + -0.0089671 + ], + [ + 109.3777259, + -0.0087675 + ], + [ + 109.3774549, + -0.0079593 + ], + [ + 109.3774472, + -0.0078927 + ], + [ + 109.3774427, + -0.0078892 + ], + [ + 109.3774307, + -0.0078906 + ], + [ + 109.377407, + -0.0078033 + ], + [ + 109.3773748, + -0.0077191 + ], + [ + 109.3772344, + -0.0073863 + ], + [ + 109.3770468, + -0.006782 + ], + [ + 109.3769079, + -0.0063523 + ], + [ + 109.3768599, + -0.0062759 + ], + [ + 109.3768036, + -0.0062057 + ], + [ + 109.3767392, + -0.0061432 + ], + [ + 109.3765984, + -0.0060309 + ], + [ + 109.3765378, + -0.0059648 + ], + [ + 109.3764821, + -0.0058938 + ], + [ + 109.3764461, + -0.0058115 + ], + [ + 109.3764169, + -0.0057261 + ], + [ + 109.3763555, + -0.005462 + ], + [ + 109.3763317, + -0.0053752 + ], + [ + 109.3762582, + -0.0052102 + ], + [ + 109.3760494, + -0.0048099 + ], + [ + 109.3757244, + -0.0042679 + ], + [ + 109.375691, + -0.0041849 + ], + [ + 109.3756792, + -0.004096 + ], + [ + 109.3756943, + -0.0040083 + ], + [ + 109.3757328, + -0.0039274 + ], + [ + 109.3758387, + -0.0037814 + ], + [ + 109.3758771, + -0.0037014 + ], + [ + 109.3758822, + -0.0036113 + ], + [ + 109.3758846, + -0.0034657 + ], + [ + 109.3758402, + -0.003323 + ], + [ + 109.3757609, + -0.0031426 + ], + [ + 109.3757456, + -0.0030881 + ], + [ + 109.3757456, + -0.0030182 + ], + [ + 109.3757595, + -0.0029846 + ], + [ + 109.3757929, + -0.002944 + ], + [ + 109.3758917, + -0.0028677 + ], + [ + 109.3759674, + -0.0027941 + ], + [ + 109.3759947, + -0.0027328 + ], + [ + 109.3760051, + -0.0026704 + ], + [ + 109.375974, + -0.0025862 + ], + [ + 109.3759254, + -0.0025107 + ], + [ + 109.3758577, + -0.0024525 + ], + [ + 109.3757837, + -0.0024013 + ], + [ + 109.3756283, + -0.0023105 + ], + [ + 109.3754667, + -0.0022319 + ], + [ + 109.3752916, + -0.0021913 + ], + [ + 109.3752031, + -0.0021769 + ], + [ + 109.3750243, + -0.0021588 + ], + [ + 109.3749358, + -0.0021454 + ], + [ + 109.3748501, + -0.0021186 + ], + [ + 109.3747668, + -0.002085 + ], + [ + 109.3746874, + -0.0020428 + ], + [ + 109.3746115, + -0.0019955 + ], + [ + 109.3738221, + -0.0012565 + ], + [ + 109.3736165, + -0.0010811 + ], + [ + 109.3734182, + -0.0008974 + ], + [ + 109.3732969, + -0.0007641 + ], + [ + 109.3732508, + -0.0006868 + ], + [ + 109.3732089, + -0.0006069 + ], + [ + 109.373172, + -0.0005245 + ], + [ + 109.373143, + -0.0004391 + ], + [ + 109.3730706, + -0.0001778 + ], + [ + 109.373022, + 0.0000718 + ], + [ + 109.3729981, + 0.000342 + ], + [ + 109.3729593, + 0.0004221 + ], + [ + 109.3728963, + 0.0004847 + ], + [ + 109.3728196, + 0.0005317 + ], + [ + 109.3727507, + 0.0005883 + ], + [ + 109.3726979, + 0.0006603 + ], + [ + 109.3726209, + 0.0008238 + ], + [ + 109.3725903, + 0.0009085 + ], + [ + 109.3724879, + 0.0012553 + ], + [ + 109.3723913, + 0.0014072 + ], + [ + 109.3722877, + 0.0015443 + ], + [ + 109.3722597, + 0.0017229 + ], + [ + 109.3722108, + 0.0018969 + ], + [ + 109.3721685, + 0.0020724 + ], + [ + 109.3721455, + 0.0022517 + ], + [ + 109.3720933, + 0.0027918 + ], + [ + 109.3720682, + 0.0029708 + ], + [ + 109.3720285, + 0.0031471 + ], + [ + 109.3719757, + 0.00332 + ], + [ + 109.3719148, + 0.0034901 + ], + [ + 109.3718455, + 0.0036569 + ], + [ + 109.3717671, + 0.0038196 + ], + [ + 109.3717186, + 0.0039931 + ], + [ + 109.3716922, + 0.0041721 + ], + [ + 109.371671, + 0.0043516 + ], + [ + 109.3716052, + 0.0047071 + ], + [ + 109.3715607, + 0.005066 + ], + [ + 109.3715275, + 0.0052438 + ], + [ + 109.3714828, + 0.0054189 + ], + [ + 109.3713289, + 0.0061255 + ], + [ + 109.3713044, + 0.0063047 + ], + [ + 109.3712868, + 0.0064846 + ], + [ + 109.3712643, + 0.006664 + ], + [ + 109.3712291, + 0.0068404 + ], + [ + 109.3711297, + 0.0069895 + ], + [ + 109.3708403, + 0.0072002 + ], + [ + 109.370784, + 0.0073701 + ], + [ + 109.3707283, + 0.0079097 + ], + [ + 109.3707042, + 0.0080889 + ], + [ + 109.370667, + 0.0082657 + ], + [ + 109.3706209, + 0.0084404 + ], + [ + 109.3705734, + 0.0086148 + ], + [ + 109.3705164, + 0.0087863 + ], + [ + 109.3704513, + 0.0089548 + ], + [ + 109.3703658, + 0.0091123 + ], + [ + 109.3701986, + 0.0091609 + ], + [ + 109.3700189, + 0.0091596 + ], + [ + 109.3698471, + 0.0091989 + ], + [ + 109.3697117, + 0.0093173 + ], + [ + 109.3695706, + 0.0094284 + ], + [ + 109.3694128, + 0.0095149 + ], + [ + 109.3693578, + 0.009676 + ], + [ + 109.3694532, + 0.0098261 + ], + [ + 109.3695639, + 0.0099676 + ], + [ + 109.3696069, + 0.010141 + ], + [ + 109.3695992, + 0.0103215 + ], + [ + 109.3695851, + 0.0105016 + ], + [ + 109.3696127, + 0.0106792 + ], + [ + 109.3696883, + 0.0108431 + ], + [ + 109.3697431, + 0.0110149 + ], + [ + 109.3698665, + 0.011543 + ], + [ + 109.3699002, + 0.0117207 + ], + [ + 109.3699193, + 0.0119003 + ], + [ + 109.3699181, + 0.0120811 + ], + [ + 109.3698722, + 0.0122545 + ], + [ + 109.3698467, + 0.0124318 + ], + [ + 109.3699285, + 0.0125907 + ], + [ + 109.3700339, + 0.0127371 + ], + [ + 109.3701097, + 0.0129004 + ], + [ + 109.3702259, + 0.0132427 + ], + [ + 109.3702917, + 0.0134109 + ], + [ + 109.3703681, + 0.0135745 + ], + [ + 109.3704524, + 0.0137341 + ], + [ + 109.3705297, + 0.0138974 + ], + [ + 109.3705801, + 0.0140689 + ], + [ + 109.3704971, + 0.0142137 + ], + [ + 109.3703383, + 0.0142979 + ], + [ + 109.3702191, + 0.0144233 + ], + [ + 109.3702203, + 0.0146013 + ], + [ + 109.370293, + 0.0147661 + ], + [ + 109.3703359, + 0.0149397 + ], + [ + 109.370325, + 0.0153012 + ], + [ + 109.3703084, + 0.0154812 + ], + [ + 109.3702725, + 0.0156584 + ], + [ + 109.3702313, + 0.0158344 + ], + [ + 109.3702081, + 0.0160134 + ], + [ + 109.3702343, + 0.0161905 + ], + [ + 109.3703115, + 0.016353 + ], + [ + 109.3704059, + 0.0165069 + ], + [ + 109.3704908, + 0.0166663 + ], + [ + 109.3705658, + 0.0168305 + ], + [ + 109.3706273, + 0.0170004 + ], + [ + 109.3706741, + 0.0171748 + ], + [ + 109.3706925, + 0.0173544 + ], + [ + 109.3706906, + 0.0177162 + ], + [ + 109.3706797, + 0.0180777 + ], + [ + 109.3706406, + 0.0182533 + ], + [ + 109.3705487, + 0.0184077 + ], + [ + 109.3704869, + 0.0185751 + ], + [ + 109.3704969, + 0.0187543 + ], + [ + 109.3705982, + 0.0189017 + ], + [ + 109.3707083, + 0.0190441 + ], + [ + 109.3707731, + 0.0192122 + ], + [ + 109.3708037, + 0.0193903 + ], + [ + 109.3708578, + 0.0197479 + ], + [ + 109.3709194, + 0.0202869 + ], + [ + 109.3709333, + 0.020467 + ], + [ + 109.3709799, + 0.0206413 + ], + [ + 109.3710169, + 0.0208169 + ], + [ + 109.370967, + 0.0209858 + ], + [ + 109.3708205, + 0.0210841 + ], + [ + 109.3706487, + 0.0211354 + ], + [ + 109.3704911, + 0.0212146 + ], + [ + 109.3685986, + 0.021837 + ], + [ + 109.3653292, + 0.022765 + ], + [ + 109.3619028, + 0.0240499 + ], + [ + 109.3571058, + 0.0258202 + ], + [ + 109.3521819, + 0.0269911 + ], + [ + 109.3484619, + 0.0279787 + ], + [ + 109.3336119, + 0.0319211 + ], + [ + 109.3213982, + 0.0349248 + ], + [ + 109.3097594, + 0.0377872 + ], + [ + 109.3087415, + 0.0381168 + ], + [ + 109.3075042, + 0.0365847 + ], + [ + 109.3069516, + 0.0362024 + ], + [ + 109.3064506, + 0.0357025 + ], + [ + 109.3042403, + 0.0347293 + ], + [ + 109.30347, + 0.0338443 + ], + [ + 109.2994279, + 0.0322397 + ], + [ + 109.2983288, + 0.0309507 + ], + [ + 109.2979084, + 0.0302325 + ], + [ + 109.2976719, + 0.0297053 + ], + [ + 109.2968118, + 0.0271588 + ], + [ + 109.2960776, + 0.0252037 + ], + [ + 109.2956724, + 0.024062 + ], + [ + 109.2951035, + 0.0221229 + ], + [ + 109.2941818, + 0.0187902 + ], + [ + 109.2927546, + 0.0148399 + ], + [ + 109.2920294, + 0.0127309 + ], + [ + 109.2904892, + 0.0084894 + ], + [ + 109.2872372, + 0.0059046 + ] + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/webgis-spbu/migrate_now.php b/webgis-spbu/migrate_now.php new file mode 100644 index 0000000..afd323d --- /dev/null +++ b/webgis-spbu/migrate_now.php @@ -0,0 +1,10 @@ +exec("ALTER TABLE rumah_ibadah_points ADD COLUMN IF NOT EXISTS jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya' AFTER nama"); + $pdo->exec("ALTER TABLE rumah_ibadah_points ADD COLUMN IF NOT EXISTS kontak VARCHAR(120) NULL AFTER jenis"); + echo "Columns added successfully.\n"; +} catch (PDOException $e) { + echo "Failed to add columns: " . $e->getMessage() . "\n"; +} diff --git a/webgis-spbu/osm_pontianak.json b/webgis-spbu/osm_pontianak.json new file mode 100644 index 0000000..d657cb7 --- /dev/null +++ b/webgis-spbu/osm_pontianak.json @@ -0,0 +1,9 @@ + + +406 Not Acceptable + +

Not Acceptable

+

An appropriate representation of the requested resource could not be found on this server.

+
+
Apache/2.4.67 (Debian) Server at overpass-api.de Port 443
+ diff --git a/webgis-spbu/osm_to_geojson_draft.js b/webgis-spbu/osm_to_geojson_draft.js new file mode 100644 index 0000000..2173a22 --- /dev/null +++ b/webgis-spbu/osm_to_geojson_draft.js @@ -0,0 +1,40 @@ +const fs = require('fs'); + +const data = JSON.parse(fs.readFileSync('osm_pontianak.json', 'utf8')); + +const geojson = { + type: 'FeatureCollection', + features: [] +}; + +data.elements.forEach(el => { + if (el.type === 'relation' && el.tags && el.tags.admin_level === '6') { + const name = el.tags.name; + // Extract polygons from relation members + const coordinates = []; + el.members.forEach(member => { + if (member.type === 'way' && member.geometry) { + const wayCoords = member.geometry.map(pt => [pt.lon, pt.lat]); + coordinates.push(wayCoords); + } + }); + + // Assemble into MultiPolygon or Polygon (simplistic) + if (coordinates.length > 0) { + geojson.features.push({ + type: 'Feature', + properties: { + kecamatan: name, + ...el.tags + }, + geometry: { + type: 'Polygon', + coordinates: coordinates + } + }); + } + } +}); + +// Since OSM returns fragments of outer and inner rings in relations, a naive conversion might have topological issues in Leaflet (like crossing lines if not ordered). +// We should use an existing library like osmtogeojson. Let's install it and use it. diff --git a/webgis-spbu/php/create_jalan.php b/webgis-spbu/php/create_jalan.php new file mode 100644 index 0000000..a3ee5e2 --- /dev/null +++ b/webgis-spbu/php/create_jalan.php @@ -0,0 +1,49 @@ +false,"message"=>"Data kosong"]); + exit; +} + +try { + $geojson = json_encode($data['geometry'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + try { + $stmt = $pdo->prepare(" + INSERT INTO jalan_lines + (nama_jalan, status_jalan, panjang_meter, geojson) + VALUES (?, ?, ?, ?) + "); + + $stmt->execute([ + $data['nama_jalan'], + $data['status_jalan'], + $data['panjang_meter'], + $geojson + ]); + } catch (Exception $inner) { + // Fallback untuk skema spatial: simpan ke kolom `geom` jika `geojson` tidak ada. + $stmt = $pdo->prepare(" + INSERT INTO jalan_lines + (nama_jalan, status_jalan, panjang_meter, geom) + VALUES (?, ?, ?, ST_GeomFromGeoJSON(?)) + "); + + $stmt->execute([ + $data['nama_jalan'], + $data['status_jalan'], + $data['panjang_meter'], + $geojson + ]); + } + + echo json_encode(["success"=>true]); + +} catch (Exception $e) { + echo json_encode(["success"=>false,"message"=>$e->getMessage()]); +} +?> diff --git a/webgis-spbu/php/create_parsil.php b/webgis-spbu/php/create_parsil.php new file mode 100644 index 0000000..f140f50 --- /dev/null +++ b/webgis-spbu/php/create_parsil.php @@ -0,0 +1,65 @@ + false, "status" => "error", "message" => "Payload JSON kosong atau tidak valid."]); + exit; +} + +$status_kepemilikan = trim($data['status_kepemilikan'] ?? ''); +$luas_m2 = (float)($data['luas_m2'] ?? 0); +$geometry = $data['geometry'] ?? null; + +if ($status_kepemilikan === '' || !$geometry) { + echo json_encode(["success" => false, "status" => "error", "message" => "Data parsil belum lengkap (status/geometry)."]); + exit; +} + +try { + $geomJson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + try { + $stmt = $pdo->prepare(" + INSERT INTO parsil_tanah (status_kepemilikan, luas_m2, geojson) + VALUES (:status_kepemilikan, :luas_m2, :geojson) + "); + + $stmt->execute([ + ':status_kepemilikan' => $status_kepemilikan, + ':luas_m2' => $luas_m2, + ':geojson' => $geomJson + ]); + } catch (Exception $inner) { + // Fallback untuk skema spatial: simpan ke kolom `geom` jika `geojson` tidak ada. + try { + $stmt = $pdo->prepare(" + INSERT INTO parsil_tanah (status_kepemilikan, luas_m2, geom) + VALUES (:status_kepemilikan, :luas_m2, ST_GeomFromGeoJSON(:geom)) + "); + + $stmt->execute([ + ':status_kepemilikan' => $status_kepemilikan, + ':luas_m2' => $luas_m2, + ':geom' => $geomJson + ]); + } catch (Exception $inner2) { + // Last resort: minimal insert. + $stmt = $pdo->prepare(" + INSERT INTO parsil_tanah (geom) + VALUES (ST_GeomFromGeoJSON(:geom)) + "); + $stmt->execute([':geom' => $geomJson]); + } + } + + echo json_encode(["success" => true, "status" => "success", "message" => "Area Parsil berhasil disimpan."]); + +} catch (Exception $e) { + echo json_encode(["success" => false, "status" => "error", "message" => $e->getMessage()]); +} +?> diff --git a/webgis-spbu/php/create_pemukiman_miskin.php b/webgis-spbu/php/create_pemukiman_miskin.php new file mode 100644 index 0000000..92db675 --- /dev/null +++ b/webgis-spbu/php/create_pemukiman_miskin.php @@ -0,0 +1,192 @@ + $_POST['kk_nama'] ?? '', + 'nik' => $_POST['nik'] ?? '', + 'jumlah_anggota' => $_POST['jumlah_anggota'] ?? '', + 'latitude' => $_POST['latitude'] ?? null, + 'longitude' => $_POST['longitude'] ?? null, + 'address' => $_POST['address'] ?? '', + 'kelurahan' => $_POST['kelurahan'] ?? '', + 'kecamatan' => $_POST['kecamatan'] ?? '', + 'status_bantuan' => $_POST['status_bantuan'] ?? 'Belum dibantu', + 'jenis_bantuan' => $_POST['jenis_bantuan'] ?? '', + 'tanggal_bantuan' => $_POST['tanggal_bantuan'] ?? '', + 'anggota' => $_POST['anggota'] ?? '[]' + ]; + } + $json = json_decode(file_get_contents('php://input'), true); + return is_array($json) ? $json : null; +} + +function compute_nearest_ibadah(PDO $pdo, $lat, $lng) { + $stmt = $pdo->query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points"); + $rows = $stmt->fetchAll(); + $best = null; + foreach ($rows as $r) { + $rLat = (float)$r['latitude']; + $rLng = (float)$r['longitude']; + $radius = (float)$r['radius_meter']; + if ($radius <= 0) continue; + $d = haversine_m($lat, $lng, $rLat, $rLng); + if ($d <= $radius) { + if ($best === null || $d < $best['jarak_meter']) { + $best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d]; + } + } + } + return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null]; +} + +function save_upload_if_any() { + if (!isset($_FILES['bukti_file']) || !is_array($_FILES['bukti_file'])) return null; + if (($_FILES['bukti_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) return null; + + $dir = __DIR__ . '/../uploads/bukti'; + if (!is_dir($dir)) @mkdir($dir, 0775, true); + if (!is_dir($dir)) return null; + + $orig = (string)($_FILES['bukti_file']['name'] ?? 'file'); + $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); + $allowed = ['jpg','jpeg','png','webp','pdf']; + if ($ext && !in_array($ext, $allowed, true)) return null; + + $safeBase = preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($orig, PATHINFO_FILENAME)); + $safeBase = trim($safeBase, '_'); + if ($safeBase === '') $safeBase = 'bukti'; + $filename = $safeBase . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . ($ext ? ('.' . $ext) : ''); + $dest = $dir . '/' . $filename; + + if (!move_uploaded_file($_FILES['bukti_file']['tmp_name'], $dest)) return null; + return 'uploads/bukti/' . $filename; +} + +$data = get_payload(); +if (!$data) { + echo json_encode(["success" => false, "message" => "Payload kosong atau tidak valid."]); + exit; +} + +$kk_nama = trim($data['kk_nama'] ?? $data['nama'] ?? ''); +$nik = trim($data['nik'] ?? ''); +$jumlah_anggota = (int)($data['jumlah_anggota'] ?? 0); +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; +$address = trim($data['address'] ?? ''); +$kelurahan = trim($data['kelurahan'] ?? ''); +$kecamatan = trim($data['kecamatan'] ?? ''); +$status_bantuan = trim($data['status_bantuan'] ?? 'Belum dibantu'); +$jenis_bantuan = trim($data['jenis_bantuan'] ?? ''); +$tanggal_bantuan = trim($data['tanggal_bantuan'] ?? ''); +$anggotaRaw = $data['anggota'] ?? []; + +$validStatus = ['Belum dibantu', 'Sudah dibantu', 'Menunggu verifikasi']; +if (!in_array($status_bantuan, $validStatus, true)) $status_bantuan = 'Belum dibantu'; + +if ($kk_nama === '' || $latitude === null || $longitude === null) { + echo json_encode(["success" => false, "message" => "Data penduduk miskin belum lengkap (nama/lat/lng)."]); + exit; +} + +try { + $lat = (float)$latitude; + $lng = (float)$longitude; + $nearest = compute_nearest_ibadah($pdo, $lat, $lng); + $uploadPath = save_upload_if_any(); + + $pdo->beginTransaction(); + // Backward-compat: some installs still have old column `nama`. + // Prefer new schema if columns exist. + $cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasNew = in_array('kk_nama', $cols, true); + if ($hasNew) { + $stmt = $pdo->prepare(" + INSERT INTO pemukiman_miskin_points + (kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, + status_bantuan, jenis_bantuan, tanggal_bantuan, bukti_file, jarak_meter, rumah_ibadah_id) + VALUES + (:kk_nama, :nik, :jumlah_anggota, :latitude, :longitude, :address, :kelurahan, :kecamatan, + :status_bantuan, :jenis_bantuan, :tanggal_bantuan, :bukti_file, :jarak_meter, :rumah_ibadah_id) + "); + $stmt->execute([ + ':kk_nama' => $kk_nama, + ':nik' => ($nik === '' ? null : $nik), + ':jumlah_anggota' => $jumlah_anggota, + ':latitude' => $lat, + ':longitude' => $lng, + ':address' => ($address === '' ? null : $address), + ':kelurahan' => ($kelurahan === '' ? null : $kelurahan), + ':kecamatan' => ($kecamatan === '' ? null : $kecamatan), + ':status_bantuan' => $status_bantuan, + ':jenis_bantuan' => ($jenis_bantuan === '' ? null : $jenis_bantuan), + ':tanggal_bantuan' => ($tanggal_bantuan === '' ? null : $tanggal_bantuan), + ':bukti_file' => $uploadPath, + ':jarak_meter' => $nearest['jarak_meter'], + ':rumah_ibadah_id' => $nearest['rumah_ibadah_id'] + ]); + } else { + $stmt = $pdo->prepare(" + INSERT INTO pemukiman_miskin_points (nama, latitude, longitude, address, rumah_ibadah_id) + VALUES (:nama, :latitude, :longitude, :address, :rumah_ibadah_id) + "); + $stmt->execute([ + ':nama' => $kk_nama, + ':latitude' => $lat, + ':longitude' => $lng, + ':address' => ($address === '' ? null : $address), + ':rumah_ibadah_id' => $nearest['rumah_ibadah_id'] + ]); + } + + $newId = (int)$pdo->lastInsertId(); + + // anggota keluarga (optional) + $anggota = is_string($anggotaRaw) ? json_decode($anggotaRaw, true) : $anggotaRaw; + if (is_array($anggota) && count($anggota) > 0 && $hasNew) { + $ins = $pdo->prepare(" + INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan, keterangan) + VALUES (:penduduk_id, :nama, :hubungan, :umur, :pekerjaan, :keterangan) + "); + foreach ($anggota as $a) { + $n = trim((string)($a['nama'] ?? '')); + $h = trim((string)($a['hubungan'] ?? '')); + if ($n === '' || $h === '') continue; + $ins->execute([ + ':penduduk_id' => $newId, + ':nama' => $n, + ':hubungan' => $h, + ':umur' => isset($a['umur']) && $a['umur'] !== '' ? (int)$a['umur'] : null, + ':pekerjaan' => ($a['pekerjaan'] ?? '') === '' ? null : (string)$a['pekerjaan'], + ':keterangan' => ($a['keterangan'] ?? '') === '' ? null : (string)$a['keterangan'] + ]); + } + } + + $pdo->commit(); + echo json_encode([ + "success" => true, + "id" => $newId, + "rumah_ibadah_id" => $nearest['rumah_ibadah_id'], + "jarak_meter" => $nearest['jarak_meter'], + "message" => "Penduduk miskin berhasil ditambahkan" + ]); +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(["success" => false, "message" => $e->getMessage()]); +} diff --git a/webgis-spbu/php/create_point.php b/webgis-spbu/php/create_point.php new file mode 100644 index 0000000..1b160b8 --- /dev/null +++ b/webgis-spbu/php/create_point.php @@ -0,0 +1,124 @@ + false, "status" => "error", "message" => "Payload JSON kosong atau tidak valid."]); + exit; +} + +$nama = trim($data['nama'] ?? ''); +$no = trim($data['no'] ?? ''); +$deskripsi = trim($data['deskripsi'] ?? ''); +$status_24jam = isset($data['status_24jam']) ? (int)$data['status_24jam'] : 0; +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; + +if ($nama === '' || $no === '' || $latitude === null || $longitude === null) { + echo json_encode(["success" => false, "status" => "error", "message" => "Data point belum lengkap (nama/no/lat/lng)."]); + exit; +} + +$pointGeojson = json_encode([ + "type" => "Point", + "coordinates" => [(float)$longitude, (float)$latitude] +], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +// Optional legacy field if client ever sends it +$geomPayload = $data['geom'] ?? null; +if (!is_string($geomPayload) || $geomPayload === '') { + $geomPayload = $pointGeojson; +} + +try { + // Preferred schema (non-spatial, consistent with update_point.php) + try { + $stmt = $pdo->prepare(" + INSERT INTO spbu_points (nama, no, deskripsi, status_24jam, latitude, longitude) + VALUES (:nama, :no, :deskripsi, :status_24jam, :latitude, :longitude) + "); + + $stmt->execute([ + ':nama' => $nama, + ':no' => $no, + ':deskripsi' => $deskripsi, + ':status_24jam' => $status_24jam, + ':latitude' => $latitude, + ':longitude' => $longitude + ]); + + echo json_encode([ + "success" => true, + "status" => "success", + "message" => "Point SPBU berhasil ditambahkan", + "id" => (int)$pdo->lastInsertId() + ]); + exit; + } catch (Exception $e1) { + // continue to fallback attempts + } + + // Fallback schema: `geom` stored as LONGTEXT (GeoJSON string) + try { + $stmt = $pdo->prepare(" + INSERT INTO spbu_points (nama, no, deskripsi, status_24jam, latitude, longitude, geom) + VALUES (:nama, :no, :deskripsi, :status_24jam, :latitude, :longitude, :geom) + "); + + $stmt->execute([ + ':nama' => $nama, + ':no' => $no, + ':deskripsi' => $deskripsi, + ':status_24jam' => $status_24jam, + ':latitude' => $latitude, + ':longitude' => $longitude, + ':geom' => $geomPayload + ]); + + echo json_encode([ + "success" => true, + "status" => "success", + "message" => "Point SPBU berhasil ditambahkan", + "id" => (int)$pdo->lastInsertId() + ]); + exit; + } catch (Exception $e2) { + // continue to fallback attempts + } + + // Fallback schema: only (nama, geom) where geom is LONGTEXT GeoJSON + try { + $stmt = $pdo->prepare("INSERT INTO spbu_points (nama, geom) VALUES (:nama, :geom)"); + $stmt->execute([ + ':nama' => $nama, + ':geom' => $geomPayload + ]); + + echo json_encode([ + "success" => true, + "status" => "success", + "message" => "Point SPBU berhasil ditambahkan", + "id" => (int)$pdo->lastInsertId() + ]); + exit; + } catch (Exception $e3) { + // continue to fallback attempts + } + + // Fallback schema: `geom` is spatial GEOMETRY/POINT + $stmt = $pdo->prepare("INSERT INTO spbu_points (nama, geom) VALUES (:nama, ST_GeomFromGeoJSON(:geom))"); + $stmt->execute([ + ':nama' => $nama, + ':geom' => $pointGeojson + ]); + + echo json_encode([ + "success" => true, + "status" => "success", + "message" => "Point SPBU berhasil ditambahkan", + "id" => (int)$pdo->lastInsertId() + ]); +} catch (Exception $e) { + echo json_encode(["success" => false, "status" => "error", "message" => $e->getMessage()]); +} diff --git a/webgis-spbu/php/create_rumah_ibadah.php b/webgis-spbu/php/create_rumah_ibadah.php new file mode 100644 index 0000000..dccc4f5 --- /dev/null +++ b/webgis-spbu/php/create_rumah_ibadah.php @@ -0,0 +1,60 @@ + false, "message" => "Payload JSON kosong atau tidak valid."]); + exit; +} + +$nama = trim($data['nama'] ?? ''); +$jenis = trim($data['jenis'] ?? 'Lainnya'); +$kontak = trim($data['kontak'] ?? ''); +$radius_meter = (float)($data['radius_meter'] ?? 0); +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; +$address = trim($data['address'] ?? ''); + +if ($nama === '' || $radius_meter <= 0 || $latitude === null || $longitude === null) { + echo json_encode(["success" => false, "message" => "Data rumah ibadah belum lengkap (nama/radius/lat/lng)."]); + exit; +} + +try { + $cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasJenis = in_array('jenis', $cols, true); + $hasKontak = in_array('kontak', $cols, true); + + if ($hasJenis && $hasKontak) { + $stmt = $pdo->prepare(" + INSERT INTO rumah_ibadah_points (nama, jenis, kontak, radius_meter, latitude, longitude, address) + VALUES (:nama, :jenis, :kontak, :radius_meter, :latitude, :longitude, :address) + "); + $stmt->execute([ + ':nama' => $nama, + ':jenis' => ($jenis === '' ? 'Lainnya' : $jenis), + ':kontak' => ($kontak === '' ? null : $kontak), + ':radius_meter' => $radius_meter, + ':latitude' => $latitude, + ':longitude' => $longitude, + ':address' => ($address === '' ? null : $address) + ]); + } else { + $stmt = $pdo->prepare(" + INSERT INTO rumah_ibadah_points (nama, radius_meter, latitude, longitude, address) + VALUES (:nama, :radius_meter, :latitude, :longitude, :address) + "); + $stmt->execute([ + ':nama' => $nama, + ':radius_meter' => $radius_meter, + ':latitude' => $latitude, + ':longitude' => $longitude, + ':address' => ($address === '' ? null : $address) + ]); + } + + echo json_encode(["success" => true, "id" => (int)$pdo->lastInsertId(), "message" => "Rumah ibadah berhasil ditambahkan"]); +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => $e->getMessage()]); +} diff --git a/webgis-spbu/php/delete_jalan.php b/webgis-spbu/php/delete_jalan.php new file mode 100644 index 0000000..bf63df6 --- /dev/null +++ b/webgis-spbu/php/delete_jalan.php @@ -0,0 +1,16 @@ + false, "message" => "ID tidak valid"]); + exit; +} + +$stmt = $pdo->prepare("DELETE FROM jalan_lines WHERE id = ?"); +$stmt->execute([$id]); + +echo json_encode(["success" => true, "message" => "Data jalan berhasil dihapus"]); \ No newline at end of file diff --git a/webgis-spbu/php/delete_parsil.php b/webgis-spbu/php/delete_parsil.php new file mode 100644 index 0000000..d5b3e60 --- /dev/null +++ b/webgis-spbu/php/delete_parsil.php @@ -0,0 +1,16 @@ + false, "message" => "ID tidak valid"]); + exit; +} + +$stmt = $pdo->prepare("DELETE FROM parsil_tanah WHERE id = ?"); +$stmt->execute([$id]); + +echo json_encode(["success" => true, "message" => "Data parsil berhasil dihapus"]); \ No newline at end of file diff --git a/webgis-spbu/php/delete_pemukiman_miskin.php b/webgis-spbu/php/delete_pemukiman_miskin.php new file mode 100644 index 0000000..cc70ab5 --- /dev/null +++ b/webgis-spbu/php/delete_pemukiman_miskin.php @@ -0,0 +1,33 @@ + false, "message" => "ID tidak valid"]); + exit; +} + +try { + // best-effort delete uploaded proof file + try { + $cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + if (in_array('bukti_file', $cols, true)) { + $stmt = $pdo->prepare("SELECT bukti_file FROM pemukiman_miskin_points WHERE id = ?"); + $stmt->execute([$id]); + $path = $stmt->fetchColumn(); + if ($path && is_string($path) && strpos($path, 'uploads/bukti/') === 0) { + $full = __DIR__ . '/../' . $path; + if (is_file($full)) @unlink($full); + } + } + } catch (Exception $e) { /* ignore */ } + + $stmt = $pdo->prepare("DELETE FROM pemukiman_miskin_points WHERE id = ?"); + $stmt->execute([$id]); + echo json_encode(["success" => true, "message" => "Pemukiman miskin berhasil dihapus"]); +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => $e->getMessage()]); +} diff --git a/webgis-spbu/php/delete_point.php b/webgis-spbu/php/delete_point.php new file mode 100644 index 0000000..566c920 --- /dev/null +++ b/webgis-spbu/php/delete_point.php @@ -0,0 +1,16 @@ + false, "message" => "ID tidak valid"]); + exit; +} + +$stmt = $pdo->prepare("DELETE FROM spbu_points WHERE id = ?"); +$stmt->execute([$id]); + +echo json_encode(["success" => true, "message" => "Data point berhasil dihapus"]); \ No newline at end of file diff --git a/webgis-spbu/php/delete_rumah_ibadah.php b/webgis-spbu/php/delete_rumah_ibadah.php new file mode 100644 index 0000000..4508624 --- /dev/null +++ b/webgis-spbu/php/delete_rumah_ibadah.php @@ -0,0 +1,19 @@ + false, "message" => "ID tidak valid"]); + exit; +} + +try { + $stmt = $pdo->prepare("DELETE FROM rumah_ibadah_points WHERE id = ?"); + $stmt->execute([$id]); + echo json_encode(["success" => true, "message" => "Rumah ibadah berhasil dihapus"]); +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => $e->getMessage()]); +} diff --git a/webgis-spbu/php/koneksi.php b/webgis-spbu/php/koneksi.php new file mode 100644 index 0000000..cf301d1 --- /dev/null +++ b/webgis-spbu/php/koneksi.php @@ -0,0 +1,27 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; + +try { + $pdo = new PDO($dsn, $user, $pass, $options); +} catch (PDOException $e) { + http_response_code(500); + echo json_encode([ + "success" => false, + "message" => "Koneksi database gagal: " . $e->getMessage() + ]); + exit; +} +?> \ No newline at end of file diff --git a/webgis-spbu/php/read_anggota_keluarga.php b/webgis-spbu/php/read_anggota_keluarga.php new file mode 100644 index 0000000..40c538d --- /dev/null +++ b/webgis-spbu/php/read_anggota_keluarga.php @@ -0,0 +1,24 @@ +prepare(" + SELECT id, penduduk_id, nama, hubungan, umur, pekerjaan, keterangan + FROM anggota_keluarga + WHERE penduduk_id = :id + ORDER BY id ASC + "); + $stmt->execute([':id' => $penduduk_id]); + $rows = $stmt->fetchAll(); + echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +} catch (Exception $e) { + echo json_encode([]); +} + diff --git a/webgis-spbu/php/read_jalan.php b/webgis-spbu/php/read_jalan.php new file mode 100644 index 0000000..7e97bcc --- /dev/null +++ b/webgis-spbu/php/read_jalan.php @@ -0,0 +1,51 @@ +fetch()) { + $geometry = json_decode($row['geojson'] ?? '', true); + if (!$geometry) { + continue; + } + + $features[] = [ + "type" => "Feature", + "geometry" => $geometry, + "properties" => [ + "id" => (int)($row['id'] ?? 0), + "nama_jalan" => $row['nama_jalan'] ?? null, + "status_jalan" => $row['status_jalan'] ?? null, + "panjang_meter" => isset($row['panjang_meter']) ? (float)$row['panjang_meter'] : null + ] + ]; + } + return $features; +} + +try { + // Preferred schema: geojson column. + $stmt = $pdo->query("SELECT id, nama_jalan, status_jalan, panjang_meter, geojson FROM jalan_lines ORDER BY id ASC"); + $features = rowsToFeatures($stmt); +} catch (Exception $e) { + // Fallback schema: spatial geom. + $stmt = $pdo->query(" + SELECT + id, + nama_jalan, + status_jalan, + panjang_meter, + ST_AsGeoJSON(geom) AS geojson + FROM jalan_lines + ORDER BY id ASC + "); + $features = rowsToFeatures($stmt); +} + +echo json_encode([ + "type" => "FeatureCollection", + "features" => $features +], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/webgis-spbu/php/read_parsil.php b/webgis-spbu/php/read_parsil.php new file mode 100644 index 0000000..b9d0f7e --- /dev/null +++ b/webgis-spbu/php/read_parsil.php @@ -0,0 +1,49 @@ +fetch()) { + $geometry = json_decode($row['geojson'] ?? '', true); + if (!$geometry) { + continue; + } + + $features[] = [ + "type" => "Feature", + "geometry" => $geometry, + "properties" => [ + "id" => (int)($row['id'] ?? 0), + "status_kepemilikan" => $row['status_kepemilikan'] ?? null, + "luas_m2" => isset($row['luas_m2']) ? (float)$row['luas_m2'] : null + ] + ]; + } + return $features; +} + +try { + // Preferred schema: geojson column. + $stmt = $pdo->query("SELECT id, status_kepemilikan, luas_m2, geojson FROM parsil_tanah ORDER BY id ASC"); + $features = rowsToFeatures($stmt); +} catch (Exception $e) { + // Fallback schema: spatial geom. + $stmt = $pdo->query(" + SELECT + id, + status_kepemilikan, + luas_m2, + ST_AsGeoJSON(geom) AS geojson + FROM parsil_tanah + ORDER BY id ASC + "); + $features = rowsToFeatures($stmt); +} + +echo json_encode([ + "type" => "FeatureCollection", + "features" => $features +], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/webgis-spbu/php/read_pemukiman_miskin.php b/webgis-spbu/php/read_pemukiman_miskin.php new file mode 100644 index 0000000..bc5b48b --- /dev/null +++ b/webgis-spbu/php/read_pemukiman_miskin.php @@ -0,0 +1,127 @@ + (int)($r['id'] ?? 0), + 'jarak_meter' => $d, + 'row' => $r + ]; + } + } + } + return $best; +} + +// Prefer new schema; fall back to old schema if needed. +$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); +$hasNew = in_array('kk_nama', $cols, true); + +$hasJarak = in_array('jarak_meter', $cols, true); +$hasRumahIbadahId = in_array('rumah_ibadah_id', $cols, true); + +// Fetch rumah ibadah once; used for real-time coverage computation server-side. +$ibadah = []; +try { + $stmtI = $pdo->query("SELECT id, nama, jenis, kontak, radius_meter, latitude, longitude FROM rumah_ibadah_points"); + $ibadah = $stmtI->fetchAll(); +} catch (Exception $e) { + $ibadah = []; +} + +// Load pemukiman rows +$rows = []; +try { + if ($hasNew) { + $stmt = $pdo->query(" + SELECT p.*, + ( + SELECT COUNT(*) FROM anggota_keluarga a WHERE a.penduduk_id = p.id + ) AS anggota_count + FROM pemukiman_miskin_points p + ORDER BY p.id ASC + "); + $rows = $stmt->fetchAll(); + } else { + $stmt = $pdo->query("SELECT p.* FROM pemukiman_miskin_points p ORDER BY p.id ASC"); + $rows = $stmt->fetchAll(); + } +} catch (Exception $e) { + $rows = []; +} + +// Best-effort: keep stored rumah_ibadah_id/jarak_meter in sync with latest radius rules. +$upd = null; +try { + if ($hasRumahIbadahId && $hasJarak) { + $upd = $pdo->prepare("UPDATE pemukiman_miskin_points SET rumah_ibadah_id = :rid, jarak_meter = :jarak WHERE id = :id"); + } elseif ($hasRumahIbadahId) { + $upd = $pdo->prepare("UPDATE pemukiman_miskin_points SET rumah_ibadah_id = :rid WHERE id = :id"); + } +} catch (Exception $e) { + $upd = null; +} + +foreach ($rows as &$p) { + $lat = isset($p['latitude']) ? (float)$p['latitude'] : null; + $lng = isset($p['longitude']) ? (float)$p['longitude'] : null; + if ($lat === null || $lng === null) continue; + + $best = compute_nearest_ibadah($ibadah, $lat, $lng); + $rid = $best ? (int)$best['rumah_ibadah_id'] : null; + $jarak = $best ? (float)$best['jarak_meter'] : null; + $storedRid = isset($p['rumah_ibadah_id']) ? (int)$p['rumah_ibadah_id'] : null; + + // overwrite output with authoritative computed coverage + $p['rumah_ibadah_id'] = $rid; + if ($hasJarak) $p['jarak_meter'] = $jarak; + + if ($best && isset($best['row']) && is_array($best['row'])) { + $p['rumah_ibadah_nama'] = $best['row']['nama'] ?? null; + $p['rumah_ibadah_jenis'] = $best['row']['jenis'] ?? null; + $p['rumah_ibadah_kontak'] = $best['row']['kontak'] ?? null; + $p['rumah_ibadah_radius'] = $best['row']['radius_meter'] ?? null; + } else { + $p['rumah_ibadah_nama'] = null; + $p['rumah_ibadah_jenis'] = null; + $p['rumah_ibadah_kontak'] = null; + $p['rumah_ibadah_radius'] = null; + } + + // best-effort DB sync + try { + if ($upd && ((string)$storedRid !== (string)$rid)) { + if ($hasRumahIbadahId && $hasJarak) { + $upd->execute([':rid' => $rid, ':jarak' => $jarak, ':id' => (int)$p['id']]); + } elseif ($hasRumahIbadahId) { + $upd->execute([':rid' => $rid, ':id' => (int)$p['id']]); + } + } + } catch (Exception $e) { + // ignore + } +} +unset($p); + +echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/webgis-spbu/php/read_point.php b/webgis-spbu/php/read_point.php new file mode 100644 index 0000000..ef9bc05 --- /dev/null +++ b/webgis-spbu/php/read_point.php @@ -0,0 +1,96 @@ += 2) { + // GeoJSON: [lng, lat] + $row['longitude'] = $coords[0]; + $row['latitude'] = $coords[1]; + } + } + } + } + return $rows; +} + +try { + // Preferred schema: has latitude/longitude columns. + $stmt = $pdo->query(" + SELECT id, nama, no, deskripsi, status_24jam, latitude, longitude + FROM spbu_points + ORDER BY id ASC + "); + $rows = $stmt->fetchAll(); + echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} catch (Exception $e) { + // continue +} + +try { + // Fallback schema: latitude/longitude exists, geom stored as LONGTEXT GeoJSON. + $stmt = $pdo->query(" + SELECT id, nama, no, deskripsi, status_24jam, latitude, longitude, geom + FROM spbu_points + ORDER BY id ASC + "); + $rows = $stmt->fetchAll(); + $rows = normalizePointRows($rows); + echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} catch (Exception $e) { + // continue +} + +try { + // Fallback schema: only geom LONGTEXT; derive latitude/longitude from GeoJSON. + $stmt = $pdo->query(" + SELECT + id, + nama, + NULL AS no, + NULL AS deskripsi, + 0 AS status_24jam, + geom + FROM spbu_points + ORDER BY id ASC + "); + $rows = $stmt->fetchAll(); + $rows = normalizePointRows($rows); + echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} catch (Exception $e) { + // continue +} + +try { + // Fallback schema: spatial geom + $stmt = $pdo->query(" + SELECT + id, + nama, + NULL AS no, + NULL AS deskripsi, + 0 AS status_24jam, + ST_Y(geom) AS latitude, + ST_X(geom) AS longitude + FROM spbu_points + ORDER BY id ASC + "); + $rows = $stmt->fetchAll(); + echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} catch (Exception $e) { + // continue +} + +// Last resort +$stmt = $pdo->query("SELECT * FROM spbu_points ORDER BY id ASC"); +$rows = $stmt->fetchAll(); +echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/webgis-spbu/php/read_rumah_ibadah.php b/webgis-spbu/php/read_rumah_ibadah.php new file mode 100644 index 0000000..e11c3af --- /dev/null +++ b/webgis-spbu/php/read_rumah_ibadah.php @@ -0,0 +1,17 @@ +query(" + SELECT r.*, + ( + SELECT COUNT(*) + FROM pemukiman_miskin_points p + WHERE p.rumah_ibadah_id = r.id + ) AS binaan_count + FROM rumah_ibadah_points r + ORDER BY r.id ASC +"); +$rows = $stmt->fetchAll(); +echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/webgis-spbu/php/reverse_geocode.php b/webgis-spbu/php/reverse_geocode.php new file mode 100644 index 0000000..bcebbe5 --- /dev/null +++ b/webgis-spbu/php/reverse_geocode.php @@ -0,0 +1,42 @@ + false, "message" => "lat/lng wajib diisi"]); + exit; +} + +// Nominatim usage policy requires a valid User-Agent identifying the application. +$url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=" . urlencode($lat) . "&lon=" . urlencode($lng); + +try { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 8); + curl_setopt($ch, CURLOPT_TIMEOUT, 12); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "User-Agent: webgis-spbu/1.0 (XAMPP PHP reverse geocode)" + ]); + $resp = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $err = curl_error($ch); + curl_close($ch); + + if ($resp === false || $code < 200 || $code >= 300) { + echo json_encode(["success" => false, "message" => $err ?: ("HTTP " . $code)]); + exit; + } + + $json = json_decode($resp, true); + $display = $json['display_name'] ?? null; + echo json_encode([ + "success" => true, + "address" => $display, + "raw" => $json + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => $e->getMessage()]); +} diff --git a/webgis-spbu/php/update_jalan.php b/webgis-spbu/php/update_jalan.php new file mode 100644 index 0000000..5448f51 --- /dev/null +++ b/webgis-spbu/php/update_jalan.php @@ -0,0 +1,63 @@ + false, "message" => "Data kosong"]); + exit; +} + +$id = (int)($data['id'] ?? 0); +$nama_jalan = trim($data['nama_jalan'] ?? ''); +$status_jalan = trim($data['status_jalan'] ?? ''); +$panjang_meter = (float)($data['panjang_meter'] ?? 0); +$geometry = $data['geometry'] ?? null; + +if ($id <= 0 || $nama_jalan === '' || $status_jalan === '' || !$geometry) { + echo json_encode(["success" => false, "message" => "Data update jalan belum lengkap"]); + exit; +} + +$sql = "UPDATE jalan_lines + SET nama_jalan = :nama_jalan, + status_jalan = :status_jalan, + panjang_meter = :panjang_meter, + geojson = :geojson + WHERE id = :id"; + +$geojson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +try { + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':nama_jalan' => $nama_jalan, + ':status_jalan' => $status_jalan, + ':panjang_meter' => $panjang_meter, + ':geojson' => $geojson, + ':id' => $id + ]); + + echo json_encode(["success" => true, "message" => "Data jalan berhasil diupdate"]); + exit; +} catch (Exception $inner) { + // fallback to spatial geom schema +} + +$stmt = $pdo->prepare("UPDATE jalan_lines + SET nama_jalan = :nama_jalan, + status_jalan = :status_jalan, + panjang_meter = :panjang_meter, + geom = ST_GeomFromGeoJSON(:geojson) + WHERE id = :id"); + +$stmt->execute([ + ':nama_jalan' => $nama_jalan, + ':status_jalan' => $status_jalan, + ':panjang_meter' => $panjang_meter, + ':geojson' => $geojson, + ':id' => $id +]); + +echo json_encode(["success" => true, "message" => "Data jalan berhasil diupdate"]); diff --git a/webgis-spbu/php/update_parsil.php b/webgis-spbu/php/update_parsil.php new file mode 100644 index 0000000..0609153 --- /dev/null +++ b/webgis-spbu/php/update_parsil.php @@ -0,0 +1,58 @@ + false, "message" => "Data kosong"]); + exit; +} + +$id = (int)($data['id'] ?? 0); +$status_kepemilikan = trim($data['status_kepemilikan'] ?? ''); +$luas_m2 = (float)($data['luas_m2'] ?? 0); +$geometry = $data['geometry'] ?? null; + +if ($id <= 0 || $status_kepemilikan === '' || !$geometry) { + echo json_encode(["success" => false, "message" => "Data update parsil belum lengkap"]); + exit; +} + +$sql = "UPDATE parsil_tanah + SET status_kepemilikan = :status_kepemilikan, + luas_m2 = :luas_m2, + geojson = :geojson + WHERE id = :id"; + +$geojson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +try { + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':status_kepemilikan' => $status_kepemilikan, + ':luas_m2' => $luas_m2, + ':geojson' => $geojson, + ':id' => $id + ]); + + echo json_encode(["success" => true, "message" => "Data parsil berhasil diupdate"]); + exit; +} catch (Exception $inner) { + // fallback to spatial geom schema +} + +$stmt = $pdo->prepare("UPDATE parsil_tanah + SET status_kepemilikan = :status_kepemilikan, + luas_m2 = :luas_m2, + geom = ST_GeomFromGeoJSON(:geojson) + WHERE id = :id"); + +$stmt->execute([ + ':status_kepemilikan' => $status_kepemilikan, + ':luas_m2' => $luas_m2, + ':geojson' => $geojson, + ':id' => $id +]); + +echo json_encode(["success" => true, "message" => "Data parsil berhasil diupdate"]); diff --git a/webgis-spbu/php/update_pemukiman_miskin.php b/webgis-spbu/php/update_pemukiman_miskin.php new file mode 100644 index 0000000..46e4ad8 --- /dev/null +++ b/webgis-spbu/php/update_pemukiman_miskin.php @@ -0,0 +1,213 @@ + $_POST['id'] ?? 0, + 'kk_nama' => $_POST['kk_nama'] ?? '', + 'nik' => $_POST['nik'] ?? '', + 'jumlah_anggota' => $_POST['jumlah_anggota'] ?? '', + 'latitude' => $_POST['latitude'] ?? null, + 'longitude' => $_POST['longitude'] ?? null, + 'address' => $_POST['address'] ?? '', + 'kelurahan' => $_POST['kelurahan'] ?? '', + 'kecamatan' => $_POST['kecamatan'] ?? '', + 'status_bantuan' => $_POST['status_bantuan'] ?? 'Belum dibantu', + 'jenis_bantuan' => $_POST['jenis_bantuan'] ?? '', + 'tanggal_bantuan' => $_POST['tanggal_bantuan'] ?? '', + 'anggota' => $_POST['anggota'] ?? '[]' + ]; + } + $json = json_decode(file_get_contents('php://input'), true); + return is_array($json) ? $json : null; +} + +function compute_nearest_ibadah(PDO $pdo, $lat, $lng) { + $stmt = $pdo->query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points"); + $rows = $stmt->fetchAll(); + $best = null; + foreach ($rows as $r) { + $rLat = (float)$r['latitude']; + $rLng = (float)$r['longitude']; + $radius = (float)$r['radius_meter']; + if ($radius <= 0) continue; + $d = haversine_m($lat, $lng, $rLat, $rLng); + if ($d <= $radius) { + if ($best === null || $d < $best['jarak_meter']) { + $best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d]; + } + } + } + return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null]; +} + +function save_upload_if_any() { + if (!isset($_FILES['bukti_file']) || !is_array($_FILES['bukti_file'])) return null; + if (($_FILES['bukti_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) return null; + $dir = __DIR__ . '/../uploads/bukti'; + if (!is_dir($dir)) @mkdir($dir, 0775, true); + if (!is_dir($dir)) return null; + + $orig = (string)($_FILES['bukti_file']['name'] ?? 'file'); + $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); + $allowed = ['jpg','jpeg','png','webp','pdf']; + if ($ext && !in_array($ext, $allowed, true)) return null; + + $safeBase = preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($orig, PATHINFO_FILENAME)); + $safeBase = trim($safeBase, '_'); + if ($safeBase === '') $safeBase = 'bukti'; + $filename = $safeBase . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . ($ext ? ('.' . $ext) : ''); + $dest = $dir . '/' . $filename; + if (!move_uploaded_file($_FILES['bukti_file']['tmp_name'], $dest)) return null; + return 'uploads/bukti/' . $filename; +} + +$data = get_payload(); +if (!$data) { + echo json_encode(["success" => false, "message" => "Payload kosong atau tidak valid."]); + exit; +} + +$id = (int)($data['id'] ?? 0); +$kk_nama = trim($data['kk_nama'] ?? $data['nama'] ?? ''); +$nik = trim($data['nik'] ?? ''); +$jumlah_anggota = (int)($data['jumlah_anggota'] ?? 0); +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; +$address = trim($data['address'] ?? ''); +$kelurahan = trim($data['kelurahan'] ?? ''); +$kecamatan = trim($data['kecamatan'] ?? ''); +$status_bantuan = trim($data['status_bantuan'] ?? 'Belum dibantu'); +$jenis_bantuan = trim($data['jenis_bantuan'] ?? ''); +$tanggal_bantuan = trim($data['tanggal_bantuan'] ?? ''); +$anggotaRaw = $data['anggota'] ?? []; + +$validStatus = ['Belum dibantu', 'Sudah dibantu', 'Menunggu verifikasi']; +if (!in_array($status_bantuan, $validStatus, true)) $status_bantuan = 'Belum dibantu'; + +if ($id <= 0 || $kk_nama === '' || $latitude === null || $longitude === null) { + echo json_encode(["success" => false, "message" => "Data update penduduk miskin belum lengkap."]); + exit; +} + +try { + $lat = (float)$latitude; + $lng = (float)$longitude; + $nearest = compute_nearest_ibadah($pdo, $lat, $lng); + $uploadPath = save_upload_if_any(); + + $cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasNew = in_array('kk_nama', $cols, true); + + $pdo->beginTransaction(); + + if ($hasNew) { + if ($uploadPath === null) { + $stmt = $pdo->prepare("SELECT bukti_file FROM pemukiman_miskin_points WHERE id = :id"); + $stmt->execute([':id' => $id]); + $uploadPath = $stmt->fetchColumn() ?: null; + } + + $stmt = $pdo->prepare(" + UPDATE pemukiman_miskin_points + SET kk_nama = :kk_nama, + nik = :nik, + jumlah_anggota = :jumlah_anggota, + latitude = :latitude, + longitude = :longitude, + address = :address, + kelurahan = :kelurahan, + kecamatan = :kecamatan, + status_bantuan = :status_bantuan, + jenis_bantuan = :jenis_bantuan, + tanggal_bantuan = :tanggal_bantuan, + bukti_file = :bukti_file, + jarak_meter = :jarak_meter, + rumah_ibadah_id = :rumah_ibadah_id + WHERE id = :id + "); + $stmt->execute([ + ':kk_nama' => $kk_nama, + ':nik' => ($nik === '' ? null : $nik), + ':jumlah_anggota' => $jumlah_anggota, + ':latitude' => $lat, + ':longitude' => $lng, + ':address' => ($address === '' ? null : $address), + ':kelurahan' => ($kelurahan === '' ? null : $kelurahan), + ':kecamatan' => ($kecamatan === '' ? null : $kecamatan), + ':status_bantuan' => $status_bantuan, + ':jenis_bantuan' => ($jenis_bantuan === '' ? null : $jenis_bantuan), + ':tanggal_bantuan' => ($tanggal_bantuan === '' ? null : $tanggal_bantuan), + ':bukti_file' => $uploadPath, + ':jarak_meter' => $nearest['jarak_meter'], + ':rumah_ibadah_id' => $nearest['rumah_ibadah_id'], + ':id' => $id + ]); + + // anggota keluarga (replace) + $pdo->prepare("DELETE FROM anggota_keluarga WHERE penduduk_id = :id")->execute([':id' => $id]); + $anggota = is_string($anggotaRaw) ? json_decode($anggotaRaw, true) : $anggotaRaw; + if (is_array($anggota) && count($anggota) > 0) { + $ins = $pdo->prepare(" + INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan, keterangan) + VALUES (:penduduk_id, :nama, :hubungan, :umur, :pekerjaan, :keterangan) + "); + foreach ($anggota as $a) { + $n = trim((string)($a['nama'] ?? '')); + $h = trim((string)($a['hubungan'] ?? '')); + if ($n === '' || $h === '') continue; + $ins->execute([ + ':penduduk_id' => $id, + ':nama' => $n, + ':hubungan' => $h, + ':umur' => isset($a['umur']) && $a['umur'] !== '' ? (int)$a['umur'] : null, + ':pekerjaan' => ($a['pekerjaan'] ?? '') === '' ? null : (string)$a['pekerjaan'], + ':keterangan' => ($a['keterangan'] ?? '') === '' ? null : (string)$a['keterangan'] + ]); + } + } + } else { + $stmt = $pdo->prepare(" + UPDATE pemukiman_miskin_points + SET nama = :nama, + latitude = :latitude, + longitude = :longitude, + address = :address, + rumah_ibadah_id = :rumah_ibadah_id + WHERE id = :id + "); + $stmt->execute([ + ':nama' => $kk_nama, + ':latitude' => $lat, + ':longitude' => $lng, + ':address' => ($address === '' ? null : $address), + ':rumah_ibadah_id' => $nearest['rumah_ibadah_id'], + ':id' => $id + ]); + } + + $pdo->commit(); + echo json_encode([ + "success" => true, + "rumah_ibadah_id" => $nearest['rumah_ibadah_id'], + "jarak_meter" => $nearest['jarak_meter'], + "message" => "Penduduk miskin berhasil diupdate" + ]); +} catch (Exception $e) { + if ($pdo->inTransaction()) $pdo->rollBack(); + echo json_encode(["success" => false, "message" => $e->getMessage()]); +} diff --git a/webgis-spbu/php/update_pemukiman_miskin_location.php b/webgis-spbu/php/update_pemukiman_miskin_location.php new file mode 100644 index 0000000..8a53362 --- /dev/null +++ b/webgis-spbu/php/update_pemukiman_miskin_location.php @@ -0,0 +1,95 @@ +query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points"); + $rows = $stmt->fetchAll(); + $best = null; + foreach ($rows as $r) { + $rLat = (float)$r['latitude']; + $rLng = (float)$r['longitude']; + $radius = (float)$r['radius_meter']; + if ($radius <= 0) continue; + $d = haversine_m($lat, $lng, $rLat, $rLng); + if ($d <= $radius) { + if ($best === null || $d < $best['jarak_meter']) { + $best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d]; + } + } + } + return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null]; +} + +$data = json_decode(file_get_contents('php://input'), true); +if (!is_array($data)) { + echo json_encode(["success" => false, "message" => "Payload JSON kosong atau tidak valid."]); + exit; +} + +$id = (int)($data['id'] ?? 0); +$lat = isset($data['latitude']) ? (float)$data['latitude'] : null; +$lng = isset($data['longitude']) ? (float)$data['longitude'] : null; +if ($id <= 0 || $lat === null || $lng === null) { + echo json_encode(["success" => false, "message" => "Data update lokasi belum lengkap (id/lat/lng)."]); + exit; +} + +try { + $nearest = compute_nearest_ibadah($pdo, $lat, $lng); + $cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasNew = in_array('kk_nama', $cols, true); + + if ($hasNew) { + $stmt = $pdo->prepare(" + UPDATE pemukiman_miskin_points + SET latitude = :lat, + longitude = :lng, + jarak_meter = :jarak_meter, + rumah_ibadah_id = :rumah_ibadah_id + WHERE id = :id + "); + $stmt->execute([ + ':lat' => $lat, + ':lng' => $lng, + ':jarak_meter' => $nearest['jarak_meter'], + ':rumah_ibadah_id' => $nearest['rumah_ibadah_id'], + ':id' => $id + ]); + } else { + $stmt = $pdo->prepare(" + UPDATE pemukiman_miskin_points + SET latitude = :lat, + longitude = :lng, + rumah_ibadah_id = :rumah_ibadah_id + WHERE id = :id + "); + $stmt->execute([ + ':lat' => $lat, + ':lng' => $lng, + ':rumah_ibadah_id' => $nearest['rumah_ibadah_id'], + ':id' => $id + ]); + } + + echo json_encode([ + "success" => true, + "rumah_ibadah_id" => $nearest['rumah_ibadah_id'], + "jarak_meter" => $nearest['jarak_meter'], + "message" => "Lokasi penduduk miskin berhasil diupdate" + ]); +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => $e->getMessage()]); +} + diff --git a/webgis-spbu/php/update_point.php b/webgis-spbu/php/update_point.php new file mode 100644 index 0000000..018ac70 --- /dev/null +++ b/webgis-spbu/php/update_point.php @@ -0,0 +1,115 @@ + false, "message" => "Data kosong"]); + exit; +} + +$id = (int)($data['id'] ?? 0); +$nama = trim($data['nama'] ?? ''); +$no = trim($data['no'] ?? ''); +$deskripsi = trim($data['deskripsi'] ?? ''); +$status_24jam = isset($data['status_24jam']) ? (int)$data['status_24jam'] : 0; +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; + +if ($id <= 0 || $nama === '' || $no === '' || $latitude === null || $longitude === null) { + echo json_encode(["success" => false, "message" => "Data update point belum lengkap"]); + exit; +} + +$sql = "UPDATE spbu_points + SET nama = :nama, + no = :no, + deskripsi = :deskripsi, + status_24jam = :status_24jam, + latitude = :latitude, + longitude = :longitude + WHERE id = :id"; + +$pointGeojson = json_encode([ + "type" => "Point", + "coordinates" => [(float)$longitude, (float)$latitude] +], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +try { + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':nama' => $nama, + ':no' => $no, + ':deskripsi' => $deskripsi, + ':status_24jam' => $status_24jam, + ':latitude' => $latitude, + ':longitude' => $longitude, + ':id' => $id + ]); + + echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]); + exit; +} catch (Exception $inner) { + // continue fallback +} + +// Fallback schema: geom LONGTEXT +try { + $stmt = $pdo->prepare("UPDATE spbu_points + SET nama = :nama, + no = :no, + deskripsi = :deskripsi, + status_24jam = :status_24jam, + latitude = :latitude, + longitude = :longitude, + geom = :geom + WHERE id = :id"); + + $stmt->execute([ + ':nama' => $nama, + ':no' => $no, + ':deskripsi' => $deskripsi, + ':status_24jam' => $status_24jam, + ':latitude' => $latitude, + ':longitude' => $longitude, + ':geom' => $pointGeojson, + ':id' => $id + ]); + + echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]); + exit; +} catch (Exception $inner2) { + // continue fallback +} + +// Fallback schema: only (nama, geom) where geom is LONGTEXT GeoJSON +try { + $stmt = $pdo->prepare("UPDATE spbu_points + SET nama = :nama, + geom = :geom + WHERE id = :id"); + $stmt->execute([ + ':nama' => $nama, + ':geom' => $pointGeojson, + ':id' => $id + ]); + + echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]); + exit; +} catch (Exception $inner3) { + // continue fallback +} + +// Fallback schema: spatial geom +$stmt = $pdo->prepare("UPDATE spbu_points + SET nama = :nama, + geom = ST_GeomFromGeoJSON(:geom) + WHERE id = :id"); +$stmt->execute([ + ':nama' => $nama, + ':geom' => $pointGeojson, + ':id' => $id +]); + +echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]); diff --git a/webgis-spbu/php/update_rumah_ibadah.php b/webgis-spbu/php/update_rumah_ibadah.php new file mode 100644 index 0000000..e9c91a3 --- /dev/null +++ b/webgis-spbu/php/update_rumah_ibadah.php @@ -0,0 +1,75 @@ + false, "message" => "Payload JSON kosong atau tidak valid."]); + exit; +} + +$id = (int)($data['id'] ?? 0); +$nama = trim($data['nama'] ?? ''); +$jenis = trim($data['jenis'] ?? 'Lainnya'); +$kontak = trim($data['kontak'] ?? ''); +$radius_meter = (float)($data['radius_meter'] ?? 0); +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; +$address = trim($data['address'] ?? ''); + +if ($id <= 0 || $nama === '' || $radius_meter <= 0 || $latitude === null || $longitude === null) { + echo json_encode(["success" => false, "message" => "Data update rumah ibadah belum lengkap."]); + exit; +} + +try { + $cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0); + $hasJenis = in_array('jenis', $cols, true); + $hasKontak = in_array('kontak', $cols, true); + + if ($hasJenis && $hasKontak) { + $stmt = $pdo->prepare(" + UPDATE rumah_ibadah_points + SET nama = :nama, + jenis = :jenis, + kontak = :kontak, + radius_meter = :radius_meter, + latitude = :latitude, + longitude = :longitude, + address = :address + WHERE id = :id + "); + $stmt->execute([ + ':nama' => $nama, + ':jenis' => ($jenis === '' ? 'Lainnya' : $jenis), + ':kontak' => ($kontak === '' ? null : $kontak), + ':radius_meter' => $radius_meter, + ':latitude' => $latitude, + ':longitude' => $longitude, + ':address' => ($address === '' ? null : $address), + ':id' => $id + ]); + } else { + $stmt = $pdo->prepare(" + UPDATE rumah_ibadah_points + SET nama = :nama, + radius_meter = :radius_meter, + latitude = :latitude, + longitude = :longitude, + address = :address + WHERE id = :id + "); + $stmt->execute([ + ':nama' => $nama, + ':radius_meter' => $radius_meter, + ':latitude' => $latitude, + ':longitude' => $longitude, + ':address' => ($address === '' ? null : $address), + ':id' => $id + ]); + } + + echo json_encode(["success" => true, "message" => "Rumah ibadah berhasil diupdate"]); +} catch (Exception $e) { + echo json_encode(["success" => false, "message" => $e->getMessage()]); +} diff --git a/webgis-spbu/sql/schema_webgis_spbu.sql b/webgis-spbu/sql/schema_webgis_spbu.sql new file mode 100644 index 0000000..28f9b1b --- /dev/null +++ b/webgis-spbu/sql/schema_webgis_spbu.sql @@ -0,0 +1,466 @@ +-- Schema WebGIS SPBU + Jalan + Parsil (MySQL / MariaDB) +-- Catatan: +-- - Data LINE/POLYGON disimpan sebagai GeoJSON geometry (tipe: geojson) di kolom LONGTEXT `geojson`. +-- - Panjang (meter) dan luas (m2) dihitung otomatis di LeafletJS, bukan input manual. + +CREATE TABLE IF NOT EXISTS spbu_points ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL, + no VARCHAR(60) NOT NULL, + deskripsi TEXT NULL, + status_24jam TINYINT(1) NOT NULL DEFAULT 0, + latitude DECIMAL(10,7) NOT NULL, + longitude DECIMAL(10,7) NOT NULL, + -- opsional (kompatibilitas): simpan GeoJSON Point sebagai string + geom LONGTEXT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_spbu_status24 (status_24jam), + INDEX idx_spbu_latlng (latitude, longitude) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS jalan_lines ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_jalan VARCHAR(200) NOT NULL, + status_jalan VARCHAR(20) NOT NULL, -- Nasional | Provinsi | Kabupaten + panjang_meter DOUBLE NOT NULL DEFAULT 0, + geojson LONGTEXT NOT NULL, -- GeoJSON geometry: { "type":"LineString", "coordinates":[...] } + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_jalan_status (status_jalan) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS parsil_tanah ( + id INT AUTO_INCREMENT PRIMARY KEY, + status_kepemilikan VARCHAR(10) NOT NULL, -- SHM | HGB | HGU | HP + luas_m2 DOUBLE NOT NULL DEFAULT 0, + geojson LONGTEXT NOT NULL, -- GeoJSON geometry: { "type":"Polygon", "coordinates":[...] } + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_parsil_status (status_kepemilikan) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS rumah_ibadah_points ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(200) NOT NULL, + jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya', -- Masjid|Musholla|Gereja|Katolik|Pura|Vihara|Klenteng|Lainnya + kontak VARCHAR(120) NULL, + radius_meter DOUBLE NOT NULL DEFAULT 300, + latitude DECIMAL(10,7) NOT NULL, + longitude DECIMAL(10,7) NOT NULL, + address TEXT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_ibadah_jenis (jenis), + INDEX idx_ibadah_latlng (latitude, longitude) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pemukiman_miskin_points ( + id INT AUTO_INCREMENT PRIMARY KEY, + kk_nama VARCHAR(200) NOT NULL, + nik VARCHAR(32) NULL, + jumlah_anggota INT NOT NULL DEFAULT 0, + latitude DECIMAL(10,7) NOT NULL, + longitude DECIMAL(10,7) NOT NULL, + address TEXT NULL, + kelurahan VARCHAR(120) NULL, + kecamatan VARCHAR(120) NULL, + status_bantuan VARCHAR(24) NOT NULL DEFAULT 'Belum dibantu', -- Belum dibantu|Sudah dibantu|Menunggu verifikasi + jenis_bantuan VARCHAR(120) NULL, + tanggal_bantuan DATE NULL, + bukti_file VARCHAR(255) NULL, + jarak_meter DOUBLE NULL, + -- id rumah ibadah yang menaungi (hasil pencarian radius terdekat) + rumah_ibadah_id INT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_miskin_latlng (latitude, longitude), + INDEX idx_miskin_ibadah (rumah_ibadah_id), + INDEX idx_miskin_status (status_bantuan), + CONSTRAINT fk_miskin_ibadah FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah_points(id) + ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS anggota_keluarga ( + id INT AUTO_INCREMENT PRIMARY KEY, + penduduk_id INT NOT NULL, + nama VARCHAR(200) NOT NULL, + hubungan VARCHAR(60) NOT NULL, + umur INT NULL, + pekerjaan VARCHAR(120) NULL, + keterangan VARCHAR(255) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_anggota_penduduk (penduduk_id), + CONSTRAINT fk_anggota_penduduk FOREIGN KEY (penduduk_id) REFERENCES pemukiman_miskin_points(id) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- (Opsional) User login untuk admin/operator. +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + username VARCHAR(60) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'operator', -- admin|operator + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Jika ingin strict value, bisa pakai ENUM, misal: +-- status_jalan ENUM('Nasional','Provinsi','Kabupaten') +-- status_kepemilikan ENUM('SHM','HGB','HGU','HP') + + +-- ========================================================== +-- MIGRATION (ALTER) UNTUK DATABASE LAMA (TANPA HILANGKAN DATA) +-- ========================================================== +-- Kapan perlu menjalankan bagian ini? +-- - Jika database Anda dibuat dari project lama (kolom/tipe berbeda), +-- misalnya masih pakai kolom spatial `geom` atau belum punya kolom +-- `latitude/longitude/status_24jam/geojson`. +-- - Jika database Anda sudah dibuat pakai schema di atas (CREATE TABLE), +-- Anda biasanya TIDAK perlu ALTER apa-apa. +-- +-- Cara pakai: +-- - Jalankan file ini di phpMyAdmin / MySQL client. +-- - Bagian CREATE TABLE aman (IF NOT EXISTS). +-- - Bagian MIGRATION ini aman (cek INFORMATION_SCHEMA sebelum ALTER). + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS migrate_webgis_spbu $$ +CREATE PROCEDURE migrate_webgis_spbu() +BEGIN + -- ---------------------- + -- spbu_points + -- ---------------------- + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' + ) THEN + + -- Kolom atribut inti + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'no' + ) THEN + ALTER TABLE spbu_points ADD COLUMN no VARCHAR(60) NULL AFTER nama; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'deskripsi' + ) THEN + ALTER TABLE spbu_points ADD COLUMN deskripsi TEXT NULL AFTER no; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'status_24jam' + ) THEN + ALTER TABLE spbu_points ADD COLUMN status_24jam TINYINT(1) NOT NULL DEFAULT 0 AFTER deskripsi; + END IF; + + -- Lat/Lng: untuk menjaga data lama, dibuat NULL-able dulu. + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'latitude' + ) THEN + ALTER TABLE spbu_points ADD COLUMN latitude DECIMAL(10,7) NULL AFTER status_24jam; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'longitude' + ) THEN + ALTER TABLE spbu_points ADD COLUMN longitude DECIMAL(10,7) NULL AFTER latitude; + END IF; + + -- Kolom geom (opsional) untuk kompatibilitas legacy + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'geom' + ) THEN + ALTER TABLE spbu_points ADD COLUMN geom LONGTEXT NULL AFTER longitude; + END IF; + + -- Index (jika belum ada) + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND index_name = 'idx_spbu_status24' + ) THEN + ALTER TABLE spbu_points ADD INDEX idx_spbu_status24 (status_24jam); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND index_name = 'idx_spbu_latlng' + ) THEN + ALTER TABLE spbu_points ADD INDEX idx_spbu_latlng (latitude, longitude); + END IF; + + -- Backfill latitude/longitude dari geom GeoJSON (jika geom berisi JSON valid) + -- Catatan: ini hanya mengisi yang masih NULL supaya tidak menimpa data baru. + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'geom' + ) THEN + -- Optional: JSON_VALID/JSON_EXTRACT bisa tidak tersedia pada versi tertentu. + -- Best-effort: jika fungsi tidak didukung, blok ini di-skip tanpa menghentikan migrasi. + BEGIN + DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END; + UPDATE spbu_points + SET + longitude = CAST(JSON_UNQUOTE(JSON_EXTRACT(geom, '$.coordinates[0]')) AS DECIMAL(10,7)), + latitude = CAST(JSON_UNQUOTE(JSON_EXTRACT(geom, '$.coordinates[1]')) AS DECIMAL(10,7)) + WHERE (latitude IS NULL OR longitude IS NULL) + AND geom IS NOT NULL + AND JSON_VALID(geom) = 1 + AND JSON_UNQUOTE(JSON_EXTRACT(geom, '$.type')) = 'Point'; + END; + END IF; + END IF; + + -- ---------------------- + -- jalan_lines + -- ---------------------- + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' + ) THEN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'panjang_meter' + ) THEN + ALTER TABLE jalan_lines ADD COLUMN panjang_meter DOUBLE NOT NULL DEFAULT 0 AFTER status_jalan; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'geojson' + ) THEN + ALTER TABLE jalan_lines ADD COLUMN geojson LONGTEXT NULL AFTER panjang_meter; + END IF; + + -- Backfill geojson dari kolom spatial geom (jika ada) + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'geom' + ) THEN + BEGIN + DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END; + UPDATE jalan_lines + SET geojson = ST_AsGeoJSON(geom) + WHERE (geojson IS NULL OR geojson = '') AND geom IS NOT NULL; + END; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND index_name = 'idx_jalan_status' + ) THEN + ALTER TABLE jalan_lines ADD INDEX idx_jalan_status (status_jalan); + END IF; + END IF; + + -- ---------------------- + -- parsil_tanah + -- ---------------------- + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' + ) THEN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'luas_m2' + ) THEN + ALTER TABLE parsil_tanah ADD COLUMN luas_m2 DOUBLE NOT NULL DEFAULT 0 AFTER status_kepemilikan; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'geojson' + ) THEN + ALTER TABLE parsil_tanah ADD COLUMN geojson LONGTEXT NULL AFTER luas_m2; + END IF; + + -- Backfill geojson dari kolom spatial geom (jika ada) + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'geom' + ) THEN + BEGIN + DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END; + UPDATE parsil_tanah + SET geojson = ST_AsGeoJSON(geom) + WHERE (geojson IS NULL OR geojson = '') AND geom IS NOT NULL; + END; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND index_name = 'idx_parsil_status' + ) THEN + ALTER TABLE parsil_tanah ADD INDEX idx_parsil_status (status_kepemilikan); + END IF; + END IF; + + -- ---------------------- + -- rumah_ibadah_points + -- ---------------------- + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' + ) THEN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'jenis' + ) THEN + ALTER TABLE rumah_ibadah_points ADD COLUMN jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya' AFTER nama; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'kontak' + ) THEN + ALTER TABLE rumah_ibadah_points ADD COLUMN kontak VARCHAR(120) NULL AFTER jenis; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'radius_meter' + ) THEN + ALTER TABLE rumah_ibadah_points ADD COLUMN radius_meter DOUBLE NOT NULL DEFAULT 300 AFTER kontak; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'address' + ) THEN + ALTER TABLE rumah_ibadah_points ADD COLUMN address TEXT NULL AFTER longitude; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND index_name = 'idx_ibadah_latlng' + ) THEN + ALTER TABLE rumah_ibadah_points ADD INDEX idx_ibadah_latlng (latitude, longitude); + END IF; + END IF; + + -- ---------------------- + -- pemukiman_miskin_points + -- ---------------------- + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' + ) THEN + -- Backward compat: kalau masih pakai kolom `nama`, migrasikan ke `kk_nama` + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kk_nama' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN kk_nama VARCHAR(200) NULL AFTER id; + BEGIN + DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END; + UPDATE pemukiman_miskin_points + SET kk_nama = COALESCE(NULLIF(kk_nama, ''), NULLIF(nama, '')) + WHERE (kk_nama IS NULL OR kk_nama = ''); + END; + -- jadikan NOT NULL kalau sudah terisi + BEGIN + DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END; + ALTER TABLE pemukiman_miskin_points MODIFY kk_nama VARCHAR(200) NOT NULL; + END; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'nik' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN nik VARCHAR(32) NULL AFTER kk_nama; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jumlah_anggota' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN jumlah_anggota INT NOT NULL DEFAULT 0 AFTER nik; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kelurahan' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN kelurahan VARCHAR(120) NULL AFTER address; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kecamatan' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN kecamatan VARCHAR(120) NULL AFTER kelurahan; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'status_bantuan' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN status_bantuan VARCHAR(24) NOT NULL DEFAULT 'Belum dibantu' AFTER kecamatan; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jenis_bantuan' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN jenis_bantuan VARCHAR(120) NULL AFTER status_bantuan; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'tanggal_bantuan' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN tanggal_bantuan DATE NULL AFTER jenis_bantuan; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'bukti_file' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN bukti_file VARCHAR(255) NULL AFTER tanggal_bantuan; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jarak_meter' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN jarak_meter DOUBLE NULL AFTER bukti_file; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'rumah_ibadah_id' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD COLUMN rumah_ibadah_id INT NULL AFTER jarak_meter; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND index_name = 'idx_miskin_ibadah' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD INDEX idx_miskin_ibadah (rumah_ibadah_id); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND index_name = 'idx_miskin_latlng' + ) THEN + ALTER TABLE pemukiman_miskin_points ADD INDEX idx_miskin_latlng (latitude, longitude); + END IF; + END IF; +END $$ + +-- Jalankan migrasi (aman dijalankan berulang) +CALL migrate_webgis_spbu() $$ +DROP PROCEDURE IF EXISTS migrate_webgis_spbu $$ + +DELIMITER ; diff --git a/webgis-spbu/uploads/bukti/.gitkeep b/webgis-spbu/uploads/bukti/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/webgis-spbu/uploads/bukti/.gitkeep @@ -0,0 +1 @@ +