commit c90ce9aedcb24a3d71e8c35e6b45475ecacddf57 Author: z0rayy Date: Mon Jun 8 21:16:23 2026 +0700 Initial commit untuk UAS SIG diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..0f4e337 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,255 @@ +# WebGIS SPBU System - Feature Development Guide + +## Status Fitur yang Sudah Dikembangkan + +### ✅ Completed (Selesai) + +#### 1. **Redesigned UI dengan Sidebar Navigation** +- Modern dashboard dengan sidebar menu di sebelah kiri +- Navigasi panel yang mudah antar fitur +- Responsive design untuk berbagai ukuran layar +- Professional color scheme tanpa emoji + +**Files:** +- `index_new.html` - UI baru dengan sidebar (rename ke `index.html` untuk menggunakan) +- `js/app.js` - Main application logic + +#### 2. **Dashboard & Analytics** +- Menampilkan total statistik data (SPBU, Jalan, Parsil, Ibadah, Miskin) +- Analytics cards dengan data counters +- Real-time update data badges + +**Panel:** Analytics Dashboard + +#### 3. **Data Management (Create, Read, Update, Delete)** +- ✅ Tabel management untuk setiap tipe data +- ✅ Modal forms untuk Add/Edit data +- ✅ Delete functionality dengan confirmation +- ✅ Real-time validation + +**Backend Files:** +- `backend/create.php` - Create SPBU +- `backend/read.php` - Read SPBU +- `backend/update.php` - Update SPBU (NEW) +- `backend/delete.php` - Delete SPBU (NEW) +- `backend/create_jalan.php` - Create Jalan +- `backend/read_jalan.php` - Read Jalan +- `backend/update_jalan.php` - Update Jalan (NEW) +- `backend/delete_jalan.php` - Delete Jalan (NEW) +- `backend/create_parsil.php` - Create Parsil +- `backend/read_parsil.php` - Read Parsil +- `backend/update_parsil.php` - Update Parsil (NEW) +- `backend/delete_parsil.php` - Delete Parsil (NEW) +- `backend/create_ibadah.php` - Create Ibadah +- `backend/read_ibadah.php` - Read Ibadah +- `backend/update_ibadah.php` - Update Ibadah (NEW) +- `backend/delete_ibadah.php` - Delete Ibadah (NEW) +- `backend/create_miskin.php` - Create Miskin +- `backend/read_miskin.php` - Read Miskin +- `backend/update_miskin.php` - Update Miskin (NEW) +- `backend/delete_miskin.php` - Delete Miskin (NEW) + +#### 4. **Advanced Search & Filter** +- Search input di setiap panel data +- Filter buttons untuk kategori spesifik +- Real-time table filtering + +**Features:** +- SPBU: Filter by status (24 Jam / Tidak 24 Jam) +- Jalan: Filter by type (Nasional / Provinsi / Kabupaten) +- Parsil: Filter by type (SHM / HGB / HGU / HP) +- Global search functionality + +#### 5. **Map Integration** +- Interactive Leaflet map dengan multiple basemaps +- Layer visualization untuk setiap tipe data +- Color-coded markers dan shapes +- Popup info saat klik marker + +--- + +## 🚀 Cara Menggunakan Sistem Baru + +### 1. **Setup** +```bash +# Buat database menggunakan database.sql +mysql -u root -p < database.sql + +# File sudah ready di: +# - index_new.html (rename jadi index.html untuk use) +# - js/app.js +# - backend/update*.php, delete*.php files +``` + +### 2. **Access Application** +``` +http://localhost/webgis-sig/index_new.html +``` + +### 3. **Navigasi Sidebar** +- **Dashboard Map** - Peta interaktif dengan semua layer +- **Analytics** - Dashboard dengan statistik data +- **SPBU / Jalan / Parsil / Ibadah / Pend. Miskin** - Data management panels +- **Search** - Advanced search & filter +- **Routing** - Find routes & nearest locations +- **Export** - Export/Import data +- **Account** - User settings + +### 4. **Fitur-Fitur Utama** + +#### Data Management +1. Klik panel di sidebar (e.g., "SPBU") +2. Gunakan search input untuk cari data +3. Gunakan filter buttons untuk kategori +4. Klik "Add" button untuk tambah data baru +5. Klik "Edit" atau "Delete" di action column + +#### Dashboard +1. Pilih "Analytics" di sidebar +2. Lihat statistik real-time +3. Data auto-update saat ada perubahan + +#### Map Visualization +1. Pilih "Dashboard Map" di sidebar +2. Lihat semua layer pada peta +3. Gunakan layer control (kanan bawah) untuk toggle layer +4. Klik marker untuk lihat detail + +#### Search +1. Pilih "Search" di sidebar +2. Gunakan "Search by Keywords" untuk cari nama/lokasi +3. Gunakan "Search by Location" dengan radius slider + +--- + +## 📋 API Endpoints + +### SPBU +- POST `/backend/create.php` - Create SPBU +- GET `/backend/read.php` - Read all SPBU +- POST `/backend/update.php` - Update SPBU (NEW) +- POST `/backend/delete.php` - Delete SPBU (NEW) + +### Jalan +- POST `/backend/create_jalan.php` - Create Jalan +- GET `/backend/read_jalan.php` - Read all Jalan +- POST `/backend/update_jalan.php` - Update Jalan (NEW) +- POST `/backend/delete_jalan.php` - Delete Jalan (NEW) + +### Parsil +- POST `/backend/create_parsil.php` - Create Parsil +- GET `/backend/read_parsil.php` - Read all Parsil +- POST `/backend/update_parsil.php` - Update Parsil (NEW) +- POST `/backend/delete_parsil.php` - Delete Parsil (NEW) + +### Ibadah +- POST `/backend/create_ibadah.php` - Create Ibadah +- GET `/backend/read_ibadah.php` - Read all Ibadah +- POST `/backend/update_ibadah.php` - Update Ibadah (NEW) +- POST `/backend/delete_ibadah.php` - Delete Ibadah (NEW) + +### Miskin +- POST `/backend/create_miskin.php` - Create Miskin +- GET `/backend/read_miskin.php` - Read all Miskin +- POST `/backend/update_miskin.php` - Update Miskin (NEW) +- POST `/backend/delete_miskin.php` - Delete Miskin (NEW) + +--- + +## 🔄 Fitur yang Masih Dalam Pengembangan + +### ⏳ In Progress / Planned + +1. **Advanced Map Features** + - Heatmap layer untuk kepadatan data + - Cluster markers untuk marker berdekatan + - Measure distance/area tool + - Advanced draw/annotation tools + +2. **User Authentication** + - Login system + - User registration + - Role-based access control + - Password management + +3. **Routing & Navigation** + - Find optimal route antar lokasi + - Nearest SPBU finder + - Distance/time estimation + - Navigation instructions + +4. **Export & Import** + - Export to CSV, XLSX, GeoJSON, PDF + - Import data dari file (bulk upload) + - Data validation saat import + +5. **Notifications & Alerts** + - Real-time notifications + - Data change alerts + - Duplicate data detection + +6. **Mobile Responsive Design** + - Optimize untuk mobile/tablet + - Touch-friendly controls + - Responsive layout + +--- + +## 🛠️ Development Notes + +### Frontend Stack +- HTML5 / CSS3 (Custom responsive design) +- Vanilla JavaScript (ES6+) +- Leaflet.js - Map library +- Turf.js - Geospatial analysis + +### Backend Stack +- PHP 7.4+ +- MySQL/MariaDB +- RESTful API design +- JSON responses + +### File Structure +``` +webgis-sig/ +├── index.html (original, akan diganti) +├── index_new.html (new UI dengan sidebar) ← GUNAKAN INI +├── database.sql +├── backend/ +│ ├── koneksi.php +│ ├── create.php, read.php, update.php, delete.php (SPBU) +│ ├── create_jalan.php, read_jalan.php, update_jalan.php, delete_jalan.php (JALAN) +│ ├── create_parsil.php, read_parsil.php, update_parsil.php, delete_parsil.php (PARSIL) +│ ├── create_ibadah.php, read_ibadah.php, update_ibadah.php, delete_ibadah.php (IBADAH) +│ ├── create_miskin.php, read_miskin.php, update_miskin.php, delete_miskin.php (MISKIN) +│ └── [Future: auth.php, export.php, import.php, etc] +├── js/ +│ ├── app.js (main logic) ← NEW +│ └── map.js (original, bisa dihapus) +└── test_*.php (for testing) +``` + +--- + +## ⚡ Next Steps + +1. **Rename index_new.html ke index.html** (atau update server config) +2. **Test semua CRUD operations** untuk memastikan semuanya berjalan +3. **Lanjutkan fitur Authentication** untuk user management +4. **Implementasi Advanced Map Features** seperti heatmap & clustering +5. **Tambahkan Export/Import functionality** + +--- + +## 📞 Support + +Untuk issues atau pertanyaan, silakan check: +- Console browser (F12) untuk error messages +- Network tab untuk API request/response +- Server logs di XAMPP untuk PHP errors + +--- + +**Last Updated:** May 2026 +**Status:** Ongoing Development +**Version:** 2.0 (New UI & CRUD Features) diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..098fe8e --- /dev/null +++ b/admin.html @@ -0,0 +1,2018 @@ + + + + + + + WebGIS SPBU — Admin Dashboard + + + + + + + + + + + + + + + + +
+ +
+
+
+
WG
+
+

WebGIS

+

SPBU Systems

+
+
+ +
+ +
+ +
+
Main
+
+
+
▦
+
Dashboard Map
+
+
+
◈
+
Analytics
+
+
+
+ + +
+
Data
+
+
+
S
+
SPBU
+
0
+
+
+
J
+
Jalan
+
0
+
+
+
P
+
Parsil
+
0
+
+
+
I
+
Ibadah
+
0
+
+
+
M
+
Pend. Miskin
+
0
+
+
+
+ + +
+
Settings
+
+
+
⚙
+
Account
+
+
+
⎋
+
Logout
+
+
+
+
+
+ + +
+ +
+
+

Dashboard Map

+
+ Map +
+
+
+
+
+ 0 Data +
+
+
+ + +
+ + +
+
+ + +
+

🔍 Pencarian Global

+
+ +
+
+
+ + +
+

📍 ROUTING & RADIUS

+ +
+ +
RADIUS SEARCH
+ +
+ 500 m + (Klik peta) +
+
+ + +
+
⊕ Tambah Data
+ +
📍 Titik
+ + + + +
✏️ Gambar
+ + + +
+ +
+ +
+
SPBU
+
+
+ Beroperasi 24 Jam +
+
+
+ Tidak 24 Jam +
+
Jalan
+
+
+ Nasional +
+
+
+ Provinsi +
+
+
+ Kabupaten +
+
Parsil Tanah
+
+
+ SHM +
+
+
+ HGB +
+
+
+ HGU +
+
+
+ HP +
+
Sosial Religi
+
+
+ Rumah Ibadah +
+
+
+ Penduduk Miskin +
+
+
+ + +
+
+

Analytics Dashboard

+
+
+
+
+
Total SPBU
+
0
+
Data aktif
+
+
+
Total Jalan
+
0
+
Data tercatat
+
+
+
Total Parsil
+
0
+
Data tercatat
+
+
+
Rumah Ibadah
+
0
+
Data tercatat
+
+
+
Pend. Miskin
+
0
+
Data tercatat
+
+
+ +
+
+

SPBU Status Distribution

+
+
+ Chart akan muncul di sini +
+
+ +
+
+

Jalan Type Distribution

+
+
+ Chart akan muncul di sini +
+
+
+
+ + +
+
+

SPBU Data Management

+
+ +
+
+
+
+ + ⌕ +
+
+ + + +
+
+ + + + + + + + + + + + + + + +
NamaNo SPBUStatusLokasiActions
Loading...
+
+
+
+ + +
+
+

JALAN Data Management

+
+ +
+
+
+
+ + ⌕ +
+
+ + + + +
+
+ + + + + + + + + + + + + + +
Nama JalanStatusPanjangActions
Loading...
+
+
+
+ + +
+
+

PARSIL Data Management

+
+ +
+
+
+
+ + ⌕ +
+
+ + + + + +
+
+ + + + + + + + + + + + + + +
Nama PemilikStatusLuasActions
Loading...
+
+
+
+ + +
+
+

IBADAH Data Management

+
+ +
+
+
+
+ + ⌕ +
+
+ + + + + + + + + + + + + + +
NamaAlamatLokasiActions
Loading...
+
+
+
+ + +
+
+

PENDUDUK MISKIN Data Management

+
+ +
+
+
+
+ + ⌕ +
+
+ + + + + + + + + + + + + + +
Nama KKKeteranganLokasiActions
Loading...
+
+
+
+ + + + +
+
+

Account Settings

+
+
+
+
+

User Profile

+
+
+ + +
+
+ + +
+ +
+ +
+
+

Change Password

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+ +
+
+ + +
+
+
+

Add SPBU

+ +
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+

Add Jalan

+ +
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + Paste LineString GeoJSON geometry + +
+
+
+
+ + +
+
+
+ + +
+
+
+

Add Parsil

+ +
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + Paste Polygon GeoJSON geometry + +
+
+
+
+ + +
+
+
+ + +
+
+
+

Add Rumah Ibadah

+ +
+
+
+ +
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+

Add Penduduk Miskin

+ +
+
+
+ +
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+

Pilih Jenis Data

