feat: initial commit
This commit is contained in:
+393
@@ -0,0 +1,393 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Portal WebGIS Terpadu</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg-body: #f8fafc;
|
||||
--text-main: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #e2e8f0;
|
||||
|
||||
--accent-spbu: #2563eb;
|
||||
--accent-spbu-light: #eff6ff;
|
||||
|
||||
--accent-geo: #625ff4;
|
||||
--accent-geo-light: #eef0ff;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background-color: var(--bg-body);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
/* Leaflet Background Map */
|
||||
#bg-map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -2;
|
||||
opacity: 1; /* Transparansi penuh agar peta jelas */
|
||||
pointer-events: none;
|
||||
filter: blur(6px) saturate(1.2); /* Efek blur kaca pada peta agar teks terbaca jelas */
|
||||
transform: scale(1.05); /* Mencegah pinggiran putih karena blur */
|
||||
}
|
||||
|
||||
/* Gradient Overlay agar teks tetap terbaca, sangat tipis karena peta sudah di-blur */
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
background: rgba(248, 250, 252, 0.4);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Decorative Glows */
|
||||
.glow-blue {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
width: 50vw;
|
||||
height: 50vw;
|
||||
background: radial-gradient(circle, rgba(37, 99, 235, 0.05) 0%, rgba(255, 255, 255, 0) 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.glow-purple {
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
width: 50vw;
|
||||
height: 50vw;
|
||||
background: radial-gradient(circle, rgba(98, 95, 244, 0.05) 0%, rgba(255, 255, 255, 0) 70%);
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Header Area */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 100px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.badge i {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: -1px;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
text-shadow: 0 0 10px white, 0 0 20px white;
|
||||
/* Agar tetap terbaca di atas peta */
|
||||
}
|
||||
|
||||
/* Project Cards */
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
border-radius: 24px;
|
||||
padding: 2.5rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-spbu::after {
|
||||
background-color: var(--accent-spbu);
|
||||
}
|
||||
|
||||
.card-geo::after {
|
||||
background-color: var(--accent-geo);
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.1);
|
||||
border-color: transparent;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card-spbu .icon-wrap {
|
||||
background-color: var(--accent-spbu-light);
|
||||
color: var(--accent-spbu);
|
||||
}
|
||||
|
||||
.card-geo .icon-wrap {
|
||||
background-color: var(--accent-geo-light);
|
||||
color: var(--accent-geo);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 1rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
background-color: var(--bg-body);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.card-spbu .tag i {
|
||||
color: var(--accent-spbu);
|
||||
}
|
||||
|
||||
.card-geo .tag i {
|
||||
color: var(--accent-geo);
|
||||
}
|
||||
|
||||
.action-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-spbu .action-link {
|
||||
color: var(--accent-spbu);
|
||||
}
|
||||
|
||||
.card-geo .action-link {
|
||||
color: var(--accent-geo);
|
||||
}
|
||||
|
||||
.project-card:hover .action-link {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Leaflet Background -->
|
||||
<div id="bg-map"></div>
|
||||
<div class="map-overlay"></div>
|
||||
|
||||
<div class="glow-blue"></div>
|
||||
<div class="glow-purple"></div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="header">
|
||||
<div class="badge">
|
||||
<i class="fa-solid fa-circle-check"></i> PORTAL SISTEM INFORMASI GEOGRAFIS
|
||||
</div>
|
||||
<h1>Eksplorasi Data Spasial<br>Kota Pontianak</h1>
|
||||
<p class="subtitle">Platform terpadu untuk visualisasi dan manajemen data geografis. Silakan pilih aplikasi
|
||||
di bawah ini untuk memulai.</p>
|
||||
</div>
|
||||
|
||||
<div class="projects-grid">
|
||||
|
||||
<!-- Project 1 -->
|
||||
<a href="webgis-spbu/" class="project-card card-spbu">
|
||||
<div class="icon-wrap">
|
||||
<i class="fa-solid fa-map-location-dot"></i>
|
||||
</div>
|
||||
<h2>Manajemen Tata Ruang & SPBU</h2>
|
||||
<p class="desc">
|
||||
Sistem Informasi Geografis komprehensif untuk pemetaan Point of Interest (SPBU), jaringan jalan, dan
|
||||
parsel tanah di wilayah administratif.
|
||||
</p>
|
||||
<div class="tags">
|
||||
<span class="tag"><i class="fa-solid fa-gas-pump"></i> SPBU</span>
|
||||
<span class="tag"><i class="fa-solid fa-road"></i> Jaringan Jalan</span>
|
||||
<span class="tag"><i class="fa-solid fa-vector-square"></i> Kavling Tanah</span>
|
||||
</div>
|
||||
<div class="action-link">
|
||||
Buka Aplikasi <i class="fa-solid fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Project 2 -->
|
||||
<a href="webgis-geopoverty/" class="project-card card-geo">
|
||||
<div class="icon-wrap">
|
||||
<i class="fa-solid fa-hands-holding-child"></i>
|
||||
</div>
|
||||
<h2>GeoPoverty & Pemetaan Sosial</h2>
|
||||
<p class="desc">
|
||||
Platform interaktif pemetaan kemiskinan berbasis partisipasi rumah ibadah. Membantu identifikasi dan
|
||||
distribusi bantuan sosial agar lebih tepat sasaran.
|
||||
</p>
|
||||
<div class="tags">
|
||||
<span class="tag"><i class="fa-solid fa-users"></i> Data Kemiskinan</span>
|
||||
<span class="tag"><i class="fa-solid fa-place-of-worship"></i> Rumah Ibadah</span>
|
||||
<span class="tag"><i class="fa-solid fa-chart-pie"></i> Analisis Bantuan</span>
|
||||
</div>
|
||||
<div class="action-link">
|
||||
Buka Aplikasi <i class="fa-solid fa-arrow-right"></i>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Inisialisasi peta Pontianak sebagai background statis
|
||||
var map = L.map('bg-map', {
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
dragging: false,
|
||||
scrollWheelZoom: false,
|
||||
doubleClickZoom: false,
|
||||
boxZoom: false,
|
||||
keyboard: false
|
||||
}).setView([-0.026330, 109.342504], 14); // Koordinat Pontianak
|
||||
|
||||
// Menggunakan tile CartoDB Voyager yang lebih berwarna (hijau, biru, kuning) namun tetap terang
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,205 @@
|
||||
# 🗺️ WebGIS Project - Manajemen Data Spasial
|
||||
|
||||
## 📌 Deskripsi Proyek
|
||||
|
||||
Proyek ini merupakan aplikasi **WebGIS berbasis LeafletJS, PHP, dan MySQL** yang digunakan untuk mengelola data spasial dalam bentuk:
|
||||
|
||||
* 📍 Point (SPBU)
|
||||
* ➖ Line / Polyline (Jalan)
|
||||
* 🔷 Polygon (Kavling Tanah)
|
||||
|
||||
Aplikasi ini memungkinkan user untuk:
|
||||
|
||||
* Menambahkan data langsung dari peta
|
||||
* Menyimpan ke database
|
||||
* Menampilkan kembali data pada peta dengan visualisasi berbeda
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Tujuan Tugas
|
||||
|
||||
Mahasiswa diharapkan mampu:
|
||||
|
||||
* Mengimplementasikan WebGIS berbasis web
|
||||
* Mengelola data spasial (Point, Line, Polygon)
|
||||
* Melakukan analisis sederhana:
|
||||
|
||||
* Panjang jalan
|
||||
* Luas tanah
|
||||
* Menggunakan LeafletJS untuk visualisasi peta
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Fitur Aplikasi
|
||||
|
||||
### 1. 📍 Data Point - SPBU
|
||||
|
||||
* Input dengan klik pada peta
|
||||
* Menyimpan:
|
||||
|
||||
* Nama SPBU
|
||||
* Nomor SPBU
|
||||
* Status (24 jam / tidak)
|
||||
* Latitude & Longitude
|
||||
* Ditampilkan sebagai marker pada peta
|
||||
|
||||
---
|
||||
|
||||
### 2. ➖ Data Line - Jalan
|
||||
|
||||
* Input dengan menggambar garis (Polyline)
|
||||
* Status jalan:
|
||||
|
||||
* Jalan Nasional
|
||||
* Jalan Provinsi
|
||||
* Jalan Kabupaten
|
||||
* Warna berbeda sesuai status:
|
||||
|
||||
* Nasional → Merah
|
||||
* Provinsi → Biru
|
||||
* Kabupaten → Hijau
|
||||
* Panjang jalan dihitung otomatis (meter)
|
||||
|
||||
---
|
||||
|
||||
### 3. 🔷 Data Polygon - Kavling Tanah
|
||||
|
||||
* Input dengan menggambar area (Polygon)
|
||||
* Status kepemilikan:
|
||||
|
||||
* SHM
|
||||
* HGB
|
||||
* HGU
|
||||
* HP
|
||||
* Warna berbeda sesuai status
|
||||
* Luas tanah dihitung otomatis (m²)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Teknologi yang Digunakan
|
||||
|
||||
* 🌐 Frontend: HTML, CSS, JavaScript
|
||||
* 🗺️ Library: LeafletJS + Leaflet Draw ([leaflet.github.io][1])
|
||||
* 🖥️ Backend: PHP
|
||||
* 🗄️ Database: MySQL
|
||||
|
||||
Leaflet mendukung berbagai bentuk geometri seperti:
|
||||
|
||||
* Marker (Point)
|
||||
* Polyline (Line)
|
||||
* Polygon (Area) ([igismap.com][2])
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Struktur Database
|
||||
|
||||
### Tabel SPBU
|
||||
|
||||
* nama
|
||||
* no_spbu
|
||||
* status
|
||||
* latitude
|
||||
* longitude
|
||||
|
||||
### Tabel Jalan
|
||||
|
||||
* nama
|
||||
* status
|
||||
* panjang
|
||||
* geom (koordinat)
|
||||
|
||||
### Tabel Kavling
|
||||
|
||||
* nama
|
||||
* status
|
||||
* luas
|
||||
* geom (koordinat)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Alur Sistem
|
||||
|
||||
### Input Data
|
||||
|
||||
1. User memilih mode (Point / Line / Polygon)
|
||||
2. User melakukan input di peta:
|
||||
|
||||
* Klik (Point)
|
||||
* Draw (Line / Polygon)
|
||||
3. Sistem menghitung:
|
||||
|
||||
* Panjang (Line)
|
||||
* Luas (Polygon)
|
||||
4. Data dikirim ke server (PHP)
|
||||
5. Data disimpan ke MySQL
|
||||
|
||||
---
|
||||
|
||||
### Menampilkan Data
|
||||
|
||||
1. Data diambil dari database
|
||||
2. Ditampilkan di peta
|
||||
3. Warna disesuaikan berdasarkan kategori
|
||||
4. Data juga ditampilkan di sidebar
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Konsep GIS yang Digunakan
|
||||
|
||||
* Spatial Data:
|
||||
|
||||
* Point
|
||||
* Line
|
||||
* Polygon
|
||||
* Attribute Data
|
||||
* Visualisasi berbasis kategori
|
||||
* Analisis spasial sederhana:
|
||||
|
||||
* Distance (panjang)
|
||||
* Area (luas)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Cara Menjalankan
|
||||
|
||||
1. Jalankan MySQL
|
||||
2. Masuk ke folder project:
|
||||
|
||||
```
|
||||
cd webgis-spbu
|
||||
```
|
||||
3. Jalankan server:
|
||||
|
||||
```
|
||||
php -S localhost:8000
|
||||
```
|
||||
4. Buka di browser:
|
||||
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 Catatan Penting
|
||||
|
||||
* Jangan menggunakan Live Server (tidak support PHP)
|
||||
* Gunakan Leaflet Draw untuk input Line & Polygon
|
||||
* Perhitungan panjang & luas HARUS otomatis (tidak manual)
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Kesimpulan
|
||||
|
||||
Aplikasi ini merupakan implementasi dasar WebGIS yang mencakup:
|
||||
|
||||
* Input data spasial
|
||||
* Penyimpanan database
|
||||
* Visualisasi peta interaktif
|
||||
|
||||
Proyek ini menunjukkan pemahaman terhadap konsep dasar Sistem Informasi Geografis (SIG) dalam pengembangan aplikasi berbasis web.
|
||||
|
||||
---
|
||||
|
||||
[1]: https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html?utm_source=chatgpt.com "Leaflet Draw Documentation"
|
||||
[2]: https://www.igismap.com/leafletjs-point-polyline-polygon-rectangle-circle/?utm_source=chatgpt.com "Leaflet.js - Point, Polyline, Polygon, Rectangle, Circle"
|
||||
@@ -0,0 +1,780 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**SPESIFIKASI KEBUTUHAN PERANGKAT LUNAK**
|
||||
|
||||
**(SKPL / Software Requirements Specification)**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**WebGIS Pemetaan Kemiskinan Berbasis**
|
||||
|
||||
**Partisipasi Rumah Ibadah**
|
||||
|
||||
*(Studi Kasus: Masjid di Kota Pontianak)*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Versi 1.0 | Pontianak, 2025
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*Disusun sebagai pemenuhan tugas mata kuliah*
|
||||
|
||||
***Sistem Informasi Geografis (SIG)***
|
||||
|
||||
|
||||
|
||||
# **BAB 1 – PENDAHULUAN**
|
||||
|
||||
## **1.1 Tujuan Dokumen**
|
||||
|
||||
Dokumen Spesifikasi Kebutuhan Perangkat Lunak (SKPL) ini mendeskripsikan secara lengkap kebutuhan fungsional dan non-fungsional dari sistem WebGIS Pemetaan Kemiskinan Berbasis Partisipasi Rumah Ibadah. Dokumen ini menjadi acuan bagi pengembang, penguji, dan seluruh pemangku kepentingan dalam proses pengembangan sistem.
|
||||
|
||||
|
||||
|
||||
## **1.2 Latar Belakang dan Deskripsi Sistem**
|
||||
|
||||
Kemiskinan merupakan masalah sosial yang memerlukan penanganan terkoordinasi dan berbasis data. Di tingkat komunitas, rumah ibadah — khususnya masjid — memegang peran sentral sebagai pusat kegiatan sosial masyarakat. Masjid secara aktif mengelola program zakat, infak, sedekah (ZIS), dan berbagai bantuan sosial lainnya. Namun, distribusi bantuan tersebut seringkali tidak optimal karena ketiadaan data spasial mengenai sebaran keluarga miskin di sekitar masjid.
|
||||
|
||||
|
||||
|
||||
Sistem yang dibangun adalah sebuah WebGIS (Web-based Geographic Information System) yang memungkinkan pemetaan interaktif titik-titik kemiskinan dan lokasi masjid di wilayah Kota Pontianak. Dengan fitur radius layanan masjid yang dapat dikonfigurasi, sistem ini secara visual menunjukkan keluarga miskin mana yang berada dalam jangkauan layanan sosial suatu masjid dan mana yang belum terjangkau, sehingga membantu perencanaan distribusi bantuan yang lebih tepat sasaran.
|
||||
|
||||
|
||||
|
||||
Sistem ini dibangun menggunakan teknologi Leaflet.js untuk peta interaktif, PHP sebagai backend, dan MySQL sebagai basis data, yang dapat diakses melalui browser web tanpa instalasi khusus.
|
||||
|
||||
|
||||
|
||||
## **1.3 Lingkup Masalah**
|
||||
|
||||
Sistem ini mencakup:
|
||||
|
||||
• Pemetaan dan pengelolaan data titik kemiskinan (keluarga miskin) secara spasial.
|
||||
|
||||
• Pemetaan dan pengelolaan data masjid sebagai representasi Rumah Ibadah.
|
||||
|
||||
• Visualisasi radius jangkauan layanan sosial masjid terhadap sebaran titik kemiskinan.
|
||||
|
||||
• Analisis visual untuk mendukung perencanaan distribusi bantuan berbasis lokasi.
|
||||
|
||||
|
||||
|
||||
Sistem ini TIDAK mencakup:
|
||||
|
||||
• Pengelolaan transaksi keuangan, zakat, atau distribusi bantuan secara langsung.
|
||||
|
||||
• Integrasi dengan sistem informasi pemerintah (DTKS, BPJS, dsb.) pada versi ini.
|
||||
|
||||
• Rumah ibadah selain masjid (gereja, pura, vihara) — merupakan rencana pengembangan lanjutan.
|
||||
|
||||
|
||||
|
||||
## **1.4 Definisi, Akronim, dan Istilah**
|
||||
|
||||
|
||||
|
||||
| Istilah / Akronim | Definisi |
|
||||
| :---- | :---- |
|
||||
| WebGIS | Web-based Geographic Information System – sistem informasi geografis berbasis web browser |
|
||||
| SKPL / SRS | Spesifikasi Kebutuhan Perangkat Lunak / Software Requirements Specification |
|
||||
| SIG | Sistem Informasi Geografis |
|
||||
| Kemiskinan (entitas) | Data spasial yang merepresentasikan lokasi dan atribut keluarga miskin dalam sistem |
|
||||
| KK | Kepala Keluarga |
|
||||
| Masjid (entitas) | Data spasial lokasi masjid beserta atribut dan radius layanannya dalam sistem |
|
||||
| PIC Masjid | Person In Charge – pengurus/penanggung jawab masjid yang terdaftar dalam sistem |
|
||||
| Radius Layanan | Jangkauan area pelayanan sosial masjid, divisualisasikan sebagai lingkaran pada peta (100–5000 m) |
|
||||
| Leaflet.js | Library JavaScript open-source untuk peta interaktif berbasis web |
|
||||
| OSM | OpenStreetMap – penyedia basemap peta dasar bersifat open-source |
|
||||
| CRUD | Create, Read, Update, Delete – operasi dasar pengelolaan data |
|
||||
| Marker | Ikon titik pada peta yang merepresentasikan lokasi suatu entitas |
|
||||
| Mode View | Mode tampil saja (read-only); semua fungsi edit dinonaktifkan |
|
||||
| Mode Edit | Mode aktif yang memungkinkan operator menambah, menghapus, dan memperbarui data |
|
||||
| ZIS | Zakat, Infak, Sedekah – program sosial berbasis rumah ibadah |
|
||||
|
||||
|
||||
|
||||
## **1.5 Referensi**
|
||||
|
||||
• Leaflet.js Documentation – https://leafletjs.com/
|
||||
|
||||
• OpenStreetMap – https://www.openstreetmap.org/
|
||||
|
||||
• IEEE Std 830-1998: IEEE Recommended Practice for Software Requirements Specifications
|
||||
|
||||
• Peraturan Presiden No. 15 Tahun 2010 tentang Percepatan Penanggulangan Kemiskinan
|
||||
|
||||
• Undang-Undang No. 13 Tahun 2011 tentang Penanganan Fakir Miskin
|
||||
|
||||
• BPS – Data Kemiskinan Kota Pontianak
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **BAB 2 – DESKRIPSI UMUM SISTEM**
|
||||
|
||||
## **2.1 Perspektif Produk**
|
||||
|
||||
WebGIS ini adalah aplikasi perangkat lunak berbasis browser yang berdiri mandiri (standalone). Sistem ini menggantikan proses manual pencatatan dan pemetaan data kemiskinan yang selama ini tidak memiliki komponen spasial. Sistem beroperasi pada arsitektur client-server:
|
||||
|
||||
• Client: Browser web modern (Chrome, Firefox, Edge) — tidak memerlukan instalasi tambahan
|
||||
|
||||
• Server: PHP (built-in server untuk pengembangan, Apache/Nginx untuk produksi)
|
||||
|
||||
• Database: MySQL untuk penyimpanan data atribut dan koordinat
|
||||
|
||||
• Basemap: Tile peta OpenStreetMap (memerlukan koneksi internet)
|
||||
|
||||
|
||||
|
||||
Sistem ini dirancang sebagai alat bantu pengambilan keputusan (Decision Support Tool) bagi:
|
||||
|
||||
• Pengurus masjid: untuk mengetahui sebaran keluarga miskin dalam radius layanannya
|
||||
|
||||
• Aparat kelurahan/kecamatan: untuk pemetaan kemiskinan di wilayah administratif
|
||||
|
||||
• Relawan sosial: untuk perencanaan rute dan target distribusi bantuan
|
||||
|
||||
|
||||
|
||||
## **2.2 Fungsi Produk**
|
||||
|
||||
|
||||
|
||||
| No | Fungsi Utama | Deskripsi |
|
||||
| :---- | :---- | :---- |
|
||||
| 1 | Pemetaan Kemiskinan | Input, tampil, dan hapus titik lokasi keluarga miskin beserta atribut nama KK, jumlah anggota keluarga, dan alamat |
|
||||
| 2 | Pemetaan Masjid | Input, tampil, dan hapus data masjid beserta nama PIC dan alamat |
|
||||
| 3 | Radius Layanan Masjid | Visualisasi lingkaran jangkauan layanan masjid yang dapat dikonfigurasi (100–5000 m) untuk analisis cakupan terhadap titik kemiskinan |
|
||||
| 4 | Filter Layer | Toggle tampil/sembunyikan layer kemiskinan dan masjid secara independen |
|
||||
| 5 | Mode View / Edit | Pemisahan mode tampil (publik) dan mode edit (operator) untuk kontrol akses sederhana |
|
||||
| 6 | Visualisasi Tematik | Simbologi warna berbeda untuk marker kemiskinan (oranye) dan masjid (biru) agar mudah dibedakan |
|
||||
| 7 | Fokus Lokasi | Klik card di sidebar untuk menavigasi peta ke lokasi entitas terpilih |
|
||||
|
||||
|
||||
|
||||
## **2.3 Karakteristik Pengguna**
|
||||
|
||||
|
||||
|
||||
| Tipe Pengguna | Karakteristik | Hak Akses dalam Sistem |
|
||||
| :---- | :---- | :---- |
|
||||
| Operator | Pengurus masjid, staf kelurahan, atau relawan yang bertanggung jawab atas kebenaran data. Memiliki kemampuan dasar penggunaan browser dan peta digital. | Mode View \+ Mode Edit (CRUD penuh: tambah, lihat, hapus, update radius) |
|
||||
| Masyarakat / Publik | Warga atau pihak yang ingin melihat peta sebaran kemiskinan dan masjid. Tidak perlu login. | Mode View saja (hanya lihat dan navigasi peta) |
|
||||
|
||||
|
||||
|
||||
Catatan: Versi 1.0 belum mengimplementasikan sistem autentikasi berbasis akun. Pemisahan akses dilakukan melalui mode UI (View/Edit). Implementasi login merupakan kebutuhan pengembangan lanjutan.
|
||||
|
||||
|
||||
|
||||
## **2.4 Batasan Sistem**
|
||||
|
||||
• Belum terdapat sistem autentikasi/login pada versi 1.0.
|
||||
|
||||
• Koordinat disimpan sebagai nilai desimal (DECIMAL), bukan tipe spasial MySQL POINT.
|
||||
|
||||
• Analisis spasial terbatas pada visualisasi radius buffer — belum ada query "titik dalam radius" secara otomatis.
|
||||
|
||||
• Tidak mendukung export data ke format GIS (Shapefile, GeoJSON, KML).
|
||||
|
||||
• Hanya mendukung rumah ibadah jenis masjid; rumah ibadah lain merupakan rencana pengembangan.
|
||||
|
||||
• Memerlukan koneksi internet untuk memuat tile peta OpenStreetMap.
|
||||
|
||||
• Tidak mendukung pengeditan bersamaan oleh banyak pengguna (no concurrent editing).
|
||||
|
||||
|
||||
|
||||
## **2.5 Asumsi dan Ketergantungan**
|
||||
|
||||
• Server menjalankan PHP versi 7.4 atau lebih baru dengan ekstensi mysqli aktif.
|
||||
|
||||
• MySQL versi 5.7 atau lebih baru tersedia dan dapat diakses oleh PHP.
|
||||
|
||||
• Browser pengguna mendukung ES6, Fetch API, dan Leaflet.js.
|
||||
|
||||
• Koordinat menggunakan sistem referensi WGS84 (EPSG:4326).
|
||||
|
||||
• Data titik kemiskinan diperoleh dari survei lapangan oleh relawan yang terlatih.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **BAB 3 – KEBUTUHAN FUNGSIONAL**
|
||||
|
||||
Kebutuhan fungsional didefinisikan per modul. Setiap kebutuhan memiliki ID unik, deskripsi, spesifikasi input-proses-output, dan tingkat prioritas.
|
||||
|
||||
|
||||
|
||||
## **3.1 Modul Peta Interaktif**
|
||||
|
||||
### **KF-01 – Inisialisasi Peta**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-01 |
|
||||
| Nama | Inisialisasi Peta Interaktif |
|
||||
| Deskripsi | Sistem menampilkan peta interaktif berbasis Leaflet.js dengan tile basemap OpenStreetMap, terpusat pada koordinat Kota Pontianak. |
|
||||
| Input | Koordinat pusat default (Pontianak), zoom level awal |
|
||||
| Proses | Inisialisasi map container Leaflet, muat tile OSM, set viewport awal |
|
||||
| Output | Peta interaktif tampil di browser; pengguna dapat zoom, pan, dan klik peta |
|
||||
| Prioritas | Tinggi (Critical) |
|
||||
|
||||
|
||||
|
||||
### **KF-02 – Toggle Layer Tematik**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-02 |
|
||||
| Nama | Manajemen Visibilitas Layer |
|
||||
| Deskripsi | Pengguna dapat menampilkan atau menyembunyikan layer Kemiskinan dan layer Masjid secara independen melalui kontrol overlay Leaflet dan checkbox di sidebar. |
|
||||
| Input | Klik checkbox sidebar atau overlay control di pojok peta |
|
||||
| Proses | Tambah atau hapus layer group dari map object; sinkronisasi state checkbox dengan layer control |
|
||||
| Output | Layer tampil atau hilang dari peta secara real-time tanpa reload halaman |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
## **3.2 Modul Data Kemiskinan**
|
||||
|
||||
### **KF-03 – Tambah Data Keluarga Miskin**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-03 |
|
||||
| Nama | Input Titik Lokasi Keluarga Miskin |
|
||||
| Deskripsi | Operator menambahkan data keluarga miskin dengan mengklik titik lokasi di peta dan mengisi formulir atribut. Sistem menyimpan data dan menampilkan marker di peta. |
|
||||
| Input | Nama kepala keluarga (teks), jumlah KK (angka positif), alamat (teks), koordinat latitude/longitude (dari klik peta) |
|
||||
| Proses | Validasi input tidak kosong → POST ke create\_kemiskinan.php → INSERT ke tabel kemiskinan → refresh layer kemiskinan |
|
||||
| Output | Marker oranye muncul di lokasi yang diklik; card data muncul di sidebar; data tersimpan di database |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
### **KF-04 – Lihat Daftar Data Kemiskinan**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-04 |
|
||||
| Nama | Tampil Semua Data Keluarga Miskin |
|
||||
| Deskripsi | Sistem memuat seluruh data kemiskinan dari database dan menampilkannya sebagai marker di peta beserta daftar card di sidebar secara otomatis saat halaman dibuka. |
|
||||
| Input | Request load halaman atau aksi refresh data |
|
||||
| Proses | GET read\_kemiskinan.php → parse JSON response → render marker per record → render sidebar card per record |
|
||||
| Output | Semua titik kemiskinan tampil sebagai marker oranye di peta; daftar card tampil di sidebar dengan atribut nama KK, jumlah KK, dan alamat |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
### **KF-05 – Fokus ke Lokasi Keluarga Miskin**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-05 |
|
||||
| Nama | Navigasi Peta ke Lokasi Keluarga Miskin |
|
||||
| Deskripsi | Pengguna mengklik tombol 'Fokus' pada card di sidebar, dan peta secara otomatis berpindah serta memperbesar tampilan ke lokasi keluarga miskin tersebut. |
|
||||
| Input | Klik tombol Fokus pada card keluarga miskin di sidebar |
|
||||
| Proses | Ambil nilai latitude/longitude dari data card → panggil map.setView(koordinat, zoom) |
|
||||
| Output | Peta bergerak dan memperbesar ke koordinat keluarga miskin yang dipilih |
|
||||
| Prioritas | Sedang |
|
||||
|
||||
|
||||
|
||||
### **KF-06 – Hapus Data Keluarga Miskin**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-06 |
|
||||
| Nama | Hapus Titik Lokasi Keluarga Miskin |
|
||||
| Deskripsi | Operator (dalam Mode Edit) menghapus data keluarga miskin dari sistem. Tombol hapus hanya aktif dalam Mode Edit. |
|
||||
| Input | Klik tombol 'Hapus' pada card di sidebar (hanya aktif di Mode Edit) |
|
||||
| Proses | Konfirmasi penghapusan → POST ke delete\_kemiskinan.php dengan id → DELETE dari database → hapus marker dari layer peta → hapus card dari sidebar |
|
||||
| Output | Marker hilang dari peta, card hilang dari sidebar, data terhapus permanen dari database |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
## **3.3 Modul Data Masjid**
|
||||
|
||||
### **KF-07 – Tambah Data Masjid**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-07 |
|
||||
| Nama | Input Lokasi dan Data Masjid |
|
||||
| Deskripsi | Operator menambahkan data masjid dengan mengklik lokasi di peta dan mengisi formulir. Sistem menampilkan marker masjid beserta lingkaran radius layanannya. |
|
||||
| Input | Nama masjid (teks), nama PIC/pengurus (teks), alamat (teks), radius layanan awal (integer, default 500 m, range 100–5000), koordinat lat/lng dari klik peta |
|
||||
| Proses | Validasi input → POST ke create\_masjid.php → INSERT ke tabel masjid → render marker biru \+ L.circle radius di peta → render card di sidebar |
|
||||
| Output | Marker biru dan lingkaran radius muncul di peta; card masjid muncul di sidebar dengan tombol update radius |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
### **KF-08 – Lihat Daftar Data Masjid**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-08 |
|
||||
| Nama | Tampil Semua Data Masjid dan Radius Layanan |
|
||||
| Deskripsi | Sistem memuat seluruh data masjid dari database dan menampilkannya sebagai marker biru beserta lingkaran radius layanan di peta, serta daftar card di sidebar. |
|
||||
| Input | Request load halaman atau aksi refresh data |
|
||||
| Proses | GET read\_masjid.php → parse JSON → render marker biru per masjid → render L.circle sesuai nilai radius → render card sidebar |
|
||||
| Output | Semua masjid tampil sebagai marker biru; lingkaran radius masing-masing masjid tampil di peta; sidebar menampilkan nama, PIC, alamat, dan nilai radius saat ini |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
### **KF-09 – Fokus ke Lokasi Masjid**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-09 |
|
||||
| Nama | Navigasi Peta ke Lokasi Masjid |
|
||||
| Deskripsi | Pengguna mengklik tombol 'Fokus' pada card masjid di sidebar untuk menavigasi peta ke lokasi masjid tersebut. |
|
||||
| Input | Klik tombol Fokus pada card masjid di sidebar |
|
||||
| Proses | Ambil koordinat masjid → panggil map.setView(koordinat, zoom) |
|
||||
| Output | Peta bergerak dan memperbesar ke lokasi masjid yang dipilih |
|
||||
| Prioritas | Sedang |
|
||||
|
||||
|
||||
|
||||
### **KF-10 – Perbarui Radius Layanan Masjid**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-10 |
|
||||
| Nama | Update Radius Jangkauan Layanan Masjid |
|
||||
| Deskripsi | Operator mengubah nilai radius layanan masjid untuk memperbarui visualisasi lingkaran cakupan di peta. Fitur ini krusial untuk analisis apakah keluarga miskin tertentu berada dalam jangkauan layanan masjid. |
|
||||
| Input | ID masjid, nilai radius baru (integer, range 100–5000 meter) |
|
||||
| Proses | Validasi range nilai radius → POST ke update\_masjid\_radius.php → UPDATE kolom radius di database → re-render L.circle dengan radius baru di peta |
|
||||
| Output | Lingkaran radius masjid di peta berubah ukuran secara real-time sesuai nilai radius yang diperbarui |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
### **KF-11 – Hapus Data Masjid**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-11 |
|
||||
| Nama | Hapus Data Masjid |
|
||||
| Deskripsi | Operator (dalam Mode Edit) menghapus data masjid beserta lingkaran radiusnya dari sistem. |
|
||||
| Input | Klik tombol 'Hapus' pada card masjid di sidebar (hanya aktif di Mode Edit) |
|
||||
| Proses | Konfirmasi penghapusan → POST ke delete\_masjid.php dengan id → DELETE dari database → hapus marker dan lingkaran radius dari peta → hapus card dari sidebar |
|
||||
| Output | Marker dan lingkaran radius masjid hilang dari peta; card hilang dari sidebar; data terhapus dari database |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
## **3.4 Modul Mode Sistem**
|
||||
|
||||
### **KF-12 – Mode View (Tampil)**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-12 |
|
||||
| Nama | Mode View / Tampil |
|
||||
| Deskripsi | Mode default saat sistem dibuka. Semua kontrol edit (tambah data, hapus, update radius) dinonaktifkan. Pengguna hanya dapat melihat peta, membaca data di sidebar, menggunakan filter layer, dan navigasi fokus lokasi. |
|
||||
| Input | Klik tombol/toggle 'View Mode' atau kondisi awal sistem |
|
||||
| Proses | Set state mode \= 'view' → disable tombol tambah, hapus, update radius → sembunyikan alat gambar |
|
||||
| Output | UI dalam keadaan read-only; tombol edit tampak abu-abu/nonaktif |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
### **KF-13 – Mode Edit**
|
||||
|
||||
| Atribut | Deskripsi |
|
||||
| :---- | :---- |
|
||||
| ID Kebutuhan | KF-13 |
|
||||
| Nama | Mode Edit |
|
||||
| Deskripsi | Mode aktif yang memungkinkan operator melakukan operasi CRUD. Semua kontrol input dan hapus diaktifkan. |
|
||||
| Input | Klik tombol/toggle 'Edit Mode' |
|
||||
| Proses | Set state mode \= 'edit' → enable tombol tambah data, hapus, update radius |
|
||||
| Output | UI dalam keadaan edit aktif; klik pada peta memicu input form data baru; tombol hapus dan update radius aktif |
|
||||
| Prioritas | Tinggi |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **BAB 4 – KEBUTUHAN NON-FUNGSIONAL**
|
||||
|
||||
|
||||
|
||||
## **4.1 Performa**
|
||||
|
||||
| ID | Kebutuhan | Metrik Target |
|
||||
| :---- | :---- | :---- |
|
||||
| KNF-01 | Waktu muat peta awal | \< 5 detik pada koneksi internet 10 Mbps |
|
||||
| KNF-02 | Waktu respons operasi CRUD | \< 2 detik per operasi pada jaringan lokal |
|
||||
| KNF-03 | Kapasitas render data | Mampu menampilkan hingga 500 marker tanpa penurunan performa signifikan |
|
||||
|
||||
|
||||
|
||||
## **4.2 Keamanan**
|
||||
|
||||
| ID | Kebutuhan | Keterangan |
|
||||
| :---- | :---- | :---- |
|
||||
| KNF-04 | Validasi Input Server-side | Semua input dari client divalidasi dan di-sanitasi di sisi PHP sebelum diteruskan ke database untuk mencegah SQL Injection dan XSS. |
|
||||
| KNF-05 | Prepared Statement | Seluruh query ke database menggunakan mysqli prepared statements atau PDO. |
|
||||
| KNF-06 | Autentikasi (Pengembangan Lanjutan) | Versi mendatang wajib mengimplementasikan sistem login berbasis session/JWT untuk memisahkan akses Operator dan Publik. |
|
||||
| KNF-07 | HTTPS di Produksi | Sistem di-deploy menggunakan protokol HTTPS di lingkungan produksi. |
|
||||
|
||||
|
||||
|
||||
## **4.3 Kemudahan Penggunaan (Usability)**
|
||||
|
||||
| ID | Kebutuhan | Keterangan |
|
||||
| :---- | :---- | :---- |
|
||||
| KNF-08 | Antarmuka Intuitif | Navigasi peta mengikuti konvensi WebGIS standar (zoom scroll, drag pan, klik untuk detail). |
|
||||
| KNF-09 | Resolusi Minimum | Antarmuka dapat digunakan dengan baik pada resolusi layar minimal 1280x720 piksel. |
|
||||
| KNF-10 | Feedback Aksi | Setiap operasi CRUD memberikan notifikasi sukses atau pesan error yang informatif. |
|
||||
| KNF-11 | Konsistensi Simbologi | Warna marker konsisten di seluruh sesi: oranye \= kemiskinan, biru \= masjid. |
|
||||
|
||||
|
||||
|
||||
## **4.4 Reliabilitas**
|
||||
|
||||
| ID | Kebutuhan | Keterangan |
|
||||
| :---- | :---- | :---- |
|
||||
| KNF-12 | Integritas Data | Setiap perubahan data langsung disimpan ke database; tidak ada state yang hanya tersimpan di memori browser. |
|
||||
| KNF-13 | Penanganan Error | Jika koneksi database gagal atau tile peta tidak termuat, sistem menampilkan pesan error yang informatif — tidak memunculkan halaman kosong atau crash. |
|
||||
|
||||
|
||||
|
||||
## **4.5 Portabilitas**
|
||||
|
||||
| ID | Kebutuhan | Keterangan |
|
||||
| :---- | :---- | :---- |
|
||||
| KNF-14 | Lintas Browser | Berjalan normal di Chrome 90+, Firefox 88+, dan Edge 90+. |
|
||||
| KNF-15 | Lintas OS Server | Dapat di-deploy di Linux (Ubuntu/Debian) maupun Windows Server dengan PHP dan MySQL. |
|
||||
| KNF-16 | Kemudahan Instalasi | Untuk pengembangan, sistem dapat dijalankan dengan satu perintah: php \-S localhost:8000. |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **BAB 5 – ANTARMUKA EKSTERNAL**
|
||||
|
||||
## **5.1 Antarmuka Pengguna (UI)**
|
||||
|
||||
Sistem menggunakan antarmuka single-page dengan tiga area utama:
|
||||
|
||||
|
||||
|
||||
### **5.1.1 Toolbar Atas**
|
||||
|
||||
• Tombol toggle Mode (View / Edit)
|
||||
|
||||
• Tombol switch tipe data aktif (Kemiskinan / Masjid)
|
||||
|
||||
• Judul dan identitas aplikasi
|
||||
|
||||
|
||||
|
||||
### **5.1.2 Sidebar Kiri**
|
||||
|
||||
• Checkbox filter layer: Kemiskinan dan Masjid (sinkron dengan overlay control Leaflet)
|
||||
|
||||
• Daftar card untuk setiap entitas yang termuat dari database
|
||||
|
||||
• Card Kemiskinan berisi: nama KK, jumlah KK, alamat, tombol Fokus, tombol Hapus (aktif di Edit Mode)
|
||||
|
||||
• Card Masjid berisi: nama masjid, nama PIC, alamat, radius saat ini, tombol Fokus, input \+ tombol Update Radius (aktif di Edit Mode), tombol Hapus (aktif di Edit Mode)
|
||||
|
||||
|
||||
|
||||
### **5.1.3 Area Peta Utama**
|
||||
|
||||
• Peta interaktif Leaflet dengan basemap OpenStreetMap
|
||||
|
||||
• Marker oranye untuk titik kemiskinan; marker biru untuk masjid
|
||||
|
||||
• Lingkaran radius layanan di setiap masjid (dapat dikonfigurasi)
|
||||
|
||||
• Overlay control di pojok kanan atas untuk toggle layer
|
||||
|
||||
• Pop-up atribut saat marker diklik
|
||||
|
||||
|
||||
|
||||
## **5.2 Antarmuka Perangkat Lunak**
|
||||
|
||||
|
||||
|
||||
| Komponen Eksternal | Versi | Peran |
|
||||
| :---- | :---- | :---- |
|
||||
| Leaflet.js | 1.x | Library peta interaktif utama — render marker, lingkaran radius, kontrol peta |
|
||||
| OpenStreetMap Tiles | N/A | Penyedia basemap — tile server eksternal, memerlukan koneksi internet |
|
||||
| PHP (Backend) | 7.4+ | Menangani logika bisnis, validasi input, dan komunikasi dengan database |
|
||||
| MySQL | 5.7+ | Penyimpanan data atribut kemiskinan dan masjid |
|
||||
| Fetch API (Browser) | ES6+ | Mekanisme komunikasi asinkron antara frontend JavaScript dan endpoint PHP |
|
||||
|
||||
|
||||
|
||||
## **5.3 Antarmuka Komunikasi (API Endpoint)**
|
||||
|
||||
|
||||
|
||||
| Endpoint | Method | Fungsi | Format Respons |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| php/read\_kemiskinan.php | GET | Ambil semua data keluarga miskin | JSON Array |
|
||||
| php/create\_kemiskinan.php | POST | Tambah data keluarga miskin baru | Text (success/error) |
|
||||
| php/delete\_kemiskinan.php | POST | Hapus data keluarga miskin berdasarkan ID | Text (success/error) |
|
||||
| php/read\_masjid.php | GET | Ambil semua data masjid | JSON Array |
|
||||
| php/create\_masjid.php | POST | Tambah data masjid baru | Text (success/error) |
|
||||
| php/update\_masjid\_radius.php | POST | Perbarui radius layanan masjid | Text (success/error) |
|
||||
| php/delete\_masjid.php | POST | Hapus data masjid berdasarkan ID | Text (success/error) |
|
||||
|
||||
|
||||
|
||||
## **5.4 Skema Basis Data**
|
||||
|
||||
|
||||
|
||||
### **Tabel: kemiskinan**
|
||||
|
||||
| Kolom | Tipe Data | Keterangan |
|
||||
| :---- | :---- | :---- |
|
||||
| id | INT AUTO\_INCREMENT (PK) | Primary key, unik per record |
|
||||
| nama\_kepala\_keluarga | VARCHAR(255) NOT NULL | Nama kepala keluarga miskin |
|
||||
| jumlah\_kk | INT NOT NULL | Jumlah anggota keluarga (angka positif) |
|
||||
| alamat | TEXT NOT NULL | Alamat lengkap tempat tinggal |
|
||||
| latitude | DECIMAL(10,8) NOT NULL | Koordinat lintang (WGS84, range \-90 hingga \+90) |
|
||||
| longitude | DECIMAL(11,8) NOT NULL | Koordinat bujur (WGS84, range \-180 hingga \+180) |
|
||||
| created\_at | TIMESTAMP DEFAULT NOW() | Waktu pencatatan data |
|
||||
|
||||
|
||||
|
||||
### **Tabel: masjid**
|
||||
|
||||
| Kolom | Tipe Data | Keterangan |
|
||||
| :---- | :---- | :---- |
|
||||
| id | INT AUTO\_INCREMENT (PK) | Primary key, unik per record |
|
||||
| nama\_masjid | VARCHAR(255) NOT NULL | Nama lengkap masjid |
|
||||
| pic\_masjid | VARCHAR(255) NOT NULL | Nama pengurus/penanggung jawab masjid |
|
||||
| alamat | TEXT NOT NULL | Alamat lengkap masjid |
|
||||
| latitude | DECIMAL(10,8) NOT NULL | Koordinat lintang (WGS84) |
|
||||
| longitude | DECIMAL(11,8) NOT NULL | Koordinat bujur (WGS84) |
|
||||
| radius | INT DEFAULT 500 | Radius layanan dalam meter (range valid: 100–5000) |
|
||||
| created\_at | TIMESTAMP DEFAULT NOW() | Waktu pencatatan data |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **BAB 6 – ANALISIS BISNIS PROSES DAN KONTRIBUSI PENGENTASAN KEMISKINAN**
|
||||
|
||||
## **6.1 Konteks dan Permasalahan**
|
||||
|
||||
Pengentasan kemiskinan di tingkat komunitas memerlukan dua elemen kunci: data yang akurat tentang siapa dan di mana keluarga miskin berada, serta jaringan distribusi bantuan yang efisien. Masjid sebagai lembaga sosial komunitas memiliki kapasitas distribusi tersebut melalui program ZIS, namun seringkali tidak memiliki peta sebaran penerima manfaat.
|
||||
|
||||
|
||||
|
||||
Permasalahan utama yang diselesaikan sistem ini:
|
||||
|
||||
• Tidak adanya data spasial mengenai sebaran keluarga miskin di sekitar masjid.
|
||||
|
||||
• Distribusi bantuan bersifat reaktif (menunggu yang datang) bukan proaktif (mendatangi yang membutuhkan).
|
||||
|
||||
• Potensi tumpang tindih cakupan antar masjid yang berdekatan, atau sebaliknya, ada wilayah yang tidak terjangkau masjid manapun (coverage gap).
|
||||
|
||||
|
||||
|
||||
## **6.2 Alur Bisnis Proses**
|
||||
|
||||
### **6.2.1 Proses Pendataan Keluarga Miskin (Partisipatif)**
|
||||
|
||||
1\. Relawan atau pengurus RT/RW melakukan survei lapangan untuk mengidentifikasi keluarga miskin di lingkungannya.
|
||||
|
||||
2\. Data survei (nama KK, jumlah anggota, alamat, dan koordinat GPS dari smartphone) dibawa ke sistem.
|
||||
|
||||
3\. Operator membuka WebGIS dalam Mode Edit, memilih tipe 'Kemiskinan', dan mengklik lokasi di peta sesuai koordinat survei.
|
||||
|
||||
4\. Operator mengisi formulir atribut dan menyimpan data.
|
||||
|
||||
5\. Sistem menampilkan marker oranye di peta dan menambahkan card di sidebar.
|
||||
|
||||
6\. Operator dapat memverifikasi posisi marker sesuai alamat yang tercatat.
|
||||
|
||||
|
||||
|
||||
### **6.2.2 Proses Registrasi Masjid dan Penetapan Radius Layanan**
|
||||
|
||||
7\. Pengurus masjid mendaftarkan masjidnya ke sistem: nama, nama PIC, alamat, dan koordinat lokasi masjid.
|
||||
|
||||
8\. Sistem menampilkan marker biru dengan lingkaran radius default 500 m.
|
||||
|
||||
9\. Pengurus mengamati titik-titik kemiskinan (marker oranye) yang berada di dalam dan di luar radius.
|
||||
|
||||
10\. Pengurus menyesuaikan nilai radius sesuai kapasitas nyata masjid (misalnya dipersempit ke 300 m jika sumber daya terbatas, atau diperluas ke 1000 m jika kapasitas besar).
|
||||
|
||||
11\. Radius yang sudah disepakati disimpan ke database untuk digunakan sebagai acuan distribusi.
|
||||
|
||||
|
||||
|
||||
### **6.2.3 Proses Analisis Cakupan dan Perencanaan Distribusi**
|
||||
|
||||
12\. Operator membuka peta dalam Mode View dengan kedua layer (Kemiskinan \+ Masjid) aktif.
|
||||
|
||||
13\. Secara visual, operator mengidentifikasi: marker oranye mana yang berada di dalam lingkaran biru (terjangkau) dan mana yang di luar (belum terjangkau).
|
||||
|
||||
14\. Untuk titik kemiskinan yang tidak terjangkau masjid manapun, operator dapat berkoordinasi dengan masjid terdekat untuk memperluas layanan atau dengan aparat kelurahan untuk program bantuan alternatif.
|
||||
|
||||
15\. Daftar card di sidebar digunakan sebagai dasar daftar penerima manfaat untuk masing-masing masjid.
|
||||
|
||||
|
||||
|
||||
## **6.3 Kontribusi Setiap Fitur terhadap Pengentasan Kemiskinan**
|
||||
|
||||
|
||||
|
||||
| Fitur Sistem | Kontribusi Langsung terhadap Pengentasan Kemiskinan |
|
||||
| :---- | :---- |
|
||||
| Titik Peta Kemiskinan | Mengubah data survei lapangan menjadi basis data spasial yang dapat dianalisis secara visual — menggantikan daftar nama yang tidak dapat dianalisis secara geografis. |
|
||||
| Marker Tematik Oranye | Memberikan representasi visual yang jelas dan cepat terbaca bagi pengguna non-teknis untuk memahami sebaran kemiskinan di wilayah mereka. |
|
||||
| Lokasi Masjid di Peta | Menunjukkan posisi relatif masjid terhadap kantong-kantong kemiskinan untuk perencanaan program sosial yang lebih terarah. |
|
||||
| Radius Layanan Masjid | Fitur kunci: memvisualisasikan jangkauan program sosial masjid dan mengidentifikasi gap cakupan (keluarga miskin yang tidak terjangkau masjid manapun). |
|
||||
| Konfigurasi Radius (Update) | Memungkinkan penyesuaian dinamis sesuai kapasitas nyata masjid — radius bukan nilai tetap, melainkan mencerminkan kemampuan riil program sosial. |
|
||||
| Filter Layer Independen | Memudahkan fokus analisis: hanya tampilkan kemiskinan untuk melihat sebaran, hanya tampilkan masjid untuk melihat distribusi layanan, atau keduanya untuk analisis cakupan. |
|
||||
| Sidebar dengan Data Atribut | Menyediakan daftar terstruktur yang dapat digunakan langsung sebagai daftar penerima bantuan oleh pengurus masjid. |
|
||||
| Mode View/Edit | Memisahkan penggunaan sistem sebagai alat analisis publik (View) dan alat pemeliharaan data oleh operator (Edit) — mendukung partisipasi masyarakat luas. |
|
||||
|
||||
|
||||
|
||||
## **6.4 Rencana Pengembangan Lanjutan**
|
||||
|
||||
Untuk meningkatkan dampak sistem terhadap pengentasan kemiskinan di versi selanjutnya:
|
||||
|
||||
• Autentikasi multi-role: Admin, Operator Masjid, Aparat Kelurahan, dan Publik.
|
||||
|
||||
• Analisis spasial otomatis: query database untuk menampilkan daftar keluarga miskin dalam radius suatu masjid secara otomatis (bukan hanya visual).
|
||||
|
||||
• Laporan statistik: jumlah KK per kelurahan, persentase cakupan layanan masjid, peta panas (heatmap) kemiskinan.
|
||||
|
||||
• Modul rumah ibadah lain: gereja, pura, vihara, dan kelenteng — untuk cakupan lintas agama.
|
||||
|
||||
• Export data: ke format Excel atau PDF untuk pelaporan ke instansi pemerintah.
|
||||
|
||||
• Integrasi DTKS (Data Terpadu Kesejahteraan Sosial) Kementerian Sosial.
|
||||
|
||||
• Fitur upload foto rumah sebagai bukti verifikasi lapangan.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **BAB 7 – DIAGRAM SISTEM**
|
||||
|
||||
## **7.1 Arsitektur Sistem**
|
||||
|
||||
Sistem menggunakan arsitektur three-tier (tiga lapis):
|
||||
|
||||
|
||||
|
||||
| ARSITEKTUR THREE-TIER WebGIS Kemiskinan & Masjid ┌─────────────────────────────────────────────┐ │ PRESENTATION LAYER (Client) │ │ Browser: HTML \+ CSS \+ Vanilla JavaScript │ │ Leaflet.js | Fetch API | Sidebar UI │ └─────────────────┬───────────────────────────┘ *HTTP/HTTPS | JSON Response* ┌─────────────────┴───────────────────────────┐ │ BUSINESS LOGIC LAYER (Server) │ │ PHP Procedural | 7 Endpoint CRUD │ │ Validasi Input | Query Builder │ └─────────────────┬───────────────────────────┘ *mysqli | SQL Query* ┌─────────────────┴───────────────────────────┐ │ DATA LAYER (Database) │ │ MySQL Database │ │ Tabel: kemiskinan | Tabel: masjid │ └─────────────────────────────────────────────┘ |
|
||||
| :---- |
|
||||
|
||||
|
||||
|
||||
## **7.2 Alur Data – Tambah Titik Kemiskinan**
|
||||
|
||||
| OPERATOR → Klik lokasi di peta (Leaflet click event) BROWSER → Tampil form: Nama KK | Jumlah KK | Alamat OPERATOR → Isi dan submit form JS → fetch POST php/create\_kemiskinan.php PHP → Validasi & sanitasi input PHP → INSERT INTO kemiskinan (...) VALUES (...) MySQL → Simpan data, return success PHP → Return 'success' (text) JS → Refresh: GET read\_kemiskinan.php MAP → Render marker oranye di koordinat baru SIDEBAR → Tambah card baru di daftar kemiskinan |
|
||||
| :---- |
|
||||
|
||||
|
||||
|
||||
## **7.3 Alur Data – Update Radius Masjid**
|
||||
|
||||
| OPERATOR → Input nilai radius baru di card masjid (sidebar) OPERATOR → Klik tombol 'Update Radius' JS → Validasi range (100-5000) JS → fetch POST php/update\_masjid\_radius.php PHP → UPDATE masjid SET radius=? WHERE id=? MySQL → Update berhasil, return success JS → Re-render L.circle dengan radius baru di peta MAP → Lingkaran radius masjid berubah ukuran real-time |
|
||||
| :---- |
|
||||
|
||||
|
||||
|
||||
## **7.4 Entity Relationship Diagram (ERD)**
|
||||
|
||||
|
||||
|
||||
| Entitas | Atribut |
|
||||
| :---- | :---- |
|
||||
| kemiskinan | id (PK) | nama\_kepala\_keluarga | jumlah\_kk | alamat | latitude | longitude | created\_at |
|
||||
| masjid | id (PK) | nama\_masjid | pic\_masjid | alamat | latitude | longitude | radius | created\_at |
|
||||
|
||||
|
||||
|
||||
Catatan: Kedua tabel tidak memiliki foreign key — relasi logis antara masjid dan kemiskinan ditentukan secara spasial (kalkulasi jarak dan radius) di layer peta, bukan melalui relasi database relasional. Ini merupakan desain yang disederhanakan untuk versi 1.0; versi lanjutan dapat mengimplementasikan tabel relasi atau tipe spasial MySQL.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **LAMPIRAN**
|
||||
|
||||
## **A. Ringkasan Kebutuhan Fungsional**
|
||||
|
||||
|
||||
|
||||
| ID | Nama Kebutuhan | Modul | Prioritas |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| KF-01 | Inisialisasi Peta Interaktif | Peta | Tinggi |
|
||||
| KF-02 | Toggle Layer Tematik | Peta | Tinggi |
|
||||
| KF-03 | Tambah Data Keluarga Miskin | Kemiskinan | Tinggi |
|
||||
| KF-04 | Lihat Daftar Data Kemiskinan | Kemiskinan | Tinggi |
|
||||
| KF-05 | Fokus ke Lokasi Keluarga Miskin | Kemiskinan | Sedang |
|
||||
| KF-06 | Hapus Data Keluarga Miskin | Kemiskinan | Tinggi |
|
||||
| KF-07 | Tambah Data Masjid | Masjid | Tinggi |
|
||||
| KF-08 | Lihat Daftar Data Masjid | Masjid | Tinggi |
|
||||
| KF-09 | Fokus ke Lokasi Masjid | Masjid | Sedang |
|
||||
| KF-10 | Perbarui Radius Layanan Masjid | Masjid | Tinggi |
|
||||
| KF-11 | Hapus Data Masjid | Masjid | Tinggi |
|
||||
| KF-12 | Mode View | Mode Sistem | Tinggi |
|
||||
| KF-13 | Mode Edit | Mode Sistem | Tinggi |
|
||||
|
||||
|
||||
|
||||
## **B. Simbologi Peta**
|
||||
|
||||
|
||||
|
||||
| Elemen Peta | Simbol | Warna | Makna |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| Titik Kemiskinan | Marker bulat | Oranye (\#FFA500) | Lokasi keluarga miskin |
|
||||
| Masjid | Marker bulat | Biru (\#0000FF) | Lokasi masjid (rumah ibadah) |
|
||||
| Radius Layanan Masjid | Lingkaran transparan | Biru muda (opacity 0.2) | Area jangkauan program sosial masjid |
|
||||
|
||||
|
||||
|
||||
## **C. Kamus Data (Data Dictionary)**
|
||||
|
||||
|
||||
|
||||
| Field | Tabel | Tipe Data | Validasi |
|
||||
| :---- | :---- | :---- | :---- |
|
||||
| nama\_kepala\_keluarga | kemiskinan | VARCHAR(255) | Wajib diisi, tidak boleh kosong |
|
||||
| jumlah\_kk | kemiskinan | INT | Wajib diisi, angka bulat positif \> 0 |
|
||||
| alamat | kemiskinan | TEXT | Wajib diisi |
|
||||
| latitude | kemiskinan, masjid | DECIMAL(10,8) | Range: \-90 hingga \+90 |
|
||||
| longitude | kemiskinan, masjid | DECIMAL(11,8) | Range: \-180 hingga \+180 |
|
||||
| nama\_masjid | masjid | VARCHAR(255) | Wajib diisi, tidak boleh kosong |
|
||||
| pic\_masjid | masjid | VARCHAR(255) | Wajib diisi |
|
||||
| radius | masjid | INT | Range valid: 100–5000 meter; default 500 |
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# GeoPoverty Light: UI/UX Design Context & Guidelines (v1.0)
|
||||
|
||||
Dokumen ini mendefinisikan bahasa visual dan standar antarmuka untuk platform **GeoPoverty GIS**. Fokus utama desain adalah kejernihan data, profesionalisme, dan efisiensi navigasi spasial dalam balutan estetika yang bersih dan modern.
|
||||
|
||||
---
|
||||
|
||||
## 1. Filosofi Desain: "Lumina GIS"
|
||||
Estetika ini mengutamakan **kejernihan (clarity)** dan **kedalaman (depth)**. Dengan menggunakan latar belakang yang terang dan kontras tinggi, data kemiskinan yang kompleks diubah menjadi informasi yang mudah dicerna tanpa kehilangan kesan premium.
|
||||
|
||||
---
|
||||
|
||||
## 2. Identitas Visual & Palet Warna
|
||||
Menggunakan skema warna yang didominasi oleh warna dingin dan netral untuk menciptakan kesan platform yang objektif dan tepercaya.
|
||||
|
||||
### Warna Utama (Core Colors)
|
||||
- **Primary (Electric Indigo)**: `#6366F1` - Digunakan untuk aksi utama (CTA), indikator aktif, dan elemen branding.
|
||||
- **Surface (Ghost White)**: `#F7F9FB` - Warna latar belakang utama untuk menjaga tampilan tetap segar dan bersih.
|
||||
- **On-Surface (Deep Slate)**: `#1A2138` - Warna teks utama untuk keterbacaan maksimal.
|
||||
- **Secondary (Teal)**: `#14B8A6` - Digunakan untuk indikator status positif atau kategori infrastruktur tertentu.
|
||||
- **Error (Crimson)**: `#EF4444` - Digunakan untuk status "Prioritas 1" atau kondisi kritis.
|
||||
- **Outline (Soft Steel)**: `#D8DADC` - Untuk garis pemisah dan border yang halus.
|
||||
|
||||
---
|
||||
|
||||
## 3. Tipografi
|
||||
Menggunakan font **Hanken Grotesk** untuk kesan modern dan teknis yang tetap ramah pengguna.
|
||||
|
||||
- **Display/Headlines**: Bold, kerning rapat (-0.02em). Memberikan penekanan kuat pada judul panel.
|
||||
- **Body Text**: Regular, line-height 1.6. Memastikan alamat dan deskripsi panjang mudah dibaca.
|
||||
- **Labels/Data**: Semi-bold, ukuran 11-12px. Digunakan untuk statistik wilayah dan label marker.
|
||||
|
||||
---
|
||||
|
||||
## 4. Prinsip Layout & Komposisi
|
||||
Platform ini menggunakan pendekatan **Layered Architecture**:
|
||||
|
||||
### A. Base Layer (The Map)
|
||||
Peta selalu menjadi kanvas dasar. Gunakan basemap dengan saturasi rendah agar marker data (Indigo/Teal/Red) terlihat menonjol.
|
||||
|
||||
### B. Glassmorphic Panels
|
||||
Semua panel UI (Sidebar, Header, Cards) menggunakan efek transparansi:
|
||||
- `background: rgba(255, 255, 255, 0.8);`
|
||||
- `backdrop-filter: blur(16px);`
|
||||
- `border: 1px solid rgba(255, 255, 255, 0.3);`
|
||||
|
||||
### C. Container Strategy
|
||||
- **Corner Radius**: Konsisten di **16px** untuk elemen besar dan **8px** untuk elemen kecil (tombol/badge).
|
||||
- **Elevation**: Gunakan bayangan yang sangat lembut (`shadow-sm`) untuk memisahkan panel dari peta, menghindari garis hitam yang kaku.
|
||||
|
||||
---
|
||||
|
||||
## 5. Komponen Antarmuka (UI Components)
|
||||
|
||||
### Data Cards (Keluarga & Infrastruktur)
|
||||
- **State**: Kartu yang dipilih (selected) harus mendapatkan aksen border Primary 2px.
|
||||
- **Hierarchy**: Nama Kepala Keluarga sebagai judul utama, diikuti oleh alamat dan status bantuan dalam bentuk badge berwarna.
|
||||
|
||||
### Interactive Markers
|
||||
- **Active State**: Saat diklik, marker harus memberikan feedback visual (pembesaran ukuran atau pendaran cahaya/glow).
|
||||
- **Radius Visualization**: Lingkaran radius rumah ibadah menggunakan fill opacity 5% dengan garis tepi tipis (1px dashed).
|
||||
|
||||
---
|
||||
|
||||
## 6. Pengalaman Pengguna (UX) & Interaksi
|
||||
1. **Contextual Disclosure**: Tampilkan detail data hanya saat dibutuhkan (on-click/on-hover) untuk menjaga kebersihan peta.
|
||||
2. **Smooth Transitions**: Gunakan durasi transisi 300ms untuk pembukaan panel sidebar atau modal.
|
||||
3. **Action Priority**: Tombol "Lapor Kondisi" atau "Tambah Data" harus memiliki kontras warna tertinggi (Primary) agar mudah ditemukan seketika.
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementasi CSS (Tokens)
|
||||
```css
|
||||
:root {
|
||||
--lumina-primary: #6366f1;
|
||||
--lumina-surface: rgba(247, 249, 251, 0.8);
|
||||
--lumina-text: #1a2138;
|
||||
--lumina-radius: 16px;
|
||||
--lumina-glass: blur(16px);
|
||||
}
|
||||
|
||||
.panel-premium {
|
||||
background: var(--lumina-surface);
|
||||
backdrop-filter: var(--lumina-glass);
|
||||
border-radius: var(--lumina-radius);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
-- ============================================================
|
||||
-- WebGIS Pemetaan Kemiskinan Berbasis Partisipasi Rumah Ibadah
|
||||
-- Studi Kasus: Masjid di Kota Pontianak
|
||||
-- Schema Versi 1.0 – sesuai SRS_CONTEXT.md Bab 5.4
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_kemiskinan
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_kemiskinan;
|
||||
|
||||
-- ── Tabel: kemiskinan ─────────────────────────────────────────
|
||||
-- Menyimpan data spasial lokasi dan atribut keluarga miskin.
|
||||
-- Koordinat menggunakan WGS84 (EPSG:4326).
|
||||
CREATE TABLE IF NOT EXISTS kemiskinan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_kepala_keluarga VARCHAR(255) NOT NULL COMMENT 'Nama kepala keluarga miskin',
|
||||
jumlah_kk INT NOT NULL COMMENT 'Jumlah anggota keluarga, angka bulat positif',
|
||||
alamat TEXT NOT NULL COMMENT 'Alamat lengkap tempat tinggal',
|
||||
latitude DECIMAL(10,8) NOT NULL COMMENT 'Koordinat lintang WGS84, range -90 s.d. +90',
|
||||
longitude DECIMAL(11,8) NOT NULL COMMENT 'Koordinat bujur WGS84, range -180 s.d. +180',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Waktu pencatatan data'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── Tabel: masjid ─────────────────────────────────────────────
|
||||
-- Menyimpan data spasial lokasi masjid beserta radius layanan sosial.
|
||||
-- Radius (meter) digunakan sebagai parameter visualisasi L.circle di Leaflet.js.
|
||||
CREATE TABLE IF NOT EXISTS masjid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_masjid VARCHAR(255) NOT NULL COMMENT 'Nama lengkap masjid',
|
||||
pic_masjid VARCHAR(255) NOT NULL COMMENT 'Nama pengurus/penanggung jawab (PIC)',
|
||||
alamat TEXT NOT NULL COMMENT 'Alamat lengkap masjid',
|
||||
latitude DECIMAL(10,8) NOT NULL COMMENT 'Koordinat lintang WGS84',
|
||||
longitude DECIMAL(11,8) NOT NULL COMMENT 'Koordinat bujur WGS84',
|
||||
radius INT NOT NULL DEFAULT 500 COMMENT 'Radius layanan sosial dalam meter, range 100-5000',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Waktu pencatatan data'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── Catatan ERD (SRS Bab 7.4) ─────────────────────────────────
|
||||
-- Kedua tabel TIDAK memiliki foreign key.
|
||||
-- Relasi logis antara masjid dan kemiskinan ditentukan secara spasial
|
||||
-- (kalkulasi jarak vs radius) di layer peta Leaflet, bukan melalui
|
||||
-- relasi database relasional. Ini merupakan desain yang disederhanakan
|
||||
-- untuk versi 1.0. Versi lanjutan dapat mengimplementasikan tabel relasi
|
||||
-- atau tipe spasial MySQL POINT/GEOMETRY.
|
||||
@@ -0,0 +1,31 @@
|
||||
<mxfile host="Electron" modified="2024" agent="Mozilla/5.0" version="22.1.2" type="device">
|
||||
<diagram id="diag1" name="Page-1">
|
||||
<mxGraphModel dx="1000" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="a1_title" value="Alur Data: Tambah Titik Kemiskinan" style="text;html=1;align=center;fontStyle=1;fontSize=18;" vertex="1" parent="1"><mxGeometry x="80" y="20" width="600" height="40" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="n1" value="Pengurus Login &
Aktifkan Mode Edit" style="rounded=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="80" y="80" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="n2" value="Klik Lokasi di Peta" style="rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="280" y="80" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="n3" value="Modal Form Muncul
(Input Data Keluarga)" style="rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="480" y="80" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="n4" value="Submit Form
(Validasi Client-Side)" style="rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="480" y="200" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="n5" value="Fetch API POST
(create_kemiskinan.php)" style="rounded=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="280" y="200" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="n6" value="Validasi Sesi (check_auth.php)
& Validasi Data PHP" style="shape=hexagon;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="40" y="190" width="180" height="80" as="geometry"/></mxCell>
|
||||
<mxCell id="n7" value="INSERT INTO kemiskinan" style="shape=cylinder3;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1"><mxGeometry x="70" y="320" width="120" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="n8" value="Response Success JSON" style="rounded=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="280" y="325" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="n9" value="Update Peta (Marker Baru)
& Sidebar (Card Baru)" style="rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="480" y="325" width="140" height="60" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="e_1" edge="1" source="n1" target="n2" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="e_2" edge="1" source="n2" target="n3" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="e_3" edge="1" source="n3" target="n4" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="e_4" edge="1" source="n4" target="n5" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="e_5" edge="1" source="n5" target="n6" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="e_6" edge="1" source="n6" target="n7" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="e_7" edge="1" source="n7" target="n8" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="e_8" edge="1" source="n8" target="n9" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,31 @@
|
||||
<mxfile host="Electron" modified="2024" agent="Mozilla/5.0" version="22.1.2" type="device">
|
||||
<diagram id="diag1" name="Page-1">
|
||||
<mxGraphModel dx="1000" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="a2_title" value="Alur Data: Update Radius Rumah Ibadah" style="text;html=1;align=center;fontStyle=1;fontSize=18;" vertex="1" parent="1"><mxGeometry x="80" y="20" width="600" height="40" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="m1" value="Pengurus Ubah Input Radius
di Panel Manajemen Data" style="rounded=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"><mxGeometry x="80" y="80" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="m2" value="Klik Tombol Update Radius" style="rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="300" y="80" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="m3" value="Fetch API POST
(update_masjid_radius.php)" style="rounded=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="520" y="80" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="m4" value="Validasi Sesi & Hak Akses" style="shape=hexagon;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="510" y="200" width="180" height="80" as="geometry"/></mxCell>
|
||||
<mxCell id="m5" value="UPDATE masjid SET radius" style="shape=cylinder3;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1"><mxGeometry x="320" y="205" width="120" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="m6" value="Response Success" style="rounded=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry x="80" y="210" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="m7" value="Render Ulang Lingkaran (Circle)
di Leaflet.js" style="rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="80" y="320" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="m8" value="Hitung Ulang Status Terjangkau
(Warna Marker Keluarga)" style="rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="300" y="320" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="m9" value="Perbarui Card Statistik
Wilayah" style="rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"><mxGeometry x="520" y="320" width="160" height="60" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="em_1" edge="1" source="m1" target="m2" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="em_2" edge="1" source="m2" target="m3" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="em_3" edge="1" source="m3" target="m4" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="em_4" edge="1" source="m4" target="m5" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="em_5" edge="1" source="m5" target="m6" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="em_6" edge="1" source="m6" target="m7" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="em_7" edge="1" source="m7" target="m8" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="em_8" edge="1" source="m8" target="m9" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,46 @@
|
||||
<mxfile host="Electron" modified="2024" agent="Mozilla/5.0" version="22.1.2" type="device">
|
||||
<diagram id="diag1" name="Page-1">
|
||||
<mxGraphModel dx="1000" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<mxCell id="p1_title" value="Arsitektur Sistem WebGIS GeoPoverty v2.0" style="text;html=1;align=center;fontStyle=1;fontSize=18;fontColor=#1e3a5f;" vertex="1" parent="1"><mxGeometry x="80" y="20" width="800" height="40" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Users -->
|
||||
<mxCell id="p1_user1" value="👤 PUBLIK / WARGA
(Akses tanpa login)" style="ellipse;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=1;fontSize=12;" vertex="1" parent="1"><mxGeometry x="120" y="80" width="200" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_user2" value="👑 PENGURUS / PIMPINAN
(Akses dengan login)" style="ellipse;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1;fontSize=12;" vertex="1" parent="1"><mxGeometry x="580" y="80" width="200" height="70" as="geometry"/></mxCell>
|
||||
|
||||
<!-- PRESENTATION LAYER -->
|
||||
<mxCell id="p1_pres" value="🖥️ PRESENTATION LAYER (Client Browser)" style="swimlane;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;fontSize=13;startSize=35;" vertex="1" parent="1"><mxGeometry x="80" y="200" width="800" height="150" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_html" value="UI & Layout
(index.html, lumina.css)" style="rounded=1;fillColor=#fff;strokeColor=#6c8ebf;fontSize=11;" vertex="1" parent="p1_pres"><mxGeometry x="40" y="55" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_mapjs" value="Peta Interaktif
(map.js, Leaflet, OSM)" style="rounded=1;fillColor=#fff;strokeColor=#6c8ebf;fontSize=11;" vertex="1" parent="p1_pres"><mxGeometry x="210" y="55" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_loadjs" value="Logika Aplikasi & Fetch
(loadData.js, auth.js)" style="rounded=1;fillColor=#fff;strokeColor=#6c8ebf;fontSize=11;" vertex="1" parent="p1_pres"><mxGeometry x="380" y="55" width="140" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_moduljs" value="Modul Fitur
(pesan.js, laporan.js, dll)" style="rounded=1;fillColor=#fff;strokeColor=#6c8ebf;fontSize=11;" vertex="1" parent="p1_pres"><mxGeometry x="550" y="55" width="140" height="60" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Arrow User to Pres -->
|
||||
<mxCell id="e1" edge="1" source="p1_user1" target="p1_pres" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="e2" edge="1" source="p1_user2" target="p1_pres" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<!-- BUSINESS LOGIC LAYER -->
|
||||
<mxCell id="p1_biz" value="⚙️ BUSINESS LOGIC LAYER (Server — PHP)" style="swimlane;fillColor=#d5e8d4;strokeColor=#82b366;fontStyle=1;fontSize=13;startSize=35;" vertex="1" parent="1"><mxGeometry x="80" y="420" width="800" height="150" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_auth" value="Autentikasi & Sesi
(login.php, check_auth.php)" style="rounded=1;fillColor=#fff;strokeColor=#82b366;fontSize=11;" vertex="1" parent="p1_biz"><mxGeometry x="40" y="55" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_crud1" value="CRUD Utama
(kemiskinan, masjid)" style="rounded=1;fillColor=#fff;strokeColor=#82b366;fontSize=11;" vertex="1" parent="p1_biz"><mxGeometry x="230" y="55" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_crud2" value="Modul Tambahan
(anggota, bantuan, pesan)" style="rounded=1;fillColor=#fff;strokeColor=#82b366;fontSize=11;" vertex="1" parent="p1_biz"><mxGeometry x="420" y="55" width="160" height="60" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_export" value="Utilitas
(export_csv.php, koneksi.php)" style="rounded=1;fillColor=#fff;strokeColor=#82b366;fontSize=11;" vertex="1" parent="p1_biz"><mxGeometry x="610" y="55" width="160" height="60" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Arrow Pres to Biz -->
|
||||
<mxCell id="e3" value="REST API (Fetch POST/GET JSON)" style="edgeStyle=orthogonalEdgeStyle;fontSize=11;fontStyle=1;" edge="1" source="p1_pres" target="p1_biz" parent="1"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="480" y="380"/></Array></mxGeometry></mxCell>
|
||||
|
||||
<!-- DATA LAYER -->
|
||||
<mxCell id="p1_data" value="🗄️ DATA LAYER (MySQL — webgis_kemiskinan)" style="swimlane;fillColor=#ffe6cc;strokeColor=#d79b00;fontStyle=1;fontSize=13;startSize=35;" vertex="1" parent="1"><mxGeometry x="80" y="640" width="800" height="140" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_db1" value="Data Spasial
(kemiskinan, masjid)" style="shape=cylinder3;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff;strokeColor=#d79b00;fontSize=11;" vertex="1" parent="p1_data"><mxGeometry x="150" y="50" width="120" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_db2" value="Data Relasional
(anggota, bantuan, dll)" style="shape=cylinder3;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff;strokeColor=#d79b00;fontSize=11;" vertex="1" parent="p1_data"><mxGeometry x="340" y="50" width="120" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="p1_db3" value="Data Pengguna
(pengguna)" style="shape=cylinder3;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff;strokeColor=#d79b00;fontSize=11;" vertex="1" parent="p1_data"><mxGeometry x="530" y="50" width="120" height="70" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Arrow Biz to Data -->
|
||||
<mxCell id="e4" value="MySQLi Prepared Statements" style="edgeStyle=orthogonalEdgeStyle;fontSize=11;fontStyle=1;" edge="1" source="p1_biz" target="p1_data" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,51 @@
|
||||
<mxfile host="Electron" modified="2024" agent="Mozilla/5.0" version="22.1.2" type="device">
|
||||
<diagram id="diag1" name="Page-1">
|
||||
<mxGraphModel dx="1000" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="e_title" value="Entity Relationship Diagram (ERD) - WebGIS GeoPoverty v2.0" style="text;html=1;align=center;fontStyle=1;fontSize=18;" vertex="1" parent="1"><mxGeometry x="40" y="20" width="900" height="40" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Table: pengguna -->
|
||||
<mxCell id="t_pengguna" value="<b>pengguna</b><hr><p style="margin:0;line-height:1.5;">PK id : INT<br>username : VARCHAR<br>password_hash : VARCHAR<br>nama_lengkap : VARCHAR<br>role : ENUM<br>FK masjid_id : INT<br>aktif : TINYINT</p>" style="shape=rectangle;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;spacingLeft=10;spacingTop=10;spacingRight=10;spacingBottom=10;fontSize=12;html=1;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="40" y="100" width="220" height="200" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Table: masjid -->
|
||||
<mxCell id="t_masjid" value="<b>masjid (rumah ibadah)</b><hr><p style="margin:0;line-height:1.5;">PK id : INT<br>nama_masjid : VARCHAR<br>pic_masjid : VARCHAR<br>alamat : TEXT<br>latitude : DECIMAL<br>longitude : DECIMAL<br>radius : INT<br>jenis_ibadah : ENUM</p>" style="shape=rectangle;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;spacingLeft=10;spacingTop=10;spacingRight=10;spacingBottom=10;fontSize=12;html=1;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="360" y="100" width="220" height="200" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Table: kemiskinan -->
|
||||
<mxCell id="t_kemiskinan" value="<b>kemiskinan</b><hr><p style="margin:0;line-height:1.5;">PK id : INT<br>nama_kepala_keluarga : VARCHAR<br>jumlah_kk : INT<br>alamat : TEXT<br>latitude : DECIMAL<br>longitude : DECIMAL<br>status_keluarga : ENUM</p>" style="shape=rectangle;fillColor=#d5e8d4;strokeColor=#82b366;align=left;verticalAlign=top;spacingLeft=10;spacingTop=10;spacingRight=10;spacingBottom=10;fontSize=12;html=1;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="680" y="100" width="220" height="200" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Table: pesan_komunikasi -->
|
||||
<mxCell id="t_pesan" value="<b>pesan_komunikasi</b><hr><p style="margin:0;line-height:1.5;">PK id : INT<br>FK pengirim_id : INT<br>FK penerima_id : INT<br>judul : VARCHAR<br>isi : TEXT<br>jenis : ENUM<br>status : ENUM</p>" style="shape=rectangle;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;spacingLeft=10;spacingTop=10;spacingRight=10;spacingBottom=10;fontSize=12;html=1;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="40" y="360" width="220" height="200" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Table: histori_bantuan -->
|
||||
<mxCell id="t_bantuan" value="<b>histori_bantuan</b><hr><p style="margin:0;line-height:1.5;">PK id : INT<br>FK kemiskinan_id : INT<br>FK masjid_id : INT<br>jenis_bantuan : ENUM<br>jumlah_bantuan : DECIMAL<br>tanggal_bantuan : DATE<br>status_penyaluran : ENUM</p>" style="shape=rectangle;fillColor=#f8cecc;strokeColor=#b85450;align=left;verticalAlign=top;spacingLeft=10;spacingTop=10;spacingRight=10;spacingBottom=10;fontSize=12;html=1;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="360" y="360" width="220" height="200" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Table: anggota_keluarga -->
|
||||
<mxCell id="t_anggota" value="<b>anggota_keluarga</b><hr><p style="margin:0;line-height:1.5;">PK id : INT<br>FK kemiskinan_id : INT<br>nama : VARCHAR<br>tanggal_lahir : DATE<br>jenis_kelamin : ENUM<br>hubungan_keluarga : ENUM<br>status_hidup : ENUM<br>pendidikan : ENUM</p>" style="shape=rectangle;fillColor=#ffe6cc;strokeColor=#d79b00;align=left;verticalAlign=top;spacingLeft=10;spacingTop=10;spacingRight=10;spacingBottom=10;fontSize=12;html=1;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="680" y="360" width="220" height="200" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Table: laporan_kondisi -->
|
||||
<mxCell id="t_laporan" value="<b>laporan_kondisi</b><hr><p style="margin:0;line-height:1.5;">PK id : INT<br>FK kemiskinan_id : INT<br>nama_pelapor : VARCHAR<br>nomor_wa : VARCHAR<br>kategori : ENUM<br>deskripsi : TEXT<br>status_laporan : ENUM</p>" style="shape=rectangle;fillColor=#f5f5f5;strokeColor=#666666;align=left;verticalAlign=top;spacingLeft=10;spacingTop=10;spacingRight=10;spacingBottom=10;fontSize=12;html=1;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="680" y="620" width="220" height="180" as="geometry"/></mxCell>
|
||||
|
||||
<!-- Relationships -->
|
||||
<!-- pengguna to masjid -->
|
||||
<mxCell id="r1" value="1..N" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;endFill=1;startArrow=none;fontSize=11;" edge="1" source="t_pengguna" target="t_masjid" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<!-- pesan to pengguna -->
|
||||
<mxCell id="r2" value="N..1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;endFill=1;startArrow=none;fontSize=11;" edge="1" source="t_pesan" target="t_pengguna" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<!-- bantuan to masjid -->
|
||||
<mxCell id="r3" value="N..1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;endFill=1;startArrow=none;fontSize=11;" edge="1" source="t_bantuan" target="t_masjid" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<!-- bantuan to kemiskinan -->
|
||||
<mxCell id="r4" value="N..1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;endFill=1;startArrow=none;fontSize=11;" edge="1" source="t_bantuan" target="t_kemiskinan" parent="1"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="470" y="330"/><mxPoint x="790" y="330"/></Array></mxGeometry></mxCell>
|
||||
|
||||
<!-- anggota to kemiskinan -->
|
||||
<mxCell id="r5" value="N..1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;endFill=1;startArrow=none;fontSize=11;" edge="1" source="t_anggota" target="t_kemiskinan" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<!-- laporan to kemiskinan -->
|
||||
<mxCell id="r6" value="N..1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;endFill=1;startArrow=none;fontSize=11;" edge="1" source="t_laporan" target="t_kemiskinan" parent="1"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="950" y="710"/><mxPoint x="950" y="200"/></Array></mxGeometry></mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,13 @@
|
||||
# Penjelasan Diagram WebGIS Kemiskinan & Masjid
|
||||
|
||||
### 1. Arsitektur Sistem (Three-Tier)
|
||||
Sistem ini dibangun menggunakan arsitektur *Three-Tier* yang memisahkan aplikasi menjadi tiga lapisan utama. Lapisan pertama adalah *Presentation Layer* (Client) yang menggunakan HTML, CSS, JavaScript murni, dan Leaflet.js untuk menampilkan antarmuka peta interaktif kepada pengguna. Lapisan kedua adalah *Business Logic Layer* (Server) berbasis PHP murni yang menangani pemrosesan data, validasi, dan operasi CRUD melalui API *endpoints*. Lapisan ketiga adalah *Data Layer* berupa database MySQL (`webgis_kemiskinan`) yang bertugas menyimpan data spasial dan atribut dari entitas kemiskinan serta masjid secara persisten.
|
||||
|
||||
### 2. Alur Data Tambah Titik Kemiskinan
|
||||
Proses penambahan data kemiskinan dimulai ketika operator mengklik lokasi tertentu pada peta yang memicu munculnya modal *form* input data. Setelah operator mengisi atribut seperti nama kepala keluarga, jumlah anggota, dan alamat, sistem pada sisi *client* (JavaScript) akan memvalidasi input tersebut sebelum mengirimkannya ke server melalui *Fetch API* (metode POST). Skrip PHP di server kemudian akan memvalidasi ulang data dan melakukan *query INSERT* ke database MySQL; jika berhasil, server mengembalikan respons sukses sehingga antarmuka peta dan daftar pada *sidebar* akan diperbarui secara *real-time* untuk menampilkan titik koordinat kemiskinan yang baru.
|
||||
|
||||
### 3. Alur Data Update Radius Masjid
|
||||
Pembaruan radius layanan masjid dilakukan langsung melalui *card* masjid yang ada di *sidebar* tanpa perlu membuka halaman baru. Saat operator mengubah nilai radius dan menekan tombol *Update*, sistem memastikan nilai yang dimasukkan valid (berada dalam rentang 100 hingga 5.000 meter). Data ID masjid dan radius baru dikirimkan secara asinkron ke server PHP untuk memperbarui data di database (`UPDATE`), lalu secara instan *client* akan menyesuaikan ukuran lingkaran (*circle*) radius masjid pada peta serta menghitung dan memperbarui ulang warna marker kemiskinan (merah atau hijau) berdasarkan jangkauan radius yang baru.
|
||||
|
||||
### 4. Entity Relationship Diagram (ERD)
|
||||
ERD sistem ini terdiri dari dua entitas utama yang saling independen secara struktural yaitu `kemiskinan` dan `masjid`, di mana masing-masing memiliki atribut *Primary Key* (`id`) serta menyimpan data koordinat spasial (`latitude` dan `longitude`). Keduanya tidak dihubungkan oleh *Foreign Key* relasional secara fisik di tingkat database, melainkan terhubung melalui relasi spasial logis yang dihitung pada sisi *client* (menggunakan fungsi jarak Leaflet). Status jangkauan sebuah keluarga miskin ditentukan secara dinamis dengan membandingkan jarak koordinat kemiskinan ke koordinat masjid terdekat; jika jarak tersebut lebih kecil atau sama dengan atribut `radius` milik masjid, maka keluarga tersebut masuk dalam area terjangkau.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,244 @@
|
||||
/* ============================================================
|
||||
js/anggota.js — Profil individu anggota keluarga v2.1
|
||||
============================================================ */
|
||||
|
||||
window.labelHubungan = {
|
||||
kepala_keluarga:'Kepala Keluarga', istri:'Istri/Suami', anak:'Anak',
|
||||
orang_tua:'Orang Tua', saudara:'Saudara', lainnya:'Lainnya'
|
||||
};
|
||||
window.labelStatusHidup = {
|
||||
hidup:'Hidup', meninggal:'Meninggal', pindah:'Pindah', menikah_keluar:'Menikah/Keluar'
|
||||
};
|
||||
window.labelPendidikan = {
|
||||
belum_sekolah:'Belum Sekolah', paud:'PAUD/TK', sd:'SD', smp:'SMP', sma:'SMA/SMK',
|
||||
kuliah:'Kuliah/S1', pesantren:'Pesantren', pelatihan:'Pelatihan Vokasi',
|
||||
bekerja:'Bekerja', tidak_sekolah:'Tidak Sekolah'
|
||||
};
|
||||
|
||||
window.getSaranPendidikan = function (umur) {
|
||||
if (umur === null || umur < 0) return null;
|
||||
if (umur < 5) return { saran: 'PAUD/TK atau belum sekolah' };
|
||||
if (umur <= 12) return { saran: 'SD (usia ' + umur + ' tahun)' };
|
||||
if (umur <= 15) return { saran: 'SMP (usia ' + umur + ' tahun)' };
|
||||
if (umur <= 18) return { saran: 'SMA/SMK (usia ' + umur + ' tahun)' };
|
||||
if (umur <= 24) return { saran: 'Kuliah / Pelatihan Vokasi (usia ' + umur + ' tahun)' };
|
||||
return { saran: 'Pelatihan / Bekerja (usia ' + umur + ' tahun)' };
|
||||
};
|
||||
|
||||
window.validasiPendidikan = function (umur, pendidikan) {
|
||||
if (umur === null || !pendidikan) return null;
|
||||
var map = {
|
||||
belum_sekolah:[0,5], paud:[3,7], sd:[6,13], smp:[12,16],
|
||||
sma:[15,19], kuliah:[18,30], pesantren:[6,25],
|
||||
pelatihan:[15,99], bekerja:[17,99], tidak_sekolah:[0,99]
|
||||
};
|
||||
var range = map[pendidikan];
|
||||
if (!range) return null;
|
||||
if (umur < range[0] || umur > range[1]) {
|
||||
var saran = window.getSaranPendidikan(umur);
|
||||
return { cocok: false, pesan: 'Umur ' + umur + ' th tidak sesuai dengan "' +
|
||||
(window.labelPendidikan[pendidikan]||pendidikan) + '".' +
|
||||
(saran ? ' Disarankan: ' + saran.saran : '') };
|
||||
}
|
||||
return { cocok: true, pesan: null };
|
||||
};
|
||||
|
||||
window._hitungUmur = function (tglLahir) {
|
||||
if (!tglLahir) return null;
|
||||
var today = new Date(), lahir = new Date(tglLahir);
|
||||
if (isNaN(lahir.getTime())) return null;
|
||||
var u = today.getFullYear() - lahir.getFullYear();
|
||||
var m = today.getMonth() - lahir.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < lahir.getDate())) u--;
|
||||
return u;
|
||||
};
|
||||
|
||||
/* ── Buka modal tambah / edit ── */
|
||||
window.openTambahAnggotaModal = function (kemiskinanId, namaKK) {
|
||||
document.getElementById('anggota-kem-id').value = kemiskinanId;
|
||||
document.getElementById('anggota-edit-id').value = '';
|
||||
document.getElementById('anggota-nama').value = '';
|
||||
document.getElementById('anggota-tgl-lahir').value = '';
|
||||
document.getElementById('anggota-jk').value = 'laki_laki';
|
||||
document.getElementById('anggota-hubungan').value = 'anak';
|
||||
document.getElementById('anggota-status').value = 'hidup';
|
||||
document.getElementById('anggota-pendidikan').value = 'belum_sekolah';
|
||||
document.getElementById('anggota-catatan').value = '';
|
||||
var s = document.getElementById('anggota-saran');
|
||||
if (s) { s.textContent = ''; s.className = 'saran-box'; s.style.display = 'none'; }
|
||||
document.getElementById('modal-anggota-title').textContent = 'Tambah Anggota — ' + window.escapeHtml(namaKK);
|
||||
|
||||
var modal = document.getElementById('modal-anggota');
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('anggota-nama').focus();
|
||||
modal.onclick = function (e) { if (e.target === this) this.style.display = 'none'; };
|
||||
|
||||
window._bindAnggotaEvents();
|
||||
document.getElementById('btn-submit-anggota').onclick = window._submitTambahAnggota;
|
||||
};
|
||||
|
||||
window.openEditAnggotaModal = function (a, namaKK) {
|
||||
document.getElementById('anggota-kem-id').value = a.kemiskinan_id;
|
||||
document.getElementById('anggota-edit-id').value = a.id;
|
||||
document.getElementById('anggota-nama').value = a.nama;
|
||||
document.getElementById('anggota-tgl-lahir').value = a.tanggal_lahir || '';
|
||||
document.getElementById('anggota-jk').value = a.jenis_kelamin;
|
||||
document.getElementById('anggota-hubungan').value = a.hubungan_keluarga;
|
||||
document.getElementById('anggota-status').value = a.status_hidup;
|
||||
document.getElementById('anggota-pendidikan').value = a.pendidikan;
|
||||
document.getElementById('anggota-catatan').value = a.catatan_khusus || '';
|
||||
document.getElementById('modal-anggota-title').textContent = 'Edit Anggota — ' + window.escapeHtml(namaKK);
|
||||
|
||||
var modal = document.getElementById('modal-anggota');
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('anggota-nama').focus();
|
||||
modal.onclick = function (e) { if (e.target === this) this.style.display = 'none'; };
|
||||
|
||||
window._bindAnggotaEvents();
|
||||
window._updateAnggotaSaran();
|
||||
document.getElementById('btn-submit-anggota').onclick = window._submitEditAnggota;
|
||||
};
|
||||
|
||||
window._bindAnggotaEvents = function () {
|
||||
var tgl = document.getElementById('anggota-tgl-lahir');
|
||||
var pend = document.getElementById('anggota-pendidikan');
|
||||
if (!tgl._bound) {
|
||||
tgl.addEventListener('change', window._updateAnggotaSaran);
|
||||
pend.addEventListener('change', window._updateAnggotaSaran);
|
||||
tgl._bound = true;
|
||||
}
|
||||
};
|
||||
|
||||
window._updateAnggotaSaran = function () {
|
||||
var tgl = document.getElementById('anggota-tgl-lahir').value;
|
||||
var pend = document.getElementById('anggota-pendidikan').value;
|
||||
var s = document.getElementById('anggota-saran');
|
||||
if (!s) return;
|
||||
var umur = window._hitungUmur(tgl);
|
||||
if (umur === null) { s.style.display = 'none'; s.textContent = ''; return; }
|
||||
s.style.display = 'block';
|
||||
var v = window.validasiPendidikan(umur, pend);
|
||||
if (!v) { s.className = 'saran-box info'; s.textContent = 'Umur: ' + umur + ' tahun'; return; }
|
||||
if (v.cocok) {
|
||||
var sar = window.getSaranPendidikan(umur);
|
||||
s.className = 'saran-box ok';
|
||||
s.textContent = '✓ Umur ' + umur + ' tahun — pendidikan sesuai.';
|
||||
} else {
|
||||
s.className = 'saran-box warn';
|
||||
s.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i> ' + v.pesan;
|
||||
}
|
||||
};
|
||||
|
||||
window._submitTambahAnggota = async function () {
|
||||
var btn = document.getElementById('btn-submit-anggota');
|
||||
btn.disabled = true; btn.textContent = 'Menyimpan...';
|
||||
try {
|
||||
await window.postForm('php/create_anggota.php', window._collectAnggotaForm());
|
||||
document.getElementById('modal-anggota').style.display = 'none';
|
||||
window.showToast('Anggota berhasil ditambahkan.', 'success');
|
||||
if (window._currentDetailItem) window.openDetailPanel(window._currentDetailItem);
|
||||
} catch (err) { window.showToast('Gagal: ' + err.message, 'error'); }
|
||||
finally { btn.disabled = false; btn.textContent = 'Simpan'; }
|
||||
};
|
||||
|
||||
window._submitEditAnggota = async function () {
|
||||
var btn = document.getElementById('btn-submit-anggota');
|
||||
btn.disabled = true; btn.textContent = 'Menyimpan...';
|
||||
var payload = window._collectAnggotaForm();
|
||||
payload.id = document.getElementById('anggota-edit-id').value;
|
||||
try {
|
||||
await window.postForm('php/update_anggota.php', payload);
|
||||
document.getElementById('modal-anggota').style.display = 'none';
|
||||
window.showToast('Data anggota diperbarui.', 'success');
|
||||
if (window._currentDetailItem) window.openDetailPanel(window._currentDetailItem);
|
||||
} catch (err) { window.showToast('Gagal: ' + err.message, 'error'); }
|
||||
finally { btn.disabled = false; btn.textContent = 'Simpan'; }
|
||||
};
|
||||
|
||||
window._collectAnggotaForm = function () {
|
||||
return {
|
||||
kemiskinan_id: document.getElementById('anggota-kem-id').value,
|
||||
nama: document.getElementById('anggota-nama').value.trim(),
|
||||
tanggal_lahir: document.getElementById('anggota-tgl-lahir').value,
|
||||
jenis_kelamin: document.getElementById('anggota-jk').value,
|
||||
hubungan_keluarga: document.getElementById('anggota-hubungan').value,
|
||||
status_hidup: document.getElementById('anggota-status').value,
|
||||
pendidikan: document.getElementById('anggota-pendidikan').value,
|
||||
catatan_khusus: document.getElementById('anggota-catatan').value.trim()
|
||||
};
|
||||
};
|
||||
|
||||
window.deleteAnggota = async function (id, nama) {
|
||||
if (!confirm('Hapus anggota "' + nama + '"?')) return;
|
||||
try {
|
||||
await window.postForm('php/delete_anggota.php', { id });
|
||||
window.showToast('Anggota dihapus.', 'success');
|
||||
if (window._currentDetailItem) window.openDetailPanel(window._currentDetailItem);
|
||||
} catch (err) { window.showToast('Gagal: ' + err.message, 'error'); }
|
||||
};
|
||||
|
||||
/* ── Render tab anggota ── */
|
||||
window.renderTabAnggota = async function (item) {
|
||||
var container = document.getElementById('detail-tab-anggota');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="loading">Memuat data anggota...</div>';
|
||||
var data = await window.fetchJson('php/read_anggota.php?kemiskinan_id=' + item.id, []);
|
||||
container.innerHTML = '';
|
||||
|
||||
if (window.appAuth.role === 'pengurus_masjid') {
|
||||
var btnAdd = document.createElement('button');
|
||||
btnAdd.className = 'btn btn-add';
|
||||
btnAdd.innerHTML = '+ Tambah Anggota';
|
||||
btnAdd.onclick = function () { window.openTambahAnggotaModal(item.id, item.nama_kepala_keluarga); };
|
||||
container.appendChild(btnAdd);
|
||||
}
|
||||
|
||||
if (!data.length) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'empty';
|
||||
empty.textContent = 'Belum ada data anggota.';
|
||||
container.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(function (a) {
|
||||
var umur = a.umur !== null ? parseInt(a.umur) : null;
|
||||
var umurTxt = umur !== null ? umur + ' tahun' : '—';
|
||||
var tglTxt = a.tgl_lahir_fmt || '—';
|
||||
var val = umur !== null ? window.validasiPendidikan(umur, a.pendidikan) : null;
|
||||
|
||||
var card = document.createElement('div');
|
||||
card.className = 'anggota-card ' + a.status_hidup;
|
||||
|
||||
/* Status chip */
|
||||
var chipColors = { hidup:'chip-green', meninggal:'chip-gray', pindah:'chip-amber', menikah_keluar:'chip-purple' };
|
||||
var chipColor = chipColors[a.status_hidup] || 'chip-gray';
|
||||
|
||||
card.innerHTML =
|
||||
'<div class="ang-head">' +
|
||||
'<span class="ang-name">' + window.escapeHtml(a.nama) + '</span>' +
|
||||
'<span class="chip ' + chipColor + '">' + (window.labelStatusHidup[a.status_hidup] || a.status_hidup) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="ang-row"><span class="ang-lbl">Hubungan</span>' + (window.labelHubungan[a.hubungan_keluarga] || a.hubungan_keluarga) + '</div>' +
|
||||
'<div class="ang-row"><span class="ang-lbl">Lahir</span>' + tglTxt + ' <em class="umur-em">(' + umurTxt + ')</em></div>' +
|
||||
'<div class="ang-row"><span class="ang-lbl">JK</span>' + (a.jenis_kelamin === 'laki_laki' ? 'Laki-laki' : 'Perempuan') + '</div>' +
|
||||
'<div class="ang-row"><span class="ang-lbl">Pendidikan</span>' + (window.labelPendidikan[a.pendidikan] || a.pendidikan) + '</div>' +
|
||||
(val && !val.cocok ? '<div class="ang-warn"><i class="fa-solid fa-triangle-exclamation"></i> ' + window.escapeHtml(val.pesan) + '</div>' : '') +
|
||||
(a.catatan_khusus ? '<div class="ang-row"><span class="ang-lbl">Catatan</span><em>' + window.escapeHtml(a.catatan_khusus) + '</em></div>' : '') +
|
||||
(a.updated_at_fmt ? '<div class="ang-row"><span class="ang-lbl">Update</span><em class="umur-em">' + a.updated_at_fmt + '</em></div>' : '');
|
||||
|
||||
if (window.appAuth.role === 'pengurus_masjid') {
|
||||
var acts = document.createElement('div');
|
||||
acts.className = 'ang-acts';
|
||||
var btnE = document.createElement('button');
|
||||
btnE.className = 'btn btn-edit'; btnE.innerHTML = '<i class="fa-solid fa-pen"></i> Edit';
|
||||
btnE.onclick = (function(aa){ return function(){ window.openEditAnggotaModal(aa, item.nama_kepala_keluarga); }; })(a);
|
||||
var btnH = document.createElement('button');
|
||||
btnH.className = 'btn btn-delete'; btnH.textContent = 'Hapus';
|
||||
btnH.onclick = (function(aa){ return function(){ window.deleteAnggota(aa.id, aa.nama); }; })(a);
|
||||
acts.appendChild(btnE); acts.appendChild(btnH);
|
||||
card.appendChild(acts);
|
||||
}
|
||||
container.appendChild(card);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
/* ============================================================
|
||||
js/auth.js — Login / Logout / Role management v2.1
|
||||
============================================================ */
|
||||
|
||||
window.appAuth = { loggedIn: false, nama: '', role: '' };
|
||||
|
||||
window.checkAuthState = async function () {
|
||||
try {
|
||||
var res = await fetch('php/check_auth.php', { cache: 'no-store' });
|
||||
var data = await res.json();
|
||||
window.appAuth.loggedIn = !!data.loggedIn;
|
||||
window.appAuth.nama = data.nama || '';
|
||||
window.appAuth.role = data.role || '';
|
||||
} catch (e) {
|
||||
window.appAuth = { loggedIn: false, nama: '', role: '' };
|
||||
}
|
||||
window.renderAuthBar();
|
||||
if (!window.appAuth.loggedIn) {
|
||||
window.openLoginModal();
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Render auth area di navbar ── */
|
||||
window.renderAuthBar = function () {
|
||||
var area = document.getElementById('auth-area');
|
||||
if (!area) return;
|
||||
area.innerHTML = '';
|
||||
|
||||
/* Clean up old nav buttons */
|
||||
var navMenus = document.getElementById('nav-menus');
|
||||
if (navMenus) {
|
||||
['btn-pesan-nav','btn-laporan-mgr-nav'].forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.remove();
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.appAuth.loggedIn) {
|
||||
/* Show Masuk button */
|
||||
var btnMasuk = document.createElement('button');
|
||||
btnMasuk.className = 'nav-btn';
|
||||
btnMasuk.id = 'btn-masuk-nav';
|
||||
btnMasuk.innerHTML = '<i class="fa-solid fa-right-to-bracket"></i> Masuk';
|
||||
btnMasuk.onclick = window.openLoginModal;
|
||||
area.appendChild(btnMasuk);
|
||||
} else {
|
||||
/* Role chip */
|
||||
var chip = document.createElement('span');
|
||||
var isPimpinan = window.appAuth.role === 'pimpinan_daerah';
|
||||
chip.className = 'role-chip ' + (isPimpinan ? 'pimpinan' : 'pengurus');
|
||||
chip.innerHTML = isPimpinan
|
||||
? '<i class="fa-solid fa-crown"></i> Pimpinan'
|
||||
: '<i class="fa-solid fa-mosque"></i> Pengurus';
|
||||
area.appendChild(chip);
|
||||
|
||||
var name = document.createElement('span');
|
||||
name.className = 'auth-name';
|
||||
name.textContent = window.appAuth.nama;
|
||||
area.appendChild(name);
|
||||
|
||||
/* Pengurus: add Pesan & Laporan buttons to nav-menus */
|
||||
if (window.appAuth.role === 'pengurus_masjid' && navMenus) {
|
||||
var btnMsg = document.createElement('button');
|
||||
btnMsg.className = 'nav-btn msg-btn';
|
||||
btnMsg.id = 'btn-pesan-nav';
|
||||
btnMsg.innerHTML = '<i class="fa-solid fa-message"></i> Pesan <span class="lumina-badge" id="nav-badge-pesan" style="display:none; background:var(--lumina-primary);"></span>';
|
||||
btnMsg.onclick = window.openPesanPanel;
|
||||
navMenus.appendChild(btnMsg);
|
||||
|
||||
var btnLap = document.createElement('button');
|
||||
btnLap.className = 'nav-btn mgr-btn';
|
||||
btnLap.id = 'btn-laporan-mgr-nav';
|
||||
btnLap.innerHTML = '<i class="fa-solid fa-clipboard-list"></i> Laporan <span class="lumina-badge" id="nav-badge-laporan" style="display:none; background:var(--lumina-error);"></span>';
|
||||
btnLap.onclick = window.openLaporanPanel;
|
||||
navMenus.appendChild(btnLap);
|
||||
}
|
||||
|
||||
var btnLogout = document.createElement('button');
|
||||
btnLogout.className = 'nav-btn logout-btn';
|
||||
btnLogout.id = 'btn-logout-nav';
|
||||
btnLogout.innerHTML = '<i class="fa-solid fa-arrow-right-from-bracket"></i> Keluar';
|
||||
btnLogout.onclick = window.doLogout;
|
||||
area.appendChild(btnLogout);
|
||||
}
|
||||
|
||||
window.updateToolbarForRole();
|
||||
|
||||
if (window.pollLaporanNotification) window.pollLaporanNotification();
|
||||
if (window.pollPesanNotification) window.pollPesanNotification();
|
||||
};
|
||||
|
||||
|
||||
/* ── Role-based toolbar control ── */
|
||||
window.updateToolbarForRole = function () {
|
||||
var editBtn = document.getElementById('luminaModeEdit');
|
||||
var menuKeluarga = document.getElementById('menu-tambah-keluarga');
|
||||
var menuIbadah = document.getElementById('menu-tambah-ibadah');
|
||||
|
||||
var isPengurus = window.appAuth && window.appAuth.loggedIn && window.appAuth.role === 'pengurus_masjid';
|
||||
|
||||
if (isPengurus) {
|
||||
if (editBtn) editBtn.style.display = 'block';
|
||||
if (menuKeluarga) menuKeluarga.style.display = 'flex';
|
||||
if (menuIbadah) menuIbadah.style.display = 'flex';
|
||||
} else {
|
||||
if (editBtn) editBtn.style.display = 'none';
|
||||
if (menuKeluarga) menuKeluarga.style.display = 'none';
|
||||
if (menuIbadah) menuIbadah.style.display = 'none';
|
||||
if (window.appState && window.appState.mode === 'edit') window.setMode('view');
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Open login modal ── */
|
||||
window.openLoginModal = function () {
|
||||
var overlay = document.getElementById('modal-login');
|
||||
if (!overlay) return;
|
||||
document.getElementById('login-username').value = '';
|
||||
document.getElementById('login-password').value = '';
|
||||
document.getElementById('login-error').textContent = '';
|
||||
overlay.style.display = 'flex';
|
||||
document.getElementById('login-username').focus();
|
||||
// Do not allow closing
|
||||
};
|
||||
|
||||
/* ── Login submit ── */
|
||||
window.doLogin = async function (e) {
|
||||
e.preventDefault();
|
||||
var btn = document.getElementById('btn-submit-login');
|
||||
var errEl = document.getElementById('login-error');
|
||||
var uname = document.getElementById('login-username').value.trim();
|
||||
var pass = document.getElementById('login-password').value;
|
||||
|
||||
if (!uname || !pass) {
|
||||
errEl.textContent = 'Username dan password wajib diisi.';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true; btn.textContent = 'Masuk...';
|
||||
errEl.textContent = '';
|
||||
errEl.style.display = 'none';
|
||||
|
||||
try {
|
||||
var res = await fetch('php/login.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ username: uname, password: pass })
|
||||
});
|
||||
var data = await res.json();
|
||||
if (data.success) {
|
||||
window.appAuth = { loggedIn: true, nama: data.nama, role: data.role };
|
||||
document.getElementById('modal-login').style.display = 'none';
|
||||
window.renderAuthBar();
|
||||
window.loadAllData();
|
||||
window.showToast('Selamat datang, ' + data.nama + '!', 'success');
|
||||
} else {
|
||||
errEl.textContent = data.error || 'Login gagal.';
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
errEl.textContent = 'Koneksi gagal: ' + err.message;
|
||||
errEl.style.display = 'block';
|
||||
} finally {
|
||||
btn.disabled = false; btn.innerHTML = 'Masuk Sekarang <i class="fa-solid fa-arrow-right"></i>';
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Logout ── */
|
||||
window.doLogout = async function () {
|
||||
await fetch('php/logout.php', { cache: 'no-store' });
|
||||
window.appAuth = { loggedIn: false, nama: '', role: '' };
|
||||
window.renderAuthBar();
|
||||
if (window.appState && window.appState.mode === 'edit') window.setMode('view');
|
||||
window.loadAllData();
|
||||
window.openLoginModal();
|
||||
window.showToast('Anda telah keluar.', 'info');
|
||||
};
|
||||
|
||||
/* ── Toggle Password Visibility ── */
|
||||
window.togglePassword = function () {
|
||||
var passInput = document.getElementById('login-password');
|
||||
var eyeIcon = document.getElementById('login-password-eye');
|
||||
if (passInput.type === 'password') {
|
||||
passInput.type = 'text';
|
||||
eyeIcon.classList.remove('fa-eye');
|
||||
eyeIcon.classList.add('fa-eye-slash');
|
||||
} else {
|
||||
passInput.type = 'password';
|
||||
eyeIcon.classList.remove('fa-eye-slash');
|
||||
eyeIcon.classList.add('fa-eye');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/* ============================================================
|
||||
js/bantuan.js — Histori bantuan v2.1
|
||||
============================================================ */
|
||||
|
||||
window.labelJenisBantuan = {
|
||||
sembako:'Sembako', uang:'Uang/Dana', pendidikan:'Pendidikan',
|
||||
kesehatan:'Kesehatan', pakaian:'Pakaian', pelatihan:'Pelatihan', lainnya:'Lainnya'
|
||||
};
|
||||
window.labelStatusBantuan = {
|
||||
tersalur:'Tersalur', belum:'Belum Tersalur', batal:'Dibatalkan'
|
||||
};
|
||||
|
||||
/* Render tab bantuan */
|
||||
window.renderTabBantuan = async function (item) {
|
||||
var container = document.getElementById('detail-tab-bantuan');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="loading">Memuat histori bantuan...</div>';
|
||||
|
||||
var data = await window.fetchJson('php/read_bantuan.php?kemiskinan_id=' + item.id, []);
|
||||
container.innerHTML = '';
|
||||
|
||||
/* Alert jatuh tempo */
|
||||
var jt = data.filter(function(d){ return parseInt(d.jatuh_tempo) === 1; });
|
||||
if (jt.length) {
|
||||
var alert = document.createElement('div');
|
||||
alert.className = 'bantuan-alert';
|
||||
alert.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i> ' + jt.length + ' bantuan sudah jatuh tempo dan belum disalurkan!';
|
||||
container.appendChild(alert);
|
||||
}
|
||||
|
||||
if (window.appAuth.role === 'pengurus_masjid') {
|
||||
var btnAdd = document.createElement('button');
|
||||
btnAdd.className = 'btn btn-add';
|
||||
btnAdd.innerHTML = '+ Catat Bantuan Baru';
|
||||
btnAdd.onclick = function () { window.openTambahBantuanModal(item.id, item.nama_kepala_keluarga); };
|
||||
container.appendChild(btnAdd);
|
||||
}
|
||||
|
||||
if (!data.length) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'empty'; empty.textContent = 'Belum ada histori bantuan.';
|
||||
container.appendChild(empty); return;
|
||||
}
|
||||
|
||||
data.forEach(function (b) {
|
||||
var jmlTxt = b.jumlah_bantuan ? b.jumlah_bantuan + ' ' + (b.satuan || '') : '—';
|
||||
var isJt = parseInt(b.jatuh_tempo) === 1;
|
||||
|
||||
var card = document.createElement('div');
|
||||
card.className = 'b-card ' + b.status_penyaluran + (isJt ? ' jatuh' : '');
|
||||
|
||||
var chipColors = { tersalur:'chip-green', belum:'chip-amber', batal:'chip-gray' };
|
||||
|
||||
card.innerHTML =
|
||||
'<div class="b-head">' +
|
||||
'<span class="b-jenis">' + (window.labelJenisBantuan[b.jenis_bantuan] || b.jenis_bantuan) + '</span>' +
|
||||
'<span class="chip ' + (chipColors[b.status_penyaluran]||'chip-gray') + '">' + (window.labelStatusBantuan[b.status_penyaluran]||b.status_penyaluran) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="b-desc">' + window.escapeHtml(b.deskripsi_bantuan) + '</div>' +
|
||||
'<div class="b-meta">📅 ' + (b.tanggal_fmt||'—') + ' · ' + window.escapeHtml(jmlTxt) + (b.nama_masjid ? ' · ' + window.escapeHtml(b.nama_masjid) : '') + '</div>' +
|
||||
(b.periode_fmt ? '<div class="b-periode' + (isJt ? ' late' : '') + '">Berikutnya: ' + b.periode_fmt + (isJt ? ' — Jatuh Tempo!' : '') + '</div>' : '') +
|
||||
(b.dicatat_oleh ? '<div class="b-meta">Dicatat: ' + window.escapeHtml(b.dicatat_oleh) + '</div>' : '');
|
||||
|
||||
if (window.appAuth.role === 'pengurus_masjid') {
|
||||
var acts = document.createElement('div');
|
||||
acts.className = 'b-acts';
|
||||
|
||||
if (b.status_penyaluran === 'belum') {
|
||||
var btnS = document.createElement('button');
|
||||
btnS.className = 'btn btn-salur'; btnS.textContent = '✓ Tandai Tersalur';
|
||||
btnS.onclick = (function(bid){ return async function(){
|
||||
try {
|
||||
await window.postForm('php/update_bantuan_status.php', { id: bid, status_penyaluran: 'tersalur' });
|
||||
window.showToast('Bantuan ditandai tersalur.', 'success');
|
||||
window.renderTabBantuan(item);
|
||||
} catch(e){ window.showToast('Gagal: ' + e.message, 'error'); }
|
||||
}; })(b.id);
|
||||
acts.appendChild(btnS);
|
||||
|
||||
var btnBt = document.createElement('button');
|
||||
btnBt.className = 'btn btn-delete'; btnBt.textContent = 'Batalkan';
|
||||
btnBt.onclick = (function(bid){ return async function(){
|
||||
if (!confirm('Batalkan bantuan ini?')) return;
|
||||
try {
|
||||
await window.postForm('php/update_bantuan_status.php', { id: bid, status_penyaluran: 'batal' });
|
||||
window.showToast('Bantuan dibatalkan.', 'info');
|
||||
window.renderTabBantuan(item);
|
||||
} catch(e){ window.showToast('Gagal: ' + e.message, 'error'); }
|
||||
}; })(b.id);
|
||||
acts.appendChild(btnBt);
|
||||
}
|
||||
|
||||
var btnH = document.createElement('button');
|
||||
btnH.className = 'btn btn-delete'; btnH.innerHTML = '<i class="fa-solid fa-trash"></i>';
|
||||
btnH.onclick = (function(bid){ return async function(){
|
||||
if (!confirm('Hapus catatan bantuan ini?')) return;
|
||||
try {
|
||||
await window.postForm('php/delete_bantuan.php', { id: bid });
|
||||
window.showToast('Catatan dihapus.', 'success');
|
||||
window.renderTabBantuan(item);
|
||||
} catch(e){ window.showToast('Gagal: ' + e.message, 'error'); }
|
||||
}; })(b.id);
|
||||
acts.appendChild(btnH);
|
||||
card.appendChild(acts);
|
||||
}
|
||||
container.appendChild(card);
|
||||
});
|
||||
};
|
||||
|
||||
/* Modal tambah bantuan */
|
||||
window.openTambahBantuanModal = function (kemiskinanId, namaKK) {
|
||||
document.getElementById('bantuan-kem-id').value = kemiskinanId;
|
||||
document.getElementById('bantuan-jenis').value = 'sembako';
|
||||
document.getElementById('bantuan-deskripsi').value = '';
|
||||
document.getElementById('bantuan-jumlah').value = '';
|
||||
document.getElementById('bantuan-satuan').value = '';
|
||||
document.getElementById('bantuan-tanggal').value = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('bantuan-periode').value = '';
|
||||
document.getElementById('bantuan-status').value = 'belum';
|
||||
document.getElementById('bantuan-dicatat').value = window.appAuth.nama || '';
|
||||
document.getElementById('modal-bantuan-title').textContent = 'Catat Bantuan — ' + namaKK;
|
||||
|
||||
var modal = document.getElementById('modal-bantuan');
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('bantuan-deskripsi').focus();
|
||||
modal.onclick = function (e) { if (e.target === this) this.style.display = 'none'; };
|
||||
|
||||
document.getElementById('btn-submit-bantuan').onclick = async function () {
|
||||
var btn = this;
|
||||
btn.disabled = true; btn.textContent = 'Menyimpan...';
|
||||
var desc = document.getElementById('bantuan-deskripsi').value.trim();
|
||||
if (!desc) { window.showToast('Deskripsi wajib diisi.', 'error'); btn.disabled=false; btn.textContent='Simpan Bantuan'; return; }
|
||||
var payload = {
|
||||
kemiskinan_id: document.getElementById('bantuan-kem-id').value,
|
||||
jenis_bantuan: document.getElementById('bantuan-jenis').value,
|
||||
deskripsi_bantuan: desc,
|
||||
jumlah_bantuan: document.getElementById('bantuan-jumlah').value,
|
||||
satuan: document.getElementById('bantuan-satuan').value.trim(),
|
||||
tanggal_bantuan: document.getElementById('bantuan-tanggal').value,
|
||||
periode_berikutnya:document.getElementById('bantuan-periode').value,
|
||||
status_penyaluran: document.getElementById('bantuan-status').value,
|
||||
dicatat_oleh: document.getElementById('bantuan-dicatat').value.trim()
|
||||
};
|
||||
try {
|
||||
await window.postForm('php/create_bantuan.php', payload);
|
||||
modal.style.display = 'none';
|
||||
window.showToast('Bantuan berhasil dicatat.', 'success');
|
||||
if (window._currentDetailItem && window._currentDetailItem.id === parseInt(payload.kemiskinan_id)) {
|
||||
window.renderTabBantuan(window._currentDetailItem);
|
||||
}
|
||||
} catch (err) { window.showToast('Gagal: ' + err.message, 'error'); }
|
||||
finally { btn.disabled=false; btn.textContent='Simpan Bantuan'; }
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,330 @@
|
||||
/* ============================================================
|
||||
js/laporan.js — Sistem pelaporan warga v2.2
|
||||
============================================================ */
|
||||
|
||||
window.labelKategoriLaporan = {
|
||||
sakit:'Sakit / Butuh Bantuan Medis', kematian:'Kematian',
|
||||
kelahiran:'Kelahiran Bayi Baru', pernikahan:'Pernikahan',
|
||||
pindah:'Pindah Tempat', darurat_ekonomi:'Darurat Ekonomi', lainnya:'Lainnya'
|
||||
};
|
||||
window.labelStatusLaporan = {
|
||||
menunggu:'Menunggu', diproses:'Diproses', selesai:'Selesai', ditolak:'Ditolak'
|
||||
};
|
||||
|
||||
var _kategoriEmoji = {
|
||||
sakit:'🤒', kematian:'💔', kelahiran:'👶', pernikahan:'💍',
|
||||
pindah:'📦', darurat_ekonomi:'🆘', lainnya:'📣'
|
||||
};
|
||||
|
||||
/* ─────────────────────────────────────────────────────
|
||||
Floating detail box
|
||||
───────────────────────────────────────────────────── */
|
||||
window.openLaporanDetail = function (laporan) {
|
||||
var fl = document.getElementById('laporan-detail-float');
|
||||
if (!fl) return;
|
||||
|
||||
var status = laporan.status_laporan || 'menunggu';
|
||||
var emoji = _kategoriEmoji[laporan.kategori] || '📣';
|
||||
var label = window.labelKategoriLaporan[laporan.kategori] || laporan.kategori;
|
||||
var statusLabel = window.labelStatusLaporan[status] || status;
|
||||
|
||||
/* Status bar colour */
|
||||
var bar = document.getElementById('ldf-status-bar');
|
||||
bar.className = 'ldf-status-bar ' + status;
|
||||
|
||||
/* Icon */
|
||||
var icon = document.getElementById('ldf-icon');
|
||||
icon.textContent = emoji;
|
||||
icon.className = 'ldf-icon ' + status;
|
||||
|
||||
/* Title / subtitle */
|
||||
document.getElementById('ldf-title').textContent = label;
|
||||
document.getElementById('ldf-subtitle').textContent = '● ' + statusLabel;
|
||||
document.getElementById('ldf-subtitle').style.color = {
|
||||
menunggu: 'var(--amber)', diproses: 'var(--blue)',
|
||||
selesai: 'var(--green)', ditolak: 'var(--text-muted)'
|
||||
}[status] || 'var(--text-muted)';
|
||||
|
||||
/* Deskripsi */
|
||||
document.getElementById('ldf-deskripsi').textContent = laporan.deskripsi || '(tidak ada deskripsi)';
|
||||
|
||||
/* KK terkait */
|
||||
var sectionKK = document.getElementById('ldf-section-kk');
|
||||
var badgeKK = document.getElementById('ldf-kk-badge');
|
||||
if (laporan.nama_kepala_keluarga) {
|
||||
badgeKK.innerHTML = '<i class="fa-solid fa-location-dot"></i> ' + laporan.nama_kepala_keluarga;
|
||||
sectionKK.style.display = '';
|
||||
} else {
|
||||
sectionKK.style.display = 'none';
|
||||
}
|
||||
|
||||
/* Info rows */
|
||||
document.getElementById('ldf-pelapor').textContent = laporan.nama_pelapor || '-';
|
||||
|
||||
var rowWA = document.getElementById('ldf-row-wa');
|
||||
if (laporan.nomor_wa) {
|
||||
document.getElementById('ldf-wa').textContent = laporan.nomor_wa;
|
||||
rowWA.style.display = '';
|
||||
} else {
|
||||
rowWA.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('ldf-waktu').textContent = laporan.created_fmt || '-';
|
||||
|
||||
var rowDitangani = document.getElementById('ldf-row-ditangani');
|
||||
if (laporan.ditangani_oleh) {
|
||||
document.getElementById('ldf-ditangani').textContent = laporan.ditangani_oleh;
|
||||
rowDitangani.style.display = '';
|
||||
} else {
|
||||
rowDitangani.style.display = 'none';
|
||||
}
|
||||
|
||||
/* Action buttons (pengurus only) */
|
||||
var footer = document.getElementById('ldf-footer');
|
||||
footer.innerHTML = '';
|
||||
if (window.appAuth && window.appAuth.role === 'pengurus_masjid' && status !== 'selesai' && status !== 'ditolak') {
|
||||
footer.style.display = '';
|
||||
var statuses = [
|
||||
{ val: 'diproses', label: '<i class="fa-solid fa-rotate"></i> Proses', cls: 'btn-proses' },
|
||||
{ val: 'selesai', label: '<i class="fa-solid fa-check"></i> Selesai', cls: 'btn-selesai' },
|
||||
{ val: 'ditolak', label: '<i class="fa-solid fa-xmark"></i> Tolak', cls: 'btn-tolak' }
|
||||
];
|
||||
statuses.forEach(function (s) {
|
||||
if (s.val === status) return;
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'btn ' + s.cls;
|
||||
btn.innerHTML = s.label;
|
||||
btn.onclick = (function (lid, sv) {
|
||||
return async function () {
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await window.postForm('php/update_laporan_status.php', {
|
||||
id: lid, status_laporan: sv, ditangani_oleh: window.appAuth.nama
|
||||
});
|
||||
window.showToast('Status diperbarui ke: ' + window.labelStatusLaporan[sv], 'success');
|
||||
window.closeLaporanDetail();
|
||||
window.loadLaporan(document.getElementById('laporan-filter-status').value);
|
||||
} catch (e) {
|
||||
window.showToast('Gagal: ' + e.message, 'error');
|
||||
btn.disabled = false;
|
||||
}
|
||||
};
|
||||
})(laporan.id, s.val);
|
||||
footer.appendChild(btn);
|
||||
});
|
||||
} else {
|
||||
footer.style.display = 'none';
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
document.getElementById('ldf-close').onclick = window.closeLaporanDetail;
|
||||
|
||||
/* Show with animation */
|
||||
fl.style.display = 'flex';
|
||||
};
|
||||
|
||||
window.closeLaporanDetail = function () {
|
||||
var fl = document.getElementById('laporan-detail-float');
|
||||
if (fl) fl.style.display = 'none';
|
||||
};
|
||||
|
||||
/* ── Form laporan publik ── */
|
||||
window.openLaporanPublikModal = function (kemiskinanId, namaKK) {
|
||||
var modal = document.getElementById('modal-laporan-publik');
|
||||
document.getElementById('laporan-kem-id').value = kemiskinanId || '';
|
||||
document.getElementById('laporan-nama-pelapor').value = '';
|
||||
document.getElementById('laporan-nomor-wa').value = '';
|
||||
document.getElementById('laporan-kategori').value = 'lainnya';
|
||||
document.getElementById('laporan-deskripsi').value = '';
|
||||
document.getElementById('laporan-judul-kkname').textContent = namaKK
|
||||
? 'Laporkan Kondisi: ' + namaKK
|
||||
: 'Laporkan Kondisi Warga';
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('laporan-nama-pelapor').focus();
|
||||
modal.onclick = function (e) { if (e.target === this) this.style.display = 'none'; };
|
||||
};
|
||||
|
||||
/* ── Tombol submit laporan dipanggil dari inline HTML ── */
|
||||
window._submitLaporanBtn = async function () {
|
||||
var btn = document.getElementById('btn-submit-laporan');
|
||||
var nama = document.getElementById('laporan-nama-pelapor').value.trim();
|
||||
var desc = document.getElementById('laporan-deskripsi').value.trim();
|
||||
if (!nama || !desc) { window.showToast('Nama dan deskripsi wajib diisi.', 'error'); return; }
|
||||
|
||||
btn.disabled = true; btn.textContent = 'Mengirim...';
|
||||
var payload = {
|
||||
kemiskinan_id: document.getElementById('laporan-kem-id').value,
|
||||
nama_pelapor: nama,
|
||||
nomor_wa: document.getElementById('laporan-nomor-wa').value.trim(),
|
||||
kategori: document.getElementById('laporan-kategori').value,
|
||||
deskripsi: desc
|
||||
};
|
||||
try {
|
||||
var res = await fetch('php/create_laporan.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(payload)
|
||||
});
|
||||
var text = (await res.text()).trim();
|
||||
if (text === 'success') {
|
||||
document.getElementById('modal-laporan-publik').style.display = 'none';
|
||||
if (window.Swal) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Laporan Terkirim',
|
||||
text: 'Laporan berhasil dikirim. Terima kasih atas informasi Anda!',
|
||||
confirmButtonText: 'Tutup'
|
||||
});
|
||||
} else {
|
||||
window.showToast('Laporan berhasil dikirim. Terima kasih!', 'success');
|
||||
}
|
||||
} else {
|
||||
window.showToast('Gagal: ' + text, 'error');
|
||||
}
|
||||
} catch (err) { window.showToast('Gagal: ' + err.message, 'error'); }
|
||||
finally { btn.disabled = false; btn.textContent = 'Kirim Laporan'; }
|
||||
};
|
||||
|
||||
/* ── Panel manajemen (pengurus) ── */
|
||||
window.openLaporanPanel = async function () {
|
||||
var panel = document.getElementById('panel-laporan');
|
||||
if (panel) {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) sidebar.style.display = 'none';
|
||||
var pes = document.getElementById('panel-pesan');
|
||||
if (pes) pes.style.display = 'none';
|
||||
|
||||
panel.style.display = 'flex';
|
||||
document.getElementById('laporan-filter-status').value = 'menunggu';
|
||||
window.loadLaporan('menunggu');
|
||||
}
|
||||
};
|
||||
window.closeLaporanPanel = function () {
|
||||
var p = document.getElementById('panel-laporan');
|
||||
if (p) p.style.display = 'none';
|
||||
window.closeLaporanDetail();
|
||||
|
||||
// Auto-open sidebar
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) sidebar.style.display = 'flex';
|
||||
};
|
||||
|
||||
window.loadLaporan = async function (statusFilter) {
|
||||
var container = document.getElementById('laporan-list-container');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="loading">Memuat laporan...</div>';
|
||||
|
||||
var url = 'php/read_laporan.php' + (statusFilter ? '?status=' + statusFilter : '');
|
||||
var data = await window.fetchJson(url, []);
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!data.length) {
|
||||
container.innerHTML = '<div class="empty">Tidak ada laporan.</div>'; return;
|
||||
}
|
||||
|
||||
data.forEach(function (l) {
|
||||
var chipColors = { menunggu:'chip-amber', diproses:'chip-blue', selesai:'chip-green', ditolak:'chip-gray' };
|
||||
var emoji = _kategoriEmoji[l.kategori] || '📣';
|
||||
|
||||
var card = document.createElement('div');
|
||||
card.className = 'l-card ' + l.status_laporan;
|
||||
card.title = 'Klik untuk detail lengkap';
|
||||
|
||||
var badgeCls = { menunggu:'chip-amber', diproses:'chip-blue', selesai:'chip-green', ditolak:'chip-gray' }[l.status_laporan] || 'chip-gray';
|
||||
var kkHtml = l.nama_kepala_keluarga
|
||||
? '<div class="l-kk"><i class="fa-solid fa-location-dot"></i> KK: ' + window.escapeHtml(l.nama_kepala_keluarga) + '</div>' : '';
|
||||
|
||||
card.innerHTML =
|
||||
'<div class="l-head">' +
|
||||
'<span class="l-cat">' + emoji + ' ' + (window.labelKategoriLaporan[l.kategori] || l.kategori) + '</span>' +
|
||||
'<span class="chip ' + badgeCls + '">' + window.escapeHtml(window.labelStatusLaporan[l.status_laporan] || l.status_laporan) + '</span>' +
|
||||
'</div>' +
|
||||
kkHtml +
|
||||
'<div class="l-desc">' + window.escapeHtml(l.deskripsi) + '</div>' +
|
||||
'<div class="l-meta"><i class="fa-solid fa-user"></i> ' + window.escapeHtml(l.nama_pelapor) +
|
||||
(l.nomor_wa ? ' · <i class="fa-solid fa-mobile-screen"></i> ' + window.escapeHtml(l.nomor_wa) : '') +
|
||||
' · <i class="fa-regular fa-clock"></i> ' + l.created_fmt + '</div>' +
|
||||
(l.ditangani_oleh ? '<div class="l-meta"><i class="fa-solid fa-check"></i> Ditangani: ' + window.escapeHtml(l.ditangani_oleh) + '</div>' : '');
|
||||
|
||||
/* Klik kartu → buka floating detail */
|
||||
card.addEventListener('click', function (e) {
|
||||
/* Jangan trigger jika yang diklik tombol aksi */
|
||||
if (e.target.closest('button')) return;
|
||||
window.openLaporanDetail(l);
|
||||
});
|
||||
|
||||
if (window.appAuth.role === 'pengurus_masjid' && l.status_laporan !== 'selesai' && l.status_laporan !== 'ditolak') {
|
||||
var acts = document.createElement('div');
|
||||
acts.className = 'l-acts';
|
||||
var statuses = [
|
||||
{ val: 'diproses', label: 'Proses', cls: 'btn-proses' },
|
||||
{ val: 'selesai', label: 'Selesai', cls: 'btn-selesai' },
|
||||
{ val: 'ditolak', label: 'Tolak', cls: 'btn-tolak' }
|
||||
];
|
||||
statuses.forEach(function (s) {
|
||||
if (s.val === l.status_laporan) return;
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'btn ' + s.cls; btn.textContent = s.label;
|
||||
btn.onclick = (function(lid, sv){ return async function(){
|
||||
try {
|
||||
await window.postForm('php/update_laporan_status.php', {
|
||||
id: lid, status_laporan: sv, ditangani_oleh: window.appAuth.nama
|
||||
});
|
||||
window.showToast('Status diperbarui.', 'success');
|
||||
window.closeLaporanDetail();
|
||||
window.loadLaporan(document.getElementById('laporan-filter-status').value);
|
||||
} catch(e){ window.showToast('Gagal: ' + e.message, 'error'); }
|
||||
}; })(l.id, s.val);
|
||||
acts.appendChild(btn);
|
||||
});
|
||||
card.appendChild(acts);
|
||||
}
|
||||
container.appendChild(card);
|
||||
});
|
||||
};
|
||||
|
||||
/* Polling for Laporan Notification */
|
||||
window.pollLaporanNotification = async function() {
|
||||
if (!window.appAuth || window.appAuth.role !== 'pengurus_masjid') {
|
||||
var np = document.getElementById('notif-panel');
|
||||
if (np) np.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var res = await fetch('php/read_laporan.php');
|
||||
var text = await res.text();
|
||||
var data = JSON.parse(text);
|
||||
if(Array.isArray(data)) {
|
||||
var pendingCount = data.filter(function(l) { return l.status_laporan === 'menunggu'; }).length;
|
||||
|
||||
var badge = document.getElementById('nav-badge-laporan');
|
||||
if(badge) {
|
||||
if(pendingCount > 0) {
|
||||
badge.style.display = 'inline-flex';
|
||||
badge.textContent = pendingCount;
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
var np = document.getElementById('notif-panel');
|
||||
var nb = document.getElementById('notif-body');
|
||||
if (np && nb) {
|
||||
if(pendingCount > 0) {
|
||||
var isPanelOpen = document.getElementById('panel-laporan') && document.getElementById('panel-laporan').style.display === 'flex';
|
||||
if(!isPanelOpen && (!window._notifDismissedForCount || window._notifDismissedForCount < pendingCount)) {
|
||||
np.style.display = 'block';
|
||||
nb.innerHTML = 'Terdapat <b>' + pendingCount + ' laporan baru</b> dari warga yang belum diproses.<br>Harap segera ditindaklanjuti!';
|
||||
window._notifDismissedForCount = pendingCount;
|
||||
}
|
||||
} else {
|
||||
np.style.display = 'none';
|
||||
window._notifDismissedForCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
// Start polling every 10 seconds
|
||||
setInterval(window.pollLaporanNotification, 10000);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,377 @@
|
||||
/* ============================================================
|
||||
map.js – WebGIS Pemetaan Kemiskinan & Masjid (SRS v1.0)
|
||||
============================================================ */
|
||||
|
||||
window.appState = {
|
||||
mode: "view",
|
||||
activeType: "kemiskinan",
|
||||
layers: {},
|
||||
layerVisibility: {
|
||||
kemiskinan: true,
|
||||
masjid: true
|
||||
},
|
||||
isHeatmap: false,
|
||||
isRouting: false,
|
||||
routingWaypoints: []
|
||||
};
|
||||
|
||||
/* ── Utility: escape HTML ── */
|
||||
window.escapeHtml = function (value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
/* ── Utility: SVG pin marker ── */
|
||||
window.createPinIcon = function (bgColor, svgSymbol) {
|
||||
var svg = [
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42">',
|
||||
'<defs>',
|
||||
'<filter id="shadow" x="-30%" y="-20%" width="160%" height="160%">',
|
||||
'<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="rgba(0,0,0,0.35)"/>',
|
||||
'</filter>',
|
||||
'</defs>',
|
||||
/* Pin body */
|
||||
'<path d="M16 2C9.37 2 4 7.37 4 14c0 9 12 26 12 26S28 23 28 14C28 7.37 22.63 2 16 2z"',
|
||||
'fill="' + bgColor + '" filter="url(#shadow)"/>',
|
||||
/* White inner circle */
|
||||
'<circle cx="16" cy="14" r="8" fill="rgba(255,255,255,0.25)"/>',
|
||||
/* Symbol */
|
||||
svgSymbol,
|
||||
'</svg>'
|
||||
].join('');
|
||||
return L.divIcon({
|
||||
className: 'custom-pin-icon',
|
||||
html: svg,
|
||||
iconSize: [32, 42],
|
||||
iconAnchor: [16, 42],
|
||||
popupAnchor: [0, -44]
|
||||
});
|
||||
};
|
||||
|
||||
/* Kemiskinan – merah (tidak terjangkau) */
|
||||
window.redIcon = window.createPinIcon('#dc2626',
|
||||
'<text x="16" y="19" text-anchor="middle" font-size="11" fill="white" font-family="Arial">👪</text>'
|
||||
);
|
||||
/* Kemiskinan – hijau (terjangkau) */
|
||||
window.greenIcon = window.createPinIcon('#16a34a',
|
||||
'<text x="16" y="19" text-anchor="middle" font-size="11" fill="white" font-family="Arial">👪</text>'
|
||||
);
|
||||
/* Masjid – biru */
|
||||
window.blueIcon = window.createPinIcon('#1d4ed8',
|
||||
'<text x="16" y="19" text-anchor="middle" font-size="11" fill="white" font-family="Arial">🕌</text>'
|
||||
);
|
||||
/* Fallback orange (tidak digunakan, tapi tetap didefinisikan) */
|
||||
window.orangeIcon = window.createPinIcon('#f97316',
|
||||
'<text x="16" y="19" text-anchor="middle" font-size="11" fill="white" font-family="Arial">👪</text>'
|
||||
);
|
||||
|
||||
/* ── Utility: POST form-encoded ── */
|
||||
window.postForm = async function (url, payload) {
|
||||
var response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(payload)
|
||||
});
|
||||
var text = (await response.text()).trim();
|
||||
if (!response.ok || text !== "success") {
|
||||
throw new Error(text || "Permintaan gagal.");
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Navbar hint ── */
|
||||
window.updateMapHint = function () {
|
||||
var hint = document.getElementById("mapHint");
|
||||
if (!hint) { return; }
|
||||
|
||||
if (appState.mode === "view") {
|
||||
hint.textContent = "Mode Lihat aktif — data keluarga prasejahtera & rumah ibadah ditampilkan. Klik kartu untuk navigasi peta.";
|
||||
return;
|
||||
}
|
||||
if (appState.activeType === "kemiskinan") {
|
||||
hint.textContent = "Mode Edit aktif — klik titik di peta untuk menambahkan data keluarga prasejahtera.";
|
||||
return;
|
||||
}
|
||||
hint.textContent = "Mode Edit aktif — klik titik di peta untuk menambahkan data rumah ibadah baru.";
|
||||
};
|
||||
|
||||
/* ── Toolbar state ── */
|
||||
window.updateToolbarState = function () {
|
||||
var states = {
|
||||
modeViewBtn: appState.mode === "view",
|
||||
modeEditBtn: appState.mode === "edit",
|
||||
typeKemiskinanBtn: appState.activeType === "kemiskinan",
|
||||
typeMasjidBtn: appState.activeType === "masjid"
|
||||
};
|
||||
Object.keys(states).forEach(function (id) {
|
||||
var btn = document.getElementById(id);
|
||||
if (!btn) { return; }
|
||||
btn.classList.toggle("active", states[id]);
|
||||
/* Mode buttons get a different active colour */
|
||||
if (id === "modeEditBtn" && states[id]) {
|
||||
btn.classList.add("mode-active");
|
||||
} else {
|
||||
btn.classList.remove("mode-active");
|
||||
}
|
||||
});
|
||||
|
||||
var viewBtn = document.getElementById("luminaModeView");
|
||||
var editBtn = document.getElementById("luminaModeEdit");
|
||||
if (viewBtn && editBtn) {
|
||||
viewBtn.style.opacity = appState.mode === "view" ? "1" : "0.5";
|
||||
viewBtn.style.boxShadow = appState.mode === "view" ? "0 4px 12px rgba(99,102,241,0.25)" : "none";
|
||||
|
||||
editBtn.style.opacity = appState.mode === "edit" ? "1" : "0.5";
|
||||
editBtn.style.boxShadow = appState.mode === "edit" ? "0 4px 12px rgba(20,184,166,0.25)" : "none";
|
||||
}
|
||||
};
|
||||
|
||||
window.setActiveMenu = function(element) {
|
||||
var items = document.querySelectorAll('.lumina-menu-item');
|
||||
items.forEach(function(item) {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
element.classList.add('active');
|
||||
};
|
||||
|
||||
/* ── Layer visibility ── */
|
||||
window.setLayerVisibility = function (key, isVisible) {
|
||||
appState.layerVisibility[key] = !!isVisible;
|
||||
var layer = appState.layers[key];
|
||||
if (!layer || !window.map) { return; }
|
||||
if (isVisible) {
|
||||
if (!map.hasLayer(layer)) { map.addLayer(layer); }
|
||||
} else {
|
||||
if (map.hasLayer(layer)) { map.removeLayer(layer); }
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Mode & type switches ── */
|
||||
window.setMode = function (mode) {
|
||||
appState.mode = mode;
|
||||
window.updateToolbarState();
|
||||
window.updateMapHint();
|
||||
|
||||
var mapEl = document.getElementById("map");
|
||||
if (mapEl) {
|
||||
if (mode === "edit") {
|
||||
mapEl.classList.add("edit-mode");
|
||||
} else {
|
||||
mapEl.classList.remove("edit-mode");
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window.loadAllData === "function") {
|
||||
window.loadAllData();
|
||||
}
|
||||
};
|
||||
|
||||
window.setDataType = function (type) {
|
||||
appState.activeType = type;
|
||||
window.updateToolbarState();
|
||||
window.updateMapHint();
|
||||
if (typeof window.filterSidebar === 'function') {
|
||||
window.filterSidebar(type);
|
||||
}
|
||||
};
|
||||
|
||||
window.filterSidebar = function(type) {
|
||||
var secKem = document.getElementById('sec-kemiskinan');
|
||||
var secMsj = document.getElementById('sec-masjid');
|
||||
|
||||
if (secKem) secKem.style.display = (type === 'semua' || type === 'kemiskinan') ? 'block' : 'none';
|
||||
if (secMsj) secMsj.style.display = (type === 'semua' || type === 'masjid') ? 'block' : 'none';
|
||||
|
||||
var btnSemua = document.getElementById('filter-btn-semua');
|
||||
var btnKem = document.getElementById('filter-btn-kemiskinan');
|
||||
var btnMsj = document.getElementById('filter-btn-masjid');
|
||||
|
||||
if (btnSemua) btnSemua.classList.toggle('active', type === 'semua');
|
||||
if (btnKem) btnKem.classList.toggle('active', type === 'kemiskinan');
|
||||
if (btnMsj) btnMsj.classList.toggle('active', type === 'masjid');
|
||||
|
||||
var sidebarSub = document.getElementById('sidebar-sub');
|
||||
if (sidebarSub) {
|
||||
if (type === 'semua') sidebarSub.textContent = 'Menampilkan semua data';
|
||||
else if (type === 'kemiskinan') sidebarSub.textContent = 'Menampilkan list Data Keluarga';
|
||||
else sidebarSub.textContent = 'Menampilkan list Rumah Ibadah';
|
||||
}
|
||||
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (sidebar && sidebar.style.display === 'none') {
|
||||
sidebar.style.display = 'flex';
|
||||
var pes = document.getElementById('panel-pesan');
|
||||
if (pes) pes.style.display = 'none';
|
||||
var lap = document.getElementById('panel-laporan');
|
||||
if (lap) lap.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Format helpers ── */
|
||||
window.formatMeters = function (value) {
|
||||
return Number(value).toLocaleString("id-ID", { maximumFractionDigits: 0 }) + " m";
|
||||
};
|
||||
|
||||
window.isValidCoordinate = function (lat, lng) {
|
||||
return Number.isFinite(Number(lat)) && Number.isFinite(Number(lng));
|
||||
};
|
||||
|
||||
window.isValidRadius = function (radius) {
|
||||
var r = Number(radius);
|
||||
return Number.isFinite(r) && r >= 100 && r <= 5000;
|
||||
};
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
DOMContentLoaded – inisialisasi peta (KF-01)
|
||||
───────────────────────────────────────────── */
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
/* Inisialisasi peta terpusat di Pontianak */
|
||||
window.map = L.map("map").setView([-0.0263, 109.3425], 13);
|
||||
|
||||
// --- Base maps ---
|
||||
window.osmBase = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
});
|
||||
window.darkBase = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap contributors © CARTO'
|
||||
});
|
||||
|
||||
// Add default basemap
|
||||
window.osmBase.addTo(map);
|
||||
|
||||
/* ── Layers ── */
|
||||
appState.layers.kemiskinan = L.layerGroup().addTo(map);
|
||||
appState.layers.masjid = L.layerGroup().addTo(map);
|
||||
appState.layers.heatmap = L.layerGroup(); // Do not add yet
|
||||
|
||||
/* Klik peta – hanya aktif di Mode Edit (KF-03, KF-07) */
|
||||
map.on("click", function (event) {
|
||||
if (appState.mode !== "edit") { return; }
|
||||
|
||||
var lat = event.latlng.lat;
|
||||
var lng = event.latlng.lng;
|
||||
|
||||
if (appState.activeType === "kemiskinan") {
|
||||
if (typeof window.openKemiskinanModal === "function") {
|
||||
window.openKemiskinanModal(lat, lng);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.activeType === "masjid") {
|
||||
if (typeof window.openMasjidModal === "function") {
|
||||
window.openMasjidModal(lat, lng);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* Init state awal */
|
||||
window.setMode("view");
|
||||
window.setDataType("kemiskinan");
|
||||
|
||||
setTimeout(function () { map.invalidateSize(); }, 300);
|
||||
|
||||
/* Cek session login, lalu load data */
|
||||
if (typeof window.checkAuthState === "function") {
|
||||
window.checkAuthState().then(function () {
|
||||
if (typeof window.loadAllData === "function") { window.loadAllData(); }
|
||||
});
|
||||
} else if (typeof window.loadAllData === "function") {
|
||||
window.loadAllData();
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Heatmap & Routing Features ── */
|
||||
window.toggleHeatmap = function(btnEl) {
|
||||
if(!window.appState) return;
|
||||
window.appState.isHeatmap = !window.appState.isHeatmap;
|
||||
btnEl.classList.toggle('active', window.appState.isHeatmap);
|
||||
|
||||
if(window.appState.isHeatmap) {
|
||||
document.getElementById('desc-heatmap').style.display = 'block';
|
||||
// Hide kemiskinan markers
|
||||
window.appState.layers.kemiskinan.eachLayer(function(l) { l.setOpacity(0); });
|
||||
// Show heatmap
|
||||
if(!window.map.hasLayer(window.appState.layers.heatmap)) {
|
||||
window.appState.layers.heatmap.addTo(window.map);
|
||||
}
|
||||
// Switch to Dark mode for better visual
|
||||
window.map.removeLayer(window.osmBase);
|
||||
window.darkBase.addTo(window.map);
|
||||
} else {
|
||||
document.getElementById('desc-heatmap').style.display = 'none';
|
||||
// Show kemiskinan markers
|
||||
window.appState.layers.kemiskinan.eachLayer(function(l) { l.setOpacity(1); });
|
||||
// Hide heatmap
|
||||
if(window.map.hasLayer(window.appState.layers.heatmap)) {
|
||||
window.map.removeLayer(window.appState.layers.heatmap);
|
||||
}
|
||||
// Restore map base
|
||||
window.map.removeLayer(window.darkBase);
|
||||
window.osmBase.addTo(window.map);
|
||||
}
|
||||
};
|
||||
|
||||
window.toggleRoutingMode = function(btnEl) {
|
||||
if(!window.appState) return;
|
||||
window.appState.isRouting = true;
|
||||
window.appState.routingWaypoints = [];
|
||||
btnEl.classList.add('active');
|
||||
document.getElementById('desc-routing').style.display = 'block';
|
||||
|
||||
// Show instruction panel
|
||||
document.getElementById('routing-instruction-panel').style.display = 'block';
|
||||
document.getElementById('routing-instruction-text').innerHTML = 'Silakan klik <b>Masjid</b> atau <b>Rumah Warga</b> di peta sebagai titik awal rute.';
|
||||
};
|
||||
|
||||
window.cancelRouting = function() {
|
||||
if(!window.appState) return;
|
||||
window.appState.isRouting = false;
|
||||
window.appState.routingWaypoints = [];
|
||||
document.getElementById('btn-toggle-routing').classList.remove('active');
|
||||
document.getElementById('desc-routing').style.display = 'none';
|
||||
document.getElementById('routing-instruction-panel').style.display = 'none';
|
||||
|
||||
// Remove routing control if exists
|
||||
if(window.routingControl) {
|
||||
window.map.removeControl(window.routingControl);
|
||||
window.routingControl = null;
|
||||
}
|
||||
};
|
||||
|
||||
window.handleRoutingClick = function(lat, lng, name) {
|
||||
window.appState.routingWaypoints.push(L.latLng(lat, lng));
|
||||
var pts = window.appState.routingWaypoints;
|
||||
var inst = document.getElementById('routing-instruction-text');
|
||||
|
||||
if(pts.length === 1) {
|
||||
inst.innerHTML = '<div style="margin-bottom:6px; color:#16a34a;"><i class="fa-solid fa-check"></i> Titik Awal: <b>' + window.escapeHtml(name) + '</b></div>' +
|
||||
'<div style="margin-bottom:6px;"><b>Langkah 2:</b> Pilih Tujuan</div>' +
|
||||
'<span style="color:var(--lumina-text-muted);">Klik rumah/masjid lain di peta sebagai tujuan akhir.</span>';
|
||||
} else if(pts.length === 2) {
|
||||
inst.innerHTML = '<div style="margin-bottom:6px; color:#16a34a;"><i class="fa-solid fa-check"></i> Rute Berhasil Dibuat</div>' +
|
||||
'<span style="color:var(--lumina-text-muted);">Detail arah jalan (navigasi) muncul di layar sebelah kanan peta.</span>';
|
||||
|
||||
// Remove existing control
|
||||
if(window.routingControl) {
|
||||
window.map.removeControl(window.routingControl);
|
||||
}
|
||||
|
||||
window.routingControl = L.Routing.control({
|
||||
waypoints: pts,
|
||||
routeWhileDragging: false,
|
||||
addWaypoints: false,
|
||||
show: true,
|
||||
lineOptions: {
|
||||
styles: [{color: '#2563eb', opacity: 0.8, weight: 6}]
|
||||
}
|
||||
}).addTo(window.map);
|
||||
|
||||
window.appState.isRouting = false; // Turn off click interception after 2 points
|
||||
document.getElementById('btn-toggle-routing').classList.remove('active');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
/* ============================================================
|
||||
js/pesan.js — Media komunikasi pengurus v2.2
|
||||
============================================================ */
|
||||
|
||||
window.labelJenisPesan = {
|
||||
permintaan_bantuan: 'Permintaan Bantuan',
|
||||
koordinasi: 'Koordinasi',
|
||||
pengumuman: 'Pengumuman'
|
||||
};
|
||||
|
||||
var _jenisEmoji = {
|
||||
permintaan_bantuan: '🆘',
|
||||
koordinasi: '📋',
|
||||
pengumuman: '📢'
|
||||
};
|
||||
|
||||
/* ─────────────────────────────────────────────────────
|
||||
Floating detail box
|
||||
───────────────────────────────────────────────────── */
|
||||
window.openPesanDetail = async function (p) {
|
||||
var fl = document.getElementById('pesan-detail-float');
|
||||
if (!fl) return;
|
||||
|
||||
var jenis = p.jenis || 'koordinasi';
|
||||
var emoji = _jenisEmoji[jenis] || '💬';
|
||||
var jenisLabel = window.labelJenisPesan[jenis] || jenis;
|
||||
var isBroadcast = !p.penerima_id;
|
||||
var roleLabel = p.role_pengirim === 'pimpinan_daerah' ? '<i class="fa-solid fa-crown"></i> Pimpinan Daerah' : '<i class="fa-solid fa-mosque"></i> Pengurus Masjid';
|
||||
|
||||
/* Accent bar */
|
||||
document.getElementById('pdf-accent-bar').className = 'pdf-accent-bar ' + jenis;
|
||||
|
||||
/* Icon */
|
||||
var icon = document.getElementById('pdf-icon');
|
||||
icon.textContent = emoji;
|
||||
icon.className = 'pdf-icon ' + jenis;
|
||||
|
||||
/* Title (judul pesan) + subtitle (jenis) */
|
||||
document.getElementById('pdf-title').textContent = p.judul || 'Pesan';
|
||||
document.getElementById('pdf-subtitle').textContent = jenisLabel;
|
||||
|
||||
/* Isi */
|
||||
document.getElementById('pdf-isi').textContent = p.isi || '(tidak ada isi)';
|
||||
|
||||
/* Info rows */
|
||||
document.getElementById('pdf-pengirim').textContent = p.nama_pengirim || '-';
|
||||
document.getElementById('pdf-role').innerHTML = roleLabel;
|
||||
document.getElementById('pdf-jenis-label').textContent = emoji + ' ' + jenisLabel;
|
||||
document.getElementById('pdf-waktu').textContent = p.created_fmt || '-';
|
||||
|
||||
/* Broadcast badge */
|
||||
document.getElementById('pdf-row-broadcast').style.display = isBroadcast ? '' : 'none';
|
||||
|
||||
/* Baru badge */
|
||||
document.getElementById('pdf-row-baru').style.display = (p.status === 'baru') ? '' : 'none';
|
||||
|
||||
/* Close */
|
||||
document.getElementById('pdf-close').onclick = window.closePesanDetail;
|
||||
|
||||
/* Show */
|
||||
fl.style.display = 'flex';
|
||||
|
||||
/* Mark as read if it is new and we are not the sender */
|
||||
if (p.status === 'baru' && p.pengirim_id != window.appAuth.id) {
|
||||
try {
|
||||
var formData = new FormData();
|
||||
formData.append('id', p.id);
|
||||
formData.append('status', 'dibaca');
|
||||
await fetch('php/update_pesan_status.php', { method: 'POST', body: formData });
|
||||
|
||||
p.status = 'dibaca';
|
||||
|
||||
// Remove 'baru' indicator from card visually
|
||||
var cardEl = document.querySelector('.p-card[data-id="' + p.id + '"]');
|
||||
if (cardEl) {
|
||||
cardEl.classList.remove('baru');
|
||||
var dot = cardEl.querySelector('.p-new');
|
||||
if (dot) dot.remove();
|
||||
}
|
||||
|
||||
// Immediately update the notification badge
|
||||
if (window.pollPesanNotification) window.pollPesanNotification();
|
||||
} catch (e) {
|
||||
console.error('Failed to update message status', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.closePesanDetail = function () {
|
||||
var fl = document.getElementById('pesan-detail-float');
|
||||
if (fl) fl.style.display = 'none';
|
||||
};
|
||||
|
||||
/* ── Panel open/close ── */
|
||||
window.openPesanPanel = async function () {
|
||||
var panel = document.getElementById('panel-pesan');
|
||||
if (panel) {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) sidebar.style.display = 'none';
|
||||
var lap = document.getElementById('panel-laporan');
|
||||
if (lap) lap.style.display = 'none';
|
||||
panel.style.display = 'flex';
|
||||
await window.loadPesan();
|
||||
}
|
||||
};
|
||||
window.closePesanPanel = function () {
|
||||
var p = document.getElementById('panel-pesan');
|
||||
if (p) p.style.display = 'none';
|
||||
window.closePesanDetail();
|
||||
|
||||
// Auto-open sidebar
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) sidebar.style.display = 'flex';
|
||||
};
|
||||
|
||||
/* ── Load & render pesan ── */
|
||||
window.loadPesan = async function () {
|
||||
var container = document.getElementById('pesan-list-container');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="loading">Memuat pesan...</div>';
|
||||
var data = await window.fetchJson('php/read_pesan.php', []);
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!data.length) {
|
||||
container.innerHTML = '<div class="empty">Belum ada pesan. Mulai komunikasi di bawah.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(function (p) {
|
||||
var jenisClass = { permintaan_bantuan:'bantuan', koordinasi:'', pengumuman:'pengumuman' }[p.jenis] || '';
|
||||
var chipColors = { permintaan_bantuan:'chip-red', koordinasi:'chip-blue', pengumuman:'chip-amber' };
|
||||
var isBroadcast = !p.penerima_id;
|
||||
var emoji = _jenisEmoji[p.jenis] || '💬';
|
||||
|
||||
var card = document.createElement('div');
|
||||
card.className = 'p-card' + (jenisClass ? ' ' + jenisClass : '') + (p.status === 'baru' ? ' baru' : '');
|
||||
card.title = 'Klik untuk detail lengkap';
|
||||
card.dataset.id = p.id;
|
||||
|
||||
card.innerHTML =
|
||||
'<div class="p-head">' +
|
||||
'<span class="chip ' + (chipColors[p.jenis]||'chip-gray') + '">' + emoji + ' ' + (window.labelJenisPesan[p.jenis]||p.jenis) + '</span>' +
|
||||
(isBroadcast ? '<span class="chip chip-purple">📡 Broadcast</span>' : '') +
|
||||
(p.status === 'baru' ? '<span class="p-new">●</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="p-judul">' + window.escapeHtml(p.judul) + '</div>' +
|
||||
'<div class="p-isi">' + window.escapeHtml(p.isi) + '</div>' +
|
||||
'<div class="p-meta">👤 ' + window.escapeHtml(p.nama_pengirim) +
|
||||
' (' + (p.role_pengirim === 'pimpinan_daerah' ? 'Pimpinan' : 'Pengurus') + ')' +
|
||||
' · 🕐 ' + p.created_fmt + '</div>';
|
||||
|
||||
/* Klik kartu → buka floating detail */
|
||||
card.addEventListener('click', function () {
|
||||
window.openPesanDetail(p);
|
||||
});
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
};
|
||||
|
||||
/* ── Submit kirim pesan ── */
|
||||
window.submitPesan = async function (e) {
|
||||
e.preventDefault();
|
||||
if (!window.appAuth.loggedIn) { window.showToast('Login diperlukan.', 'error'); return; }
|
||||
|
||||
var btn = document.getElementById('btn-submit-pesan');
|
||||
var judul = document.getElementById('pesan-judul').value.trim();
|
||||
var isi = document.getElementById('pesan-isi').value.trim();
|
||||
var jenis = document.getElementById('pesan-jenis').value;
|
||||
|
||||
if (!judul || !isi) { window.showToast('Judul dan isi wajib diisi.', 'error'); return; }
|
||||
btn.disabled = true; btn.textContent = 'Mengirim...';
|
||||
|
||||
try {
|
||||
await window.postForm('php/send_pesan.php', { judul, isi, jenis });
|
||||
document.getElementById('pesan-judul').value = '';
|
||||
document.getElementById('pesan-isi').value = '';
|
||||
window.showToast('Pesan terkirim.', 'success');
|
||||
await window.loadPesan();
|
||||
} catch (err) { window.showToast('Gagal: ' + err.message, 'error'); }
|
||||
finally { btn.disabled = false; btn.textContent = 'Kirim Pesan'; }
|
||||
};
|
||||
|
||||
/* Polling for Pesan Notification */
|
||||
window.pollPesanNotification = async function() {
|
||||
if (!window.appAuth) return;
|
||||
try {
|
||||
var res = await fetch('php/read_pesan.php');
|
||||
var text = await res.text();
|
||||
var data = JSON.parse(text);
|
||||
if(Array.isArray(data)) {
|
||||
// Count messages from OTHER users that are considered "baru"
|
||||
// Since we don't have a read-status per user in this simple app, we can just count messages from others in the last 24 hours, or just count 'baru'
|
||||
var pendingCount = data.filter(function(m) {
|
||||
return m.status === 'baru' && m.pengirim_id != window.appAuth.id;
|
||||
}).length;
|
||||
|
||||
var badge = document.getElementById('nav-badge-pesan');
|
||||
if(badge) {
|
||||
if(pendingCount > 0) {
|
||||
badge.style.display = 'inline-flex';
|
||||
badge.textContent = pendingCount;
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
// Start polling every 15 seconds
|
||||
setInterval(window.pollPesanNotification, 15000);
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
// php/check_auth.php
|
||||
require_once 'koneksi.php';
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($_SESSION['user_id'])) {
|
||||
echo json_encode(['loggedIn' => false]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'loggedIn' => true,
|
||||
'nama' => $_SESSION['user_nama'] ?? '',
|
||||
'role' => $_SESSION['user_role'] ?? ''
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// php/create_anggota.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$kemiskinanId = intval($_POST['kemiskinan_id'] ?? 0);
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$tanggalLahir = trim($_POST['tanggal_lahir'] ?? '');
|
||||
$jenisKelamin = trim($_POST['jenis_kelamin'] ?? 'laki_laki');
|
||||
$hubungan = trim($_POST['hubungan_keluarga'] ?? 'anak');
|
||||
$statusHidup = trim($_POST['status_hidup'] ?? 'hidup');
|
||||
$pendidikan = trim($_POST['pendidikan'] ?? 'belum_sekolah');
|
||||
$catatan = trim($_POST['catatan_khusus'] ?? '');
|
||||
|
||||
if (!$kemiskinanId || !$nama) {
|
||||
http_response_code(400); echo 'Data tidak lengkap.'; exit;
|
||||
}
|
||||
|
||||
// Validasi enum
|
||||
$validJK = ['laki_laki','perempuan'];
|
||||
$validHub = ['kepala_keluarga','istri','anak','orang_tua','saudara','lainnya'];
|
||||
$validStatus = ['hidup','meninggal','pindah','menikah_keluar'];
|
||||
$validPendid = ['belum_sekolah','paud','sd','smp','sma','kuliah','pesantren','pelatihan','bekerja','tidak_sekolah'];
|
||||
|
||||
if (!in_array($jenisKelamin, $validJK)) $jenisKelamin = 'laki_laki';
|
||||
if (!in_array($hubungan, $validHub)) $hubungan = 'anak';
|
||||
if (!in_array($statusHidup, $validStatus)) $statusHidup = 'hidup';
|
||||
if (!in_array($pendidikan, $validPendid)) $pendidikan = 'belum_sekolah';
|
||||
|
||||
// Validasi tanggal
|
||||
$tgl = (!empty($tanggalLahir) && strtotime($tanggalLahir)) ? $tanggalLahir : null;
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO anggota_keluarga (kemiskinan_id, nama, tanggal_lahir, jenis_kelamin, hubungan_keluarga, status_hidup, pendidikan, catatan_khusus)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
if (!$stmt) { http_response_code(500); echo 'Gagal prepare.'; exit; }
|
||||
|
||||
$stmt->bind_param('isssssss', $kemiskinanId, $nama, $tgl, $jenisKelamin, $hubungan, $statusHidup, $pendidikan, $catatan);
|
||||
if ($stmt->execute()) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500); echo 'Gagal menyimpan: ' . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// php/create_bantuan.php — simpan histori bantuan
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$kemiskinanId = intval($_POST['kemiskinan_id'] ?? 0);
|
||||
$masjidId = !empty($_POST['masjid_id']) ? intval($_POST['masjid_id']) : null;
|
||||
$jenisBantuan = trim($_POST['jenis_bantuan'] ?? 'lainnya');
|
||||
$deskripsi = trim($_POST['deskripsi_bantuan'] ?? '');
|
||||
$jumlahBantuan = !empty($_POST['jumlah_bantuan']) ? floatval($_POST['jumlah_bantuan']) : null;
|
||||
$satuan = trim($_POST['satuan'] ?? '');
|
||||
$tanggal = trim($_POST['tanggal_bantuan'] ?? date('Y-m-d'));
|
||||
$periodeBerikutnya = !empty($_POST['periode_berikutnya']) ? trim($_POST['periode_berikutnya']) : null;
|
||||
$statusPenyaluran = trim($_POST['status_penyaluran'] ?? 'belum');
|
||||
$dicatatOleh = trim($_POST['dicatat_oleh'] ?? '');
|
||||
|
||||
if (!$kemiskinanId || !$deskripsi) {
|
||||
http_response_code(400); echo 'Data tidak lengkap.'; exit;
|
||||
}
|
||||
$validJenis = ['sembako','uang','pendidikan','kesehatan','pakaian','pelatihan','lainnya'];
|
||||
$validStatus = ['tersalur','belum','batal'];
|
||||
if (!in_array($jenisBantuan, $validJenis)) $jenisBantuan = 'lainnya';
|
||||
if (!in_array($statusPenyaluran, $validStatus)) $statusPenyaluran = 'belum';
|
||||
if (!strtotime($tanggal)) $tanggal = date('Y-m-d');
|
||||
if ($periodeBerikutnya && !strtotime($periodeBerikutnya)) $periodeBerikutnya = null;
|
||||
|
||||
$esc = fn($v) => mysqli_real_escape_string($conn, (string)$v);
|
||||
|
||||
$mjId = $masjidId ? intval($masjidId) : 'NULL';
|
||||
$jml = $jumlahBantuan !== null ? floatval($jumlahBantuan) : 'NULL';
|
||||
$prd = $periodeBerikutnya ? "'" . $esc($periodeBerikutnya) . "'" : 'NULL';
|
||||
|
||||
$sql = "INSERT INTO histori_bantuan
|
||||
(kemiskinan_id, masjid_id, jenis_bantuan, deskripsi_bantuan, jumlah_bantuan, satuan, tanggal_bantuan, periode_berikutnya, status_penyaluran, dicatat_oleh)
|
||||
VALUES (
|
||||
{$kemiskinanId}, {$mjId},
|
||||
'{$esc($jenisBantuan)}', '{$esc($deskripsi)}',
|
||||
{$jml}, '{$esc($satuan)}', '{$esc($tanggal)}',
|
||||
{$prd}, '{$esc($statusPenyaluran)}', '{$esc($dicatatOleh)}'
|
||||
)";
|
||||
|
||||
if (mysqli_query($conn, $sql)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500); echo 'Gagal: ' . mysqli_error($conn);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// php/create_kemiskinan.php
|
||||
include 'koneksi.php';
|
||||
|
||||
$namaKepalaKeluarga = trim($_POST['nama_kepala_keluarga'] ?? '');
|
||||
$jumlahKkRaw = $_POST['jumlah_kk'] ?? null;
|
||||
$jumlahKk = filter_var($jumlahKkRaw, FILTER_VALIDATE_INT);
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$statusKeluarga = $_POST['status_keluarga'] ?? 'aktif';
|
||||
$latitudeRaw = $_POST['lat'] ?? null;
|
||||
$longitudeRaw = $_POST['lng'] ?? null;
|
||||
$latitude = is_numeric($latitudeRaw) ? (float) $latitudeRaw : null;
|
||||
$longitude = is_numeric($longitudeRaw) ? (float) $longitudeRaw : null;
|
||||
|
||||
$validStatus = ['aktif', 'meninggal', 'pindah', 'sudah_mampu'];
|
||||
if (!in_array($statusKeluarga, $validStatus)) { $statusKeluarga = 'aktif'; }
|
||||
|
||||
if ($namaKepalaKeluarga === '' || $alamat === '' || $jumlahKk === false || $jumlahKk === null || $jumlahKk < 1 || $latitude === null || $longitude === null) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = null;
|
||||
|
||||
try {
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"INSERT INTO kemiskinan (nama_kepala_keluarga, jumlah_kk, alamat, latitude, longitude, status_keluarga) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "sisdds", $namaKepalaKeluarga, $jumlahKk, $alamat, $latitude, $longitude, $statusKeluarga);
|
||||
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
if ($statement) {
|
||||
mysqli_stmt_close($statement);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// php/create_laporan.php — PUBLIK, tidak butuh login
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$kemiskinanId = !empty($_POST['kemiskinan_id']) ? intval($_POST['kemiskinan_id']) : null;
|
||||
$namaPelapor = trim($_POST['nama_pelapor'] ?? '');
|
||||
$nomorWa = trim($_POST['nomor_wa'] ?? '');
|
||||
$kategori = trim($_POST['kategori'] ?? 'lainnya');
|
||||
$deskripsi = trim($_POST['deskripsi'] ?? '');
|
||||
|
||||
if (!$namaPelapor || !$deskripsi) {
|
||||
http_response_code(400); echo 'Nama pelapor dan deskripsi wajib diisi.'; exit;
|
||||
}
|
||||
|
||||
$validKategori = ['sakit','kematian','kelahiran','pernikahan','pindah','darurat_ekonomi','lainnya'];
|
||||
if (!in_array($kategori, $validKategori)) $kategori = 'lainnya';
|
||||
|
||||
$esc = fn($v) => mysqli_real_escape_string($conn, (string)$v);
|
||||
$kemId = $kemiskinanId ? intval($kemiskinanId) : 'NULL';
|
||||
|
||||
$sql = "INSERT INTO laporan_kondisi (kemiskinan_id, nama_pelapor, nomor_wa, kategori, deskripsi)
|
||||
VALUES ({$kemId}, '{$esc($namaPelapor)}', '{$esc($nomorWa)}', '{$esc($kategori)}', '{$esc($deskripsi)}')";
|
||||
|
||||
if (mysqli_query($conn, $sql)) { echo 'success'; }
|
||||
else { http_response_code(500); echo 'Gagal: ' . mysqli_error($conn); }
|
||||
?>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
// php/create_masjid.php — tambah rumah ibadah
|
||||
include 'koneksi.php';
|
||||
|
||||
$namaIbadah = trim($_POST['nama_masjid'] ?? '');
|
||||
$picIbadah = trim($_POST['pic_masjid'] ?? '');
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$latitudeRaw = $_POST['lat'] ?? null;
|
||||
$longitudeRaw= $_POST['lng'] ?? null;
|
||||
$radiusRaw = $_POST['radius'] ?? 500;
|
||||
$jenisIbadah = trim($_POST['jenis_ibadah'] ?? 'masjid');
|
||||
$latitude = is_numeric($latitudeRaw) ? (float) $latitudeRaw : null;
|
||||
$longitude = is_numeric($longitudeRaw) ? (float) $longitudeRaw : null;
|
||||
$radius = is_numeric($radiusRaw) ? (float) $radiusRaw : null;
|
||||
|
||||
$validJenis = ['masjid','gereja','vihara','pura','klenteng','kapel','lainnya'];
|
||||
if (!in_array($jenisIbadah, $validJenis)) { $jenisIbadah = 'lainnya'; }
|
||||
|
||||
if ($namaIbadah === '' || $picIbadah === '' || $alamat === '' || $latitude === null || $longitude === null || $radius === null || $radius < 100 || $radius > 5000) {
|
||||
http_response_code(400);
|
||||
exit('error: data tidak lengkap');
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO masjid (nama_masjid, pic_masjid, alamat, latitude, longitude, radius, jenis_ibadah)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
if (!$stmt) { http_response_code(500); echo 'Gagal prepare: ' . $conn->error; exit; }
|
||||
|
||||
$stmt->bind_param('sssddis', $namaIbadah, $picIbadah, $alamat, $latitude, $longitude, $radius, $jenisIbadah);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
?>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// php/delete_anggota.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
if (!$id) { http_response_code(400); echo 'ID tidak valid.'; exit; }
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM anggota_keluarga WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
if ($stmt->execute()) { echo 'success'; }
|
||||
else { http_response_code(500); echo 'Gagal: ' . $stmt->error; }
|
||||
$stmt->close();
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// php/delete_bantuan.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
if (!$id) { http_response_code(400); echo 'ID tidak valid.'; exit; }
|
||||
if (mysqli_query($conn, "DELETE FROM histori_bantuan WHERE id={$id}")) { echo 'success'; }
|
||||
else { http_response_code(500); echo mysqli_error($conn); }
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
|
||||
if ($id === false || $id === null || $id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "DELETE FROM kemiskinan WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "i", $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
|
||||
if ($id === false || $id === null || $id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "DELETE FROM masjid WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "i", $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE & ~E_WARNING);
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="Data_Keluarga_Prasejahtera_GeoPoverty.csv"');
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
// Header CSV
|
||||
fputcsv($output, [
|
||||
'ID',
|
||||
'Nama Kepala Keluarga',
|
||||
'Alamat',
|
||||
'Latitude',
|
||||
'Longitude',
|
||||
'Status Keluarga',
|
||||
'Jumlah Jiwa',
|
||||
'Daftar Anggota Keluarga (Nama - Hubungan - Pendidikan)',
|
||||
'Total Bantuan Diterima (Kali)',
|
||||
'Riwayat Bantuan (Detail)',
|
||||
'Laporan Darurat / Kondisi',
|
||||
'Tanggal Didata',
|
||||
'Terakhir Diubah'
|
||||
]);
|
||||
|
||||
$query = "
|
||||
SELECT
|
||||
k.id, k.nama_kepala_keluarga, k.alamat, k.latitude, k.longitude, k.status_keluarga, k.jumlah_kk, k.created_at, k.updated_at,
|
||||
(SELECT GROUP_CONCAT(CONCAT(nama, ' (', hubungan_keluarga, ' - ', pendidikan, ')') SEPARATOR ' | ') FROM anggota_keluarga WHERE kemiskinan_id = k.id) as rincian_anggota,
|
||||
(SELECT COUNT(id) FROM histori_bantuan WHERE kemiskinan_id = k.id AND status_penyaluran = 'tersalur') as total_bantuan,
|
||||
(SELECT GROUP_CONCAT(CONCAT(tanggal_bantuan, ': ', jenis_bantuan, ' - ', deskripsi_bantuan) SEPARATOR ' | ') FROM histori_bantuan WHERE kemiskinan_id = k.id AND status_penyaluran = 'tersalur') as riwayat_bantuan,
|
||||
(SELECT GROUP_CONCAT(CONCAT(DATE(created_at), ': ', kategori, ' - ', deskripsi) SEPARATOR ' | ') FROM laporan_kondisi WHERE kemiskinan_id = k.id) as laporan_darurat
|
||||
FROM kemiskinan k
|
||||
ORDER BY k.id DESC
|
||||
";
|
||||
|
||||
$result = mysqli_query($conn, $query);
|
||||
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
|
||||
// Clean status label
|
||||
$status = ucfirst(str_replace('_', ' ', $row['status_keluarga']));
|
||||
if ($row['status_keluarga'] == 'sudah_mampu') $status = 'Sudah Mampu';
|
||||
|
||||
fputcsv($output, [
|
||||
$row['id'],
|
||||
$row['nama_kepala_keluarga'],
|
||||
$row['alamat'],
|
||||
$row['latitude'],
|
||||
$row['longitude'],
|
||||
$status,
|
||||
$row['jumlah_kk'],
|
||||
$row['rincian_anggota'] ? $row['rincian_anggota'] : 'Belum didata',
|
||||
$row['total_bantuan'] . ' kali',
|
||||
$row['riwayat_bantuan'] ? $row['riwayat_bantuan'] : 'Tidak ada catatan bantuan',
|
||||
$row['laporan_darurat'] ? $row['laporan_darurat'] : 'Tidak ada laporan',
|
||||
$row['created_at'],
|
||||
$row['updated_at']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
exit();
|
||||
?>
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
$conn = mysqli_connect("127.0.0.1", "root", "", "webgis_kemiskinan");
|
||||
|
||||
if (!$conn) {
|
||||
die("Koneksi gagal: " . mysqli_connect_error());
|
||||
}
|
||||
|
||||
mysqli_set_charset($conn, "utf8mb4");
|
||||
|
||||
// Helper: tambah kolom hanya jika belum ada
|
||||
function addColumnIfNotExists($conn, $table, $column, $definition)
|
||||
{
|
||||
$db = mysqli_select_db($conn, 'webgis_kemiskinan') ? 'webgis_kemiskinan' : '';
|
||||
$check = mysqli_query(
|
||||
$conn,
|
||||
"SELECT COUNT(*) as cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = '$column'"
|
||||
);
|
||||
if ($check) {
|
||||
$row = mysqli_fetch_assoc($check);
|
||||
if ((int) $row['cnt'] === 0) {
|
||||
mysqli_query($conn, "ALTER TABLE `$table` ADD COLUMN `$column` $definition");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$bootstrapQueries = [
|
||||
// ── Tabel: kemiskinan
|
||||
"CREATE TABLE IF NOT EXISTS kemiskinan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_kepala_keluarga VARCHAR(255) NOT NULL,
|
||||
jumlah_kk INT NOT NULL,
|
||||
alamat TEXT NOT NULL,
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
status_keluarga ENUM('aktif','meninggal','pindah','sudah_mampu') NOT NULL DEFAULT 'aktif',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
// ── Tabel: masjid (rumah ibadah)
|
||||
"CREATE TABLE IF NOT EXISTS masjid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_masjid VARCHAR(255) NOT NULL,
|
||||
pic_masjid VARCHAR(255) NOT NULL,
|
||||
alamat TEXT NOT NULL,
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
radius INT NOT NULL DEFAULT 500,
|
||||
jenis_ibadah ENUM('masjid','gereja','vihara','pura','klenteng','kapel','lainnya') NOT NULL DEFAULT 'masjid',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
// ── Tabel: pengguna (login multi-role)
|
||||
"CREATE TABLE IF NOT EXISTS pengguna (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
nama_lengkap VARCHAR(255) NOT NULL,
|
||||
role ENUM('pengurus_masjid','pimpinan_daerah') NOT NULL DEFAULT 'pengurus_masjid',
|
||||
masjid_id INT NULL,
|
||||
aktif TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
// ── Tabel: anggota_keluarga (individu per KK)
|
||||
"CREATE TABLE IF NOT EXISTS anggota_keluarga (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kemiskinan_id INT NOT NULL,
|
||||
nama VARCHAR(255) NOT NULL,
|
||||
tanggal_lahir DATE NULL,
|
||||
jenis_kelamin ENUM('laki_laki','perempuan') NOT NULL DEFAULT 'laki_laki',
|
||||
hubungan_keluarga ENUM('kepala_keluarga','istri','anak','orang_tua','saudara','lainnya') NOT NULL DEFAULT 'anak',
|
||||
status_hidup ENUM('hidup','meninggal','pindah','menikah_keluar') NOT NULL DEFAULT 'hidup',
|
||||
pendidikan ENUM('belum_sekolah','paud','sd','smp','sma','kuliah','pesantren','pelatihan','bekerja','tidak_sekolah') NOT NULL DEFAULT 'belum_sekolah',
|
||||
catatan_khusus TEXT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (kemiskinan_id) REFERENCES kemiskinan(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
// ── Tabel: histori_bantuan
|
||||
"CREATE TABLE IF NOT EXISTS histori_bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kemiskinan_id INT NOT NULL,
|
||||
masjid_id INT NULL,
|
||||
jenis_bantuan ENUM('sembako','uang','pendidikan','kesehatan','pakaian','pelatihan','lainnya') NOT NULL DEFAULT 'lainnya',
|
||||
deskripsi_bantuan TEXT NOT NULL,
|
||||
jumlah_bantuan DECIMAL(10,2) NULL,
|
||||
satuan VARCHAR(50) NULL COMMENT 'e.g. karung, kg, Rp, bulan',
|
||||
tanggal_bantuan DATE NOT NULL,
|
||||
periode_berikutnya DATE NULL COMMENT 'Untuk bantuan berkala',
|
||||
status_penyaluran ENUM('tersalur','belum','batal') NOT NULL DEFAULT 'belum',
|
||||
dicatat_oleh VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (kemiskinan_id) REFERENCES kemiskinan(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
// ── Tabel: laporan_kondisi (dari warga)
|
||||
"CREATE TABLE IF NOT EXISTS laporan_kondisi (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kemiskinan_id INT NULL,
|
||||
nama_pelapor VARCHAR(255) NOT NULL,
|
||||
nomor_wa VARCHAR(20) NULL,
|
||||
kategori ENUM('sakit','kematian','kelahiran','pernikahan','pindah','darurat_ekonomi','lainnya') NOT NULL DEFAULT 'lainnya',
|
||||
deskripsi TEXT NOT NULL,
|
||||
status_laporan ENUM('menunggu','diproses','selesai','ditolak') NOT NULL DEFAULT 'menunggu',
|
||||
ditangani_oleh VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
// ── Tabel: pesan_komunikasi
|
||||
"CREATE TABLE IF NOT EXISTS pesan_komunikasi (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
pengirim_id INT NOT NULL,
|
||||
penerima_id INT NULL COMMENT 'NULL = broadcast ke semua',
|
||||
judul VARCHAR(255) NOT NULL,
|
||||
isi TEXT NOT NULL,
|
||||
jenis ENUM('permintaan_bantuan','koordinasi','pengumuman') NOT NULL DEFAULT 'koordinasi',
|
||||
status ENUM('baru','dibaca','diarsip') NOT NULL DEFAULT 'baru',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
];
|
||||
|
||||
foreach ($bootstrapQueries as $query) {
|
||||
mysqli_query($conn, $query);
|
||||
}
|
||||
|
||||
// ── Migrasi aman: tambah kolom baru ke tabel yang sudah ada
|
||||
addColumnIfNotExists(
|
||||
$conn,
|
||||
'kemiskinan',
|
||||
'status_keluarga',
|
||||
"ENUM('aktif','meninggal','pindah','sudah_mampu') NOT NULL DEFAULT 'aktif'"
|
||||
);
|
||||
addColumnIfNotExists(
|
||||
$conn,
|
||||
'kemiskinan',
|
||||
'updated_at',
|
||||
"TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
|
||||
);
|
||||
addColumnIfNotExists(
|
||||
$conn,
|
||||
'masjid',
|
||||
'jenis_ibadah',
|
||||
"ENUM('masjid','gereja','vihara','pura','klenteng','kapel','lainnya') NOT NULL DEFAULT 'masjid'"
|
||||
);
|
||||
|
||||
// ── Seed akun default jika tabel pengguna masih kosong
|
||||
$checkUser = mysqli_query($conn, "SELECT COUNT(*) as total FROM pengguna");
|
||||
if ($checkUser) {
|
||||
$row = mysqli_fetch_assoc($checkUser);
|
||||
if ((int) $row['total'] === 0) {
|
||||
$defaultUsers = [
|
||||
['pengurus1', password_hash('password', PASSWORD_DEFAULT), 'Pengurus Masjid Al-Ikhlas', 'pengurus_masjid'],
|
||||
['pimpinan1', password_hash('password', PASSWORD_DEFAULT), 'Kepala Dinas Sosial', 'pimpinan_daerah'],
|
||||
];
|
||||
$stmt = mysqli_prepare($conn, "INSERT INTO pengguna (username, password_hash, nama_lengkap, role) VALUES (?, ?, ?, ?)");
|
||||
if ($stmt) {
|
||||
foreach ($defaultUsers as $u) {
|
||||
mysqli_stmt_bind_param($stmt, 'ssss', $u[0], $u[1], $u[2], $u[3]);
|
||||
mysqli_stmt_execute($stmt);
|
||||
}
|
||||
mysqli_stmt_close($stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helper: cek autentikasi (panggil di endpoint yang butuh login)
|
||||
function requireAuth($requiredRole = null)
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
if (empty($_SESSION['user_id'])) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Tidak terautentikasi. Silakan login.']);
|
||||
exit;
|
||||
}
|
||||
if ($requiredRole && $_SESSION['user_role'] !== $requiredRole) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Akses ditolak. Role tidak sesuai.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function isLoggedIn()
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
return !empty($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
function currentRole()
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
return $_SESSION['user_role'] ?? null;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// php/login.php
|
||||
require_once 'koneksi.php';
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
|
||||
if (!$username || !$password) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Username dan password wajib diisi.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT id, password_hash, nama_lengkap, role FROM pengguna WHERE username = ? AND aktif = 1 LIMIT 1");
|
||||
$stmt->bind_param('s', $username);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$user = $result->fetch_assoc();
|
||||
$stmt->close();
|
||||
|
||||
if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Username atau password salah.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user_nama'] = $user['nama_lengkap'];
|
||||
$_SESSION['user_role'] = $user['role'];
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'nama' => $user['nama_lengkap'],
|
||||
'role' => $user['role']
|
||||
]);
|
||||
?>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
// php/logout.php
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
session_destroy();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true]);
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
// php/read_anggota.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$kemiskinanId = intval($_GET['kemiskinan_id'] ?? 0);
|
||||
if (!$kemiskinanId) {
|
||||
echo json_encode([]); exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT *,
|
||||
TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE()) AS umur,
|
||||
DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i') AS updated_at_fmt,
|
||||
DATE_FORMAT(tanggal_lahir, '%d/%m/%Y') AS tgl_lahir_fmt
|
||||
FROM anggota_keluarga
|
||||
WHERE kemiskinan_id = ?
|
||||
ORDER BY hubungan_keluarga, nama"
|
||||
);
|
||||
$stmt->bind_param('i', $kemiskinanId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$data[] = $row;
|
||||
}
|
||||
$stmt->close();
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// php/read_bantuan.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$kemiskinanId = intval($_GET['kemiskinan_id'] ?? 0);
|
||||
if (!$kemiskinanId) { echo json_encode([]); exit; }
|
||||
|
||||
$esc = fn($v) => mysqli_real_escape_string($conn, (string)$v);
|
||||
|
||||
$sql = "SELECT hb.*, m.nama_masjid,
|
||||
DATE_FORMAT(hb.tanggal_bantuan, '%d/%m/%Y') AS tanggal_fmt,
|
||||
DATE_FORMAT(hb.periode_berikutnya, '%d/%m/%Y') AS periode_fmt,
|
||||
CASE WHEN hb.periode_berikutnya IS NOT NULL AND hb.periode_berikutnya <= CURDATE() AND hb.status_penyaluran = 'belum' THEN 1 ELSE 0 END AS jatuh_tempo
|
||||
FROM histori_bantuan hb
|
||||
LEFT JOIN masjid m ON hb.masjid_id = m.id
|
||||
WHERE hb.kemiskinan_id = {$kemiskinanId}
|
||||
ORDER BY hb.tanggal_bantuan DESC";
|
||||
|
||||
$result = mysqli_query($conn, $sql);
|
||||
$data = [];
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
// php/read_kemiskinan.php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$query = "SELECT *, DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i:%s') as updated_at_fmt FROM kemiskinan ORDER BY updated_at DESC, id DESC";
|
||||
$result = mysqli_query($conn, $query);
|
||||
$data = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// php/read_laporan.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$statusFilter = trim($_GET['status'] ?? '');
|
||||
$kemiskinanIdRaw = isset($_GET['kemiskinan_id']) ? intval($_GET['kemiskinan_id']) : 0;
|
||||
$validStatus = ['menunggu','diproses','selesai','ditolak'];
|
||||
|
||||
$conditions = [];
|
||||
|
||||
if ($kemiskinanIdRaw > 0) {
|
||||
$conditions[] = "lk.kemiskinan_id = {$kemiskinanIdRaw}";
|
||||
}
|
||||
if (in_array($statusFilter, $validStatus)) {
|
||||
$esc = mysqli_real_escape_string($conn, $statusFilter);
|
||||
$conditions[] = "lk.status_laporan = '{$esc}'";
|
||||
}
|
||||
|
||||
$where = count($conditions) ? 'WHERE ' . implode(' AND ', $conditions) : '';
|
||||
|
||||
$sql = "SELECT lk.*, k.nama_kepala_keluarga, k.alamat AS alamat_kk,
|
||||
DATE_FORMAT(lk.created_at, '%d/%m/%Y %H:%i') AS created_fmt
|
||||
FROM laporan_kondisi lk
|
||||
LEFT JOIN kemiskinan k ON lk.kemiskinan_id = k.id
|
||||
{$where}
|
||||
ORDER BY lk.created_at DESC
|
||||
LIMIT 200";
|
||||
|
||||
$result = mysqli_query($conn, $sql);
|
||||
$data = [];
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) { $data[] = $row; }
|
||||
}
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$query = "SELECT * FROM masjid ORDER BY id DESC";
|
||||
$result = mysqli_query($conn, $query);
|
||||
$data = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// php/read_pesan.php
|
||||
require_once 'koneksi.php';
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$userId = intval($_SESSION['user_id'] ?? 0);
|
||||
if (!$userId) {
|
||||
echo json_encode([]); exit;
|
||||
}
|
||||
|
||||
// Ambil pesan broadcast + pesan yang ditujukan ke user ini
|
||||
$sql = "SELECT pm.*,
|
||||
p.nama_lengkap AS nama_pengirim, p.role AS role_pengirim,
|
||||
DATE_FORMAT(pm.created_at, '%d/%m/%Y %H:%i') AS created_fmt
|
||||
FROM pesan_komunikasi pm
|
||||
JOIN pengguna p ON pm.pengirim_id = p.id
|
||||
WHERE pm.penerima_id IS NULL OR pm.penerima_id = {$userId}
|
||||
ORDER BY pm.created_at DESC
|
||||
LIMIT 100";
|
||||
|
||||
$result = mysqli_query($conn, $sql);
|
||||
$data = [];
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) { $data[] = $row; }
|
||||
}
|
||||
|
||||
// Mark as read — semua pesan yang belum dibaca oleh user ini
|
||||
$markSql = "UPDATE pesan_komunikasi SET status='dibaca'
|
||||
WHERE status='baru' AND (penerima_id = {$userId} OR penerima_id IS NULL) AND pengirim_id != {$userId}";
|
||||
mysqli_query($conn, $markSql);
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// php/send_pesan.php
|
||||
require_once 'koneksi.php';
|
||||
if (session_status() === PHP_SESSION_NONE) { session_start(); }
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$pengirimId = intval($_SESSION['user_id'] ?? 0);
|
||||
if (!$pengirimId) { http_response_code(401); echo 'Tidak terautentikasi.'; exit; }
|
||||
|
||||
$penerimaId = !empty($_POST['penerima_id']) ? intval($_POST['penerima_id']) : null;
|
||||
$judul = trim($_POST['judul'] ?? '');
|
||||
$isi = trim($_POST['isi'] ?? '');
|
||||
$jenis = trim($_POST['jenis'] ?? 'koordinasi');
|
||||
|
||||
if (!$judul || !$isi) { http_response_code(400); echo 'Judul dan isi wajib diisi.'; exit; }
|
||||
$validJenis = ['permintaan_bantuan','koordinasi','pengumuman'];
|
||||
if (!in_array($jenis, $validJenis)) $jenis = 'koordinasi';
|
||||
|
||||
$esc = fn($v) => mysqli_real_escape_string($conn, (string)$v);
|
||||
$penerima = $penerimaId ? intval($penerimaId) : 'NULL';
|
||||
|
||||
$sql = "INSERT INTO pesan_komunikasi (pengirim_id, penerima_id, judul, isi, jenis)
|
||||
VALUES ({$pengirimId}, {$penerima}, '{$esc($judul)}', '{$esc($isi)}', '{$esc($jenis)}')";
|
||||
|
||||
if (mysqli_query($conn, $sql)) { echo 'success'; }
|
||||
else { http_response_code(500); echo mysqli_error($conn); }
|
||||
?>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// php/update_anggota.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$tanggalLahir = trim($_POST['tanggal_lahir'] ?? '');
|
||||
$jenisKelamin = trim($_POST['jenis_kelamin'] ?? 'laki_laki');
|
||||
$hubungan = trim($_POST['hubungan_keluarga'] ?? 'anak');
|
||||
$statusHidup = trim($_POST['status_hidup'] ?? 'hidup');
|
||||
$pendidikan = trim($_POST['pendidikan'] ?? 'belum_sekolah');
|
||||
$catatan = trim($_POST['catatan_khusus'] ?? '');
|
||||
|
||||
if (!$id || !$nama) {
|
||||
http_response_code(400); echo 'Data tidak lengkap.'; exit;
|
||||
}
|
||||
|
||||
$validJK = ['laki_laki','perempuan'];
|
||||
$validHub = ['kepala_keluarga','istri','anak','orang_tua','saudara','lainnya'];
|
||||
$validStatus = ['hidup','meninggal','pindah','menikah_keluar'];
|
||||
$validPendid = ['belum_sekolah','paud','sd','smp','sma','kuliah','pesantren','pelatihan','bekerja','tidak_sekolah'];
|
||||
|
||||
if (!in_array($jenisKelamin, $validJK)) $jenisKelamin = 'laki_laki';
|
||||
if (!in_array($hubungan, $validHub)) $hubungan = 'anak';
|
||||
if (!in_array($statusHidup, $validStatus)) $statusHidup = 'hidup';
|
||||
if (!in_array($pendidikan, $validPendid)) $pendidikan = 'belum_sekolah';
|
||||
|
||||
// Validasi tanggal — null jika kosong/tidak valid
|
||||
$tgl = (!empty($tanggalLahir) && strtotime($tanggalLahir)) ? $tanggalLahir : null;
|
||||
|
||||
// 8 kolom: nama(s) tgl(s) jk(s) hub(s) status(s) pendidikan(s) catatan(s) id(i)
|
||||
$stmt = $conn->prepare(
|
||||
"UPDATE anggota_keluarga
|
||||
SET nama=?, tanggal_lahir=?, jenis_kelamin=?, hubungan_keluarga=?,
|
||||
status_hidup=?, pendidikan=?, catatan_khusus=?
|
||||
WHERE id=?"
|
||||
);
|
||||
if (!$stmt) { http_response_code(500); echo 'Gagal prepare: ' . $conn->error; exit; }
|
||||
|
||||
$stmt->bind_param('sssssssi', $nama, $tgl, $jenisKelamin, $hubungan, $statusHidup, $pendidikan, $catatan, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500); echo 'Gagal: ' . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
// php/update_bantuan_status.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$status = trim($_POST['status_penyaluran'] ?? '');
|
||||
$valid = ['tersalur','belum','batal'];
|
||||
|
||||
if (!$id || !in_array($status, $valid)) {
|
||||
http_response_code(400); echo 'Data tidak valid.'; exit;
|
||||
}
|
||||
|
||||
$sql = "UPDATE histori_bantuan SET status_penyaluran='" . mysqli_real_escape_string($conn, $status) . "' WHERE id={$id}";
|
||||
if (mysqli_query($conn, $sql)) { echo 'success'; }
|
||||
else { http_response_code(500); echo 'Gagal: ' . mysqli_error($conn); }
|
||||
?>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// php/update_kemiskinan.php
|
||||
require_once 'koneksi.php';
|
||||
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
$nama = isset($_POST['nama_kepala_keluarga']) ? trim($_POST['nama_kepala_keluarga']) : '';
|
||||
$jml = isset($_POST['jumlah_kk']) ? intval($_POST['jumlah_kk']) : 0;
|
||||
$alamat = isset($_POST['alamat']) ? trim($_POST['alamat']) : '';
|
||||
$status = isset($_POST['status_keluarga']) ? trim($_POST['status_keluarga']) : 'aktif';
|
||||
|
||||
$validStatus = ['aktif', 'meninggal', 'pindah', 'sudah_mampu'];
|
||||
if (!in_array($status, $validStatus)) { $status = 'aktif'; }
|
||||
|
||||
if (!$id || !$nama || $jml < 1 || !$alamat) {
|
||||
http_response_code(400);
|
||||
echo 'Data tidak lengkap atau tidak valid.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE kemiskinan SET nama_kepala_keluarga=?, jumlah_kk=?, alamat=?, status_keluarga=? WHERE id=?");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo 'Gagal menyiapkan query.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param('sissi', $nama, $jml, $alamat, $status, $id);
|
||||
if ($stmt->execute()) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'Gagal memperbarui data: ' . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
// php/update_laporan_status.php
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$status = trim($_POST['status_laporan'] ?? '');
|
||||
$ditanganiOleh = trim($_POST['ditangani_oleh'] ?? '');
|
||||
$valid = ['menunggu','diproses','selesai','ditolak'];
|
||||
|
||||
if (!$id || !in_array($status, $valid)) {
|
||||
http_response_code(400); echo 'Data tidak valid.'; exit;
|
||||
}
|
||||
|
||||
$esc = fn($v) => mysqli_real_escape_string($conn, (string)$v);
|
||||
$sql = "UPDATE laporan_kondisi SET status_laporan='{$esc($status)}', ditangani_oleh='{$esc($ditanganiOleh)}' WHERE id={$id}";
|
||||
|
||||
if (mysqli_query($conn, $sql)) { echo 'success'; }
|
||||
else { http_response_code(500); echo mysqli_error($conn); }
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// php/update_masjid.php — perbarui rumah ibadah
|
||||
require_once 'koneksi.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
$nama = isset($_POST['nama_masjid']) ? trim($_POST['nama_masjid']) : '';
|
||||
$pic = isset($_POST['pic_masjid']) ? trim($_POST['pic_masjid']) : '';
|
||||
$alamat = isset($_POST['alamat']) ? trim($_POST['alamat']) : '';
|
||||
$jenisIbadah = isset($_POST['jenis_ibadah'])? trim($_POST['jenis_ibadah']) : 'masjid';
|
||||
|
||||
$validJenis = ['masjid','gereja','vihara','pura','klenteng','kapel','lainnya'];
|
||||
if (!in_array($jenisIbadah, $validJenis)) { $jenisIbadah = 'lainnya'; }
|
||||
|
||||
if (!$id || !$nama || !$pic || !$alamat) {
|
||||
http_response_code(400);
|
||||
echo 'Data tidak lengkap.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE masjid SET nama_masjid=?, pic_masjid=?, alamat=?, jenis_ibadah=? WHERE id=?");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo 'Gagal menyiapkan query.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param('ssssi', $nama, $pic, $alamat, $jenisIbadah, $id);
|
||||
if ($stmt->execute()) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'Gagal memperbarui data: ' . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$radiusRaw = $_POST['radius'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
$radius = is_numeric($radiusRaw) ? (float) $radiusRaw : null;
|
||||
|
||||
if ($id === false || $id === null || $id <= 0 || $radius === null || $radius < 100 || $radius > 5000) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "UPDATE masjid SET radius = ? WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "di", $radius, $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
include 'check_auth.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||
$status = isset($_POST['status']) ? $_POST['status'] : 'dibaca';
|
||||
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
die("Invalid ID");
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE pesan_komunikasi SET status = ? WHERE id = ?");
|
||||
$stmt->bind_param("si", $status, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo "Success";
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo "Error: " . $conn->error;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,205 @@
|
||||
# 🗺️ WebGIS Project - Manajemen Data Spasial
|
||||
|
||||
## 📌 Deskripsi Proyek
|
||||
|
||||
Proyek ini merupakan aplikasi **WebGIS berbasis LeafletJS, PHP, dan MySQL** yang digunakan untuk mengelola data spasial dalam bentuk:
|
||||
|
||||
* 📍 Point (SPBU)
|
||||
* ➖ Line / Polyline (Jalan)
|
||||
* 🔷 Polygon (Kavling Tanah)
|
||||
|
||||
Aplikasi ini memungkinkan user untuk:
|
||||
|
||||
* Menambahkan data langsung dari peta
|
||||
* Menyimpan ke database
|
||||
* Menampilkan kembali data pada peta dengan visualisasi berbeda
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Tujuan Tugas
|
||||
|
||||
Mahasiswa diharapkan mampu:
|
||||
|
||||
* Mengimplementasikan WebGIS berbasis web
|
||||
* Mengelola data spasial (Point, Line, Polygon)
|
||||
* Melakukan analisis sederhana:
|
||||
|
||||
* Panjang jalan
|
||||
* Luas tanah
|
||||
* Menggunakan LeafletJS untuk visualisasi peta
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Fitur Aplikasi
|
||||
|
||||
### 1. 📍 Data Point - SPBU
|
||||
|
||||
* Input dengan klik pada peta
|
||||
* Menyimpan:
|
||||
|
||||
* Nama SPBU
|
||||
* Nomor SPBU
|
||||
* Status (24 jam / tidak)
|
||||
* Latitude & Longitude
|
||||
* Ditampilkan sebagai marker pada peta
|
||||
|
||||
---
|
||||
|
||||
### 2. ➖ Data Line - Jalan
|
||||
|
||||
* Input dengan menggambar garis (Polyline)
|
||||
* Status jalan:
|
||||
|
||||
* Jalan Nasional
|
||||
* Jalan Provinsi
|
||||
* Jalan Kabupaten
|
||||
* Warna berbeda sesuai status:
|
||||
|
||||
* Nasional → Merah
|
||||
* Provinsi → Biru
|
||||
* Kabupaten → Hijau
|
||||
* Panjang jalan dihitung otomatis (meter)
|
||||
|
||||
---
|
||||
|
||||
### 3. 🔷 Data Polygon - Kavling Tanah
|
||||
|
||||
* Input dengan menggambar area (Polygon)
|
||||
* Status kepemilikan:
|
||||
|
||||
* SHM
|
||||
* HGB
|
||||
* HGU
|
||||
* HP
|
||||
* Warna berbeda sesuai status
|
||||
* Luas tanah dihitung otomatis (m²)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Teknologi yang Digunakan
|
||||
|
||||
* 🌐 Frontend: HTML, CSS, JavaScript
|
||||
* 🗺️ Library: LeafletJS + Leaflet Draw ([leaflet.github.io][1])
|
||||
* 🖥️ Backend: PHP
|
||||
* 🗄️ Database: MySQL
|
||||
|
||||
Leaflet mendukung berbagai bentuk geometri seperti:
|
||||
|
||||
* Marker (Point)
|
||||
* Polyline (Line)
|
||||
* Polygon (Area) ([igismap.com][2])
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Struktur Database
|
||||
|
||||
### Tabel SPBU
|
||||
|
||||
* nama
|
||||
* no_spbu
|
||||
* status
|
||||
* latitude
|
||||
* longitude
|
||||
|
||||
### Tabel Jalan
|
||||
|
||||
* nama
|
||||
* status
|
||||
* panjang
|
||||
* geom (koordinat)
|
||||
|
||||
### Tabel Kavling
|
||||
|
||||
* nama
|
||||
* status
|
||||
* luas
|
||||
* geom (koordinat)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Alur Sistem
|
||||
|
||||
### Input Data
|
||||
|
||||
1. User memilih mode (Point / Line / Polygon)
|
||||
2. User melakukan input di peta:
|
||||
|
||||
* Klik (Point)
|
||||
* Draw (Line / Polygon)
|
||||
3. Sistem menghitung:
|
||||
|
||||
* Panjang (Line)
|
||||
* Luas (Polygon)
|
||||
4. Data dikirim ke server (PHP)
|
||||
5. Data disimpan ke MySQL
|
||||
|
||||
---
|
||||
|
||||
### Menampilkan Data
|
||||
|
||||
1. Data diambil dari database
|
||||
2. Ditampilkan di peta
|
||||
3. Warna disesuaikan berdasarkan kategori
|
||||
4. Data juga ditampilkan di sidebar
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Konsep GIS yang Digunakan
|
||||
|
||||
* Spatial Data:
|
||||
|
||||
* Point
|
||||
* Line
|
||||
* Polygon
|
||||
* Attribute Data
|
||||
* Visualisasi berbasis kategori
|
||||
* Analisis spasial sederhana:
|
||||
|
||||
* Distance (panjang)
|
||||
* Area (luas)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Cara Menjalankan
|
||||
|
||||
1. Jalankan MySQL
|
||||
2. Masuk ke folder project:
|
||||
|
||||
```
|
||||
cd webgis-spbu
|
||||
```
|
||||
3. Jalankan server:
|
||||
|
||||
```
|
||||
php -S localhost:8000
|
||||
```
|
||||
4. Buka di browser:
|
||||
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 Catatan Penting
|
||||
|
||||
* Jangan menggunakan Live Server (tidak support PHP)
|
||||
* Gunakan Leaflet Draw untuk input Line & Polygon
|
||||
* Perhitungan panjang & luas HARUS otomatis (tidak manual)
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Kesimpulan
|
||||
|
||||
Aplikasi ini merupakan implementasi dasar WebGIS yang mencakup:
|
||||
|
||||
* Input data spasial
|
||||
* Penyimpanan database
|
||||
* Visualisasi peta interaktif
|
||||
|
||||
Proyek ini menunjukkan pemahaman terhadap konsep dasar Sistem Informasi Geografis (SIG) dalam pengembangan aplikasi berbasis web.
|
||||
|
||||
---
|
||||
|
||||
[1]: https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html?utm_source=chatgpt.com "Leaflet Draw Documentation"
|
||||
[2]: https://www.igismap.com/leafletjs-point-polyline-polygon-rectangle-circle/?utm_source=chatgpt.com "Leaflet.js - Point, Polyline, Polygon, Rectangle, Circle"
|
||||
@@ -0,0 +1,86 @@
|
||||
CREATE DATABASE IF NOT EXISTS webgis_spbu;
|
||||
|
||||
USE webgis_spbu;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(100) NOT NULL,
|
||||
no_spbu VARCHAR(50) NOT NULL,
|
||||
status ENUM('24jam', 'tidak') NOT NULL,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS jalan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(100) NOT NULL,
|
||||
status ENUM('Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten') NOT NULL,
|
||||
panjang DOUBLE NOT NULL,
|
||||
geom LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kavling (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(100) NOT NULL,
|
||||
status ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
|
||||
luas DOUBLE NOT NULL,
|
||||
geom LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kemiskinan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_kepala_keluarga VARCHAR(150) NOT NULL,
|
||||
jumlah_kk INT NOT NULL,
|
||||
alamat TEXT NOT NULL,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS masjid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_masjid VARCHAR(150) NOT NULL,
|
||||
pic_masjid VARCHAR(150) NOT NULL,
|
||||
alamat TEXT NOT NULL,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
radius DOUBLE NOT NULL DEFAULT 500,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE spbu
|
||||
MODIFY COLUMN nama VARCHAR(100) NOT NULL,
|
||||
MODIFY COLUMN no_spbu VARCHAR(50) NOT NULL,
|
||||
MODIFY COLUMN status ENUM('24jam', 'tidak') NOT NULL,
|
||||
MODIFY COLUMN latitude DOUBLE NOT NULL,
|
||||
MODIFY COLUMN longitude DOUBLE NOT NULL;
|
||||
|
||||
ALTER TABLE jalan
|
||||
MODIFY COLUMN nama VARCHAR(100) NOT NULL,
|
||||
MODIFY COLUMN status ENUM('Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten') NOT NULL,
|
||||
MODIFY COLUMN panjang DOUBLE NOT NULL,
|
||||
MODIFY COLUMN geom LONGTEXT NOT NULL;
|
||||
|
||||
ALTER TABLE kavling
|
||||
MODIFY COLUMN nama VARCHAR(100) NOT NULL,
|
||||
MODIFY COLUMN status ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
|
||||
MODIFY COLUMN luas DOUBLE NOT NULL,
|
||||
MODIFY COLUMN geom LONGTEXT NOT NULL;
|
||||
|
||||
ALTER TABLE kemiskinan
|
||||
MODIFY COLUMN nama_kepala_keluarga VARCHAR(150) NOT NULL,
|
||||
MODIFY COLUMN jumlah_kk INT NOT NULL,
|
||||
MODIFY COLUMN alamat TEXT NOT NULL,
|
||||
MODIFY COLUMN latitude DOUBLE NOT NULL,
|
||||
MODIFY COLUMN longitude DOUBLE NOT NULL;
|
||||
|
||||
ALTER TABLE masjid
|
||||
MODIFY COLUMN nama_masjid VARCHAR(150) NOT NULL,
|
||||
MODIFY COLUMN pic_masjid VARCHAR(150) NOT NULL,
|
||||
MODIFY COLUMN alamat TEXT NOT NULL,
|
||||
MODIFY COLUMN latitude DOUBLE NOT NULL,
|
||||
MODIFY COLUMN longitude DOUBLE NOT NULL,
|
||||
MODIFY COLUMN radius DOUBLE NOT NULL DEFAULT 500;
|
||||
@@ -0,0 +1,939 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebGIS SPBU</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary: #5b5cf6;
|
||||
--primary-strong: #4f46e5;
|
||||
--primary-soft: #ecebff;
|
||||
--success: #16a34a;
|
||||
--danger: #ef4444;
|
||||
--warning: #f97316;
|
||||
--info: #2563eb;
|
||||
--ink: #0f172a;
|
||||
--muted: #667085;
|
||||
--line: #e6eaf2;
|
||||
--panel: rgba(255, 255, 255, 0.96);
|
||||
--shadow: 0 18px 45px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||
color: var(--ink);
|
||||
background: #eef2f7;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 14px;
|
||||
left: 18px;
|
||||
right: 18px;
|
||||
z-index: 1200;
|
||||
height: 64px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.92);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
|
||||
backdrop-filter: blur(16px);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 9px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--primary);
|
||||
background: var(--primary-soft);
|
||||
border: 1px solid #d9d8ff;
|
||||
font-size: 18px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.navbar h2 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: var(--primary-strong);
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.navbar p {
|
||||
margin: 2px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
max-width: 540px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.navbar button,
|
||||
.panel-button,
|
||||
.action-chip {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 11px;
|
||||
background: #fff;
|
||||
color: #475467;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.action-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
color: #344054;
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #667085;
|
||||
border-color: #edf0f6;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.map-stage {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.left-panel,
|
||||
#sidebar,
|
||||
.stats-card {
|
||||
position: absolute;
|
||||
z-index: 1100;
|
||||
border: 1px solid rgba(226, 232, 240, 0.95);
|
||||
border-radius: 16px;
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(16px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
top: 92px;
|
||||
left: 18px;
|
||||
bottom: 18px;
|
||||
width: 292px;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
top: 92px;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
width: 410px;
|
||||
padding: 18px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
left: 336px;
|
||||
bottom: 24px;
|
||||
width: 430px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
margin-bottom: 18px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.mode-switch button {
|
||||
min-height: 40px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
color: #667085;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mode-switch button.active {
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
background: var(--primary);
|
||||
box-shadow: 0 8px 18px rgba(91, 92, 246, 0.24);
|
||||
}
|
||||
|
||||
.panel-button {
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
color: #667085;
|
||||
background: #f8fafc;
|
||||
border-color: #e8edf5;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.panel-button.active {
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
background: var(--primary);
|
||||
box-shadow: 0 8px 18px rgba(91, 92, 246, 0.24);
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.panel-section h3,
|
||||
.section h3,
|
||||
.stats-title {
|
||||
margin: 0 0 10px;
|
||||
color: #667085;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.type-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.panel-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
color: #667085;
|
||||
background: #eef2f7;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
flex: 0 0 28px;
|
||||
}
|
||||
|
||||
.panel-button.active .tool-icon {
|
||||
color: var(--primary);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.legend-list {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
color: #536179;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 99px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
margin: 0;
|
||||
color: var(--primary-strong);
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sidebar-subtitle {
|
||||
margin: 7px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.panel-close {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #667085;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.section-count {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border-radius: 99px;
|
||||
background: #eef2ff;
|
||||
color: var(--primary-strong);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #c8cdfd;
|
||||
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--ink);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
color: #175cd3;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 7px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.green {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.red {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.blue {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.orange {
|
||||
background: #ffedd5;
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.gray {
|
||||
background: #eef2f7;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.delete-btn,
|
||||
.secondary-btn {
|
||||
min-width: 42px;
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: #dc2626;
|
||||
border-color: #fecaca;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
color: #1d4ed8;
|
||||
border-color: #bfdbfe;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.delete-btn:disabled,
|
||||
.secondary-btn:disabled {
|
||||
background: #f3f4f6;
|
||||
color: #a2aab8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.custom-point-icon {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.point-dot {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
border-radius: 999px;
|
||||
border: 3px solid white;
|
||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed #d7dce7;
|
||||
background: #f8fafc;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: rgba(15, 23, 42, 0.46);
|
||||
backdrop-filter: blur(4px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-backdrop.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.edit-modal {
|
||||
width: min(460px, 100%);
|
||||
max-height: min(720px, 92vh);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(226, 232, 240, 0.95);
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.26);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 18px 20px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.edit-modal-title {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 18px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.edit-modal-subtitle {
|
||||
margin: 5px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.edit-modal-close {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
color: #667085;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.edit-modal-body {
|
||||
padding: 18px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.form-field input,
|
||||
.form-field select,
|
||||
.form-field textarea {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
border: 1px solid #dbe2ec;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
color: var(--ink);
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-field textarea {
|
||||
min-height: 86px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-field input:focus,
|
||||
.form-field select:focus,
|
||||
.form-field textarea:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(91, 92, 246, 0.12);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
display: none;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
color: #b42318;
|
||||
background: #fff1f3;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-error.is-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.edit-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 14px 20px 18px;
|
||||
border-top: 1px solid var(--line);
|
||||
background: #fbfcff;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
min-height: 40px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 0 14px;
|
||||
color: #475467;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.modal-btn.primary {
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
background: var(--primary);
|
||||
box-shadow: 0 8px 18px rgba(91, 92, 246, 0.24);
|
||||
}
|
||||
|
||||
.filter-box {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-height: 38px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 10px;
|
||||
color: #536179;
|
||||
background: #f8fafc;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-item input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stats-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--primary);
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 6px;
|
||||
color: #667085;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom {
|
||||
border: none !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
|
||||
.leaflet-top.leaflet-left {
|
||||
top: 108px;
|
||||
left: 320px;
|
||||
}
|
||||
|
||||
.leaflet-top.leaflet-right {
|
||||
top: 92px;
|
||||
right: 446px;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a,
|
||||
.leaflet-control-layers {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.navbar {
|
||||
grid-template-columns: minmax(260px, 1fr) auto;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
left: 336px;
|
||||
width: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.map-stage {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: 70vh auto auto auto;
|
||||
padding-top: 92px;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.navbar,
|
||||
.left-panel,
|
||||
#sidebar,
|
||||
.stats-card,
|
||||
#map {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
height: 58px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.navbar p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 70vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.leaflet-top.leaflet-left {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.leaflet-top.leaflet-right {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.left-panel,
|
||||
#sidebar,
|
||||
.stats-card {
|
||||
width: auto;
|
||||
margin: 12px;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="navbar">
|
||||
<div class="brand">
|
||||
<div class="brand-icon"><i class="fa-solid fa-map-location-dot"></i></div>
|
||||
<div>
|
||||
<h2>Admin WebGIS SPBU</h2>
|
||||
<p id="mapHint">Mode view aktif. Pindah ke edit jika ingin menambah atau menghapus data.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<a href="../index.html" class="action-chip" style="text-decoration: none; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; color: #344054; transition: all 0.2s; border-color: #d0d5dd;" onmouseover="this.style.backgroundColor='#f9fafb'; this.style.borderColor='#98a2b3'" onmouseout="this.style.backgroundColor='#fff'; this.style.borderColor='#d0d5dd'">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
|
||||
Portal Utama
|
||||
</a>
|
||||
<button class="action-chip status-text">Admin SPBU</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-stage">
|
||||
<div id="map"></div>
|
||||
|
||||
<aside class="left-panel">
|
||||
<div class="mode-switch">
|
||||
<button id="modeViewBtn" onclick="setMode('view')">◉ Lihat</button>
|
||||
<button id="modeEditBtn" onclick="setMode('edit')">✎ Edit</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<h3>Alat Peta</h3>
|
||||
<div class="type-list">
|
||||
<button class="panel-button" id="typeSpbuBtn" onclick="setDataType('spbu')"><span class="tool-icon"><i class="fa-solid fa-gas-pump"></i></span><span>Tambah SPBU</span></button>
|
||||
<button class="panel-button" id="typeKemiskinanBtn" onclick="setDataType('kemiskinan')"><span class="tool-icon"><i class="fa-solid fa-users"></i></span><span>Tambah Kemiskinan</span></button>
|
||||
<button class="panel-button" id="typeMasjidBtn" onclick="setDataType('masjid')"><span class="tool-icon"><i class="fa-solid fa-mosque"></i></span><span>Tambah Masjid</span></button>
|
||||
<button class="panel-button" id="typeJalanBtn" onclick="setDataType('jalan')"><span class="tool-icon"><i class="fa-solid fa-road"></i></span><span>Tambah Jalan</span></button>
|
||||
<button class="panel-button" id="typeKavlingBtn" onclick="setDataType('kavling')"><span class="tool-icon"><i class="fa-solid fa-vector-square"></i></span><span>Tambah Kavling</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="layerFilters"></div>
|
||||
|
||||
<div class="panel-section">
|
||||
<h3>Legenda Peta</h3>
|
||||
<div class="legend-list">
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#28a745"></span>SPBU 24 jam</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span>SPBU tidak 24 jam</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#ff8c00"></span>Kemiskinan</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#007bff"></span>Masjid dan radius</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#6f42c1"></span>Kavling</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="stats-card">
|
||||
<div class="stats-head">
|
||||
<h3 class="stats-title">Statistik Wilayah</h3>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div>
|
||||
<div class="stat-value" id="statSpbu">0</div>
|
||||
<div class="stat-label">Total SPBU</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value" id="statMasjid">0</div>
|
||||
<div class="stat-label">Masjid</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value" id="statJalan">0</div>
|
||||
<div class="stat-label">Jalan</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value" id="statKavling">0</div>
|
||||
<div class="stat-label">Kavling</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="sidebar"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop" id="editModalBackdrop" aria-hidden="true">
|
||||
<form class="edit-modal" id="editModalForm">
|
||||
<div class="edit-modal-header">
|
||||
<div>
|
||||
<h2 class="edit-modal-title" id="editModalTitle">Edit Data</h2>
|
||||
<p class="edit-modal-subtitle" id="editModalSubtitle">Perbarui atribut data terpilih.</p>
|
||||
</div>
|
||||
<button class="edit-modal-close" id="editModalClose" type="button">×</button>
|
||||
</div>
|
||||
<div class="edit-modal-body">
|
||||
<div class="form-error" id="editModalError"></div>
|
||||
<div class="form-grid" id="editModalFields"></div>
|
||||
</div>
|
||||
<div class="edit-modal-actions">
|
||||
<button class="modal-btn" id="editModalCancel" type="button">Batal</button>
|
||||
<button class="modal-btn primary" type="submit">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
<script src="js/map.js"></script>
|
||||
<script src="js/line.js"></script>
|
||||
<script src="js/polygon.js"></script>
|
||||
<script src="js/loadData.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
window.getRoadColor = function (status) {
|
||||
if (status === "Jalan Nasional") {
|
||||
return "#dc3545";
|
||||
}
|
||||
|
||||
if (status === "Jalan Provinsi") {
|
||||
return "#007bff";
|
||||
}
|
||||
|
||||
return "#28a745";
|
||||
};
|
||||
|
||||
window.normalizeRoadStatus = function (value) {
|
||||
var normalized = String(value || "").trim().toLowerCase();
|
||||
|
||||
if (normalized === "jalan nasional" || normalized === "nasional") {
|
||||
return "Jalan Nasional";
|
||||
}
|
||||
|
||||
if (normalized === "jalan provinsi" || normalized === "provinsi") {
|
||||
return "Jalan Provinsi";
|
||||
}
|
||||
|
||||
if (normalized === "jalan kabupaten" || normalized === "kabupaten") {
|
||||
return "Jalan Kabupaten";
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
window.serializeGeometry = function (latlngs) {
|
||||
return JSON.stringify(latlngs.map(function (point) {
|
||||
return [
|
||||
Number(point.lat.toFixed(6)),
|
||||
Number(point.lng.toFixed(6))
|
||||
];
|
||||
}));
|
||||
};
|
||||
|
||||
window.parseGeometry = function (geom) {
|
||||
try {
|
||||
var parsed = JSON.parse(geom);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
window.collectRoadData = async function () {
|
||||
return window.openEditModal({
|
||||
title: "Tambah Jalan",
|
||||
subtitle: "Lengkapi atribut jalan untuk garis yang sudah digambar.",
|
||||
fields: [
|
||||
{ name: "nama", label: "Nama Jalan", value: "", required: true },
|
||||
{
|
||||
name: "status",
|
||||
label: "Status Jalan",
|
||||
type: "select",
|
||||
value: "Jalan Nasional",
|
||||
options: [
|
||||
{ value: "Jalan Nasional", label: "Jalan Nasional" },
|
||||
{ value: "Jalan Provinsi", label: "Jalan Provinsi" },
|
||||
{ value: "Jalan Kabupaten", label: "Jalan Kabupaten" }
|
||||
]
|
||||
}
|
||||
],
|
||||
validate: function (data) {
|
||||
if (!data.nama || !window.normalizeRoadStatus(data.status)) {
|
||||
return "Nama dan status jalan wajib valid.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.buildRoadLayer = function (jalan) {
|
||||
var coordinates = window.parseGeometry(jalan.geom).map(function (point) {
|
||||
return L.latLng(point[0], point[1]);
|
||||
});
|
||||
|
||||
if (coordinates.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return L.polyline(coordinates, {
|
||||
color: window.getRoadColor(jalan.status),
|
||||
weight: 4
|
||||
}).bindPopup(
|
||||
"<b>" + window.escapeHtml(jalan.nama) + "</b><br>" +
|
||||
"Status: " + window.escapeHtml(jalan.status) + "<br>" +
|
||||
"Panjang: " + window.formatMeters(jalan.panjang)
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,567 @@
|
||||
window.appState = {
|
||||
mode: "view",
|
||||
activeType: "spbu",
|
||||
drawControl: null,
|
||||
layers: {},
|
||||
layerControl: null,
|
||||
layerVisibility: {
|
||||
spbu24: true,
|
||||
spbuTidak: true,
|
||||
kemiskinan: true,
|
||||
masjid: true,
|
||||
jalan: true,
|
||||
kavling: true
|
||||
}
|
||||
};
|
||||
|
||||
window.escapeHtml = function (value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
window.createPointIcon = function (color) {
|
||||
return L.divIcon({
|
||||
className: "custom-point-icon",
|
||||
html: '<span class="point-dot" style="background:' + color + ';"></span>',
|
||||
iconSize: [18, 18],
|
||||
iconAnchor: [9, 9],
|
||||
popupAnchor: [0, -12]
|
||||
});
|
||||
};
|
||||
|
||||
window.greenIcon = window.createPointIcon("#28a745");
|
||||
window.redIcon = window.createPointIcon("#dc3545");
|
||||
window.orangeIcon = window.createPointIcon("#ff8c00");
|
||||
window.blueIcon = window.createPointIcon("#007bff");
|
||||
|
||||
window.calculateLineLength = function (latlngs) {
|
||||
var total = 0;
|
||||
|
||||
for (var i = 1; i < latlngs.length; i += 1) {
|
||||
total += map.distance(latlngs[i - 1], latlngs[i]);
|
||||
}
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
window.calculatePolygonArea = function (latlngs) {
|
||||
if (L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === "function") {
|
||||
return L.GeometryUtil.geodesicArea(latlngs);
|
||||
}
|
||||
|
||||
if (!latlngs.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var earthRadius = 6378137;
|
||||
var avgLat = latlngs.reduce(function (sum, point) {
|
||||
return sum + point.lat;
|
||||
}, 0) / latlngs.length;
|
||||
var avgLatRad = avgLat * Math.PI / 180;
|
||||
var projected = latlngs.map(function (point) {
|
||||
return {
|
||||
x: earthRadius * point.lng * Math.PI / 180 * Math.cos(avgLatRad),
|
||||
y: earthRadius * point.lat * Math.PI / 180
|
||||
};
|
||||
});
|
||||
var area = 0;
|
||||
|
||||
for (var i = 0; i < projected.length; i += 1) {
|
||||
var next = projected[(i + 1) % projected.length];
|
||||
area += (projected[i].x * next.y) - (next.x * projected[i].y);
|
||||
}
|
||||
|
||||
return Math.abs(area / 2);
|
||||
};
|
||||
|
||||
window.formatMeters = function (value) {
|
||||
return Number(value).toLocaleString("id-ID", {
|
||||
maximumFractionDigits: 2
|
||||
}) + " m";
|
||||
};
|
||||
|
||||
window.formatSquareMeters = function (value) {
|
||||
return Number(value).toLocaleString("id-ID", {
|
||||
maximumFractionDigits: 2
|
||||
}) + " m²";
|
||||
};
|
||||
|
||||
window.postForm = async function (url, payload) {
|
||||
var response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: new URLSearchParams(payload)
|
||||
});
|
||||
var text = (await response.text()).trim();
|
||||
|
||||
if (!response.ok || text !== "success") {
|
||||
throw new Error(text || "Permintaan gagal.");
|
||||
}
|
||||
};
|
||||
|
||||
window.collectSpbuData = async function () {
|
||||
return window.openEditModal({
|
||||
title: "Tambah SPBU",
|
||||
subtitle: "Lengkapi atribut SPBU untuk titik yang dipilih di peta.",
|
||||
fields: [
|
||||
{ name: "nama", label: "Nama SPBU", value: "", required: true },
|
||||
{ name: "no", label: "Nomor SPBU", value: "", required: true },
|
||||
{
|
||||
name: "status",
|
||||
label: "Status Operasional",
|
||||
type: "select",
|
||||
value: "24jam",
|
||||
options: [
|
||||
{ value: "24jam", label: "24 Jam" },
|
||||
{ value: "tidak", label: "Tidak 24 Jam" }
|
||||
]
|
||||
}
|
||||
],
|
||||
validate: function (data) {
|
||||
if (!data.nama || !data.no) {
|
||||
return "Nama dan nomor SPBU wajib diisi.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.collectKemiskinanData = async function () {
|
||||
return window.openEditModal({
|
||||
title: "Tambah Data Kemiskinan",
|
||||
subtitle: "Lengkapi data keluarga untuk titik yang dipilih di peta.",
|
||||
fields: [
|
||||
{ name: "nama_kepala_keluarga", label: "Nama Kepala Keluarga", value: "", required: true },
|
||||
{ name: "jumlah_kk", label: "Jumlah KK", type: "number", min: 1, step: 1, value: 1, required: true },
|
||||
{ name: "alamat", label: "Alamat", type: "textarea", value: "", required: true }
|
||||
],
|
||||
validate: function (data) {
|
||||
if (!data.nama_kepala_keluarga || !data.alamat) {
|
||||
return "Nama kepala keluarga dan alamat wajib diisi.";
|
||||
}
|
||||
if (!Number.isInteger(data.jumlah_kk) || data.jumlah_kk < 1) {
|
||||
return "Jumlah KK harus angka minimal 1.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.collectMasjidData = async function () {
|
||||
return window.openEditModal({
|
||||
title: "Tambah Masjid",
|
||||
subtitle: "Lengkapi identitas masjid untuk titik yang dipilih di peta.",
|
||||
fields: [
|
||||
{ name: "nama_masjid", label: "Nama Masjid", value: "", required: true },
|
||||
{ name: "pic_masjid", label: "PIC Masjid", value: "", required: true },
|
||||
{ name: "alamat", label: "Alamat Masjid", type: "textarea", value: "", required: true },
|
||||
{ name: "radius", label: "Radius Jangkauan (meter)", type: "number", min: 100, max: 5000, step: 1, value: 500, required: true }
|
||||
],
|
||||
validate: function (data) {
|
||||
if (!data.nama_masjid || !data.pic_masjid || !data.alamat) {
|
||||
return "Nama, PIC, dan alamat masjid wajib diisi.";
|
||||
}
|
||||
if (!Number.isInteger(data.radius) || data.radius < 100 || data.radius > 5000) {
|
||||
return "Radius harus angka antara 100 sampai 5000 meter.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.updateMapHint = function () {
|
||||
var hint = document.getElementById("mapHint");
|
||||
if (!hint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.mode === "view") {
|
||||
hint.textContent = "Mode lihat aktif. Data hanya ditampilkan di peta dan sidebar.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.activeType === "spbu") {
|
||||
hint.textContent = "Mode edit SPBU aktif. Klik peta untuk menambahkan titik SPBU.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.activeType === "kemiskinan") {
|
||||
hint.textContent = "Mode edit Kemiskinan aktif. Klik peta untuk menambahkan titik data kemiskinan.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.activeType === "masjid") {
|
||||
hint.textContent = "Mode edit Masjid aktif. Klik peta untuk menambahkan titik masjid dengan radius awal 500 meter.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.activeType === "jalan") {
|
||||
hint.textContent = "Mode edit Jalan aktif. Gunakan tool draw polyline pada peta.";
|
||||
return;
|
||||
}
|
||||
|
||||
hint.textContent = "Mode edit Kavling aktif. Gunakan tool draw polygon pada peta.";
|
||||
};
|
||||
|
||||
window.updateToolbarState = function () {
|
||||
var buttonStates = {
|
||||
modeViewBtn: appState.mode === "view",
|
||||
modeEditBtn: appState.mode === "edit",
|
||||
typeSpbuBtn: appState.activeType === "spbu",
|
||||
typeKemiskinanBtn: appState.activeType === "kemiskinan",
|
||||
typeMasjidBtn: appState.activeType === "masjid",
|
||||
typeJalanBtn: appState.activeType === "jalan",
|
||||
typeKavlingBtn: appState.activeType === "kavling"
|
||||
};
|
||||
|
||||
Object.keys(buttonStates).forEach(function (id) {
|
||||
var button = document.getElementById(id);
|
||||
if (button) {
|
||||
button.classList.toggle("active", buttonStates[id]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.configureDrawControl = function () {
|
||||
if (!window.map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.drawControl) {
|
||||
map.removeControl(appState.drawControl);
|
||||
appState.drawControl = null;
|
||||
}
|
||||
|
||||
if (appState.mode !== "edit" || appState.activeType === "spbu" || appState.activeType === "kemiskinan" || appState.activeType === "masjid") {
|
||||
return;
|
||||
}
|
||||
|
||||
var drawOptions = {
|
||||
polyline: false,
|
||||
polygon: false,
|
||||
rectangle: false,
|
||||
circle: false,
|
||||
marker: false,
|
||||
circlemarker: false
|
||||
};
|
||||
|
||||
if (appState.activeType === "jalan") {
|
||||
drawOptions.polyline = {
|
||||
shapeOptions: {
|
||||
color: "#dc3545",
|
||||
weight: 4
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (appState.activeType === "kavling") {
|
||||
drawOptions.polygon = {
|
||||
allowIntersection: false,
|
||||
shapeOptions: {
|
||||
color: "#ff8c00",
|
||||
weight: 3,
|
||||
fillOpacity: 0.35
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
appState.drawControl = new L.Control.Draw({
|
||||
edit: false,
|
||||
draw: drawOptions
|
||||
});
|
||||
|
||||
map.addControl(appState.drawControl);
|
||||
};
|
||||
|
||||
window.setMode = function (mode) {
|
||||
appState.mode = mode;
|
||||
window.updateToolbarState();
|
||||
window.updateMapHint();
|
||||
window.configureDrawControl();
|
||||
|
||||
if (typeof window.loadAllData === "function") {
|
||||
window.loadAllData();
|
||||
}
|
||||
};
|
||||
|
||||
window.setDataType = function (type) {
|
||||
appState.activeType = type;
|
||||
window.updateToolbarState();
|
||||
window.updateMapHint();
|
||||
window.configureDrawControl();
|
||||
};
|
||||
|
||||
window.getLayerGroupByKey = function (key) {
|
||||
if (key === "spbu24") {
|
||||
return appState.layers.spbu24;
|
||||
}
|
||||
|
||||
if (key === "spbuTidak") {
|
||||
return appState.layers.spbuTidak;
|
||||
}
|
||||
|
||||
if (key === "kemiskinan") {
|
||||
return appState.layers.kemiskinan;
|
||||
}
|
||||
|
||||
if (key === "masjid") {
|
||||
return appState.layers.masjid;
|
||||
}
|
||||
|
||||
if (key === "jalan") {
|
||||
return appState.layers.jalan;
|
||||
}
|
||||
|
||||
if (key === "kavling") {
|
||||
return appState.layers.kavling;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
window.setLayerVisibility = function (key, isVisible) {
|
||||
appState.layerVisibility[key] = !!isVisible;
|
||||
|
||||
var layer = window.getLayerGroupByKey(key);
|
||||
if (!layer || !window.map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
if (!map.hasLayer(layer)) {
|
||||
map.addLayer(layer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (map.hasLayer(layer)) {
|
||||
map.removeLayer(layer);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
window.map = L.map("map").setView([-0.0263, 109.3425], 13);
|
||||
|
||||
appState.layers.spbu24 = L.layerGroup().addTo(map);
|
||||
appState.layers.spbuTidak = L.layerGroup().addTo(map);
|
||||
appState.layers.kemiskinan = L.layerGroup().addTo(map);
|
||||
appState.layers.masjid = L.layerGroup().addTo(map);
|
||||
appState.layers.jalan = L.layerGroup().addTo(map);
|
||||
appState.layers.kavling = L.layerGroup().addTo(map);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap"
|
||||
}).addTo(map);
|
||||
|
||||
appState.layerControl = L.control.layers(null, {
|
||||
"SPBU 24 Jam": appState.layers.spbu24,
|
||||
"SPBU Tidak 24 Jam": appState.layers.spbuTidak,
|
||||
"Kemiskinan": appState.layers.kemiskinan,
|
||||
"Masjid": appState.layers.masjid,
|
||||
"Jalan": appState.layers.jalan,
|
||||
"Kavling": appState.layers.kavling
|
||||
}).addTo(map);
|
||||
|
||||
map.on("overlayadd", function (event) {
|
||||
if (event.layer === appState.layers.spbu24) {
|
||||
appState.layerVisibility.spbu24 = true;
|
||||
} else if (event.layer === appState.layers.spbuTidak) {
|
||||
appState.layerVisibility.spbuTidak = true;
|
||||
} else if (event.layer === appState.layers.kemiskinan) {
|
||||
appState.layerVisibility.kemiskinan = true;
|
||||
} else if (event.layer === appState.layers.masjid) {
|
||||
appState.layerVisibility.masjid = true;
|
||||
} else if (event.layer === appState.layers.jalan) {
|
||||
appState.layerVisibility.jalan = true;
|
||||
} else if (event.layer === appState.layers.kavling) {
|
||||
appState.layerVisibility.kavling = true;
|
||||
}
|
||||
|
||||
if (typeof window.refreshLayerFilterUI === "function") {
|
||||
window.refreshLayerFilterUI();
|
||||
}
|
||||
});
|
||||
|
||||
map.on("overlayremove", function (event) {
|
||||
if (event.layer === appState.layers.spbu24) {
|
||||
appState.layerVisibility.spbu24 = false;
|
||||
} else if (event.layer === appState.layers.spbuTidak) {
|
||||
appState.layerVisibility.spbuTidak = false;
|
||||
} else if (event.layer === appState.layers.kemiskinan) {
|
||||
appState.layerVisibility.kemiskinan = false;
|
||||
} else if (event.layer === appState.layers.masjid) {
|
||||
appState.layerVisibility.masjid = false;
|
||||
} else if (event.layer === appState.layers.jalan) {
|
||||
appState.layerVisibility.jalan = false;
|
||||
} else if (event.layer === appState.layers.kavling) {
|
||||
appState.layerVisibility.kavling = false;
|
||||
}
|
||||
|
||||
if (typeof window.refreshLayerFilterUI === "function") {
|
||||
window.refreshLayerFilterUI();
|
||||
}
|
||||
});
|
||||
|
||||
map.on("click", async function (event) {
|
||||
if (appState.mode !== "edit") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.activeType === "spbu") {
|
||||
var spbuData = await window.collectSpbuData();
|
||||
if (!spbuData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.postForm("php/create.php", {
|
||||
nama: spbuData.nama,
|
||||
no: spbuData.no,
|
||||
status: spbuData.status,
|
||||
lat: event.latlng.lat.toFixed(6),
|
||||
lng: event.latlng.lng.toFixed(6)
|
||||
});
|
||||
|
||||
await window.loadAllData();
|
||||
alert("Data SPBU berhasil disimpan.");
|
||||
} catch (error) {
|
||||
alert("Gagal menyimpan SPBU: " + error.message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.activeType === "kemiskinan") {
|
||||
var kemiskinanData = await window.collectKemiskinanData();
|
||||
if (!kemiskinanData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.postForm("php/create_kemiskinan.php", {
|
||||
nama_kepala_keluarga: kemiskinanData.nama_kepala_keluarga,
|
||||
jumlah_kk: kemiskinanData.jumlah_kk,
|
||||
alamat: kemiskinanData.alamat,
|
||||
lat: event.latlng.lat.toFixed(6),
|
||||
lng: event.latlng.lng.toFixed(6)
|
||||
});
|
||||
|
||||
await window.loadAllData();
|
||||
alert("Data kemiskinan berhasil disimpan.");
|
||||
} catch (error) {
|
||||
alert("Gagal menyimpan data kemiskinan: " + error.message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState.activeType === "masjid") {
|
||||
var masjidData = await window.collectMasjidData();
|
||||
if (!masjidData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.postForm("php/create_masjid.php", {
|
||||
nama_masjid: masjidData.nama_masjid,
|
||||
pic_masjid: masjidData.pic_masjid,
|
||||
alamat: masjidData.alamat,
|
||||
radius: masjidData.radius,
|
||||
lat: event.latlng.lat.toFixed(6),
|
||||
lng: event.latlng.lng.toFixed(6)
|
||||
});
|
||||
|
||||
await window.loadAllData();
|
||||
alert("Data masjid berhasil disimpan.");
|
||||
} catch (error) {
|
||||
alert("Gagal menyimpan data masjid: " + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
map.on(L.Draw.Event.CREATED, async function (event) {
|
||||
var layer = event.layer;
|
||||
|
||||
if (appState.mode !== "edit") {
|
||||
alert("Pindah ke mode edit sebelum menggambar data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.layerType === "polyline") {
|
||||
if (appState.activeType !== "jalan") {
|
||||
alert("Tipe aktif saat ini bukan Jalan.");
|
||||
return;
|
||||
}
|
||||
|
||||
var lineLatLngs = layer.getLatLngs();
|
||||
var panjang = window.calculateLineLength(lineLatLngs);
|
||||
var roadData = await window.collectRoadData();
|
||||
|
||||
if (!roadData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.postForm("php/create_line.php", {
|
||||
nama: roadData.nama,
|
||||
status: roadData.status,
|
||||
panjang: panjang.toFixed(2),
|
||||
geom: window.serializeGeometry(lineLatLngs)
|
||||
});
|
||||
|
||||
await window.loadAllData();
|
||||
alert("Data jalan berhasil disimpan.");
|
||||
} catch (error) {
|
||||
alert("Gagal menyimpan jalan: " + error.message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.layerType === "polygon") {
|
||||
if (appState.activeType !== "kavling") {
|
||||
alert("Tipe aktif saat ini bukan Kavling.");
|
||||
return;
|
||||
}
|
||||
|
||||
var polygonLatLngs = layer.getLatLngs()[0] || [];
|
||||
var luas = window.calculatePolygonArea(polygonLatLngs);
|
||||
var landData = await window.collectPolygonData();
|
||||
|
||||
if (!landData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.postForm("php/create_polygon.php", {
|
||||
nama: landData.nama,
|
||||
status: landData.status,
|
||||
luas: luas.toFixed(2),
|
||||
geom: window.serializeGeometry(polygonLatLngs)
|
||||
});
|
||||
|
||||
await window.loadAllData();
|
||||
alert("Data kavling berhasil disimpan.");
|
||||
} catch (error) {
|
||||
alert("Gagal menyimpan kavling: " + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.setMode("view");
|
||||
window.setDataType("spbu");
|
||||
|
||||
setTimeout(function () {
|
||||
map.invalidateSize();
|
||||
}, 300);
|
||||
|
||||
if (typeof window.loadAllData === "function") {
|
||||
window.loadAllData();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
window.getPolygonColor = function (status) {
|
||||
if (status === "SHM") {
|
||||
return "#ff8c00";
|
||||
}
|
||||
|
||||
if (status === "HGB") {
|
||||
return "#6f42c1";
|
||||
}
|
||||
|
||||
if (status === "HGU") {
|
||||
return "#20c997";
|
||||
}
|
||||
|
||||
return "#6c757d";
|
||||
};
|
||||
|
||||
window.normalizePolygonStatus = function (value) {
|
||||
var normalized = String(value || "").trim().toUpperCase();
|
||||
var allowed = ["SHM", "HGB", "HGU", "HP"];
|
||||
|
||||
if (allowed.indexOf(normalized) === -1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
window.collectPolygonData = async function () {
|
||||
return window.openEditModal({
|
||||
title: "Tambah Kavling",
|
||||
subtitle: "Lengkapi atribut kavling untuk polygon yang sudah digambar.",
|
||||
fields: [
|
||||
{ name: "nama", label: "Nama Kavling", value: "", required: true },
|
||||
{
|
||||
name: "status",
|
||||
label: "Status Kavling",
|
||||
type: "select",
|
||||
value: "SHM",
|
||||
options: [
|
||||
{ value: "SHM", label: "SHM" },
|
||||
{ value: "HGB", label: "HGB" },
|
||||
{ value: "HGU", label: "HGU" },
|
||||
{ value: "HP", label: "HP" }
|
||||
]
|
||||
}
|
||||
],
|
||||
validate: function (data) {
|
||||
if (!data.nama || !window.normalizePolygonStatus(data.status)) {
|
||||
return "Nama dan status kavling wajib valid.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.buildPolygonLayer = function (kavling) {
|
||||
var coordinates = window.parseGeometry(kavling.geom).map(function (point) {
|
||||
return L.latLng(point[0], point[1]);
|
||||
});
|
||||
|
||||
if (coordinates.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var color = window.getPolygonColor(kavling.status);
|
||||
|
||||
return L.polygon(coordinates, {
|
||||
color: color,
|
||||
fillColor: color,
|
||||
fillOpacity: 0.35,
|
||||
weight: 3
|
||||
}).bindPopup(
|
||||
"<b>" + window.escapeHtml(kavling.nama) + "</b><br>" +
|
||||
"Status: " + window.escapeHtml(kavling.status) + "<br>" +
|
||||
"Luas: " + window.formatSquareMeters(kavling.luas)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$nomor = trim($_POST['no'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$latitudeRaw = $_POST['lat'] ?? null;
|
||||
$longitudeRaw = $_POST['lng'] ?? null;
|
||||
$latitude = is_numeric($latitudeRaw) ? (float) $latitudeRaw : null;
|
||||
$longitude = is_numeric($longitudeRaw) ? (float) $longitudeRaw : null;
|
||||
$allowedStatus = ['24jam', 'tidak'];
|
||||
|
||||
if ($nama === '' || $nomor === '' || !in_array($status, $allowedStatus, true) || $latitude === null || $longitude === null) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"INSERT INTO spbu (nama, no_spbu, status, latitude, longitude) VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "sssdd", $nama, $nomor, $status, $latitude, $longitude);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$namaKepalaKeluarga = trim($_POST['nama_kepala_keluarga'] ?? '');
|
||||
$jumlahKkRaw = $_POST['jumlah_kk'] ?? null;
|
||||
$jumlahKk = filter_var($jumlahKkRaw, FILTER_VALIDATE_INT);
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$latitudeRaw = $_POST['lat'] ?? null;
|
||||
$longitudeRaw = $_POST['lng'] ?? null;
|
||||
$latitude = is_numeric($latitudeRaw) ? (float) $latitudeRaw : null;
|
||||
$longitude = is_numeric($longitudeRaw) ? (float) $longitudeRaw : null;
|
||||
|
||||
if ($namaKepalaKeluarga === '' || $alamat === '' || $jumlahKk === false || $jumlahKk === null || $jumlahKk < 1 || $latitude === null || $longitude === null) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = null;
|
||||
|
||||
try {
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"INSERT INTO kemiskinan (nama_kepala_keluarga, jumlah_kk, alamat, latitude, longitude) VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "sisdd", $namaKepalaKeluarga, $jumlahKk, $alamat, $latitude, $longitude);
|
||||
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
if ($statement) {
|
||||
mysqli_stmt_close($statement);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$panjangRaw = $_POST['panjang'] ?? null;
|
||||
$panjang = is_numeric($panjangRaw) ? (float) $panjangRaw : null;
|
||||
$geom = trim($_POST['geom'] ?? '');
|
||||
$allowedStatus = ['Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten'];
|
||||
|
||||
if ($nama === '' || $geom === '' || !in_array($status, $allowedStatus, true) || $panjang === null) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"INSERT INTO jalan (nama, status, panjang, geom) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "ssds", $nama, $status, $panjang, $geom);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$namaMasjid = trim($_POST['nama_masjid'] ?? '');
|
||||
$picMasjid = trim($_POST['pic_masjid'] ?? '');
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$latitudeRaw = $_POST['lat'] ?? null;
|
||||
$longitudeRaw = $_POST['lng'] ?? null;
|
||||
$radiusRaw = $_POST['radius'] ?? 500;
|
||||
$latitude = is_numeric($latitudeRaw) ? (float) $latitudeRaw : null;
|
||||
$longitude = is_numeric($longitudeRaw) ? (float) $longitudeRaw : null;
|
||||
$radius = is_numeric($radiusRaw) ? (float) $radiusRaw : null;
|
||||
|
||||
if ($namaMasjid === '' || $picMasjid === '' || $alamat === '' || $latitude === null || $longitude === null || $radius === null || $radius < 100 || $radius > 5000) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = null;
|
||||
|
||||
try {
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"INSERT INTO masjid (nama_masjid, pic_masjid, alamat, latitude, longitude, radius) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "sssddd", $namaMasjid, $picMasjid, $alamat, $latitude, $longitude, $radius);
|
||||
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
if ($statement) {
|
||||
mysqli_stmt_close($statement);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$luasRaw = $_POST['luas'] ?? null;
|
||||
$luas = is_numeric($luasRaw) ? (float) $luasRaw : null;
|
||||
$geom = trim($_POST['geom'] ?? '');
|
||||
$allowedStatus = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
|
||||
if ($nama === '' || $geom === '' || !in_array($status, $allowedStatus, true) || $luas === null) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"INSERT INTO kavling (nama, status, luas, geom) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "ssds", $nama, $status, $luas, $geom);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
|
||||
if ($id === false || $id === null || $id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "DELETE FROM spbu WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "i", $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
|
||||
if ($id === false || $id === null || $id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "DELETE FROM kemiskinan WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "i", $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
|
||||
if ($id === false || $id === null || $id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "DELETE FROM jalan WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "i", $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
|
||||
if ($id === false || $id === null || $id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "DELETE FROM masjid WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "i", $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
|
||||
if ($id === false || $id === null || $id <= 0) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "DELETE FROM kavling WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "i", $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
$conn = mysqli_connect("127.0.0.1", "root", "", "webgis_spbu");
|
||||
|
||||
if (!$conn) {
|
||||
die("Koneksi gagal: " . mysqli_connect_error());
|
||||
}
|
||||
|
||||
mysqli_set_charset($conn, "utf8mb4");
|
||||
|
||||
$bootstrapQueries = [
|
||||
"CREATE TABLE IF NOT EXISTS kemiskinan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_kepala_keluarga VARCHAR(150) NOT NULL,
|
||||
jumlah_kk INT NOT NULL,
|
||||
alamat TEXT NOT NULL,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS masjid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_masjid VARCHAR(150) NOT NULL,
|
||||
pic_masjid VARCHAR(150) NOT NULL,
|
||||
alamat TEXT NOT NULL,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
radius DOUBLE NOT NULL DEFAULT 500,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)"
|
||||
];
|
||||
|
||||
foreach ($bootstrapQueries as $query) {
|
||||
mysqli_query($conn, $query);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$query = "SELECT * FROM spbu ORDER BY id DESC";
|
||||
$result = mysqli_query($conn, $query);
|
||||
$data = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$query = "SELECT * FROM kemiskinan ORDER BY id DESC";
|
||||
$result = mysqli_query($conn, $query);
|
||||
$data = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$query = "SELECT * FROM jalan ORDER BY id DESC";
|
||||
$result = mysqli_query($conn, $query);
|
||||
$data = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$query = "SELECT * FROM masjid ORDER BY id DESC";
|
||||
$result = mysqli_query($conn, $query);
|
||||
$data = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$query = "SELECT * FROM kavling ORDER BY id DESC";
|
||||
$result = mysqli_query($conn, $query);
|
||||
$data = [];
|
||||
|
||||
if ($result) {
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$nomor = trim($_POST['no'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$allowedStatus = ['24jam', 'tidak'];
|
||||
|
||||
if ($id === false || $id === null || $id <= 0 || $nama === '' || $nomor === '' || !in_array($status, $allowedStatus, true)) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"UPDATE spbu SET nama = ?, no_spbu = ?, status = ? WHERE id = ?"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "sssi", $nama, $nomor, $status, $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
$namaKepalaKeluarga = trim($_POST['nama_kepala_keluarga'] ?? '');
|
||||
$jumlahKkRaw = $_POST['jumlah_kk'] ?? null;
|
||||
$jumlahKk = filter_var($jumlahKkRaw, FILTER_VALIDATE_INT);
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
|
||||
if ($id === false || $id === null || $id <= 0 || $namaKepalaKeluarga === '' || $jumlahKk === false || $jumlahKk === null || $jumlahKk < 1 || $alamat === '') {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"UPDATE kemiskinan SET nama_kepala_keluarga = ?, jumlah_kk = ?, alamat = ? WHERE id = ?"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "sisi", $namaKepalaKeluarga, $jumlahKk, $alamat, $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$allowedStatus = ['Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten'];
|
||||
|
||||
if ($id === false || $id === null || $id <= 0 || $nama === '' || !in_array($status, $allowedStatus, true)) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"UPDATE jalan SET nama = ?, status = ? WHERE id = ?"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "ssi", $nama, $status, $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
$namaMasjid = trim($_POST['nama_masjid'] ?? '');
|
||||
$picMasjid = trim($_POST['pic_masjid'] ?? '');
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$radiusRaw = $_POST['radius'] ?? null;
|
||||
$radius = is_numeric($radiusRaw) ? (float) $radiusRaw : null;
|
||||
|
||||
if ($id === false || $id === null || $id <= 0 || $namaMasjid === '' || $picMasjid === '' || $alamat === '' || $radius === null || $radius < 100 || $radius > 5000) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"UPDATE masjid SET nama_masjid = ?, pic_masjid = ?, alamat = ?, radius = ? WHERE id = ?"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "sssdi", $namaMasjid, $picMasjid, $alamat, $radius, $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$radiusRaw = $_POST['radius'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
$radius = is_numeric($radiusRaw) ? (float) $radiusRaw : null;
|
||||
|
||||
if ($id === false || $id === null || $id <= 0 || $radius === null || $radius < 100 || $radius > 5000) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare($conn, "UPDATE masjid SET radius = ? WHERE id = ?");
|
||||
mysqli_stmt_bind_param($statement, "di", $radius, $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$idRaw = $_POST['id'] ?? null;
|
||||
$id = filter_var($idRaw, FILTER_VALIDATE_INT);
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
$allowedStatus = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
|
||||
if ($id === false || $id === null || $id <= 0 || $nama === '' || !in_array($status, $allowedStatus, true)) {
|
||||
http_response_code(400);
|
||||
exit('error');
|
||||
}
|
||||
|
||||
$statement = mysqli_prepare(
|
||||
$conn,
|
||||
"UPDATE kavling SET nama = ?, status = ? WHERE id = ?"
|
||||
);
|
||||
|
||||
mysqli_stmt_bind_param($statement, "ssi", $nama, $status, $id);
|
||||
|
||||
try {
|
||||
if (mysqli_stmt_execute($statement)) {
|
||||
echo 'success';
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo 'error';
|
||||
}
|
||||
} catch (mysqli_sql_exception $exception) {
|
||||
http_response_code(500);
|
||||
echo 'error: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
mysqli_stmt_close($statement);
|
||||
?>
|
||||
Reference in New Issue
Block a user