add files and folders

This commit is contained in:
2026-06-10 17:28:32 +07:00
parent 52f4cf86ab
commit 61f4fb2e71
80 changed files with 17551 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Portal WebGIS - UAS Project</title>
<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=Outfit:wght@300;400;600;800&family=Inter:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--primary: #4F46E5;
--primary-hover: #4338CA;
--secondary: #10B981;
--secondary-hover: #059669;
--bg-gradient-start: #0f172a;
--bg-gradient-end: #1e1b4b;
--text-main: #f8fafc;
--text-muted: #cbd5e1;
--card-bg: rgba(255, 255, 255, 0.05);
--card-border: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: hidden;
position: relative;
}
/* Ambient Background Orbs */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
z-index: 0;
animation: float 10s infinite ease-in-out alternate;
}
.orb-1 {
width: 400px;
height: 400px;
background: rgba(79, 70, 229, 0.3);
top: -100px;
left: -100px;
}
.orb-2 {
width: 300px;
height: 300px;
background: rgba(16, 185, 129, 0.2);
bottom: -50px;
right: -50px;
animation-delay: -5s;
}
@keyframes float {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(30px, 50px) scale(1.1); }
}
header {
z-index: 10;
text-align: center;
margin-bottom: 4rem;
animation: slideDown 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
h1 {
font-family: 'Outfit', sans-serif;
font-size: 3.5rem;
font-weight: 800;
margin-bottom: 0.5rem;
background: linear-gradient(to right, #818cf8, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -1px;
}
header p {
font-size: 1.2rem;
color: var(--text-muted);
font-weight: 300;
}
.projects-container {
display: flex;
gap: 2.5rem;
z-index: 10;
flex-wrap: wrap;
justify-content: center;
width: 100%;
max-width: 1000px;
padding: 0 2rem;
}
/* Glassmorphism Cards */
.card {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 24px;
padding: 2.5rem;
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
text-decoration: none;
color: inherit;
position: relative;
overflow: hidden;
animation: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.card:nth-child(2) {
animation-delay: 0.2s;
}
.card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.05), transparent);
transform: translateX(-100%);
transition: 0.6s;
}
.card:hover {
transform: translateY(-10px) scale(1.02);
border-color: rgba(255, 255, 255, 0.3);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
}
.card:hover::before {
transform: translateX(100%);
}
.card-icon {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
font-size: 2rem;
}
.icon-public {
background: rgba(16, 185, 129, 0.15);
color: #34d399;
box-shadow: 0 0 20px rgba(16, 185, 129, 0.2);
}
.icon-private {
background: rgba(79, 70, 229, 0.15);
color: #818cf8;
box-shadow: 0 0 20px rgba(79, 70, 229, 0.2);
}
.card-title {
font-family: 'Outfit', sans-serif;
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 1rem;
}
.card-desc {
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 2rem;
flex-grow: 1;
}
.card-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 99px;
font-size: 0.8rem;
font-weight: 500;
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.badge-public {
background: rgba(16, 185, 129, 0.2);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.badge-private {
background: rgba(79, 70, 229, 0.2);
color: #818cf8;
border: 1px solid rgba(79, 70, 229, 0.3);
}
.card-footer {
display: flex;
align-items: center;
font-weight: 500;
color: var(--text-main);
}
.card-footer svg {
width: 20px;
height: 20px;
margin-left: 0.5rem;
transition: transform 0.3s ease;
}
.card:hover .card-footer svg {
transform: translateX(5px);
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
h1 { font-size: 2.5rem; }
.projects-container { flex-direction: column; align-items: center; }
}
</style>
</head>
<body>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<header>
<h1>WebGIS Project UAS</h1>
<p>Silakan pilih proyek yang ingin Anda akses</p>
</header>
<div class="projects-container">
<!-- Card Project 1: Tanpa Login (SPBU) -->
<a href="./webgis-spbu/index.html" class="card">
<div class="card-badge badge-public">Akses Publik</div>
<div class="card-icon icon-public">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
</div>
<h2 class="card-title">Project 1: WebGIS SPBU</h2>
<p class="card-desc">Jelajahi peta interaktif untuk SPBU, Jalan, Parsil, dan lainnya secara langsung <strong>tanpa perlu login</strong>.</p>
<div class="card-footer">
Buka Peta Publik
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</div>
</a>
<!-- Card Project 2: Dengan Login (Kemiskinan) -->
<a href="./webgis-kemiskinan/admin/login.php" class="card">
<div class="card-badge badge-private">Akses Login</div>
<div class="card-icon icon-private">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
</div>
<h2 class="card-title">Project 2: WebGIS Kemiskinan</h2>
<p class="card-desc">Sistem manajemen dan analisis pengentasan kemiskinan berbasis masyarakat. <strong>Memerlukan login</strong> untuk akses.</p>
<div class="card-footer">
Login ke Sistem
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
</div>
</a>
</div>
<div style="margin-top: 3rem; animation: fadeInUp 1s both; z-index: 10;">
<a href="manual.html" style="color: var(--text-muted); text-decoration: none; display: flex; align-items: center; gap: 8px; font-weight: 500; padding: 10px 20px; border-radius: 99px; background: var(--card-bg); border: 1px solid var(--card-border); backdrop-filter: blur(10px); transition: all 0.3s;" onmouseover="this.style.color='#fff'; this.style.borderColor='rgba(255,255,255,0.3)';" onmouseout="this.style.color='var(--text-muted)'; this.style.borderColor='var(--card-border)';">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
Baca Manual Book
</a>
</div>
</body>
</html>
+199
View File
@@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manual Book - WebGIS Project UAS</title>
<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=Outfit:wght@300;400;600;800&family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #4F46E5;
--secondary: #10B981;
--bg-gradient-start: #0f172a;
--bg-gradient-end: #1e1b4b;
--text-main: #f8fafc;
--text-muted: #cbd5e1;
--card-bg: rgba(255, 255, 255, 0.05);
--card-border: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
color: var(--text-main);
min-height: 100vh;
overflow-x: hidden;
position: relative;
}
/* Ambient Background Orbs */
.orb {
position: fixed;
border-radius: 50%;
filter: blur(80px);
z-index: 0;
animation: float 10s infinite ease-in-out alternate;
}
.orb-1 { width: 400px; height: 400px; background: rgba(79, 70, 229, 0.2); top: -100px; left: -100px; }
.orb-2 { width: 300px; height: 300px; background: rgba(16, 185, 129, 0.15); bottom: -50px; right: -50px; animation-delay: -5s; }
@keyframes float {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(30px, 50px) scale(1.1); }
}
.container {
position: relative;
z-index: 10;
max-width: 900px;
margin: 0 auto;
padding: 40px 20px;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--text-muted);
text-decoration: none;
font-weight: 500;
margin-bottom: 30px;
transition: color 0.2s;
}
.back-btn:hover { color: #fff; }
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
font-family: 'Outfit', sans-serif;
font-size: 3rem;
font-weight: 800;
margin-bottom: 10px;
background: linear-gradient(to right, #818cf8, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -1px;
}
.content-box {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 24px;
padding: 40px;
line-height: 1.7;
}
h2 { font-family: 'Outfit', sans-serif; font-size: 1.8rem; margin-top: 30px; margin-bottom: 15px; color: #fff; border-bottom: 1px solid var(--card-border); padding-bottom: 10px; }
h3 { font-family: 'Outfit', sans-serif; font-size: 1.3rem; margin-top: 25px; margin-bottom: 10px; color: #e2e8f0; }
p { margin-bottom: 15px; color: var(--text-muted); }
ul, ol { margin-left: 20px; margin-bottom: 15px; color: var(--text-muted); }
li { margin-bottom: 8px; }
strong { color: #f8fafc; }
.alert {
padding: 15px 20px;
border-radius: 12px;
margin: 20px 0;
font-weight: 500;
border-left: 4px solid;
}
.alert-tip { background: rgba(16, 185, 129, 0.1); border-color: #34d399; color: #a7f3d0; }
.alert-important { background: rgba(79, 70, 229, 0.1); border-color: #818cf8; color: #c7d2fe; }
.alert-caution { background: rgba(239, 68, 68, 0.1); border-color: #f87171; color: #fecaca; }
@media (max-width: 768px) {
h1 { font-size: 2.2rem; }
.content-box { padding: 25px; }
}
</style>
</head>
<body>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="container">
<a href="index.html" class="back-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
Kembali ke Landing Page
</a>
<header>
<h1>Manual Book</h1>
<p style="font-size: 1.2rem;">Panduan Lengkap Portal WebGIS Project UAS</p>
</header>
<div class="content-box">
<p>Selamat datang di Panduan Penggunaan Sistem Portal WebGIS. Sistem ini terdiri dari dua proyek utama yang terintegrasi dalam satu landing page. Panduan ini dirancang untuk memudahkan Anda memahami dan mengoperasikan seluruh fitur yang tersedia sesuai dengan hak akses (role) Anda.</p>
<h2>1. Navigasi Awal (Landing Page)</h2>
<p>Saat pertama kali mengakses sistem, Anda akan disambut oleh halaman Portal Utama.</p>
<ul>
<li><strong>Kartu Pertama (WebGIS SPBU):</strong> Digunakan untuk mengakses peta fasilitas umum secara publik (tanpa login).</li>
<li><strong>Kartu Kedua (WebGIS Kemiskinan):</strong> Digunakan untuk mengakses sistem manajemen kemiskinan. Anda akan diarahkan ke halaman login karena sistem ini berbasis otorisasi.</li>
</ul>
<div class="alert alert-tip">
<strong>💡 Tip:</strong> Jika Anda sedang berada di dalam salah satu peta, Anda selalu bisa menekan tombol "Kembali ke Landing Page" (di sidebar kiri atau atas) untuk kembali ke menu awal.
</div>
<h2>2. Project 1: WebGIS SPBU (Akses Publik)</h2>
<p>Proyek ini dirancang sebagai sistem informasi geografis interaktif yang bersifat terbuka untuk masyarakat luas.</p>
<h3>A. Melihat dan Mencari Lokasi</h3>
<ul>
<li><strong>Peta Interaktif:</strong> Anda dapat menggeser (drag) dan memperbesar (scroll/zoom) peta.</li>
<li><strong>Filter & Layer:</strong> Di panel sebelah kiri, terdapat checkbox untuk menyalakan atau mematikan tampilan layer tertentu (contoh: menyembunyikan Jalan atau Rumah Ibadah).</li>
<li><strong>Filter Cepat:</strong> Gunakan tombol di Toolbar peta untuk langsung menyorot "SPBU 24 Jam".</li>
</ul>
<h3>B. Menambah Data Spasial (Simulasi)</h3>
<p>Sistem ini menyediakan alat untuk menggambar objek langsung di peta:</p>
<ul>
<li><strong>Point:</strong> Klik tombol Tambah Point, lalu klik area pada peta.</li>
<li><strong>Jalan:</strong> Klik Tambah Jalan, klik titik-titik untuk membuat jalur, klik ganda untuk selesai.</li>
<li><strong>Parsil:</strong> Buat area poligon dan satukan kembali ke titik awal.</li>
</ul>
<h2>3. Project 2: WebGIS Kemiskinan (Berbasis Otorisasi)</h2>
<p>Proyek ini adalah sistem tata kelola data kemiskinan yang terstruktur secara ketat berdasarkan Role-Based Access Control (RBAC).</p>
<div class="alert alert-important">
<strong>⚠️ Penting:</strong> Kebijakan Akses Peta sangat ketat. Peta ini tidak bisa diakses secara anonim. Jika Anda memaksa masuk melalui URL, Anda akan otomatis dialihkan ke halaman Login.
</div>
<h3>A. Hak Akses (Role)</h3>
<ul>
<li><strong>Admin (Administrator):</strong> Akses absolut. Bisa mengelola semua penduduk, semua rumah ibadah, user, dan histori log.</li>
<li><strong>Pengurus Masjid/Gereja:</strong> Dibatasi hanya mengelola warga dan bantuan di dalam <em>radius wilayah rumah ibadahnya</em>.</li>
<li><strong>Pimpinan Daerah:</strong> Akses <em>Read-Only</em>. Dirancang untuk Bupati/Camat yang perlu melihat statistik dan Heatmap secara luas tanpa bisa mengubah data.</li>
<li><strong>Warga Masyarakat:</strong> Dapat digunakan warga untuk sekadar login dan memantau transparansi (jika fitur diekspos).</li>
</ul>
<h3>B. Fitur Peta Kemiskinan</h3>
<ul>
<li><strong>Layer Ekonomi:</strong> Titik berwarna Hijau (Mampu), Kuning (Rentan), dan Merah (Miskin).</li>
<li><strong>Lapor Darurat 🚨:</strong> Tombol segitiga peringatan di atas peta digunakan warga untuk melaporkan tetangga yang kritis. Form bisa diisi anonim.</li>
<li><strong>Blind Spot:</strong> Di tab Statistik, pimpinan dapat menekan "Cari Titik Blind Spot" untuk menemukan warga miskin yang belum pernah mendapat bantuan apa pun.</li>
</ul>
<div class="alert alert-caution">
<strong>⚡ Peringatan Keamanan:</strong> Segala aktivitas seperti Edit, Hapus, dan Verifikasi akan direkam secara permanen ke dalam tabel <em>Log Aktivitas</em> bersama dengan Alamat IP dan waktu (Timestamp) Anda. Harap berhati-hati saat memanipulasi data!
</div>
</div>
</div>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
FROM php:8.2-apache
RUN docker-php-ext-install mysqli pdo pdo_mysql \
&& a2enmod rewrite
COPY . /var/www/html/
RUN mkdir -p /var/www/html/uploads/bukti \
&& mkdir -p /var/www/html/uploads/foto_laporan \
&& mkdir -p /var/www/html/uploads/foto_profil \
&& chown -R www-data:www-data /var/www/html/uploads \
&& chown -R www-data:www-data /var/www/html
+185
View File
@@ -0,0 +1,185 @@
# WebGIS Pengentasan Kemiskinan v2.0
### Berbasis Masyarakat & Rumah Ibadah
> Pengembangan dari sistem webgis-spbu menjadi sistem manajemen sosial yang lengkap, real-time, dan berbasis relasi database.
---
## 🚀 Fitur Lengkap
| Fitur | Status |
|---|---|
| Peta Leaflet interaktif + MarkerCluster | ✅ |
| Layer: penduduk, keluarga, rumah ibadah, laporan, jalan | ✅ |
| Warna marker berdasar status ekonomi (merah/kuning/hijau) | ✅ |
| CRUD penduduk lengkap (NIK, umur otomatis, kesehatan, dll) | ✅ |
| Sistem keluarga (KK → anggota) | ✅ |
| Validasi logika pendidikan vs umur | ✅ |
| Laporan masyarakat (siapapun bisa lapor, tanpa login) | ✅ |
| Manajemen bantuan + histori per penduduk | ✅ |
| Deteksi otomatis warga belum pernah dibantu | ✅ |
| Prioritas bantuan (KRITIS = miskin + sakit parah) | ✅ |
| Multi-role: admin, pengurus, pimpinan, masyarakat | ✅ |
| Chat/diskusi antar pengurus per rumah ibadah | ✅ |
| Notifikasi real-time (polling) | ✅ |
| Audit log semua aktivitas | ✅ |
| Dashboard monitoring statistik | ✅ |
| Data dummy siap pakai (Pontianak) | ✅ |
---
## 📁 Struktur Direktori
```
webgis-kemiskinan/
├── index.html ← Peta WebGIS utama (publik)
├── admin/
│ ├── login.php ← Halaman login
│ ├── logout.php
│ ├── init_admin.php ← Inisialisasi user (jalankan sekali!)
│ ├── _layout.php ← Layout admin (header + nav)
│ ├── index.php ← Dashboard
│ ├── penduduk.php ← CRUD penduduk
│ ├── bantuan.php ← Catat penyaluran bantuan
│ ├── histori_bantuan.php ← Riwayat bantuan
│ ├── belum_dibantu.php ← Warga belum tersentuh bantuan
│ ├── laporan.php ← Manajemen laporan masyarakat
│ └── log.php ← Audit log (admin only)
├── php/
│ ├── koneksi.php ← Config DB + helper functions
│ ├── middleware/
│ │ └── auth.php ← Autentikasi & otorisasi
│ └── api/
│ ├── auth.php ← Login/logout API
│ ├── penduduk.php ← CRUD penduduk (RESTful)
│ ├── keluarga.php ← CRUD keluarga
│ ├── bantuan.php ← Bantuan + histori + stats
│ ├── laporan.php ← Laporan masyarakat
│ ├── rumah_ibadah.php ← CRUD rumah ibadah
│ └── pesan.php ← Chat + notifikasi
├── sql/
│ └── schema.sql ← Schema + data dummy
└── uploads/
├── bukti/ ← Bukti penyaluran bantuan
└── foto_laporan/ ← Foto dari laporan masyarakat
```
---
## ⚙️ Instalasi (XAMPP)
### 1. Salin Folder
```
C:\xampp\htdocs\webgis-kemiskinan\
```
### 2. Buat Database
- Buka **phpMyAdmin**`http://localhost/phpmyadmin`
- Buat database baru: `webgis_kemiskinan`
- Import: `sql/schema.sql`
### 3. Konfigurasi DB
Edit `php/koneksi.php`:
```php
define('DB_NAME', 'webgis_kemiskinan');
define('DB_USER', 'root');
define('DB_PASS', ''); // sesuaikan password MySQL Anda
```
### 4. Inisialisasi User
Buka sekali di browser:
```
http://localhost/webgis-kemiskinan/admin/init_admin.php
```
> **Hapus file ini setelah dijalankan!**
### 5. Akses Sistem
| URL | Keterangan |
|---|---|
| `http://localhost/webgis-kemiskinan/` | Peta WebGIS (publik) |
| `http://localhost/webgis-kemiskinan/admin/login.php` | Panel Admin |
---
## 👤 Akun Demo
| Username | Password | Role |
|---|---|---|
| `admin` | `password` | Administrator |
| `pengurus1` | `password` | Pengurus Masjid Al-Falah |
| `pimpinan1` | `password` | Pimpinan Daerah |
| `warga1` | `password` | Masyarakat |
> Hash yang digunakan adalah hash Laravel default untuk string `"password"`.
> Untuk produksi, ganti dengan: `password_hash('PasswordBaru', PASSWORD_BCRYPT)`
---
## 🗄️ Tabel Database
| Tabel | Fungsi |
|---|---|
| `roles` | Definisi role sistem |
| `users` | Akun pengguna |
| `rumah_ibadah` | Data rumah ibadah + radius |
| `keluarga` | Data KK |
| `penduduk` | Data individu (NIK sebagai PK) |
| `riwayat_pelatihan` | Pelatihan kerja per penduduk |
| `jenis_bantuan` | Master jenis bantuan |
| `histori_bantuan` | Riwayat penyaluran bantuan |
| `laporan` | Laporan kondisi dari masyarakat |
| `pesan` | Chat antar pengurus |
| `notifikasi` | Notifikasi per user |
| `log_aktivitas` | Audit trail semua perubahan data |
| `jalan_lines` | Layer jalan (GeoJSON) |
| `parsil_tanah` | Layer parsil tanah (GeoJSON) |
---
## 🧠 Logika Khusus
### Validasi Pendidikan vs Umur
- Umur 712 tahun → wajib masih sekolah (SD)
- Umur 1315 tahun → wajib masih sekolah (SMP)
- Umur 1618 tahun → wajib masih sekolah (SMA/SMK)
- Umur >18 tahun → bebas (bisa tidak sekolah / ikut pelatihan)
### Prioritas Bantuan (otomatis)
| Kondisi | Prioritas |
|---|---|
| Miskin + Sakit Parah/Disabilitas | 🔴 KRITIS |
| Miskin + Belum pernah dibantu | 🟠 TINGGI |
| Miskin (umum) | 🟡 SEDANG |
| Rentan + Sakit | 🟡 SEDANG |
| Lainnya | 🟢 RENDAH |
### Umur Otomatis
Kolom `umur` di tabel `penduduk` adalah **GENERATED COLUMN** — dihitung langsung dari `tanggal_lahir` setiap kali data dibaca. Tidak perlu update manual.
---
## 🔒 Keamanan
- PDO dengan prepared statements (anti SQL injection)
- `password_hash()` / `password_verify()` untuk password
- Session dengan `session_regenerate_id()` saat login
- Validasi input di sisi server untuk semua endpoint
- Otorisasi per-role di setiap API endpoint
- Upload file: validasi ekstensi + ukuran + nama acak
---
## 📱 Cara Buat Laporan (Masyarakat)
1. Buka `http://localhost/webgis-kemiskinan/`
2. Klik tombol **"Buat Laporan"** di pojok kanan atas
3. Isi deskripsi kondisi (nama pelapor opsional)
4. Klik peta untuk memilih lokasi otomatis
5. Kirim → pengurus akan mendapat notifikasi otomatis
---
## 🔄 Pengembangan Lanjutan (Saran)
- [ ] Integrasi SMS/WhatsApp notifikasi
- [ ] Export PDF laporan distribusi bantuan
- [ ] Login via Google / warga menggunakan NIK + tanggal lahir
- [ ] Mobile app (React Native / Flutter)
- [ ] Sinkronisasi data dengan Dukcapil
+121
View File
@@ -0,0 +1,121 @@
<?php
require_once __DIR__ . '/../php/koneksi.php';
require_once __DIR__ . '/../php/middleware/auth.php';
function render_header(string $title): void {
$user = $_SESSION['user_nama'] ?? 'Tamu';
$role = $_SESSION['role'] ?? '';
$unread = 0;
global $pdo;
if (!empty($_SESSION['user_id'])) {
try {
$stmtN = $pdo->prepare("SELECT COUNT(*) FROM notifikasi WHERE user_id=:u AND dibaca=0");
$stmtN->execute([':u' => $_SESSION['user_id']]);
$unread = (int)$stmtN->fetchColumn();
} catch(Exception $e){}
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= h($title) ?> — WebGIS Kemiskinan</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f172a;--sur:#1e293b;--sur2:#334155;--acc:#3b82f6;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--txt:#f1f5f9;--txt2:#94a3b8;--brd:#334155;--rad:12px}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--txt);min-height:100vh}
#topbar{background:var(--sur);border-bottom:1px solid var(--brd);padding:0 20px;height:54px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:100}
#topbar .logo{font-weight:900;font-size:15px} #topbar .logo span{color:var(--acc)}
#topbar .spacer{flex:1}
#topbar .user{font-size:12px;color:var(--txt2);font-weight:700}
#topbar a.tbtn,#topbar button.tbtn{background:var(--sur2);border:1px solid var(--brd);color:var(--txt);padding:6px 12px;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;cursor:pointer;transition:.2s;display:inline-flex;align-items:center;gap:5px}
#topbar a.tbtn:hover,#topbar button.tbtn:hover{background:var(--acc);border-color:var(--acc);color:#fff}
#wrap{display:flex;min-height:calc(100vh - 54px)}
#nav{width:220px;background:var(--sur);border-right:1px solid var(--brd);padding:16px 0;flex-shrink:0}
#nav a{display:flex;align-items:center;gap:10px;padding:10px 20px;color:var(--txt2);text-decoration:none;font-size:13px;font-weight:700;transition:.2s;border-left:3px solid transparent}
#nav a:hover,#nav a.active{color:var(--txt);background:rgba(59,130,246,.1);border-left-color:var(--acc)}
#nav .nav-section{padding:12px 20px 4px;font-size:10px;font-weight:900;color:var(--brd);text-transform:uppercase;letter-spacing:.5px}
#content{flex:1;padding:24px;overflow-x:auto}
.page-title{font-size:20px;font-weight:900;margin-bottom:20px;display:flex;align-items:center;gap:10px}
.card{background:var(--sur);border:1px solid var(--brd);border-radius:var(--rad);padding:20px;margin-bottom:16px}
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px}
.stat{background:var(--sur);border:1px solid var(--brd);border-radius:var(--rad);padding:16px;text-align:center}
.stat .val{font-size:28px;font-weight:900} .stat .lbl{font-size:11px;color:var(--txt2);font-weight:700;margin-top:2px}
.stat.red .val{color:var(--red)} .stat.yellow .val{color:var(--yellow)} .stat.green .val{color:var(--green)} .stat.blue .val{color:var(--acc)}
table{width:100%;border-collapse:collapse;font-size:13px}
th{background:var(--sur2);padding:10px 12px;text-align:left;font-size:11px;font-weight:900;color:var(--txt2);text-transform:uppercase}
td{padding:10px 12px;border-bottom:1px solid var(--brd)}
tr:hover td{background:rgba(255,255,255,.02)}
.tag{display:inline-block;padding:2px 8px;border-radius:99px;font-size:11px;font-weight:800}
.tag.miskin{background:rgba(239,68,68,.15);color:var(--red)}
.tag.rentan{background:rgba(245,158,11,.15);color:var(--yellow)}
.tag.mampu{background:rgba(34,197,94,.15);color:var(--green)}
.tag.pending{background:rgba(245,158,11,.15);color:var(--yellow)}
.tag.diproses{background:rgba(59,130,246,.15);color:var(--acc)}
.tag.selesai{background:rgba(34,197,94,.15);color:var(--green)}
.tag.darurat{background:rgba(239,68,68,.2);color:var(--red)}
.btn{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-weight:700;font-size:12px;display:inline-flex;align-items:center;gap:5px;transition:.2s;text-decoration:none}
.btn-primary{background:var(--acc);color:#fff} .btn-primary:hover{background:#2563eb}
.btn-success{background:var(--green);color:#fff}
.btn-danger{background:var(--red);color:#fff}
.btn-secondary{background:var(--sur2);color:var(--txt);border:1px solid var(--brd)}
.btn-sm{padding:4px 10px;font-size:11px}
input,select,textarea{background:var(--sur2);border:1px solid var(--brd);color:var(--txt);padding:8px 12px;border-radius:8px;font-size:13px;width:100%}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--acc)}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.form-group{display:flex;flex-direction:column;gap:5px}
.form-group.full{grid-column:1/-1}
label{font-size:11px;font-weight:700;color:var(--txt2);text-transform:uppercase}
.notice{padding:10px 14px;border-radius:8px;border:1px solid;font-size:13px;margin-bottom:12px}
.actions{display:flex;gap:6px;flex-wrap:wrap}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
@media(max-width:768px){#nav{display:none}#content{padding:16px}.form-grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<div id="topbar">
<div class="logo">🗺️ <span>WebGIS</span> Kemiskinan</div>
<div class="spacer"></div>
<span class="user">👤 <?= h($user) ?> <span style="color:var(--acc)">(<?= h($role) ?>)</span></span>
<a class="tbtn" href="../index.html"><i class="fa-solid fa-map"></i> Buka Peta</a>
<a class="tbtn" href="logout.php"><i class="fa-solid fa-right-from-bracket"></i> Logout</a>
</div>
<div id="wrap">
<div id="nav">
<div class="nav-section">Menu Utama</div>
<a href="index.php" <?= basename($_SERVER['PHP_SELF'])==='index.php'?'class="active"':'' ?>><i class="fa-solid fa-gauge" style="width:16px"></i> Dashboard</a>
<?php if (in_array($role, ['admin','pengurus'])): ?>
<div class="nav-section">Data</div>
<a href="penduduk.php" <?= basename($_SERVER['PHP_SELF'])==='penduduk.php'?'class="active"':'' ?>><i class="fa-solid fa-users" style="width:16px"></i> Penduduk</a>
<a href="keluarga.php" <?= basename($_SERVER['PHP_SELF'])==='keluarga.php'?'class="active"':'' ?>><i class="fa-solid fa-house-chimney-user" style="width:16px"></i> Keluarga (KK)</a>
<a href="rumah_ibadah.php" <?= basename($_SERVER['PHP_SELF'])==='rumah_ibadah.php'?'class="active"':'' ?>><i class="fa-solid fa-mosque" style="width:16px"></i> Rumah Ibadah</a>
<div class="nav-section">Bantuan</div>
<a href="bantuan.php" <?= basename($_SERVER['PHP_SELF'])==='bantuan.php'?'class="active"':'' ?>><i class="fa-solid fa-gift" style="width:16px"></i> Catat Bantuan</a>
<a href="histori_bantuan.php" <?= basename($_SERVER['PHP_SELF'])==='histori_bantuan.php'?'class="active"':'' ?>><i class="fa-solid fa-clock-rotate-left" style="width:16px"></i> Histori Bantuan</a>
<a href="belum_dibantu.php" <?= basename($_SERVER['PHP_SELF'])==='belum_dibantu.php'?'class="active"':'' ?>><i class="fa-solid fa-triangle-exclamation" style="width:16px;color:var(--red)"></i> Belum Dibantu</a>
<div class="nav-section">Laporan</div>
<a href="laporan.php" <?= basename($_SERVER['PHP_SELF'])==='laporan.php'?'class="active"':'' ?>><i class="fa-solid fa-flag" style="width:16px"></i> Laporan Masuk</a>
<?php endif; ?>
<?php if ($role === 'admin'): ?>
<div class="nav-section">Sistem</div>
<a href="users.php" <?= basename($_SERVER['PHP_SELF'])==='users.php'?'class="active"':'' ?>><i class="fa-solid fa-user-shield" style="width:16px"></i> Manajemen User</a>
<a href="log.php" <?= basename($_SERVER['PHP_SELF'])==='log.php'?'class="active"':'' ?>><i class="fa-solid fa-scroll" style="width:16px"></i> Log Aktivitas</a>
<?php endif; ?>
</div>
<div id="content">
<?php
}
function render_footer(): void {
echo '</div></div></body></html>';
}
function require_login(array $roles = []): void {
requireLogin($roles);
}
+155
View File
@@ -0,0 +1,155 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin','pengurus']);
$msg = ''; $err = '';
$nikPre = $_GET['nik'] ?? '';
$namaPre = $_GET['nama'] ?? '';
if (isset($_POST['save'])) {
$nik = trim($_POST['nik'] ?? '');
$idJb = (int)($_POST['id_jenis_bantuan'] ?? 0);
$tgl = $_POST['tanggal'] ?? date('Y-m-d');
if (!preg_match('/^\d{16}$/', $nik)) $err = 'NIK tidak valid.';
elseif (!$idJb) $err = 'Jenis bantuan wajib dipilih.';
else {
try {
// Cek penduduk ada
$pddk = $pdo->prepare("SELECT nik,id_keluarga FROM penduduk WHERE nik=:nik AND status_hidup='hidup'");
$pddk->execute([':nik'=>$nik]);
$pddk = $pddk->fetch();
if (!$pddk) throw new Exception("Penduduk dengan NIK $nik tidak ditemukan / meninggal.");
$bukti = null;
if (!empty($_FILES['bukti']['name'])) {
$bukti = uploadFile($_FILES['bukti'], UPLOAD_BUKTI);
}
$stmt = $pdo->prepare("
INSERT INTO histori_bantuan (nik,id_jenis_bantuan,id_keluarga,rumah_ibadah_id,tanggal,jumlah,nilai_rupiah,status_penyaluran,catatan,bukti_file,disalurkan_oleh)
VALUES(:nik,:jb,:kk,:ri,:tgl,:jml,:rp,:status,:cat,:bukti,:user)
");
$stmt->execute([
':nik'=>$nik, ':jb'=>$idJb, ':kk'=>$pddk['id_keluarga'],
':ri'=>$_SESSION['rumah_ibadah_id'] ?: null,
':tgl'=>$tgl, ':jml'=>$_POST['jumlah']??'' ?: null,
':rp'=>$_POST['nilai_rupiah']??'' ?: null,
':status'=>$_POST['status_penyaluran']??'disalurkan',
':cat'=>$_POST['catatan']??'' ?: null,
':bukti'=>$bukti, ':user'=>$_SESSION['user_id'],
]);
logAktivitas($pdo, (int)$_SESSION['user_id'], 'CREATE', 'histori_bantuan', (string)$pdo->lastInsertId());
$msg = "Bantuan untuk NIK $nik berhasil dicatat.";
} catch(Exception $e) { $err = $e->getMessage(); }
}
}
$jenisList = $pdo->query("SELECT * FROM jenis_bantuan WHERE aktif=1 ORDER BY kategori,nama")->fetchAll();
render_header('Catat Bantuan');
?>
<div class="page-title">🎁 Catat Penyaluran Bantuan</div>
<?php if ($msg): ?><div class="notice" style="border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div>
<div class="card">
<form method="POST" enctype="multipart/form-data">
<div class="form-grid">
<div class="form-group full">
<label>NIK Penerima *</label>
<input name="nik" maxlength="16" value="<?= h($nikPre) ?>" placeholder="16 digit NIK" required onblur="cekPenduduk(this.value)">
</div>
<div class="form-group full" id="info-pddk" style="background:#334155;border-radius:8px;padding:10px;font-size:12px;display:<?= $namaPre?'block':'none' ?>">
<?= $namaPre ? '👤 '.$namaPre : '' ?>
</div>
<div class="form-group full">
<label>Jenis Bantuan *</label>
<select name="id_jenis_bantuan" required>
<option value="">— Pilih —</option>
<?php foreach ($jenisList as $j): ?>
<option value="<?= $j['id'] ?>"><?= h($j['nama']) ?> (<?= h($j['kategori']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Tanggal *</label>
<input type="date" name="tanggal" required value="<?= date('Y-m-d') ?>">
</div>
<div class="form-group">
<label>Status</label>
<select name="status_penyaluran">
<option value="disalurkan">Disalurkan</option>
<option value="dijadwalkan">Dijadwalkan</option>
</select>
</div>
<div class="form-group">
<label>Jumlah / Keterangan</label>
<input name="jumlah" placeholder="mis: 1 paket sembako">
</div>
<div class="form-group">
<label>Nilai (Rp)</label>
<input type="number" name="nilai_rupiah" placeholder="0">
</div>
<div class="form-group full">
<label>Catatan</label>
<textarea name="catatan" rows="2" placeholder="Opsional"></textarea>
</div>
<div class="form-group full">
<label>Bukti Foto/Dokumen</label>
<input type="file" name="bukti" accept=".jpg,.jpeg,.png,.webp,.pdf">
</div>
</div>
<button type="submit" name="save" value="1" class="btn btn-success" style="margin-top:14px;width:100%">✅ Simpan Bantuan</button>
</form>
</div>
</div>
<div>
<div class="card">
<strong>📋 Bantuan Terakhir Disalurkan</strong>
<table style="margin-top:12px">
<thead><tr><th>Penerima</th><th>Bantuan</th><th>Tanggal</th></tr></thead>
<tbody>
<?php
$recent = $pdo->query("
SELECT p.nama_lengkap, jb.nama AS nama_bantuan, hb.tanggal
FROM histori_bantuan hb
JOIN penduduk p ON p.nik=hb.nik
JOIN jenis_bantuan jb ON jb.id=hb.id_jenis_bantuan
WHERE hb.status_penyaluran='disalurkan'
ORDER BY hb.created_at DESC LIMIT 10
")->fetchAll();
foreach ($recent as $r): ?>
<tr>
<td><?= h($r['nama_lengkap']) ?></td>
<td><?= h($r['nama_bantuan']) ?></td>
<td style="font-size:11px"><?= date('d/m/Y', strtotime($r['tanggal'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
async function cekPenduduk(nik) {
if (!/^\d{16}$/.test(nik)) return;
const res = await fetch('../php/api/penduduk.php?action=detail&nik='+nik);
const json = await res.json();
const info = document.getElementById('info-pddk');
if (json.success) {
const p = json.data;
info.style.display = 'block';
info.innerHTML = `👤 <strong>${p.nama_lengkap}</strong> | ${p.umur}th |
<span style="color:${p.status_ekonomi==='miskin'?'#ef4444':'#f59e0b'}">${p.status_ekonomi}</span> |
Sudah dibantu: ${json.histori.length}x`;
} else {
info.style.display = 'block';
info.innerHTML = '<span style="color:#ef4444">❌ NIK tidak ditemukan</span>';
}
}
</script>
<?php render_footer(); ?>
+71
View File
@@ -0,0 +1,71 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin','pengurus','pimpinan']);
$rows = [];
try {
$rows = $pdo->query("
SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan,
p.umur, p.pekerjaan, p.penghasilan, p.alamat,
k.no_kk, k.nama_kk, ri.nama AS nama_ri, ri.jenis AS jenis_ri
FROM penduduk p
LEFT JOIN keluarga k ON k.id=p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id
WHERE p.status_hidup='hidup'
AND p.status_ekonomi IN ('miskin','rentan')
AND NOT EXISTS(
SELECT 1 FROM histori_bantuan hb
WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan'
)
ORDER BY
CASE p.status_ekonomi WHEN 'miskin' THEN 1 ELSE 2 END,
CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 WHEN 'disabilitas' THEN 2 WHEN 'sakit_ringan' THEN 3 ELSE 4 END,
p.nama_lengkap
")->fetchAll();
} catch(Exception $e) {}
render_header('Belum Pernah Dibantu');
?>
<div class="page-title">⚠️ Warga Belum Pernah Menerima Bantuan</div>
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);border-radius:12px;padding:14px 18px;margin-bottom:16px;font-size:13px">
Ditemukan <strong style="color:#ef4444"><?= count($rows) ?> jiwa</strong> dengan status ekonomi miskin/rentan yang belum pernah menerima bantuan apapun. Prioritaskan yang kondisi kesehatannya kritis.
</div>
<div class="card" style="padding:0;overflow:hidden">
<table>
<thead><tr>
<th>Nama</th><th>NIK</th><th>Status</th><th>Kesehatan</th><th>Umur</th><th>Pekerjaan</th><th>Rumah Ibadah</th>
<?php if (in_array($_SESSION['role'], ['admin','pengurus'])): ?><th>Aksi</th><?php endif; ?>
</tr></thead>
<tbody>
<?php foreach ($rows as $r):
$kritis = $r['status_ekonomi']==='miskin' && in_array($r['kondisi_kesehatan'],['sakit_parah','disabilitas']);
?>
<tr <?= $kritis ? 'style="background:rgba(239,68,68,.05)"' : '' ?>>
<td>
<?= $kritis ? '🔥 ' : '' ?><strong><?= h($r['nama_lengkap']) ?></strong>
<?= $r['status_ekonomi']==='miskin' && $r['kondisi_kesehatan']==='sakit_parah' ? '<span class="tag darurat" style="font-size:9px;margin-left:4px">KRITIS</span>' : '' ?>
</td>
<td style="font-size:11px;color:#94a3b8"><?= h($r['nik']) ?></td>
<td><span class="tag <?= $r['status_ekonomi'] ?>"><?= h($r['status_ekonomi']) ?></span></td>
<td><?= h(str_replace('_',' ',$r['kondisi_kesehatan'])) ?></td>
<td><?= $r['umur'] ?> th</td>
<td><?= h($r['pekerjaan'] ?: '-') ?></td>
<td><?= h($r['nama_ri'] ?: '-') ?></td>
<?php if (in_array($_SESSION['role'], ['admin','pengurus'])): ?>
<td>
<a href="bantuan.php?nik=<?= h($r['nik']) ?>&nama=<?= urlencode($r['nama_lengkap']) ?>" class="btn btn-success btn-sm">
🎁 Beri Bantuan
</a>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr><td colspan="8" style="text-align:center;padding:24px;color:#22c55e">🎉 Semua warga miskin/rentan sudah pernah dibantu!</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php render_footer(); ?>
+92
View File
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lupa Password WebGIS Pengentasan Kemiskinan</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#0f172a;color:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px}
.card{background:#1e293b;border:1px solid #334155;border-radius:20px;padding:40px 36px;width:100%;max-width:400px;box-shadow:0 25px 60px rgba(0,0,0,.5)}
.logo{text-align:center;margin-bottom:28px}
.logo .icon{font-size:40px;margin-bottom:8px}
.logo h1{font-size:20px;font-weight:900}
.logo p{font-size:12px;color:#94a3b8;margin-top:4px}
.form-group{margin-bottom:16px}
label{display:block;font-size:11px;font-weight:700;color:#94a3b8;text-transform:uppercase;margin-bottom:6px}
input{width:100%;background:#0f172a;border:1px solid #334155;color:#f1f5f9;padding:11px 14px;border-radius:10px;font-size:14px}
input:focus{outline:none;border-color:#3b82f6}
.btn{width:100%;background:#3b82f6;color:#fff;border:none;padding:12px;border-radius:10px;font-size:14px;font-weight:800;cursor:pointer;transition:.2s}
.btn:hover{background:#2563eb}
.error{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:#ef4444;padding:10px 14px;border-radius:8px;font-size:13px;margin-bottom:16px;}
.success{background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.3);color:#22c55e;padding:10px 14px;border-radius:8px;font-size:13px;margin-bottom:16px;}
.back{text-align:center;margin-top:16px}
.back a{color:#3b82f6;font-size:13px;text-decoration:none}
</style>
</head>
<body>
<?php
require_once __DIR__ . '/../php/koneksi.php';
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = sanitizeStr($_POST['username'] ?? '');
$email = sanitizeStr($_POST['email'] ?? '');
$new_password = $_POST['new_password'] ?? '';
if (!$username || !$email || !$new_password) {
$error = 'Semua kolom wajib diisi.';
} else {
// Cek kecocokan username dan email
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = :u AND email = :e LIMIT 1");
$stmt->execute([':u' => $username, ':e' => $email]);
$user = $stmt->fetch();
if ($user) {
$hash = password_hash($new_password, PASSWORD_DEFAULT);
$update = $pdo->prepare("UPDATE users SET password_hash = :p WHERE id = :id");
$update->execute([':p' => $hash, ':id' => $user['id']]);
$success = 'Password berhasil direset! Silakan login dengan password baru Anda.';
} else {
$error = 'Username dan Email tidak cocok atau tidak ditemukan.';
}
}
}
?>
<div class="card">
<div class="logo">
<div class="icon">🔑</div>
<h1>Reset Password</h1>
<p>Masukkan Username & Email Terdaftar</p>
</div>
<?php if ($error): ?>
<div class="error"><?= h($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="success"><?= h($success) ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label>Username</label>
<input type="text" name="username" placeholder="Cth: admin" required autofocus>
</div>
<div class="form-group">
<label>Email Terdaftar</label>
<input type="email" name="email" placeholder="Cth: admin@webgis.local" required>
</div>
<div class="form-group">
<label>Password Baru</label>
<input type="password" name="new_password" placeholder="Masukkan password baru" required>
</div>
<button type="submit" class="btn">🔄 Reset Password</button>
</form>
<div class="back"><a href="login.php">← Kembali ke Halaman Login</a></div>
</div>
</body>
</html>
@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin','pengurus','pimpinan']);
$rows = [];
try {
$rows = $pdo->query("
SELECT hb.id, hb.nik, p.nama_lengkap, hb.tanggal,
jb.nama AS nama_bantuan, jb.kategori,
hb.jumlah, hb.nilai_rupiah, hb.status_penyaluran,
ri.nama AS nama_ri, u.nama AS disalurkan_oleh
FROM histori_bantuan hb
JOIN penduduk p ON p.nik=hb.nik
JOIN jenis_bantuan jb ON jb.id=hb.id_jenis_bantuan
LEFT JOIN rumah_ibadah ri ON ri.id=hb.rumah_ibadah_id
LEFT JOIN users u ON u.id=hb.disalurkan_oleh
ORDER BY hb.tanggal DESC
LIMIT 500
")->fetchAll();
} catch(Exception $e) {}
render_header('Histori Bantuan');
?>
<div class="page-title">📜 Histori Penyaluran Bantuan</div>
<div class="card" style="padding:0;overflow:hidden">
<table>
<thead><tr>
<th>#</th><th>Nama Penerima</th><th>Jenis Bantuan</th><th>Jumlah</th>
<th>Tanggal</th><th>Rumah Ibadah</th><th>Disalurkan Oleh</th><th>Status</th>
</tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= $r['id'] ?></td>
<td><?= h($r['nama_lengkap']) ?><br><span style="font-size:10px;color:#94a3b8"><?= h($r['nik']) ?></span></td>
<td><?= h($r['nama_bantuan']) ?> <span style="font-size:10px;color:#94a3b8">(<?= h($r['kategori']) ?>)</span></td>
<td><?= h($r['jumlah'] ?? '-') ?><?= $r['nilai_rupiah'] ? '<br><span style="font-size:10px;color:#22c55e">Rp '.number_format($r['nilai_rupiah'],0,',','.').'</span>' : '' ?></td>
<td><?= date('d/m/Y', strtotime($r['tanggal'])) ?></td>
<td><?= h($r['nama_ri'] ?? '-') ?></td>
<td><?= h($r['disalurkan_oleh'] ?? '-') ?></td>
<td><span class="tag <?= $r['status_penyaluran']==='disalurkan'?'selesai':($r['status_penyaluran']==='dijadwalkan'?'pending':'darurat') ?>"><?= h($r['status_penyaluran']) ?></span></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="8" style="text-align:center;padding:24px;color:#94a3b8">Belum ada data</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<?php render_footer(); ?>
+128
View File
@@ -0,0 +1,128 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin','pengurus','pimpinan']);
$stats = [];
try {
$stats = $pdo->query("
SELECT
(SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='miskin' AND status_hidup='hidup') AS miskin,
(SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='rentan' AND status_hidup='hidup') AS rentan,
(SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup') AS total_pddk,
(SELECT COUNT(*) FROM keluarga) AS total_kk,
(SELECT COUNT(*) FROM rumah_ibadah) AS total_ri,
(SELECT COUNT(*) FROM laporan WHERE status='pending') AS laporan_pending,
(SELECT COUNT(*) FROM laporan WHERE tingkat_urgensi='darurat' AND status NOT IN ('selesai','ditolak')) AS laporan_darurat,
(SELECT COUNT(*) FROM histori_bantuan WHERE MONTH(tanggal)=MONTH(CURDATE()) AND YEAR(tanggal)=YEAR(CURDATE()) AND status_penyaluran='disalurkan') AS bantuan_bulan,
(SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup' AND status_ekonomi IN ('miskin','rentan')
AND NOT EXISTS(SELECT 1 FROM histori_bantuan hb WHERE hb.nik=penduduk.nik AND hb.status_penyaluran='disalurkan')
) AS belum_dibantu
")->fetch();
} catch(Exception $e) {}
// Laporan terbaru
$laporan_terbaru = [];
try {
$laporan_terbaru = $pdo->query("
SELECT l.id, l.tingkat_urgensi, l.kategori, l.status, l.created_at,
l.nama_pelapor, p.nama_lengkap AS nama_terdampak
FROM laporan l
LEFT JOIN penduduk p ON p.nik = l.nik_terdampak
ORDER BY
CASE l.tingkat_urgensi WHEN 'darurat' THEN 1 WHEN 'tinggi' THEN 2 ELSE 3 END,
l.created_at DESC
LIMIT 8
")->fetchAll();
} catch(Exception $e) {}
// Prioritas belum dibantu
$prioritas = [];
try {
$prioritas = $pdo->query("
SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan,
p.alamat, k.no_kk, ri.nama AS nama_ri
FROM penduduk p
LEFT JOIN keluarga k ON k.id=p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id
WHERE p.status_hidup='hidup'
AND p.status_ekonomi='miskin'
AND p.kondisi_kesehatan IN ('sakit_parah','disabilitas')
AND NOT EXISTS(SELECT 1 FROM histori_bantuan hb WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan')
ORDER BY CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 ELSE 2 END
LIMIT 5
")->fetchAll();
} catch(Exception $e) {}
render_header('Dashboard');
?>
<div class="page-title">📊 Dashboard Monitoring</div>
<!-- Stats -->
<div class="stats-row">
<div class="stat red"><div class="val"><?= $stats['miskin'] ?? 0 ?></div><div class="lbl">Warga Miskin</div></div>
<div class="stat yellow"><div class="val"><?= $stats['rentan'] ?? 0 ?></div><div class="lbl">Warga Rentan</div></div>
<div class="stat blue"><div class="val"><?= $stats['total_kk'] ?? 0 ?></div><div class="lbl">Total KK</div></div>
<div class="stat green"><div class="val"><?= $stats['bantuan_bulan'] ?? 0 ?></div><div class="lbl">Bantuan Bulan Ini</div></div>
<div class="stat red"><div class="val"><?= $stats['laporan_pending'] ?? 0 ?></div><div class="lbl">Laporan Pending</div></div>
<div class="stat red"><div class="val"><?= $stats['belum_dibantu'] ?? 0 ?></div><div class="lbl">Belum Pernah Dibantu</div></div>
</div>
<?php if (!empty($stats['laporan_darurat']) && $stats['laporan_darurat'] > 0): ?>
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);border-radius:12px;padding:14px 18px;margin-bottom:16px;display:flex;align-items:center;gap:12px">
<span style="font-size:24px">🚨</span>
<div>
<div style="font-weight:900;color:#ef4444"><?= $stats['laporan_darurat'] ?> LAPORAN DARURAT belum ditangani!</div>
<div style="font-size:12px;color:#94a3b8;margin-top:2px">Segera tindak lanjuti laporan kondisi darurat dari masyarakat.</div>
</div>
<a href="laporan.php?filter=darurat" class="btn btn-danger" style="margin-left:auto">Lihat →</a>
</div>
<?php endif; ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<!-- Laporan terbaru -->
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<strong>📋 Laporan Terbaru</strong>
<a href="laporan.php" class="btn btn-secondary btn-sm">Semua →</a>
</div>
<table>
<thead><tr><th>Pelapor/Terdampak</th><th>Urgensi</th><th>Status</th></tr></thead>
<tbody>
<?php foreach ($laporan_terbaru as $l): ?>
<tr>
<td><?= h($l['nama_terdampak'] ?? $l['nama_pelapor'] ?? 'Anonim') ?></td>
<td><span class="tag <?= $l['tingkat_urgensi'] === 'darurat' ? 'darurat' : 'pending' ?>"><?= h($l['tingkat_urgensi']) ?></span></td>
<td><span class="tag <?= $l['status'] ?>"><?= h($l['status']) ?></span></td>
</tr>
<?php endforeach; ?>
<?php if (empty($laporan_terbaru)): ?><tr><td colspan="3" style="text-align:center;color:#94a3b8">Belum ada laporan</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<!-- Prioritas kritis -->
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<strong>🔥 Prioritas KRITIS (Belum Dibantu)</strong>
<a href="belum_dibantu.php" class="btn btn-danger btn-sm">Semua →</a>
</div>
<table>
<thead><tr><th>Nama</th><th>Status</th><th>Kesehatan</th></tr></thead>
<tbody>
<?php foreach ($prioritas as $p): ?>
<tr>
<td><a href="penduduk.php?detail=<?= h($p['nik']) ?>" style="color:var(--acc)"><?= h($p['nama_lengkap']) ?></a></td>
<td><span class="tag miskin"><?= h($p['status_ekonomi']) ?></span></td>
<td style="color:#ef4444"><?= h($p['kondisi_kesehatan']) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($prioritas)): ?><tr><td colspan="3" style="text-align:center;color:#94a3b8">🎉 Tidak ada kasus kritis saat ini</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php render_footer(); ?>
+32
View File
@@ -0,0 +1,32 @@
<?php
// Script inisialisasi — jalankan SEKALI untuk membuat akun admin & demo
// URL: http://localhost/webgis-kemiskinan/admin/init_admin.php
// HAPUS file ini setelah dipakai di produksi!
require_once __DIR__ . '/../php/koneksi.php';
$users = [
['admin', 'Administrator', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 1, null],
['pengurus1', 'H. Ahmad Fauzi (Pengurus)', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 2, 1],
['pimpinan1', 'Camat Pontianak Selatan', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 3, null],
['warga1', 'Budi Santoso', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 4, null],
];
$created = 0;
foreach ($users as [$uname, $nama, $hash, $role, $ri]) {
$cek = $pdo->prepare("SELECT id FROM users WHERE username=:u");
$cek->execute([':u'=>$uname]);
if (!$cek->fetch()) {
$pdo->prepare("INSERT INTO users (role_id,nama,username,password_hash,rumah_ibadah_id) VALUES(:r,:n,:u,:p,:ri)")
->execute([':r'=>$role,':n'=>$nama,':u'=>$uname,':p'=>$hash,':ri'=>$ri]);
$created++;
echo "✅ User '$uname' dibuat.<br>";
} else {
echo "️ User '$uname' sudah ada — dilewati.<br>";
}
}
echo "<br><strong>$created user baru dibuat.</strong><br>";
echo "<br>Password semua akun demo: <code>password</code><br>";
echo "<br><strong>Segera hapus file ini setelah digunakan!</strong><br>";
echo "<br><a href='login.php'>→ Ke halaman login</a>";
+159
View File
@@ -0,0 +1,159 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin','pengurus','pimpinan']);
$role = $_SESSION['role'];
$canEdit = in_array($role, ['admin','pengurus']);
$msg = ''; $err = '';
// DELETE
if ($canEdit && isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
try {
$pdo->prepare("DELETE FROM keluarga WHERE id=:id")->execute([':id'=>$id]);
logAktivitas($pdo, (int)$_SESSION['user_id'], 'DELETE', 'keluarga', (string)$id);
$msg = "Data keluarga #$id dihapus.";
} catch(Exception $e) { $err = 'Gagal hapus: '.$e->getMessage(); }
}
// SAVE
if ($canEdit && isset($_POST['save'])) {
$nokk = trim($_POST['no_kk'] ?? '');
$nama = trim($_POST['nama_kk'] ?? '');
$idEdit = (int)($_POST['id_edit'] ?? 0);
if (!preg_match('/^\d{16}$/', $nokk)) $err = 'No KK harus 16 digit.';
elseif (strlen($nama) < 3) $err = 'Nama KK terlalu pendek.';
else {
try {
$data = [
':nokk' => $nokk, ':nama' => $nama,
':almt' => $_POST['alamat']??'' ?: null,
':kel' => $_POST['kelurahan']??'' ?: null,
':kec' => $_POST['kecamatan']??'' ?: null,
':kota' => $_POST['kota']??'' ?: null,
':lat' => $_POST['latitude']??'' ?: null,
':lng' => $_POST['longitude']??'' ?: null,
':se' => $_POST['status_ekonomi']??'rentan',
':ri' => $_POST['rumah_ibadah_id']??'' ?: null,
];
if ($idEdit) {
$data[':id'] = $idEdit;
$pdo->prepare("UPDATE keluarga SET no_kk=:nokk,nama_kk=:nama,alamat=:almt,kelurahan=:kel,
kecamatan=:kec,kota=:kota,latitude=:lat,longitude=:lng,status_ekonomi=:se,
rumah_ibadah_id=:ri WHERE id=:id")->execute($data);
$msg = "Data KK $nama diperbarui.";
logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'keluarga', (string)$idEdit);
} else {
$pdo->prepare("INSERT INTO keluarga (no_kk,nama_kk,alamat,kelurahan,kecamatan,kota,latitude,longitude,status_ekonomi,rumah_ibadah_id)
VALUES(:nokk,:nama,:almt,:kel,:kec,:kota,:lat,:lng,:se,:ri)")->execute($data);
$msg = "Keluarga $nama berhasil ditambahkan.";
logAktivitas($pdo, (int)$_SESSION['user_id'], 'CREATE', 'keluarga', (string)$pdo->lastInsertId());
}
} catch(Exception $e) { $err = $e->getMessage(); }
}
}
$edit = null;
if (isset($_GET['edit'])) {
$s = $pdo->prepare("SELECT * FROM keluarga WHERE id=:id");
$s->execute([':id'=>(int)$_GET['edit']]);
$edit = $s->fetch() ?: null;
}
$search = trim($_GET['s'] ?? '');
$where = []; $params = [];
if ($search) { $where[] = '(k.nama_kk LIKE :q OR k.no_kk LIKE :q2)'; $params[':q']='%'.$search.'%'; $params[':q2']='%'.$search.'%'; }
$whereStr = $where ? 'WHERE '.implode(' AND ',$where) : '';
$rows = $pdo->prepare("
SELECT k.*, ri.nama AS nama_ri,
(SELECT COUNT(*) FROM penduduk p WHERE p.id_keluarga=k.id) AS jml_anggota
FROM keluarga k
LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id
$whereStr ORDER BY k.nama_kk LIMIT 300
");
$rows->execute($params);
$rows = $rows->fetchAll();
$riList = $pdo->query("SELECT id,nama,jenis FROM rumah_ibadah ORDER BY nama")->fetchAll();
render_header('Keluarga (KK)');
?>
<div class="page-title">🏠 Data Keluarga (Kartu Keluarga)</div>
<?php if ($msg): ?><div class="notice" style="border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<div style="display:grid;grid-template-columns:<?= $canEdit ? '1fr 1.5fr' : '1fr' ?>;gap:16px">
<?php if ($canEdit): ?>
<div class="card">
<strong><?= $edit ? '✏️ Edit KK' : ' Tambah KK' ?></strong><br><br>
<form method="POST">
<?php if ($edit): ?><input type="hidden" name="id_edit" value="<?= $edit['id'] ?>"><?php endif; ?>
<div class="form-grid">
<div class="form-group full"><label>No KK (16 digit) *</label>
<input name="no_kk" maxlength="16" required value="<?= h($edit['no_kk'] ?? '') ?>"></div>
<div class="form-group full"><label>Nama Kepala Keluarga *</label>
<input name="nama_kk" required value="<?= h($edit['nama_kk'] ?? '') ?>"></div>
<div class="form-group full"><label>Alamat</label>
<textarea name="alamat" rows="2"><?= h($edit['alamat'] ?? '') ?></textarea></div>
<div class="form-group"><label>Kelurahan</label>
<input name="kelurahan" value="<?= h($edit['kelurahan'] ?? '') ?>"></div>
<div class="form-group"><label>Kecamatan</label>
<input name="kecamatan" value="<?= h($edit['kecamatan'] ?? '') ?>"></div>
<div class="form-group"><label>Status Ekonomi</label>
<select name="status_ekonomi">
<?php foreach (['miskin','rentan','mampu'] as $s): ?>
<option value="<?= $s ?>" <?= ($edit['status_ekonomi'] ?? 'rentan')===$s?'selected':'' ?>><?= ucfirst($s) ?></option>
<?php endforeach; ?>
</select></div>
<div class="form-group"><label>Rumah Ibadah Terdekat</label>
<select name="rumah_ibadah_id">
<option value="">— Tidak ada —</option>
<?php foreach ($riList as $ri): ?>
<option value="<?= $ri['id'] ?>" <?= ($edit['rumah_ibadah_id'] ?? '')==$ri['id']?'selected':'' ?>><?= h($ri['nama']) ?></option>
<?php endforeach; ?>
</select></div>
<div class="form-group"><label>Latitude</label>
<input name="latitude" type="number" step="any" value="<?= h($edit['latitude'] ?? '') ?>"></div>
<div class="form-group"><label>Longitude</label>
<input name="longitude" type="number" step="any" value="<?= h($edit['longitude'] ?? '') ?>"></div>
</div>
<br>
<button type="submit" name="save" class="btn btn-primary"><?= $edit ? '💾 Update' : ' Simpan' ?></button>
<?php if ($edit): ?><a href="keluarga.php" class="btn btn-secondary">Batal</a><?php endif; ?>
</form>
</div>
<?php endif; ?>
<div>
<div style="display:flex;gap:8px;margin-bottom:12px">
<form method="GET" style="display:flex;gap:8px;flex:1">
<input name="s" placeholder="Cari nama / No KK…" value="<?= h($search) ?>" style="flex:1">
<button class="btn btn-primary">🔍</button>
</form>
</div>
<div class="card" style="padding:0;overflow:hidden">
<table>
<thead><tr><th>No KK</th><th>Nama KK</th><th>Status</th><th>Anggota</th><th>Rumah Ibadah</th><?= $canEdit?'<th>Aksi</th>':'' ?></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td style="font-size:11px"><?= h($r['no_kk']) ?></td>
<td><strong><?= h($r['nama_kk']) ?></strong><br><span style="font-size:11px;color:#94a3b8"><?= h($r['kecamatan']??'') ?></span></td>
<td><span class="tag <?= $r['status_ekonomi'] ?>"><?= h($r['status_ekonomi']) ?></span></td>
<td><?= $r['jml_anggota'] ?> jiwa</td>
<td style="font-size:12px"><?= h($r['nama_ri'] ?? '-') ?></td>
<?php if ($canEdit): ?>
<td class="actions">
<a href="?edit=<?= $r['id'] ?>" class="btn btn-secondary btn-sm">✏️</a>
<a href="?delete=<?= $r['id'] ?>" class="btn btn-danger btn-sm" onclick="return confirm('Hapus KK ini?')">🗑️</a>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="6" style="text-align:center;padding:24px;color:#94a3b8">Belum ada data</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php render_footer(); ?>
+167
View File
@@ -0,0 +1,167 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin','pengurus','pimpinan']);
$currentUser = ['id' => (int)$_SESSION['user_id'], 'role' => $_SESSION['role']];
$filter = $_GET['filter'] ?? 'semua';
$msg = ''; $err = '';
// Update status laporan
if (isset($_POST['update_status']) && in_array($currentUser['role'], ['admin','pengurus'])) {
$id = (int)($_POST['lap_id'] ?? 0);
$status = $_POST['status'] ?? '';
$catatan = trim($_POST['catatan'] ?? '');
$valid = ['diverifikasi','diproses','selesai','ditolak'];
if ($id > 0 && in_array($status, $valid)) {
try {
$sets = ['status=:s','updated_at=NOW()'];
$params = [':s'=>$status,':id'=>$id];
if ($status === 'diverifikasi') { $sets[] = 'diverifikasi_oleh=:v'; $params[':v'] = $currentUser['id']; }
if ($status === 'diproses') { $sets[] = 'ditangani_oleh=:t'; $params[':t'] = $currentUser['id']; }
if ($status === 'selesai') { $sets[] = 'tanggal_selesai=NOW()'; }
if ($catatan) { $sets[] = 'catatan_verifikasi=:c'; $params[':c'] = $catatan; }
$pdo->prepare("UPDATE laporan SET ".implode(',',$sets)." WHERE id=:id")->execute($params);
logAktivitas($pdo, $currentUser['id'], 'UPDATE', 'laporan', (string)$id, null, ['status'=>$status]);
$msg = "Status laporan #$id diperbarui ke '$status'.";
} catch(Exception $e) { $err = $e->getMessage(); }
}
}
// Ambil laporan
$where = [];
$params = [];
if ($filter === 'pending') { $where[] = "status='pending'"; }
if ($filter === 'darurat') { $where[] = "tingkat_urgensi IN ('darurat','tinggi')"; }
if ($filter === 'diproses') { $where[] = "status IN ('diverifikasi','diproses')"; }
if ($filter === 'selesai') { $where[] = "status='selesai'"; }
$whereStr = $where ? 'WHERE '.implode(' AND ',$where) : '';
$rows = [];
try {
$rows = $pdo->query("
SELECT l.*, p.nama_lengkap AS nama_terdampak, u.nama AS nama_verifikator
FROM laporan l
LEFT JOIN penduduk p ON p.nik=l.nik_terdampak
LEFT JOIN users u ON u.id=l.diverifikasi_oleh
$whereStr
ORDER BY CASE l.tingkat_urgensi WHEN 'darurat' THEN 1 WHEN 'tinggi' THEN 2 ELSE 3 END,
CASE l.status WHEN 'pending' THEN 1 WHEN 'diverifikasi' THEN 2 ELSE 3 END,
l.created_at DESC
")->fetchAll();
} catch(Exception $e) { $err = $e->getMessage(); }
render_header('Manajemen Laporan');
?>
<div class="page-title">🚩 Laporan Masyarakat</div>
<?php if ($msg): ?><div class="notice" style="border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">
<?php foreach (['semua'=>'Semua','pending'=>'Pending','darurat'=>'🔴 Darurat','diproses'=>'Diproses','selesai'=>'Selesai'] as $k=>$v): ?>
<a href="?filter=<?= $k ?>" class="btn <?= $filter===$k?'btn-primary':'btn-secondary' ?>"><?= $v ?></a>
<?php endforeach; ?>
</div>
<div class="card" style="padding:0;overflow:hidden">
<table>
<thead>
<tr>
<th>#</th><th>Pelapor</th><th>Terdampak</th><th>Kategori</th>
<th>Urgensi</th><th>Status</th><th>Tanggal</th><th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= $r['id'] ?></td>
<td><?= h($r['nama_pelapor'] ?: 'Anonim') ?></td>
<td><?= h($r['nama_terdampak'] ?? ($r['nik_terdampak'] ?? '-')) ?></td>
<td style="font-size:11px"><?= h(str_replace('_',' ',$r['kategori'])) ?></td>
<td><span class="tag <?= $r['tingkat_urgensi']==='darurat'?'darurat':'pending' ?>"><?= h($r['tingkat_urgensi']) ?></span></td>
<td><span class="tag <?= $r['status'] ?>"><?= h($r['status']) ?></span></td>
<td style="font-size:11px"><?= date('d/m/y H:i', strtotime($r['created_at'])) ?></td>
<td>
<?php if (in_array($currentUser['role'], ['admin','pengurus'])): ?>
<button class="btn btn-secondary btn-sm" onclick="showUpdate(<?= $r['id'] ?>, '<?= h($r['status']) ?>', '<?= addslashes($r['catatan_verifikasi'] ?? '') ?>')">
Update
</button>
<?php endif; ?>
<button class="btn btn-secondary btn-sm" onclick="showDetail(<?= htmlspecialchars(json_encode($r), ENT_QUOTES) ?>)">Detail</button>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr><td colspan="8" style="text-align:center;padding:30px;color:#94a3b8">Tidak ada laporan</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Modal Update Status -->
<div id="modal-update" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:999;align-items:center;justify-content:center">
<div style="background:#1e293b;border-radius:16px;padding:24px;width:100%;max-width:440px;border:1px solid #334155">
<h3 style="margin-bottom:16px">Update Status Laporan #<span id="upd-id"></span></h3>
<form method="POST">
<input type="hidden" name="lap_id" id="upd-lap-id">
<div class="form-group" style="margin-bottom:12px">
<label>Status Baru</label>
<select name="status" id="upd-status">
<option value="diverifikasi">Diverifikasi</option>
<option value="diproses">Diproses</option>
<option value="selesai">Selesai</option>
<option value="ditolak">Ditolak</option>
</select>
</div>
<div class="form-group" style="margin-bottom:16px">
<label>Catatan</label>
<textarea name="catatan" id="upd-catatan" rows="3" placeholder="Catatan verifikasi/tindakan..."></textarea>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('modal-update').style.display='none'">Batal</button>
<button type="submit" name="update_status" value="1" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
<!-- Modal Detail -->
<div id="modal-detail" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:999;align-items:center;justify-content:center">
<div style="background:#1e293b;border-radius:16px;padding:24px;width:100%;max-width:520px;border:1px solid #334155;max-height:80vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3>Detail Laporan</h3>
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('modal-detail').style.display='none'">✕</button>
</div>
<div id="detail-content"></div>
</div>
</div>
<script>
function showUpdate(id, status, catatan) {
document.getElementById('upd-id').textContent = id;
document.getElementById('upd-lap-id').value = id;
document.getElementById('upd-status').value = status;
document.getElementById('upd-catatan').value = catatan;
const m = document.getElementById('modal-update');
m.style.display = 'flex';
}
function showDetail(data) {
const m = document.getElementById('modal-detail');
document.getElementById('detail-content').innerHTML = `
<div style="font-size:13px;display:grid;gap:8px">
<div><strong>Pelapor:</strong> ${data.nama_pelapor || 'Anonim'}</div>
<div><strong>Terdampak:</strong> ${data.nik_terdampak || '-'}</div>
<div><strong>Kategori:</strong> ${data.kategori.replace(/_/g,' ')}</div>
<div><strong>Urgensi:</strong> ${data.tingkat_urgensi}</div>
<div><strong>Alamat:</strong> ${data.alamat || '-'}</div>
<div style="background:#334155;border-radius:8px;padding:10px;margin-top:4px"><strong>Deskripsi:</strong><br>${data.deskripsi}</div>
${data.catatan_verifikasi ? `<div><strong>Catatan:</strong> ${data.catatan_verifikasi}</div>` : ''}
${data.foto ? `<div><strong>Foto:</strong> <a href="../uploads/foto_laporan/${data.foto}" target="_blank" style="color:#3b82f6">Lihat Foto</a></div>` : ''}
</div>
`;
m.style.display = 'flex';
}
</script>
<?php render_footer(); ?>
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin']);
$rows = $pdo->query("
SELECT l.*, u.nama AS nama_user
FROM log_aktivitas l
LEFT JOIN users u ON u.id=l.user_id
ORDER BY l.created_at DESC
LIMIT 200
")->fetchAll();
render_header('Log Aktivitas');
?>
<div class="page-title">📜 Log Aktivitas Sistem</div>
<div class="card" style="padding:0;overflow:hidden">
<table>
<thead><tr><th>Waktu</th><th>User</th><th>Aksi</th><th>Tabel</th><th>Record</th><th>IP</th></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td style="font-size:11px"><?= date('d/m/y H:i:s', strtotime($r['created_at'])) ?></td>
<td><?= h($r['nama_user'] ?? 'Sistem') ?></td>
<td><span class="tag <?= $r['aksi']==='DELETE'?'darurat':($r['aksi']==='CREATE'?'selesai':'pending') ?>"><?= h($r['aksi']) ?></span></td>
<td style="font-size:11px"><?= h($r['tabel'] ?? '-') ?></td>
<td style="font-size:11px"><?= h($r['record_id'] ?? '-') ?></td>
<td style="font-size:10px;color:#94a3b8"><?= h($r['ip_address'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php render_footer(); ?>
+89
View File
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Login WebGIS Pengentasan Kemiskinan</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#0f172a;color:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px}
.card{background:#1e293b;border:1px solid #334155;border-radius:20px;padding:40px 36px;width:100%;max-width:400px;box-shadow:0 25px 60px rgba(0,0,0,.5)}
.logo{text-align:center;margin-bottom:28px}
.logo .icon{font-size:40px;margin-bottom:8px}
.logo h1{font-size:20px;font-weight:900}
.logo p{font-size:12px;color:#94a3b8;margin-top:4px}
.form-group{margin-bottom:16px}
label{display:block;font-size:11px;font-weight:700;color:#94a3b8;text-transform:uppercase;margin-bottom:6px}
input{width:100%;background:#0f172a;border:1px solid #334155;color:#f1f5f9;padding:11px 14px;border-radius:10px;font-size:14px}
input:focus{outline:none;border-color:#3b82f6}
.btn{width:100%;background:#3b82f6;color:#fff;border:none;padding:12px;border-radius:10px;font-size:14px;font-weight:800;cursor:pointer;transition:.2s}
.btn:hover{background:#2563eb}
.error{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:#ef4444;padding:10px 14px;border-radius:8px;font-size:13px;margin-bottom:16px;display:none}
.roles{margin-top:20px;background:#0f172a;border-radius:10px;padding:14px}
.roles h4{font-size:11px;font-weight:900;color:#94a3b8;margin-bottom:10px;text-transform:uppercase}
.role-row{display:flex;justify-content:space-between;font-size:12px;margin-bottom:6px;padding:6px 0;border-bottom:1px solid #1e293b}
.role-row:last-child{border:none;margin:0}
.role-badge{background:#334155;padding:2px 8px;border-radius:99px;font-size:10px;font-weight:700}
.back{text-align:center;margin-top:16px}
.back a{color:#3b82f6;font-size:13px;text-decoration:none}
</style>
</head>
<body>
<?php
require_once __DIR__ . '/../php/koneksi.php';
require_once __DIR__ . '/../php/middleware/auth.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = doLogin($pdo, $_POST['username'] ?? '', $_POST['password'] ?? '');
if ($result['success']) {
$redirect = [
'admin' => 'index.php',
'pengurus' => 'index.php',
'pimpinan' => 'index.php',
'masyarakat' => '../index.html',
];
header('Location: ' . ($redirect[$result['role']] ?? 'index.php'));
exit;
}
$error = $result['message'];
}
?>
<div class="card">
<div class="logo">
<div class="icon">🗺️</div>
<h1>WebGIS Kemiskinan</h1>
<p>Berbasis Masyarakat & Rumah Ibadah</p>
</div>
<?php if ($error): ?>
<div class="error" style="display:block"><?= h($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label>Username</label>
<input type="text" name="username" placeholder="Masukkan username" required autofocus value="<?= h($_POST['username'] ?? '') ?>">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" placeholder="Masukkan password" required>
</div>
<div class="form-group" style="text-align:right; margin-top:-8px; margin-bottom:16px;">
<a href="forgot.php" style="color:#3b82f6; font-size:11px; text-decoration:none; font-weight:700;">Lupa Password?</a>
</div>
<button type="submit" class="btn">🔐 Masuk</button>
</form>
<div class="roles">
<h4>Akun Demo (password: password)</h4>
<div class="role-row"><span>admin</span><span class="role-badge">Administrator</span></div>
<div class="role-row"><span>pengurus1</span><span class="role-badge">Pengurus Masjid</span></div>
<div class="role-row"><span>pimpinan1</span><span class="role-badge">Pimpinan Daerah</span></div>
<div class="role-row"><span>warga1</span><span class="role-badge">Masyarakat</span></div>
</div>
<div class="back"><a href="../../index.html">← Kembali ke Landing Page</a></div>
</div>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/../php/koneksi.php';
require_once __DIR__ . '/../php/middleware/auth.php';
doLogout($pdo);
header('Location: login.php');
exit;
+297
View File
@@ -0,0 +1,297 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin','pengurus','pimpinan']);
$role = $_SESSION['role'];
$canEdit = in_array($role, ['admin','pengurus']);
$msg = ''; $err = '';
// DELETE
if ($canEdit && isset($_GET['delete'])) {
$nik = $_GET['delete'];
try {
$pdo->prepare("DELETE FROM penduduk WHERE nik=:nik")->execute([':nik'=>$nik]);
logAktivitas($pdo, (int)$_SESSION['user_id'], 'DELETE', 'penduduk', $nik);
$msg = "Penduduk NIK $nik dihapus.";
} catch(Exception $e) { $err = $e->getMessage(); }
}
// SAVE
if ($canEdit && isset($_POST['save'])) {
$nik = trim($_POST['nik'] ?? '');
$nama = trim($_POST['nama_lengkap'] ?? '');
$tgl = $_POST['tanggal_lahir'] ?? '';
$nikEdit = $_POST['nik_edit'] ?? '';
if (!preg_match('/^\d{16}$/', $nik)) $err = 'NIK harus 16 digit.';
elseif (strlen($nama) < 3) $err = 'Nama terlalu pendek.';
else {
try {
$data = [
':kk'=>$_POST['id_keluarga']?:(null),
':nama'=>$nama, ':jk'=>$_POST['jenis_kelamin']??'L',
':tgl'=>$tgl, ':sk'=>$_POST['status_keluarga']??'anggota_lain',
':sp'=>$_POST['status_perkawinan']??'belum_kawin',
':sh'=>$_POST['status_hidup']??'hidup',
':ptk'=>$_POST['pendidikan_terakhir']??'tidak_sekolah',
':spd'=>$_POST['status_pendidikan']??'tidak_sekolah',
':pkr'=>$_POST['pekerjaan']??'' ?: null,
':pgn'=>(float)($_POST['penghasilan']??0),
':se'=>$_POST['status_ekonomi']??'rentan',
':kkh'=>$_POST['kondisi_kesehatan']??'sehat',
':ckkh'=>$_POST['catatan_kesehatan']??'' ?: null,
':tlp'=>$_POST['no_telp']??'' ?: null,
':almt'=>$_POST['alamat']??'' ?: null,
':lat'=>$_POST['latitude']??'' ?: null,
':lng'=>$_POST['longitude']??'' ?: null,
];
if ($nikEdit) {
$data[':nik'] = $nikEdit;
$pdo->prepare("UPDATE penduduk SET id_keluarga=:kk,nama_lengkap=:nama,jenis_kelamin=:jk,tanggal_lahir=:tgl,
status_keluarga=:sk,status_perkawinan=:sp,status_hidup=:sh,pendidikan_terakhir=:ptk,status_pendidikan=:spd,
pekerjaan=:pkr,penghasilan=:pgn,status_ekonomi=:se,kondisi_kesehatan=:kkh,catatan_kesehatan=:ckkh,
no_telp=:tlp,alamat=:almt,latitude=:lat,longitude=:lng WHERE nik=:nik")->execute($data);
$msg = "Data penduduk $nama diperbarui.";
} else {
$data[':nik'] = $nik;
$pdo->prepare("INSERT INTO penduduk (nik,id_keluarga,nama_lengkap,jenis_kelamin,tanggal_lahir,
status_keluarga,status_perkawinan,status_hidup,pendidikan_terakhir,status_pendidikan,
pekerjaan,penghasilan,status_ekonomi,kondisi_kesehatan,catatan_kesehatan,no_telp,alamat,latitude,longitude)
VALUES(:nik,:kk,:nama,:jk,:tgl,:sk,:sp,:sh,:ptk,:spd,:pkr,:pgn,:se,:kkh,:ckkh,:tlp,:almt,:lat,:lng)")->execute($data);
$msg = "Penduduk $nama berhasil ditambahkan.";
}
logAktivitas($pdo, (int)$_SESSION['user_id'], $nikEdit?'UPDATE':'CREATE', 'penduduk', $nik);
} catch(Exception $e) { $err = $e->getMessage(); }
}
}
$edit = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM penduduk WHERE nik=:nik");
$stmt->execute([':nik'=>$_GET['edit']]);
$edit = $stmt->fetch() ?: null;
}
// List penduduk
$search = trim($_GET['s'] ?? '');
$se = $_GET['se'] ?? '';
$where = []; $params = [];
if ($search) { $where[] = '(p.nama_lengkap LIKE :q OR p.nik LIKE :q2)'; $params[':q']='%'.$search.'%'; $params[':q2']='%'.$search.'%'; }
if ($se) { $where[] = 'p.status_ekonomi=:se'; $params[':se']=$se; }
$whereStr = $where ? 'WHERE '.implode(' AND ',$where) : '';
$rows = $pdo->prepare("
SELECT p.nik,p.nama_lengkap,p.umur,p.jenis_kelamin,p.status_ekonomi,p.kondisi_kesehatan,
p.pekerjaan,p.status_hidup,k.no_kk,ri.nama AS nama_ri,
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan') AS jml_bantuan
FROM penduduk p
LEFT JOIN keluarga k ON k.id=p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id
$whereStr
ORDER BY CASE p.status_ekonomi WHEN 'miskin' THEN 1 WHEN 'rentan' THEN 2 ELSE 3 END, p.nama_lengkap
LIMIT 300
");
$rows->execute($params);
$rows = $rows->fetchAll();
// Keluarga untuk dropdown
$kkList = $pdo->query("SELECT id, no_kk, nama_kk FROM keluarga ORDER BY nama_kk")->fetchAll();
render_header('Manajemen Penduduk');
?>
<div class="page-title">👥 Manajemen Penduduk</div>
<?php if ($msg): ?><div class="notice" style="border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<?php if ($canEdit): ?>
<!-- FORM -->
<div class="card">
<strong><?= $edit ? '✏️ Edit: '.h($edit['nama_lengkap']) : ' Tambah Penduduk' ?></strong>
<form method="POST" style="margin-top:14px">
<input type="hidden" name="nik_edit" value="<?= h($edit['nik']??'') ?>">
<div class="form-grid">
<div class="form-group">
<label>NIK *</label>
<input name="nik" maxlength="16" required value="<?= h($edit['nik']??'') ?>" <?= $edit?'readonly':'' ?>>
</div>
<div class="form-group">
<label>Keluarga (KK)</label>
<select name="id_keluarga">
<option value="">— Pilih KK —</option>
<?php foreach ($kkList as $kk): ?>
<option value="<?= $kk['id'] ?>" <?= ($edit['id_keluarga']??'')==$kk['id']?'selected':'' ?>><?= h($kk['no_kk'].' — '.$kk['nama_kk']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group full">
<label>Nama Lengkap *</label>
<input name="nama_lengkap" required value="<?= h($edit['nama_lengkap']??'') ?>">
</div>
<div class="form-group">
<label>Jenis Kelamin</label>
<select name="jenis_kelamin">
<option value="L" <?= ($edit['jenis_kelamin']??'L')==='L'?'selected':'' ?>>Laki-laki</option>
<option value="P" <?= ($edit['jenis_kelamin']??'')==='P'?'selected':'' ?>>Perempuan</option>
</select>
</div>
<div class="form-group">
<label>Tanggal Lahir *</label>
<input type="date" name="tanggal_lahir" required value="<?= h($edit['tanggal_lahir']??'') ?>" onchange="hitungUmur(this)">
</div>
<div class="form-group">
<label>Status Keluarga</label>
<select name="status_keluarga">
<?php foreach (['kepala_keluarga'=>'Kepala Keluarga','istri'=>'Istri','anak'=>'Anak','menantu'=>'Menantu','orang_tua'=>'Orang Tua','anggota_lain'=>'Anggota Lain'] as $v=>$l): ?>
<option value="<?= $v ?>" <?= ($edit['status_keluarga']??'')===$v?'selected':'' ?>><?= $l ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Status Perkawinan</label>
<select name="status_perkawinan">
<?php foreach (['belum_kawin'=>'Belum Kawin','kawin'=>'Kawin','cerai_hidup'=>'Cerai Hidup','cerai_mati'=>'Cerai Mati'] as $v=>$l): ?>
<option value="<?= $v ?>" <?= ($edit['status_perkawinan']??'')===$v?'selected':'' ?>><?= $l ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Status Hidup</label>
<select name="status_hidup">
<option value="hidup" <?= ($edit['status_hidup']??'hidup')==='hidup'?'selected':'' ?>>Hidup</option>
<option value="meninggal" <?= ($edit['status_hidup']??'')==='meninggal'?'selected':'' ?>>Meninggal</option>
</select>
</div>
<div class="form-group">
<label>Pendidikan Terakhir</label>
<select name="pendidikan_terakhir" onchange="cekPendidikan()">
<?php foreach (['tidak_sekolah','SD','SMP','SMA','SMK','D3','S1','S2','S3'] as $v): ?>
<option value="<?= $v ?>" <?= ($edit['pendidikan_terakhir']??'')===$v?'selected':'' ?>><?= $v ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Status Pendidikan</label>
<select name="status_pendidikan" id="status_pendidikan" onchange="cekPendidikan()">
<option value="sekolah" <?= ($edit['status_pendidikan']??'')==='sekolah'?'selected':'' ?>>Masih Sekolah</option>
<option value="tidak_sekolah" <?= ($edit['status_pendidikan']??'')==='tidak_sekolah'?'selected':'' ?>>Tidak Sekolah</option>
<option value="lulus" <?= ($edit['status_pendidikan']??'')==='lulus'?'selected':'' ?>>Lulus</option>
</select>
</div>
<div class="form-group">
<label>Status Ekonomi</label>
<select name="status_ekonomi">
<option value="miskin" <?= ($edit['status_ekonomi']??'')==='miskin'?'selected':'' ?>>Miskin</option>
<option value="rentan" <?= ($edit['status_ekonomi']??'rentan')==='rentan'?'selected':'' ?>>Rentan</option>
<option value="mampu" <?= ($edit['status_ekonomi']??'')==='mampu'?'selected':'' ?>>Mampu</option>
</select>
</div>
<div class="form-group">
<label>Kondisi Kesehatan</label>
<select name="kondisi_kesehatan">
<?php foreach (['sehat'=>'Sehat','sakit_ringan'=>'Sakit Ringan','sakit_parah'=>'Sakit Parah','disabilitas'=>'Disabilitas'] as $v=>$l): ?>
<option value="<?= $v ?>" <?= ($edit['kondisi_kesehatan']??'sehat')===$v?'selected':'' ?>><?= $l ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Pekerjaan</label>
<input name="pekerjaan" value="<?= h($edit['pekerjaan']??'') ?>">
</div>
<div class="form-group">
<label>Penghasilan / Bulan (Rp)</label>
<input type="number" name="penghasilan" value="<?= h($edit['penghasilan']??0) ?>">
</div>
<div class="form-group full">
<label>Alamat</label>
<input name="alamat" value="<?= h($edit['alamat']??'') ?>">
</div>
<div class="form-group">
<label>Latitude</label>
<input name="latitude" value="<?= h($edit['latitude']??'') ?>">
</div>
<div class="form-group">
<label>Longitude</label>
<input name="longitude" value="<?= h($edit['longitude']??'') ?>">
</div>
</div>
<div id="pendidikan-warn" style="display:none;background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.3);border-radius:8px;padding:8px 12px;font-size:12px;color:#f59e0b;margin-top:10px"></div>
<div style="display:flex;gap:8px;margin-top:14px">
<button type="submit" name="save" value="1" class="btn btn-primary">💾 <?= $edit ? 'Update' : 'Simpan' ?></button>
<?php if ($edit): ?><a href="penduduk.php" class="btn btn-secondary">Batal</a><?php endif; ?>
</div>
</form>
</div>
<?php endif; ?>
<!-- TABEL -->
<div class="card" style="padding:0;overflow:hidden">
<div style="padding:14px 16px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;border-bottom:1px solid var(--brd)">
<form method="GET" style="display:flex;gap:8px;flex:1;min-width:200px">
<input name="s" placeholder="Cari nama / NIK..." value="<?= h($search) ?>" style="flex:1">
<select name="se" style="width:120px">
<option value="">Semua Status</option>
<option value="miskin" <?= $se==='miskin'?'selected':'' ?>>Miskin</option>
<option value="rentan" <?= $se==='rentan'?'selected':'' ?>>Rentan</option>
<option value="mampu" <?= $se==='mampu'?'selected':'' ?>>Mampu</option>
</select>
<button type="submit" class="btn btn-secondary">Cari</button>
<a href="penduduk.php" class="btn btn-secondary">Reset</a>
</form>
<span style="font-size:12px;color:#94a3b8"><?= count($rows) ?> data</span>
</div>
<table>
<thead><tr><th>NIK</th><th>Nama</th><th>Umur/JK</th><th>Status Ekonomi</th><th>Kesehatan</th><th>Bantuan</th><th>Rumah Ibadah</th><?php if($canEdit): ?><th>Aksi</th><?php endif; ?></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td style="font-size:11px"><?= h($r['nik']) ?></td>
<td><strong><?= h($r['nama_lengkap']) ?></strong><?= $r['status_hidup']==='meninggal'?'<br><span style="font-size:10px;color:#ef4444">†meninggal</span>':'' ?></td>
<td><?= $r['umur'] ?>th / <?= $r['jenis_kelamin']==='L'?'♂':'♀' ?></td>
<td><span class="tag <?= $r['status_ekonomi'] ?>"><?= h($r['status_ekonomi']) ?></span></td>
<td style="font-size:11px"><?= h(str_replace('_',' ',$r['kondisi_kesehatan'])) ?></td>
<td><?= $r['jml_bantuan'] > 0 ? "<span style='color:#22c55e'>✅ {$r['jml_bantuan']}x</span>" : "<span style='color:#ef4444'>⚠️ Belum</span>" ?></td>
<td style="font-size:11px"><?= h($r['nama_ri'] ?? '-') ?></td>
<?php if($canEdit): ?>
<td class="actions">
<a href="penduduk.php?edit=<?= h($r['nik']) ?>" class="btn btn-secondary btn-sm">Edit</a>
<a href="bantuan.php?nik=<?= h($r['nik']) ?>&nama=<?= urlencode($r['nama_lengkap']) ?>" class="btn btn-success btn-sm">Bantu</a>
<?php if ($_SESSION['role']==='admin'): ?>
<a href="penduduk.php?delete=<?= h($r['nik']) ?>" class="btn btn-danger btn-sm" onclick="return confirm('Hapus <?= h(addslashes($r['nama_lengkap'])) ?>?')">Hapus</a>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="8" style="text-align:center;padding:24px;color:#94a3b8">Tidak ada data</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<script>
function hitungUmur(el) {
const tgl = el.value;
if (!tgl) return;
const umur = Math.floor((new Date() - new Date(tgl)) / (365.25*24*3600*1000));
cekPendidikanUmur(umur);
}
function cekPendidikan() {
const tgl = document.querySelector('[name=tanggal_lahir]').value;
if (!tgl) return;
const umur = Math.floor((new Date() - new Date(tgl)) / (365.25*24*3600*1000));
cekPendidikanUmur(umur);
}
function cekPendidikanUmur(umur) {
const status = document.getElementById('status_pendidikan').value;
const warn = document.getElementById('pendidikan-warn');
let msg = '';
if (umur>=7&&umur<=12&&status!=='sekolah') msg = `⚠️ Anak usia ${umur} tahun seharusnya masih sekolah SD.`;
if (umur>=13&&umur<=15&&status!=='sekolah') msg = `⚠️ Anak usia ${umur} tahun seharusnya masih sekolah SMP.`;
if (umur>=16&&umur<=18&&status!=='sekolah') msg = `⚠️ Anak usia ${umur} tahun seharusnya masih sekolah SMA/SMK.`;
warn.style.display = msg ? 'block' : 'none';
warn.textContent = msg;
}
</script>
<?php render_footer(); ?>
+141
View File
@@ -0,0 +1,141 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin','pengurus','pimpinan']);
$role = $_SESSION['role'];
$canEdit = in_array($role, ['admin','pengurus']);
$msg = ''; $err = '';
// DELETE
if ($canEdit && isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
try {
$pdo->prepare("DELETE FROM rumah_ibadah WHERE id=:id")->execute([':id'=>$id]);
logAktivitas($pdo, (int)$_SESSION['user_id'], 'DELETE', 'rumah_ibadah', (string)$id);
$msg = "Rumah ibadah #$id dihapus.";
} catch(Exception $e) { $err = 'Gagal hapus: '.$e->getMessage(); }
}
// SAVE
if ($canEdit && isset($_POST['save'])) {
$nama = trim($_POST['nama'] ?? '');
$lat = $_POST['latitude'] ?? '';
$lng = $_POST['longitude'] ?? '';
$idEdit = (int)($_POST['id_edit'] ?? 0);
if (strlen($nama) < 3) $err = 'Nama terlalu pendek.';
elseif (!is_numeric($lat) || !is_numeric($lng)) $err = 'Latitude/Longitude tidak valid.';
else {
try {
$data = [
':nama' => $nama,
':jenis' => $_POST['jenis']??'Lainnya',
':kontak'=> $_POST['kontak']??'' ?: null,
':rad' => (float)($_POST['radius_meter']??300),
':lat' => (float)$lat, ':lng' => (float)$lng,
':almt' => $_POST['alamat']??'' ?: null,
':kel' => $_POST['kelurahan']??'' ?: null,
':kec' => $_POST['kecamatan']??'' ?: null,
':kota' => $_POST['kota']??'' ?: null,
':desk' => $_POST['deskripsi']??'' ?: null,
];
if ($idEdit) {
$data[':id'] = $idEdit;
$pdo->prepare("UPDATE rumah_ibadah SET nama=:nama,jenis=:jenis,kontak=:kontak,radius_meter=:rad,
latitude=:lat,longitude=:lng,alamat=:almt,kelurahan=:kel,kecamatan=:kec,kota=:kota,deskripsi=:desk
WHERE id=:id")->execute($data);
$msg = "$nama diperbarui.";
logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'rumah_ibadah', (string)$idEdit);
} else {
$pdo->prepare("INSERT INTO rumah_ibadah (nama,jenis,kontak,radius_meter,latitude,longitude,alamat,kelurahan,kecamatan,kota,deskripsi)
VALUES(:nama,:jenis,:kontak,:rad,:lat,:lng,:almt,:kel,:kec,:kota,:desk)")->execute($data);
$msg = "$nama berhasil ditambahkan.";
logAktivitas($pdo, (int)$_SESSION['user_id'], 'CREATE', 'rumah_ibadah', (string)$pdo->lastInsertId());
}
} catch(Exception $e) { $err = $e->getMessage(); }
}
}
$edit = null;
if (isset($_GET['edit'])) {
$s = $pdo->prepare("SELECT * FROM rumah_ibadah WHERE id=:id");
$s->execute([':id'=>(int)$_GET['edit']]);
$edit = $s->fetch() ?: null;
}
$rows = $pdo->query("
SELECT ri.*,
(SELECT COUNT(*) FROM keluarga k WHERE k.rumah_ibadah_id=ri.id) AS jml_kk
FROM rumah_ibadah ri ORDER BY ri.nama
")->fetchAll();
$jenisOpts = ['Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya'];
render_header('Rumah Ibadah');
?>
<div class="page-title">🕌 Data Rumah Ibadah</div>
<?php if ($msg): ?><div class="notice" style="border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<div style="display:grid;grid-template-columns:<?= $canEdit ? '1fr 1.5fr' : '1fr' ?>;gap:16px">
<?php if ($canEdit): ?>
<div class="card">
<strong><?= $edit ? '✏️ Edit' : ' Tambah Rumah Ibadah' ?></strong><br><br>
<form method="POST">
<?php if ($edit): ?><input type="hidden" name="id_edit" value="<?= $edit['id'] ?>"><?php endif; ?>
<div class="form-grid">
<div class="form-group full"><label>Nama *</label>
<input name="nama" required value="<?= h($edit['nama'] ?? '') ?>"></div>
<div class="form-group"><label>Jenis</label>
<select name="jenis">
<?php foreach ($jenisOpts as $j): ?>
<option value="<?= $j ?>" <?= ($edit['jenis']??'Lainnya')===$j?'selected':'' ?>><?= $j ?></option>
<?php endforeach; ?>
</select></div>
<div class="form-group"><label>Kontak / No Telp</label>
<input name="kontak" value="<?= h($edit['kontak'] ?? '') ?>"></div>
<div class="form-group"><label>Radius Layanan (meter)</label>
<input name="radius_meter" type="number" min="50" max="5000" value="<?= $edit['radius_meter'] ?? 300 ?>"></div>
<div class="form-group"><label>Latitude *</label>
<input name="latitude" type="number" step="any" required value="<?= $edit['latitude'] ?? '' ?>"></div>
<div class="form-group"><label>Longitude *</label>
<input name="longitude" type="number" step="any" required value="<?= $edit['longitude'] ?? '' ?>"></div>
<div class="form-group full"><label>Alamat</label>
<textarea name="alamat" rows="2"><?= h($edit['alamat'] ?? '') ?></textarea></div>
<div class="form-group"><label>Kelurahan</label>
<input name="kelurahan" value="<?= h($edit['kelurahan'] ?? '') ?>"></div>
<div class="form-group"><label>Kecamatan</label>
<input name="kecamatan" value="<?= h($edit['kecamatan'] ?? '') ?>"></div>
</div>
<br>
<button type="submit" name="save" class="btn btn-primary"><?= $edit ? '💾 Update' : ' Simpan' ?></button>
<?php if ($edit): ?><a href="rumah_ibadah.php" class="btn btn-secondary">Batal</a><?php endif; ?>
</form>
</div>
<?php endif; ?>
<div class="card" style="padding:0;overflow:hidden">
<table>
<thead><tr><th>Nama</th><th>Jenis</th><th>Alamat</th><th>Radius</th><th>KK</th><?= $canEdit?'<th>Aksi</th>':'' ?></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><strong><?= h($r['nama']) ?></strong><br><span style="font-size:10px;color:#94a3b8"><?= h($r['kontak']??'') ?></span></td>
<td style="font-size:12px"><?= h($r['jenis']) ?></td>
<td style="font-size:11px"><?= h($r['kelurahan']??'') ?><?= $r['kecamatan'] ? ', '.$r['kecamatan'] : '' ?></td>
<td><?= $r['radius_meter'] ?>m</td>
<td><?= $r['jml_kk'] ?> KK</td>
<?php if ($canEdit): ?>
<td class="actions">
<a href="?edit=<?= $r['id'] ?>" class="btn btn-secondary btn-sm">✏️</a>
<a href="?delete=<?= $r['id'] ?>" class="btn btn-danger btn-sm" onclick="return confirm('Hapus rumah ibadah ini?')">🗑️</a>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="6" style="text-align:center;padding:24px;color:#94a3b8">Belum ada data</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php render_footer(); ?>
+163
View File
@@ -0,0 +1,163 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login(['admin']);
$msg = ''; $err = '';
// TOGGLE AKTIF
if (isset($_GET['toggle'])) {
$id = (int)$_GET['toggle'];
if ($id !== (int)$_SESSION['user_id']) {
try {
$pdo->prepare("UPDATE users SET aktif = 1 - aktif WHERE id=:id")->execute([':id'=>$id]);
logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'users', (string)$id);
$msg = "Status user #$id diperbarui.";
} catch(Exception $e) { $err = $e->getMessage(); }
} else { $err = 'Tidak dapat mengubah status akun sendiri.'; }
}
// RESET PASSWORD
if (isset($_POST['reset_pw'])) {
$id = (int)($_POST['user_id'] ?? 0);
$pw = $_POST['new_password'] ?? '';
if (strlen($pw) < 6) $err = 'Password minimal 6 karakter.';
else {
$hash = password_hash($pw, PASSWORD_BCRYPT);
$pdo->prepare("UPDATE users SET password_hash=:h WHERE id=:id")->execute([':h'=>$hash,':id'=>$id]);
logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'users', (string)$id, null, ['action'=>'reset_password']);
$msg = "Password user #$id berhasil direset.";
}
}
// SAVE USER BARU
if (isset($_POST['save'])) {
$uname = trim($_POST['username'] ?? '');
$nama = trim($_POST['nama'] ?? '');
$pw = $_POST['password'] ?? '';
$idEdit = (int)($_POST['id_edit'] ?? 0);
if (strlen($uname) < 3) $err = 'Username minimal 3 karakter.';
elseif (strlen($nama) < 3) $err = 'Nama minimal 3 karakter.';
elseif (!$idEdit && strlen($pw) < 6) $err = 'Password minimal 6 karakter.';
else {
try {
if ($idEdit) {
$pdo->prepare("UPDATE users SET nama=:nama,role_id=:rid,email=:em,no_telp=:tlp,rumah_ibadah_id=:ri WHERE id=:id")
->execute([':nama'=>$nama,':rid'=>(int)$_POST['role_id'],':em'=>$_POST['email']??'' ?: null,
':tlp'=>$_POST['no_telp']??'' ?: null,':ri'=>$_POST['rumah_ibadah_id']??'' ?: null,':id'=>$idEdit]);
$msg = "User $nama diperbarui.";
logAktivitas($pdo, (int)$_SESSION['user_id'], 'UPDATE', 'users', (string)$idEdit);
} else {
$hash = password_hash($pw, PASSWORD_BCRYPT);
$pdo->prepare("INSERT INTO users (role_id,nama,username,email,password_hash,no_telp,rumah_ibadah_id)
VALUES(:rid,:nama,:uname,:em,:hash,:tlp,:ri)")
->execute([':rid'=>(int)$_POST['role_id'],':nama'=>$nama,':uname'=>$uname,
':em'=>$_POST['email']??'' ?: null,':hash'=>$hash,
':tlp'=>$_POST['no_telp']??'' ?: null,':ri'=>$_POST['rumah_ibadah_id']??'' ?: null]);
$msg = "User $uname berhasil dibuat.";
logAktivitas($pdo, (int)$_SESSION['user_id'], 'CREATE', 'users', (string)$pdo->lastInsertId());
}
} catch(Exception $e) { $err = $e->getMessage(); }
}
}
$edit = null;
if (isset($_GET['edit'])) {
$s = $pdo->prepare("SELECT * FROM users WHERE id=:id");
$s->execute([':id'=>(int)$_GET['edit']]);
$edit = $s->fetch() ?: null;
}
$rows = $pdo->query("
SELECT u.*, r.nama AS role_nama, ri.nama AS nama_ri
FROM users u
JOIN roles r ON r.id=u.role_id
LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id
ORDER BY u.role_id, u.nama
")->fetchAll();
$roles = $pdo->query("SELECT * FROM roles ORDER BY id")->fetchAll();
$riList = $pdo->query("SELECT id,nama FROM rumah_ibadah ORDER BY nama")->fetchAll();
render_header('Manajemen User');
?>
<div class="page-title">👥 Manajemen User</div>
<?php if ($msg): ?><div class="notice" style="border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<div style="display:grid;grid-template-columns:1fr 1.8fr;gap:16px">
<div>
<div class="card">
<strong><?= $edit ? '✏️ Edit User' : ' Tambah User' ?></strong><br><br>
<form method="POST">
<?php if ($edit): ?><input type="hidden" name="id_edit" value="<?= $edit['id'] ?>"><?php endif; ?>
<div class="form-group" style="margin-bottom:10px"><label>Username <?= $edit ? '' : '*' ?></label>
<input name="username" <?= $edit ? 'disabled value="'.h($edit['username'] ?? '').'"' : 'required' ?>></div>
<div class="form-group" style="margin-bottom:10px"><label>Nama Lengkap *</label>
<input name="nama" required value="<?= h($edit['nama'] ?? '') ?>"></div>
<?php if (!$edit): ?>
<div class="form-group" style="margin-bottom:10px"><label>Password * (min 6 karakter)</label>
<input name="password" type="password" required minlength="6"></div>
<?php endif; ?>
<div class="form-group" style="margin-bottom:10px"><label>Role</label>
<select name="role_id">
<?php foreach ($roles as $r): ?>
<option value="<?= $r['id'] ?>" <?= ($edit['role_id']??4)==$r['id']?'selected':'' ?>><?= h($r['label']) ?></option>
<?php endforeach; ?>
</select></div>
<div class="form-group" style="margin-bottom:10px"><label>Email</label>
<input name="email" type="email" value="<?= h($edit['email'] ?? '') ?>"></div>
<div class="form-group" style="margin-bottom:10px"><label>No Telp</label>
<input name="no_telp" value="<?= h($edit['no_telp'] ?? '') ?>"></div>
<div class="form-group" style="margin-bottom:10px"><label>Rumah Ibadah (untuk Pengurus)</label>
<select name="rumah_ibadah_id">
<option value="">— Tidak ada —</option>
<?php foreach ($riList as $ri): ?>
<option value="<?= $ri['id'] ?>" <?= ($edit['rumah_ibadah_id']??'')==$ri['id']?'selected':'' ?>><?= h($ri['nama']) ?></option>
<?php endforeach; ?>
</select></div>
<br>
<button type="submit" name="save" class="btn btn-primary"><?= $edit ? '💾 Update' : ' Buat' ?></button>
<?php if ($edit): ?><a href="users.php" class="btn btn-secondary">Batal</a><?php endif; ?>
</form>
</div>
<?php if ($edit): ?>
<div class="card" style="margin-top:0">
<strong>🔑 Reset Password</strong><br><br>
<form method="POST">
<input type="hidden" name="user_id" value="<?= $edit['id'] ?>">
<div class="form-group" style="margin-bottom:10px"><label>Password Baru *</label>
<input name="new_password" type="password" required minlength="6" placeholder="min 6 karakter"></div>
<button type="submit" name="reset_pw" class="btn btn-danger">🔑 Reset</button>
</form>
</div>
<?php endif; ?>
</div>
<div class="card" style="padding:0;overflow:hidden">
<table>
<thead><tr><th>Nama</th><th>Username</th><th>Role</th><th>Rumah Ibadah</th><th>Status</th><th>Aksi</th></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr <?= !$r['aktif'] ? 'style="opacity:.5"' : '' ?>>
<td><strong><?= h($r['nama']) ?></strong><br><span style="font-size:10px;color:#94a3b8"><?= h($r['email']??'') ?></span></td>
<td style="font-size:12px"><?= h($r['username']) ?></td>
<td><span class="tag <?= $r['role_nama']==='admin'?'darurat':($r['role_nama']==='pengurus'?'diproses':'pending') ?>"><?= h($r['role_nama']) ?></span></td>
<td style="font-size:11px"><?= h($r['nama_ri'] ?? '-') ?></td>
<td><?= $r['aktif'] ? '<span class="tag selesai">Aktif</span>' : '<span class="tag darurat">Nonaktif</span>' ?></td>
<td class="actions">
<a href="?edit=<?= $r['id'] ?>" class="btn btn-secondary btn-sm">✏️</a>
<?php if ($r['id'] != $_SESSION['user_id']): ?>
<a href="?toggle=<?= $r['id'] ?>" class="btn <?= $r['aktif']?'btn-danger':'btn-success' ?> btn-sm" onclick="return confirm('Ubah status user ini?')">
<?= $r['aktif'] ? '🚫' : '✅' ?>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php render_footer(); ?>
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
<?php
// ============================================================
// API: Autentikasi
// POST ?action=login
// POST ?action=logout
// GET ?action=me
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
if ($method === 'POST' && $action === 'login') {
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$username = sanitizeStr($body['username'] ?? '');
$password = $body['password'] ?? '';
if (!$username || !$password) {
jsonResponse(false, 'Username dan password wajib diisi.', [], 400);
}
$result = doLogin($pdo, $username, $password);
if (!$result['success']) {
jsonResponse(false, $result['message'], [], 401);
}
jsonResponse(true, 'Login berhasil.', [
'role' => $result['role'],
'user_id' => $_SESSION['user_id'],
'nama' => $_SESSION['user_nama'],
]);
}
if ($method === 'POST' && $action === 'logout') {
doLogout($pdo);
jsonResponse(true, 'Logout berhasil.');
}
if ($method === 'GET' && $action === 'me') {
$user = getApiUser($pdo);
if (!$user) jsonResponse(false, 'Belum login.', [], 401);
$detail = $pdo->prepare("SELECT u.id, u.nama, u.username, u.email, r.nama AS role, u.rumah_ibadah_id, ri.nama AS nama_ri FROM users u JOIN roles r ON r.id=u.role_id LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id WHERE u.id=:id");
$detail->execute([':id' => $user['id']]);
jsonResponse(true, 'OK', ['data' => $detail->fetch()]);
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
+239
View File
@@ -0,0 +1,239 @@
<?php
// ============================================================
// API: Bantuan & Histori Bantuan
// GET ?action=jenis|histori&nik=...
// POST action=salurkan
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
// ============================================================
// GET
// ============================================================
if ($method === 'GET') {
if ($action === 'jenis') {
$rows = $pdo->query("SELECT * FROM jenis_bantuan WHERE aktif=1 ORDER BY kategori, nama")->fetchAll();
jsonResponse(true, 'OK', ['data' => $rows]);
}
if ($action === 'histori') {
$nik = sanitizeStr($_GET['nik'] ?? '');
if ($nik && validateNIK($nik)) {
// Histori per penduduk
$stmt = $pdo->prepare("
SELECT hb.*, jb.nama AS nama_bantuan, jb.kategori, jb.satuan,
ri.nama AS nama_rumah_ibadah, u.nama AS disalurkan_oleh_nama
FROM histori_bantuan hb
JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan
LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id
LEFT JOIN users u ON u.id = hb.disalurkan_oleh
WHERE hb.nik = :nik
ORDER BY hb.tanggal DESC
");
$stmt->execute([':nik' => $nik]);
$data = $stmt->fetchAll();
// Ringkasan
$summary = [
'total_bantuan' => count($data),
'kategori' => [],
];
foreach ($data as $row) {
$summary['kategori'][$row['kategori']] = ($summary['kategori'][$row['kategori']] ?? 0) + 1;
}
jsonResponse(true, 'OK', ['data' => $data, 'summary' => $summary]);
}
// List semua histori (admin/pengurus)
if (!canViewAll($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$where = [];
$params = [];
if (!empty($_GET['bulan'])) { $where[] = 'MONTH(hb.tanggal) = :bln'; $params[':bln'] = (int)$_GET['bulan']; }
if (!empty($_GET['tahun'])) { $where[] = 'YEAR(hb.tanggal) = :thn'; $params[':thn'] = (int)$_GET['tahun']; }
if (!empty($_GET['kategori'])) { $where[] = 'jb.kategori = :kat'; $params[':kat'] = $_GET['kategori']; }
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$stmt = $pdo->prepare("
SELECT hb.id, hb.nik, p.nama_lengkap, hb.tanggal,
jb.nama AS nama_bantuan, jb.kategori,
hb.jumlah, hb.nilai_rupiah, hb.status_penyaluran,
ri.nama AS nama_ri
FROM histori_bantuan hb
JOIN penduduk p ON p.nik = hb.nik
JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan
LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id
$whereStr
ORDER BY hb.tanggal DESC
LIMIT 1000
");
$stmt->execute($params);
jsonResponse(true, 'OK', ['data' => $stmt->fetchAll()]);
}
if ($action === 'belum_dibantu') {
// Warga miskin yang BELUM pernah menerima bantuan
$rows = $pdo->query("
SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan,
p.pekerjaan, p.alamat, p.latitude, p.longitude,
k.nama_kk, k.no_kk, ri.nama AS nama_ri
FROM penduduk p
LEFT JOIN keluarga k ON k.id = p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id
WHERE p.status_hidup = 'hidup'
AND p.status_ekonomi IN ('miskin', 'rentan')
AND NOT EXISTS (
SELECT 1 FROM histori_bantuan hb
WHERE hb.nik = p.nik AND hb.status_penyaluran = 'disalurkan'
)
ORDER BY
CASE p.status_ekonomi WHEN 'miskin' THEN 1 ELSE 2 END,
CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 WHEN 'disabilitas' THEN 2 ELSE 3 END
")->fetchAll();
jsonResponse(true, 'OK', ['data' => $rows, 'total' => count($rows)]);
}
if ($action === 'dashboard_stats') {
// Statistik untuk dashboard
$stats = $pdo->query("
SELECT
(SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='miskin' AND status_hidup='hidup') AS total_miskin,
(SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='rentan' AND status_hidup='hidup') AS total_rentan,
(SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='mampu' AND status_hidup='hidup') AS total_mampu,
(SELECT COUNT(*) FROM keluarga) AS total_keluarga,
(SELECT COUNT(*) FROM rumah_ibadah) AS total_rumah_ibadah,
(SELECT COUNT(*) FROM laporan WHERE status='pending') AS laporan_pending,
(SELECT COUNT(*) FROM laporan WHERE status='diproses') AS laporan_diproses,
(SELECT COUNT(*) FROM histori_bantuan WHERE MONTH(tanggal)=MONTH(CURDATE()) AND status_penyaluran='disalurkan') AS bantuan_bulan_ini,
(SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup' AND status_ekonomi IN ('miskin','rentan')
AND NOT EXISTS (SELECT 1 FROM histori_bantuan hb WHERE hb.nik=penduduk.nik AND hb.status_penyaluran='disalurkan')
) AS belum_pernah_dibantu
")->fetch();
// Bantuan per kategori bulan ini
$per_kategori = $pdo->query("
SELECT jb.kategori, COUNT(*) AS jumlah
FROM histori_bantuan hb
JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan
WHERE MONTH(hb.tanggal) = MONTH(CURDATE())
AND hb.status_penyaluran = 'disalurkan'
GROUP BY jb.kategori
")->fetchAll();
jsonResponse(true, 'OK', ['data' => $stats, 'per_kategori' => $per_kategori]);
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
// ============================================================
// POST: salurkan bantuan
// ============================================================
if ($method === 'POST' && $action === 'salurkan') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$nik = sanitizeStr($body['nik'] ?? '');
$idJenis = (int)($body['id_jenis_bantuan'] ?? 0);
$tanggal = sanitizeStr($body['tanggal'] ?? date('Y-m-d'));
$jumlah = sanitizeStr($body['jumlah'] ?? '');
$status = sanitizeStr($body['status_penyaluran'] ?? 'dijadwalkan');
if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400);
if ($idJenis <= 0) jsonResponse(false, 'Jenis bantuan harus dipilih.', [], 400);
if (!strtotime($tanggal)) jsonResponse(false, 'Format tanggal tidak valid.', [], 400);
// Cek penduduk ada
$cek = $pdo->prepare("SELECT nik, id_keluarga FROM penduduk WHERE nik = :nik AND status_hidup='hidup'");
$cek->execute([':nik' => $nik]);
$pddk = $cek->fetch();
if (!$pddk) jsonResponse(false, 'Penduduk tidak ditemukan atau sudah meninggal.', [], 404);
// Cek jenis bantuan ada
$cekJb = $pdo->prepare("SELECT id FROM jenis_bantuan WHERE id = :id AND aktif=1");
$cekJb->execute([':id' => $idJenis]);
if (!$cekJb->fetch()) jsonResponse(false, 'Jenis bantuan tidak ditemukan.', [], 404);
$buktiNama = null;
if (!empty($_FILES['bukti']['name'])) {
try { $buktiNama = uploadFile($_FILES['bukti'], UPLOAD_BUKTI); }
catch (RuntimeException $e) { jsonResponse(false, $e->getMessage(), [], 400); }
}
$stmt = $pdo->prepare("
INSERT INTO histori_bantuan
(nik, id_jenis_bantuan, id_keluarga, rumah_ibadah_id, tanggal,
jumlah, nilai_rupiah, status_penyaluran, catatan, bukti_file, disalurkan_oleh)
VALUES
(:nik,:jb,:kk,:ri,:tgl,:jml,:rp,:status,:cat,:bukti,:user)
");
$stmt->execute([
':nik' => $nik,
':jb' => $idJenis,
':kk' => $pddk['id_keluarga'],
':ri' => $currentUser['rumah_ibadah_id'],
':tgl' => $tanggal,
':jml' => $jumlah ?: null,
':rp' => !empty($body['nilai_rupiah']) ? (float)$body['nilai_rupiah'] : null,
':status' => in_array($status, ['dijadwalkan','disalurkan','dibatalkan']) ? $status : 'dijadwalkan',
':cat' => sanitizeStr($body['catatan'] ?? '') ?: null,
':bukti' => $buktiNama,
':user' => $currentUser['id'],
]);
$hbId = (int)$pdo->lastInsertId();
logAktivitas($pdo, $currentUser['id'], 'CREATE', 'histori_bantuan', (string)$hbId, null, $body);
// Notifikasi ke pimpinan
$pimpinan = $pdo->query("SELECT id FROM users WHERE role_id=3")->fetchAll();
foreach ($pimpinan as $pm) {
$pdo->prepare("INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id) VALUES (:u,:j,:i,'bantuan',:r)")
->execute([':u' => $pm['id'], ':j' => 'Bantuan Disalurkan', ':i' => "Bantuan untuk NIK $nik telah dicatat.", ':r' => $hbId]);
}
jsonResponse(true, 'Bantuan berhasil dicatat.', ['id' => $hbId], 201);
}
// ============================================================
// PUT: update status penyaluran
// ============================================================
if ($method === 'PUT') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$status = sanitizeStr($body['status_penyaluran'] ?? '');
if (!in_array($status, ['dijadwalkan','disalurkan','dibatalkan'])) {
jsonResponse(false, 'Status tidak valid.', [], 400);
}
$old = $pdo->prepare("SELECT * FROM histori_bantuan WHERE id = :id");
$old->execute([':id' => $id]);
if (!$old->fetch()) jsonResponse(false, 'Data tidak ditemukan.', [], 404);
$pdo->prepare("UPDATE histori_bantuan SET status_penyaluran=:s WHERE id=:id")
->execute([':s' => $status, ':id' => $id]);
logAktivitas($pdo, $currentUser['id'], 'UPDATE', 'histori_bantuan', (string)$id);
jsonResponse(true, 'Status penyaluran diperbarui.', ['id' => $id]);
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+183
View File
@@ -0,0 +1,183 @@
<?php
// ============================================================
// API: Keluarga
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
if ($method === 'GET') {
if ($action === 'list') {
$where = [];
$params = [];
if (!empty($_GET['status_ekonomi'])) {
$where[] = 'k.status_ekonomi = :se';
$params[':se'] = $_GET['status_ekonomi'];
}
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$rows = $pdo->prepare("
SELECT k.*,
ri.nama AS nama_ri, ri.jenis AS jenis_ri,
COUNT(p.nik) AS jumlah_anggota_db,
SUM(CASE WHEN p.kondisi_kesehatan IN ('sakit_parah','disabilitas') THEN 1 ELSE 0 END) AS anggota_sakit
FROM keluarga k
LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id
LEFT JOIN penduduk p ON p.id_keluarga = k.id AND p.status_hidup='hidup'
$whereStr
GROUP BY k.id
ORDER BY CASE k.status_ekonomi WHEN 'miskin' THEN 1 WHEN 'rentan' THEN 2 ELSE 3 END ASC
");
$rows->execute($params);
jsonResponse(true, 'OK', ['data' => $rows->fetchAll()]);
}
if ($action === 'detail') {
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare("SELECT k.*, ri.nama AS nama_ri FROM keluarga k LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id WHERE k.id=:id");
$stmt->execute([':id' => $id]);
$kk = $stmt->fetch();
if (!$kk) jsonResponse(false, 'Keluarga tidak ditemukan.', [], 404);
$anggota = $pdo->prepare("SELECT * FROM penduduk WHERE id_keluarga=:id ORDER BY CASE status_keluarga WHEN 'kepala_keluarga' THEN 1 ELSE 2 END, tanggal_lahir");
$anggota->execute([':id' => $id]);
$histori = $pdo->prepare("
SELECT hb.tanggal, jb.nama AS bantuan, hb.jumlah, hb.status_penyaluran, p.nama_lengkap
FROM histori_bantuan hb
JOIN penduduk p ON p.nik=hb.nik
JOIN jenis_bantuan jb ON jb.id=hb.id_jenis_bantuan
WHERE hb.id_keluarga=:id
ORDER BY hb.tanggal DESC
LIMIT 50
");
$histori->execute([':id' => $id]);
jsonResponse(true, 'OK', ['data' => $kk, 'anggota' => $anggota->fetchAll(), 'histori' => $histori->fetchAll()]);
}
if ($action === 'geojson') {
$stmt = $pdo->query("
SELECT k.id, k.no_kk, k.nama_kk, k.status_ekonomi, k.latitude, k.longitude,
COUNT(p.nik) AS jml_anggota, ri.nama AS nama_ri
FROM keluarga k
LEFT JOIN penduduk p ON p.id_keluarga=k.id AND p.status_hidup='hidup'
LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id
WHERE k.latitude IS NOT NULL AND k.longitude IS NOT NULL
GROUP BY k.id
");
$rows = $stmt->fetchAll();
$features = [];
foreach ($rows as $r) {
$features[] = [
'type' => 'Feature',
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['longitude'],(float)$r['latitude']]],
'properties' => array_merge($r, [
'color' => match($r['status_ekonomi']){'miskin'=>'#ef4444','rentan'=>'#f59e0b',default=>'#22c55e'},
'layer' => 'keluarga'
])
];
}
header('Content-Type: application/json');
echo json_encode(['type'=>'FeatureCollection','features'=>$features], JSON_UNESCAPED_UNICODE);
exit;
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
if ($method === 'POST') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$noKk = sanitizeStr($body['no_kk'] ?? '');
$namaKk = sanitizeStr($body['nama_kk'] ?? '');
if (strlen($noKk) !== 16) jsonResponse(false, 'No KK harus 16 digit.', [], 400);
if (strlen($namaKk) < 3) jsonResponse(false, 'Nama KK terlalu pendek.', [], 400);
$cek = $pdo->prepare("SELECT id FROM keluarga WHERE no_kk=:nkk");
$cek->execute([':nkk' => $noKk]);
if ($cek->fetch()) jsonResponse(false, 'No KK sudah terdaftar.', [], 409);
$stmt = $pdo->prepare("
INSERT INTO keluarga (no_kk, nama_kk, alamat, kelurahan, kecamatan, kota, latitude, longitude, status_ekonomi, rumah_ibadah_id)
VALUES (:nkk,:nkk_nama,:almt,:kel,:kec,:kota,:lat,:lng,:se,:ri)
");
$stmt->execute([
':nkk' => $noKk,
':nkk_nama' => $namaKk,
':almt' => sanitizeStr($body['alamat'] ?? '') ?: null,
':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null,
':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null,
':kota' => sanitizeStr($body['kota'] ?? '') ?: null,
':lat' => !empty($body['latitude']) ? (float)$body['latitude'] : null,
':lng' => !empty($body['longitude']) ? (float)$body['longitude'] : null,
':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'),
':ri' => !empty($body['rumah_ibadah_id']) ? (int)$body['rumah_ibadah_id'] : null,
]);
$newId = (int)$pdo->lastInsertId();
logAktivitas($pdo, $currentUser['id']??null, 'CREATE', 'keluarga', (string)$newId, null, $body);
jsonResponse(true, 'Keluarga berhasil ditambahkan.', ['id' => $newId], 201);
}
if ($method === 'PUT') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
if ($id <= 0) jsonResponse(false, 'ID tidak valid.', [], 400);
if ($action === 'update_koordinat') {
$lat = (float)($body['latitude'] ?? 0);
$lng = (float)($body['longitude'] ?? 0);
if (!$lat || !$lng) jsonResponse(false, 'Koordinat tidak valid.', [], 400);
$pdo->prepare("UPDATE keluarga SET latitude=:lat, longitude=:lng WHERE id=:id")
->execute([':lat'=>$lat, ':lng'=>$lng, ':id'=>$id]);
logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'keluarga', (string)$id, null, ['lat'=>$lat,'lng'=>$lng]);
jsonResponse(true, 'Koordinat diperbarui.', ['id' => $id]);
}
$pdo->prepare("
UPDATE keluarga SET
nama_kk=:nkk, alamat=:almt, kelurahan=:kel, kecamatan=:kec,
kota=:kota, latitude=:lat, longitude=:lng,
status_ekonomi=:se, rumah_ibadah_id=:ri
WHERE id=:id
")->execute([
':nkk' => sanitizeStr($body['nama_kk'] ?? ''),
':almt' => sanitizeStr($body['alamat'] ?? '') ?: null,
':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null,
':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null,
':kota' => sanitizeStr($body['kota'] ?? '') ?: null,
':lat' => !empty($body['latitude']) ? (float)$body['latitude'] : null,
':lng' => !empty($body['longitude']) ? (float)$body['longitude'] : null,
':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'),
':ri' => !empty($body['rumah_ibadah_id']) ? (int)$body['rumah_ibadah_id'] : null,
':id' => $id,
]);
logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'keluarga', (string)$id);
jsonResponse(true, 'Keluarga diperbarui.', ['id' => $id]);
}
if ($method === 'DELETE') {
if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin yang bisa menghapus keluarga.', [], 403);
$id = (int)($_GET['id'] ?? 0);
// Cek apakah masih ada anggota
$jml = (int)$pdo->prepare("SELECT COUNT(*) FROM penduduk WHERE id_keluarga=:id")->execute([':id'=>$id]);
if ($jml > 0) jsonResponse(false, 'Tidak bisa dihapus: masih ada anggota keluarga.', [], 409);
$pdo->prepare("DELETE FROM keluarga WHERE id=:id")->execute([':id' => $id]);
logAktivitas($pdo, $currentUser['id']??null, 'DELETE', 'keluarga', (string)$id);
jsonResponse(true, 'Keluarga dihapus.');
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+255
View File
@@ -0,0 +1,255 @@
<?php
// ============================================================
// API: Laporan Masyarakat
// GET ?action=list|detail&id=...
// POST action=create (multipart untuk foto)
// PUT action=update_status&id=...
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
// ============================================================
// GET: list laporan
// ============================================================
if ($method === 'GET') {
if ($action === 'list') {
$where = [];
$params = [];
if (!empty($_GET['status'])) {
$where[] = 'l.status = :status';
$params[':status'] = $_GET['status'];
}
if (!empty($_GET['urgensi'])) {
$where[] = 'l.tingkat_urgensi = :urgensi';
$params[':urgensi'] = $_GET['urgensi'];
}
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$rows = $pdo->prepare("
SELECT l.*,
p.nama_lengkap AS nama_terdampak,
u.nama AS nama_verifikator
FROM laporan l
LEFT JOIN penduduk p ON p.nik = l.nik_terdampak
LEFT JOIN users u ON u.id = l.diverifikasi_oleh
$whereStr
ORDER BY
CASE l.tingkat_urgensi WHEN 'darurat' THEN 1 WHEN 'tinggi' THEN 2 WHEN 'sedang' THEN 3 ELSE 4 END ASC,
CASE l.status WHEN 'pending' THEN 1 WHEN 'diverifikasi' THEN 2 ELSE 3 END ASC,
l.created_at DESC
LIMIT 500
");
$rows->execute($params);
jsonResponse(true, 'OK', ['data' => $rows->fetchAll(), 'total' => $rows->rowCount()]);
}
if ($action === 'detail') {
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare("
SELECT l.*, p.nama_lengkap AS nama_terdampak, p.status_ekonomi,
p.kondisi_kesehatan, p.alamat AS alamat_penduduk
FROM laporan l
LEFT JOIN penduduk p ON p.nik = l.nik_terdampak
WHERE l.id = :id
");
$stmt->execute([':id' => $id]);
$data = $stmt->fetch();
if (!$data) jsonResponse(false, 'Laporan tidak ditemukan.', [], 404);
jsonResponse(true, 'OK', ['data' => $data]);
}
if ($action === 'geojson') {
// Untuk layer marker laporan di peta
$stmt = $pdo->query("
SELECT id, nama_pelapor, pelapor_id, kategori, tingkat_urgensi, status,
latitude, longitude, alamat, created_at
FROM laporan
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
AND status NOT IN ('selesai','ditolak')
");
$rows = $stmt->fetchAll();
$features = [];
foreach ($rows as $r) {
$color = match($r['tingkat_urgensi']) {
'darurat' => '#dc2626',
'tinggi' => '#ea580c',
'sedang' => '#ca8a04',
default => '#2563eb',
};
$features[] = [
'type' => 'Feature',
'geometry' => ['type' => 'Point', 'coordinates' => [(float)$r['longitude'], (float)$r['latitude']]],
'properties' => array_merge($r, ['color' => $color, 'layer' => 'laporan'])
];
}
header('Content-Type: application/json');
echo json_encode(['type' => 'FeatureCollection', 'features' => $features], JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'stats') {
$stats = $pdo->query("
SELECT
SUM(status='pending') AS pending,
SUM(status='diverifikasi') AS diverifikasi,
SUM(status='diproses') AS diproses,
SUM(status='selesai') AS selesai,
SUM(tingkat_urgensi='darurat') AS darurat,
COUNT(*) AS total
FROM laporan
")->fetch();
jsonResponse(true, 'OK', ['data' => $stats]);
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
// ============================================================
// POST: buat laporan baru (bisa dilakukan masyarakat umum)
// ============================================================
if ($method === 'POST') {
$deskripsi = sanitizeStr($_POST['deskripsi'] ?? '', 2000);
$kategori = sanitizeStr($_POST['kategori'] ?? 'butuh_bantuan_lain');
$urgensi = sanitizeStr($_POST['tingkat_urgensi'] ?? 'sedang');
$lat = !empty($_POST['latitude']) ? (float)$_POST['latitude'] : null;
$lng = !empty($_POST['longitude']) ? (float)$_POST['longitude'] : null;
if (strlen($deskripsi) < 10) jsonResponse(false, 'Deskripsi laporan terlalu singkat.', [], 400);
$fotoNama = null;
if (!empty($_FILES['foto']['name'])) {
try {
$fotoNama = uploadFile($_FILES['foto'], UPLOAD_LAPORAN, ALLOWED_IMG_EXT);
} catch (RuntimeException $e) {
jsonResponse(false, $e->getMessage(), [], 400);
}
}
$stmt = $pdo->prepare("
INSERT INTO laporan
(nama_pelapor, pelapor_id, nik_terdampak, deskripsi, kategori, tingkat_urgensi,
latitude, longitude, alamat, foto, status)
VALUES
(:np, :pid, :nik, :dsk, :kat, :urg, :lat, :lng, :almt, :foto, 'pending')
");
$stmt->execute([
':np' => sanitizeStr($_POST['nama_pelapor'] ?? '') ?: null,
':pid' => $currentUser['id'] ?? null,
':nik' => sanitizeStr($_POST['nik_terdampak'] ?? '') ?: null,
':dsk' => $deskripsi,
':kat' => $kategori,
':urg' => $urgensi,
':lat' => $lat,
':lng' => $lng,
':almt' => sanitizeStr($_POST['alamat'] ?? '') ?: null,
':foto' => $fotoNama,
]);
$laporanId = (int)$pdo->lastInsertId();
// Kirim notifikasi ke pengurus jika darurat
if (in_array($urgensi, ['darurat', 'tinggi'])) {
$pengurus = $pdo->query("SELECT id FROM users WHERE role_id = 2")->fetchAll();
foreach ($pengurus as $p) {
$pdo->prepare("
INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id)
VALUES (:uid, :judul, :isi, 'laporan', :ref)
")->execute([
':uid' => $p['id'],
':judul' => '🚨 Laporan ' . strtoupper($urgensi) . ' masuk!',
':isi' => "Ada laporan baru kategori '$kategori'. Segera ditangani.",
':ref' => $laporanId,
]);
}
}
logAktivitas($pdo, $currentUser['id'] ?? null, 'LAPORAN', 'laporan', (string)$laporanId);
jsonResponse(true, 'Laporan berhasil dikirim. Terima kasih!', ['id' => $laporanId], 201);
}
// ============================================================
// PUT: Update status laporan (hanya pengurus/admin)
// ============================================================
if ($method === 'PUT') {
if (!canVerify($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) jsonResponse(false, 'ID tidak valid.', [], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$status = sanitizeStr($body['status'] ?? '');
$valid = ['diverifikasi', 'diproses', 'selesai', 'ditolak'];
if (!in_array($status, $valid)) jsonResponse(false, 'Status tidak valid.', [], 400);
$old = $pdo->prepare("SELECT * FROM laporan WHERE id = :id");
$old->execute([':id' => $id]);
$dataLama = $old->fetch();
if (!$dataLama) jsonResponse(false, 'Laporan tidak ditemukan.', [], 404);
$sets = ['status = :status', 'updated_at = NOW()'];
$params = [':id' => $id, ':status' => $status];
if ($status === 'diverifikasi' && $currentUser) {
$sets[] = 'diverifikasi_oleh = :verif';
$params[':verif'] = $currentUser['id'];
}
if ($status === 'diproses' && $currentUser) {
$sets[] = 'ditangani_oleh = :tangani';
$params[':tangani'] = $currentUser['id'];
}
if ($status === 'selesai') {
$sets[] = 'tanggal_selesai = NOW()';
}
if (!empty($body['catatan_verifikasi'])) {
$sets[] = 'catatan_verifikasi = :catatan';
$params[':catatan'] = sanitizeStr($body['catatan_verifikasi'], 1000);
}
$pdo->prepare("UPDATE laporan SET " . implode(', ', $sets) . " WHERE id = :id")->execute($params);
logAktivitas($pdo, $currentUser['id'], 'UPDATE', 'laporan', (string)$id, $dataLama, $body);
jsonResponse(true, "Status laporan diperbarui menjadi '$status'.", ['id' => $id]);
}
// ============================================================
// PUT: Update koordinat (via drag map)
// ============================================================
if ($method === 'PUT' && $action === 'update_koordinat') {
if (!canVerify($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true);
$lat = isset($body['latitude']) ? (float)$body['latitude'] : null;
$lng = isset($body['longitude']) ? (float)$body['longitude'] : null;
if (!$lat || !$lng) jsonResponse(false, 'Latitude dan Longitude wajib diisi.', [], 400);
$pdo->prepare("UPDATE laporan SET latitude = :lat, longitude = :lng, updated_at = NOW() WHERE id = :id")->execute([
':lat' => $lat,
':lng' => $lng,
':id' => $id
]);
logAktivitas($pdo, $currentUser['id'], 'UPDATE_KOORDINAT', 'laporan', (string)$id);
jsonResponse(true, 'Koordinat laporan berhasil diperbarui.', ['id' => $id]);
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+328
View File
@@ -0,0 +1,328 @@
<?php
// ============================================================
// API: Penduduk
// GET ?action=list|detail&nik=...
// POST action=create
// PUT action=update&nik=...
// DELETE action=delete&nik=...
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
// ============================================================
// GET
// ============================================================
if ($method === 'GET') {
if ($action === 'list') {
$where = ['p.status_hidup = "hidup"'];
$params = [];
if (!empty($_GET['status_ekonomi'])) {
$where[] = 'p.status_ekonomi = :se';
$params[':se'] = $_GET['status_ekonomi'];
}
if (!empty($_GET['id_keluarga'])) {
$where[] = 'p.id_keluarga = :kk';
$params[':kk'] = (int)$_GET['id_keluarga'];
}
if (!empty($_GET['belum_dibantu'])) {
$where[] = 'NOT EXISTS (SELECT 1 FROM histori_bantuan hb WHERE hb.nik = p.nik AND hb.status_penyaluran="disalurkan")';
}
if (!empty($_GET['kondisi_kesehatan'])) {
$where[] = 'p.kondisi_kesehatan = :kk2';
$params[':kk2'] = $_GET['kondisi_kesehatan'];
}
if (!empty($_GET['search'])) {
$where[] = '(p.nama_lengkap LIKE :q OR p.nik LIKE :q2)';
$params[':q'] = '%' . $_GET['search'] . '%';
$params[':q2'] = '%' . $_GET['search'] . '%';
}
$whereStr = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$sql = "
SELECT
p.nik, p.nama_lengkap, p.jenis_kelamin, p.tanggal_lahir,
p.umur, p.status_keluarga, p.status_perkawinan,
p.status_ekonomi, p.kondisi_kesehatan,
p.pekerjaan, p.penghasilan,
p.pendidikan_terakhir, p.status_pendidikan,
p.alamat, p.latitude, p.longitude, p.foto,
p.id_keluarga,
k.no_kk, k.nama_kk, k.status_ekonomi AS status_ekonomi_kk,
ri.nama AS nama_rumah_ibadah, ri.jenis AS jenis_rumah_ibadah,
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.nik = p.nik AND hb.status_penyaluran='disalurkan') AS jumlah_bantuan_diterima,
(SELECT MAX(hb2.tanggal) FROM histori_bantuan hb2 WHERE hb2.nik = p.nik AND hb2.status_penyaluran='disalurkan') AS terakhir_dibantu
FROM penduduk p
LEFT JOIN keluarga k ON k.id = p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id
$whereStr
ORDER BY
CASE p.status_ekonomi WHEN 'miskin' THEN 1 WHEN 'rentan' THEN 2 ELSE 3 END ASC,
CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 WHEN 'disabilitas' THEN 2 WHEN 'sakit_ringan' THEN 3 ELSE 4 END ASC
LIMIT 1000
";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
// Hitung prioritas untuk setiap penduduk
foreach ($rows as &$r) {
$belumDibantu = (int)$r['jumlah_bantuan_diterima'] === 0;
$r['prioritas_bantuan'] = hitungPrioritasBantuan($r['status_ekonomi'], $r['kondisi_kesehatan'], $belumDibantu);
}
unset($r);
jsonResponse(true, 'OK', ['data' => $rows, 'total' => count($rows)]);
}
if ($action === 'detail') {
$nik = sanitizeStr($_GET['nik'] ?? '');
if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400);
$stmt = $pdo->prepare("
SELECT p.*, k.no_kk, k.nama_kk, ri.nama AS nama_ri
FROM penduduk p
LEFT JOIN keluarga k ON k.id = p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id
WHERE p.nik = :nik
");
$stmt->execute([':nik' => $nik]);
$penduduk = $stmt->fetch();
if (!$penduduk) jsonResponse(false, 'Data tidak ditemukan.', [], 404);
// Histori bantuan
$stmtHB = $pdo->prepare("
SELECT hb.*, jb.nama AS nama_bantuan, jb.kategori, ri.nama AS nama_ri
FROM histori_bantuan hb
JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan
LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id
WHERE hb.nik = :nik
ORDER BY hb.tanggal DESC
");
$stmtHB->execute([':nik' => $nik]);
$histori = $stmtHB->fetchAll();
// Pelatihan
$stmtPl = $pdo->prepare("SELECT * FROM riwayat_pelatihan WHERE nik = :nik ORDER BY tanggal_mulai DESC");
$stmtPl->execute([':nik' => $nik]);
$pelatihan = $stmtPl->fetchAll();
// Validasi pendidikan
$validasi = validasiPendidikanUmur(
(int)$penduduk['umur'],
$penduduk['pendidikan_terakhir'],
$penduduk['status_pendidikan']
);
$belumDibantu = empty($histori);
$penduduk['prioritas_bantuan'] = hitungPrioritasBantuan($penduduk['status_ekonomi'], $penduduk['kondisi_kesehatan'], $belumDibantu);
jsonResponse(true, 'OK', [
'data' => $penduduk,
'histori' => $histori,
'pelatihan' => $pelatihan,
'validasi' => $validasi,
]);
}
if ($action === 'geojson') {
// Untuk layer Leaflet
$stmt = $pdo->query("
SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan,
p.latitude, p.longitude,
k.nama_kk,
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan') AS jml_bantuan
FROM penduduk p
LEFT JOIN keluarga k ON k.id = p.id_keluarga
WHERE p.latitude IS NOT NULL AND p.longitude IS NOT NULL
AND p.status_hidup = 'hidup'
");
$rows = $stmt->fetchAll();
$features = [];
foreach ($rows as $r) {
$color = match($r['status_ekonomi']) {
'miskin' => '#ef4444',
'rentan' => '#f59e0b',
default => '#22c55e'
};
$features[] = [
'type' => 'Feature',
'geometry' => [
'type' => 'Point',
'coordinates' => [(float)$r['longitude'], (float)$r['latitude']]
],
'properties' => array_merge($r, ['color' => $color])
];
}
header('Content-Type: application/json');
echo json_encode(['type' => 'FeatureCollection', 'features' => $features], JSON_UNESCAPED_UNICODE);
exit;
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
// ============================================================
// POST - Create
// ============================================================
if ($method === 'POST') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$nik = sanitizeStr($body['nik'] ?? '');
$nama = sanitizeStr($body['nama_lengkap'] ?? '');
$jk = sanitizeStr($body['jenis_kelamin'] ?? '');
$tgl = sanitizeStr($body['tanggal_lahir'] ?? '');
$idKk = !empty($body['id_keluarga']) ? (int)$body['id_keluarga'] : null;
if (!validateNIK($nik)) jsonResponse(false, 'Format NIK harus 16 digit angka.', [], 400);
if (strlen($nama) < 3) jsonResponse(false, 'Nama terlalu pendek.', [], 400);
if (!in_array($jk, ['L','P'])) jsonResponse(false, 'Jenis kelamin tidak valid.', [], 400);
if (!strtotime($tgl)) jsonResponse(false, 'Format tanggal lahir tidak valid.', [], 400);
// Cek duplikat NIK
$cek = $pdo->prepare("SELECT nik FROM penduduk WHERE nik = :nik");
$cek->execute([':nik' => $nik]);
if ($cek->fetch()) jsonResponse(false, 'NIK sudah terdaftar.', [], 409);
// Hitung umur untuk validasi pendidikan
$umur = (int)(new DateTime())->diff(new DateTime($tgl))->y;
$statusPendidikan = sanitizeStr($body['status_pendidikan'] ?? 'tidak_sekolah');
$pendidikan = sanitizeStr($body['pendidikan_terakhir'] ?? 'tidak_sekolah');
$validasi = validasiPendidikanUmur($umur, $pendidikan, $statusPendidikan);
$lat = !empty($body['latitude']) ? (float)$body['latitude'] : null;
$lng = !empty($body['longitude']) ? (float)$body['longitude'] : null;
if ($lat !== null && !validateCoord((string)$lat, 'lat')) jsonResponse(false, 'Latitude tidak valid.', [], 400);
if ($lng !== null && !validateCoord((string)$lng, 'lng')) jsonResponse(false, 'Longitude tidak valid.', [], 400);
$stmt = $pdo->prepare("
INSERT INTO penduduk
(nik, id_keluarga, nama_lengkap, jenis_kelamin, tanggal_lahir,
status_keluarga, status_perkawinan, status_hidup,
pendidikan_terakhir, status_pendidikan,
pekerjaan, penghasilan, status_ekonomi,
kondisi_kesehatan, catatan_kesehatan,
no_telp, alamat, latitude, longitude)
VALUES
(:nik,:kk,:nama,:jk,:tgl,
:sk,:sp,:sh,
:ptk,:spd,
:pkr,:pgn,:se,
:kkh,:ckkh,
:tlp,:almt,:lat,:lng)
");
$stmt->execute([
':nik' => $nik,
':kk' => $idKk,
':nama' => $nama,
':jk' => $jk,
':tgl' => $tgl,
':sk' => sanitizeStr($body['status_keluarga'] ?? 'anggota_lain'),
':sp' => sanitizeStr($body['status_perkawinan'] ?? 'belum_kawin'),
':sh' => sanitizeStr($body['status_hidup'] ?? 'hidup'),
':ptk' => $pendidikan,
':spd' => $statusPendidikan,
':pkr' => sanitizeStr($body['pekerjaan'] ?? '') ?: null,
':pgn' => !empty($body['penghasilan']) ? (float)$body['penghasilan'] : 0,
':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'),
':kkh' => sanitizeStr($body['kondisi_kesehatan'] ?? 'sehat'),
':ckkh' => sanitizeStr($body['catatan_kesehatan'] ?? '') ?: null,
':tlp' => sanitizeStr($body['no_telp'] ?? '') ?: null,
':almt' => sanitizeStr($body['alamat'] ?? '') ?: null,
':lat' => $lat,
':lng' => $lng,
]);
logAktivitas($pdo, $currentUser['id'] ?? null, 'CREATE', 'penduduk', $nik, null, $body);
jsonResponse(true, 'Penduduk berhasil ditambahkan.', [
'nik' => $nik,
'validasi' => $validasi
], 201);
}
// ============================================================
// PUT - Update
// ============================================================
if ($method === 'PUT') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$nik = sanitizeStr($_GET['nik'] ?? '');
if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
// Ambil data lama
$old = $pdo->prepare("SELECT * FROM penduduk WHERE nik = :nik");
$old->execute([':nik' => $nik]);
$dataLama = $old->fetch();
if (!$dataLama) jsonResponse(false, 'Data tidak ditemukan.', [], 404);
$sets = [];
$params = [':nik' => $nik];
$fieldMap = [
'id_keluarga' => ':kk', 'nama_lengkap' => ':nama', 'jenis_kelamin' => ':jk',
'tanggal_lahir' => ':tgl', 'status_keluarga' => ':sk', 'status_perkawinan' => ':sp',
'status_hidup' => ':sh', 'pendidikan_terakhir' => ':ptk', 'status_pendidikan' => ':spd',
'pekerjaan' => ':pkr', 'penghasilan' => ':pgn', 'status_ekonomi' => ':se',
'kondisi_kesehatan' => ':kkh', 'catatan_kesehatan' => ':ckkh',
'no_telp' => ':tlp', 'alamat' => ':almt', 'latitude' => ':lat', 'longitude' => ':lng',
];
foreach ($fieldMap as $field => $param) {
if (array_key_exists($field, $body)) {
$sets[] = "$field = $param";
$params[$param] = $body[$field] === '' ? null : $body[$field];
}
}
if (empty($sets)) jsonResponse(false, 'Tidak ada data yang diubah.', [], 400);
$sql = "UPDATE penduduk SET " . implode(', ', $sets) . " WHERE nik = :nik";
$pdo->prepare($sql)->execute($params);
logAktivitas($pdo, $currentUser['id'] ?? null, 'UPDATE', 'penduduk', $nik, $dataLama, $body);
jsonResponse(true, 'Data penduduk berhasil diperbarui.', ['nik' => $nik]);
}
// ============================================================
// DELETE
// ============================================================
if ($method === 'DELETE') {
if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin yang bisa menghapus data.', [], 403);
$nik = sanitizeStr($_GET['nik'] ?? '');
if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400);
$old = $pdo->prepare("SELECT * FROM penduduk WHERE nik = :nik");
$old->execute([':nik' => $nik]);
$dataLama = $old->fetch();
if (!$dataLama) jsonResponse(false, 'Data tidak ditemukan.', [], 404);
$pdo->prepare("DELETE FROM penduduk WHERE nik = :nik")->execute([':nik' => $nik]);
logAktivitas($pdo, $currentUser['id'] ?? null, 'DELETE', 'penduduk', $nik, $dataLama, null);
jsonResponse(true, 'Data penduduk dihapus.');
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+127
View File
@@ -0,0 +1,127 @@
<?php
// ============================================================
// API: Pesan / Chat antar Pengurus Rumah Ibadah
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
// Harus login untuk fitur chat
if (!$currentUser) jsonResponse(false, 'Harus login.', [], 401);
if ($method === 'GET') {
if ($action === 'inbox') {
$riId = !empty($_GET['rumah_ibadah_id']) ? (int)$_GET['rumah_ibadah_id'] : null;
if ($riId) {
// Chat diskusi per rumah ibadah
$stmt = $pdo->prepare("
SELECT p.*, u.nama AS pengirim, u.role_id,
ri.nama AS nama_ri
FROM pesan p
JOIN users u ON u.id = p.dari_user_id
LEFT JOIN rumah_ibadah ri ON ri.id = p.ke_rumah_ibadah_id
WHERE p.ke_rumah_ibadah_id = :ri
ORDER BY p.created_at ASC
LIMIT 200
");
$stmt->execute([':ri' => $riId]);
} else {
// Pesan langsung ke user
$stmt = $pdo->prepare("
SELECT p.*, u.nama AS pengirim
FROM pesan p
JOIN users u ON u.id = p.dari_user_id
WHERE p.ke_user_id = :uid OR p.dari_user_id = :uid2
ORDER BY p.created_at ASC
LIMIT 200
");
$stmt->execute([':uid' => $currentUser['id'], ':uid2' => $currentUser['id']]);
}
$pesan = $stmt->fetchAll();
// Tandai sudah dibaca
if ($riId) {
$pdo->prepare("UPDATE pesan SET dibaca=1 WHERE ke_rumah_ibadah_id=:ri AND dari_user_id != :uid")
->execute([':ri' => $riId, ':uid' => $currentUser['id']]);
} else {
$pdo->prepare("UPDATE pesan SET dibaca=1 WHERE ke_user_id=:uid")
->execute([':uid' => $currentUser['id']]);
}
jsonResponse(true, 'OK', ['data' => $pesan]);
}
if ($action === 'notifikasi') {
$stmt = $pdo->prepare("
SELECT * FROM notifikasi
WHERE user_id = :uid
ORDER BY created_at DESC
LIMIT 30
");
$stmt->execute([':uid' => $currentUser['id']]);
$notif = $stmt->fetchAll();
$unread = (int)$pdo->prepare("SELECT COUNT(*) FROM notifikasi WHERE user_id=:uid AND dibaca=0")
->execute([':uid' => $currentUser['id']]);
jsonResponse(true, 'OK', ['data' => $notif, 'unread' => array_sum(array_column($notif, 'dibaca') === array_map(fn($r) => $r['dibaca'] == 0, $notif))]);
}
if ($action === 'pengurus_list') {
// Daftar pengurus untuk pilihan chat
$rows = $pdo->query("
SELECT u.id, u.nama, u.email, ri.nama AS nama_ri, ri.jenis AS jenis_ri
FROM users u
LEFT JOIN rumah_ibadah ri ON ri.id = u.rumah_ibadah_id
WHERE u.role_id IN (1,2) AND u.aktif=1
ORDER BY u.nama
")->fetchAll();
jsonResponse(true, 'OK', ['data' => $rows]);
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
if ($method === 'POST') {
// Kirim pesan
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$isi = sanitizeStr($body['isi'] ?? '', 2000);
if (strlen(trim($isi)) < 1) jsonResponse(false, 'Pesan tidak boleh kosong.', [], 400);
$keUserId = !empty($body['ke_user_id']) ? (int)$body['ke_user_id'] : null;
$keRiId = !empty($body['ke_rumah_ibadah_id']) ? (int)$body['ke_rumah_ibadah_id'] : null;
if (!$keUserId && !$keRiId) jsonResponse(false, 'Tujuan pesan harus diisi.', [], 400);
$stmt = $pdo->prepare("
INSERT INTO pesan (dari_user_id, ke_user_id, ke_rumah_ibadah_id, isi)
VALUES (:dari, :ke_user, :ke_ri, :isi)
");
$stmt->execute([
':dari' => $currentUser['id'],
':ke_user' => $keUserId,
':ke_ri' => $keRiId,
':isi' => $isi,
]);
$newId = (int)$pdo->lastInsertId();
// Notifikasi ke penerima
if ($keUserId) {
$pdo->prepare("INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id) VALUES (:u,:j,:i,'pesan',:r)")
->execute([':u'=>$keUserId, ':j'=>"Pesan dari {$currentUser['role']}", ':i'=>substr($isi,0,100), ':r'=>$newId]);
}
jsonResponse(true, 'Pesan terkirim.', ['id' => $newId], 201);
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+111
View File
@@ -0,0 +1,111 @@
<?php
// ============================================================
// API: Rumah Ibadah
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
if ($method === 'GET') {
if ($action === 'list') {
$rows = $pdo->query("
SELECT ri.*,
COUNT(k.id) AS jumlah_kk,
SUM(CASE WHEN k.status_ekonomi='miskin' THEN 1 ELSE 0 END) AS kk_miskin
FROM rumah_ibadah ri
LEFT JOIN keluarga k ON k.rumah_ibadah_id = ri.id
GROUP BY ri.id
ORDER BY ri.nama
")->fetchAll();
jsonResponse(true, 'OK', ['data' => $rows]);
}
if ($action === 'geojson') {
$rows = $pdo->query("SELECT * FROM rumah_ibadah")->fetchAll();
$features = [];
foreach ($rows as $r) {
$features[] = [
'type' => 'Feature',
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['longitude'],(float)$r['latitude']]],
'properties' => $r
];
}
header('Content-Type: application/json');
echo json_encode(['type'=>'FeatureCollection','features'=>$features], JSON_UNESCAPED_UNICODE);
exit;
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
if ($method === 'POST') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$nama = sanitizeStr($body['nama'] ?? '');
$lat = (float)($body['latitude'] ?? 0);
$lng = (float)($body['longitude'] ?? 0);
if (strlen($nama) < 3) jsonResponse(false, 'Nama terlalu pendek.', [], 400);
if (!$lat || !$lng) jsonResponse(false, 'Koordinat wajib diisi.', [], 400);
$stmt = $pdo->prepare("
INSERT INTO rumah_ibadah (nama, jenis, kontak, radius_meter, latitude, longitude, alamat, kelurahan, kecamatan)
VALUES (:nama,:jenis,:kontak,:radius,:lat,:lng,:alamat,:kel,:kec)
");
$stmt->execute([
':nama' => $nama,
':jenis' => sanitizeStr($body['jenis'] ?? 'Lainnya'),
':kontak' => sanitizeStr($body['kontak'] ?? '') ?: null,
':radius' => (float)($body['radius_meter'] ?? 300),
':lat' => $lat, ':lng' => $lng,
':alamat' => sanitizeStr($body['alamat'] ?? '') ?: null,
':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null,
':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null,
]);
$newId = (int)$pdo->lastInsertId();
logAktivitas($pdo, $currentUser['id']??null, 'CREATE', 'rumah_ibadah', (string)$newId, null, $body);
jsonResponse(true, 'Rumah ibadah ditambahkan.', ['id' => $newId], 201);
}
if ($method === 'PUT') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
if ($action === 'update_koordinat') {
$lat = (float)($body['latitude'] ?? 0);
$lng = (float)($body['longitude'] ?? 0);
if (!$lat || !$lng) jsonResponse(false, 'Koordinat tidak valid.', [], 400);
$pdo->prepare("UPDATE rumah_ibadah SET latitude=:lat, longitude=:lng WHERE id=:id")
->execute([':lat'=>$lat, ':lng'=>$lng, ':id'=>$id]);
logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'rumah_ibadah', (string)$id, null, ['lat'=>$lat,'lng'=>$lng]);
jsonResponse(true, 'Koordinat diperbarui.', ['id' => $id]);
}
$pdo->prepare("
UPDATE rumah_ibadah SET nama=:nama,jenis=:jenis,kontak=:kontak,radius_meter=:radius,
latitude=:lat,longitude=:lng,alamat=:alamat,kelurahan=:kel,kecamatan=:kec WHERE id=:id
")->execute([
':nama'=>sanitizeStr($body['nama']??''), ':jenis'=>sanitizeStr($body['jenis']??'Lainnya'),
':kontak'=>sanitizeStr($body['kontak']??'')?: null, ':radius'=>(float)($body['radius_meter']??300),
':lat'=>(float)($body['latitude']??0), ':lng'=>(float)($body['longitude']??0),
':alamat'=>sanitizeStr($body['alamat']??'')?: null, ':kel'=>sanitizeStr($body['kelurahan']??'')?: null,
':kec'=>sanitizeStr($body['kecamatan']??'')?: null, ':id'=>$id,
]);
logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'rumah_ibadah', (string)$id);
jsonResponse(true, 'Rumah ibadah diperbarui.', ['id' => $id]);
}
if ($method === 'DELETE') {
if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$pdo->prepare("DELETE FROM rumah_ibadah WHERE id=:id")->execute([':id'=>$id]);
logAktivitas($pdo, $currentUser['id']??null, 'DELETE', 'rumah_ibadah', (string)$id);
jsonResponse(true, 'Dihapus.');
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+177
View File
@@ -0,0 +1,177 @@
<?php
// ============================================================
// Konfigurasi & Koneksi Database
// WebGIS Pengentasan Kemiskinan v2.0
// ============================================================
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_PORT', getenv('DB_PORT') ?: '3306');
define('DB_NAME', getenv('DB_NAME') ?: 'webgis_kemiskinan');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASSWORD') ?: '');
define('DB_CHARSET', 'utf8mb4');
define('APP_NAME', 'WebGIS Pengentasan Kemiskinan');
define('APP_VERSION', '2.0.0');
define('APP_URL', 'http://localhost/webgis-kemiskinan');
define('APP_ENV', 'development'); // Ganti ke 'production' saat deploy
define('UPLOAD_PATH', __DIR__ . '/../uploads/');
define('UPLOAD_BUKTI', UPLOAD_PATH . 'bukti/');
define('UPLOAD_LAPORAN', UPLOAD_PATH . 'foto_laporan/');
define('UPLOAD_FOTO', UPLOAD_PATH . 'foto_profil/');
define('MAX_FILE_SIZE', 5 * 1024 * 1024); // 5 MB
define('ALLOWED_IMG_EXT', ['jpg','jpeg','png','webp']);
define('ALLOWED_FILE_EXT', ['jpg','jpeg','png','webp','pdf']);
// Buat folder upload jika belum ada
foreach ([UPLOAD_BUKTI, UPLOAD_LAPORAN, UPLOAD_FOTO] as $dir) {
if (!is_dir($dir)) @mkdir($dir, 0755, true);
}
// ============================================================
// Koneksi PDO
// ============================================================
$dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$pdoOptions = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $pdoOptions);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
// Jangan expose detail error di produksi
$errMsg = (defined('APP_ENV') && APP_ENV === 'development') ? $e->getMessage() : 'Hubungi administrator sistem.';
echo json_encode([
'success' => false,
'message' => 'Koneksi database gagal: ' . DB_NAME . '@' . DB_HOST . ' tidak ditemukan. Pastikan database sudah dibuat dan nama database benar.',
'error' => $errMsg
]);
exit;
}
// ============================================================
// Helper: Response JSON
// ============================================================
function jsonResponse(bool $success, string $message, array $data = [], int $httpCode = 200): void {
http_response_code($httpCode);
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
echo json_encode(array_merge(['success' => $success, 'message' => $message], $data), JSON_UNESCAPED_UNICODE);
exit;
}
// ============================================================
// Helper: Sanitasi & Validasi
// ============================================================
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
function sanitizeStr(?string $v, int $max = 255): string {
return mb_substr(trim($v ?? ''), 0, $max);
}
function validateNIK(string $nik): bool {
return preg_match('/^\d{16}$/', $nik) === 1;
}
function validateCoord(string $val, string $type = 'lat'): bool {
$f = (float)$val;
return $type === 'lat' ? ($f >= -90 && $f <= 90) : ($f >= -180 && $f <= 180);
}
// ============================================================
// Helper: Upload File
// ============================================================
function uploadFile(array $file, string $destDir, array $allowedExt = null): ?string {
if ($allowedExt === null) $allowedExt = ALLOWED_FILE_EXT;
if ($file['error'] !== UPLOAD_ERR_OK) return null;
if ($file['size'] > MAX_FILE_SIZE) throw new RuntimeException('Ukuran file melebihi 5MB.');
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt, true)) throw new RuntimeException("Format file .$ext tidak diizinkan.");
$filename = uniqid('img_', true) . '.' . $ext;
$dest = rtrim($destDir, '/') . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) throw new RuntimeException('Gagal menyimpan file.');
return $filename;
}
// ============================================================
// Helper: Log Aktivitas
// ============================================================
function logAktivitas(PDO $pdo, ?int $userId, string $aksi, string $tabel = '', string $recordId = '', $dataLama = null, $dataBaru = null): void {
try {
$stmt = $pdo->prepare("
INSERT INTO log_aktivitas (user_id, aksi, tabel, record_id, data_lama, data_baru, ip_address, user_agent)
VALUES (:u, :a, :t, :r, :dl, :db, :ip, :ua)
");
$stmt->execute([
':u' => $userId,
':a' => $aksi,
':t' => $tabel,
':r' => $recordId,
':dl' => $dataLama ? json_encode($dataLama, JSON_UNESCAPED_UNICODE) : null,
':db' => $dataBaru ? json_encode($dataBaru, JSON_UNESCAPED_UNICODE) : null,
':ip' => $_SERVER['REMOTE_ADDR'] ?? null,
':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
]);
} catch (Exception) { /* log gagal tidak menghentikan proses */ }
}
// ============================================================
// Validasi Logika Pendidikan vs Umur
// ============================================================
function validasiPendidikanUmur(int $umur, string $pendidikan, string $statusPendidikan): array {
$warnings = [];
$errors = [];
if ($umur >= 7 && $umur <= 12) {
if ($statusPendidikan !== 'sekolah') {
$warnings[] = "Anak usia $umur tahun semestinya masih sekolah SD.";
}
} elseif ($umur >= 13 && $umur <= 15) {
if ($statusPendidikan !== 'sekolah') {
$warnings[] = "Anak usia $umur tahun semestinya masih sekolah SMP.";
}
} elseif ($umur >= 16 && $umur <= 18) {
if ($statusPendidikan !== 'sekolah') {
$warnings[] = "Anak usia $umur tahun semestinya masih sekolah SMA/SMK.";
}
}
// Umur > 18: bebas, boleh tidak sekolah, boleh pelatihan
return ['warnings' => $warnings, 'errors' => $errors];
}
// ============================================================
// Prioritas Bantuan
// ============================================================
function hitungPrioritasBantuan(string $statusEkonomi, string $kondisiKesehatan, bool $belumPernahDibantu): string {
if ($statusEkonomi === 'miskin' && in_array($kondisiKesehatan, ['sakit_parah', 'disabilitas'])) {
return 'KRITIS';
}
if ($statusEkonomi === 'miskin' && $belumPernahDibantu) {
return 'TINGGI';
}
if ($statusEkonomi === 'miskin') {
return 'SEDANG';
}
if ($statusEkonomi === 'rentan' && in_array($kondisiKesehatan, ['sakit_parah', 'disabilitas'])) {
return 'SEDANG';
}
return 'RENDAH';
}
+125
View File
@@ -0,0 +1,125 @@
<?php
// ============================================================
// Middleware Autentikasi & Otorisasi
// ============================================================
require_once __DIR__ . '/../koneksi.php';
if (session_status() === PHP_SESSION_NONE) {
session_start([
'cookie_httponly' => true,
'cookie_samesite' => 'Strict',
'use_strict_mode' => true,
]);
}
// ============================================================
// Fungsi login / logout
// ============================================================
function doLogin(PDO $pdo, string $username, string $password): array {
$stmt = $pdo->prepare("
SELECT u.*, r.nama AS role_nama
FROM users u
JOIN roles r ON r.id = u.role_id
WHERE u.username = :u AND u.aktif = 1
LIMIT 1
");
$stmt->execute([':u' => $username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
return ['success' => false, 'message' => 'Username atau password salah.'];
}
// Update last_login
$pdo->prepare("UPDATE users SET last_login = NOW() WHERE id = :id")
->execute([':id' => $user['id']]);
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_nama'] = $user['nama'];
$_SESSION['role'] = $user['role_nama'];
$_SESSION['role_id'] = $user['role_id'];
$_SESSION['rumah_ibadah_id'] = $user['rumah_ibadah_id'];
session_regenerate_id(true);
logAktivitas($pdo, $user['id'], 'LOGIN', 'users', (string)$user['id']);
return ['success' => true, 'role' => $user['role_nama']];
}
function doLogout(PDO $pdo): void {
if (isset($_SESSION['user_id'])) {
logAktivitas($pdo, $_SESSION['user_id'], 'LOGOUT', 'users', (string)$_SESSION['user_id']);
}
session_destroy();
}
// ============================================================
// Guard: wajib login
// ============================================================
function requireLogin(array $allowedRoles = []): array {
if (empty($_SESSION['user_id'])) {
// Jika request API
if (!empty($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
jsonResponse(false, 'Sesi habis. Silakan login kembali.', [], 401);
}
header('Location: ' . APP_URL . '/admin/login.php');
exit;
}
if (!empty($allowedRoles) && !in_array($_SESSION['role'], $allowedRoles, true)) {
if (!empty($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
jsonResponse(false, 'Tidak punya akses.', [], 403);
}
header('Location: ' . APP_URL . '/admin/forbidden.php');
exit;
}
return [
'id' => (int)$_SESSION['user_id'],
'nama' => $_SESSION['user_nama'],
'role' => $_SESSION['role'],
'role_id' => (int)$_SESSION['role_id'],
'rumah_ibadah_id' => $_SESSION['rumah_ibadah_id'] ? (int)$_SESSION['rumah_ibadah_id'] : null,
];
}
// ============================================================
// Guard API: token-based (opsional, untuk mobile app nanti)
// ============================================================
function getApiUser(PDO $pdo): ?array {
// Cek session dulu
if (!empty($_SESSION['user_id'])) {
return [
'id' => (int)$_SESSION['user_id'],
'role' => $_SESSION['role'],
'role_id' => (int)$_SESSION['role_id'],
'rumah_ibadah_id' => $_SESSION['rumah_ibadah_id'] ? (int)$_SESSION['rumah_ibadah_id'] : null,
];
}
return null; // belum login = null
}
// ============================================================
// Cek izin per fitur
// ============================================================
function canEdit(?array $user): bool {
if (!$user) return false;
return in_array($user['role'], ['admin', 'pengurus'], true);
}
function canVerify(?array $user): bool {
if (!$user) return false;
return in_array($user['role'], ['admin', 'pengurus'], true);
}
function canViewAll(?array $user): bool {
if (!$user) return false;
return in_array($user['role'], ['admin', 'pengurus', 'pimpinan'], true);
}
function isAdmin(?array $user): bool {
return $user && $user['role'] === 'admin';
}
+384
View File
@@ -0,0 +1,384 @@
-- ============================================================
-- WebGIS Pengentasan Kemiskinan Berbasis Masyarakat & Rumah Ibadah
-- Schema Lengkap v2.0 — FIXED VERSION
-- Engine: MySQL / MariaDB | Charset: utf8mb4
-- ============================================================
-- PENTING: Buat database terlebih dahulu jika belum ada
CREATE DATABASE IF NOT EXISTS webgis_kemiskinan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE webgis_kemiskinan;
SET FOREIGN_KEY_CHECKS = 0;
SET NAMES utf8mb4;
-- ============================================================
-- DROP TABLES (urutan terbalik dari dependensi FK)
-- ============================================================
DROP TABLE IF EXISTS notifikasi;
DROP TABLE IF EXISTS log_aktivitas;
DROP TABLE IF EXISTS pesan;
DROP TABLE IF EXISTS laporan;
DROP TABLE IF EXISTS histori_bantuan;
DROP TABLE IF EXISTS jenis_bantuan;
DROP TABLE IF EXISTS riwayat_pelatihan;
DROP TABLE IF EXISTS penduduk;
DROP TABLE IF EXISTS keluarga;
DROP TABLE IF EXISTS rumah_ibadah;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS roles;
DROP TABLE IF EXISTS jalan_lines;
DROP TABLE IF EXISTS parsil_tanah;
-- ============================================================
-- 1. ROLES & USERS
-- ============================================================
CREATE TABLE roles (
id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL UNIQUE,
label VARCHAR(80) NOT NULL,
deskripsi TEXT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO roles (nama, label) VALUES
('admin', 'Administrator Sistem'),
('pengurus', 'Pengurus Rumah Ibadah'),
('pimpinan', 'Pimpinan Daerah'),
('masyarakat', 'Masyarakat Umum');
-- ============================================================
-- 2. RUMAH IBADAH (dibuat sebelum users karena users FK ke sini)
-- ============================================================
CREATE TABLE rumah_ibadah (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
jenis ENUM('Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya') NOT NULL DEFAULT 'Lainnya',
kontak VARCHAR(120) NULL,
radius_meter DOUBLE NOT NULL DEFAULT 300,
latitude DECIMAL(10,7) NOT NULL,
longitude DECIMAL(10,7) NOT NULL,
alamat TEXT NULL,
kelurahan VARCHAR(120) NULL,
kecamatan VARCHAR(120) NULL,
kota VARCHAR(120) NULL,
deskripsi TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ri_jenis (jenis),
INDEX idx_ri_latlng (latitude, longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 3. USERS (setelah roles & rumah_ibadah)
-- ============================================================
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
role_id TINYINT UNSIGNED NOT NULL DEFAULT 4,
nama VARCHAR(150) NOT NULL,
username VARCHAR(60) NOT NULL UNIQUE,
email VARCHAR(150) NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
no_telp VARCHAR(20) NULL,
foto VARCHAR(255) NULL,
aktif TINYINT(1) NOT NULL DEFAULT 1,
rumah_ibadah_id INT UNSIGNED NULL,
last_login DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_users_role (role_id),
CONSTRAINT fk_users_role FOREIGN KEY (role_id) REFERENCES roles(id),
CONSTRAINT fk_users_ri FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 4. KELUARGA (KK)
-- ============================================================
CREATE TABLE keluarga (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
no_kk VARCHAR(20) NOT NULL UNIQUE,
nama_kk VARCHAR(200) NOT NULL,
alamat TEXT NULL,
kelurahan VARCHAR(120) NULL,
kecamatan VARCHAR(120) NULL,
kota VARCHAR(120) NULL,
latitude DECIMAL(10,7) NULL,
longitude DECIMAL(10,7) NULL,
status_ekonomi ENUM('miskin','rentan','mampu') NOT NULL DEFAULT 'rentan',
rumah_ibadah_id INT UNSIGNED NULL,
jarak_ri_meter DOUBLE NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_kk_status (status_ekonomi),
INDEX idx_kk_latlng (latitude, longitude),
INDEX idx_kk_ri (rumah_ibadah_id),
CONSTRAINT fk_kk_ri FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 5. PENDUDUK
-- ============================================================
CREATE TABLE penduduk (
nik VARCHAR(16) NOT NULL PRIMARY KEY,
id_keluarga INT UNSIGNED NULL,
nama_lengkap VARCHAR(200) NOT NULL,
jenis_kelamin ENUM('L','P') NOT NULL,
tanggal_lahir DATE NOT NULL,
umur TINYINT UNSIGNED GENERATED ALWAYS AS (
TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE())
) VIRTUAL,
status_keluarga ENUM('kepala_keluarga','istri','anak','menantu','cucu','orang_tua','mertua','anggota_lain') NOT NULL DEFAULT 'anggota_lain',
status_perkawinan ENUM('belum_kawin','kawin','cerai_hidup','cerai_mati') NOT NULL DEFAULT 'belum_kawin',
status_hidup ENUM('hidup','meninggal') NOT NULL DEFAULT 'hidup',
pendidikan_terakhir ENUM('tidak_sekolah','SD','SMP','SMA','SMK','D1','D2','D3','S1','S2','S3') NOT NULL DEFAULT 'tidak_sekolah',
status_pendidikan ENUM('sekolah','tidak_sekolah','lulus') NOT NULL DEFAULT 'tidak_sekolah',
pekerjaan VARCHAR(150) NULL,
penghasilan DECIMAL(15,2) NULL DEFAULT 0,
status_ekonomi ENUM('miskin','rentan','mampu') NOT NULL DEFAULT 'rentan',
kondisi_kesehatan ENUM('sehat','sakit_ringan','sakit_parah','disabilitas') NOT NULL DEFAULT 'sehat',
catatan_kesehatan TEXT NULL,
no_telp VARCHAR(20) NULL,
alamat TEXT NULL,
latitude DECIMAL(10,7) NULL,
longitude DECIMAL(10,7) NULL,
foto VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_pddk_keluarga (id_keluarga),
INDEX idx_pddk_status_ek (status_ekonomi),
INDEX idx_pddk_status_hid (status_hidup),
INDEX idx_pddk_latlng (latitude, longitude),
CONSTRAINT fk_pddk_keluarga FOREIGN KEY (id_keluarga) REFERENCES keluarga(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 6. RIWAYAT PELATIHAN
-- ============================================================
CREATE TABLE riwayat_pelatihan (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nik VARCHAR(16) NOT NULL,
nama_pelatihan VARCHAR(200) NOT NULL,
penyelenggara VARCHAR(150) NULL,
tanggal_mulai DATE NULL,
tanggal_selesai DATE NULL,
sertifikat VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_platihan_nik (nik),
CONSTRAINT fk_platihan_nik FOREIGN KEY (nik) REFERENCES penduduk(nik) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 7. JENIS BANTUAN (master)
-- ============================================================
CREATE TABLE jenis_bantuan (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150) NOT NULL,
kategori ENUM('sembako','pendidikan','kesehatan','ekonomi','perumahan','lainnya') NOT NULL DEFAULT 'lainnya',
sumber VARCHAR(150) NULL,
satuan VARCHAR(50) NULL,
deskripsi TEXT NULL,
aktif TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO jenis_bantuan (nama, kategori, sumber, satuan) VALUES
('Sembako Bulanan', 'sembako', 'Rumah Ibadah', '1 paket/bulan'),
('Beasiswa Pendidikan', 'pendidikan','BAZNAS', 'Rp/semester'),
('Bantuan Kesehatan', 'kesehatan', 'Puskesmas', '1 kali/kunjungan'),
('Modal Usaha Mikro', 'ekonomi', 'Baznas', 'Rp/program'),
('Renovasi Rumah', 'perumahan', 'Pemerintah', 'paket/unit');
-- ============================================================
-- 8. HISTORI BANTUAN
-- ============================================================
CREATE TABLE histori_bantuan (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nik VARCHAR(16) NOT NULL,
id_jenis_bantuan INT UNSIGNED NOT NULL,
id_keluarga INT UNSIGNED NULL,
rumah_ibadah_id INT UNSIGNED NULL,
tanggal DATE NOT NULL,
jumlah VARCHAR(100) NULL,
nilai_rupiah DECIMAL(15,2) NULL,
status_penyaluran ENUM('dijadwalkan','disalurkan','dibatalkan') NOT NULL DEFAULT 'dijadwalkan',
catatan TEXT NULL,
bukti_file VARCHAR(255) NULL,
disalurkan_oleh INT UNSIGNED NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_hb_nik (nik),
INDEX idx_hb_tanggal (tanggal),
INDEX idx_hb_status (status_penyaluran),
CONSTRAINT fk_hb_nik FOREIGN KEY (nik) REFERENCES penduduk(nik) ON DELETE CASCADE,
CONSTRAINT fk_hb_jenis FOREIGN KEY (id_jenis_bantuan) REFERENCES jenis_bantuan(id),
CONSTRAINT fk_hb_kk FOREIGN KEY (id_keluarga) REFERENCES keluarga(id) ON DELETE SET NULL,
CONSTRAINT fk_hb_ri FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL,
CONSTRAINT fk_hb_user FOREIGN KEY (disalurkan_oleh) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 9. LAPORAN MASYARAKAT
-- ============================================================
CREATE TABLE laporan (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nama_pelapor VARCHAR(150) NULL,
pelapor_id INT UNSIGNED NULL,
nik_terdampak VARCHAR(16) NULL,
deskripsi TEXT NOT NULL,
kategori ENUM('darurat_pangan','darurat_kesehatan','darurat_ekonomi','butuh_bantuan_lain') NOT NULL DEFAULT 'butuh_bantuan_lain',
tingkat_urgensi ENUM('rendah','sedang','tinggi','darurat') NOT NULL DEFAULT 'sedang',
latitude DECIMAL(10,7) NULL,
longitude DECIMAL(10,7) NULL,
alamat TEXT NULL,
foto VARCHAR(255) NULL,
status ENUM('pending','diverifikasi','diproses','selesai','ditolak') NOT NULL DEFAULT 'pending',
catatan_verifikasi TEXT NULL,
diverifikasi_oleh INT UNSIGNED NULL,
ditangani_oleh INT UNSIGNED NULL,
tanggal_selesai DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_lap_status (status),
INDEX idx_lap_urgensi (tingkat_urgensi),
INDEX idx_lap_latlng (latitude, longitude),
CONSTRAINT fk_lap_nik FOREIGN KEY (nik_terdampak) REFERENCES penduduk(nik) ON DELETE SET NULL,
CONSTRAINT fk_lap_pelapor FOREIGN KEY (pelapor_id) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_lap_verif FOREIGN KEY (diverifikasi_oleh) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_lap_tangan FOREIGN KEY (ditangani_oleh) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 10. PESAN / CHAT ANTAR PENGURUS
-- ============================================================
CREATE TABLE pesan (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
dari_user_id INT UNSIGNED NOT NULL,
ke_user_id INT UNSIGNED NULL,
ke_rumah_ibadah_id INT UNSIGNED NULL,
isi TEXT NOT NULL,
dibaca TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_pesan_dari (dari_user_id),
INDEX idx_pesan_ke (ke_user_id),
CONSTRAINT fk_pesan_dari FOREIGN KEY (dari_user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_pesan_ke FOREIGN KEY (ke_user_id) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_pesan_ri FOREIGN KEY (ke_rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 11. LOG AKTIVITAS (audit trail)
-- ============================================================
CREATE TABLE log_aktivitas (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NULL,
aksi VARCHAR(50) NOT NULL,
tabel VARCHAR(60) NULL,
record_id VARCHAR(32) NULL,
data_lama JSON NULL,
data_baru JSON NULL,
ip_address VARCHAR(45) NULL,
user_agent VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_log_user (user_id),
INDEX idx_log_tabel (tabel),
INDEX idx_log_aksi (aksi),
INDEX idx_log_time (created_at),
CONSTRAINT fk_log_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 12. LAYER SPASIAL TAMBAHAN
-- ============================================================
CREATE TABLE jalan_lines (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(200) NOT NULL,
status_jalan ENUM('Nasional','Provinsi','Kabupaten','Desa') NOT NULL DEFAULT 'Kabupaten',
panjang_meter DOUBLE NOT NULL DEFAULT 0,
geojson LONGTEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_jalan_status (status_jalan)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE parsil_tanah (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
status_kepemilikan ENUM('SHM','HGB','HGU','HP','Wakaf','Lainnya') NOT NULL DEFAULT 'SHM',
luas_m2 DOUBLE NOT NULL DEFAULT 0,
geojson LONGTEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_parsil_status (status_kepemilikan)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- 13. NOTIFIKASI
-- ============================================================
CREATE TABLE notifikasi (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
judul VARCHAR(200) NOT NULL,
isi TEXT NOT NULL,
tipe ENUM('laporan','bantuan','sistem','pesan') NOT NULL DEFAULT 'sistem',
ref_id INT UNSIGNED NULL,
dibaca TINYINT(1) NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_notif_user (user_id),
INDEX idx_notif_dibaca (dibaca),
CONSTRAINT fk_notif_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
-- ============================================================
-- DATA DUMMY
-- ============================================================
INSERT IGNORE INTO users (role_id, nama, username, email, password_hash) VALUES
(1, 'Administrator', 'admin', 'admin@webgis.local', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'),
(2, 'H. Ahmad Fauzi', 'pengurus1', 'pengurus1@webgis.local', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'),
(3, 'Camat Pontianak Selatan','pimpinan1', 'pimpinan@webgis.local', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'),
(4, 'Budi Santoso', 'warga1', 'warga1@webgis.local', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi');
INSERT IGNORE INTO rumah_ibadah (nama, jenis, kontak, radius_meter, latitude, longitude, alamat, kelurahan, kecamatan) VALUES
('Masjid Al-Falah', 'Masjid', '08113001234', 400, -0.0200, 109.3410, 'Jl. Gajah Mada No. 1', 'Benua Melayu Darat', 'Pontianak Selatan'),
('Masjid Raya Mujahidin', 'Masjid', '08113005678', 600, -0.0250, 109.3430, 'Jl. Ahmad Yani No. 10', 'Akcaya', 'Pontianak Selatan'),
('GKE Pontianak', 'Gereja Protestan', '08113009999', 300, -0.0280, 109.3480, 'Jl. Nusa Indah', 'Sungai Bangkong', 'Pontianak Kota'),
('Vihara Bodhicitta', 'Vihara', '08113004321', 250, -0.0310, 109.3520, 'Jl. Tanjungpura No. 5', 'Mariana', 'Pontianak Kota');
INSERT IGNORE INTO keluarga (no_kk, nama_kk, alamat, kelurahan, kecamatan, latitude, longitude, status_ekonomi, rumah_ibadah_id) VALUES
('6171010101010001', 'Ahmad Firdaus', 'Jl. Melati No. 3', 'Benua Melayu Darat', 'Pontianak Selatan', -0.0210, 109.3415, 'miskin', 1),
('6171010101010002', 'Siti Rahma', 'Jl. Kenanga No. 5', 'Benua Melayu Darat', 'Pontianak Selatan', -0.0220, 109.3420, 'miskin', 1),
('6171010101010003', 'Budi Hermawan','Jl. Mawar No. 7', 'Akcaya', 'Pontianak Selatan', -0.0240, 109.3435, 'rentan', 2),
('6171010101010004', 'Maria Goretti','Jl. Nusa Indah No.2', 'Sungai Bangkong', 'Pontianak Kota', -0.0275, 109.3475, 'miskin', 3),
('6171010101010005', 'Tan Ah Kow', 'Jl. Tanjungpura 12', 'Mariana', 'Pontianak Kota', -0.0305, 109.3515, 'rentan', 4);
INSERT IGNORE INTO penduduk (nik, id_keluarga, nama_lengkap, jenis_kelamin, tanggal_lahir, status_keluarga, status_perkawinan, status_ekonomi, pekerjaan, penghasilan, kondisi_kesehatan, alamat, latitude, longitude) VALUES
('6171010101010001', 1, 'Ahmad Firdaus', 'L', '1980-03-15', 'kepala_keluarga', 'kawin', 'miskin', 'Buruh Harian', 800000, 'sehat', 'Jl. Melati No. 3', -0.0210, 109.3415),
('6171010101010002', 1, 'Siti Aisyah', 'P', '1983-07-22', 'istri', 'kawin', 'miskin', 'Ibu Rumah Tangga', 0, 'sehat', 'Jl. Melati No. 3', -0.0210, 109.3415),
('6171010101010003', 1, 'Reza Firdaus', 'L', '2010-11-05', 'anak', 'belum_kawin', 'miskin', 'Pelajar', 0, 'sehat', 'Jl. Melati No. 3', -0.0210, 109.3415),
('6171020202020001', 2, 'Siti Rahma', 'P', '1975-01-10', 'kepala_keluarga', 'cerai_mati', 'miskin', 'Pedagang Kecil', 500000, 'sakit_ringan','Jl. Kenanga No. 5', -0.0220, 109.3420),
('6171020202020002', 2, 'Dewi Rahma', 'P', '2008-05-30', 'anak', 'belum_kawin', 'miskin', 'Pelajar', 0, 'sehat', 'Jl. Kenanga No. 5', -0.0220, 109.3420),
('6171030303030001', 3, 'Budi Hermawan', 'L', '1990-09-20', 'kepala_keluarga', 'kawin', 'rentan', 'Ojek Online', 1500000, 'sehat', 'Jl. Mawar No. 7', -0.0240, 109.3435),
('6171040404040001', 4, 'Yohanes Goretti', 'L', '1970-12-01', 'kepala_keluarga', 'kawin', 'miskin', 'Tidak bekerja', 0, 'sakit_parah', 'Jl. Nusa Indah No.2', -0.0275, 109.3475),
('6171050505050001', 5, 'Tan Ah Kow', 'L', '1968-06-18', 'kepala_keluarga', 'kawin', 'rentan', 'Pedagang', 1200000, 'sehat', 'Jl. Tanjungpura 12', -0.0305, 109.3515);
INSERT IGNORE INTO histori_bantuan (nik, id_jenis_bantuan, id_keluarga, rumah_ibadah_id, tanggal, jumlah, status_penyaluran, catatan) VALUES
('6171010101010001', 1, 1, 1, CURDATE() - INTERVAL 30 DAY, '1 paket sembako', 'disalurkan', 'Penyaluran rutin bulan lalu'),
('6171020202020001', 2, 2, 1, CURDATE() - INTERVAL 15 DAY, 'Rp 500.000', 'disalurkan', 'Bantuan pendidikan anak'),
('6171040404040001', 3, 4, 3, CURDATE() - INTERVAL 7 DAY, '1 kali pemeriksaan', 'disalurkan', 'Kunjungan kesehatan darurat');
INSERT IGNORE INTO laporan (nama_pelapor, nik_terdampak, deskripsi, kategori, tingkat_urgensi, latitude, longitude, alamat, status) VALUES
('Anonim', '6171020202020001', 'Ibu Siti sudah 3 hari tidak makan, anaknya juga sakit', 'darurat_pangan', 'darurat', -0.0220, 109.3420, 'Jl. Kenanga No. 5', 'pending'),
('Pak RT 03', NULL, 'Ada lansia sebatang kara tidak ada yang merawat di RT 03','butuh_bantuan_lain','tinggi', -0.0265, 109.3460, 'RT 03 Sungai Bangkong','diverifikasi'),
('Maria Magdalena', '6171040404040001', 'Pak Yohanes sakit parah, tidak ada biaya berobat', 'darurat_kesehatan', 'darurat', -0.0275, 109.3475, 'Jl. Nusa Indah No.2', 'diproses');
+11
View File
@@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/php/koneksi.php';
try {
// Tambahkan kolom pelapor_id jika belum ada
$pdo->exec("ALTER TABLE laporan ADD COLUMN pelapor_id INT UNSIGNED NULL AFTER nama_pelapor");
$pdo->exec("ALTER TABLE laporan ADD CONSTRAINT fk_lap_pelapor FOREIGN KEY (pelapor_id) REFERENCES users(id) ON DELETE SET NULL");
echo "Kolom pelapor_id berhasil ditambahkan.\n";
} catch (PDOException $e) {
echo "Error atau kolom sudah ada: " . $e->getMessage() . "\n";
}
View File
+10
View File
@@ -0,0 +1,10 @@
FROM php:8.2-apache
RUN docker-php-ext-install mysqli pdo pdo_mysql \
&& a2enmod rewrite
COPY . /var/www/html/
RUN mkdir -p /var/www/html/uploads \
&& chown -R www-data:www-data /var/www/html/uploads \
&& chown -R www-data:www-data /var/www/html
+32
View File
@@ -0,0 +1,32 @@
# WebGIS Sebaran Penduduk Miskin + Rumah Ibadah
Stack: PHP (XAMPP), MySQL/MariaDB, LeafletJS + Leaflet.draw (tanpa framework).
## Fitur
- Peta interaktif (fullscreen) + layer: rumah ibadah, radius, penduduk miskin.
- CRUD lewat aksi di peta (klik untuk point, popup untuk edit/hapus, edit lokasi + drag).
- Reverse geocoding (server proxy `php/reverse_geocode.php` ke Nominatim).
- Auto-relasi penduduk miskin → rumah ibadah terdekat **dalam radius** (jarak dihitung otomatis).
- Marker penduduk miskin:
- Merah: Belum dibantu
- Kuning: Menunggu verifikasi
- Hijau: Sudah dibantu
- Outline: menunjukkan inside/outside radius rumah ibadah
- Form penduduk miskin mendukung anggota keluarga (dinamis) + upload bukti (jpg/png/webp/pdf).
- Admin panel (login) + CRUD dasar + import/export CSV.
## Instalasi (XAMPP)
1. Pastikan folder project berada di `htdocs`, misalnya: `C:\\xampp\\htdocs\\webgis-spbu`.
2. Buat database `webgis_spbu` di phpMyAdmin.
3. Import schema: `sql/schema_webgis_spbu.sql`.
4. Sesuaikan konfigurasi DB di `php/koneksi.php`.
5. Jalankan Apache + MySQL di XAMPP.
6. Buka aplikasi:
- WebGIS: `http://localhost/webgis-spbu/`
- Admin: `http://localhost/webgis-spbu/admin/login.php`
- Jika belum ada user, jalankan sekali: `http://localhost/webgis-spbu/admin/init_admin.php`
## Catatan Upload Bukti
- File disimpan ke `uploads/bukti/`.
- Pastikan folder ini writable oleh Apache.
+18
View File
@@ -0,0 +1,18 @@
<?php
session_start();
function is_logged_in() {
return isset($_SESSION['user']) && is_array($_SESSION['user']);
}
function require_login() {
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
}
function current_user() {
return $_SESSION['user'] ?? null;
}
+39
View File
@@ -0,0 +1,39 @@
<?php
require_once __DIR__ . '/../php/koneksi.php';
require_once __DIR__ . '/_auth.php';
function h($v) { return htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8'); }
function render_header($title) {
$u = current_user();
$name = $u ? h($u['name'] ?? $u['username'] ?? '') : '';
echo '<!doctype html><html lang="id"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">';
echo '<title>' . h($title) . '</title>';
echo '<link rel="stylesheet" href="../css/style.css">';
echo '<style>
body{background:#0b1220;margin:0}
.admin-shell{max-width:1100px;margin:0 auto;padding:22px}
.admin-top{display:flex;align-items:center;justify-content:space-between;color:#e5e7eb;margin-bottom:16px}
.admin-card{background:#fff;border-radius:18px;box-shadow:0 18px 45px rgba(0,0,0,.25);padding:18px}
.admin-nav a{color:#e5e7eb;text-decoration:none;margin-right:12px;font-weight:800}
.admin-nav a:hover{text-decoration:underline}
table{width:100%;border-collapse:collapse}
th,td{padding:10px 10px;border-bottom:1px solid #e2e8f0;font-size:14px;text-align:left;vertical-align:top}
th{font-weight:900}
.tag{display:inline-block;padding:4px 8px;border-radius:999px;background:#eef2ff;color:#1d4ed8;font-weight:900;font-size:12px}
.actions{display:flex;gap:8px;flex-wrap:wrap}
.actions a,.actions button{border:0;border-radius:12px;padding:8px 10px;font-weight:900;cursor:pointer}
.actions a{background:#0f172a;color:#fff;text-decoration:none}
.actions button.danger{background:#dc2626;color:#fff}
.actions a.secondary{background:#e2e8f0;color:#0f172a}
</style></head><body>';
echo '<div class="admin-shell">';
echo '<div class="admin-top"><div><div style="font-size:22px;font-weight:900;color:#fff">Admin Panel</div><div class="admin-nav">';
echo '<a href="index.php">Dashboard</a><a href="rumah_ibadah.php">Rumah Ibadah</a><a href="penduduk_miskin.php">Penduduk Miskin</a>';
echo '</div></div><div style="color:#cbd5e1;font-weight:800">' . $name . ' <a href="logout.php" style="color:#93c5fd;text-decoration:none">Logout</a></div></div>';
}
function render_footer() {
echo '</div></body></html>';
}
+53
View File
@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login();
// Stats
$counts = [
'rumah_ibadah' => 0,
'penduduk' => 0,
'dibantu' => 0,
'belum' => 0,
'verifikasi' => 0
];
try {
$counts['rumah_ibadah'] = (int)$pdo->query("SELECT COUNT(*) FROM rumah_ibadah_points")->fetchColumn();
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('status_bantuan', $cols, true);
$counts['penduduk'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points")->fetchColumn();
if ($hasNew) {
$counts['dibantu'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Sudah dibantu'")->fetchColumn();
$counts['belum'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Belum dibantu'")->fetchColumn();
$counts['verifikasi'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Menunggu verifikasi'")->fetchColumn();
}
} catch (Exception $e) {
}
render_header('Admin Dashboard');
?>
<div class="admin-card">
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px">
<div style="background:#0b1220;border-radius:18px;padding:14px;color:#fff">
<div style="font-weight:900;font-size:28px"><?= (int)$counts['rumah_ibadah'] ?></div>
<div style="color:#cbd5e1;font-weight:800">Rumah Ibadah</div>
</div>
<div style="background:#0b1220;border-radius:18px;padding:14px;color:#fff">
<div style="font-weight:900;font-size:28px"><?= (int)$counts['penduduk'] ?></div>
<div style="color:#cbd5e1;font-weight:800">Penduduk Miskin</div>
</div>
<div style="background:#052e1a;border-radius:18px;padding:14px;color:#fff">
<div style="font-weight:900;font-size:28px"><?= (int)$counts['dibantu'] ?></div>
<div style="color:#bbf7d0;font-weight:800">Sudah dibantu</div>
</div>
<div style="background:#3b0a0a;border-radius:18px;padding:14px;color:#fff">
<div style="font-weight:900;font-size:28px"><?= (int)$counts['belum'] ?></div>
<div style="color:#fecaca;font-weight:800">Belum dibantu</div>
</div>
</div>
<div style="margin-top:14px;color:#475569;font-weight:800">
Menu admin ini melengkapi WebGIS utama: `index.html`.
</div>
</div>
<?php render_footer(); ?>
+34
View File
@@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/../php/koneksi.php';
header('Content-Type: text/html; charset=utf-8');
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
username VARCHAR(60) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'operator',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
$count = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
if ($count > 0) {
echo "Users sudah ada. Silakan login di <a href='login.php'>login.php</a>.";
exit;
}
$username = 'admin';
$password = 'admin123';
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (name, username, password_hash, role) VALUES (:n,:u,:p,'admin')");
$stmt->execute([':n' => 'Administrator', ':u' => $username, ':p' => $hash]);
echo "Admin dibuat. Username: <b>admin</b>, Password: <b>admin123</b>. Lanjut login: <a href='login.php'>login.php</a>";
} catch (Exception $e) {
echo "Gagal init admin: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
}
+62
View File
@@ -0,0 +1,62 @@
<?php
require_once __DIR__ . '/../php/koneksi.php';
require_once __DIR__ . '/_auth.php';
if (is_logged_in()) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = (string)($_POST['password'] ?? '');
try {
$stmt = $pdo->prepare("SELECT id, name, username, password_hash, role FROM users WHERE username = :u LIMIT 1");
$stmt->execute([':u' => $username]);
$u = $stmt->fetch();
if ($u && password_verify($password, $u['password_hash'])) {
$_SESSION['user'] = ['id' => (int)$u['id'], 'name' => $u['name'], 'username' => $u['username'], 'role' => $u['role']];
header('Location: index.php');
exit;
}
$error = 'Username atau password salah.';
} catch (Exception $e) {
$error = $e->getMessage();
}
}
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Login Admin</title>
<link rel="stylesheet" href="../css/style.css">
<style>
body{background:#0b1220;margin:0;display:grid;place-items:center;min-height:100vh;padding:18px}
.card{background:#fff;border-radius:22px;box-shadow:0 18px 45px rgba(0,0,0,.25);width:100%;max-width:420px;padding:18px}
.title{font-weight:900;font-size:22px;margin:0 0 4px}
.sub{color:#64748b;margin:0 0 14px;font-weight:700}
.err{background:#fef2f2;border:1px solid #fecaca;color:#991b1b;padding:10px 12px;border-radius:14px;font-weight:800;margin-bottom:12px}
.btnx{width:100%;margin-top:12px}
a{color:#2563eb;text-decoration:none;font-weight:900}
</style>
</head>
<body>
<div class="card">
<div class="title">Login Admin</div>
<div class="sub">Jika belum ada user, jalankan <a href="init_admin.php">init_admin.php</a>.</div>
<?php if ($error): ?><div class="err"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
<form method="post">
<label>Username</label>
<input name="username" required>
<label style="margin-top:10px;display:block;">Password</label>
<input name="password" type="password" required>
<button class="btn btn-primary btnx" type="submit">Masuk</button>
</form>
</div>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/_auth.php';
session_destroy();
header('Location: login.php');
exit;
+286
View File
@@ -0,0 +1,286 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login();
function csv_cell($v) {
$s = (string)($v ?? '');
$s = str_replace('"', '""', $s);
return '"' . $s . '"';
}
// Export CSV
if (isset($_GET['export'])) {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="penduduk_miskin.csv"');
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
$out = fopen('php://output', 'w');
if ($hasNew) {
fputcsv($out, ['id','kk_nama','nik','jumlah_anggota','lat','lng','alamat','kelurahan','kecamatan','status_bantuan','jenis_bantuan','tanggal_bantuan','rumah_ibadah_id','jarak_meter']);
$rows = $pdo->query("SELECT id, kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan, rumah_ibadah_id, jarak_meter FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll();
foreach ($rows as $r) {
fputcsv($out, [
$r['id'], $r['kk_nama'], $r['nik'], $r['jumlah_anggota'],
$r['latitude'], $r['longitude'], $r['address'], $r['kelurahan'], $r['kecamatan'],
$r['status_bantuan'], $r['jenis_bantuan'], $r['tanggal_bantuan'],
$r['rumah_ibadah_id'], $r['jarak_meter']
]);
}
} else {
fputcsv($out, ['id','nama','lat','lng','alamat','rumah_ibadah_id']);
$rows = $pdo->query("SELECT id, nama, latitude, longitude, address, rumah_ibadah_id FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll();
foreach ($rows as $r) fputcsv($out, [$r['id'],$r['nama'],$r['latitude'],$r['longitude'],$r['address'],$r['rumah_ibadah_id']]);
}
fclose($out);
exit;
}
$msg = '';
$err = '';
// Delete
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
try {
$stmt = $pdo->prepare("DELETE FROM pemukiman_miskin_points WHERE id=:id");
$stmt->execute([':id' => $id]);
$msg = 'Berhasil dihapus.';
} catch (Exception $e) {
$err = $e->getMessage();
}
}
// Import CSV (minimal)
if (isset($_POST['import_csv'])) {
if (!isset($_FILES['csv_file']) || ($_FILES['csv_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
$err = 'CSV tidak ditemukan.';
} else {
try {
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
$fp = fopen($_FILES['csv_file']['tmp_name'], 'r');
$header = fgetcsv($fp);
if (!$header) throw new Exception('Header CSV kosong.');
$map = [];
foreach ($header as $i => $hcol) $map[strtolower(trim($hcol))] = $i;
$pdo->beginTransaction();
$ins = $hasNew
? $pdo->prepare("INSERT INTO pemukiman_miskin_points (kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan) VALUES (:kk,:nik,:j,:lat,:lng,:a,:kel,:kec,:s,:jb,:tb)")
: $pdo->prepare("INSERT INTO pemukiman_miskin_points (nama, latitude, longitude, address) VALUES (:kk,:lat,:lng,:a)");
$n = 0;
while (($row = fgetcsv($fp)) !== false) {
$kk = $row[$map['kk_nama'] ?? $map['nama'] ?? -1] ?? '';
$lat = $row[$map['lat'] ?? $map['latitude'] ?? -1] ?? '';
$lng = $row[$map['lng'] ?? $map['longitude'] ?? -1] ?? '';
if (trim($kk) === '' || trim($lat) === '' || trim($lng) === '') continue;
if ($hasNew) {
$ins->execute([
':kk' => $kk,
':nik' => ($row[$map['nik'] ?? -1] ?? '') ?: null,
':j' => (int)($row[$map['jumlah_anggota'] ?? -1] ?? 0),
':lat' => $lat,
':lng' => $lng,
':a' => ($row[$map['alamat'] ?? $map['address'] ?? -1] ?? '') ?: null,
':kel' => ($row[$map['kelurahan'] ?? -1] ?? '') ?: null,
':kec' => ($row[$map['kecamatan'] ?? -1] ?? '') ?: null,
':s' => ($row[$map['status_bantuan'] ?? -1] ?? 'Belum dibantu') ?: 'Belum dibantu',
':jb' => ($row[$map['jenis_bantuan'] ?? -1] ?? '') ?: null,
':tb' => ($row[$map['tanggal_bantuan'] ?? -1] ?? '') ?: null
]);
} else {
$ins->execute([
':kk' => $kk,
':lat' => $lat,
':lng' => $lng,
':a' => ($row[$map['alamat'] ?? $map['address'] ?? -1] ?? '') ?: null
]);
}
$n++;
}
$pdo->commit();
fclose($fp);
$msg = "Import selesai: $n baris.";
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
$err = $e->getMessage();
}
}
}
// Simple edit/create (without anggota details)
if (isset($_POST['save_penduduk'])) {
$id = (int)($_POST['id'] ?? 0);
$kk = trim($_POST['kk_nama'] ?? '');
$nik = trim($_POST['nik'] ?? '');
$jml = (int)($_POST['jumlah_anggota'] ?? 0);
$lat = $_POST['latitude'] ?? null;
$lng = $_POST['longitude'] ?? null;
$addr = trim($_POST['address'] ?? '');
$kel = trim($_POST['kelurahan'] ?? '');
$kec = trim($_POST['kecamatan'] ?? '');
$status = trim($_POST['status_bantuan'] ?? 'Belum dibantu');
$jb = trim($_POST['jenis_bantuan'] ?? '');
$tb = trim($_POST['tanggal_bantuan'] ?? '');
if ($kk === '' || $lat === null || $lng === null) $err = 'Data belum lengkap (nama/lat/lng).';
else {
try {
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
if (!$hasNew) throw new Exception('Schema belum mendukung fitur penduduk detail. Jalankan schema terbaru.');
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE pemukiman_miskin_points SET kk_nama=:kk, nik=:nik, jumlah_anggota=:j, latitude=:lat, longitude=:lng, address=:a, kelurahan=:kel, kecamatan=:kec, status_bantuan=:s, jenis_bantuan=:jb, tanggal_bantuan=:tb WHERE id=:id");
$stmt->execute([':kk'=>$kk,':nik'=>$nik ?: null,':j'=>$jml,':lat'=>$lat,':lng'=>$lng,':a'=>$addr ?: null,':kel'=>$kel ?: null,':kec'=>$kec ?: null,':s'=>$status ?: 'Belum dibantu',':jb'=>$jb ?: null,':tb'=>$tb ?: null,':id'=>$id]);
$msg = 'Berhasil diupdate.';
} else {
$stmt = $pdo->prepare("INSERT INTO pemukiman_miskin_points (kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan) VALUES (:kk,:nik,:j,:lat,:lng,:a,:kel,:kec,:s,:jb,:tb)");
$stmt->execute([':kk'=>$kk,':nik'=>$nik ?: null,':j'=>$jml,':lat'=>$lat,':lng'=>$lng,':a'=>$addr ?: null,':kel'=>$kel ?: null,':kec'=>$kec ?: null,':s'=>$status ?: 'Belum dibantu',':jb'=>$jb ?: null,':tb'=>$tb ?: null]);
$msg = 'Berhasil ditambahkan.';
}
} catch (Exception $e) { $err = $e->getMessage(); }
}
}
$edit = null;
if (isset($_GET['edit'])) {
$id = (int)$_GET['edit'];
$stmt = $pdo->prepare("SELECT * FROM pemukiman_miskin_points WHERE id=:id");
$stmt->execute([':id' => $id]);
$edit = $stmt->fetch() ?: null;
}
$rows = [];
try {
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
if ($hasNew) {
$rows = $pdo->query("
SELECT p.*, r.nama AS rumah_ibadah_nama
FROM pemukiman_miskin_points p
LEFT JOIN rumah_ibadah_points r ON r.id=p.rumah_ibadah_id
ORDER BY p.id ASC
")->fetchAll();
} else {
$rows = $pdo->query("SELECT * FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll();
}
} catch (Exception $e) {}
render_header('Admin Penduduk Miskin');
?>
<div class="admin-card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
<div style="font-weight:900;font-size:18px"><?= $edit ? 'Edit' : 'Tambah' ?> Penduduk Miskin</div>
<div class="actions">
<a class="secondary" href="penduduk_miskin.php">Reset</a>
<a href="penduduk_miskin.php?export=1">Export CSV</a>
</div>
</div>
<?php if ($msg): ?><div class="notice" style="margin-top:12px;border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="margin-top:12px;border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<form method="post" style="margin-top:12px">
<input type="hidden" name="id" value="<?= h($edit['id'] ?? 0) ?>">
<div class="grid-2">
<div>
<label>Nama Kepala Keluarga</label>
<input name="kk_nama" required value="<?= h($edit['kk_nama'] ?? '') ?>">
</div>
<div>
<label>NIK (opsional)</label>
<input name="nik" value="<?= h($edit['nik'] ?? '') ?>">
</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div>
<label>Jumlah Anggota</label>
<input name="jumlah_anggota" type="number" min="0" step="1" value="<?= h($edit['jumlah_anggota'] ?? 0) ?>">
</div>
<div>
<label>Status Bantuan</label>
<select name="status_bantuan">
<?php foreach (['Belum dibantu','Menunggu verifikasi','Sudah dibantu'] as $s): ?>
<option value="<?= h($s) ?>" <?= (($edit['status_bantuan'] ?? 'Belum dibantu') === $s ? 'selected' : '') ?>><?= h($s) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div>
<label>Jenis Bantuan</label>
<input name="jenis_bantuan" value="<?= h($edit['jenis_bantuan'] ?? '') ?>">
</div>
<div>
<label>Tanggal Bantuan</label>
<input name="tanggal_bantuan" type="date" value="<?= h($edit['tanggal_bantuan'] ?? '') ?>">
</div>
</div>
<label style="margin-top:10px;display:block;">Alamat</label>
<input name="address" value="<?= h($edit['address'] ?? '') ?>">
<div class="grid-2" style="margin-top:10px">
<div>
<label>Kelurahan</label>
<input name="kelurahan" value="<?= h($edit['kelurahan'] ?? '') ?>">
</div>
<div>
<label>Kecamatan</label>
<input name="kecamatan" value="<?= h($edit['kecamatan'] ?? '') ?>">
</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div>
<label>Latitude</label>
<input name="latitude" required value="<?= h($edit['latitude'] ?? '') ?>">
</div>
<div>
<label>Longitude</label>
<input name="longitude" required value="<?= h($edit['longitude'] ?? '') ?>">
</div>
</div>
<div class="modal-actions" style="margin-top:12px">
<button class="btn btn-primary" name="save_penduduk" value="1" type="submit"><?= $edit ? 'Update' : 'Tambah' ?></button>
<a class="btn btn-secondary" href="../index.html" style="text-align:center;line-height:38px;text-decoration:none">Kembali ke WebGIS</a>
</div>
</form>
</div>
<div class="admin-card" style="margin-top:14px">
<div style="font-weight:900;font-size:18px;margin-bottom:8px">Import CSV</div>
<form method="post" enctype="multipart/form-data">
<input type="file" name="csv_file" accept=".csv" required>
<button class="btn btn-secondary" name="import_csv" value="1" type="submit" style="margin-top:10px">Import</button>
<div style="margin-top:8px;color:#475569;font-weight:800">Kolom minimal: `kk_nama`/`nama`, `lat`, `lng`.</div>
</form>
</div>
<div class="admin-card" style="margin-top:14px">
<div style="font-weight:900;font-size:18px;margin-bottom:8px">Daftar Penduduk Miskin</div>
<table>
<thead>
<tr>
<th>ID</th><th>Kepala Keluarga</th><th>Status</th><th>Rumah Ibadah</th><th>Koordinat</th><th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= h($r['kk_nama'] ?? $r['nama'] ?? '') ?></td>
<td><span class="tag"><?= h($r['status_bantuan'] ?? '-') ?></span></td>
<td><?= h($r['rumah_ibadah_nama'] ?? '-') ?></td>
<td><?= h(($r['latitude'] ?? '') . ', ' . ($r['longitude'] ?? '')) ?></td>
<td class="actions">
<a class="secondary" href="penduduk_miskin.php?edit=<?= (int)$r['id'] ?>">Edit</a>
<a href="penduduk_miskin.php?delete=<?= (int)$r['id'] ?>" onclick="return confirm('Hapus data ini?')">Hapus</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php render_footer(); ?>
+166
View File
@@ -0,0 +1,166 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login();
$msg = '';
$err = '';
// Delete
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
try {
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah_points WHERE id=:id");
$stmt->execute([':id' => $id]);
$msg = 'Berhasil dihapus.';
} catch (Exception $e) {
$err = $e->getMessage();
}
}
// Save (create/update)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$nama = trim($_POST['nama'] ?? '');
$jenis = trim($_POST['jenis'] ?? 'Lainnya');
$kontak = trim($_POST['kontak'] ?? '');
$radius = (float)($_POST['radius_meter'] ?? 300);
$lat = $_POST['latitude'] ?? null;
$lng = $_POST['longitude'] ?? null;
$address = trim($_POST['address'] ?? '');
if ($nama === '' || $radius <= 0 || $lat === null || $lng === null) {
$err = 'Data belum lengkap (nama/radius/lat/lng).';
} else {
try {
$cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasJenis = in_array('jenis', $cols, true);
$hasKontak = in_array('kontak', $cols, true);
if ($id > 0) {
if ($hasJenis && $hasKontak) {
$stmt = $pdo->prepare("UPDATE rumah_ibadah_points SET nama=:n, jenis=:j, kontak=:k, radius_meter=:r, latitude=:lat, longitude=:lng, address=:a WHERE id=:id");
$stmt->execute([':n'=>$nama,':j'=>$jenis ?: 'Lainnya',':k'=>$kontak ?: null,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null,':id'=>$id]);
} else {
$stmt = $pdo->prepare("UPDATE rumah_ibadah_points SET nama=:n, radius_meter=:r, latitude=:lat, longitude=:lng, address=:a WHERE id=:id");
$stmt->execute([':n'=>$nama,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null,':id'=>$id]);
}
$msg = 'Berhasil diupdate.';
} else {
if ($hasJenis && $hasKontak) {
$stmt = $pdo->prepare("INSERT INTO rumah_ibadah_points (nama, jenis, kontak, radius_meter, latitude, longitude, address) VALUES (:n,:j,:k,:r,:lat,:lng,:a)");
$stmt->execute([':n'=>$nama,':j'=>$jenis ?: 'Lainnya',':k'=>$kontak ?: null,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null]);
} else {
$stmt = $pdo->prepare("INSERT INTO rumah_ibadah_points (nama, radius_meter, latitude, longitude, address) VALUES (:n,:r,:lat,:lng,:a)");
$stmt->execute([':n'=>$nama,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null]);
}
$msg = 'Berhasil ditambahkan.';
}
} catch (Exception $e) {
$err = $e->getMessage();
}
}
}
$edit = null;
if (isset($_GET['edit'])) {
$id = (int)$_GET['edit'];
$stmt = $pdo->prepare("SELECT * FROM rumah_ibadah_points WHERE id=:id");
$stmt->execute([':id' => $id]);
$edit = $stmt->fetch() ?: null;
}
$rows = [];
try {
$rows = $pdo->query("
SELECT r.*,
(SELECT COUNT(*) FROM pemukiman_miskin_points p WHERE p.rumah_ibadah_id=r.id) AS binaan_count
FROM rumah_ibadah_points r
ORDER BY r.id ASC
")->fetchAll();
} catch (Exception $e) {}
render_header('Admin Rumah Ibadah');
?>
<div class="admin-card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
<div style="font-weight:900;font-size:18px"><?= $edit ? 'Edit' : 'Tambah' ?> Rumah Ibadah</div>
<a class="actions a secondary" href="rumah_ibadah.php" style="background:#e2e8f0;color:#0f172a;border-radius:12px;padding:8px 10px;font-weight:900;text-decoration:none">Reset</a>
</div>
<?php if ($msg): ?><div class="notice" style="margin-top:12px;border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="margin-top:12px;border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<form method="post" style="margin-top:12px">
<input type="hidden" name="id" value="<?= h($edit['id'] ?? 0) ?>">
<div class="grid-2">
<div>
<label>Nama</label>
<input name="nama" required value="<?= h($edit['nama'] ?? '') ?>">
</div>
<div>
<label>Jenis</label>
<select name="jenis">
<?php foreach (['Masjid','Musholla','Gereja','Katolik','Pura','Vihara','Klenteng','Lainnya'] as $j): ?>
<option value="<?= h($j) ?>" <?= (($edit['jenis'] ?? 'Lainnya') === $j ? 'selected' : '') ?>><?= h($j) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<label style="margin-top:10px;display:block;">Kontak</label>
<input name="kontak" value="<?= h($edit['kontak'] ?? '') ?>">
<div class="grid-2" style="margin-top:10px">
<div>
<label>Radius (meter)</label>
<input name="radius_meter" type="number" min="1" step="1" required value="<?= h($edit['radius_meter'] ?? 300) ?>">
</div>
<div>
<label>Alamat</label>
<input name="address" value="<?= h($edit['address'] ?? '') ?>">
</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div>
<label>Latitude</label>
<input name="latitude" required value="<?= h($edit['latitude'] ?? '') ?>">
</div>
<div>
<label>Longitude</label>
<input name="longitude" required value="<?= h($edit['longitude'] ?? '') ?>">
</div>
</div>
<div class="modal-actions" style="margin-top:12px">
<button class="btn btn-primary" type="submit"><?= $edit ? 'Update' : 'Tambah' ?></button>
<a class="btn btn-secondary" href="../index.html" style="text-align:center;line-height:38px;text-decoration:none">Kembali ke WebGIS</a>
</div>
</form>
</div>
<div class="admin-card" style="margin-top:14px">
<div style="font-weight:900;font-size:18px;margin-bottom:8px">Daftar Rumah Ibadah</div>
<table>
<thead>
<tr>
<th>ID</th><th>Nama</th><th>Jenis</th><th>Radius</th><th>Binaan</th><th>Koordinat</th><th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= h($r['nama'] ?? '') ?></td>
<td><span class="tag"><?= h($r['jenis'] ?? 'Lainnya') ?></span></td>
<td><?= (int)($r['radius_meter'] ?? 0) ?> m</td>
<td><?= (int)($r['binaan_count'] ?? 0) ?></td>
<td><?= h(($r['latitude'] ?? '') . ', ' . ($r['longitude'] ?? '')) ?></td>
<td class="actions">
<a class="secondary" href="rumah_ibadah.php?edit=<?= (int)$r['id'] ?>">Edit</a>
<a href="rumah_ibadah.php?delete=<?= (int)$r['id'] ?>" onclick="return confirm('Hapus data ini?')">Hapus</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php render_footer(); ?>
+779
View File
@@ -0,0 +1,779 @@
:root {
--bg: #0d1520;
--bg-soft: #10182c;
--bg-card: #141e2e;
--card: #ffffff;
--text: #e5e7eb;
--text-secondary: #cbd5e1;
--muted: #8b94a5;
--line: #e2e8f0;
--primary: #3b82f6;
--primary-dark: #1d4ed8;
--primary-light: #60a5fa;
--secondary: #6b7280;
--danger: #ef4444;
--success: #10b981;
--warning: #f59e0b;
--shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
--shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 25px 60px rgba(0, 0, 0, 0.3);
--radius: 16px;
}
* {
box-sizing: border-box;
}
html, body {
height: 100%;
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #0f172a;
color: #0f172a;
}
body.crosshair #map {
cursor: crosshair;
}
.hidden {
display: none !important;
}
.app-shell {
display: grid;
grid-template-columns: 320px 1fr;
height: 100vh;
}
.sidebar {
background: var(--bg);
color: var(--text);
padding: 20px;
overflow-y: auto;
border-right: 1px solid rgba(255,255,255,0.05);
box-shadow: inset 0 0 40px rgba(0,0,0,0.2);
}
.sidebar::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 6px; }
.btn-back {
display: block;
text-align: center;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
color: var(--text-secondary);
border-radius: 12px;
padding: 10px;
font-size: 13px;
margin-bottom: 20px;
text-decoration: none;
font-weight: 600;
transition: all 0.2s;
}
.btn-back:hover { background: rgba(255,255,255,0.1); color: #fff; }
.brand {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 24px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.brand-badge {
width: 56px;
height: 56px;
border-radius: 14px;
display: grid;
place-items: center;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
color: #fff;
font-weight: 900;
font-size: 16px;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
letter-spacing: -0.5px;
}
.brand h1 {
font-family: 'Outfit', sans-serif;
font-size: 20px;
margin: 0;
font-weight: 800;
background: linear-gradient(to right, #60a5fa, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
.brand p {
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.3px;
}
.mode-info {
padding: 14px 16px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(139, 92, 246, 0.1) 100%);
border: 1px solid rgba(147, 197, 253, 0.3);
border-radius: 12px;
margin-bottom: 22px;
font-size: 14px;
color: #e0f2fe;
font-weight: 600;
}
.action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 24px;
}
.btn {
appearance: none;
border: 0;
padding: 12px 10px;
border-radius: 12px;
font-weight: 600;
font-size: 12px;
cursor: pointer;
transition: all .2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
text-align: center;
}
.btn i { font-size: 16px; margin-bottom: 2px; }
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0,0,0,0.25);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-primary {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #fff;
}
.btn-secondary {
background: rgba(255,255,255,0.03);
color: var(--text-secondary);
border: 1px solid rgba(255,255,255,0.08);
}
.btn-secondary:hover {
background: rgba(255,255,255,0.08);
color: #fff;
border-color: rgba(255,255,255,0.15);
}
.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: #fff;
}
.btn.is-active {
outline: 2px solid rgba(147, 197, 253, 0.6);
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), 0 4px 12px rgba(0,0,0,0.15);
}
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 24px;
}
.stat-card {
background: linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 14px;
padding: 16px 18px;
display: grid;
gap: 4px;
backdrop-filter: blur(8px);
transition: all .2s ease;
}
.stat-value {
font-size: 28px;
font-weight: 900;
color: #fff;
letter-spacing: -0.5px;
}
.stat-label {
color: var(--muted);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.2px;
}
.panel {
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 14px;
padding: 16px 18px;
margin-bottom: 16px;
backdrop-filter: blur(8px);
}
.panel h2 {
font-size: 14px;
margin: 0 0 12px;
color: #fff;
font-weight: 800;
letter-spacing: -0.3px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
margin: 8px 0;
font-size: 13px;
color: var(--muted);
font-weight: 500;
}
.legend-sep {
margin-top: 12px;
}
.swatch {
width: 16px;
height: 16px;
border-radius: 5px;
display: inline-block;
}
.nasional { background: #ef4444; }
.provinsi { background: #3b82f6; }
.kabupaten { background: #22c55e; }
.spbu24 { background: #22c55e; }
.spbuNon { background: #ef4444; }
.shm { background: #f59e0b; }
.hgb { background: #8b5cf6; }
.hgu { background: #14b8a6; }
.hp { background: #94a3b8; }
.ri { background: #7c3aed; }
.pmGreen { background: #16a34a; }
.pmRed { background: #dc2626; }
.help-box {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.05) 100%);
border: 1px solid rgba(147, 197, 253, 0.3);
color: #e0f2fe;
padding: 16px 18px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
line-height: 1.6;
backdrop-filter: blur(8px);
}
.list {
display: grid;
gap: 8px;
max-height: 220px;
overflow: auto;
padding-right: 6px;
}
.list::-webkit-scrollbar {
width: 10px;
}
.list::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.35);
border-radius: 999px;
border: 2px solid rgba(15, 23, 42, 0.3);
}
.list-item {
width: 100%;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
color: #e5e7eb;
border-radius: 12px;
padding: 11px 13px;
cursor: pointer;
transition: all .2s ease;
}
.list-item:hover {
transform: translateX(4px);
border-color: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.08);
}
.list-item.is-active {
border-color: rgba(147, 197, 253, 0.55);
background: rgba(37, 99, 235, 0.16);
}
.list-title {
font-weight: 800;
font-size: 13px;
color: #fff;
letter-spacing: -0.2px;
}
.list-meta {
margin-top: 4px;
font-size: 11px;
color: var(--muted);
font-weight: 500;
}
.list-empty {
font-size: 13px;
color: #cbd5e1;
padding: 10px 12px;
border-radius: 14px;
border: 1px dashed rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.04);
}
.spbu-icon {
background: transparent;
border: 0;
}
.spbu-dot {
width: 14px;
height: 14px;
border-radius: 999px;
border: 2px solid #ffffff;
box-shadow: 0 10px 18px rgba(15, 23, 42, 0.22);
}
.spbu-dot--green { background: #22c55e; }
.spbu-dot--red { background: #ef4444; }
.pm-icon {
background: transparent;
border: 0;
}
.pm-hit {
width: var(--pm-size, 30px);
height: var(--pm-size, 30px);
border-radius: 999px;
background: #0f172a;
border: 2px solid #ffffff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 14px 22px rgba(15, 23, 42, 0.22);
cursor: grab;
touch-action: none;
}
.pm-hit--green {
background: #16a34a;
}
.pm-hit--red {
background: #dc2626;
}
.pm-hit:active {
cursor: grabbing;
}
.pm-svg {
display: block;
}
.ri-icon {
background: transparent;
border: 0;
}
.ri-hit {
width: 24px;
height: 24px;
border-radius: 10px;
background: #7c3aed;
border: 2px solid #ffffff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 18px rgba(15, 23, 42, 0.18);
}
.ri-svg {
display: block;
}
.map-area {
position: relative;
background: #e2e8f0;
}
.map-toolbar {
position: absolute;
top: 14px;
right: 14px;
z-index: 1200;
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(226, 232, 240, 0.9);
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.12);
backdrop-filter: blur(8px);
}
.map-toolbar-row {
display: flex;
gap: 8px;
}
.map-tool {
border: 0;
background: #0f172a;
color: #fff;
padding: 9px 12px;
border-radius: 14px;
cursor: pointer;
font-weight: 800;
font-size: 13px;
transition: transform .15s ease, opacity .15s ease;
}
.map-tool:hover {
transform: translateY(-1px);
opacity: 0.96;
}
.map-tool:disabled {
opacity: 0.55;
cursor: not-allowed;
transform: none;
}
.map-tool.is-active {
background: linear-gradient(135deg, #0f172a 0%, #1f2a44 100%);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.12);
}
.edit-bar {
position: absolute;
bottom: 14px;
right: 14px;
z-index: 800;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 10px 10px 12px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.14);
backdrop-filter: blur(8px);
}
.edit-bar-title {
font-weight: 900;
font-size: 13px;
color: #0f172a;
margin-right: 2px;
}
.edit-bar-btn {
border: 0;
background: #e2e8f0;
color: #0f172a;
padding: 9px 12px;
border-radius: 14px;
cursor: pointer;
font-weight: 900;
font-size: 13px;
transition: transform .15s ease, opacity .15s ease;
}
.edit-bar-btn.primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: #fff;
}
.edit-bar-btn.danger {
background: linear-gradient(135deg, var(--danger), #b91c1c);
color: #fff;
}
.edit-bar-btn.is-active {
outline: 2px solid rgba(15, 23, 42, 0.22);
box-shadow: 0 0 0 5px rgba(239, 68, 68, 0.12), 0 14px 30px rgba(15, 23, 42, 0.12);
}
.edit-bar-btn:hover {
transform: translateY(-1px);
opacity: 0.96;
}
.edit-bar-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
#map {
width: 100%;
height: 100%;
position: relative;
z-index: 0;
}
.leaflet-control-container .leaflet-draw {
display: none !important;
}
.modal {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.54);
display: grid;
place-items: center;
z-index: 9999;
padding: 20px;
}
.modal-card {
width: 100%;
max-width: 540px;
background: #fff;
border-radius: 22px;
box-shadow: var(--shadow);
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, #f8fafc 0%, #fff 100%);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
}
.close-btn {
border: 0;
background: transparent;
font-size: 30px;
line-height: 1;
cursor: pointer;
color: #475569;
}
#featureForm {
padding: 18px 20px 20px;
}
.form-section {
display: grid;
gap: 10px;
}
.form-section h4 {
margin: 0 0 6px;
font-size: 15px;
color: #0f172a;
}
label {
font-size: 13px;
font-weight: 700;
color: #334155;
}
input, textarea, select {
width: 100%;
border: 1px solid #cbd5e1;
background: #fff;
border-radius: 14px;
padding: 11px 12px;
font: inherit;
color: #0f172a;
}
input[readonly] {
background: #f8fafc;
color: #475569;
}
.checkbox-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
font-weight: 600;
color: var(--text-secondary);
background: rgba(255,255,255,0.03);
border-radius: 10px;
margin-bottom: 6px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.checkbox-row:hover { background: rgba(255,255,255,0.06); color: #fff; }
.checkbox-row input {
accent-color: var(--primary);
width: 16px;
height: 16px;
cursor: pointer;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.notice {
margin: 0 0 14px;
padding: 12px 14px;
border-radius: 14px;
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
font-size: 14px;
line-height: 1.45;
}
.modal-actions {
display: flex;
gap: 10px;
margin-top: 18px;
}
.modal-actions .btn {
flex: 1;
box-shadow: none;
}
.leaflet-popup-content-wrapper {
border-radius: 14px;
}
.leaflet-popup-content {
margin: 12px 14px;
font-size: 13px;
line-height: 1.5;
}
.popup-actions {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.popup-btn {
border: 0;
padding: 7px 10px;
border-radius: 10px;
cursor: pointer;
font-weight: 700;
color: #fff;
}
.popup-btn.edit { background: #2563eb; }
.popup-btn.spatial { background: #7c3aed; }
.popup-btn.delete { background: #dc2626; }
.toast {
position: fixed;
left: calc(320px + 16px);
bottom: 16px;
z-index: 10000;
max-width: min(520px, calc(100vw - 32px));
padding: 12px 14px;
border-radius: 16px;
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.22);
border: 1px solid rgba(226, 232, 240, 0.9);
background: rgba(255, 255, 255, 0.96);
color: #0f172a;
font-weight: 700;
line-height: 1.35;
cursor: pointer;
word-break: break-word;
}
.toast.toast-success {
border-color: rgba(34, 197, 94, 0.35);
}
.toast.toast-error {
border-color: rgba(239, 68, 68, 0.35);
}
.toast.toast-info {
border-color: rgba(59, 130, 246, 0.35);
}
@media (max-width: 960px) {
.app-shell {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
padding: 20px;
}
.action-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stats {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.map-area {
min-height: 60vh;
}
.map-toolbar {
top: 10px;
right: 10px;
left: 10px;
justify-content: space-between;
}
.edit-bar {
left: 10px;
right: 10px;
bottom: 10px;
justify-content: space-between;
}
.toast {
left: 16px;
}
}
+61
View File
@@ -0,0 +1,61 @@
const https = require('https');
const fs = require('fs');
const kecamatans = [
"Kecamatan Pontianak Barat",
"Kecamatan Pontianak Kota",
"Kecamatan Pontianak Selatan",
"Kecamatan Pontianak Tenggara",
"Kecamatan Pontianak Timur",
"Kecamatan Pontianak Utara"
];
const featureCollection = {
type: "FeatureCollection",
features: []
};
let completed = 0;
function fetchGeojson(name) {
const url = `https://nominatim.openstreetmap.org/search.php?q=${encodeURIComponent(name)}&polygon_geojson=1&format=jsonv2`;
const options = {
headers: { 'User-Agent': 'WebGIS-SPBU-App/1.0' }
};
https.get(url, options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const results = JSON.parse(data);
const geomResult = results.find(r => r.geojson && r.category === 'boundary' && r.type === 'administrative');
if (geomResult) {
featureCollection.features.push({
type: "Feature",
properties: {
Kecamatan: name.replace('Kecamatan ', ''),
Name: name
},
geometry: geomResult.geojson
});
console.log("Fetched " + name);
} else {
console.log("Not found for " + name);
}
completed++;
if (completed === kecamatans.length) {
fs.writeFileSync('js/pontianak_kecamatan.geojson', JSON.stringify(featureCollection, null, 2));
console.log("All done! Saved to js/pontianak_kecamatan.geojson");
}
});
}).on('error', (e) => {
console.error(e);
});
}
// Nominatim has a usage policy of 1 request per second. We will stagger the requests.
kecamatans.forEach((kec, i) => {
setTimeout(() => {
fetchGeojson(kec);
}, i * 1500); // 1.5 seconds apart
});
+29
View File
@@ -0,0 +1,29 @@
const https = require('https');
const fs = require('fs');
const query = `[out:json];
area["name"="Pontianak"]->.searchArea;
(
relation["admin_level"="6"](area.searchArea);
);
out geom;`;
const options = {
hostname: 'overpass-api.de',
path: '/api/interpreter',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
fs.writeFileSync('osm_pontianak.json', data);
console.log('Done downloading OSM data.');
});
});
req.write(query);
req.end();
+359
View File
@@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>WebGIS SPBU, Jalan, Parsil, Rumah Ibadah, dan Pemukiman</title>
<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="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<div class="app-shell">
<!-- SIDEBAR -->
<aside class="sidebar">
<a href="../index.html" class="btn btn-back"><i class="fa-solid fa-arrow-left"></i> Kembali ke Landing Page</a>
<div class="brand">
<div class="brand-badge" aria-hidden="true"><i class="fa-solid fa-map-location-dot"></i></div>
<div>
<h1>WebGIS SPBU</h1>
<p>Infrastruktur & Pemukiman</p>
</div>
</div>
<div id="modeInfo" class="mode-info">Mode: Tidak aktif</div>
<div class="action-grid">
<button id="btnModePoint" class="btn btn-primary" type="button" onclick="setMode('point')"><i class="fa-solid fa-gas-pump"></i> Tambah Point</button>
<button id="btnModeJalan" class="btn btn-secondary" type="button" onclick="setMode('jalan')"><i class="fa-solid fa-road"></i> Tambah Jalan</button>
<button id="btnModeParsil" class="btn btn-secondary" type="button" onclick="setMode('parsil')"><i class="fa-solid fa-draw-polygon"></i> Tambah Parsil</button>
<button id="btnModeRumahIbadah" class="btn btn-secondary" type="button" onclick="setMode('rumahIbadah')"><i class="fa-solid fa-mosque"></i> Rumah Ibadah</button>
<button id="btnModePemukiman" class="btn btn-secondary" type="button" onclick="setMode('pemukiman')"><i class="fa-solid fa-house-chimney-crack"></i> Pemukiman</button>
<button id="btnResetMode" class="btn btn-danger" type="button" onclick="resetMode()"><i class="fa-solid fa-xmark"></i> Batal Mode</button>
</div>
<div class="stats" aria-label="Statistik layer">
<div class="stat-card">
<div class="stat-value" id="countPoints">0</div>
<div class="stat-label">Point SPBU</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countJalan">0</div>
<div class="stat-label">Jalan</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countParsil">0</div>
<div class="stat-label">Parsil</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countRumahIbadah">0</div>
<div class="stat-label">Rumah Ibadah</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countPemukiman">0</div>
<div class="stat-label">Pemukiman Miskin</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countSektor">0</div>
<div class="stat-label">Sektor Kecamatan</div>
</div>
</div>
<div class="panel layer-panel">
<h2><i class="fa-solid fa-layer-group"></i> Layer Peta</h2>
<label class="checkbox-row">
<input type="checkbox" id="togglePoints" checked>
<span class="chk-label">Point SPBU</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="toggleJalan" checked>
<span class="chk-label">Jaringan Jalan</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="toggleParsil" checked>
<span class="chk-label">Batas Parsil</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="toggleRumahIbadah" checked>
<span class="chk-label">Rumah Ibadah (+ radius)</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="togglePemukiman" checked>
<span class="chk-label">Pemukiman Miskin</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="toggleSektor" checked>
<span class="chk-label">Sektor Kecamatan</span>
</label>
</div>
<div class="panel">
<h2>Daftar SPBU</h2>
<div id="pointsList" class="list" aria-label="Daftar point SPBU"></div>
</div>
<div class="panel">
<h2>Daftar Jalan</h2>
<div id="jalanList" class="list" aria-label="Daftar jalan"></div>
</div>
<div class="panel">
<h2>Daftar Parsil</h2>
<div id="parsilList" class="list" aria-label="Daftar parsil"></div>
</div>
<div class="panel">
<h2>Daftar Rumah Ibadah</h2>
<div id="rumahIbadahList" class="list" aria-label="Daftar rumah ibadah"></div>
</div>
<div class="panel">
<h2>Daftar Pemukiman Miskin</h2>
<div id="pemukimanList" class="list" aria-label="Daftar pemukiman miskin"></div>
</div>
<div class="panel">
<h2>Daftar Sektor Kecamatan</h2>
<div id="sektorList" class="list" aria-label="Daftar sektor kecamatan"></div>
</div>
<div class="panel">
<h2>Legenda</h2>
<div class="legend-item"><span class="swatch spbu24"></span> SPBU 24 Jam</div>
<div class="legend-item"><span class="swatch spbuNon"></span> SPBU Tidak 24 Jam</div>
<div class="legend-item"><span class="swatch nasional"></span> Jalan Nasional</div>
<div class="legend-item"><span class="swatch provinsi"></span> Jalan Provinsi</div>
<div class="legend-item"><span class="swatch kabupaten"></span> Jalan Kabupaten</div>
<div class="legend-item legend-sep"><span class="swatch shm"></span> Parsil SHM</div>
<div class="legend-item"><span class="swatch hgb"></span> Parsil HGB</div>
<div class="legend-item"><span class="swatch hgu"></span> Parsil HGU</div>
<div class="legend-item"><span class="swatch hp"></span> Parsil HP</div>
<div class="legend-item legend-sep"><span class="swatch ri"></span> Rumah Ibadah</div>
<div class="legend-item"><span class="swatch pmGreen"></span> Pemukiman (dalam radius bantuan)</div>
<div class="legend-item"><span class="swatch pmRed"></span> Pemukiman (di luar radius bantuan)</div>
</div>
<div id="helpBox" class="help-box">
Pilih mode di atas, lalu:
<br><b>Point</b>: klik lokasi di peta.
<br><b>Jalan</b>: gambar garis, double-click untuk selesai.
<br><b>Parsil</b>: gambar poligon, klik titik awal untuk selesai.
<br><b>Rumah Ibadah</b>: klik lokasi di peta.
<br><b>Pemukiman</b>: klik lokasi di peta.
<br><br>Klik objek di peta untuk <b>Edit</b> / <b>Hapus</b> (CRUD).
</div>
</aside>
<!-- MAP -->
<main class="map-area">
<div class="map-toolbar" aria-label="Toolbar peta">
<div class="map-toolbar-row">
<button class="map-tool" id="btnResetView" type="button">Reset View</button>
<button class="map-tool" id="btnMyLocation" type="button">Lokasi Saya</button>
</div>
<div class="map-toolbar-row">
<button class="map-tool" id="btnFilter24" type="button">SPBU 24 Jam</button>
<button class="map-tool" id="btnFilterNon24" type="button">SPBU Tidak 24 Jam</button>
</div>
</div>
<div id="editBar" class="edit-bar hidden" aria-label="Edit lokasi">
<div class="edit-bar-title" id="editBarTitle">Edit Lokasi</div>
<button class="edit-bar-btn danger hidden" id="btnDeleteVertex" type="button">Hapus Vertex</button>
<button class="edit-bar-btn primary" id="btnSaveLocation" type="button">Simpan Lokasi</button>
<button class="edit-bar-btn" id="btnCancelLocation" type="button">Batal</button>
</div>
<div id="map"></div>
</main>
</div>
<!-- MODAL -->
<div id="featureModal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal-card">
<div class="modal-header">
<h3 id="modalTitle">Input Data</h3>
<button type="button" class="close-btn" onclick="closeModal()" aria-label="Tutup">&times;</button>
</div>
<form id="featureForm">
<p id="formNotice" class="notice hidden"></p>
<!-- POINT -->
<div id="pointFields" class="form-section hidden">
<h4>Point SPBU</h4>
<label for="namaPoint">Nama</label>
<input id="namaPoint" placeholder="Contoh: SPBU Sudirman" required>
<label for="noPoint">No</label>
<input id="noPoint" placeholder="Contoh: 64.781.01" required>
<label for="deskripsiPoint">Deskripsi</label>
<textarea id="deskripsiPoint" placeholder="Catatan singkat (opsional)" rows="3"></textarea>
<label class="checkbox-row">
<input type="checkbox" id="status24Jam"> 24 Jam
</label>
<div class="grid-2">
<div>
<label for="latPoint">Latitude</label>
<input id="latPoint" readonly>
</div>
<div>
<label for="lngPoint">Longitude</label>
<input id="lngPoint" readonly>
</div>
</div>
</div>
<!-- JALAN -->
<div id="roadFields" class="form-section hidden">
<h4>Jalan</h4>
<label for="namaJalan">Nama Jalan</label>
<input id="namaJalan" placeholder="Contoh: Jl. Ahmad Yani" required>
<label for="statusJalan">Status</label>
<select id="statusJalan">
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
</select>
</div>
<!-- PARSIL -->
<div id="parsilFields" class="form-section hidden">
<h4>Parsil</h4>
<label for="statusParsil">Status Kepemilikan</label>
<select id="statusParsil">
<option value="SHM">SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
</div>
<!-- RUMAH IBADAH -->
<div id="rumahIbadahFields" class="form-section hidden">
<h4>Rumah Ibadah</h4>
<label for="namaRumahIbadah">Nama</label>
<input id="namaRumahIbadah" placeholder="Contoh: Masjid Al-Ikhlas" required>
<label for="jenisRumahIbadah">Jenis</label>
<select id="jenisRumahIbadah">
<option value="Lainnya">Lainnya</option>
<option value="Masjid">Masjid</option>
<option value="Gereja">Gereja</option>
<option value="Pura">Pura</option>
<option value="Vihara">Vihara</option>
<option value="Klenteng">Klenteng</option>
</select>
<label for="kontakRumahIbadah">Kontak (opsional)</label>
<input id="kontakRumahIbadah" placeholder="No HP / PIC (opsional)">
<label for="radiusRumahIbadah">Radius Bantuan (meter)</label>
<input id="radiusRumahIbadah" type="number" min="1" step="1" placeholder="Contoh: 500" required>
<label for="alamatRumahIbadah">Alamat (opsional)</label>
<textarea id="alamatRumahIbadah" placeholder="Alamat singkat (opsional)" rows="2"></textarea>
<div class="grid-2">
<div>
<label for="latRumahIbadah">Latitude</label>
<input id="latRumahIbadah" readonly>
</div>
<div>
<label for="lngRumahIbadah">Longitude</label>
<input id="lngRumahIbadah" readonly>
</div>
</div>
</div>
<!-- PEMUKIMAN MISKIN -->
<div id="pemukimanFields" class="form-section hidden">
<h4>Pemukiman Miskin</h4>
<label for="kkNamaPemukiman">Nama KK</label>
<input id="kkNamaPemukiman" placeholder="Contoh: Budi" required>
<label for="nikPemukiman">NIK (opsional)</label>
<input id="nikPemukiman" placeholder="Contoh: 6171xxxxxxxxxxxx">
<label for="jumlahAnggotaPemukiman">Jumlah Anggota (opsional)</label>
<input id="jumlahAnggotaPemukiman" type="number" min="0" step="1" placeholder="Contoh: 4">
<label for="statusBantuanPemukiman">Status Bantuan</label>
<select id="statusBantuanPemukiman">
<option value="Belum dibantu">Belum dibantu</option>
<option value="Menunggu verifikasi">Menunggu verifikasi</option>
<option value="Sudah dibantu">Sudah dibantu</option>
</select>
<label for="jenisBantuanPemukiman">Jenis Bantuan (opsional)</label>
<input id="jenisBantuanPemukiman" placeholder="Contoh: Sembako">
<label for="tanggalBantuanPemukiman">Tanggal Bantuan (opsional)</label>
<input id="tanggalBantuanPemukiman" type="date">
<label for="alamatPemukiman">Alamat (opsional)</label>
<textarea id="alamatPemukiman" placeholder="Alamat singkat (opsional)" rows="2"></textarea>
<div class="grid-2">
<div>
<label for="kelurahanPemukiman">Kelurahan (opsional)</label>
<input id="kelurahanPemukiman" placeholder="Contoh: Sungai Jawi">
</div>
<div>
<label for="kecamatanPemukiman">Kecamatan (opsional)</label>
<input id="kecamatanPemukiman" placeholder="Contoh: Pontianak Kota">
</div>
</div>
<div class="grid-2">
<div>
<label for="latPemukiman">Latitude</label>
<input id="latPemukiman" readonly>
</div>
<div>
<label for="lngPemukiman">Longitude</label>
<input id="lngPemukiman" readonly>
</div>
</div>
</div>
<!-- BUTTON -->
<div class="modal-actions">
<button id="btnSubmitFeature" class="btn btn-primary" type="submit">Simpan Data</button>
<button class="btn btn-secondary" type="button" onclick="closeModal()">Batal</button>
</div>
</form>
</div>
</div>
<!-- TOAST -->
<div id="toast" class="toast hidden" aria-live="polite" aria-atomic="true"></div>
<!-- SCRIPT -->
<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="https://cdnjs.cloudflare.com/ajax/libs/leaflet.geometryutil/0.10.1/leaflet.geometryutil.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
<?php
require_once 'php/koneksi.php';
try {
$pdo->exec("ALTER TABLE rumah_ibadah_points ADD COLUMN IF NOT EXISTS jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya' AFTER nama");
$pdo->exec("ALTER TABLE rumah_ibadah_points ADD COLUMN IF NOT EXISTS kontak VARCHAR(120) NULL AFTER jenis");
echo "Columns added successfully.\n";
} catch (PDOException $e) {
echo "Failed to add columns: " . $e->getMessage() . "\n";
}
+9
View File
@@ -0,0 +1,9 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>406 Not Acceptable</title>
</head><body>
<h1>Not Acceptable</h1>
<p>An appropriate representation of the requested resource could not be found on this server.</p>
<hr>
<address>Apache/2.4.67 (Debian) Server at overpass-api.de Port 443</address>
</body></html>
+40
View File
@@ -0,0 +1,40 @@
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('osm_pontianak.json', 'utf8'));
const geojson = {
type: 'FeatureCollection',
features: []
};
data.elements.forEach(el => {
if (el.type === 'relation' && el.tags && el.tags.admin_level === '6') {
const name = el.tags.name;
// Extract polygons from relation members
const coordinates = [];
el.members.forEach(member => {
if (member.type === 'way' && member.geometry) {
const wayCoords = member.geometry.map(pt => [pt.lon, pt.lat]);
coordinates.push(wayCoords);
}
});
// Assemble into MultiPolygon or Polygon (simplistic)
if (coordinates.length > 0) {
geojson.features.push({
type: 'Feature',
properties: {
kecamatan: name,
...el.tags
},
geometry: {
type: 'Polygon',
coordinates: coordinates
}
});
}
}
});
// Since OSM returns fragments of outer and inner rings in relations, a naive conversion might have topological issues in Leaflet (like crossing lines if not ordered).
// We should use an existing library like osmtogeojson. Let's install it and use it.
+49
View File
@@ -0,0 +1,49 @@
<?php
header('Content-Type: application/json');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["success"=>false,"message"=>"Data kosong"]);
exit;
}
try {
$geojson = json_encode($data['geometry'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare("
INSERT INTO jalan_lines
(nama_jalan, status_jalan, panjang_meter, geojson)
VALUES (?, ?, ?, ?)
");
$stmt->execute([
$data['nama_jalan'],
$data['status_jalan'],
$data['panjang_meter'],
$geojson
]);
} catch (Exception $inner) {
// Fallback untuk skema spatial: simpan ke kolom `geom` jika `geojson` tidak ada.
$stmt = $pdo->prepare("
INSERT INTO jalan_lines
(nama_jalan, status_jalan, panjang_meter, geom)
VALUES (?, ?, ?, ST_GeomFromGeoJSON(?))
");
$stmt->execute([
$data['nama_jalan'],
$data['status_jalan'],
$data['panjang_meter'],
$geojson
]);
}
echo json_encode(["success"=>true]);
} catch (Exception $e) {
echo json_encode(["success"=>false,"message"=>$e->getMessage()]);
}
?>
+65
View File
@@ -0,0 +1,65 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
// Payload expected from js/app.js:
// { status_kepemilikan, luas_m2, geometry }
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(["success" => false, "status" => "error", "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$status_kepemilikan = trim($data['status_kepemilikan'] ?? '');
$luas_m2 = (float)($data['luas_m2'] ?? 0);
$geometry = $data['geometry'] ?? null;
if ($status_kepemilikan === '' || !$geometry) {
echo json_encode(["success" => false, "status" => "error", "message" => "Data parsil belum lengkap (status/geometry)."]);
exit;
}
try {
$geomJson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare("
INSERT INTO parsil_tanah (status_kepemilikan, luas_m2, geojson)
VALUES (:status_kepemilikan, :luas_m2, :geojson)
");
$stmt->execute([
':status_kepemilikan' => $status_kepemilikan,
':luas_m2' => $luas_m2,
':geojson' => $geomJson
]);
} catch (Exception $inner) {
// Fallback untuk skema spatial: simpan ke kolom `geom` jika `geojson` tidak ada.
try {
$stmt = $pdo->prepare("
INSERT INTO parsil_tanah (status_kepemilikan, luas_m2, geom)
VALUES (:status_kepemilikan, :luas_m2, ST_GeomFromGeoJSON(:geom))
");
$stmt->execute([
':status_kepemilikan' => $status_kepemilikan,
':luas_m2' => $luas_m2,
':geom' => $geomJson
]);
} catch (Exception $inner2) {
// Last resort: minimal insert.
$stmt = $pdo->prepare("
INSERT INTO parsil_tanah (geom)
VALUES (ST_GeomFromGeoJSON(:geom))
");
$stmt->execute([':geom' => $geomJson]);
}
}
echo json_encode(["success" => true, "status" => "success", "message" => "Area Parsil berhasil disimpan."]);
} catch (Exception $e) {
echo json_encode(["success" => false, "status" => "error", "message" => $e->getMessage()]);
}
?>
+192
View File
@@ -0,0 +1,192 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function haversine_m($lat1, $lon1, $lat2, $lon2) {
$R = 6371000.0; // meters
$phi1 = deg2rad((float)$lat1);
$phi2 = deg2rad((float)$lat2);
$dphi = deg2rad((float)$lat2 - (float)$lat1);
$dlambda = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dphi/2) * sin($dphi/2) + cos($phi1) * cos($phi2) * sin($dlambda/2) * sin($dlambda/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $R * $c;
}
function get_payload() {
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
if (stripos($ct, 'multipart/form-data') !== false) {
return [
'kk_nama' => $_POST['kk_nama'] ?? '',
'nik' => $_POST['nik'] ?? '',
'jumlah_anggota' => $_POST['jumlah_anggota'] ?? '',
'latitude' => $_POST['latitude'] ?? null,
'longitude' => $_POST['longitude'] ?? null,
'address' => $_POST['address'] ?? '',
'kelurahan' => $_POST['kelurahan'] ?? '',
'kecamatan' => $_POST['kecamatan'] ?? '',
'status_bantuan' => $_POST['status_bantuan'] ?? 'Belum dibantu',
'jenis_bantuan' => $_POST['jenis_bantuan'] ?? '',
'tanggal_bantuan' => $_POST['tanggal_bantuan'] ?? '',
'anggota' => $_POST['anggota'] ?? '[]'
];
}
$json = json_decode(file_get_contents('php://input'), true);
return is_array($json) ? $json : null;
}
function compute_nearest_ibadah(PDO $pdo, $lat, $lng) {
$stmt = $pdo->query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points");
$rows = $stmt->fetchAll();
$best = null;
foreach ($rows as $r) {
$rLat = (float)$r['latitude'];
$rLng = (float)$r['longitude'];
$radius = (float)$r['radius_meter'];
if ($radius <= 0) continue;
$d = haversine_m($lat, $lng, $rLat, $rLng);
if ($d <= $radius) {
if ($best === null || $d < $best['jarak_meter']) {
$best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d];
}
}
}
return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null];
}
function save_upload_if_any() {
if (!isset($_FILES['bukti_file']) || !is_array($_FILES['bukti_file'])) return null;
if (($_FILES['bukti_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) return null;
$dir = __DIR__ . '/../uploads/bukti';
if (!is_dir($dir)) @mkdir($dir, 0775, true);
if (!is_dir($dir)) return null;
$orig = (string)($_FILES['bukti_file']['name'] ?? 'file');
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp','pdf'];
if ($ext && !in_array($ext, $allowed, true)) return null;
$safeBase = preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($orig, PATHINFO_FILENAME));
$safeBase = trim($safeBase, '_');
if ($safeBase === '') $safeBase = 'bukti';
$filename = $safeBase . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . ($ext ? ('.' . $ext) : '');
$dest = $dir . '/' . $filename;
if (!move_uploaded_file($_FILES['bukti_file']['tmp_name'], $dest)) return null;
return 'uploads/bukti/' . $filename;
}
$data = get_payload();
if (!$data) {
echo json_encode(["success" => false, "message" => "Payload kosong atau tidak valid."]);
exit;
}
$kk_nama = trim($data['kk_nama'] ?? $data['nama'] ?? '');
$nik = trim($data['nik'] ?? '');
$jumlah_anggota = (int)($data['jumlah_anggota'] ?? 0);
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$address = trim($data['address'] ?? '');
$kelurahan = trim($data['kelurahan'] ?? '');
$kecamatan = trim($data['kecamatan'] ?? '');
$status_bantuan = trim($data['status_bantuan'] ?? 'Belum dibantu');
$jenis_bantuan = trim($data['jenis_bantuan'] ?? '');
$tanggal_bantuan = trim($data['tanggal_bantuan'] ?? '');
$anggotaRaw = $data['anggota'] ?? [];
$validStatus = ['Belum dibantu', 'Sudah dibantu', 'Menunggu verifikasi'];
if (!in_array($status_bantuan, $validStatus, true)) $status_bantuan = 'Belum dibantu';
if ($kk_nama === '' || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data penduduk miskin belum lengkap (nama/lat/lng)."]);
exit;
}
try {
$lat = (float)$latitude;
$lng = (float)$longitude;
$nearest = compute_nearest_ibadah($pdo, $lat, $lng);
$uploadPath = save_upload_if_any();
$pdo->beginTransaction();
// Backward-compat: some installs still have old column `nama`.
// Prefer new schema if columns exist.
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
if ($hasNew) {
$stmt = $pdo->prepare("
INSERT INTO pemukiman_miskin_points
(kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan,
status_bantuan, jenis_bantuan, tanggal_bantuan, bukti_file, jarak_meter, rumah_ibadah_id)
VALUES
(:kk_nama, :nik, :jumlah_anggota, :latitude, :longitude, :address, :kelurahan, :kecamatan,
:status_bantuan, :jenis_bantuan, :tanggal_bantuan, :bukti_file, :jarak_meter, :rumah_ibadah_id)
");
$stmt->execute([
':kk_nama' => $kk_nama,
':nik' => ($nik === '' ? null : $nik),
':jumlah_anggota' => $jumlah_anggota,
':latitude' => $lat,
':longitude' => $lng,
':address' => ($address === '' ? null : $address),
':kelurahan' => ($kelurahan === '' ? null : $kelurahan),
':kecamatan' => ($kecamatan === '' ? null : $kecamatan),
':status_bantuan' => $status_bantuan,
':jenis_bantuan' => ($jenis_bantuan === '' ? null : $jenis_bantuan),
':tanggal_bantuan' => ($tanggal_bantuan === '' ? null : $tanggal_bantuan),
':bukti_file' => $uploadPath,
':jarak_meter' => $nearest['jarak_meter'],
':rumah_ibadah_id' => $nearest['rumah_ibadah_id']
]);
} else {
$stmt = $pdo->prepare("
INSERT INTO pemukiman_miskin_points (nama, latitude, longitude, address, rumah_ibadah_id)
VALUES (:nama, :latitude, :longitude, :address, :rumah_ibadah_id)
");
$stmt->execute([
':nama' => $kk_nama,
':latitude' => $lat,
':longitude' => $lng,
':address' => ($address === '' ? null : $address),
':rumah_ibadah_id' => $nearest['rumah_ibadah_id']
]);
}
$newId = (int)$pdo->lastInsertId();
// anggota keluarga (optional)
$anggota = is_string($anggotaRaw) ? json_decode($anggotaRaw, true) : $anggotaRaw;
if (is_array($anggota) && count($anggota) > 0 && $hasNew) {
$ins = $pdo->prepare("
INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan, keterangan)
VALUES (:penduduk_id, :nama, :hubungan, :umur, :pekerjaan, :keterangan)
");
foreach ($anggota as $a) {
$n = trim((string)($a['nama'] ?? ''));
$h = trim((string)($a['hubungan'] ?? ''));
if ($n === '' || $h === '') continue;
$ins->execute([
':penduduk_id' => $newId,
':nama' => $n,
':hubungan' => $h,
':umur' => isset($a['umur']) && $a['umur'] !== '' ? (int)$a['umur'] : null,
':pekerjaan' => ($a['pekerjaan'] ?? '') === '' ? null : (string)$a['pekerjaan'],
':keterangan' => ($a['keterangan'] ?? '') === '' ? null : (string)$a['keterangan']
]);
}
}
$pdo->commit();
echo json_encode([
"success" => true,
"id" => $newId,
"rumah_ibadah_id" => $nearest['rumah_ibadah_id'],
"jarak_meter" => $nearest['jarak_meter'],
"message" => "Penduduk miskin berhasil ditambahkan"
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+124
View File
@@ -0,0 +1,124 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(["success" => false, "status" => "error", "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$nama = trim($data['nama'] ?? '');
$no = trim($data['no'] ?? '');
$deskripsi = trim($data['deskripsi'] ?? '');
$status_24jam = isset($data['status_24jam']) ? (int)$data['status_24jam'] : 0;
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
if ($nama === '' || $no === '' || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "status" => "error", "message" => "Data point belum lengkap (nama/no/lat/lng)."]);
exit;
}
$pointGeojson = json_encode([
"type" => "Point",
"coordinates" => [(float)$longitude, (float)$latitude]
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// Optional legacy field if client ever sends it
$geomPayload = $data['geom'] ?? null;
if (!is_string($geomPayload) || $geomPayload === '') {
$geomPayload = $pointGeojson;
}
try {
// Preferred schema (non-spatial, consistent with update_point.php)
try {
$stmt = $pdo->prepare("
INSERT INTO spbu_points (nama, no, deskripsi, status_24jam, latitude, longitude)
VALUES (:nama, :no, :deskripsi, :status_24jam, :latitude, :longitude)
");
$stmt->execute([
':nama' => $nama,
':no' => $no,
':deskripsi' => $deskripsi,
':status_24jam' => $status_24jam,
':latitude' => $latitude,
':longitude' => $longitude
]);
echo json_encode([
"success" => true,
"status" => "success",
"message" => "Point SPBU berhasil ditambahkan",
"id" => (int)$pdo->lastInsertId()
]);
exit;
} catch (Exception $e1) {
// continue to fallback attempts
}
// Fallback schema: `geom` stored as LONGTEXT (GeoJSON string)
try {
$stmt = $pdo->prepare("
INSERT INTO spbu_points (nama, no, deskripsi, status_24jam, latitude, longitude, geom)
VALUES (:nama, :no, :deskripsi, :status_24jam, :latitude, :longitude, :geom)
");
$stmt->execute([
':nama' => $nama,
':no' => $no,
':deskripsi' => $deskripsi,
':status_24jam' => $status_24jam,
':latitude' => $latitude,
':longitude' => $longitude,
':geom' => $geomPayload
]);
echo json_encode([
"success" => true,
"status" => "success",
"message" => "Point SPBU berhasil ditambahkan",
"id" => (int)$pdo->lastInsertId()
]);
exit;
} catch (Exception $e2) {
// continue to fallback attempts
}
// Fallback schema: only (nama, geom) where geom is LONGTEXT GeoJSON
try {
$stmt = $pdo->prepare("INSERT INTO spbu_points (nama, geom) VALUES (:nama, :geom)");
$stmt->execute([
':nama' => $nama,
':geom' => $geomPayload
]);
echo json_encode([
"success" => true,
"status" => "success",
"message" => "Point SPBU berhasil ditambahkan",
"id" => (int)$pdo->lastInsertId()
]);
exit;
} catch (Exception $e3) {
// continue to fallback attempts
}
// Fallback schema: `geom` is spatial GEOMETRY/POINT
$stmt = $pdo->prepare("INSERT INTO spbu_points (nama, geom) VALUES (:nama, ST_GeomFromGeoJSON(:geom))");
$stmt->execute([
':nama' => $nama,
':geom' => $pointGeojson
]);
echo json_encode([
"success" => true,
"status" => "success",
"message" => "Point SPBU berhasil ditambahkan",
"id" => (int)$pdo->lastInsertId()
]);
} catch (Exception $e) {
echo json_encode(["success" => false, "status" => "error", "message" => $e->getMessage()]);
}
+60
View File
@@ -0,0 +1,60 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$nama = trim($data['nama'] ?? '');
$jenis = trim($data['jenis'] ?? 'Lainnya');
$kontak = trim($data['kontak'] ?? '');
$radius_meter = (float)($data['radius_meter'] ?? 0);
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$address = trim($data['address'] ?? '');
if ($nama === '' || $radius_meter <= 0 || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data rumah ibadah belum lengkap (nama/radius/lat/lng)."]);
exit;
}
try {
$cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasJenis = in_array('jenis', $cols, true);
$hasKontak = in_array('kontak', $cols, true);
if ($hasJenis && $hasKontak) {
$stmt = $pdo->prepare("
INSERT INTO rumah_ibadah_points (nama, jenis, kontak, radius_meter, latitude, longitude, address)
VALUES (:nama, :jenis, :kontak, :radius_meter, :latitude, :longitude, :address)
");
$stmt->execute([
':nama' => $nama,
':jenis' => ($jenis === '' ? 'Lainnya' : $jenis),
':kontak' => ($kontak === '' ? null : $kontak),
':radius_meter' => $radius_meter,
':latitude' => $latitude,
':longitude' => $longitude,
':address' => ($address === '' ? null : $address)
]);
} else {
$stmt = $pdo->prepare("
INSERT INTO rumah_ibadah_points (nama, radius_meter, latitude, longitude, address)
VALUES (:nama, :radius_meter, :latitude, :longitude, :address)
");
$stmt->execute([
':nama' => $nama,
':radius_meter' => $radius_meter,
':latitude' => $latitude,
':longitude' => $longitude,
':address' => ($address === '' ? null : $address)
]);
}
echo json_encode(["success" => true, "id" => (int)$pdo->lastInsertId(), "message" => "Rumah ibadah berhasil ditambahkan"]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+16
View File
@@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM jalan_lines WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Data jalan berhasil dihapus"]);
+16
View File
@@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM parsil_tanah WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Data parsil berhasil dihapus"]);
@@ -0,0 +1,33 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
try {
// best-effort delete uploaded proof file
try {
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
if (in_array('bukti_file', $cols, true)) {
$stmt = $pdo->prepare("SELECT bukti_file FROM pemukiman_miskin_points WHERE id = ?");
$stmt->execute([$id]);
$path = $stmt->fetchColumn();
if ($path && is_string($path) && strpos($path, 'uploads/bukti/') === 0) {
$full = __DIR__ . '/../' . $path;
if (is_file($full)) @unlink($full);
}
}
} catch (Exception $e) { /* ignore */ }
$stmt = $pdo->prepare("DELETE FROM pemukiman_miskin_points WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Pemukiman miskin berhasil dihapus"]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+16
View File
@@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM spbu_points WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Data point berhasil dihapus"]);
+19
View File
@@ -0,0 +1,19 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
try {
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah_points WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Rumah ibadah berhasil dihapus"]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+27
View File
@@ -0,0 +1,27 @@
<?php
$host = getenv('DB_HOST') ?: "localhost";
$port = getenv('DB_PORT') ?: "3306";
$db = getenv('DB_NAME') ?: "webgis_spbu";
$user = getenv('DB_USER') ?: "root";
$pass = getenv('DB_PASSWORD') ?: "";
$charset = "utf8mb4";
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
"success" => false,
"message" => "Koneksi database gagal: " . $e->getMessage()
]);
exit;
}
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$penduduk_id = isset($_GET['penduduk_id']) ? (int)$_GET['penduduk_id'] : 0;
if ($penduduk_id <= 0) {
echo json_encode([]);
exit;
}
try {
$stmt = $pdo->prepare("
SELECT id, penduduk_id, nama, hubungan, umur, pekerjaan, keterangan
FROM anggota_keluarga
WHERE penduduk_id = :id
ORDER BY id ASC
");
$stmt->execute([':id' => $penduduk_id]);
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
echo json_encode([]);
}
+51
View File
@@ -0,0 +1,51 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$features = [];
function rowsToFeatures($stmt) {
$features = [];
while ($row = $stmt->fetch()) {
$geometry = json_decode($row['geojson'] ?? '', true);
if (!$geometry) {
continue;
}
$features[] = [
"type" => "Feature",
"geometry" => $geometry,
"properties" => [
"id" => (int)($row['id'] ?? 0),
"nama_jalan" => $row['nama_jalan'] ?? null,
"status_jalan" => $row['status_jalan'] ?? null,
"panjang_meter" => isset($row['panjang_meter']) ? (float)$row['panjang_meter'] : null
]
];
}
return $features;
}
try {
// Preferred schema: geojson column.
$stmt = $pdo->query("SELECT id, nama_jalan, status_jalan, panjang_meter, geojson FROM jalan_lines ORDER BY id ASC");
$features = rowsToFeatures($stmt);
} catch (Exception $e) {
// Fallback schema: spatial geom.
$stmt = $pdo->query("
SELECT
id,
nama_jalan,
status_jalan,
panjang_meter,
ST_AsGeoJSON(geom) AS geojson
FROM jalan_lines
ORDER BY id ASC
");
$features = rowsToFeatures($stmt);
}
echo json_encode([
"type" => "FeatureCollection",
"features" => $features
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+49
View File
@@ -0,0 +1,49 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$features = [];
function rowsToFeatures($stmt) {
$features = [];
while ($row = $stmt->fetch()) {
$geometry = json_decode($row['geojson'] ?? '', true);
if (!$geometry) {
continue;
}
$features[] = [
"type" => "Feature",
"geometry" => $geometry,
"properties" => [
"id" => (int)($row['id'] ?? 0),
"status_kepemilikan" => $row['status_kepemilikan'] ?? null,
"luas_m2" => isset($row['luas_m2']) ? (float)$row['luas_m2'] : null
]
];
}
return $features;
}
try {
// Preferred schema: geojson column.
$stmt = $pdo->query("SELECT id, status_kepemilikan, luas_m2, geojson FROM parsil_tanah ORDER BY id ASC");
$features = rowsToFeatures($stmt);
} catch (Exception $e) {
// Fallback schema: spatial geom.
$stmt = $pdo->query("
SELECT
id,
status_kepemilikan,
luas_m2,
ST_AsGeoJSON(geom) AS geojson
FROM parsil_tanah
ORDER BY id ASC
");
$features = rowsToFeatures($stmt);
}
echo json_encode([
"type" => "FeatureCollection",
"features" => $features
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+127
View File
@@ -0,0 +1,127 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function haversine_m($lat1, $lon1, $lat2, $lon2) {
$R = 6371000.0;
$phi1 = deg2rad((float)$lat1);
$phi2 = deg2rad((float)$lat2);
$dphi = deg2rad((float)$lat2 - (float)$lat1);
$dlambda = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dphi/2) * sin($dphi/2) + cos($phi1) * cos($phi2) * sin($dlambda/2) * sin($dlambda/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $R * $c;
}
function compute_nearest_ibadah(array $ibadahRows, $lat, $lng) {
$best = null;
foreach ($ibadahRows as $r) {
$rLat = (float)($r['latitude'] ?? 0);
$rLng = (float)($r['longitude'] ?? 0);
$radius = (float)($r['radius_meter'] ?? 0);
if ($radius <= 0) continue;
$d = haversine_m($lat, $lng, $rLat, $rLng);
if ($d <= $radius) {
if ($best === null || $d < $best['jarak_meter']) {
$best = [
'rumah_ibadah_id' => (int)($r['id'] ?? 0),
'jarak_meter' => $d,
'row' => $r
];
}
}
}
return $best;
}
// Prefer new schema; fall back to old schema if needed.
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
$hasJarak = in_array('jarak_meter', $cols, true);
$hasRumahIbadahId = in_array('rumah_ibadah_id', $cols, true);
// Fetch rumah ibadah once; used for real-time coverage computation server-side.
$ibadah = [];
try {
$stmtI = $pdo->query("SELECT id, nama, jenis, kontak, radius_meter, latitude, longitude FROM rumah_ibadah_points");
$ibadah = $stmtI->fetchAll();
} catch (Exception $e) {
$ibadah = [];
}
// Load pemukiman rows
$rows = [];
try {
if ($hasNew) {
$stmt = $pdo->query("
SELECT p.*,
(
SELECT COUNT(*) FROM anggota_keluarga a WHERE a.penduduk_id = p.id
) AS anggota_count
FROM pemukiman_miskin_points p
ORDER BY p.id ASC
");
$rows = $stmt->fetchAll();
} else {
$stmt = $pdo->query("SELECT p.* FROM pemukiman_miskin_points p ORDER BY p.id ASC");
$rows = $stmt->fetchAll();
}
} catch (Exception $e) {
$rows = [];
}
// Best-effort: keep stored rumah_ibadah_id/jarak_meter in sync with latest radius rules.
$upd = null;
try {
if ($hasRumahIbadahId && $hasJarak) {
$upd = $pdo->prepare("UPDATE pemukiman_miskin_points SET rumah_ibadah_id = :rid, jarak_meter = :jarak WHERE id = :id");
} elseif ($hasRumahIbadahId) {
$upd = $pdo->prepare("UPDATE pemukiman_miskin_points SET rumah_ibadah_id = :rid WHERE id = :id");
}
} catch (Exception $e) {
$upd = null;
}
foreach ($rows as &$p) {
$lat = isset($p['latitude']) ? (float)$p['latitude'] : null;
$lng = isset($p['longitude']) ? (float)$p['longitude'] : null;
if ($lat === null || $lng === null) continue;
$best = compute_nearest_ibadah($ibadah, $lat, $lng);
$rid = $best ? (int)$best['rumah_ibadah_id'] : null;
$jarak = $best ? (float)$best['jarak_meter'] : null;
$storedRid = isset($p['rumah_ibadah_id']) ? (int)$p['rumah_ibadah_id'] : null;
// overwrite output with authoritative computed coverage
$p['rumah_ibadah_id'] = $rid;
if ($hasJarak) $p['jarak_meter'] = $jarak;
if ($best && isset($best['row']) && is_array($best['row'])) {
$p['rumah_ibadah_nama'] = $best['row']['nama'] ?? null;
$p['rumah_ibadah_jenis'] = $best['row']['jenis'] ?? null;
$p['rumah_ibadah_kontak'] = $best['row']['kontak'] ?? null;
$p['rumah_ibadah_radius'] = $best['row']['radius_meter'] ?? null;
} else {
$p['rumah_ibadah_nama'] = null;
$p['rumah_ibadah_jenis'] = null;
$p['rumah_ibadah_kontak'] = null;
$p['rumah_ibadah_radius'] = null;
}
// best-effort DB sync
try {
if ($upd && ((string)$storedRid !== (string)$rid)) {
if ($hasRumahIbadahId && $hasJarak) {
$upd->execute([':rid' => $rid, ':jarak' => $jarak, ':id' => (int)$p['id']]);
} elseif ($hasRumahIbadahId) {
$upd->execute([':rid' => $rid, ':id' => (int)$p['id']]);
}
}
} catch (Exception $e) {
// ignore
}
}
unset($p);
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+96
View File
@@ -0,0 +1,96 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function normalizePointRows($rows) {
foreach ($rows as &$row) {
if ((!isset($row['latitude']) || $row['latitude'] === null) && isset($row['geom']) && is_string($row['geom'])) {
$geom = json_decode($row['geom'], true);
if (is_array($geom) && ($geom['type'] ?? '') === 'Point') {
$coords = $geom['coordinates'] ?? null;
if (is_array($coords) && count($coords) >= 2) {
// GeoJSON: [lng, lat]
$row['longitude'] = $coords[0];
$row['latitude'] = $coords[1];
}
}
}
}
return $rows;
}
try {
// Preferred schema: has latitude/longitude columns.
$stmt = $pdo->query("
SELECT id, nama, no, deskripsi, status_24jam, latitude, longitude
FROM spbu_points
ORDER BY id ASC
");
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Exception $e) {
// continue
}
try {
// Fallback schema: latitude/longitude exists, geom stored as LONGTEXT GeoJSON.
$stmt = $pdo->query("
SELECT id, nama, no, deskripsi, status_24jam, latitude, longitude, geom
FROM spbu_points
ORDER BY id ASC
");
$rows = $stmt->fetchAll();
$rows = normalizePointRows($rows);
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Exception $e) {
// continue
}
try {
// Fallback schema: only geom LONGTEXT; derive latitude/longitude from GeoJSON.
$stmt = $pdo->query("
SELECT
id,
nama,
NULL AS no,
NULL AS deskripsi,
0 AS status_24jam,
geom
FROM spbu_points
ORDER BY id ASC
");
$rows = $stmt->fetchAll();
$rows = normalizePointRows($rows);
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Exception $e) {
// continue
}
try {
// Fallback schema: spatial geom
$stmt = $pdo->query("
SELECT
id,
nama,
NULL AS no,
NULL AS deskripsi,
0 AS status_24jam,
ST_Y(geom) AS latitude,
ST_X(geom) AS longitude
FROM spbu_points
ORDER BY id ASC
");
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Exception $e) {
// continue
}
// Last resort
$stmt = $pdo->query("SELECT * FROM spbu_points ORDER BY id ASC");
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+17
View File
@@ -0,0 +1,17 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
// Include jumlah penduduk binaan per rumah ibadah
$stmt = $pdo->query("
SELECT r.*,
(
SELECT COUNT(*)
FROM pemukiman_miskin_points p
WHERE p.rumah_ibadah_id = r.id
) AS binaan_count
FROM rumah_ibadah_points r
ORDER BY r.id ASC
");
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+42
View File
@@ -0,0 +1,42 @@
<?php
header('Content-Type: application/json; charset=utf-8');
$lat = isset($_GET['lat']) ? (float)$_GET['lat'] : null;
$lng = isset($_GET['lng']) ? (float)$_GET['lng'] : null;
if ($lat === null || $lng === null) {
echo json_encode(["success" => false, "message" => "lat/lng wajib diisi"]);
exit;
}
// Nominatim usage policy requires a valid User-Agent identifying the application.
$url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=" . urlencode($lat) . "&lon=" . urlencode($lng);
try {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 8);
curl_setopt($ch, CURLOPT_TIMEOUT, 12);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"User-Agent: webgis-spbu/1.0 (XAMPP PHP reverse geocode)"
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($resp === false || $code < 200 || $code >= 300) {
echo json_encode(["success" => false, "message" => $err ?: ("HTTP " . $code)]);
exit;
}
$json = json_decode($resp, true);
$display = $json['display_name'] ?? null;
echo json_encode([
"success" => true,
"address" => $display,
"raw" => $json
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+63
View File
@@ -0,0 +1,63 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Data kosong"]);
exit;
}
$id = (int)($data['id'] ?? 0);
$nama_jalan = trim($data['nama_jalan'] ?? '');
$status_jalan = trim($data['status_jalan'] ?? '');
$panjang_meter = (float)($data['panjang_meter'] ?? 0);
$geometry = $data['geometry'] ?? null;
if ($id <= 0 || $nama_jalan === '' || $status_jalan === '' || !$geometry) {
echo json_encode(["success" => false, "message" => "Data update jalan belum lengkap"]);
exit;
}
$sql = "UPDATE jalan_lines
SET nama_jalan = :nama_jalan,
status_jalan = :status_jalan,
panjang_meter = :panjang_meter,
geojson = :geojson
WHERE id = :id";
$geojson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare($sql);
$stmt->execute([
':nama_jalan' => $nama_jalan,
':status_jalan' => $status_jalan,
':panjang_meter' => $panjang_meter,
':geojson' => $geojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data jalan berhasil diupdate"]);
exit;
} catch (Exception $inner) {
// fallback to spatial geom schema
}
$stmt = $pdo->prepare("UPDATE jalan_lines
SET nama_jalan = :nama_jalan,
status_jalan = :status_jalan,
panjang_meter = :panjang_meter,
geom = ST_GeomFromGeoJSON(:geojson)
WHERE id = :id");
$stmt->execute([
':nama_jalan' => $nama_jalan,
':status_jalan' => $status_jalan,
':panjang_meter' => $panjang_meter,
':geojson' => $geojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data jalan berhasil diupdate"]);
+58
View File
@@ -0,0 +1,58 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Data kosong"]);
exit;
}
$id = (int)($data['id'] ?? 0);
$status_kepemilikan = trim($data['status_kepemilikan'] ?? '');
$luas_m2 = (float)($data['luas_m2'] ?? 0);
$geometry = $data['geometry'] ?? null;
if ($id <= 0 || $status_kepemilikan === '' || !$geometry) {
echo json_encode(["success" => false, "message" => "Data update parsil belum lengkap"]);
exit;
}
$sql = "UPDATE parsil_tanah
SET status_kepemilikan = :status_kepemilikan,
luas_m2 = :luas_m2,
geojson = :geojson
WHERE id = :id";
$geojson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare($sql);
$stmt->execute([
':status_kepemilikan' => $status_kepemilikan,
':luas_m2' => $luas_m2,
':geojson' => $geojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data parsil berhasil diupdate"]);
exit;
} catch (Exception $inner) {
// fallback to spatial geom schema
}
$stmt = $pdo->prepare("UPDATE parsil_tanah
SET status_kepemilikan = :status_kepemilikan,
luas_m2 = :luas_m2,
geom = ST_GeomFromGeoJSON(:geojson)
WHERE id = :id");
$stmt->execute([
':status_kepemilikan' => $status_kepemilikan,
':luas_m2' => $luas_m2,
':geojson' => $geojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data parsil berhasil diupdate"]);
+213
View File
@@ -0,0 +1,213 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function haversine_m($lat1, $lon1, $lat2, $lon2) {
$R = 6371000.0;
$phi1 = deg2rad((float)$lat1);
$phi2 = deg2rad((float)$lat2);
$dphi = deg2rad((float)$lat2 - (float)$lat1);
$dlambda = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dphi/2) * sin($dphi/2) + cos($phi1) * cos($phi2) * sin($dlambda/2) * sin($dlambda/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $R * $c;
}
function get_payload() {
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
if (stripos($ct, 'multipart/form-data') !== false) {
return [
'id' => $_POST['id'] ?? 0,
'kk_nama' => $_POST['kk_nama'] ?? '',
'nik' => $_POST['nik'] ?? '',
'jumlah_anggota' => $_POST['jumlah_anggota'] ?? '',
'latitude' => $_POST['latitude'] ?? null,
'longitude' => $_POST['longitude'] ?? null,
'address' => $_POST['address'] ?? '',
'kelurahan' => $_POST['kelurahan'] ?? '',
'kecamatan' => $_POST['kecamatan'] ?? '',
'status_bantuan' => $_POST['status_bantuan'] ?? 'Belum dibantu',
'jenis_bantuan' => $_POST['jenis_bantuan'] ?? '',
'tanggal_bantuan' => $_POST['tanggal_bantuan'] ?? '',
'anggota' => $_POST['anggota'] ?? '[]'
];
}
$json = json_decode(file_get_contents('php://input'), true);
return is_array($json) ? $json : null;
}
function compute_nearest_ibadah(PDO $pdo, $lat, $lng) {
$stmt = $pdo->query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points");
$rows = $stmt->fetchAll();
$best = null;
foreach ($rows as $r) {
$rLat = (float)$r['latitude'];
$rLng = (float)$r['longitude'];
$radius = (float)$r['radius_meter'];
if ($radius <= 0) continue;
$d = haversine_m($lat, $lng, $rLat, $rLng);
if ($d <= $radius) {
if ($best === null || $d < $best['jarak_meter']) {
$best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d];
}
}
}
return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null];
}
function save_upload_if_any() {
if (!isset($_FILES['bukti_file']) || !is_array($_FILES['bukti_file'])) return null;
if (($_FILES['bukti_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) return null;
$dir = __DIR__ . '/../uploads/bukti';
if (!is_dir($dir)) @mkdir($dir, 0775, true);
if (!is_dir($dir)) return null;
$orig = (string)($_FILES['bukti_file']['name'] ?? 'file');
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp','pdf'];
if ($ext && !in_array($ext, $allowed, true)) return null;
$safeBase = preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($orig, PATHINFO_FILENAME));
$safeBase = trim($safeBase, '_');
if ($safeBase === '') $safeBase = 'bukti';
$filename = $safeBase . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . ($ext ? ('.' . $ext) : '');
$dest = $dir . '/' . $filename;
if (!move_uploaded_file($_FILES['bukti_file']['tmp_name'], $dest)) return null;
return 'uploads/bukti/' . $filename;
}
$data = get_payload();
if (!$data) {
echo json_encode(["success" => false, "message" => "Payload kosong atau tidak valid."]);
exit;
}
$id = (int)($data['id'] ?? 0);
$kk_nama = trim($data['kk_nama'] ?? $data['nama'] ?? '');
$nik = trim($data['nik'] ?? '');
$jumlah_anggota = (int)($data['jumlah_anggota'] ?? 0);
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$address = trim($data['address'] ?? '');
$kelurahan = trim($data['kelurahan'] ?? '');
$kecamatan = trim($data['kecamatan'] ?? '');
$status_bantuan = trim($data['status_bantuan'] ?? 'Belum dibantu');
$jenis_bantuan = trim($data['jenis_bantuan'] ?? '');
$tanggal_bantuan = trim($data['tanggal_bantuan'] ?? '');
$anggotaRaw = $data['anggota'] ?? [];
$validStatus = ['Belum dibantu', 'Sudah dibantu', 'Menunggu verifikasi'];
if (!in_array($status_bantuan, $validStatus, true)) $status_bantuan = 'Belum dibantu';
if ($id <= 0 || $kk_nama === '' || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data update penduduk miskin belum lengkap."]);
exit;
}
try {
$lat = (float)$latitude;
$lng = (float)$longitude;
$nearest = compute_nearest_ibadah($pdo, $lat, $lng);
$uploadPath = save_upload_if_any();
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
$pdo->beginTransaction();
if ($hasNew) {
if ($uploadPath === null) {
$stmt = $pdo->prepare("SELECT bukti_file FROM pemukiman_miskin_points WHERE id = :id");
$stmt->execute([':id' => $id]);
$uploadPath = $stmt->fetchColumn() ?: null;
}
$stmt = $pdo->prepare("
UPDATE pemukiman_miskin_points
SET kk_nama = :kk_nama,
nik = :nik,
jumlah_anggota = :jumlah_anggota,
latitude = :latitude,
longitude = :longitude,
address = :address,
kelurahan = :kelurahan,
kecamatan = :kecamatan,
status_bantuan = :status_bantuan,
jenis_bantuan = :jenis_bantuan,
tanggal_bantuan = :tanggal_bantuan,
bukti_file = :bukti_file,
jarak_meter = :jarak_meter,
rumah_ibadah_id = :rumah_ibadah_id
WHERE id = :id
");
$stmt->execute([
':kk_nama' => $kk_nama,
':nik' => ($nik === '' ? null : $nik),
':jumlah_anggota' => $jumlah_anggota,
':latitude' => $lat,
':longitude' => $lng,
':address' => ($address === '' ? null : $address),
':kelurahan' => ($kelurahan === '' ? null : $kelurahan),
':kecamatan' => ($kecamatan === '' ? null : $kecamatan),
':status_bantuan' => $status_bantuan,
':jenis_bantuan' => ($jenis_bantuan === '' ? null : $jenis_bantuan),
':tanggal_bantuan' => ($tanggal_bantuan === '' ? null : $tanggal_bantuan),
':bukti_file' => $uploadPath,
':jarak_meter' => $nearest['jarak_meter'],
':rumah_ibadah_id' => $nearest['rumah_ibadah_id'],
':id' => $id
]);
// anggota keluarga (replace)
$pdo->prepare("DELETE FROM anggota_keluarga WHERE penduduk_id = :id")->execute([':id' => $id]);
$anggota = is_string($anggotaRaw) ? json_decode($anggotaRaw, true) : $anggotaRaw;
if (is_array($anggota) && count($anggota) > 0) {
$ins = $pdo->prepare("
INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan, keterangan)
VALUES (:penduduk_id, :nama, :hubungan, :umur, :pekerjaan, :keterangan)
");
foreach ($anggota as $a) {
$n = trim((string)($a['nama'] ?? ''));
$h = trim((string)($a['hubungan'] ?? ''));
if ($n === '' || $h === '') continue;
$ins->execute([
':penduduk_id' => $id,
':nama' => $n,
':hubungan' => $h,
':umur' => isset($a['umur']) && $a['umur'] !== '' ? (int)$a['umur'] : null,
':pekerjaan' => ($a['pekerjaan'] ?? '') === '' ? null : (string)$a['pekerjaan'],
':keterangan' => ($a['keterangan'] ?? '') === '' ? null : (string)$a['keterangan']
]);
}
}
} else {
$stmt = $pdo->prepare("
UPDATE pemukiman_miskin_points
SET nama = :nama,
latitude = :latitude,
longitude = :longitude,
address = :address,
rumah_ibadah_id = :rumah_ibadah_id
WHERE id = :id
");
$stmt->execute([
':nama' => $kk_nama,
':latitude' => $lat,
':longitude' => $lng,
':address' => ($address === '' ? null : $address),
':rumah_ibadah_id' => $nearest['rumah_ibadah_id'],
':id' => $id
]);
}
$pdo->commit();
echo json_encode([
"success" => true,
"rumah_ibadah_id" => $nearest['rumah_ibadah_id'],
"jarak_meter" => $nearest['jarak_meter'],
"message" => "Penduduk miskin berhasil diupdate"
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
@@ -0,0 +1,95 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function haversine_m($lat1, $lon1, $lat2, $lon2) {
$R = 6371000.0;
$phi1 = deg2rad((float)$lat1);
$phi2 = deg2rad((float)$lat2);
$dphi = deg2rad((float)$lat2 - (float)$lat1);
$dlambda = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dphi/2) * sin($dphi/2) + cos($phi1) * cos($phi2) * sin($dlambda/2) * sin($dlambda/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $R * $c;
}
function compute_nearest_ibadah(PDO $pdo, $lat, $lng) {
$stmt = $pdo->query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points");
$rows = $stmt->fetchAll();
$best = null;
foreach ($rows as $r) {
$rLat = (float)$r['latitude'];
$rLng = (float)$r['longitude'];
$radius = (float)$r['radius_meter'];
if ($radius <= 0) continue;
$d = haversine_m($lat, $lng, $rLat, $rLng);
if ($d <= $radius) {
if ($best === null || $d < $best['jarak_meter']) {
$best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d];
}
}
}
return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null];
}
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
echo json_encode(["success" => false, "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$id = (int)($data['id'] ?? 0);
$lat = isset($data['latitude']) ? (float)$data['latitude'] : null;
$lng = isset($data['longitude']) ? (float)$data['longitude'] : null;
if ($id <= 0 || $lat === null || $lng === null) {
echo json_encode(["success" => false, "message" => "Data update lokasi belum lengkap (id/lat/lng)."]);
exit;
}
try {
$nearest = compute_nearest_ibadah($pdo, $lat, $lng);
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
if ($hasNew) {
$stmt = $pdo->prepare("
UPDATE pemukiman_miskin_points
SET latitude = :lat,
longitude = :lng,
jarak_meter = :jarak_meter,
rumah_ibadah_id = :rumah_ibadah_id
WHERE id = :id
");
$stmt->execute([
':lat' => $lat,
':lng' => $lng,
':jarak_meter' => $nearest['jarak_meter'],
':rumah_ibadah_id' => $nearest['rumah_ibadah_id'],
':id' => $id
]);
} else {
$stmt = $pdo->prepare("
UPDATE pemukiman_miskin_points
SET latitude = :lat,
longitude = :lng,
rumah_ibadah_id = :rumah_ibadah_id
WHERE id = :id
");
$stmt->execute([
':lat' => $lat,
':lng' => $lng,
':rumah_ibadah_id' => $nearest['rumah_ibadah_id'],
':id' => $id
]);
}
echo json_encode([
"success" => true,
"rumah_ibadah_id" => $nearest['rumah_ibadah_id'],
"jarak_meter" => $nearest['jarak_meter'],
"message" => "Lokasi penduduk miskin berhasil diupdate"
]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+115
View File
@@ -0,0 +1,115 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Data kosong"]);
exit;
}
$id = (int)($data['id'] ?? 0);
$nama = trim($data['nama'] ?? '');
$no = trim($data['no'] ?? '');
$deskripsi = trim($data['deskripsi'] ?? '');
$status_24jam = isset($data['status_24jam']) ? (int)$data['status_24jam'] : 0;
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
if ($id <= 0 || $nama === '' || $no === '' || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data update point belum lengkap"]);
exit;
}
$sql = "UPDATE spbu_points
SET nama = :nama,
no = :no,
deskripsi = :deskripsi,
status_24jam = :status_24jam,
latitude = :latitude,
longitude = :longitude
WHERE id = :id";
$pointGeojson = json_encode([
"type" => "Point",
"coordinates" => [(float)$longitude, (float)$latitude]
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare($sql);
$stmt->execute([
':nama' => $nama,
':no' => $no,
':deskripsi' => $deskripsi,
':status_24jam' => $status_24jam,
':latitude' => $latitude,
':longitude' => $longitude,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]);
exit;
} catch (Exception $inner) {
// continue fallback
}
// Fallback schema: geom LONGTEXT
try {
$stmt = $pdo->prepare("UPDATE spbu_points
SET nama = :nama,
no = :no,
deskripsi = :deskripsi,
status_24jam = :status_24jam,
latitude = :latitude,
longitude = :longitude,
geom = :geom
WHERE id = :id");
$stmt->execute([
':nama' => $nama,
':no' => $no,
':deskripsi' => $deskripsi,
':status_24jam' => $status_24jam,
':latitude' => $latitude,
':longitude' => $longitude,
':geom' => $pointGeojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]);
exit;
} catch (Exception $inner2) {
// continue fallback
}
// Fallback schema: only (nama, geom) where geom is LONGTEXT GeoJSON
try {
$stmt = $pdo->prepare("UPDATE spbu_points
SET nama = :nama,
geom = :geom
WHERE id = :id");
$stmt->execute([
':nama' => $nama,
':geom' => $pointGeojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]);
exit;
} catch (Exception $inner3) {
// continue fallback
}
// Fallback schema: spatial geom
$stmt = $pdo->prepare("UPDATE spbu_points
SET nama = :nama,
geom = ST_GeomFromGeoJSON(:geom)
WHERE id = :id");
$stmt->execute([
':nama' => $nama,
':geom' => $pointGeojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]);
+75
View File
@@ -0,0 +1,75 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$id = (int)($data['id'] ?? 0);
$nama = trim($data['nama'] ?? '');
$jenis = trim($data['jenis'] ?? 'Lainnya');
$kontak = trim($data['kontak'] ?? '');
$radius_meter = (float)($data['radius_meter'] ?? 0);
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$address = trim($data['address'] ?? '');
if ($id <= 0 || $nama === '' || $radius_meter <= 0 || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data update rumah ibadah belum lengkap."]);
exit;
}
try {
$cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasJenis = in_array('jenis', $cols, true);
$hasKontak = in_array('kontak', $cols, true);
if ($hasJenis && $hasKontak) {
$stmt = $pdo->prepare("
UPDATE rumah_ibadah_points
SET nama = :nama,
jenis = :jenis,
kontak = :kontak,
radius_meter = :radius_meter,
latitude = :latitude,
longitude = :longitude,
address = :address
WHERE id = :id
");
$stmt->execute([
':nama' => $nama,
':jenis' => ($jenis === '' ? 'Lainnya' : $jenis),
':kontak' => ($kontak === '' ? null : $kontak),
':radius_meter' => $radius_meter,
':latitude' => $latitude,
':longitude' => $longitude,
':address' => ($address === '' ? null : $address),
':id' => $id
]);
} else {
$stmt = $pdo->prepare("
UPDATE rumah_ibadah_points
SET nama = :nama,
radius_meter = :radius_meter,
latitude = :latitude,
longitude = :longitude,
address = :address
WHERE id = :id
");
$stmt->execute([
':nama' => $nama,
':radius_meter' => $radius_meter,
':latitude' => $latitude,
':longitude' => $longitude,
':address' => ($address === '' ? null : $address),
':id' => $id
]);
}
echo json_encode(["success" => true, "message" => "Rumah ibadah berhasil diupdate"]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+466
View File
@@ -0,0 +1,466 @@
-- Schema WebGIS SPBU + Jalan + Parsil (MySQL / MariaDB)
-- Catatan:
-- - Data LINE/POLYGON disimpan sebagai GeoJSON geometry (tipe: geojson) di kolom LONGTEXT `geojson`.
-- - Panjang (meter) dan luas (m2) dihitung otomatis di LeafletJS, bukan input manual.
CREATE TABLE IF NOT EXISTS spbu_points (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150) NOT NULL,
no VARCHAR(60) NOT NULL,
deskripsi TEXT NULL,
status_24jam TINYINT(1) NOT NULL DEFAULT 0,
latitude DECIMAL(10,7) NOT NULL,
longitude DECIMAL(10,7) NOT NULL,
-- opsional (kompatibilitas): simpan GeoJSON Point sebagai string
geom LONGTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_spbu_status24 (status_24jam),
INDEX idx_spbu_latlng (latitude, longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS jalan_lines (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(200) NOT NULL,
status_jalan VARCHAR(20) NOT NULL, -- Nasional | Provinsi | Kabupaten
panjang_meter DOUBLE NOT NULL DEFAULT 0,
geojson LONGTEXT NOT NULL, -- GeoJSON geometry: { "type":"LineString", "coordinates":[...] }
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_jalan_status (status_jalan)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS parsil_tanah (
id INT AUTO_INCREMENT PRIMARY KEY,
status_kepemilikan VARCHAR(10) NOT NULL, -- SHM | HGB | HGU | HP
luas_m2 DOUBLE NOT NULL DEFAULT 0,
geojson LONGTEXT NOT NULL, -- GeoJSON geometry: { "type":"Polygon", "coordinates":[...] }
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_parsil_status (status_kepemilikan)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS rumah_ibadah_points (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya', -- Masjid|Musholla|Gereja|Katolik|Pura|Vihara|Klenteng|Lainnya
kontak VARCHAR(120) NULL,
radius_meter DOUBLE NOT NULL DEFAULT 300,
latitude DECIMAL(10,7) NOT NULL,
longitude DECIMAL(10,7) NOT NULL,
address TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ibadah_jenis (jenis),
INDEX idx_ibadah_latlng (latitude, longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pemukiman_miskin_points (
id INT AUTO_INCREMENT PRIMARY KEY,
kk_nama VARCHAR(200) NOT NULL,
nik VARCHAR(32) NULL,
jumlah_anggota INT NOT NULL DEFAULT 0,
latitude DECIMAL(10,7) NOT NULL,
longitude DECIMAL(10,7) NOT NULL,
address TEXT NULL,
kelurahan VARCHAR(120) NULL,
kecamatan VARCHAR(120) NULL,
status_bantuan VARCHAR(24) NOT NULL DEFAULT 'Belum dibantu', -- Belum dibantu|Sudah dibantu|Menunggu verifikasi
jenis_bantuan VARCHAR(120) NULL,
tanggal_bantuan DATE NULL,
bukti_file VARCHAR(255) NULL,
jarak_meter DOUBLE NULL,
-- id rumah ibadah yang menaungi (hasil pencarian radius terdekat)
rumah_ibadah_id INT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_miskin_latlng (latitude, longitude),
INDEX idx_miskin_ibadah (rumah_ibadah_id),
INDEX idx_miskin_status (status_bantuan),
CONSTRAINT fk_miskin_ibadah FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah_points(id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS anggota_keluarga (
id INT AUTO_INCREMENT PRIMARY KEY,
penduduk_id INT NOT NULL,
nama VARCHAR(200) NOT NULL,
hubungan VARCHAR(60) NOT NULL,
umur INT NULL,
pekerjaan VARCHAR(120) NULL,
keterangan VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_anggota_penduduk (penduduk_id),
CONSTRAINT fk_anggota_penduduk FOREIGN KEY (penduduk_id) REFERENCES pemukiman_miskin_points(id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- (Opsional) User login untuk admin/operator.
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
username VARCHAR(60) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'operator', -- admin|operator
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Jika ingin strict value, bisa pakai ENUM, misal:
-- status_jalan ENUM('Nasional','Provinsi','Kabupaten')
-- status_kepemilikan ENUM('SHM','HGB','HGU','HP')
-- ==========================================================
-- MIGRATION (ALTER) UNTUK DATABASE LAMA (TANPA HILANGKAN DATA)
-- ==========================================================
-- Kapan perlu menjalankan bagian ini?
-- - Jika database Anda dibuat dari project lama (kolom/tipe berbeda),
-- misalnya masih pakai kolom spatial `geom` atau belum punya kolom
-- `latitude/longitude/status_24jam/geojson`.
-- - Jika database Anda sudah dibuat pakai schema di atas (CREATE TABLE),
-- Anda biasanya TIDAK perlu ALTER apa-apa.
--
-- Cara pakai:
-- - Jalankan file ini di phpMyAdmin / MySQL client.
-- - Bagian CREATE TABLE aman (IF NOT EXISTS).
-- - Bagian MIGRATION ini aman (cek INFORMATION_SCHEMA sebelum ALTER).
DELIMITER $$
DROP PROCEDURE IF EXISTS migrate_webgis_spbu $$
CREATE PROCEDURE migrate_webgis_spbu()
BEGIN
-- ----------------------
-- spbu_points
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'spbu_points'
) THEN
-- Kolom atribut inti
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'no'
) THEN
ALTER TABLE spbu_points ADD COLUMN no VARCHAR(60) NULL AFTER nama;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'deskripsi'
) THEN
ALTER TABLE spbu_points ADD COLUMN deskripsi TEXT NULL AFTER no;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'status_24jam'
) THEN
ALTER TABLE spbu_points ADD COLUMN status_24jam TINYINT(1) NOT NULL DEFAULT 0 AFTER deskripsi;
END IF;
-- Lat/Lng: untuk menjaga data lama, dibuat NULL-able dulu.
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'latitude'
) THEN
ALTER TABLE spbu_points ADD COLUMN latitude DECIMAL(10,7) NULL AFTER status_24jam;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'longitude'
) THEN
ALTER TABLE spbu_points ADD COLUMN longitude DECIMAL(10,7) NULL AFTER latitude;
END IF;
-- Kolom geom (opsional) untuk kompatibilitas legacy
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'geom'
) THEN
ALTER TABLE spbu_points ADD COLUMN geom LONGTEXT NULL AFTER longitude;
END IF;
-- Index (jika belum ada)
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND index_name = 'idx_spbu_status24'
) THEN
ALTER TABLE spbu_points ADD INDEX idx_spbu_status24 (status_24jam);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND index_name = 'idx_spbu_latlng'
) THEN
ALTER TABLE spbu_points ADD INDEX idx_spbu_latlng (latitude, longitude);
END IF;
-- Backfill latitude/longitude dari geom GeoJSON (jika geom berisi JSON valid)
-- Catatan: ini hanya mengisi yang masih NULL supaya tidak menimpa data baru.
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'geom'
) THEN
-- Optional: JSON_VALID/JSON_EXTRACT bisa tidak tersedia pada versi tertentu.
-- Best-effort: jika fungsi tidak didukung, blok ini di-skip tanpa menghentikan migrasi.
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
UPDATE spbu_points
SET
longitude = CAST(JSON_UNQUOTE(JSON_EXTRACT(geom, '$.coordinates[0]')) AS DECIMAL(10,7)),
latitude = CAST(JSON_UNQUOTE(JSON_EXTRACT(geom, '$.coordinates[1]')) AS DECIMAL(10,7))
WHERE (latitude IS NULL OR longitude IS NULL)
AND geom IS NOT NULL
AND JSON_VALID(geom) = 1
AND JSON_UNQUOTE(JSON_EXTRACT(geom, '$.type')) = 'Point';
END;
END IF;
END IF;
-- ----------------------
-- jalan_lines
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines'
) THEN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'panjang_meter'
) THEN
ALTER TABLE jalan_lines ADD COLUMN panjang_meter DOUBLE NOT NULL DEFAULT 0 AFTER status_jalan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'geojson'
) THEN
ALTER TABLE jalan_lines ADD COLUMN geojson LONGTEXT NULL AFTER panjang_meter;
END IF;
-- Backfill geojson dari kolom spatial geom (jika ada)
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'geom'
) THEN
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
UPDATE jalan_lines
SET geojson = ST_AsGeoJSON(geom)
WHERE (geojson IS NULL OR geojson = '') AND geom IS NOT NULL;
END;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND index_name = 'idx_jalan_status'
) THEN
ALTER TABLE jalan_lines ADD INDEX idx_jalan_status (status_jalan);
END IF;
END IF;
-- ----------------------
-- parsil_tanah
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah'
) THEN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'luas_m2'
) THEN
ALTER TABLE parsil_tanah ADD COLUMN luas_m2 DOUBLE NOT NULL DEFAULT 0 AFTER status_kepemilikan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'geojson'
) THEN
ALTER TABLE parsil_tanah ADD COLUMN geojson LONGTEXT NULL AFTER luas_m2;
END IF;
-- Backfill geojson dari kolom spatial geom (jika ada)
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'geom'
) THEN
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
UPDATE parsil_tanah
SET geojson = ST_AsGeoJSON(geom)
WHERE (geojson IS NULL OR geojson = '') AND geom IS NOT NULL;
END;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND index_name = 'idx_parsil_status'
) THEN
ALTER TABLE parsil_tanah ADD INDEX idx_parsil_status (status_kepemilikan);
END IF;
END IF;
-- ----------------------
-- rumah_ibadah_points
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points'
) THEN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'jenis'
) THEN
ALTER TABLE rumah_ibadah_points ADD COLUMN jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya' AFTER nama;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'kontak'
) THEN
ALTER TABLE rumah_ibadah_points ADD COLUMN kontak VARCHAR(120) NULL AFTER jenis;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'radius_meter'
) THEN
ALTER TABLE rumah_ibadah_points ADD COLUMN radius_meter DOUBLE NOT NULL DEFAULT 300 AFTER kontak;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'address'
) THEN
ALTER TABLE rumah_ibadah_points ADD COLUMN address TEXT NULL AFTER longitude;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND index_name = 'idx_ibadah_latlng'
) THEN
ALTER TABLE rumah_ibadah_points ADD INDEX idx_ibadah_latlng (latitude, longitude);
END IF;
END IF;
-- ----------------------
-- pemukiman_miskin_points
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points'
) THEN
-- Backward compat: kalau masih pakai kolom `nama`, migrasikan ke `kk_nama`
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kk_nama'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN kk_nama VARCHAR(200) NULL AFTER id;
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
UPDATE pemukiman_miskin_points
SET kk_nama = COALESCE(NULLIF(kk_nama, ''), NULLIF(nama, ''))
WHERE (kk_nama IS NULL OR kk_nama = '');
END;
-- jadikan NOT NULL kalau sudah terisi
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
ALTER TABLE pemukiman_miskin_points MODIFY kk_nama VARCHAR(200) NOT NULL;
END;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'nik'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN nik VARCHAR(32) NULL AFTER kk_nama;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jumlah_anggota'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN jumlah_anggota INT NOT NULL DEFAULT 0 AFTER nik;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kelurahan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN kelurahan VARCHAR(120) NULL AFTER address;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kecamatan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN kecamatan VARCHAR(120) NULL AFTER kelurahan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'status_bantuan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN status_bantuan VARCHAR(24) NOT NULL DEFAULT 'Belum dibantu' AFTER kecamatan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jenis_bantuan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN jenis_bantuan VARCHAR(120) NULL AFTER status_bantuan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'tanggal_bantuan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN tanggal_bantuan DATE NULL AFTER jenis_bantuan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'bukti_file'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN bukti_file VARCHAR(255) NULL AFTER tanggal_bantuan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jarak_meter'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN jarak_meter DOUBLE NULL AFTER bukti_file;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'rumah_ibadah_id'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN rumah_ibadah_id INT NULL AFTER jarak_meter;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND index_name = 'idx_miskin_ibadah'
) THEN
ALTER TABLE pemukiman_miskin_points ADD INDEX idx_miskin_ibadah (rumah_ibadah_id);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND index_name = 'idx_miskin_latlng'
) THEN
ALTER TABLE pemukiman_miskin_points ADD INDEX idx_miskin_latlng (latitude, longitude);
END IF;
END IF;
END $$
-- Jalankan migrasi (aman dijalankan berulang)
CALL migrate_webgis_spbu() $$
DROP PROCEDURE IF EXISTS migrate_webgis_spbu $$
DELIMITER ;
+1
View File
@@ -0,0 +1 @@