+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + diff --git a/backend/check_auth.php b/backend/check_auth.php new file mode 100644 index 0000000..13eca95 --- /dev/null +++ b/backend/check_auth.php @@ -0,0 +1,10 @@ + 'success', 'username' => $_SESSION['username']]); +} else { + http_response_code(401); + echo json_encode(['status' => 'error', 'message' => 'Unauthorized']); +} diff --git a/backend/create.php b/backend/create.php new file mode 100644 index 0000000..812089e --- /dev/null +++ b/backend/create.php @@ -0,0 +1,96 @@ + false, 'message' => 'Metode tidak diizinkan.']); + exit; +} + +require_once __DIR__ . '/koneksi.php'; + +// Ambil body JSON dari request +$body = file_get_contents('php://input'); +$data = json_decode($body, true); + +if (!$data) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Body request tidak valid (bukan JSON).']); + exit; +} + +// ---- Validasi field wajib ---- +$required = ['nama', 'no_spbu', 'status', 'latitude', 'longitude']; +foreach ($required as $field) { + if (!isset($data[$field]) || trim((string)$data[$field]) === '') { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Field '$field' tidak boleh kosong."]); + exit; + } +} + +$nama = trim($data['nama']); +$no_spbu = trim($data['no_spbu']); +$status = $data['status']; +$latitude = (float) $data['latitude']; +$longitude = (float) $data['longitude']; + +// Validasi nilai enum status +if (!in_array($status, ['24jam', 'tidak'], true)) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Nilai status tidak valid. Gunakan '24jam' atau 'tidak'."]); + exit; +} + +// ---- Simpan ke database dengan prepared statement ---- +$conn = getConnection(); + +$sql = "INSERT INTO spbu (nama, no_spbu, status, latitude, longitude) VALUES (?, ?, ?, ?, ?)"; +$stmt = $conn->prepare($sql); + +if (!$stmt) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyiapkan query: ' . $conn->error]); + $conn->close(); + exit; +} + +// s = string, d = double/float +$stmt->bind_param('sssdd', $nama, $no_spbu, $status, $latitude, $longitude); + +if ($stmt->execute()) { + $newId = $stmt->insert_id; + http_response_code(201); + echo json_encode([ + 'success' => true, + 'message' => 'Data SPBU berhasil disimpan.', + 'data' => [ + 'id' => $newId, + 'nama' => $nama, + 'no_spbu' => $no_spbu, + 'status' => $status, + 'latitude' => $latitude, + 'longitude' => $longitude, + ], + ]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $stmt->error]); +} + +$stmt->close(); +$conn->close(); diff --git a/backend/create_ibadah.php b/backend/create_ibadah.php new file mode 100644 index 0000000..ce5f98f --- /dev/null +++ b/backend/create_ibadah.php @@ -0,0 +1,49 @@ + false, 'message' => 'Metode tidak diizinkan.']); exit; } + +require_once __DIR__ . '/koneksi.php'; +$body = file_get_contents('php://input'); +$data = json_decode($body, true); + +if (!$data) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Body request tidak valid.']); exit; } + +$required = ['nama', 'alamat', 'latitude', 'longitude']; +foreach ($required as $field) { + if (!isset($data[$field]) || trim((string)$data[$field]) === '') { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Field '$field' wajib diisi."]); + exit; + } +} + +$nama = trim($data['nama']); +$alamat = trim($data['alamat']); +$latitude = (float) $data['latitude']; +$longitude = (float) $data['longitude']; + +$conn = getConnection(); +$sql = "INSERT INTO rumah_ibadah (nama, alamat, latitude, longitude) VALUES (?, ?, ?, ?)"; +$stmt = $conn->prepare($sql); +if (!$stmt) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Query error: ' . $conn->error]); $conn->close(); exit; } + +$stmt->bind_param('ssdd', $nama, $alamat, $latitude, $longitude); +if ($stmt->execute()) { + http_response_code(201); + echo json_encode([ + 'success' => true, + 'message' => 'Data Rumah Ibadah disimpan.', + 'data' => [ 'id' => $stmt->insert_id, 'nama' => $nama, 'alamat' => $alamat, 'latitude' => $latitude, 'longitude' => $longitude ] + ]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyimpan: ' . $stmt->error]); +} +$stmt->close(); +$conn->close(); diff --git a/backend/create_jalan.php b/backend/create_jalan.php new file mode 100644 index 0000000..043d4f5 --- /dev/null +++ b/backend/create_jalan.php @@ -0,0 +1,91 @@ + false, 'message' => 'Metode tidak diizinkan.']); + exit; +} + +require_once __DIR__ . '/koneksi.php'; + +$body = file_get_contents('php://input'); +$data = json_decode($body, true); + +if (!$data) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Body request tidak valid.']); + exit; +} + +$required = ['nama_jalan', 'status', 'panjang', 'geom']; +foreach ($required as $field) { + if (!isset($data[$field])) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Field '$field' wajib diisi."]); + exit; + } + if (!is_array($data[$field]) && trim((string)$data[$field]) === '') { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Field '$field' tidak boleh kosong."]); + exit; + } +} + +$nama_jalan = trim($data['nama_jalan']); +$status = $data['status']; +$panjang = (float) $data['panjang']; +$geom = is_array($data['geom']) ? json_encode($data['geom']) : $data['geom']; + +if (!in_array($status, ['Nasional', 'Provinsi', 'Kabupaten'], true)) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Status jalan tidak valid."]); + exit; +} + +$conn = getConnection(); +$sql = "INSERT INTO jalan (nama_jalan, status, panjang, geom) VALUES (?, ?, ?, ?)"; +$stmt = $conn->prepare($sql); + +if (!$stmt) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyiapkan query: ' . $conn->error]); + $conn->close(); + exit; +} + +$stmt->bind_param('ssds', $nama_jalan, $status, $panjang, $geom); + +if ($stmt->execute()) { + $newId = $stmt->insert_id; + http_response_code(201); + echo json_encode([ + 'success' => true, + 'message' => 'Data Jalan berhasil disimpan.', + 'data' => [ + 'id' => $newId, + 'nama_jalan' => $nama_jalan, + 'status' => $status, + 'panjang' => $panjang, + 'geom' => json_decode($geom, true), + ], + ]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $stmt->error]); +} + +$stmt->close(); +$conn->close(); diff --git a/backend/create_miskin.php b/backend/create_miskin.php new file mode 100644 index 0000000..62559a4 --- /dev/null +++ b/backend/create_miskin.php @@ -0,0 +1,49 @@ + false, 'message' => 'Metode tidak diizinkan.']); exit; } + +require_once __DIR__ . '/koneksi.php'; +$body = file_get_contents('php://input'); +$data = json_decode($body, true); + +if (!$data) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Body request tidak valid.']); exit; } + +$required = ['nama_kk', 'latitude', 'longitude']; +foreach ($required as $field) { + if (!isset($data[$field]) || trim((string)$data[$field]) === '') { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Field '$field' wajib diisi."]); + exit; + } +} + +$nama_kk = trim($data['nama_kk']); +$keterangan= isset($data['keterangan']) ? trim($data['keterangan']) : ''; +$latitude = (float) $data['latitude']; +$longitude = (float) $data['longitude']; + +$conn = getConnection(); +$sql = "INSERT INTO penduduk_miskin (nama_kk, keterangan, latitude, longitude) VALUES (?, ?, ?, ?)"; +$stmt = $conn->prepare($sql); +if (!$stmt) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Query error: ' . $conn->error]); $conn->close(); exit; } + +$stmt->bind_param('ssdd', $nama_kk, $keterangan, $latitude, $longitude); +if ($stmt->execute()) { + http_response_code(201); + echo json_encode([ + 'success' => true, + 'message' => 'Data Penduduk Miskin disimpan.', + 'data' => [ 'id' => $stmt->insert_id, 'nama_kk' => $nama_kk, 'keterangan' => $keterangan, 'latitude' => $latitude, 'longitude' => $longitude ] + ]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyimpan: ' . $stmt->error]); +} +$stmt->close(); +$conn->close(); diff --git a/backend/create_parsil.php b/backend/create_parsil.php new file mode 100644 index 0000000..5eaafc5 --- /dev/null +++ b/backend/create_parsil.php @@ -0,0 +1,91 @@ + false, 'message' => 'Metode tidak diizinkan.']); + exit; +} + +require_once __DIR__ . '/koneksi.php'; + +$body = file_get_contents('php://input'); +$data = json_decode($body, true); + +if (!$data) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Body request tidak valid.']); + exit; +} + +$required = ['nama_pemilik', 'status', 'luas', 'geom']; +foreach ($required as $field) { + if (!isset($data[$field])) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Field '$field' wajib diisi."]); + exit; + } + if (!is_array($data[$field]) && trim((string)$data[$field]) === '') { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Field '$field' tidak boleh kosong."]); + exit; + } +} + +$nama_pemilik = trim($data['nama_pemilik']); +$status = $data['status']; +$luas = (float) $data['luas']; +$geom = is_array($data['geom']) ? json_encode($data['geom']) : $data['geom']; + +if (!in_array($status, ['SHM', 'HGB', 'HGU', 'HP'], true)) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => "Status parsil tidak valid."]); + exit; +} + +$conn = getConnection(); +$sql = "INSERT INTO parsil (nama_pemilik, status, luas, geom) VALUES (?, ?, ?, ?)"; +$stmt = $conn->prepare($sql); + +if (!$stmt) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyiapkan query: ' . $conn->error]); + $conn->close(); + exit; +} + +$stmt->bind_param('ssds', $nama_pemilik, $status, $luas, $geom); + +if ($stmt->execute()) { + $newId = $stmt->insert_id; + http_response_code(201); + echo json_encode([ + 'success' => true, + 'message' => 'Data Parsil berhasil disimpan.', + 'data' => [ + 'id' => $newId, + 'nama_pemilik' => $nama_pemilik, + 'status' => $status, + 'luas' => $luas, + 'geom' => json_decode($geom, true), + ], + ]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $stmt->error]); +} + +$stmt->close(); +$conn->close(); diff --git a/backend/create_users_table.php b/backend/create_users_table.php new file mode 100644 index 0000000..4bcf1c8 --- /dev/null +++ b/backend/create_users_table.php @@ -0,0 +1,18 @@ +query($sql); + +$passwordHash = password_hash('admin123', PASSWORD_DEFAULT); +$insert = "INSERT IGNORE INTO users (username, password) VALUES ('admin', '$passwordHash')"; +$c->query($insert); + +echo 'Users table created and admin user inserted.'; diff --git a/backend/delete.php b/backend/delete.php new file mode 100644 index 0000000..05dfd51 --- /dev/null +++ b/backend/delete.php @@ -0,0 +1,37 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); + +$stmt = $conn->prepare('DELETE FROM spbu WHERE id = ?'); +$stmt->bind_param('i', $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'SPBU deleted successfully']); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to delete SPBU: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/delete_ibadah.php b/backend/delete_ibadah.php new file mode 100644 index 0000000..fc2aaa2 --- /dev/null +++ b/backend/delete_ibadah.php @@ -0,0 +1,37 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); + +$stmt = $conn->prepare('DELETE FROM rumah_ibadah WHERE id = ?'); +$stmt->bind_param('i', $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Rumah Ibadah deleted successfully']); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to delete Rumah Ibadah: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/delete_jalan.php b/backend/delete_jalan.php new file mode 100644 index 0000000..f350459 --- /dev/null +++ b/backend/delete_jalan.php @@ -0,0 +1,37 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); + +$stmt = $conn->prepare('DELETE FROM jalan WHERE id = ?'); +$stmt->bind_param('i', $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Jalan deleted successfully']); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to delete Jalan: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/delete_miskin.php b/backend/delete_miskin.php new file mode 100644 index 0000000..f679e55 --- /dev/null +++ b/backend/delete_miskin.php @@ -0,0 +1,37 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); + +$stmt = $conn->prepare('DELETE FROM penduduk_miskin WHERE id = ?'); +$stmt->bind_param('i', $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Penduduk Miskin deleted successfully']); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to delete Penduduk Miskin: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/delete_parsil.php b/backend/delete_parsil.php new file mode 100644 index 0000000..ca3085e --- /dev/null +++ b/backend/delete_parsil.php @@ -0,0 +1,37 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); + +$stmt = $conn->prepare('DELETE FROM parsil WHERE id = ?'); +$stmt->bind_param('i', $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Parsil deleted successfully']); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to delete Parsil: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/koneksi.php b/backend/koneksi.php new file mode 100644 index 0000000..981fa4a --- /dev/null +++ b/backend/koneksi.php @@ -0,0 +1,22 @@ +connect_error) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Koneksi database gagal: ' . $conn->connect_error]); + exit; + } + + $conn->set_charset('utf8mb4'); + return $conn; +} diff --git a/backend/login.php b/backend/login.php new file mode 100644 index 0000000..8c0c264 --- /dev/null +++ b/backend/login.php @@ -0,0 +1,41 @@ + 'error', 'message' => 'Invalid request method']); + exit; +} + +$input = json_decode(file_get_contents('php://input'), true); +if (!$input && isset($_POST['username'])) { + $input = $_POST; +} + +$username = $input['username'] ?? ''; +$password = $input['password'] ?? ''; + +if (empty($username) || empty($password)) { + echo json_encode(['status' => 'error', 'message' => 'Username and password required']); + exit; +} + +$stmt = $c->prepare("SELECT id, username, password FROM users WHERE username = ?"); +$stmt->bind_param("s", $username); +$stmt->execute(); +$result = $stmt->get_result(); + +if ($row = $result->fetch_assoc()) { + if (password_verify($password, $row['password'])) { + // Success + $_SESSION['user_id'] = $row['id']; + $_SESSION['username'] = $row['username']; + echo json_encode(['status' => 'success', 'message' => 'Login successful']); + exit; + } +} + +echo json_encode(['status' => 'error', 'message' => 'Invalid username or password']); diff --git a/backend/logout.php b/backend/logout.php new file mode 100644 index 0000000..b608276 --- /dev/null +++ b/backend/logout.php @@ -0,0 +1,5 @@ + 'success', 'message' => 'Logged out']); diff --git a/backend/read.php b/backend/read.php new file mode 100644 index 0000000..a0becdf --- /dev/null +++ b/backend/read.php @@ -0,0 +1,50 @@ + false, 'message' => 'Metode tidak diizinkan.']); + exit; +} + +require_once __DIR__ . '/koneksi.php'; + +$conn = getConnection(); + +$sql = "SELECT id, nama, no_spbu, status, latitude, longitude FROM spbu ORDER BY created_at DESC"; +$result = $conn->query($sql); + +if (!$result) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Query gagal: ' . $conn->error]); + $conn->close(); + exit; +} + +$spbuList = []; +while ($row = $result->fetch_assoc()) { + $spbuList[] = [ + 'id' => (int) $row['id'], + 'nama' => $row['nama'], + 'no_spbu' => $row['no_spbu'], + 'status' => $row['status'], + 'latitude' => (float) $row['latitude'], + 'longitude' => (float) $row['longitude'], + ]; +} + +echo json_encode([ + 'success' => true, + 'count' => count($spbuList), + 'data' => $spbuList, +]); + +$result->free(); +$conn->close(); diff --git a/backend/read_ibadah.php b/backend/read_ibadah.php new file mode 100644 index 0000000..e9c4c7b --- /dev/null +++ b/backend/read_ibadah.php @@ -0,0 +1,21 @@ +query("SELECT id, nama, alamat, latitude, longitude FROM rumah_ibadah ORDER BY id DESC"); + +$data = []; +if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $row['latitude'] = (float) $row['latitude']; + $row['longitude'] = (float) $row['longitude']; + $data[] = $row; + } +} + +echo json_encode(['success' => true, 'count' => count($data), 'data' => $data]); +$conn->close(); diff --git a/backend/read_jalan.php b/backend/read_jalan.php new file mode 100644 index 0000000..0595bb2 --- /dev/null +++ b/backend/read_jalan.php @@ -0,0 +1,47 @@ + false, 'message' => 'Metode tidak diizinkan.']); + exit; +} + +require_once __DIR__ . '/koneksi.php'; + +$conn = getConnection(); +$sql = "SELECT id, nama_jalan, status, panjang, geom FROM jalan ORDER BY created_at DESC"; +$result = $conn->query($sql); + +if (!$result) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Query gagal: ' . $conn->error]); + $conn->close(); + exit; +} + +$jalanList = []; +while ($row = $result->fetch_assoc()) { + $jalanList[] = [ + 'id' => (int) $row['id'], + 'nama_jalan' => $row['nama_jalan'], + 'status' => $row['status'], + 'panjang' => (float) $row['panjang'], + 'geom' => json_decode($row['geom'], true), + ]; +} + +echo json_encode([ + 'success' => true, + 'count' => count($jalanList), + 'data' => $jalanList, +]); + +$result->free(); +$conn->close(); diff --git a/backend/read_miskin.php b/backend/read_miskin.php new file mode 100644 index 0000000..ac6f2ff --- /dev/null +++ b/backend/read_miskin.php @@ -0,0 +1,21 @@ +query("SELECT id, nama_kk, keterangan, latitude, longitude FROM penduduk_miskin ORDER BY id DESC"); + +$data = []; +if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $row['latitude'] = (float) $row['latitude']; + $row['longitude'] = (float) $row['longitude']; + $data[] = $row; + } +} + +echo json_encode(['success' => true, 'count' => count($data), 'data' => $data]); +$conn->close(); diff --git a/backend/read_parsil.php b/backend/read_parsil.php new file mode 100644 index 0000000..e011df8 --- /dev/null +++ b/backend/read_parsil.php @@ -0,0 +1,47 @@ + false, 'message' => 'Metode tidak diizinkan.']); + exit; +} + +require_once __DIR__ . '/koneksi.php'; + +$conn = getConnection(); +$sql = "SELECT id, nama_pemilik, status, luas, geom FROM parsil ORDER BY created_at DESC"; +$result = $conn->query($sql); + +if (!$result) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Query gagal: ' . $conn->error]); + $conn->close(); + exit; +} + +$parsilList = []; +while ($row = $result->fetch_assoc()) { + $parsilList[] = [ + 'id' => (int) $row['id'], + 'nama_pemilik' => $row['nama_pemilik'], + 'status' => $row['status'], + 'luas' => (float) $row['luas'], + 'geom' => json_decode($row['geom'], true), + ]; +} + +echo json_encode([ + 'success' => true, + 'count' => count($parsilList), + 'data' => $parsilList, +]); + +$result->free(); +$conn->close(); diff --git a/backend/register.php b/backend/register.php new file mode 100644 index 0000000..0be65ce --- /dev/null +++ b/backend/register.php @@ -0,0 +1,44 @@ + 'error', 'message' => 'Invalid request method']); + exit; +} + +$input = json_decode(file_get_contents('php://input'), true); +if (!$input && isset($_POST['username'])) { + $input = $_POST; +} + +$username = $input['username'] ?? ''; +$password = $input['password'] ?? ''; + +if (empty($username) || empty($password)) { + echo json_encode(['status' => 'error', 'message' => 'Username and password required']); + exit; +} + +// Check if username already exists +$stmt = $c->prepare("SELECT id FROM users WHERE username = ?"); +$stmt->bind_param("s", $username); +$stmt->execute(); +$result = $stmt->get_result(); + +if ($result->fetch_assoc()) { + echo json_encode(['status' => 'error', 'message' => 'Username sudah digunakan']); + exit; +} + +$passwordHash = password_hash($password, PASSWORD_DEFAULT); +$stmt = $c->prepare("INSERT INTO users (username, password) VALUES (?, ?)"); +$stmt->bind_param("ss", $username, $passwordHash); + +if ($stmt->execute()) { + echo json_encode(['status' => 'success', 'message' => 'Registrasi berhasil, silakan login']); +} else { + echo json_encode(['status' => 'error', 'message' => 'Gagal mendaftar akun']); +} diff --git a/backend/update.php b/backend/update.php new file mode 100644 index 0000000..7570526 --- /dev/null +++ b/backend/update.php @@ -0,0 +1,56 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); +$nama = $data['nama'] ?? null; +$no_spbu = $data['no_spbu'] ?? null; +$status = $data['status'] ?? null; +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; + +$errors = []; + +if (empty($nama)) $errors[] = 'Nama is required'; +if (empty($no_spbu)) $errors[] = 'No SPBU is required'; +if (empty($status)) $errors[] = 'Status is required'; +if ($latitude === null) $errors[] = 'Latitude is required'; +if ($longitude === null) $errors[] = 'Longitude is required'; + +if (!empty($errors)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => implode(', ', $errors)]); + exit; +} + +$stmt = $conn->prepare('UPDATE spbu SET nama = ?, no_spbu = ?, status = ?, latitude = ?, longitude = ? WHERE id = ?'); +$stmt->bind_param('sssddi', $nama, $no_spbu, $status, $latitude, $longitude, $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'SPBU updated successfully', 'data' => ['id' => $id]]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to update SPBU: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/update_ibadah.php b/backend/update_ibadah.php new file mode 100644 index 0000000..f130acd --- /dev/null +++ b/backend/update_ibadah.php @@ -0,0 +1,54 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); +$nama = $data['nama'] ?? null; +$alamat = $data['alamat'] ?? null; +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; + +$errors = []; + +if (empty($nama)) $errors[] = 'Nama is required'; +if (empty($alamat)) $errors[] = 'Alamat is required'; +if ($latitude === null) $errors[] = 'Latitude is required'; +if ($longitude === null) $errors[] = 'Longitude is required'; + +if (!empty($errors)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => implode(', ', $errors)]); + exit; +} + +$stmt = $conn->prepare('UPDATE rumah_ibadah SET nama = ?, alamat = ?, latitude = ?, longitude = ? WHERE id = ?'); +$stmt->bind_param('ssddi', $nama, $alamat, $latitude, $longitude, $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Rumah Ibadah updated successfully', 'data' => ['id' => $id]]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to update Rumah Ibadah: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/update_jalan.php b/backend/update_jalan.php new file mode 100644 index 0000000..abc8048 --- /dev/null +++ b/backend/update_jalan.php @@ -0,0 +1,54 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); +$nama_jalan = $data['nama_jalan'] ?? null; +$status = $data['status'] ?? null; +$panjang = $data['panjang'] ?? null; +$geom = $data['geom'] ?? null; + +$errors = []; + +if (empty($nama_jalan)) $errors[] = 'Nama Jalan is required'; +if (empty($status)) $errors[] = 'Status is required'; +if ($panjang === null) $errors[] = 'Panjang is required'; +if (empty($geom)) $errors[] = 'Geometry is required'; + +if (!empty($errors)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => implode(', ', $errors)]); + exit; +} + +$stmt = $conn->prepare('UPDATE jalan SET nama_jalan = ?, status = ?, panjang = ?, geom = ? WHERE id = ?'); +$stmt->bind_param('ssdsi', $nama_jalan, $status, $panjang, $geom, $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Jalan updated successfully', 'data' => ['id' => $id]]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to update Jalan: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/update_miskin.php b/backend/update_miskin.php new file mode 100644 index 0000000..6b6ae1e --- /dev/null +++ b/backend/update_miskin.php @@ -0,0 +1,53 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); +$nama_kk = $data['nama_kk'] ?? null; +$keterangan = $data['keterangan'] ?? null; +$latitude = $data['latitude'] ?? null; +$longitude = $data['longitude'] ?? null; + +$errors = []; + +if (empty($nama_kk)) $errors[] = 'Nama KK is required'; +if ($latitude === null) $errors[] = 'Latitude is required'; +if ($longitude === null) $errors[] = 'Longitude is required'; + +if (!empty($errors)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => implode(', ', $errors)]); + exit; +} + +$stmt = $conn->prepare('UPDATE penduduk_miskin SET nama_kk = ?, keterangan = ?, latitude = ?, longitude = ? WHERE id = ?'); +$stmt->bind_param('ssddi', $nama_kk, $keterangan, $latitude, $longitude, $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Penduduk Miskin updated successfully', 'data' => ['id' => $id]]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to update Penduduk Miskin: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/backend/update_parsil.php b/backend/update_parsil.php new file mode 100644 index 0000000..ffaa2e0 --- /dev/null +++ b/backend/update_parsil.php @@ -0,0 +1,54 @@ + false, 'message' => 'Method not allowed']); + exit; +} + +$conn = getConnection(); +$data = json_decode(file_get_contents('php://input'), true); + +if (!isset($data['id'])) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID is required']); + exit; +} + +$id = intval($data['id']); +$nama_pemilik = $data['nama_pemilik'] ?? null; +$status = $data['status'] ?? null; +$luas = $data['luas'] ?? null; +$geom = $data['geom'] ?? null; + +$errors = []; + +if (empty($nama_pemilik)) $errors[] = 'Nama Pemilik is required'; +if (empty($status)) $errors[] = 'Status is required'; +if ($luas === null) $errors[] = 'Luas is required'; +if (empty($geom)) $errors[] = 'Geometry is required'; + +if (!empty($errors)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => implode(', ', $errors)]); + exit; +} + +$stmt = $conn->prepare('UPDATE parsil SET nama_pemilik = ?, status = ?, luas = ?, geom = ? WHERE id = ?'); +$stmt->bind_param('ssdsi', $nama_pemilik, $status, $luas, $geom, $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Parsil updated successfully', 'data' => ['id' => $id]]); +} else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Failed to update Parsil: ' . $conn->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/build.js b/build.js new file mode 100644 index 0000000..9bca7da --- /dev/null +++ b/build.js @@ -0,0 +1,406 @@ +const fs = require('fs'); +const admin = fs.readFileSync('admin.html', 'utf8'); + +// Extract CSS +const cssMatch = admin.match(/ + + +
+ ${sidebar} + +
+
+
+

Peta Persebaran SPBU

+
+ Public Map +
+
+
+ +
+
+
+ +
+

Fitur SPBU Terdekat

+ + + + +
+ SPBU Terdekat: -
+ Jarak: - km +
+
+ +
+

Legenda

+
+ SPBU +
+
+ Lokasi Anda +
+
+
+
+
+
+ + + + + +`; + fs.writeFileSync('spbu.html', html); + console.log("spbu.html updated"); +} + +// Function to build Poverty +function buildPoverty() { + let sidebar = sidebarBase; + sidebar = sidebar.replace(/
Data<\/div>[\s\S]*?
/, '
'); + sidebar = sidebar.replace(/
Tools<\/div>[\s\S]*?
/, '
'); + sidebar = sidebar.replace(/
[\s\S]*?<\/div>/g, ''); + sidebar = sidebar.replace(/
Settings<\/div>[\s\S]*?<\/nav>/, '
Account
⚙
Dashboard Admin
⎋
Logout
'); + + const html = ` + + + + + Peta Kemiskinan & Rumah Ibadah + + + + + + +
Verifikasi Akses...
+ +
+ ${sidebar} + +
+
+
+

Peta Analisis Kemiskinan

+
+ +
+
+
+ +
+
+
+ +
+

Pencarian Data

+ + +
+ +
+

Legenda

+
+ Rumah Ibadah +
+
+ Penduduk Miskin (Dalam Radius) +
+
+ Penduduk Miskin (Luar Radius) +
+
+
+
+
+
+ + + + + +`; + fs.writeFileSync('poverty.html', html); + console.log("poverty.html updated"); +} + +buildSPBU(); +buildPoverty(); diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..1145f80 --- /dev/null +++ b/database.sql @@ -0,0 +1,80 @@ +-- ============================================ +-- WebGIS SPBU - Database Schema +-- ============================================ + +CREATE DATABASE IF NOT EXISTS webgis_spbu + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE webgis_spbu; + +CREATE TABLE IF NOT EXISTS spbu ( + id INT NOT NULL AUTO_INCREMENT, + nama VARCHAR(150) NOT NULL, + no_spbu VARCHAR(50) NOT NULL, + status ENUM('24jam', 'tidak') NOT NULL DEFAULT 'tidak', + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================ +-- Contoh data awal (opsional) +-- ============================================ +INSERT INTO spbu (nama, no_spbu, status, latitude, longitude) VALUES + ('SPBU Sudirman', '64.751.01', '24jam', -0.0260000, 109.3425000), + ('SPBU Ahmad Yani', '64.751.02', 'tidak', -0.0310000, 109.3218000), + ('SPBU Sutan Syahrir','64.751.03', '24jam', -0.0195000, 109.3612000); + +-- ============================================ +-- Tabel Jalan (Line) +-- ============================================ +CREATE TABLE IF NOT EXISTS jalan ( + id INT NOT NULL AUTO_INCREMENT, + nama_jalan VARCHAR(150) NOT NULL, + status ENUM('Nasional', 'Provinsi', 'Kabupaten') NOT NULL, + panjang DECIMAL(10, 2) NOT NULL, -- dalam meter + geom LONGTEXT NOT NULL, -- GeoJSON Feature atau Geometry LineString + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================ +-- Tabel Parsil Tanah (Polygon) +-- ============================================ +CREATE TABLE IF NOT EXISTS parsil ( + id INT NOT NULL AUTO_INCREMENT, + nama_pemilik VARCHAR(150) NOT NULL, + status ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL, + luas DECIMAL(12, 2) NOT NULL, -- dalam meter persegi + geom LONGTEXT NOT NULL, -- GeoJSON Feature atau Geometry Polygon + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================ +-- Tabel Rumah Ibadah (Point) +-- ============================================ +CREATE TABLE IF NOT EXISTS rumah_ibadah ( + id INT NOT NULL AUTO_INCREMENT, + nama VARCHAR(150) NOT NULL, + alamat TEXT NOT NULL, + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ============================================ +-- Tabel Penduduk Miskin (Point) +-- ============================================ +CREATE TABLE IF NOT EXISTS penduduk_miskin ( + id INT NOT NULL AUTO_INCREMENT, + nama_kk VARCHAR(150) NOT NULL, + keterangan TEXT, + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/index.html b/index.html new file mode 100644 index 0000000..2015bc3 --- /dev/null +++ b/index.html @@ -0,0 +1,83 @@ + + + + + + WebGIS Portal - SPBU & Kemiskinan + + + + + + +
+
+ +
+

Portal WebGIS Cerdas

+

Pilih sistem informasi spasial yang ingin Anda akses. Jelajahi data geospasial dengan mudah dan interaktif.

+
+ +
+ + +
+

Peta SPBU Terdekat

+

Jelajahi lokasi SPBU di sekitar Anda. Temukan SPBU terdekat dengan mudah menggunakan fitur deteksi lokasi otomatis tanpa perlu login.

+ Akses Publik +
+ + + +
+

Peta Kemiskinan

+

Sistem analisis spasial untuk mendeteksi rumah penduduk miskin dalam radius 500 meter dari fasilitas ibadah. Memerlukan autentikasi khusus.

+ Perlu Login +
+
+ + Masuk sebagai Administrator + + + diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..3a68875 --- /dev/null +++ b/js/app.js @@ -0,0 +1,1172 @@ +// ============================================ +// app.js — Main Application Logic +// ============================================ + +// ------- API Endpoints ------- +const API = { + SPBU: { + READ: 'backend/read.php', + CREATE: 'backend/create.php', + UPDATE: 'backend/update.php', + DELETE: 'backend/delete.php' + }, + JALAN: { + READ: 'backend/read_jalan.php', + CREATE: 'backend/create_jalan.php', + UPDATE: 'backend/update_jalan.php', + DELETE: 'backend/delete_jalan.php' + }, + PARSIL: { + READ: 'backend/read_parsil.php', + CREATE: 'backend/create_parsil.php', + UPDATE: 'backend/update_parsil.php', + DELETE: 'backend/delete_parsil.php' + }, + IBADAH: { + READ: 'backend/read_ibadah.php', + CREATE: 'backend/create_ibadah.php', + UPDATE: 'backend/update_ibadah.php', + DELETE: 'backend/delete_ibadah.php' + }, + MISKIN: { + READ: 'backend/read_miskin.php', + CREATE: 'backend/create_miskin.php', + UPDATE: 'backend/update_miskin.php', + DELETE: 'backend/delete_miskin.php' + } +}; + +// ------- Global State ------- +window.appState = { + data: { + spbu: [], + jalan: [], + parsil: [], + ibadah: [], + miskin: [] + }, + currentPanel: 'panel-map', + editingId: null, + editingType: null, + mapInstance: null, + layers: {} +}; + +// ============================================ +// Map Toolbar — Global Functions (called via onclick in HTML) +// ============================================ + +window._mapClickMode = null; + +window.mapToolActivatePoint = function(type, label) { + window._mapClickMode = type; + document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active')); + const btn = document.getElementById('map-add-' + type); + if (btn) btn.classList.add('active'); + const status = document.getElementById('map-tool-status'); + const cancel = document.getElementById('map-tool-cancel'); + if (status) { + status.style.display = 'block'; + status.innerHTML = '📍 Klik lokasi di peta
untuk tambah ' + label + ''; + } + if (cancel) cancel.style.display = 'block'; + const inst = window.appState.mapInstance; + if (inst) inst.getContainer().style.cursor = 'crosshair'; +}; + +window.mapToolActivateDraw = function(type) { + const inst = window.appState.mapInstance; + document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active')); + const btn = document.getElementById('map-add-' + type); + if (btn) btn.classList.add('active'); + const status = document.getElementById('map-tool-status'); + const cancel = document.getElementById('map-tool-cancel'); + if (inst && inst.pm && typeof inst.pm.enableDraw === 'function') { + inst.pm.enableDraw(type === 'jalan' ? 'Line' : 'Polygon'); + if (status) { + status.style.display = 'block'; + status.innerHTML = type === 'jalan' + ? '✏️ Gambar garis
Klik tiap titik, double-klik selesai' + : '✏️ Gambar area
Klik tiap sudut, double-klik selesai'; + } + if (cancel) cancel.style.display = 'block'; + } else { + if (type === 'jalan') openAddJalanModal(); + else openAddParsilModal(); + } +}; + +window.mapToolCancel = function() { + window._mapClickMode = null; + document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active')); + const status = document.getElementById('map-tool-status'); + const cancel = document.getElementById('map-tool-cancel'); + if (status) status.style.display = 'none'; + if (cancel) cancel.style.display = 'none'; + const inst = window.appState.mapInstance; + if (inst) { + inst.getContainer().style.cursor = ''; + if (inst.pm && typeof inst.pm.disableDraw === 'function') inst.pm.disableDraw(); + } +}; + +// ============================================ +// Utility Functions +// ============================================ + +function showToast(message, type = 'info', duration = 3000) { + const container = document.getElementById('toast-container'); + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + + const icon = { + success: '✓', + error: '✗', + info: 'i', + warning: '!' + }[type] || 'i'; + + toast.innerHTML = `${icon}${message}`; + + container.appendChild(toast); + setTimeout(() => toast.classList.add('show'), 10); + + setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => toast.remove(), 300); + }, duration); +} + +function showModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.classList.add('show'); + } +} + +function hideModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.classList.remove('show'); + } +} + +function switchPanel(panelId) { + // Hide all panels + document.querySelectorAll('.content-panel').forEach(p => { + p.classList.remove('active'); + }); + + // Deactivate all sidebar items + document.querySelectorAll('.sidebar-item').forEach(item => { + item.classList.remove('active'); + }); + + // Show selected panel + const panel = document.getElementById(panelId); + if (panel) { + panel.classList.add('active'); + + // Mark sidebar item as active + const sidebarItem = document.querySelector(`[data-panel="${panelId}"]`); + if (sidebarItem) { + sidebarItem.classList.add('active'); + } + } + + // Update page title + const sidebarItem = document.querySelector(`[data-panel="${panelId}"]`); + if (sidebarItem) { + const title = sidebarItem.querySelector('.sidebar-item-text')?.textContent || 'Dashboard'; + document.getElementById('page-title').textContent = title; + document.getElementById('breadcrumb-text').textContent = title; + } + + window.appState.currentPanel = panelId; +} + +// ============================================ +// Data Loading & Management +// ============================================ + +async function loadAllData() { + try { + const [spbuRes, jalanRes, parsilRes, ibadahRes, miskinRes] = await Promise.all([ + fetch(API.SPBU.READ), + fetch(API.JALAN.READ), + fetch(API.PARSIL.READ), + fetch(API.IBADAH.READ), + fetch(API.MISKIN.READ) + ]); + + const spbuData = await spbuRes.json(); + const jalanData = await jalanRes.json(); + const parsilData = await parsilRes.json(); + const ibadahData = await ibadahRes.json(); + const miskinData = await miskinRes.json(); + + window.appState.data.spbu = spbuData.success ? spbuData.data : []; + window.appState.data.jalan = jalanData.success ? jalanData.data : []; + window.appState.data.parsil = parsilData.success ? parsilData.data : []; + window.appState.data.ibadah = ibadahData.success ? ibadahData.data : []; + window.appState.data.miskin = miskinData.success ? miskinData.data : []; + + updateBadges(); + updateAnalytics(); + renderTables(); + renderMapLayers(); + } catch (err) { + console.error('Error loading data:', err); + showToast('Gagal memuat data', 'error'); + } +} + +function updateBadges() { + document.getElementById('badge-spbu').textContent = window.appState.data.spbu.length; + document.getElementById('badge-jalan').textContent = window.appState.data.jalan.length; + document.getElementById('badge-parsil').textContent = window.appState.data.parsil.length; + document.getElementById('badge-ibadah').textContent = window.appState.data.ibadah.length; + document.getElementById('badge-miskin').textContent = window.appState.data.miskin.length; + + const totalData = window.appState.data.spbu.length + + window.appState.data.jalan.length + + window.appState.data.parsil.length + + window.appState.data.ibadah.length + + window.appState.data.miskin.length; + + document.getElementById('total-data-count').textContent = `${totalData} Data`; +} + +function updateAnalytics() { + document.getElementById('stat-spbu').textContent = window.appState.data.spbu.length; + document.getElementById('stat-jalan').textContent = window.appState.data.jalan.length; + document.getElementById('stat-parsil').textContent = window.appState.data.parsil.length; + document.getElementById('stat-ibadah').textContent = window.appState.data.ibadah.length; + document.getElementById('stat-miskin').textContent = window.appState.data.miskin.length; + + // Count SPBU 24 jam vs tidak + const spbu24 = window.appState.data.spbu.filter(s => s.status === '24jam').length; + const spbuTidak = window.appState.data.spbu.filter(s => s.status === 'tidak').length; + document.getElementById('stat-spbu-trend').textContent = `${spbu24} aktif 24jam`; + + // Count jalan by type + const jalanNasional = window.appState.data.jalan.filter(j => j.status === 'Nasional').length; + document.getElementById('stat-jalan-trend').textContent = `${jalanNasional} Nasional`; +} + +// ============================================ +// Table Rendering +// ============================================ + +function renderTables() { + renderSPBUTable(); + renderJalanTable(); + renderParsilTable(); + renderIbadahTable(); + renderMiskinTable(); +} + +function renderSPBUTable(filter = 'all', search = '') { + const tbody = document.getElementById('table-spbu-body'); + let data = window.appState.data.spbu; + + if (filter !== 'all') { + data = data.filter(item => item.status === filter); + } + + if (search) { + data = data.filter(item => + item.nama.toLowerCase().includes(search.toLowerCase()) || + item.no_spbu.toLowerCase().includes(search.toLowerCase()) + ); + } + + if (data.length === 0) { + tbody.innerHTML = 'Tidak ada data'; + return; + } + + tbody.innerHTML = data.map(item => ` + + ${item.nama} + ${item.no_spbu} + ${item.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'} + ${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)} + +
+ + +
+ + + `).join(''); +} + +function renderJalanTable(filter = 'all', search = '') { + const tbody = document.getElementById('table-jalan-body'); + let data = window.appState.data.jalan; + + if (filter !== 'all') { + data = data.filter(item => item.status === filter); + } + + if (search) { + data = data.filter(item => item.nama_jalan.toLowerCase().includes(search.toLowerCase())); + } + + if (data.length === 0) { + tbody.innerHTML = 'Tidak ada data'; + return; + } + + tbody.innerHTML = data.map(item => ` + + ${item.nama_jalan} + ${item.status} + ${item.panjang.toFixed(0)} m + +
+ + +
+ + + `).join(''); +} + +function renderParsilTable(filter = 'all', search = '') { + const tbody = document.getElementById('table-parsil-body'); + let data = window.appState.data.parsil; + + if (filter !== 'all') { + data = data.filter(item => item.status === filter); + } + + if (search) { + data = data.filter(item => item.nama_pemilik.toLowerCase().includes(search.toLowerCase())); + } + + if (data.length === 0) { + tbody.innerHTML = 'Tidak ada data'; + return; + } + + tbody.innerHTML = data.map(item => ` + + ${item.nama_pemilik} + ${item.status} + ${item.luas.toFixed(0)} m2 + +
+ + +
+ + + `).join(''); +} + +function renderIbadahTable(search = '') { + const tbody = document.getElementById('table-ibadah-body'); + let data = window.appState.data.ibadah; + + if (search) { + data = data.filter(item => + item.nama.toLowerCase().includes(search.toLowerCase()) || + item.alamat.toLowerCase().includes(search.toLowerCase()) + ); + } + + if (data.length === 0) { + tbody.innerHTML = 'Tidak ada data'; + return; + } + + tbody.innerHTML = data.map(item => ` + + ${item.nama} + ${item.alamat.substring(0, 30)}... + ${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)} + +
+ + +
+ + + `).join(''); +} + +function renderMiskinTable(search = '') { + const tbody = document.getElementById('table-miskin-body'); + let data = window.appState.data.miskin; + + if (search) { + data = data.filter(item => + item.nama_kk.toLowerCase().includes(search.toLowerCase()) + ); + } + + if (data.length === 0) { + tbody.innerHTML = 'Tidak ada data'; + return; + } + + tbody.innerHTML = data.map(item => ` + + ${item.nama_kk} + ${item.keterangan || '-'} + ${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)} + +
+ + +
+ + + `).join(''); +} + +// ============================================ +// CRUD Operations - SPBU +// ============================================ + +function openAddSPBUModal() { + document.getElementById('form-spbu-id').value = ''; + document.getElementById('form-spbu-nama').value = ''; + document.getElementById('form-spbu-no').value = ''; + document.getElementById('form-spbu-status').value = ''; + document.getElementById('form-spbu-lat').value = ''; + document.getElementById('form-spbu-lng').value = ''; + document.getElementById('modal-spbu-title').textContent = 'Add SPBU'; + window.appState.editingId = null; + window.appState.editingType = 'spbu'; + showModal('modal-spbu'); +} + +async function editSPBU(id) { + const item = window.appState.data.spbu.find(s => s.id == id); + if (!item) return; + + document.getElementById('form-spbu-id').value = item.id; + document.getElementById('form-spbu-nama').value = item.nama; + document.getElementById('form-spbu-no').value = item.no_spbu; + document.getElementById('form-spbu-status').value = item.status; + document.getElementById('form-spbu-lat').value = item.latitude; + document.getElementById('form-spbu-lng').value = item.longitude; + document.getElementById('modal-spbu-title').textContent = 'Edit SPBU'; + window.appState.editingId = id; + window.appState.editingType = 'spbu'; + showModal('modal-spbu'); +} + +async function saveSPBU() { + const id = document.getElementById('form-spbu-id').value; + const nama = document.getElementById('form-spbu-nama').value; + const no_spbu = document.getElementById('form-spbu-no').value; + const status = document.getElementById('form-spbu-status').value; + const latitude = parseFloat(document.getElementById('form-spbu-lat').value); + const longitude = parseFloat(document.getElementById('form-spbu-lng').value); + + if (!nama || !no_spbu || !status || !latitude || !longitude) { + showToast('Silakan isi semua field', 'error'); + return; + } + + try { + const payload = { nama, no_spbu, status, latitude, longitude }; + const endpoint = id ? API.SPBU.UPDATE : API.SPBU.CREATE; + if (id) payload.id = id; + + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const result = await res.json(); + if (result.success) { + showToast(id ? 'SPBU updated successfully' : 'SPBU created successfully', 'success'); + hideModal('modal-spbu'); + loadAllData(); + } else { + showToast(result.message || 'Error saving SPBU', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error saving SPBU', 'error'); + } +} + +async function deleteSPBU(id) { + if (!confirm('Are you sure you want to delete this SPBU?')) return; + + try { + const res = await fetch(API.SPBU.DELETE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }); + + const result = await res.json(); + if (result.success) { + showToast('SPBU deleted successfully', 'success'); + loadAllData(); + } else { + showToast(result.message || 'Error deleting SPBU', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error deleting SPBU', 'error'); + } +} + +// ============================================ +// CRUD Operations - JALAN +// ============================================ + +function openAddJalanModal() { + document.getElementById('form-jalan-id').value = ''; + document.getElementById('form-jalan-nama').value = ''; + document.getElementById('form-jalan-status').value = ''; + document.getElementById('form-jalan-panjang').value = ''; + document.getElementById('form-jalan-geom').value = ''; + document.getElementById('modal-jalan-title').textContent = 'Add Jalan'; + showModal('modal-jalan'); +} + +async function editJalan(id) { + const item = window.appState.data.jalan.find(j => j.id == id); + if (!item) return; + + document.getElementById('form-jalan-id').value = item.id; + document.getElementById('form-jalan-nama').value = item.nama_jalan; + document.getElementById('form-jalan-status').value = item.status; + document.getElementById('form-jalan-panjang').value = item.panjang; + document.getElementById('form-jalan-geom').value = item.geom; + document.getElementById('modal-jalan-title').textContent = 'Edit Jalan'; + showModal('modal-jalan'); +} + +async function saveJalan() { + const id = document.getElementById('form-jalan-id').value; + const nama_jalan = document.getElementById('form-jalan-nama').value; + const status = document.getElementById('form-jalan-status').value; + const panjang = parseFloat(document.getElementById('form-jalan-panjang').value); + const geom = document.getElementById('form-jalan-geom').value; + + if (!nama_jalan || !status || !panjang || !geom) { + showToast('Silakan isi semua field', 'error'); + return; + } + + try { + const payload = { nama_jalan, status, panjang, geom }; + const endpoint = id ? API.JALAN.UPDATE : API.JALAN.CREATE; + if (id) payload.id = id; + + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const result = await res.json(); + if (result.success) { + showToast(id ? 'Jalan updated successfully' : 'Jalan created successfully', 'success'); + hideModal('modal-jalan'); + loadAllData(); + } else { + showToast(result.message || 'Error saving Jalan', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error saving Jalan', 'error'); + } +} + +async function deleteJalan(id) { + if (!confirm('Are you sure you want to delete this Jalan?')) return; + + try { + const res = await fetch(API.JALAN.DELETE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }); + + const result = await res.json(); + if (result.success) { + showToast('Jalan deleted successfully', 'success'); + loadAllData(); + } else { + showToast(result.message || 'Error deleting Jalan', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error deleting Jalan', 'error'); + } +} + +// ============================================ +// CRUD Operations - PARSIL +// ============================================ + +function openAddParsilModal() { + document.getElementById('form-parsil-id').value = ''; + document.getElementById('form-parsil-nama').value = ''; + document.getElementById('form-parsil-status').value = ''; + document.getElementById('form-parsil-luas').value = ''; + document.getElementById('form-parsil-geom').value = ''; + document.getElementById('modal-parsil-title').textContent = 'Add Parsil'; + showModal('modal-parsil'); +} + +async function editParsil(id) { + const item = window.appState.data.parsil.find(p => p.id == id); + if (!item) return; + + document.getElementById('form-parsil-id').value = item.id; + document.getElementById('form-parsil-nama').value = item.nama_pemilik; + document.getElementById('form-parsil-status').value = item.status; + document.getElementById('form-parsil-luas').value = item.luas; + document.getElementById('form-parsil-geom').value = item.geom; + document.getElementById('modal-parsil-title').textContent = 'Edit Parsil'; + showModal('modal-parsil'); +} + +async function saveParsil() { + const id = document.getElementById('form-parsil-id').value; + const nama_pemilik = document.getElementById('form-parsil-nama').value; + const status = document.getElementById('form-parsil-status').value; + const luas = parseFloat(document.getElementById('form-parsil-luas').value); + const geom = document.getElementById('form-parsil-geom').value; + + if (!nama_pemilik || !status || !luas || !geom) { + showToast('Silakan isi semua field', 'error'); + return; + } + + try { + const payload = { nama_pemilik, status, luas, geom }; + const endpoint = id ? API.PARSIL.UPDATE : API.PARSIL.CREATE; + if (id) payload.id = id; + + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const result = await res.json(); + if (result.success) { + showToast(id ? 'Parsil updated successfully' : 'Parsil created successfully', 'success'); + hideModal('modal-parsil'); + loadAllData(); + } else { + showToast(result.message || 'Error saving Parsil', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error saving Parsil', 'error'); + } +} + +async function deleteParsil(id) { + if (!confirm('Are you sure you want to delete this Parsil?')) return; + + try { + const res = await fetch(API.PARSIL.DELETE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }); + + const result = await res.json(); + if (result.success) { + showToast('Parsil deleted successfully', 'success'); + loadAllData(); + } else { + showToast(result.message || 'Error deleting Parsil', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error deleting Parsil', 'error'); + } +} + +// ============================================ +// CRUD Operations - IBADAH +// ============================================ + +function openAddIbadahModal() { + document.getElementById('form-ibadah-id').value = ''; + document.getElementById('form-ibadah-nama').value = ''; + document.getElementById('form-ibadah-alamat').value = ''; + document.getElementById('form-ibadah-lat').value = ''; + document.getElementById('form-ibadah-lng').value = ''; + document.getElementById('modal-ibadah-title').textContent = 'Add Rumah Ibadah'; + showModal('modal-ibadah'); +} + +async function editIbadah(id) { + const item = window.appState.data.ibadah.find(i => i.id == id); + if (!item) return; + + document.getElementById('form-ibadah-id').value = item.id; + document.getElementById('form-ibadah-nama').value = item.nama; + document.getElementById('form-ibadah-alamat').value = item.alamat; + document.getElementById('form-ibadah-lat').value = item.latitude; + document.getElementById('form-ibadah-lng').value = item.longitude; + document.getElementById('modal-ibadah-title').textContent = 'Edit Rumah Ibadah'; + showModal('modal-ibadah'); +} + +async function saveIbadah() { + const id = document.getElementById('form-ibadah-id').value; + const nama = document.getElementById('form-ibadah-nama').value; + const alamat = document.getElementById('form-ibadah-alamat').value; + const latitude = parseFloat(document.getElementById('form-ibadah-lat').value); + const longitude = parseFloat(document.getElementById('form-ibadah-lng').value); + + if (!nama || !alamat || !latitude || !longitude) { + showToast('Silakan isi semua field', 'error'); + return; + } + + try { + const payload = { nama, alamat, latitude, longitude }; + const endpoint = id ? API.IBADAH.UPDATE : API.IBADAH.CREATE; + if (id) payload.id = id; + + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const result = await res.json(); + if (result.success) { + showToast(id ? 'Ibadah updated successfully' : 'Ibadah created successfully', 'success'); + hideModal('modal-ibadah'); + loadAllData(); + } else { + showToast(result.message || 'Error saving Ibadah', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error saving Ibadah', 'error'); + } +} + +async function deleteIbadah(id) { + if (!confirm('Are you sure you want to delete this Rumah Ibadah?')) return; + + try { + const res = await fetch(API.IBADAH.DELETE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }); + + const result = await res.json(); + if (result.success) { + showToast('Ibadah deleted successfully', 'success'); + loadAllData(); + } else { + showToast(result.message || 'Error deleting Ibadah', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error deleting Ibadah', 'error'); + } +} + +// ============================================ +// CRUD Operations - MISKIN +// ============================================ + +function openAddMiskinModal() { + document.getElementById('form-miskin-id').value = ''; + document.getElementById('form-miskin-nama').value = ''; + document.getElementById('form-miskin-ket').value = ''; + document.getElementById('form-miskin-lat').value = ''; + document.getElementById('form-miskin-lng').value = ''; + document.getElementById('modal-miskin-title').textContent = 'Add Penduduk Miskin'; + showModal('modal-miskin'); +} + +async function editMiskin(id) { + const item = window.appState.data.miskin.find(m => m.id == id); + if (!item) return; + + document.getElementById('form-miskin-id').value = item.id; + document.getElementById('form-miskin-nama').value = item.nama_kk; + document.getElementById('form-miskin-ket').value = item.keterangan || ''; + document.getElementById('form-miskin-lat').value = item.latitude; + document.getElementById('form-miskin-lng').value = item.longitude; + document.getElementById('modal-miskin-title').textContent = 'Edit Penduduk Miskin'; + showModal('modal-miskin'); +} + +async function saveMiskin() { + const id = document.getElementById('form-miskin-id').value; + const nama_kk = document.getElementById('form-miskin-nama').value; + const keterangan = document.getElementById('form-miskin-ket').value; + const latitude = parseFloat(document.getElementById('form-miskin-lat').value); + const longitude = parseFloat(document.getElementById('form-miskin-lng').value); + + if (!nama_kk || !latitude || !longitude) { + showToast('Silakan isi field yang diperlukan', 'error'); + return; + } + + try { + const payload = { nama_kk, keterangan, latitude, longitude }; + const endpoint = id ? API.MISKIN.UPDATE : API.MISKIN.CREATE; + if (id) payload.id = id; + + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const result = await res.json(); + if (result.success) { + showToast(id ? 'Miskin updated successfully' : 'Miskin created successfully', 'success'); + hideModal('modal-miskin'); + loadAllData(); + } else { + showToast(result.message || 'Error saving Miskin', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error saving Miskin', 'error'); + } +} + +async function deleteMiskin(id) { + if (!confirm('Are you sure you want to delete this Penduduk Miskin?')) return; + + try { + const res = await fetch(API.MISKIN.DELETE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }); + + const result = await res.json(); + if (result.success) { + showToast('Miskin deleted successfully', 'success'); + loadAllData(); + } else { + showToast(result.message || 'Error deleting Miskin', 'error'); + } + } catch (err) { + console.error('Error:', err); + showToast('Error deleting Miskin', 'error'); + } +} + +// ============================================ +// Map Initialization & Rendering +// ============================================ + +function initMap() { + const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap' + }); + + const map = L.map('map', { + center: [-0.0260, 109.3425], + zoom: 13, + zoomControl: true, + layers: [osm] + }); + + const baseMaps = { + "Standard (OSM)": osm, + "Dark": L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 }) + }; + + L.control.layers(baseMaps).addTo(map); + + // Initialize Geoman Controls (only if Geoman library is loaded) + if (map.pm && typeof map.pm.addControls === 'function') { + map.pm.addControls({ + position: 'topleft', + drawCircle: false, + drawCircleMarker: false, + drawText: false, + editMode: false, + dragMode: false, + cutPolygon: false, + removalMode: false + }); + } + + // ALWAYS set mapInstance — regardless of whether Geoman loaded + window.appState.mapInstance = map; + window.appState.layers = { + spbu24: L.layerGroup().addTo(map), + spbuTidak: L.layerGroup().addTo(map), + jalan: L.layerGroup().addTo(map), + parsil: L.layerGroup().addTo(map), + ibadah: L.layerGroup().addTo(map), + miskin: L.layerGroup().addTo(map) + }; + + // Map click handler — capture coordinates for toolbar point mode + map.on('click', function(e) { + if (!window._mapClickMode) return; + const lat = e.latlng.lat.toFixed(7); + const lng = e.latlng.lng.toFixed(7); + const mode = window._mapClickMode; + window.mapToolCancel(); // reset mode + if (mode === 'spbu') { + openAddSPBUModal(); + setTimeout(function() { + var el1 = document.getElementById('form-spbu-lat'); + var el2 = document.getElementById('form-spbu-lng'); + if (el1) el1.value = lat; + if (el2) el2.value = lng; + }, 80); + } else if (mode === 'ibadah') { + openAddIbadahModal(); + setTimeout(function() { + var el1 = document.getElementById('form-ibadah-lat'); + var el2 = document.getElementById('form-ibadah-lng'); + if (el1) el1.value = lat; + if (el2) el2.value = lng; + }, 80); + } else if (mode === 'miskin') { + openAddMiskinModal(); + setTimeout(function() { + var el1 = document.getElementById('form-miskin-lat'); + var el2 = document.getElementById('form-miskin-lng'); + if (el1) el1.value = lat; + if (el2) el2.value = lng; + }, 80); + } + }); + + // Geoman pm:create handler (for line/polygon) + if (map.pm) { + map.on('pm:create', function(e) { + var layer = e.layer; + var shape = e.shape; + window.mapToolCancel(); + if (shape === 'Line' || shape === 'Polyline') { + openAddJalanModal(); + var gj = layer.toGeoJSON(); + var geomEl = document.getElementById('form-jalan-geom'); + if (geomEl) geomEl.value = JSON.stringify(gj.geometry); + var panjEl = document.getElementById('form-jalan-panjang'); + if (panjEl && window.turf) panjEl.value = Math.round(turf.length(gj, { units: 'kilometers' }) * 1000); + } else if (shape === 'Polygon' || shape === 'Rectangle') { + openAddParsilModal(); + var gj = layer.toGeoJSON(); + var geomEl = document.getElementById('form-parsil-geom'); + if (geomEl) geomEl.value = JSON.stringify(gj.geometry); + var luasEl = document.getElementById('form-parsil-luas'); + if (luasEl && window.turf) luasEl.value = Math.round(turf.area(gj)); + } + map.removeLayer(layer); + }); + } + + return map; +} + +function renderMapLayers() { + const map = window.appState.mapInstance; + if (!map) return; + + // Clear layers + Object.values(window.appState.layers).forEach(layer => layer.clearLayers()); + + // Render SPBU + window.appState.data.spbu.forEach(item => { + const color = item.status === '24jam' ? '#22c55e' : '#f59e0b'; + const marker = L.circleMarker([item.latitude, item.longitude], { + radius: 6, + fillColor: color, + color: '#fff', + weight: 2, + opacity: 1, + fillOpacity: 0.8 + }).bindPopup(`${item.nama}
Status: ${item.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'}`); + + const layer = item.status === '24jam' ? window.appState.layers.spbu24 : window.appState.layers.spbuTidak; + marker.addTo(layer); + }); + + // Render Jalan + window.appState.data.jalan.forEach(item => { + try { + const geom = JSON.parse(item.geom); + if (geom.coordinates) { + const colorMap = { 'Nasional': '#ef4444', 'Provinsi': '#eab308', 'Kabupaten': '#10b981' }; + L.polyline(geom.coordinates, { + color: colorMap[item.status] || '#3b82f6', + weight: 3, + opacity: 0.7 + }).bindPopup(`${item.nama_jalan}
Status: ${item.status}
Panjang: ${item.panjang.toFixed(0)}m`).addTo(window.appState.layers.jalan); + } + } catch (e) {} + }); + + // Render Parsil + window.appState.data.parsil.forEach(item => { + try { + const geom = JSON.parse(item.geom); + if (geom.coordinates) { + const colorMap = { 'SHM': '#3b82f6', 'HGB': '#8b5cf6', 'HGU': '#ec4899', 'HP': '#14b8a6' }; + L.polygon(geom.coordinates, { + color: colorMap[item.status] || '#3b82f6', + weight: 2, + opacity: 0.6, + fillOpacity: 0.4 + }).bindPopup(`${item.nama_pemilik}
Status: ${item.status}
Luas: ${item.luas.toFixed(0)}m2`).addTo(window.appState.layers.parsil); + } + } catch (e) {} + }); + + // Render Ibadah + window.appState.data.ibadah.forEach(item => { + const marker = L.circleMarker([item.latitude, item.longitude], { + radius: 5, + fillColor: '#8b5cf6', + color: '#fff', + weight: 2, + opacity: 1, + fillOpacity: 0.8 + }).bindPopup(`${item.nama}
${item.alamat}`); + marker.addTo(window.appState.layers.ibadah); + }); + + // Render Miskin + window.appState.data.miskin.forEach(item => { + const marker = L.circleMarker([item.latitude, item.longitude], { + radius: 5, + fillColor: '#000000', + color: '#fff', + weight: 2, + opacity: 1, + fillOpacity: 0.8 + }).bindPopup(`${item.nama_kk}
${item.keterangan || ''}`); + marker.addTo(window.appState.layers.miskin); + }); +} + +// ============================================ +// Event Listeners & Initialization +// ============================================ + +document.addEventListener('DOMContentLoaded', () => { + // Initialize map + initMap(); + + // Sidebar navigation + document.querySelectorAll('[data-panel]').forEach(item => { + item.addEventListener('click', () => { + const panelId = item.dataset.panel; + switchPanel(panelId); + }); + }); + + // Sidebar toggle + document.getElementById('sidebar-toggle').addEventListener('click', () => { + document.querySelector('.sidebar').classList.toggle('collapsed'); + }); + + // Modal close buttons + document.querySelectorAll('[data-modal]').forEach(btn => { + btn.addEventListener('click', () => { + const modalId = btn.dataset.modal; + hideModal(modalId); + }); + }); + + // Modal overlay close + document.querySelectorAll('.modal-overlay').forEach(overlay => { + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + hideModal(overlay.id); + } + }); + }); + + // Add buttons + document.getElementById('btn-add-spbu').addEventListener('click', openAddSPBUModal); + document.getElementById('btn-add-jalan').addEventListener('click', openAddJalanModal); + document.getElementById('btn-add-parsil').addEventListener('click', openAddParsilModal); + document.getElementById('btn-add-ibadah').addEventListener('click', openAddIbadahModal); + document.getElementById('btn-add-miskin').addEventListener('click', openAddMiskinModal); + + // Save buttons + document.getElementById('btn-save-spbu').addEventListener('click', saveSPBU); + document.getElementById('btn-save-jalan').addEventListener('click', saveJalan); + document.getElementById('btn-save-parsil').addEventListener('click', saveParsil); + document.getElementById('btn-save-ibadah').addEventListener('click', saveIbadah); + document.getElementById('btn-save-miskin').addEventListener('click', saveMiskin); + + // Search and filter - SPBU + document.getElementById('search-spbu').addEventListener('input', (e) => { + renderSPBUTable('all', e.target.value); + }); + + document.querySelectorAll('#panel-spbu .filter-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('#panel-spbu .filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const search = document.getElementById('search-spbu').value; + renderSPBUTable(btn.dataset.filter, search); + }); + }); + + // Search and filter - JALAN + document.getElementById('search-jalan').addEventListener('input', (e) => { + renderJalanTable('all', e.target.value); + }); + + document.querySelectorAll('#panel-jalan .filter-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('#panel-jalan .filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const search = document.getElementById('search-jalan').value; + renderJalanTable(btn.dataset.filter, search); + }); + }); + + // Search and filter - PARSIL + document.getElementById('search-parsil').addEventListener('input', (e) => { + renderParsilTable('all', e.target.value); + }); + + document.querySelectorAll('#panel-parsil .filter-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('#panel-parsil .filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const search = document.getElementById('search-parsil').value; + renderParsilTable(btn.dataset.filter, search); + }); + }); + + // Search - IBADAH + document.getElementById('search-ibadah').addEventListener('input', (e) => { + renderIbadahTable(e.target.value); + }); + + // Search - MISKIN + document.getElementById('search-miskin').addEventListener('input', (e) => { + renderMiskinTable(e.target.value); + }); + + // Load initial data + loadAllData(); + + showToast('Welcome to WebGIS SPBU System', 'info'); +}); diff --git a/js/map.js b/js/map.js new file mode 100644 index 0000000..fb677dd --- /dev/null +++ b/js/map.js @@ -0,0 +1,882 @@ +// ============================================ +// map.js — Logic Peta WebGIS Terpadu +// ============================================ + +// ------- Konfigurasi endpoint backend ------- +const API_READ_SPBU = 'backend/read.php'; +const API_CREATE_SPBU = 'backend/create.php'; +const API_READ_JALAN = 'backend/read_jalan.php'; +const API_CREATE_JALAN= 'backend/create_jalan.php'; +const API_READ_PARSIL = 'backend/read_parsil.php'; +const API_CREATE_PARSIL='backend/create_parsil.php'; +const API_READ_IBADAH = 'backend/read_ibadah.php'; +const API_CREATE_IBADAH='backend/create_ibadah.php'; +const API_READ_MISKIN = 'backend/read_miskin.php'; +const API_CREATE_MISKIN='backend/create_miskin.php'; + +// ------- State Internal & Data Storage ------- +window.appData = { + spbu: [], + jalan: [], + parsil: [], + ibadah: [], + miskin: [] +}; + +// ------- Inisialisasi Basemap ------- +const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, attribution: '© OpenStreetMap' +}); + +const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + maxZoom: 19, attribution: 'Tiles © Esri' +}); + +const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + maxZoom: 19, attribution: '© CartoDB' +}); + +// ------- Inisialisasi Peta ------- +const map = L.map('map', { + center: [-0.0260, 109.3425], // Pusat Kota Pontianak + zoom: 13, + zoomControl: true, + layers: [osm] // Default basemap +}); + +// Tambah Basemap & Overlay Switcher (Layer Control) +const baseMaps = { + "Peta Standar (OSM)": osm, + "Citra Satelit": satellite, + "Peta Gelap": dark +}; + +// Inisialisasi Layer Groups +const layerSpbu24 = L.layerGroup().addTo(map); +const layerSpbuTidak = L.layerGroup().addTo(map); +const layerJalan = L.layerGroup().addTo(map); +const layerParsil = L.layerGroup().addTo(map); +const layerIbadah = L.layerGroup().addTo(map); +const layerMiskin = L.layerGroup().addTo(map); + +const overlays = { + "SPBU Buka 24 Jam": layerSpbu24, + "SPBU Tidak 24 Jam": layerSpbuTidak, + "Informasi Jalan": layerJalan, + "Parsil Tanah": layerParsil, + "Rumah Ibadah": layerIbadah, + "Penduduk Miskin": layerMiskin +}; + +L.control.layers(baseMaps, overlays, { position: 'bottomleft' }).addTo(map); + +// ------- Tombol Geolocation (Lokasi Saya) ------- +const LocateControl = L.Control.extend({ + options: { position: 'topleft' }, + onAdd: function (map) { + const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control'); + container.style.backgroundColor = 'white'; + container.style.width = '34px'; + container.style.height = '34px'; + container.style.cursor = 'pointer'; + container.style.display = 'flex'; + container.style.justifyContent = 'center'; + container.style.alignItems = 'center'; + container.innerHTML = '📍'; + + container.onclick = function(e) { + e.stopPropagation(); + showToast('Mencari lokasi...', 'success'); + map.locate({ setView: true, maxZoom: 17, enableHighAccuracy: true }); + } + return container; + } +}); +map.addControl(new LocateControl()); + +let gpsMarker = null; +let gpsPulse = null; + +map.on('locationfound', function(e) { + if (gpsMarker) map.removeLayer(gpsMarker); + if (gpsPulse) map.removeLayer(gpsPulse); + + const pulseIcon = L.divIcon({ className: 'gps-pulse', iconSize: [40, 40] }); + const dotIcon = L.divIcon({ className: 'gps-marker', iconSize: [16, 16] }); + + gpsPulse = L.marker(e.latlng, { icon: pulseIcon }).addTo(map); + gpsMarker = L.marker(e.latlng, { icon: dotIcon }).addTo(map); + showToast('Lokasi ditemukan!', 'success'); +}); + +map.on('locationerror', function(e) { + showToast('Tidak dapat mengakses GPS.', 'error'); +}); + +// ------- Mode State ------- +let currentMode = 'spbu'; // 'spbu', 'jalan', 'parsil', 'ibadah', 'miskin' +let inputPopup = null; +let currentDrawnLayer = null; // layer temp saat menggambar + +// Inisialisasi Leaflet-Geoman +map.pm.addControls({ + position: 'topleft', + drawMarker: false, + drawCircleMarker: false, + drawPolyline: false, + drawRectangle: false, + drawPolygon: false, + drawCircle: false, + drawText: false, + editMode: false, + dragMode: false, + cutPolygon: false, + removalMode: true +}); + +// Transisi Mode UI +const btnSpbu = document.getElementById('btn-mode-spbu'); +const btnJalan = document.getElementById('btn-mode-jalan'); +const btnParsil = document.getElementById('btn-mode-parsil'); +const btnIbadah = document.getElementById('btn-mode-ibadah'); +const btnMiskin = document.getElementById('btn-mode-miskin'); +const hintBar = document.getElementById('hint-bar'); + +function switchMode(mode) { + currentMode = mode; + btnSpbu.classList.toggle('active', mode === 'spbu'); + btnJalan.classList.toggle('active', mode === 'jalan'); + btnParsil.classList.toggle('active', mode === 'parsil'); + btnIbadah.classList.toggle('active', mode === 'ibadah'); + btnMiskin.classList.toggle('active', mode === 'miskin'); + + // Matikan PM Draw + map.pm.disableDraw(); + if (inputPopup) map.closePopup(inputPopup); + if (currentDrawnLayer) { + map.removeLayer(currentDrawnLayer); + currentDrawnLayer = null; + } + + if (mode === 'spbu') { + hintBar.innerHTML = 'Klik pada peta untuk menambahkan SPBU baru'; + } else if (mode === 'ibadah') { + hintBar.innerHTML = 'Klik pada peta untuk menambahkan Rumah Ibadah'; + } else if (mode === 'miskin') { + hintBar.innerHTML = 'Klik pada peta untuk menambahkan Penduduk Miskin'; + } else if (mode === 'jalan') { + hintBar.innerHTML = 'Klik start lalu gambar garis jalan, klik titik terakhir untuk selesai'; + map.pm.enableDraw('Line', { snappable: true, snapDistance: 20, pathOptions: { color: '#3b82f6', weight: 4 } }); + } else if (mode === 'parsil') { + hintBar.innerHTML = 'Klik start lalu gambar poligon, klik titik awal untuk selesai'; + map.pm.enableDraw('Polygon', { snappable: true, snapDistance: 20, pathOptions: { color: '#3b82f6', weight: 2, fillColor: '#3b82f6', fillOpacity: 0.4 } }); + } +} + +btnSpbu.addEventListener('click', () => switchMode('spbu')); +btnJalan.addEventListener('click', () => switchMode('jalan')); +btnParsil.addEventListener('click', () => switchMode('parsil')); +btnIbadah.addEventListener('click', () => switchMode('ibadah')); +btnMiskin.addEventListener('click', () => switchMode('miskin')); + + +// ============================================ +// SPBU LOGIC +// ============================================ +let totalSpbu = 0; +function updateCounter(count) { + totalSpbu = count; + document.getElementById('spbu-count').textContent = totalSpbu; +} + +function createSpbuIcon(status) { + const color = status === '24jam' ? '#22c55e' : '#f59e0b'; + const svg = ` + + SPBU + `; + return L.divIcon({ html: svg, className: '', iconSize: [36, 48], iconAnchor: [18, 48], popupAnchor: [0, -50] }); +} + +function spbuBadge(status) { + return status === '24jam' + ? '24 Jam' + : 'Tidak 24 Jam'; +} + +function buildSpbuPopup(d) { + return `
+
${escapeHtml(d.nama)}
+ + + + +
No. SPBU${escapeHtml(d.no_spbu)}
Status${spbuBadge(d.status)}
Lat/Lng${d.latitude}, ${d.longitude}
+
`; +} + +function addSpbuToMap(d) { + const marker = L.marker([d.latitude, d.longitude], { icon: createSpbuIcon(d.status) }); + marker.bindPopup(buildSpbuPopup(d), { maxWidth: 260 }); + + if (d.status === '24jam') { + marker.addTo(layerSpbu24); + } else { + marker.addTo(layerSpbuTidak); + } + + window.appData.spbu.push({ data: d, layer: marker }); +} + +// SPBU, IBADAH, MISKIN Map Click logic +map.on('click', async function (e) { + // Hide search results if clicking map + document.getElementById('search-results').classList.remove('show'); + + if (!['spbu', 'ibadah', 'miskin'].includes(currentMode)) return; // abaikan jika bukan point mode + + if (inputPopup) map.closePopup(inputPopup); + const { lat, lng } = e.latlng; + + let formHtml = ''; + + if (currentMode === 'spbu') { + formHtml = ` +
+
Tambah SPBU Baru
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
`; + } else if (currentMode === 'ibadah') { + showToast('Mendapatkan alamat otomatis...', 'success'); + let autoAddress = ''; + try { + const resp = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`); + const jData = await resp.json(); + if(jData && jData.display_name) autoAddress = jData.display_name; + } catch(err) { console.error(err); } + + formHtml = ` +
+
Tambah Rumah Ibadah
+
+
+ + +
+
+ + +
+
+ + +
+
+
`; + } else if (currentMode === 'miskin') { + formHtml = ` +
+
Tambah Penduduk Miskin
+
+
+ + +
+
+ + +
+
+ + +
+
+
`; + } + + inputPopup = L.popup({ maxWidth: 320, closeButton: false }) + .setLatLng(e.latlng).setContent(formHtml).openOn(map); + + setTimeout(() => { + document.getElementById('btn-cancel')?.addEventListener('click', () => map.closePopup(inputPopup)); + + // SPBU Handler + document.getElementById('form-spbu')?.addEventListener('submit', async (ev) => { + ev.preventDefault(); + const payload = { + nama: document.getElementById('f-nama').value.trim(), + no_spbu: document.getElementById('f-no').value.trim(), + status: document.getElementById('f-status').value, + latitude: lat, longitude: lng + }; + if(!payload.nama || !payload.no_spbu) return; + document.getElementById('btn-save').disabled = true; + try { + const res = await fetch(API_CREATE_SPBU, { method: 'POST', body: JSON.stringify(payload) }); + const json = await res.json(); + if (json.success) { + map.closePopup(inputPopup); + addSpbuToMap(json.data); + updateCounter(totalSpbu + 1); + showToast('SPBU berhasil ditambahkan!'); + } else showToast(json.message, 'error'); + } catch (err) { showToast('Error server', 'error'); } + }); + + // Ibadah Handler + document.getElementById('form-ibadah')?.addEventListener('submit', async (ev) => { + ev.preventDefault(); + const payload = { nama: document.getElementById('fi-nama').value.trim(), alamat: document.getElementById('fi-alamat').value.trim(), latitude: lat, longitude: lng }; + if(!payload.nama || !payload.alamat) return; + document.getElementById('btn-save').disabled = true; + try { + const res = await fetch(API_CREATE_IBADAH, { method: 'POST', body: JSON.stringify(payload) }); + const json = await res.json(); + if (json.success) { + map.closePopup(inputPopup); addIbadahToMap(json.data); showToast('Rumah Ibadah ditambahkan!'); + } else showToast(json.message, 'error'); + } catch (err) { showToast('Error server', 'error'); } + }); + + // Miskin Handler + document.getElementById('form-miskin')?.addEventListener('submit', async (ev) => { + ev.preventDefault(); + const payload = { nama_kk: document.getElementById('fm-nama').value.trim(), keterangan: document.getElementById('fm-ket').value.trim(), latitude: lat, longitude: lng }; + if(!payload.nama_kk) return; + document.getElementById('btn-save').disabled = true; + try { + const res = await fetch(API_CREATE_MISKIN, { method: 'POST', body: JSON.stringify(payload) }); + const json = await res.json(); + if (json.success) { + map.closePopup(inputPopup); addMiskinToMap(json.data); showToast('Data keluarga ditambahkan!'); + } else showToast(json.message, 'error'); + } catch (err) { showToast('Error server', 'error'); } + }); + }, 0); +}); + +// ============================================ +// IBADAH & MISKIN LOGIC (Radius Analysis) +// ============================================ + +function createIbadahIcon() { + const svg = ` + + 🕌 + `; + return L.divIcon({ html: svg, className: '', iconSize: [36, 48], iconAnchor: [18, 48], popupAnchor: [0, -48] }); +} + +let activeRadiusCircle = null; + +window.updateRadius = function(id, radius, lat, lng) { + document.getElementById(`radius-val-${id}`).innerText = `${radius} Meter`; + + if (activeRadiusCircle) map.removeLayer(activeRadiusCircle); + activeRadiusCircle = L.circle([lat, lng], { + color: '#8b5cf6', + fillColor: '#8b5cf6', + fillOpacity: 0.2, + radius: parseInt(radius) + }).addTo(map); + + let caughtCount = 0; + const centerPoint = turf.point([lng, lat]); + + window.appData.miskin.forEach(m => { + const targetPoint = turf.point([m.data.longitude, m.data.latitude]); + const distance = turf.distance(centerPoint, targetPoint, { units: 'meters' }); + + if (distance <= radius) { + caughtCount++; + // Set point menjadi merah + m.layer.setStyle({ fillColor: '#ef4444', color: '#ef4444', radius: 8 }); + } else { + // Kembali menjadi hitam + m.layer.setStyle({ fillColor: '#000000', color: '#000000', radius: 6 }); + } + }); + + document.getElementById(`radius-res-${id}`).innerHTML = `${caughtCount} rumah keluarga miskin dalam radius.`; +}; + +// When popup closes, remove radius circle +map.on('popupclose', () => { + if (activeRadiusCircle) { + map.removeLayer(activeRadiusCircle); + activeRadiusCircle = null; + window.appData.miskin.forEach(m => { + m.layer.setStyle({ fillColor: '#000000', color: '#000000', radius: 6 }); + }); + } +}); + +function buildIbadahPopup(d) { + return `
+
${escapeHtml(d.nama)}
+ + +
Alamat${escapeHtml(d.alamat)}
+
+ + +
+ 0 Meter + Geser slider +
+
+
`; +} + +function buildMiskinPopup(d) { + return `
+
Kepala Keluarga: ${escapeHtml(d.nama_kk)}
+ + +
Keterangan${escapeHtml(d.keterangan)}
+
`; +} + +function addIbadahToMap(d) { + const marker = L.marker([d.latitude, d.longitude], { icon: createIbadahIcon() }); + marker.bindPopup(buildIbadahPopup(d), { maxWidth: 300, minWidth: 260 }); + marker.addTo(layerIbadah); + window.appData.ibadah.push({ data: d, layer: marker }); +} + +function addMiskinToMap(d) { + // Gunakan L.circleMarker sebagai point sederhana (warna bawaan Hitam) + const marker = L.circleMarker([d.latitude, d.longitude], { + radius: 6, + fillColor: "#000000", + color: "#000000", + weight: 2, + opacity: 1, + fillOpacity: 0.8 + }); + marker.bindPopup(buildMiskinPopup(d), { maxWidth: 260 }); + marker.addTo(layerMiskin); + window.appData.miskin.push({ data: d, layer: marker }); +} + + + +// ============================================ +// JALAN & PARSIL DRAW LOGIC +// ============================================ + +// Pemetaan Warna +function getJalanColor(status) { + if(status === 'Nasional') return '#ef4444'; + if(status === 'Provinsi') return '#eab308'; + return '#10b981'; // Kabupaten +} + +function getParsilStyle(status) { + if (status === 'SHM') return { color: '#2563eb', fillColor: '#3b82f6', fillOpacity: 0.5 }; + if (status === 'HGB') return { color: '#7c3aed', fillColor: '#8b5cf6', fillOpacity: 0.5 }; + if (status === 'HGU') return { color: '#db2777', fillColor: '#ec4899', fillOpacity: 0.5 }; + return { color: '#0d9488', fillColor: '#14b8a6', fillOpacity: 0.5 }; // HP +} + +// Leaflet Geoman event saat entitas selesai digambar +map.on('pm:create', (e) => { + currentDrawnLayer = e.layer; + const geom = currentDrawnLayer.toGeoJSON().geometry; + + if (currentMode === 'jalan' && e.shape === 'Line') { + // Hitung panjang dalam meter via Turf.js + const lengthKm = turf.length(currentDrawnLayer.toGeoJSON()); + const panjangMeter = (lengthKm * 1000).toFixed(2); + openJalanForm(e.layer, geom, panjangMeter); + // Refresh mode drawing + map.pm.enableDraw('Line'); + } + else if (currentMode === 'parsil' && e.shape === 'Polygon') { + // Hitung luas dalam meter persegi via Turf.js + const areaSqMeters = turf.area(currentDrawnLayer.toGeoJSON()); + const luasMeter = areaSqMeters.toFixed(2); + openParsilForm(e.layer, geom, luasMeter); + // Refresh mode drawing + map.pm.enableDraw('Polygon'); + } +}); + +function openJalanForm(layer, geom, panjang) { + const centerLatlng = layer.getBounds().getCenter(); + const formHtml = ` +
+
Tambah Data Jalan
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
`; + + inputPopup = L.popup({ maxWidth: 320, closeButton: false }) + .setLatLng(centerLatlng).setContent(formHtml).openOn(map); + + setTimeout(() => { + document.getElementById('btn-cancel-draw')?.addEventListener('click', () => { + map.closePopup(inputPopup); + map.removeLayer(layer); + }); + document.getElementById('form-jalan')?.addEventListener('submit', async (ev) => { + ev.preventDefault(); + const payload = { + nama_jalan: document.getElementById('fj-nama').value.trim(), + status: document.getElementById('fj-status').value, + panjang: panjang, + geom: geom + }; + if(!payload.nama_jalan) return; + document.getElementById('btn-save-draw').disabled = true; + try { + const res = await fetch(API_CREATE_JALAN, { method: 'POST', body: JSON.stringify(payload) }); + const json = await res.json(); + if (json.success) { + map.closePopup(inputPopup); + map.removeLayer(layer); + currentDrawnLayer = null; + addJalanToMap(json.data); + showToast('Jalan berhasil ditambahkan!'); + } else showToast(json.message, 'error'); + } catch (e) { showToast('Error server', 'error'); } + document.getElementById('btn-save-draw').disabled = false; + }); + }, 0); +} + +function openParsilForm(layer, geom, luas) { + const centerLatlng = layer.getBounds().getCenter(); + const formHtml = ` +
+
Tambah Parsil Tanah
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
`; + + inputPopup = L.popup({ maxWidth: 320, closeButton: false }) + .setLatLng(centerLatlng).setContent(formHtml).openOn(map); + + setTimeout(() => { + document.getElementById('btn-cancel-draw')?.addEventListener('click', () => { + map.closePopup(inputPopup); + map.removeLayer(layer); + }); + document.getElementById('form-parsil')?.addEventListener('submit', async (ev) => { + ev.preventDefault(); + const payload = { + nama_pemilik: document.getElementById('fp-nama').value.trim(), + status: document.getElementById('fp-status').value, + luas: luas, + geom: geom + }; + if(!payload.nama_pemilik) return; + document.getElementById('btn-save-draw').disabled = true; + try { + const res = await fetch(API_CREATE_PARSIL, { method: 'POST', body: JSON.stringify(payload) }); + const json = await res.json(); + if (json.success) { + map.closePopup(inputPopup); + map.removeLayer(layer); + currentDrawnLayer = null; + addParsilToMap(json.data); + showToast('Parsil berhasil ditambahkan!'); + } else showToast(json.message, 'error'); + } catch (e) { showToast('Error server', 'error'); } + document.getElementById('btn-save-draw').disabled = false; + }); + }, 0); +} + +// Popups Data Terload +function buildJalanPopup(d) { + return `
+
${escapeHtml(d.nama_jalan)}
+ + + +
Status${d.status}
Panjang${d.panjang} m
+
`; +} + +function buildParsilPopup(d) { + return `
+
Pemilik: ${escapeHtml(d.nama_pemilik)}
+ + + +
Sertifikat${d.status}
Luas${d.luas} m²
+
`; +} + +// Add GeoJSON to Layer +function addJalanToMap(d) { + const layer = L.geoJSON(d.geom, { + style: { color: getJalanColor(d.status), weight: 5, opacity: 0.9 } + }).bindPopup(buildJalanPopup(d)).addTo(layerJalan); + window.appData.jalan.push({ data: d, layer: layer }); +} + +// ------ Fitur Hover Info Parsil ------ +function highlightParsil(e) { + const layer = e.target; + layer.setStyle({ + weight: 4, + color: '#fff', + dashArray: '', + fillOpacity: 0.8 + }); + layer.bringToFront(); +} + +function resetParsilHighlight(e) { + const layer = e.target; + const status = layer.featureProps.status; + layer.setStyle(getParsilStyle(status)); +} + +function addParsilToMap(d) { + const layerGroup = L.geoJSON(d.geom, { + style: getParsilStyle(d.status), + onEachFeature: function(feature, layer) { + layer.featureProps = d; // Simpan properti referensi ke dalam layer + + const tooltipContent = ` +
+

Informasi Parsil Tanah

+ Pemilik: ${escapeHtml(d.nama_pemilik)}
+ Sertifikat: ${d.status}
+ Luas: ${d.luas} m² +
`; + + layer.bindTooltip(tooltipContent, { + sticky: true, // Tooltip bergeser mengekori kursor kursor + direction: 'top', + className: 'custom-tooltip' + }); + + layer.on({ + mouseover: highlightParsil, + mouseout: resetParsilHighlight + }); + } + }); + layerGroup.bindPopup(buildParsilPopup(d)).addTo(layerParsil); + window.appData.parsil.push({ data: d, layer: layerGroup }); +} + + +// ============================================ +// LOGIKA PENCARIAN (SEARCH) +// ============================================ +const searchInput = document.getElementById('search-input'); +const searchResults = document.getElementById('search-results'); +const filterBtns = document.querySelectorAll('.filter-btn'); +let currentSearchFilter = 'all'; + +filterBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + filterBtns.forEach(b => b.classList.remove('active')); + e.target.classList.add('active'); + currentSearchFilter = e.target.dataset.filter; + performSearch(searchInput.value); + }); +}); + +searchInput.addEventListener('input', (e) => performSearch(e.target.value)); +// Tambahkan event focus jika user kembali nge-klik kolom search yg sudah ada isinya +searchInput.addEventListener('focus', (e) => performSearch(e.target.value)); + +function performSearch(query) { + query = query.toLowerCase().trim(); + searchResults.innerHTML = ''; + + if (!query) { + searchResults.classList.remove('show'); + return; + } + + let results = []; + + // Filter SPBU + if (currentSearchFilter === 'all' || currentSearchFilter === 'spbu') { + appData.spbu.forEach(item => { + if (item.data.nama.toLowerCase().includes(query) || item.data.no_spbu.toLowerCase().includes(query)) { + results.push({ type: 'SPBU', title: item.data.nama, meta: `No: ${item.data.no_spbu}`, item: item }); + } + }); + } + + // Filter Jalan + if (currentSearchFilter === 'all' || currentSearchFilter === 'jalan') { + appData.jalan.forEach(item => { + if (item.data.nama_jalan.toLowerCase().includes(query)) { + results.push({ type: 'Jalan', title: item.data.nama_jalan, meta: `${item.data.status} · ${item.data.panjang}m`, item: item }); + } + }); + } + + // Filter Parsil + if (currentSearchFilter === 'all' || currentSearchFilter === 'parsil') { + appData.parsil.forEach(item => { + if (item.data.nama_pemilik.toLowerCase().includes(query)) { + results.push({ type: 'Parsil', title: item.data.nama_pemilik, meta: `Sertifikat: ${item.data.status}`, item: item }); + } + }); + } + + if (results.length === 0) { + searchResults.innerHTML = '
Tidak ada hasil pemetaan
'; + } else { + results.forEach(r => { + const div = document.createElement('div'); + div.className = 'search-result-item'; + div.innerHTML = ` +
${escapeHtml(r.title)}
+
${r.type} ${escapeHtml(r.meta)}
+ `; + div.addEventListener('click', () => zoomToResult(r)); + searchResults.appendChild(div); + }); + } + searchResults.classList.add('show'); +} + +function zoomToResult(result) { + searchResults.classList.remove('show'); + searchInput.value = ''; // Opsional: clear search box saat dipilih + const layer = result.item.layer; + + if (result.type === 'SPBU') { + // FlyTo SPBU marker + map.flyTo(layer.getLatLng(), 18, { duration: 1.5 }); + // Buka popup saat flyTo selesai (mendekati target) + setTimeout(() => layer.openPopup(), 1000); + } else { + // GeoJSON Layers berbentuk grup (bisa lebih dari satu poligon/garis) + const bounds = layer.getBounds(); + map.flyToBounds(bounds, { duration: 1.5, maxZoom: 18 }); + setTimeout(() => layer.openPopup(), 1000); + } +} + + +// ============================================ +// DATA LOADER +// ============================================ +async function loadAllData() { + try { + // Load SPBU + const r1 = await fetch(API_READ_SPBU); + const j1 = await r1.json(); + if(j1.success) { j1.data.forEach(d => addSpbuToMap(d)); updateCounter(j1.count); } + + // Load Jalan + const r2 = await fetch(API_READ_JALAN); + const j2 = await r2.json(); + if(j2.success) j2.data.forEach(d => addJalanToMap(d)); + + // Load Parsil + const r3 = await fetch(API_READ_PARSIL); + const j3 = await r3.json(); + if(j3.success) j3.data.forEach(d => addParsilToMap(d)); + + // Load Ibadah + const r4 = await fetch(API_READ_IBADAH); + const j4 = await r4.json(); + if(j4.success) j4.data.forEach(d => addIbadahToMap(d)); + + // Load Miskin + const r5 = await fetch(API_READ_MISKIN); + const j5 = await r5.json(); + if(j5.success) j5.data.forEach(d => addMiskinToMap(d)); + + } catch (err) { + showToast('Gagal memuat beberapa data.', 'error'); + } +} + +// ------- Helpers ------- +function showToast(message, type = 'success') { + const container = document.getElementById('toast-container'); + const t = document.createElement('div'); + t.className = `toast toast-${type}`; + t.innerHTML = `${type === 'success' ? '✓' : '✕'}${message}`; + container.appendChild(t); + requestAnimationFrame(() => t.classList.add('toast-show')); + setTimeout(() => { + t.classList.remove('toast-show'); + t.addEventListener('transitionend', () => t.remove(), { once: true }); + }, 3000); +} + +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +// Start +loadAllData(); diff --git a/login.html b/login.html new file mode 100644 index 0000000..f126265 --- /dev/null +++ b/login.html @@ -0,0 +1,166 @@ + + + + + + Login - WebGIS SPBU & Kemiskinan + + + + + +
+
+
+ + +
+

Login Sistem

+
+
+ + +
+
+ + +
+ +
+ Belum punya akun? Daftar di sini +
+ + +
+

Daftar Akun Baru

+
+
+ + +
+
+ + +
+ +
+ Sudah punya akun? Login di sini +
+ + ← Kembali ke Beranda +
+ + + + + diff --git a/poverty.html b/poverty.html new file mode 100644 index 0000000..b8ec6d0 --- /dev/null +++ b/poverty.html @@ -0,0 +1,429 @@ + + + + + + Peta Kemiskinan & Rumah Ibadah — WebGIS + + + + + + +
+
+ Verifikasi Akses... +
+ +
+ +
+
+
+
KM
+
+

WebGIS

+

Peta Kemiskinan

+
+
+ +
+
+
+
Navigasi
+
+
+
🗺
+
Peta Kemiskinan
+
+ +
🏠
+
Beranda
+
+
+
+
+
+
+
⎋
+
Logout
+
+
+
+
+
+ + +
+
+
+

Peta Analisis Kemiskinan

+
Peta terproteksi — Login diperlukan
+
+
Loading...
+
+
+
+ + +
+

Pencarian

+ +
RUMAH IBADAH
+
+ +
+
+ +
PENDUDUK MISKIN
+
+ +
+
+
+ +
+

Legenda

+
Rumah Ibadah
+
Penduduk Miskin (Dalam Radius)
+
Penduduk Miskin (Luar Radius)
+
+
💡 Klik Rumah Ibadah untuk atur radius jangkauan
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/spbu.html b/spbu.html new file mode 100644 index 0000000..c795348 --- /dev/null +++ b/spbu.html @@ -0,0 +1,403 @@ + + + + + + Peta SPBU Publik — WebGIS + + + + + +
+ + +
+
+
+
WG
+
+

WebGIS

+

SPBU System

+
+
+ +
+
+
+
Navigasi
+
+
+
🗺
+
Peta SPBU
+
+ +
🏠
+
Beranda
+
+
+
+
+
+
+
+ + +
+
+
+

Peta Persebaran SPBU

+
Peta Publik — Data dikelola oleh Administrator
+
+
+ Memuat... + +
+
+
+
+ + +
+

Cari SPBU

+
+ +
+
+
+ +
+

📍 Lokasi & Rute

+ + +
+ SPBU Terdekat:
+ -
+ Jarak: - km +
+
+ +
+

Legenda

+
SPBU 24 Jam
+
SPBU Tidak 24 Jam
+
Lokasi Anda
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/test_api.php b/test_api.php new file mode 100644 index 0000000..ba4918d --- /dev/null +++ b/test_api.php @@ -0,0 +1,8 @@ +query($sql1); + +$sql2 = "CREATE TABLE IF NOT EXISTS penduduk_miskin ( + id INT NOT NULL AUTO_INCREMENT, + nama_kk VARCHAR(150) NOT NULL, + keterangan TEXT, + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; +$c->query($sql2); + +echo 'Tables OK'; diff --git a/verify.js b/verify.js new file mode 100644 index 0000000..2486af3 --- /dev/null +++ b/verify.js @@ -0,0 +1,18 @@ +const fs = require('fs'); +const admin = fs.readFileSync('admin.html', 'utf8'); +const app = fs.readFileSync('js/app.js', 'utf8'); + +// Check onclick attributes +console.log('SPBU onclick:', admin.includes("onclick=\"window.mapToolActivatePoint('spbu'")); +console.log('Ibadah onclick:', admin.includes("onclick=\"window.mapToolActivatePoint('ibadah'")); +console.log('Miskin onclick:', admin.includes("onclick=\"window.mapToolActivatePoint('miskin'")); +console.log('Jalan onclick:', admin.includes("onclick=\"window.mapToolActivateDraw('jalan')\"")); +console.log('Parsil onclick:', admin.includes("onclick=\"window.mapToolActivateDraw('parsil')\"")); +console.log('Cancel onclick:', admin.includes('onclick="window.mapToolCancel()"')); + +// Check global functions in app.js +console.log('\nGlobal mapToolActivatePoint:', app.includes('window.mapToolActivatePoint = function')); +console.log('Global mapToolActivateDraw:', app.includes('window.mapToolActivateDraw = function')); +console.log('Global mapToolCancel:', app.includes('window.mapToolCancel = function')); +console.log('map.click handler in initMap:', app.includes("map.on('click', function(e)")); +console.log('pm null check:', app.includes('if (map.pm && typeof map.pm.addControls'));