edit files
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
|||||||
|
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/webgis02/uploads/jalan \
|
||||||
|
/var/www/html/webgis02/uploads/parsil \
|
||||||
|
/var/www/html/webgis02/uploads/jalan_rusak \
|
||||||
|
/var/www/html/webgis-v3/uploads/bukti_bantuan \
|
||||||
|
/var/www/html/webgis-v3/uploads/foto_laporan \
|
||||||
|
&& chown -R www-data:www-data /var/www/html
|
||||||
+1158
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
|||||||
|
-- Setup database untuk CombineWebgis.
|
||||||
|
-- Import file ini lewat phpMyAdmin atau MySQL CLI setelah MySQL XAMPP berjalan normal.
|
||||||
|
-- Catatan: tabel project akan dibuat ulang agar schema bersih dan cocok dengan kode PHP.
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS db_spbu
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE db_spbu;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS spbu;
|
||||||
|
|
||||||
|
CREATE TABLE spbu (
|
||||||
|
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
nama_spbu VARCHAR(100) NOT NULL,
|
||||||
|
no_wa VARCHAR(20) NOT NULL,
|
||||||
|
status ENUM('yes','no') NOT NULL DEFAULT 'no',
|
||||||
|
latitude DOUBLE NOT NULL,
|
||||||
|
longitude DOUBLE NOT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO spbu (nama_spbu, no_wa, status, latitude, longitude) VALUES
|
||||||
|
('SPBU Gajah Mada', '08115700101', 'yes', -0.0227, 109.3294),
|
||||||
|
('SPBU Ahmad Yani', '08115700102', 'no', -0.0350, 109.3350),
|
||||||
|
('SPBU Tanjungpura', '08115700103', 'yes', -0.0180, 109.3410);
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS webgis_db
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE webgis_db;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS tb_jalan_rusak;
|
||||||
|
DROP TABLE IF EXISTS tb_parsil;
|
||||||
|
DROP TABLE IF EXISTS tb_jalan;
|
||||||
|
|
||||||
|
CREATE TABLE tb_jalan (
|
||||||
|
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
nama_jalan VARCHAR(200) NOT NULL,
|
||||||
|
status_jalan ENUM('Nasional','Provinsi','Kabupaten') NOT NULL DEFAULT 'Kabupaten',
|
||||||
|
panjang_meter DECIMAL(15,2) NOT NULL DEFAULT 0,
|
||||||
|
geojson LONGTEXT NOT NULL,
|
||||||
|
keterangan TEXT,
|
||||||
|
foto VARCHAR(255) NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE tb_parsil (
|
||||||
|
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
nama_pemilik VARCHAR(200) NOT NULL,
|
||||||
|
no_sertifikat VARCHAR(100) NOT NULL,
|
||||||
|
status_kepemilikan ENUM('SHM','HGB','HGU','HP') NOT NULL DEFAULT 'SHM',
|
||||||
|
luas_m2 DECIMAL(20,4) NOT NULL DEFAULT 0,
|
||||||
|
geojson LONGTEXT NOT NULL,
|
||||||
|
alamat TEXT,
|
||||||
|
foto VARCHAR(255) NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE tb_jalan_rusak (
|
||||||
|
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
lokasi VARCHAR(200) NOT NULL,
|
||||||
|
tingkat ENUM('Ringan','Sedang','Berat') NOT NULL DEFAULT 'Sedang',
|
||||||
|
jenis VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
geojson LONGTEXT NOT NULL,
|
||||||
|
keterangan TEXT,
|
||||||
|
foto VARCHAR(255) NULL,
|
||||||
|
tanggal_lapor DATE NOT NULL DEFAULT (CURRENT_DATE),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO tb_jalan (nama_jalan, status_jalan, panjang_meter, geojson, keterangan) VALUES
|
||||||
|
('Jl. Ahmad Yani','Nasional',1240.50,'{"type":"Feature","geometry":{"type":"LineString","coordinates":[[109.3272,-0.0207],[109.3290,-0.0215],[109.3310,-0.0225],[109.3335,-0.0238]]},"properties":{}}','Jalan Nasional utama kota Pontianak'),
|
||||||
|
('Jl. Gajah Mada','Provinsi',870.00,'{"type":"Feature","geometry":{"type":"LineString","coordinates":[[109.3250,-0.0260],[109.3265,-0.0270],[109.3280,-0.0285]]},"properties":{}}','Jalan Provinsi pusat kota'),
|
||||||
|
('Jl. Pahlawan','Kabupaten',540.75,'{"type":"Feature","geometry":{"type":"LineString","coordinates":[[109.3300,-0.0300],[109.3315,-0.0310],[109.3325,-0.0322]]},"properties":{}}','Jalan Kabupaten lingkungan');
|
||||||
|
|
||||||
|
INSERT INTO tb_parsil (nama_pemilik, no_sertifikat, status_kepemilikan, luas_m2, geojson, alamat) VALUES
|
||||||
|
('Budi Santoso','SHM-001/PTK/2023','SHM',4500.00,'{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[109.3280,-0.0240],[109.3295,-0.0240],[109.3295,-0.0255],[109.3280,-0.0255],[109.3280,-0.0240]]]},"properties":{}}','Jl. Ahmad Yani No. 10, Pontianak'),
|
||||||
|
('PT. Maju Jaya','HGB-005/PTK/2022','HGB',12000.00,'{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[109.3310,-0.0220],[109.3330,-0.0220],[109.3330,-0.0240],[109.3310,-0.0240],[109.3310,-0.0220]]]},"properties":{}}','Kawasan Industri Pontianak');
|
||||||
|
|
||||||
|
INSERT INTO tb_jalan_rusak (lokasi, tingkat, jenis, geojson, keterangan, tanggal_lapor) VALUES
|
||||||
|
('Depan SPBU Jl. Ahmad Yani','Berat','Berlubang','{"type":"Feature","geometry":{"type":"Point","coordinates":[109.3285,-0.0212]},"properties":{}}','Lubang besar diameter sekitar 50cm, berbahaya bagi pengendara','2024-10-15'),
|
||||||
|
('Pertigaan Jl. Gajah Mada','Sedang','Retak','{"type":"Feature","geometry":{"type":"Point","coordinates":[109.3260,-0.0268]},"properties":{}}','Retak memanjang sepanjang 3 meter','2024-11-02');
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Options -Indexes
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Route semua /api/* ke api/index.php
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^api/(.*)$ api/index.php [QSA,L]
|
||||||
|
|
||||||
|
# Security
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header set X-Content-Type-Options nosniff
|
||||||
|
Header set X-Frame-Options SAMEORIGIN
|
||||||
|
</IfModule>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# WebGIS Pengentasan Kemiskinan — Pontianak v2.0
|
||||||
|
### Sistem Informasi Geografis berbasis Masyarakat & Rumah Ibadah
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Struktur Proyek
|
||||||
|
|
||||||
|
```
|
||||||
|
webgis-kemiskinan/
|
||||||
|
├── index.html # Frontend utama
|
||||||
|
├── .htaccess # URL rewriting Apache
|
||||||
|
├── config/
|
||||||
|
│ ├── app.php # Konstanta aplikasi
|
||||||
|
│ └── database.php # Koneksi PDO MySQL
|
||||||
|
├── includes/
|
||||||
|
│ └── helpers.php # Auth, sanitasi, log, utils
|
||||||
|
├── api/
|
||||||
|
│ ├── index.php # Router RESTful
|
||||||
|
│ └── controllers/
|
||||||
|
│ ├── AuthController.php
|
||||||
|
│ ├── PendudukController.php
|
||||||
|
│ ├── KeluargaController.php
|
||||||
|
│ ├── BantuanController.php
|
||||||
|
│ ├── HistoriController.php
|
||||||
|
│ ├── LaporanController.php
|
||||||
|
│ ├── RumahIbadahController.php
|
||||||
|
│ ├── DashboardController.php
|
||||||
|
│ ├── PetaController.php
|
||||||
|
│ ├── PrioritasController.php
|
||||||
|
│ ├── ChatController.php
|
||||||
|
│ ├── LogController.php
|
||||||
|
│ ├── PeristiwaController.php
|
||||||
|
│ └── UserController.php
|
||||||
|
├── assets/
|
||||||
|
│ ├── css/style.css
|
||||||
|
│ └── js/
|
||||||
|
│ ├── api.js # HTTP helper
|
||||||
|
│ ├── app.js # Bootstrap & routing
|
||||||
|
│ ├── map.js # Leaflet engine
|
||||||
|
│ ├── dashboard.js
|
||||||
|
│ ├── penduduk.js
|
||||||
|
│ ├── keluarga.js
|
||||||
|
│ ├── bantuan.js
|
||||||
|
│ ├── laporan.js
|
||||||
|
│ └── ri.js # RI + Prioritas + Chat + Log
|
||||||
|
├── uploads/
|
||||||
|
│ ├── foto_laporan/
|
||||||
|
│ └── bukti_bantuan/
|
||||||
|
└── sql/
|
||||||
|
└── schema.sql # Database lengkap + seed data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cara Instalasi
|
||||||
|
|
||||||
|
### 1. Persyaratan
|
||||||
|
- PHP 8.0+
|
||||||
|
- MySQL 5.7+ / MariaDB 10.3+
|
||||||
|
- Apache/Nginx dengan mod_rewrite aktif
|
||||||
|
|
||||||
|
### 2. Setup Database
|
||||||
|
```sql
|
||||||
|
mysql -u root -p < sql/schema.sql
|
||||||
|
```
|
||||||
|
Atau import via phpMyAdmin.
|
||||||
|
|
||||||
|
### 3. Konfigurasi
|
||||||
|
Edit `config/database.php`:
|
||||||
|
```php
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_NAME', 'webgis_kemiskinan');
|
||||||
|
define('DB_USER', 'root');
|
||||||
|
define('DB_PASS', 'password_anda');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Web Server
|
||||||
|
Letakkan folder di `htdocs/` (XAMPP) atau `/var/www/html/`.
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost/webgis-kemiskinan/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Akun Demo
|
||||||
|
| Username | Password | Role |
|
||||||
|
|----------------|----------|------------|
|
||||||
|
| admin | admin123 | Admin |
|
||||||
|
| pengurus_jami | admin123 | Pengurus RI|
|
||||||
|
| pimpinan | admin123 | Pimpinan |
|
||||||
|
| warga1 | admin123 | Masyarakat |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 API Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Keterangan |
|
||||||
|
|--------|------------------------------|-------------------------------|
|
||||||
|
| POST | /api/auth | Login |
|
||||||
|
| DELETE | /api/auth/logout | Logout |
|
||||||
|
| GET | /api/auth/me | Info user saat ini |
|
||||||
|
| GET | /api/penduduk | List penduduk |
|
||||||
|
| POST | /api/penduduk | Tambah penduduk |
|
||||||
|
| PUT | /api/penduduk/:nik | Update penduduk |
|
||||||
|
| DELETE | /api/penduduk/:nik | Hapus penduduk |
|
||||||
|
| GET | /api/keluarga | List keluarga |
|
||||||
|
| POST | /api/keluarga | Tambah KK |
|
||||||
|
| GET | /api/bantuan | Jenis bantuan |
|
||||||
|
| POST | /api/histori | Catat penyaluran |
|
||||||
|
| GET | /api/histori/:nik/cek | Cek riwayat penerima |
|
||||||
|
| POST | /api/laporan | Kirim laporan (publik) |
|
||||||
|
| PUT | /api/laporan/:id | Update status laporan |
|
||||||
|
| GET | /api/rumah-ibadah | List rumah ibadah |
|
||||||
|
| GET | /api/dashboard | Statistik dashboard |
|
||||||
|
| GET | /api/peta?layer=penduduk | GeoJSON peta penduduk |
|
||||||
|
| GET | /api/peta?layer=rumah_ibadah | GeoJSON rumah ibadah |
|
||||||
|
| GET | /api/peta?layer=laporan | GeoJSON laporan |
|
||||||
|
| GET | /api/prioritas | Daftar prioritas bantuan |
|
||||||
|
| POST | /api/chat | Kirim pesan |
|
||||||
|
| POST | /api/peristiwa | Catat peristiwa kependudukan |
|
||||||
|
| GET | /api/log | Log aktivitas |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Struktur Database (ERD Ringkas)
|
||||||
|
|
||||||
|
```
|
||||||
|
role ──────────────────── user
|
||||||
|
│
|
||||||
|
rumah_ibadah ┘
|
||||||
|
│
|
||||||
|
keluarga ─────── penduduk ──── histori_bantuan ─── bantuan
|
||||||
|
│
|
||||||
|
laporan (opsional nik_terdampak)
|
||||||
|
riwayat_pendidikan
|
||||||
|
peristiwa_kependudukan
|
||||||
|
log_aktivitas
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Fitur Sistem
|
||||||
|
|
||||||
|
| # | Fitur | Status |
|
||||||
|
|---|-------|--------|
|
||||||
|
| 1 | CRUD Penduduk + validasi NIK 16 digit | ✅ |
|
||||||
|
| 2 | Umur otomatis dari tanggal lahir | ✅ |
|
||||||
|
| 3 | Validasi logika umur vs pendidikan | ✅ |
|
||||||
|
| 4 | Relasi Keluarga (KK → Anggota) | ✅ |
|
||||||
|
| 5 | Sistem peristiwa kependudukan | ✅ |
|
||||||
|
| 6 | Laporan masyarakat + status tracking | ✅ |
|
||||||
|
| 7 | Histori bantuan per NIK | ✅ |
|
||||||
|
| 8 | Cek penerima bantuan sebelumnya | ✅ |
|
||||||
|
| 9 | Multi-role (Admin/Pengurus/Pimpinan/Masyarakat) | ✅ |
|
||||||
|
| 10| WebGIS Leaflet + cluster + radius RI | ✅ |
|
||||||
|
| 11| Layer filter (miskin/rentan/mampu) | ✅ |
|
||||||
|
| 12| Prioritas bantuan otomatis (miskin+sakit) | ✅ |
|
||||||
|
| 13| Dashboard statistik real-time | ✅ |
|
||||||
|
| 14| Chat koordinasi antar pengurus | ✅ |
|
||||||
|
| 15| Log aktivitas sistem | ✅ |
|
||||||
|
| 16| Export CSV prioritas | ✅ |
|
||||||
|
| 17| Deteksi warga belum pernah dibantu | ✅ |
|
||||||
|
| 18| GeoJSON API untuk Leaflet | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Hak Akses per Role
|
||||||
|
|
||||||
|
| Fitur | Admin | Pengurus | Pimpinan | Masyarakat |
|
||||||
|
|-----------------------|-------|----------|----------|------------|
|
||||||
|
| CRUD Penduduk | ✅ | ✅ | 👁 | ❌ |
|
||||||
|
| CRUD Keluarga | ✅ | ✅ | 👁 | ❌ |
|
||||||
|
| Salurkan Bantuan | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| Verifikasi Laporan | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| Kirim Laporan | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Dashboard | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Manajemen User | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| Chat Koordinasi | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| Log Aktivitas | ✅ | ❌ | ✅ | ❌ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*WebGIS Kemiskinan Pontianak v2.0 — Dikembangkan untuk mendukung intervensi bantuan sosial tepat sasaran berbasis data spasial.*
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/AuthController.php
|
||||||
|
class AuthController {
|
||||||
|
|
||||||
|
public function index(): void { jsonError('Method tidak diizinkan', 405); }
|
||||||
|
public function show(string $id): void { jsonError('Method tidak diizinkan', 405); }
|
||||||
|
public function update(string $id): void { jsonError('Method tidak diizinkan', 405); }
|
||||||
|
public function destroy(string $id): void { jsonError('Method tidak diizinkan', 405); }
|
||||||
|
|
||||||
|
/** POST /api/auth — login */
|
||||||
|
public function store(): void {
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['username', 'password']);
|
||||||
|
if ($errs) jsonError(implode('; ', $errs));
|
||||||
|
|
||||||
|
$user = login(sanitize($body['username']), $body['password']);
|
||||||
|
if (!$user) jsonError('Username atau password salah', 401);
|
||||||
|
|
||||||
|
startSession();
|
||||||
|
$_SESSION['user'] = $user;
|
||||||
|
logActivity('login', 'user', $user['id']);
|
||||||
|
jsonSuccess($user, 'Login berhasil');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DELETE /api/auth/:any — logout */
|
||||||
|
public function logout(): void {
|
||||||
|
startSession();
|
||||||
|
$user = currentUser();
|
||||||
|
if ($user) logActivity('logout', 'user', $user['id']);
|
||||||
|
session_destroy();
|
||||||
|
jsonSuccess([], 'Logout berhasil');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/auth/me */
|
||||||
|
public function me(): void {
|
||||||
|
$user = currentUser();
|
||||||
|
if (!$user) jsonError('Tidak login', 401);
|
||||||
|
jsonSuccess($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/BantuanController.php
|
||||||
|
class BantuanController {
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$rows = $db->query("SELECT * FROM bantuan ORDER BY kategori, nama_bantuan")->fetchAll();
|
||||||
|
jsonSuccess($rows);
|
||||||
|
}
|
||||||
|
public function show(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("SELECT * FROM bantuan WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) jsonError('Jenis bantuan tidak ditemukan', 404);
|
||||||
|
jsonSuccess($row);
|
||||||
|
}
|
||||||
|
public function store(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['nama_bantuan', 'kategori']);
|
||||||
|
if ($errs) jsonError(implode('; ', $errs));
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO bantuan (nama_bantuan, kategori, sumber, deskripsi, satuan)
|
||||||
|
VALUES (?,?,?,?,?)"
|
||||||
|
)->execute([
|
||||||
|
sanitize($body['nama_bantuan']),
|
||||||
|
$body['kategori'],
|
||||||
|
sanitize($body['sumber'] ?? ''),
|
||||||
|
sanitize($body['deskripsi'] ?? ''),
|
||||||
|
sanitize($body['satuan'] ?? ''),
|
||||||
|
]);
|
||||||
|
jsonSuccess(['id' => getDB()->lastInsertId()], 'Jenis bantuan ditambahkan', 201);
|
||||||
|
}
|
||||||
|
public function update(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$body = getBody();
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare(
|
||||||
|
"UPDATE bantuan SET nama_bantuan=?, kategori=?, sumber=?, deskripsi=?, satuan=?, aktif=?
|
||||||
|
WHERE id=?"
|
||||||
|
)->execute([
|
||||||
|
sanitize($body['nama_bantuan'] ?? ''),
|
||||||
|
$body['kategori'] ?? 'lainnya',
|
||||||
|
sanitize($body['sumber'] ?? ''),
|
||||||
|
sanitize($body['deskripsi'] ?? ''),
|
||||||
|
sanitize($body['satuan'] ?? ''),
|
||||||
|
(int)($body['aktif'] ?? 1),
|
||||||
|
$id,
|
||||||
|
]);
|
||||||
|
jsonSuccess([], 'Jenis bantuan diperbarui');
|
||||||
|
}
|
||||||
|
public function destroy(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare("DELETE FROM bantuan WHERE id = ?")->execute([$id]);
|
||||||
|
jsonSuccess([], 'Jenis bantuan dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/ChatController.php
|
||||||
|
class ChatController {
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$db = getDB();
|
||||||
|
$roomId = $_GET['room'] ?? null;
|
||||||
|
$sql = "SELECT c.id, c.pesan, c.created_at, u.nama_lengkap, u.id_role,
|
||||||
|
cr.nama AS nama_room
|
||||||
|
FROM chat c JOIN user u ON c.id_user=u.id
|
||||||
|
LEFT JOIN chat_room cr ON c.id_room=cr.id
|
||||||
|
WHERE " . ($roomId ? "c.id_room=?" : "c.id_room IS NULL") .
|
||||||
|
" ORDER BY c.created_at DESC LIMIT 50";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$roomId ? $stmt->execute([$roomId]) : $stmt->execute();
|
||||||
|
jsonSuccess(array_reverse($stmt->fetchAll()));
|
||||||
|
}
|
||||||
|
public function show(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$db = getDB();
|
||||||
|
$rows = $db->query("SELECT id, nama, deskripsi FROM chat_room ORDER BY nama")->fetchAll();
|
||||||
|
jsonSuccess($rows);
|
||||||
|
}
|
||||||
|
public function store(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$user = currentUser();
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['pesan']);
|
||||||
|
if ($errs) jsonError(implode('; ',$errs));
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare("INSERT INTO chat (id_user, id_room, pesan) VALUES (?,?,?)")->execute([
|
||||||
|
$user['id'],
|
||||||
|
$body['id_room'] ?? null,
|
||||||
|
sanitize($body['pesan']),
|
||||||
|
]);
|
||||||
|
jsonSuccess(['id' => $db->lastInsertId()], 'Pesan terkirim', 201);
|
||||||
|
}
|
||||||
|
public function update(string $id): void { jsonError('N/A',405); }
|
||||||
|
public function destroy(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
getDB()->prepare("DELETE FROM chat WHERE id=?")->execute([$id]);
|
||||||
|
jsonSuccess([],'Pesan dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/DashboardController.php
|
||||||
|
class DashboardController {
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Ringkasan utama
|
||||||
|
$stat = $db->query(
|
||||||
|
"SELECT
|
||||||
|
(SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup') AS total_penduduk,
|
||||||
|
(SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup' AND status_ekonomi='miskin') AS total_miskin,
|
||||||
|
(SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup' AND status_ekonomi='rentan') AS total_rentan,
|
||||||
|
(SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup' AND status_ekonomi='mampu') AS total_mampu,
|
||||||
|
(SELECT COUNT(*) FROM keluarga) AS total_kk,
|
||||||
|
(SELECT COUNT(*) FROM rumah_ibadah WHERE aktif=1) AS total_ri,
|
||||||
|
(SELECT COUNT(*) FROM laporan WHERE status='pending') AS laporan_pending,
|
||||||
|
(SELECT COUNT(*) FROM laporan WHERE urgensi='darurat' AND status NOT IN('selesai')) AS laporan_darurat,
|
||||||
|
(SELECT COUNT(DISTINCT nik) FROM histori_bantuan WHERE status_penyaluran='disalurkan') AS sudah_dibantu,
|
||||||
|
(SELECT COUNT(*) FROM histori_bantuan WHERE MONTH(tanggal)=MONTH(CURDATE()) AND YEAR(tanggal)=YEAR(CURDATE())) AS bantuan_bulan_ini"
|
||||||
|
)->fetch();
|
||||||
|
|
||||||
|
// Sebaran per kecamatan
|
||||||
|
$kec = $db->query(
|
||||||
|
"SELECT k.kecamatan,
|
||||||
|
COUNT(DISTINCT k.id) AS jumlah_kk,
|
||||||
|
SUM(CASE WHEN k.status_ekonomi='miskin' THEN 1 ELSE 0 END) AS miskin,
|
||||||
|
SUM(CASE WHEN k.status_ekonomi='rentan' THEN 1 ELSE 0 END) AS rentan
|
||||||
|
FROM keluarga k GROUP BY k.kecamatan ORDER BY miskin DESC"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
// Trend bantuan 6 bulan
|
||||||
|
$trend = $db->query(
|
||||||
|
"SELECT DATE_FORMAT(tanggal,'%Y-%m') AS bulan, COUNT(*) AS jumlah
|
||||||
|
FROM histori_bantuan
|
||||||
|
WHERE tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||||
|
GROUP BY bulan ORDER BY bulan"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
// Belum pernah dibantu (miskin)
|
||||||
|
$belum = $db->query(
|
||||||
|
"SELECT COUNT(*) AS c FROM penduduk p
|
||||||
|
WHERE p.status_ekonomi IN ('miskin','rentan')
|
||||||
|
AND p.status_hidup='hidup'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM histori_bantuan hb WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan')"
|
||||||
|
)->fetch()['c'];
|
||||||
|
|
||||||
|
// Prioritas darurat
|
||||||
|
$prioritas = $db->query(
|
||||||
|
"SELECT * FROM v_prioritas_bantuan
|
||||||
|
WHERE level_prioritas IN ('SANGAT TINGGI','TINGGI')
|
||||||
|
LIMIT 10"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
// Beban per RI
|
||||||
|
$bebanRI = $db->query(
|
||||||
|
"SELECT ri.nama, ri.jenis,
|
||||||
|
COUNT(DISTINCT 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.id_rumah_ibadah=ri.id
|
||||||
|
WHERE ri.aktif=1
|
||||||
|
GROUP BY ri.id ORDER BY kk_miskin DESC"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
jsonSuccess([
|
||||||
|
'statistik' => $stat,
|
||||||
|
'sebaran_kecamatan' => $kec,
|
||||||
|
'trend_bantuan' => $trend,
|
||||||
|
'belum_dibantu' => (int)$belum,
|
||||||
|
'prioritas_darurat' => $prioritas,
|
||||||
|
'beban_per_ri' => $bebanRI,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(string $id): void { jsonError('Not implemented', 501); }
|
||||||
|
public function store(): void { jsonError('Not implemented', 501); }
|
||||||
|
public function update(string $id): void { jsonError('Not implemented', 501); }
|
||||||
|
public function destroy(string $id): void { jsonError('Not implemented', 501); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/HistoriController.php
|
||||||
|
class HistoriController {
|
||||||
|
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$sql = "SELECT hb.*, b.nama_bantuan, b.kategori,
|
||||||
|
p.nama_lengkap, p.status_ekonomi,
|
||||||
|
ri.nama AS nama_ri
|
||||||
|
FROM histori_bantuan hb
|
||||||
|
JOIN bantuan b ON hb.id_bantuan = b.id
|
||||||
|
JOIN penduduk p ON hb.nik = p.nik
|
||||||
|
LEFT JOIN rumah_ibadah ri ON hb.id_rumah_ibadah = ri.id
|
||||||
|
WHERE 1=1";
|
||||||
|
$params = [];
|
||||||
|
if (!empty($_GET['nik'])) {
|
||||||
|
$sql .= " AND hb.nik = ?"; $params[] = $_GET['nik'];
|
||||||
|
}
|
||||||
|
if (!empty($_GET['id_bantuan'])) {
|
||||||
|
$sql .= " AND hb.id_bantuan = ?"; $params[] = (int)$_GET['id_bantuan'];
|
||||||
|
}
|
||||||
|
if (!empty($_GET['status'])) {
|
||||||
|
$sql .= " AND hb.status_penyaluran = ?"; $params[] = $_GET['status'];
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY hb.tanggal DESC LIMIT 200";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
jsonSuccess($stmt->fetchAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT hb.*, b.nama_bantuan, b.kategori, p.nama_lengkap, ri.nama AS nama_ri
|
||||||
|
FROM histori_bantuan hb
|
||||||
|
JOIN bantuan b ON hb.id_bantuan=b.id
|
||||||
|
JOIN penduduk p ON hb.nik=p.nik
|
||||||
|
LEFT JOIN rumah_ibadah ri ON hb.id_rumah_ibadah=ri.id
|
||||||
|
WHERE hb.id=?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) jsonError('Histori tidak ditemukan', 404);
|
||||||
|
jsonSuccess($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/histori — catat penyaluran bantuan */
|
||||||
|
public function store(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['nik', 'id_bantuan', 'tanggal']);
|
||||||
|
if ($errs) jsonError(implode('; ', $errs));
|
||||||
|
|
||||||
|
$user = currentUser();
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Cek penduduk ada
|
||||||
|
$chk = $db->prepare("SELECT nik FROM penduduk WHERE nik = ?");
|
||||||
|
$chk->execute([$body['nik']]);
|
||||||
|
if (!$chk->fetch()) jsonError('NIK penduduk tidak ditemukan', 404);
|
||||||
|
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO histori_bantuan
|
||||||
|
(nik, id_bantuan, id_rumah_ibadah, id_user, tanggal, jumlah, keterangan, status_penyaluran)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?)"
|
||||||
|
)->execute([
|
||||||
|
$body['nik'],
|
||||||
|
(int)$body['id_bantuan'],
|
||||||
|
$body['id_rumah_ibadah'] ?? null,
|
||||||
|
$user['id'],
|
||||||
|
$body['tanggal'],
|
||||||
|
(float)($body['jumlah'] ?? 1),
|
||||||
|
sanitize($body['keterangan'] ?? ''),
|
||||||
|
$body['status_penyaluran'] ?? 'disalurkan',
|
||||||
|
]);
|
||||||
|
$newId = $db->lastInsertId();
|
||||||
|
logActivity('create_histori_bantuan', 'histori_bantuan', $newId, [], $body);
|
||||||
|
jsonSuccess(['id' => $newId], 'Histori bantuan dicatat', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$body = getBody();
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare(
|
||||||
|
"UPDATE histori_bantuan SET status_penyaluran=?, keterangan=?, jumlah=? WHERE id=?"
|
||||||
|
)->execute([
|
||||||
|
$body['status_penyaluran'] ?? 'disalurkan',
|
||||||
|
sanitize($body['keterangan'] ?? ''),
|
||||||
|
(float)($body['jumlah'] ?? 1),
|
||||||
|
$id,
|
||||||
|
]);
|
||||||
|
logActivity('update_histori_bantuan', 'histori_bantuan', $id, [], $body);
|
||||||
|
jsonSuccess([], 'Histori bantuan diperbarui');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare("DELETE FROM histori_bantuan WHERE id=?")->execute([$id]);
|
||||||
|
jsonSuccess([], 'Histori bantuan dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/histori/:nik/cek */
|
||||||
|
public function cek(string $nik): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT COUNT(*) AS total,
|
||||||
|
GROUP_CONCAT(DISTINCT b.nama_bantuan ORDER BY b.nama_bantuan SEPARATOR ', ') AS jenis_bantuan,
|
||||||
|
MAX(hb.tanggal) AS terakhir_dibantu
|
||||||
|
FROM histori_bantuan hb
|
||||||
|
JOIN bantuan b ON hb.id_bantuan=b.id
|
||||||
|
WHERE hb.nik=? AND hb.status_penyaluran='disalurkan'"
|
||||||
|
);
|
||||||
|
$stmt->execute([$nik]);
|
||||||
|
jsonSuccess($stmt->fetch());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/KeluargaController.php
|
||||||
|
class KeluargaController {
|
||||||
|
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$sql = "SELECT k.*, ri.nama AS nama_ri,
|
||||||
|
COUNT(p.nik) AS jumlah_anggota,
|
||||||
|
SUM(p.penghasilan) AS total_penghasilan,
|
||||||
|
COUNT(DISTINCT hb.id) AS total_bantuan
|
||||||
|
FROM keluarga k
|
||||||
|
LEFT JOIN rumah_ibadah ri ON k.id_rumah_ibadah = ri.id
|
||||||
|
LEFT JOIN penduduk p ON p.id_keluarga = k.id AND p.status_hidup='hidup'
|
||||||
|
LEFT JOIN histori_bantuan hb ON hb.nik = p.nik
|
||||||
|
WHERE 1=1";
|
||||||
|
$params = [];
|
||||||
|
if (!empty($_GET['status_ekonomi'])) {
|
||||||
|
$sql .= " AND k.status_ekonomi = ?"; $params[] = $_GET['status_ekonomi'];
|
||||||
|
}
|
||||||
|
if (!empty($_GET['kecamatan'])) {
|
||||||
|
$sql .= " AND k.kecamatan = ?"; $params[] = $_GET['kecamatan'];
|
||||||
|
}
|
||||||
|
$sql .= " GROUP BY k.id ORDER BY k.status_ekonomi, k.nama_kepala";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
jsonSuccess($stmt->fetchAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT k.*, ri.nama AS nama_ri FROM keluarga k
|
||||||
|
LEFT JOIN rumah_ibadah ri ON k.id_rumah_ibadah = ri.id
|
||||||
|
WHERE k.id = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$kk = $stmt->fetch();
|
||||||
|
if (!$kk) jsonError('Keluarga tidak ditemukan', 404);
|
||||||
|
|
||||||
|
$anggota = $db->prepare(
|
||||||
|
"SELECT nik, nama_lengkap, jenis_kelamin, status_keluarga,
|
||||||
|
TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE()) AS umur,
|
||||||
|
pekerjaan, penghasilan, status_ekonomi, kondisi_kesehatan,
|
||||||
|
pendidikan_terakhir, status_pendidikan, status_hidup
|
||||||
|
FROM penduduk WHERE id_keluarga = ? ORDER BY status_keluarga"
|
||||||
|
);
|
||||||
|
$anggota->execute([$id]);
|
||||||
|
$kk['anggota'] = $anggota->fetchAll();
|
||||||
|
jsonSuccess($kk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['no_kk', 'nama_kepala']);
|
||||||
|
if ($errs) jsonError(implode('; ', $errs));
|
||||||
|
if (!preg_match('/^\d{16}$/', $body['no_kk'])) jsonError('No KK harus 16 digit');
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$chk = $db->prepare("SELECT id FROM keluarga WHERE no_kk = ?");
|
||||||
|
$chk->execute([$body['no_kk']]);
|
||||||
|
if ($chk->fetch()) jsonError('No KK sudah terdaftar', 409);
|
||||||
|
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO keluarga (no_kk, id_rumah_ibadah, nama_kepala, alamat, rt, rw,
|
||||||
|
kelurahan, kecamatan, lat, lng, status_ekonomi)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)"
|
||||||
|
)->execute([
|
||||||
|
$body['no_kk'],
|
||||||
|
$body['id_rumah_ibadah'] ?? null,
|
||||||
|
sanitize($body['nama_kepala']),
|
||||||
|
sanitize($body['alamat'] ?? ''),
|
||||||
|
sanitize($body['rt'] ?? ''),
|
||||||
|
sanitize($body['rw'] ?? ''),
|
||||||
|
sanitize($body['kelurahan'] ?? ''),
|
||||||
|
sanitize($body['kecamatan'] ?? ''),
|
||||||
|
$body['lat'] ?? null,
|
||||||
|
$body['lng'] ?? null,
|
||||||
|
$body['status_ekonomi'] ?? 'rentan',
|
||||||
|
]);
|
||||||
|
$newId = $db->lastInsertId();
|
||||||
|
logActivity('create_keluarga', 'keluarga', $newId, [], $body);
|
||||||
|
jsonSuccess(['id' => $newId], 'Data keluarga berhasil disimpan', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$db = getDB();
|
||||||
|
$body = getBody();
|
||||||
|
$db->prepare(
|
||||||
|
"UPDATE keluarga SET id_rumah_ibadah=?, nama_kepala=?, alamat=?,
|
||||||
|
rt=?, rw=?, kelurahan=?, kecamatan=?, lat=?, lng=?, status_ekonomi=?
|
||||||
|
WHERE id=?"
|
||||||
|
)->execute([
|
||||||
|
$body['id_rumah_ibadah'] ?? null,
|
||||||
|
sanitize($body['nama_kepala'] ?? ''),
|
||||||
|
sanitize($body['alamat'] ?? ''),
|
||||||
|
sanitize($body['rt'] ?? ''),
|
||||||
|
sanitize($body['rw'] ?? ''),
|
||||||
|
sanitize($body['kelurahan'] ?? ''),
|
||||||
|
sanitize($body['kecamatan'] ?? ''),
|
||||||
|
$body['lat'] ?? null,
|
||||||
|
$body['lng'] ?? null,
|
||||||
|
$body['status_ekonomi'] ?? 'rentan',
|
||||||
|
$id,
|
||||||
|
]);
|
||||||
|
logActivity('update_keluarga', 'keluarga', $id, [], $body);
|
||||||
|
jsonSuccess([], 'Data keluarga diperbarui');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
$db = getDB();
|
||||||
|
$chk = $db->prepare("SELECT COUNT(*) AS c FROM penduduk WHERE id_keluarga = ?");
|
||||||
|
$chk->execute([$id]);
|
||||||
|
if ($chk->fetch()['c'] > 0) jsonError('Tidak bisa hapus — masih ada anggota keluarga', 409);
|
||||||
|
$db->prepare("DELETE FROM keluarga WHERE id = ?")->execute([$id]);
|
||||||
|
logActivity('delete_keluarga', 'keluarga', $id);
|
||||||
|
jsonSuccess([], 'Data keluarga dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/LaporanController.php
|
||||||
|
class LaporanController {
|
||||||
|
|
||||||
|
public function index(): void {
|
||||||
|
$user = currentUser();
|
||||||
|
// Masyarakat bisa lihat laporan mereka sendiri; pengurus+ lihat semua
|
||||||
|
$db = getDB();
|
||||||
|
$sql = "SELECT l.*, ri.nama AS nama_ri, p.nama_lengkap AS nama_terdampak
|
||||||
|
FROM laporan l
|
||||||
|
LEFT JOIN rumah_ibadah ri ON l.id_rumah_ibadah=ri.id
|
||||||
|
LEFT JOIN penduduk p ON l.nik_terdampak=p.nik
|
||||||
|
WHERE 1=1";
|
||||||
|
$params = [];
|
||||||
|
if (!empty($_GET['status'])) { $sql .= " AND l.status=?"; $params[] = $_GET['status']; }
|
||||||
|
if (!empty($_GET['urgensi'])) { $sql .= " AND l.urgensi=?"; $params[] = $_GET['urgensi']; }
|
||||||
|
$sql .= " ORDER BY FIELD(l.urgensi,'darurat','tinggi','sedang','rendah'), l.created_at DESC LIMIT 100";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
jsonSuccess($stmt->fetchAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(string $id): void {
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT l.*, ri.nama AS nama_ri, p.nama_lengkap AS nama_terdampak
|
||||||
|
FROM laporan l
|
||||||
|
LEFT JOIN rumah_ibadah ri ON l.id_rumah_ibadah=ri.id
|
||||||
|
LEFT JOIN penduduk p ON l.nik_terdampak=p.nik
|
||||||
|
WHERE l.id=?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) jsonError('Laporan tidak ditemukan', 404);
|
||||||
|
jsonSuccess($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/laporan — siapa saja bisa lapor */
|
||||||
|
public function store(): void {
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['deskripsi']);
|
||||||
|
if ($errs) jsonError(implode('; ', $errs));
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO laporan
|
||||||
|
(nama_pelapor, no_hp_pelapor, nik_terdampak, id_rumah_ibadah,
|
||||||
|
deskripsi, lat, lng, alamat_laporan, urgensi)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?)"
|
||||||
|
)->execute([
|
||||||
|
sanitize($body['nama_pelapor'] ?? ''),
|
||||||
|
sanitize($body['no_hp_pelapor'] ?? ''),
|
||||||
|
$body['nik_terdampak'] ?? null,
|
||||||
|
$body['id_rumah_ibadah'] ?? null,
|
||||||
|
sanitize($body['deskripsi']),
|
||||||
|
$body['lat'] ?? null,
|
||||||
|
$body['lng'] ?? null,
|
||||||
|
sanitize($body['alamat_laporan'] ?? ''),
|
||||||
|
$body['urgensi'] ?? 'sedang',
|
||||||
|
]);
|
||||||
|
$newId = $db->lastInsertId();
|
||||||
|
jsonSuccess(['id' => $newId], 'Laporan berhasil dikirim. Terima kasih!', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUT /api/laporan/:id — verifikasi / update status */
|
||||||
|
public function update(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$user = currentUser();
|
||||||
|
$body = getBody();
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$db->prepare(
|
||||||
|
"UPDATE laporan SET status=?, catatan_verifikasi=?,
|
||||||
|
id_verifikator=?, tgl_verifikasi=NOW()
|
||||||
|
WHERE id=?"
|
||||||
|
)->execute([
|
||||||
|
$body['status'] ?? 'diverifikasi',
|
||||||
|
sanitize($body['catatan_verifikasi'] ?? ''),
|
||||||
|
$user['id'],
|
||||||
|
$id,
|
||||||
|
]);
|
||||||
|
logActivity('update_laporan', 'laporan', $id, [], $body);
|
||||||
|
jsonSuccess([], 'Status laporan diperbarui');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
getDB()->prepare("DELETE FROM laporan WHERE id=?")->execute([$id]);
|
||||||
|
jsonSuccess([], 'Laporan dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/LogController.php
|
||||||
|
class LogController {
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT l.*, u.nama_lengkap FROM log_aktivitas l
|
||||||
|
LEFT JOIN user u ON l.id_user=u.id
|
||||||
|
ORDER BY l.created_at DESC LIMIT 100"
|
||||||
|
)->fetchAll();
|
||||||
|
jsonSuccess($rows);
|
||||||
|
}
|
||||||
|
public function show(string $id): void { jsonError('N/A',501); }
|
||||||
|
public function store(): void { jsonError('N/A',405); }
|
||||||
|
public function update(string $id): void { jsonError('N/A',405); }
|
||||||
|
public function destroy(string $id): void { jsonError('N/A',405); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/PendudukController.php
|
||||||
|
class PendudukController {
|
||||||
|
|
||||||
|
/** GET /api/penduduk */
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$where = ['p.status_hidup = ?'];
|
||||||
|
$params = ['hidup'];
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
if (!empty($_GET['status_ekonomi'])) {
|
||||||
|
$where[] = 'p.status_ekonomi = ?';
|
||||||
|
$params[] = $_GET['status_ekonomi'];
|
||||||
|
}
|
||||||
|
if (!empty($_GET['kecamatan'])) {
|
||||||
|
$where[] = 'k.kecamatan = ?';
|
||||||
|
$params[] = $_GET['kecamatan'];
|
||||||
|
}
|
||||||
|
if (!empty($_GET['id_keluarga'])) {
|
||||||
|
$where[] = 'p.id_keluarga = ?';
|
||||||
|
$params[] = (int)$_GET['id_keluarga'];
|
||||||
|
}
|
||||||
|
if (!empty($_GET['q'])) {
|
||||||
|
$where[] = '(p.nama_lengkap LIKE ? OR p.nik LIKE ?)';
|
||||||
|
$q = '%' . $_GET['q'] . '%';
|
||||||
|
$params[] = $q; $params[] = $q;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "SELECT p.nik, p.nama_lengkap, p.jenis_kelamin, p.tanggal_lahir,
|
||||||
|
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||||
|
p.status_keluarga, p.status_perkawinan, p.status_hidup,
|
||||||
|
p.agama, p.pekerjaan, p.penghasilan, p.status_ekonomi,
|
||||||
|
p.pendidikan_terakhir, p.status_pendidikan, p.kondisi_kesehatan,
|
||||||
|
p.lat, p.lng, p.id_keluarga, p.updated_at,
|
||||||
|
k.no_kk, k.nama_kepala, k.alamat, k.rt, k.rw, k.kelurahan, k.kecamatan,
|
||||||
|
ri.nama AS nama_ri, ri.jenis AS jenis_ri
|
||||||
|
FROM penduduk p
|
||||||
|
JOIN keluarga k ON p.id_keluarga = k.id
|
||||||
|
LEFT JOIN rumah_ibadah ri ON k.id_rumah_ibadah = ri.id
|
||||||
|
WHERE " . implode(' AND ', $where) . "
|
||||||
|
ORDER BY p.status_ekonomi, p.nama_lengkap";
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
jsonSuccess($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/penduduk/:nik */
|
||||||
|
public function show(string $nik): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT p.*, TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||||
|
k.no_kk, k.nama_kepala, k.alamat, k.rt, k.rw, k.kelurahan, k.kecamatan,
|
||||||
|
ri.nama AS nama_ri, ri.jenis AS jenis_ri
|
||||||
|
FROM penduduk p
|
||||||
|
JOIN keluarga k ON p.id_keluarga = k.id
|
||||||
|
LEFT JOIN rumah_ibadah ri ON k.id_rumah_ibadah = ri.id
|
||||||
|
WHERE p.nik = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$nik]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) jsonError('Penduduk tidak ditemukan', 404);
|
||||||
|
|
||||||
|
// Histori bantuan
|
||||||
|
$stmt2 = $db->prepare(
|
||||||
|
"SELECT hb.*, b.nama_bantuan, b.kategori, ri.nama AS nama_ri
|
||||||
|
FROM histori_bantuan hb
|
||||||
|
JOIN bantuan b ON hb.id_bantuan = b.id
|
||||||
|
LEFT JOIN rumah_ibadah ri ON hb.id_rumah_ibadah = ri.id
|
||||||
|
WHERE hb.nik = ? ORDER BY hb.tanggal DESC"
|
||||||
|
);
|
||||||
|
$stmt2->execute([$nik]);
|
||||||
|
$row['histori_bantuan'] = $stmt2->fetchAll();
|
||||||
|
|
||||||
|
// Riwayat pendidikan
|
||||||
|
$stmt3 = $db->prepare("SELECT * FROM riwayat_pendidikan WHERE nik = ? ORDER BY tahun_masuk DESC");
|
||||||
|
$stmt3->execute([$nik]);
|
||||||
|
$row['riwayat_pendidikan'] = $stmt3->fetchAll();
|
||||||
|
|
||||||
|
// Peristiwa
|
||||||
|
$stmt4 = $db->prepare("SELECT * FROM peristiwa_kependudukan WHERE nik = ? ORDER BY tanggal DESC");
|
||||||
|
$stmt4->execute([$nik]);
|
||||||
|
$row['peristiwa'] = $stmt4->fetchAll();
|
||||||
|
|
||||||
|
jsonSuccess($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/penduduk */
|
||||||
|
public function store(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$body = getBody();
|
||||||
|
|
||||||
|
$errs = required($body, ['nik','id_keluarga','nama_lengkap','jenis_kelamin','tanggal_lahir']);
|
||||||
|
if ($errs) jsonError(implode('; ', $errs));
|
||||||
|
|
||||||
|
// Validasi NIK 16 digit
|
||||||
|
if (!preg_match('/^\d{16}$/', $body['nik'])) jsonError('NIK harus 16 digit angka');
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Cek duplikat
|
||||||
|
$chk = $db->prepare("SELECT nik FROM penduduk WHERE nik = ?");
|
||||||
|
$chk->execute([$body['nik']]);
|
||||||
|
if ($chk->fetch()) jsonError('NIK sudah terdaftar', 409);
|
||||||
|
|
||||||
|
// Hitung umur
|
||||||
|
$tgl = new DateTime($body['tanggal_lahir']);
|
||||||
|
$umur = (new DateTime())->diff($tgl)->y;
|
||||||
|
|
||||||
|
// Validasi pendidikan
|
||||||
|
$warnings = validasiPendidikan(
|
||||||
|
$umur,
|
||||||
|
$body['pendidikan_terakhir'] ?? 'tidak_sekolah',
|
||||||
|
$body['status_pendidikan'] ?? 'tidak_sekolah'
|
||||||
|
);
|
||||||
|
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO penduduk
|
||||||
|
(nik, id_keluarga, nama_lengkap, jenis_kelamin, tanggal_lahir, tempat_lahir,
|
||||||
|
status_keluarga, status_perkawinan, status_hidup, agama, pekerjaan, penghasilan,
|
||||||
|
status_ekonomi, pendidikan_terakhir, status_pendidikan, kondisi_kesehatan,
|
||||||
|
keterangan_kesehatan, lat, lng)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||||
|
)->execute([
|
||||||
|
$body['nik'],
|
||||||
|
(int)$body['id_keluarga'],
|
||||||
|
sanitize($body['nama_lengkap']),
|
||||||
|
$body['jenis_kelamin'],
|
||||||
|
$body['tanggal_lahir'],
|
||||||
|
sanitize($body['tempat_lahir'] ?? ''),
|
||||||
|
$body['status_keluarga'] ?? 'anak',
|
||||||
|
$body['status_perkawinan'] ?? 'belum_kawin',
|
||||||
|
$body['status_hidup'] ?? 'hidup',
|
||||||
|
$body['agama'] ?? null,
|
||||||
|
sanitize($body['pekerjaan'] ?? ''),
|
||||||
|
(int)($body['penghasilan'] ?? 0),
|
||||||
|
$body['status_ekonomi'] ?? 'rentan',
|
||||||
|
$body['pendidikan_terakhir'] ?? 'tidak_sekolah',
|
||||||
|
$body['status_pendidikan'] ?? 'tidak_sekolah',
|
||||||
|
$body['kondisi_kesehatan'] ?? 'sehat',
|
||||||
|
sanitize($body['keterangan_kesehatan'] ?? ''),
|
||||||
|
$body['lat'] ?? null,
|
||||||
|
$body['lng'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
logActivity('create_penduduk', 'penduduk', $body['nik'], [], $body);
|
||||||
|
jsonSuccess(['nik' => $body['nik'], 'warnings' => $warnings], 'Data penduduk berhasil disimpan', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUT /api/penduduk/:nik */
|
||||||
|
public function update(string $nik): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$old = $db->prepare("SELECT * FROM penduduk WHERE nik = ?");
|
||||||
|
$old->execute([$nik]);
|
||||||
|
$existing = $old->fetch();
|
||||||
|
if (!$existing) jsonError('Penduduk tidak ditemukan', 404);
|
||||||
|
|
||||||
|
$body = getBody();
|
||||||
|
|
||||||
|
$tgl = new DateTime($body['tanggal_lahir'] ?? $existing['tanggal_lahir']);
|
||||||
|
$umur = (new DateTime())->diff($tgl)->y;
|
||||||
|
$warnings = validasiPendidikan(
|
||||||
|
$umur,
|
||||||
|
$body['pendidikan_terakhir'] ?? $existing['pendidikan_terakhir'],
|
||||||
|
$body['status_pendidikan'] ?? $existing['status_pendidikan']
|
||||||
|
);
|
||||||
|
|
||||||
|
$db->prepare(
|
||||||
|
"UPDATE penduduk SET
|
||||||
|
nama_lengkap=?, jenis_kelamin=?, tanggal_lahir=?, tempat_lahir=?,
|
||||||
|
status_keluarga=?, status_perkawinan=?, status_hidup=?, agama=?,
|
||||||
|
pekerjaan=?, penghasilan=?, status_ekonomi=?,
|
||||||
|
pendidikan_terakhir=?, status_pendidikan=?,
|
||||||
|
kondisi_kesehatan=?, keterangan_kesehatan=?, lat=?, lng=?
|
||||||
|
WHERE nik=?"
|
||||||
|
)->execute([
|
||||||
|
sanitize($body['nama_lengkap'] ?? $existing['nama_lengkap']),
|
||||||
|
$body['jenis_kelamin'] ?? $existing['jenis_kelamin'],
|
||||||
|
$body['tanggal_lahir'] ?? $existing['tanggal_lahir'],
|
||||||
|
sanitize($body['tempat_lahir'] ?? $existing['tempat_lahir']),
|
||||||
|
$body['status_keluarga'] ?? $existing['status_keluarga'],
|
||||||
|
$body['status_perkawinan'] ?? $existing['status_perkawinan'],
|
||||||
|
$body['status_hidup'] ?? $existing['status_hidup'],
|
||||||
|
$body['agama'] ?? $existing['agama'],
|
||||||
|
sanitize($body['pekerjaan'] ?? $existing['pekerjaan']),
|
||||||
|
(int)($body['penghasilan'] ?? $existing['penghasilan']),
|
||||||
|
$body['status_ekonomi'] ?? $existing['status_ekonomi'],
|
||||||
|
$body['pendidikan_terakhir'] ?? $existing['pendidikan_terakhir'],
|
||||||
|
$body['status_pendidikan'] ?? $existing['status_pendidikan'],
|
||||||
|
$body['kondisi_kesehatan'] ?? $existing['kondisi_kesehatan'],
|
||||||
|
sanitize($body['keterangan_kesehatan'] ?? $existing['keterangan_kesehatan']),
|
||||||
|
$body['lat'] ?? $existing['lat'],
|
||||||
|
$body['lng'] ?? $existing['lng'],
|
||||||
|
$nik,
|
||||||
|
]);
|
||||||
|
|
||||||
|
logActivity('update_penduduk', 'penduduk', $nik, $existing, $body);
|
||||||
|
jsonSuccess(['nik' => $nik, 'warnings' => $warnings], 'Data penduduk berhasil diperbarui');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DELETE /api/penduduk/:nik */
|
||||||
|
public function destroy(string $nik): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("SELECT nik FROM penduduk WHERE nik = ?");
|
||||||
|
$stmt->execute([$nik]);
|
||||||
|
if (!$stmt->fetch()) jsonError('Penduduk tidak ditemukan', 404);
|
||||||
|
|
||||||
|
$db->prepare("DELETE FROM penduduk WHERE nik = ?")->execute([$nik]);
|
||||||
|
logActivity('delete_penduduk', 'penduduk', $nik);
|
||||||
|
jsonSuccess([], 'Data penduduk dihapus');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/penduduk/:nik/validasi-pendidikan */
|
||||||
|
public function validasiPendidikan(string $nik): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT nik, nama_lengkap,
|
||||||
|
TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE()) AS umur,
|
||||||
|
pendidikan_terakhir, status_pendidikan
|
||||||
|
FROM penduduk WHERE nik = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$nik]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) jsonError('Penduduk tidak ditemukan', 404);
|
||||||
|
|
||||||
|
$warnings = validasiPendidikan($row['umur'], $row['pendidikan_terakhir'], $row['status_pendidikan']);
|
||||||
|
jsonSuccess(['penduduk' => $row, 'warnings' => $warnings]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/PeristiwaController.php
|
||||||
|
class PeristiwaController {
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT pk.*, p.nama_lengkap FROM peristiwa_kependudukan pk
|
||||||
|
JOIN penduduk p ON pk.nik=p.nik
|
||||||
|
ORDER BY pk.tanggal DESC LIMIT 100"
|
||||||
|
)->fetchAll();
|
||||||
|
jsonSuccess($rows);
|
||||||
|
}
|
||||||
|
public function show(string $id): void { jsonError('N/A',501); }
|
||||||
|
public function store(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$user = currentUser();
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['nik','jenis','tanggal']);
|
||||||
|
if ($errs) jsonError(implode('; ',$errs));
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO peristiwa_kependudukan (nik,jenis,tanggal,keterangan,id_user)
|
||||||
|
VALUES (?,?,?,?,?)"
|
||||||
|
)->execute([
|
||||||
|
$body['nik'], $body['jenis'], $body['tanggal'],
|
||||||
|
sanitize($body['keterangan'] ?? ''), $user['id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Otomatis update status penduduk
|
||||||
|
switch ($body['jenis']) {
|
||||||
|
case 'kematian':
|
||||||
|
$db->prepare("UPDATE penduduk SET status_hidup='meninggal' WHERE nik=?")->execute([$body['nik']]);
|
||||||
|
break;
|
||||||
|
case 'pernikahan':
|
||||||
|
$db->prepare("UPDATE penduduk SET status_perkawinan='kawin' WHERE nik=?")->execute([$body['nik']]);
|
||||||
|
break;
|
||||||
|
case 'perceraian':
|
||||||
|
$db->prepare("UPDATE penduduk SET status_perkawinan='cerai_hidup' WHERE nik=?")->execute([$body['nik']]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logActivity('peristiwa_'.$body['jenis'], 'penduduk', $body['nik'], [], $body);
|
||||||
|
jsonSuccess(['id' => $db->lastInsertId()], 'Peristiwa dicatat', 201);
|
||||||
|
}
|
||||||
|
public function update(string $id): void { jsonError('N/A',405); }
|
||||||
|
public function destroy(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
getDB()->prepare("DELETE FROM peristiwa_kependudukan WHERE id=?")->execute([$id]);
|
||||||
|
jsonSuccess([],'Peristiwa dihapus');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/PetaController.php
|
||||||
|
// Returns GeoJSON-ready data for Leaflet
|
||||||
|
class PetaController {
|
||||||
|
public function index(): void {
|
||||||
|
$db = getDB();
|
||||||
|
$layer = $_GET['layer'] ?? 'penduduk';
|
||||||
|
|
||||||
|
if ($layer === 'rumah_ibadah') {
|
||||||
|
$rows = $db->query("SELECT id,nama,jenis,lat,lng,radius_meter,alamat FROM rumah_ibadah WHERE aktif=1 AND lat IS NOT NULL")->fetchAll();
|
||||||
|
$features = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$features[] = [
|
||||||
|
'type' => 'Feature',
|
||||||
|
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['lng'],(float)$r['lat']]],
|
||||||
|
'properties' => $r,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
jsonSuccess(['type'=>'FeatureCollection','features'=>$features]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($layer === 'laporan') {
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT id,nama_pelapor,deskripsi,lat,lng,urgensi,status,alamat_laporan FROM laporan
|
||||||
|
WHERE lat IS NOT NULL ORDER BY FIELD(urgensi,'darurat','tinggi','sedang','rendah')"
|
||||||
|
)->fetchAll();
|
||||||
|
$features = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$features[] = [
|
||||||
|
'type' => 'Feature',
|
||||||
|
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['lng'],(float)$r['lat']]],
|
||||||
|
'properties' => $r,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
jsonSuccess(['type'=>'FeatureCollection','features'=>$features]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: penduduk (keluarga sebagai titik)
|
||||||
|
$sql = "SELECT k.id, k.no_kk, k.nama_kepala, k.alamat, k.kecamatan,
|
||||||
|
k.lat, k.lng, k.status_ekonomi,
|
||||||
|
ri.nama AS nama_ri,
|
||||||
|
COUNT(p.nik) AS jumlah_anggota,
|
||||||
|
COUNT(DISTINCT hb.id) AS total_bantuan
|
||||||
|
FROM keluarga k
|
||||||
|
LEFT JOIN rumah_ibadah ri ON k.id_rumah_ibadah=ri.id
|
||||||
|
LEFT JOIN penduduk p ON p.id_keluarga=k.id AND p.status_hidup='hidup'
|
||||||
|
LEFT JOIN histori_bantuan hb ON hb.nik=p.nik
|
||||||
|
WHERE k.lat IS NOT NULL";
|
||||||
|
$params = [];
|
||||||
|
if (!empty($_GET['status_ekonomi'])) {
|
||||||
|
$sql .= " AND k.status_ekonomi=?"; $params[] = $_GET['status_ekonomi'];
|
||||||
|
}
|
||||||
|
$sql .= " GROUP BY k.id";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$features = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$color = match($r['status_ekonomi']) {
|
||||||
|
'miskin' => '#ef4444',
|
||||||
|
'rentan' => '#f59e0b',
|
||||||
|
default => '#22c55e',
|
||||||
|
};
|
||||||
|
$r['color'] = $color;
|
||||||
|
$features[] = [
|
||||||
|
'type' => 'Feature',
|
||||||
|
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['lng'],(float)$r['lat']]],
|
||||||
|
'properties' => $r,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
jsonSuccess(['type'=>'FeatureCollection','features'=>$features]);
|
||||||
|
}
|
||||||
|
public function show(string $id): void { jsonError('Not implemented', 501); }
|
||||||
|
public function store(): void { jsonError('Not implemented', 501); }
|
||||||
|
public function update(string $id): void { jsonError('Not implemented', 501); }
|
||||||
|
public function destroy(string $id): void { jsonError('Not implemented', 501); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/PrioritasController.php
|
||||||
|
class PrioritasController {
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS, ROLE_PIMPINAN]);
|
||||||
|
$db = getDB();
|
||||||
|
$rows = $db->query("SELECT * FROM v_prioritas_bantuan ORDER BY FIELD(level_prioritas,'SANGAT TINGGI','TINGGI','SEDANG','RENDAH'), jumlah_dibantu ASC")->fetchAll();
|
||||||
|
jsonSuccess($rows);
|
||||||
|
}
|
||||||
|
public function show(string $id): void { jsonError('N/A',501); }
|
||||||
|
public function store(): void { jsonError('N/A',501); }
|
||||||
|
public function update(string $id): void { jsonError('N/A',501); }
|
||||||
|
public function destroy(string $id): void { jsonError('N/A',501); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// api/controllers/ChatController.php (in same file for brevity)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Normally in its own file — included separately below
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/RumahIbadahController.php
|
||||||
|
class RumahIbadahController {
|
||||||
|
public function index(): void {
|
||||||
|
$db = getDB();
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT ri.*, u.nama_lengkap AS nama_pengurus,
|
||||||
|
COUNT(DISTINCT k.id) AS jumlah_kk,
|
||||||
|
COUNT(DISTINCT p.nik) AS jumlah_penduduk
|
||||||
|
FROM rumah_ibadah ri
|
||||||
|
LEFT JOIN user u ON ri.id_user=u.id
|
||||||
|
LEFT JOIN keluarga k ON k.id_rumah_ibadah=ri.id
|
||||||
|
LEFT JOIN penduduk p ON p.id_keluarga=k.id AND p.status_hidup='hidup'
|
||||||
|
WHERE ri.aktif=1
|
||||||
|
GROUP BY ri.id ORDER BY ri.jenis, ri.nama"
|
||||||
|
)->fetchAll();
|
||||||
|
jsonSuccess($rows);
|
||||||
|
}
|
||||||
|
public function show(string $id): void {
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("SELECT ri.*, u.nama_lengkap AS nama_pengurus FROM rumah_ibadah ri LEFT JOIN user u ON ri.id_user=u.id WHERE ri.id=?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) jsonError('Rumah ibadah tidak ditemukan', 404);
|
||||||
|
jsonSuccess($row);
|
||||||
|
}
|
||||||
|
public function store(): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['nama','jenis','lat','lng']);
|
||||||
|
if ($errs) jsonError(implode('; ',$errs));
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO rumah_ibadah (id_user,nama,jenis,alamat,lat,lng,radius_meter,kontak,kecamatan,kelurahan)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||||
|
)->execute([
|
||||||
|
$body['id_user'] ?? null,
|
||||||
|
sanitize($body['nama']),
|
||||||
|
$body['jenis'],
|
||||||
|
sanitize($body['alamat'] ?? ''),
|
||||||
|
$body['lat'], $body['lng'],
|
||||||
|
(int)($body['radius_meter'] ?? 300),
|
||||||
|
sanitize($body['kontak'] ?? ''),
|
||||||
|
sanitize($body['kecamatan'] ?? ''),
|
||||||
|
sanitize($body['kelurahan'] ?? ''),
|
||||||
|
]);
|
||||||
|
jsonSuccess(['id' => $db->lastInsertId()], 'Rumah ibadah ditambahkan', 201);
|
||||||
|
}
|
||||||
|
public function update(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN, ROLE_PENGURUS]);
|
||||||
|
$body = getBody();
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare(
|
||||||
|
"UPDATE rumah_ibadah SET nama=?,jenis=?,alamat=?,lat=?,lng=?,radius_meter=?,kontak=?,kecamatan=?,kelurahan=? WHERE id=?"
|
||||||
|
)->execute([
|
||||||
|
sanitize($body['nama'] ?? ''),
|
||||||
|
$body['jenis'] ?? 'Masjid',
|
||||||
|
sanitize($body['alamat'] ?? ''),
|
||||||
|
$body['lat'] ?? 0, $body['lng'] ?? 0,
|
||||||
|
(int)($body['radius_meter'] ?? 300),
|
||||||
|
sanitize($body['kontak'] ?? ''),
|
||||||
|
sanitize($body['kecamatan'] ?? ''),
|
||||||
|
sanitize($body['kelurahan'] ?? ''),
|
||||||
|
$id,
|
||||||
|
]);
|
||||||
|
jsonSuccess([], 'Rumah ibadah diperbarui');
|
||||||
|
}
|
||||||
|
public function destroy(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
getDB()->prepare("UPDATE rumah_ibadah SET aktif=0 WHERE id=?")->execute([$id]);
|
||||||
|
jsonSuccess([], 'Rumah ibadah dinonaktifkan');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
// api/controllers/UserController.php
|
||||||
|
class UserController {
|
||||||
|
public function index(): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
$db = getDB();
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT u.id, u.username, u.nama_lengkap, u.email, u.no_hp,
|
||||||
|
u.aktif, u.last_login, u.created_at, r.nama_role
|
||||||
|
FROM user u JOIN role r ON u.id_role=r.id ORDER BY r.id, u.nama_lengkap"
|
||||||
|
)->fetchAll();
|
||||||
|
jsonSuccess($rows);
|
||||||
|
}
|
||||||
|
public function show(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT u.id, u.username, u.nama_lengkap, u.email, u.no_hp, u.aktif, r.nama_role
|
||||||
|
FROM user u JOIN role r ON u.id_role=r.id WHERE u.id=?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) jsonError('User tidak ditemukan',404);
|
||||||
|
jsonSuccess($row);
|
||||||
|
}
|
||||||
|
public function store(): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
$body = getBody();
|
||||||
|
$errs = required($body, ['username','password','nama_lengkap','id_role']);
|
||||||
|
if ($errs) jsonError(implode('; ',$errs));
|
||||||
|
if (strlen($body['password']) < 6) jsonError('Password minimal 6 karakter');
|
||||||
|
$db = getDB();
|
||||||
|
$chk = $db->prepare("SELECT id FROM user WHERE username=?");
|
||||||
|
$chk->execute([$body['username']]);
|
||||||
|
if ($chk->fetch()) jsonError('Username sudah dipakai',409);
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO user (id_role,username,password_hash,nama_lengkap,email,no_hp)
|
||||||
|
VALUES (?,?,?,?,?,?)"
|
||||||
|
)->execute([
|
||||||
|
(int)$body['id_role'],
|
||||||
|
sanitize($body['username']),
|
||||||
|
password_hash($body['password'], PASSWORD_BCRYPT),
|
||||||
|
sanitize($body['nama_lengkap']),
|
||||||
|
sanitize($body['email'] ?? ''),
|
||||||
|
sanitize($body['no_hp'] ?? ''),
|
||||||
|
]);
|
||||||
|
jsonSuccess(['id' => $db->lastInsertId()], 'User dibuat',201);
|
||||||
|
}
|
||||||
|
public function update(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
$body = getBody();
|
||||||
|
$db = getDB();
|
||||||
|
$sets = ["nama_lengkap=?","email=?","no_hp=?","id_role=?","aktif=?"];
|
||||||
|
$vals = [
|
||||||
|
sanitize($body['nama_lengkap'] ?? ''),
|
||||||
|
sanitize($body['email'] ?? ''),
|
||||||
|
sanitize($body['no_hp'] ?? ''),
|
||||||
|
(int)($body['id_role'] ?? 4),
|
||||||
|
(int)($body['aktif'] ?? 1),
|
||||||
|
];
|
||||||
|
if (!empty($body['password'])) {
|
||||||
|
$sets[] = "password_hash=?";
|
||||||
|
$vals[] = password_hash($body['password'], PASSWORD_BCRYPT);
|
||||||
|
}
|
||||||
|
$vals[] = $id;
|
||||||
|
$db->prepare("UPDATE user SET " . implode(',',$sets) . " WHERE id=?")->execute($vals);
|
||||||
|
jsonSuccess([],'User diperbarui');
|
||||||
|
}
|
||||||
|
public function destroy(string $id): void {
|
||||||
|
requireAuth([ROLE_ADMIN]);
|
||||||
|
getDB()->prepare("UPDATE user SET aktif=0 WHERE id=?")->execute([$id]);
|
||||||
|
jsonSuccess([],'User dinonaktifkan');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
<?php
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config/app.php';
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
// Support 2 cara routing:
|
||||||
|
// 1. ?r=resource/id (dipakai api.js baru)
|
||||||
|
// 2. RewriteRule /api/resource/id (htaccess)
|
||||||
|
if (!empty($_GET['r'])) {
|
||||||
|
$uri = trim($_GET['r'], '/');
|
||||||
|
} else {
|
||||||
|
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||||
|
$uri = preg_replace('#^.*/api(?:/index\.php)?#', '', $uri);
|
||||||
|
$uri = trim($uri, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$segments = explode('/', $uri);
|
||||||
|
$resource = $segments[0] ?? '';
|
||||||
|
$id = $segments[1] ?? null;
|
||||||
|
$action = $segments[2] ?? null;
|
||||||
|
|
||||||
|
// Coba koneksi DB — jika gagal, pakai demo mode
|
||||||
|
$dbOk = false;
|
||||||
|
try {
|
||||||
|
require_once __DIR__ . '/../config/database.php';
|
||||||
|
$testDb = getDB();
|
||||||
|
$testDb->query("SELECT 1");
|
||||||
|
$dbOk = true;
|
||||||
|
require_once __DIR__ . '/../includes/helpers.php';
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// DB tidak tersedia — pakai demo mode
|
||||||
|
$dbOk = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DEMO MODE (tanpa database) ──────────────────────────────
|
||||||
|
if (!$dbOk) {
|
||||||
|
demoRouter($resource, $id, $action, $method);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LIVE MODE ───────────────────────────────────────────────
|
||||||
|
$routes = [
|
||||||
|
'auth' => 'AuthController',
|
||||||
|
'penduduk' => 'PendudukController',
|
||||||
|
'keluarga' => 'KeluargaController',
|
||||||
|
'bantuan' => 'BantuanController',
|
||||||
|
'histori' => 'HistoriController',
|
||||||
|
'laporan' => 'LaporanController',
|
||||||
|
'rumah-ibadah' => 'RumahIbadahController',
|
||||||
|
'user' => 'UserController',
|
||||||
|
'dashboard' => 'DashboardController',
|
||||||
|
'chat' => 'ChatController',
|
||||||
|
'log' => 'LogController',
|
||||||
|
'peta' => 'PetaController',
|
||||||
|
'prioritas' => 'PrioritasController',
|
||||||
|
'peristiwa' => 'PeristiwaController',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isset($routes[$resource])) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['success'=>false,'message'=>"Endpoint '$resource' tidak ditemukan"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cls = $routes[$resource];
|
||||||
|
$file = __DIR__ . "/controllers/{$cls}.php";
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success'=>false,'message'=>"Controller $cls tidak ada"]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
require_once $file;
|
||||||
|
$ctrl = new $cls();
|
||||||
|
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET':
|
||||||
|
if ($id && $action) method_exists($ctrl,$action) ? $ctrl->$action($id) : jsonError("Aksi tidak ada",404);
|
||||||
|
elseif ($id) $ctrl->show($id);
|
||||||
|
else $ctrl->index();
|
||||||
|
break;
|
||||||
|
case 'POST':
|
||||||
|
if ($id && $action) method_exists($ctrl,$action) ? $ctrl->$action($id) : jsonError("Aksi tidak ada",404);
|
||||||
|
else $ctrl->store();
|
||||||
|
break;
|
||||||
|
case 'PUT': $id ? $ctrl->update($id) : jsonError('ID diperlukan',400); break;
|
||||||
|
case 'DELETE': $id ? $ctrl->destroy($id): jsonError('ID diperlukan',400); break;
|
||||||
|
default: jsonError('Method tidak diizinkan',405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonError($msg,$code=400){
|
||||||
|
http_response_code($code);
|
||||||
|
echo json_encode(['success'=>false,'message'=>$msg]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// DEMO ROUTER — berjalan tanpa database sama sekali
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
function demoRouter($resource, $id, $action, $method) {
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
// Session sederhana
|
||||||
|
if (session_status() === PHP_SESSION_NONE) { session_name('webgis_demo'); session_start(); }
|
||||||
|
|
||||||
|
// ── AUTH ──
|
||||||
|
if ($resource === 'auth') {
|
||||||
|
if ($method === 'GET' && $id === 'me') {
|
||||||
|
$u = $_SESSION['demo_user'] ?? null;
|
||||||
|
if ($u) ok($u); else err('Belum login',401);
|
||||||
|
}
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$users = demoUsers();
|
||||||
|
$un = $body['username'] ?? ''; $pw = $body['password'] ?? '';
|
||||||
|
foreach ($users as $u) {
|
||||||
|
if ($u['username'] === $un && $pw === 'admin123') {
|
||||||
|
$_SESSION['demo_user'] = $u;
|
||||||
|
ok($u, 'Login berhasil'); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err('Username atau password salah', 401);
|
||||||
|
}
|
||||||
|
if ($method === 'DELETE') {
|
||||||
|
session_destroy(); ok([], 'Logout berhasil');
|
||||||
|
}
|
||||||
|
err('Not found',404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DASHBOARD ──
|
||||||
|
if ($resource === 'dashboard') {
|
||||||
|
ok([
|
||||||
|
'statistik' => ['total_penduduk'=>14,'total_miskin'=>6,'total_rentan'=>4,'total_mampu'=>2,'total_kk'=>8,'total_ri'=>6,'laporan_pending'=>2,'laporan_darurat'=>1,'sudah_dibantu'=>5,'bantuan_bulan_ini'=>3],
|
||||||
|
'sebaran_kecamatan' => [
|
||||||
|
['kecamatan'=>'Pontianak Timur','jumlah_kk'=>2,'miskin'=>2,'rentan'=>1],
|
||||||
|
['kecamatan'=>'Pontianak Selatan','jumlah_kk'=>3,'miskin'=>2,'rentan'=>2],
|
||||||
|
['kecamatan'=>'Pontianak Kota','jumlah_kk'=>3,'miskin'=>2,'rentan'=>1],
|
||||||
|
],
|
||||||
|
'trend_bantuan' => [
|
||||||
|
['bulan'=>'2025-01','jumlah'=>2],['bulan'=>'2025-02','jumlah'=>1],
|
||||||
|
['bulan'=>'2025-03','jumlah'=>3],['bulan'=>'2025-04','jumlah'=>1],['bulan'=>'2025-05','jumlah'=>2],
|
||||||
|
],
|
||||||
|
'belum_dibantu' => 4,
|
||||||
|
'prioritas_darurat' => [
|
||||||
|
['nik'=>'6171010101010006','nama_lengkap'=>'Maria Magdalena','umur'=>54,'kondisi_kesehatan'=>'sakit_berat','status_ekonomi'=>'miskin','kecamatan'=>'Pontianak Selatan','jumlah_dibantu'=>1,'level_prioritas'=>'SANGAT TINGGI'],
|
||||||
|
['nik'=>'6171010101010014','nama_lengkap'=>'Tan Eng Huat','umur'=>65,'kondisi_kesehatan'=>'disabilitas','status_ekonomi'=>'miskin','kecamatan'=>'Pontianak Kota','jumlah_dibantu'=>1,'level_prioritas'=>'SANGAT TINGGI'],
|
||||||
|
['nik'=>'6171010101010010','nama_lengkap'=>'Lim Ah Kow','umur'=>59,'kondisi_kesehatan'=>'sakit_berat','status_ekonomi'=>'miskin','kecamatan'=>'Pontianak Kota','jumlah_dibantu'=>1,'level_prioritas'=>'TINGGI'],
|
||||||
|
],
|
||||||
|
'beban_per_ri' => [
|
||||||
|
["nama"=>"Masjid Jami'",'jumlah_kk'=>3,'kk_miskin'=>2],
|
||||||
|
['nama'=>'Katedral','jumlah_kk'=>2,'kk_miskin'=>2],
|
||||||
|
['nama'=>'Mujahidin','jumlah_kk'=>1,'kk_miskin'=>1],
|
||||||
|
['nama'=>'Vihara','jumlah_kk'=>2,'kk_miskin'=>2],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PENDUDUK ──
|
||||||
|
if ($resource === 'penduduk') {
|
||||||
|
if ($method === 'GET') {
|
||||||
|
if ($id) ok(demoPendudukDetail($id));
|
||||||
|
else ok(demoPenduduk());
|
||||||
|
}
|
||||||
|
if ($method === 'POST') ok(['nik'=>$body['nik']??''], 'Penduduk ditambahkan (demo)', 201);
|
||||||
|
if ($method === 'PUT') ok([], 'Penduduk diperbarui (demo)');
|
||||||
|
if ($method === 'DELETE') ok([], 'Penduduk dihapus (demo)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── KELUARGA ──
|
||||||
|
if ($resource === 'keluarga') {
|
||||||
|
if ($method === 'GET') {
|
||||||
|
if ($id) ok(['id'=>$id,'no_kk'=>'6171010101010001','nama_kepala'=>'Ahmad Suryana','anggota'=>[]]);
|
||||||
|
else ok(demoKeluarga());
|
||||||
|
}
|
||||||
|
if ($method === 'POST') ok(['id'=>99], 'KK ditambahkan (demo)', 201);
|
||||||
|
if ($method === 'PUT') ok([], 'KK diperbarui (demo)');
|
||||||
|
if ($method === 'DELETE') ok([], 'KK dihapus (demo)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BANTUAN ──
|
||||||
|
if ($resource === 'bantuan') {
|
||||||
|
if ($method === 'GET') ok(demoBantuan());
|
||||||
|
if ($method === 'POST') ok(['id'=>99], 'Bantuan ditambahkan (demo)', 201);
|
||||||
|
if ($method === 'DELETE') ok([], 'Bantuan dihapus (demo)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HISTORI ──
|
||||||
|
if ($resource === 'histori') {
|
||||||
|
if ($method === 'GET') {
|
||||||
|
if ($action === 'cek') ok(['total'=>1,'jenis_bantuan'=>'Sembako Ramadan','terakhir_dibantu'=>'2025-03-15']);
|
||||||
|
else ok(demoHistori());
|
||||||
|
}
|
||||||
|
if ($method === 'POST') ok(['id'=>99], 'Bantuan dicatat (demo)', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LAPORAN ──
|
||||||
|
if ($resource === 'laporan') {
|
||||||
|
if ($method === 'GET') ok(demoLaporan());
|
||||||
|
if ($method === 'POST') ok(['id'=>99], 'Laporan dikirim (demo)', 201);
|
||||||
|
if ($method === 'PUT') ok([], 'Status diperbarui (demo)');
|
||||||
|
if ($method === 'DELETE') ok([], 'Dihapus (demo)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RUMAH IBADAH ──
|
||||||
|
if ($resource === 'rumah-ibadah') {
|
||||||
|
if ($method === 'GET') ok(demoRI());
|
||||||
|
if ($method === 'POST') ok(['id'=>99], 'RI ditambahkan (demo)', 201);
|
||||||
|
if ($method === 'PUT') ok([], 'RI diperbarui (demo)');
|
||||||
|
if ($method === 'DELETE') ok([], 'RI dihapus (demo)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PETA ──
|
||||||
|
if ($resource === 'peta') {
|
||||||
|
$layer = $_GET['layer'] ?? 'penduduk';
|
||||||
|
if ($layer === 'penduduk') {
|
||||||
|
$features = array_map(fn($p) => [
|
||||||
|
'type'=>'Feature',
|
||||||
|
'geometry'=>['type'=>'Point','coordinates'=>[$p['lng'],$p['lat']]],
|
||||||
|
'properties'=>$p
|
||||||
|
], demoPetaPenduduk());
|
||||||
|
ok(['type'=>'FeatureCollection','features'=>$features]);
|
||||||
|
}
|
||||||
|
if ($layer === 'rumah_ibadah') {
|
||||||
|
$features = array_map(fn($r) => [
|
||||||
|
'type'=>'Feature',
|
||||||
|
'geometry'=>['type'=>'Point','coordinates'=>[$r['lng'],$r['lat']]],
|
||||||
|
'properties'=>$r
|
||||||
|
], demoPetaRI());
|
||||||
|
ok(['type'=>'FeatureCollection','features'=>$features]);
|
||||||
|
}
|
||||||
|
if ($layer === 'laporan') {
|
||||||
|
ok(['type'=>'FeatureCollection','features'=>[
|
||||||
|
['type'=>'Feature','geometry'=>['type'=>'Point','coordinates'=>[109.3311,-0.0330]],'properties'=>['deskripsi'=>'Ibu Maria sakit keras','urgensi'=>'darurat','status'=>'diverifikasi','alamat_laporan'=>'Jl. Kemuning No.3']],
|
||||||
|
['type'=>'Feature','geometry'=>['type'=>'Point','coordinates'=>[109.3415,-0.0200]],'properties'=>['deskripsi'=>'Pak Tan penyandang disabilitas','urgensi'=>'tinggi','status'=>'diproses','alamat_laporan'=>'Jl. Gusti Situt No.7']],
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
ok(['type'=>'FeatureCollection','features'=>[]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PRIORITAS ──
|
||||||
|
if ($resource === 'prioritas') {
|
||||||
|
ok([
|
||||||
|
['nik'=>'6171010101010006','nama_lengkap'=>'Maria Magdalena','umur'=>54,'status_ekonomi'=>'miskin','kondisi_kesehatan'=>'sakit_berat','kecamatan'=>'Pontianak Selatan','nama_ri'=>'Katedral','jumlah_dibantu'=>1,'level_prioritas'=>'SANGAT TINGGI'],
|
||||||
|
['nik'=>'6171010101010014','nama_lengkap'=>'Tan Eng Huat','umur'=>65,'status_ekonomi'=>'miskin','kondisi_kesehatan'=>'disabilitas','kecamatan'=>'Pontianak Kota','nama_ri'=>'Klenteng','jumlah_dibantu'=>1,'level_prioritas'=>'SANGAT TINGGI'],
|
||||||
|
['nik'=>'6171010101010010','nama_lengkap'=>'Lim Ah Kow','umur'=>59,'status_ekonomi'=>'miskin','kondisi_kesehatan'=>'sakit_berat','kecamatan'=>'Pontianak Kota','nama_ri'=>'Vihara','jumlah_dibantu'=>1,'level_prioritas'=>'SANGAT TINGGI'],
|
||||||
|
['nik'=>'6171010101010002','nama_lengkap'=>'Siti Rahma','umur'=>39,'status_ekonomi'=>'miskin','kondisi_kesehatan'=>'sakit_ringan','kecamatan'=>'Pontianak Timur','nama_ri'=>"Masjid Jami'",'jumlah_dibantu'=>2,'level_prioritas'=>'TINGGI'],
|
||||||
|
['nik'=>'6171010101010001','nama_lengkap'=>'Ahmad Suryana','umur'=>44,'status_ekonomi'=>'miskin','kondisi_kesehatan'=>'sehat','kecamatan'=>'Pontianak Timur','nama_ri'=>"Masjid Jami'",'jumlah_dibantu'=>2,'level_prioritas'=>'SEDANG'],
|
||||||
|
['nik'=>'6171010101010005','nama_lengkap'=>'Dewi Kartika','umur'=>46,'status_ekonomi'=>'rentan','kondisi_kesehatan'=>'sehat','kecamatan'=>'Pontianak Kota','nama_ri'=>"Masjid Jami'",'jumlah_dibantu'=>0,'level_prioritas'=>'SEDANG'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CHAT ──
|
||||||
|
if ($resource === 'chat') {
|
||||||
|
if ($method === 'GET') {
|
||||||
|
if ($id === 'rooms') ok([['id'=>1,'nama'=>'# Koordinasi Umum'],['id'=>2,'nama'=>'# Bantuan Darurat'],['id'=>3,'nama'=>'# Distribusi Sembako']]);
|
||||||
|
else ok([
|
||||||
|
['id'=>1,'id_user'=>2,'nama_lengkap'=>'Ahmad Fauzi','pesan'=>'Salam. Ada keluarga di Gg. Mawar yang butuh bantuan sembako segera.','created_at'=>date('Y-m-d').' 08:00:00'],
|
||||||
|
['id'=>2,'id_user'=>3,'nama_lengkap'=>'Romo Benediktus','pesan'=>'Baik. Ibu Maria juga perlu bantuan medis darurat. Mari koordinasikan.','created_at'=>date('Y-m-d').' 08:05:00'],
|
||||||
|
['id'=>3,'id_user'=>2,'nama_lengkap'=>'Ahmad Fauzi','pesan'=>'Distribusi Jumat pagi. Tolong update data ke sistem ya.','created_at'=>date('Y-m-d').' 08:10:00'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($method === 'POST') ok(['id'=>99], 'Pesan terkirim (demo)', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LOG ──
|
||||||
|
if ($resource === 'log') {
|
||||||
|
ok([
|
||||||
|
['created_at'=>date('Y-m-d').' 10:32:00','nama_lengkap'=>'Ahmad Fauzi','aksi'=>'create_histori_bantuan','tabel'=>'histori_bantuan','id_record'=>'5','ip_address'=>'192.168.1.10'],
|
||||||
|
['created_at'=>date('Y-m-d').' 09:15:00','nama_lengkap'=>'Romo Benediktus','aksi'=>'update_laporan','tabel'=>'laporan','id_record'=>'1','ip_address'=>'192.168.1.20'],
|
||||||
|
['created_at'=>date('Y-m-d').' 08:00:00','nama_lengkap'=>'Ahmad Fauzi','aksi'=>'login','tabel'=>'user','id_record'=>'2','ip_address'=>'192.168.1.10'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── USER ──
|
||||||
|
if ($resource === 'user') {
|
||||||
|
ok(demoUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
err("Endpoint '$resource' tidak ditemukan", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ok($data, $msg='OK', $code=200) {
|
||||||
|
http_response_code($code);
|
||||||
|
echo json_encode(['success'=>true,'message'=>$msg,'data'=>$data], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
function err($msg, $code=400) {
|
||||||
|
http_response_code($code);
|
||||||
|
echo json_encode(['success'=>false,'message'=>$msg,'data'=>null], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ DEMO DATA FUNCTIONS ═══════════════════════════════════════
|
||||||
|
|
||||||
|
function demoUsers() {
|
||||||
|
return [
|
||||||
|
['id'=>1,'id_role'=>1,'username'=>'admin', 'nama_lengkap'=>'Administrator Sistem', 'email'=>'admin@webgis.id', 'nama_role'=>'admin'],
|
||||||
|
['id'=>2,'id_role'=>2,'username'=>'pengurus_jami', 'nama_lengkap'=>'Ahmad Fauzi (Masjid Jami)', 'email'=>'fauzi@webgis.id', 'nama_role'=>'pengurus'],
|
||||||
|
['id'=>3,'id_role'=>2,'username'=>'pengurus_ktdr', 'nama_lengkap'=>'Romo Benediktus (Katedral)','email'=>'romo@webgis.id', 'nama_role'=>'pengurus'],
|
||||||
|
['id'=>4,'id_role'=>3,'username'=>'pimpinan', 'nama_lengkap'=>'Camat Pontianak Kota', 'email'=>'camat@webgis.id', 'nama_role'=>'pimpinan'],
|
||||||
|
['id'=>5,'id_role'=>4,'username'=>'warga1', 'nama_lengkap'=>'Budi Santoso', 'email'=>'budi@gmail.com', 'nama_role'=>'masyarakat'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoPenduduk() {
|
||||||
|
return [
|
||||||
|
['nik'=>'6171010101010001','id_keluarga'=>1,'nama_lengkap'=>'Ahmad Suryana', 'jenis_kelamin'=>'L','tanggal_lahir'=>'1980-05-15','umur'=>44,'tempat_lahir'=>'Pontianak','status_keluarga'=>'kepala','status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Islam','pekerjaan'=>'Buruh Harian','penghasilan'=>900000,'status_ekonomi'=>'miskin','pendidikan_terakhir'=>'SMP','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sehat','lat'=>-0.027,'lng'=>109.344,'no_kk'=>'6171010101010001','kecamatan'=>'Pontianak Timur','kelurahan'=>'Dalam Bugis','nama_ri'=>"Masjid Jami'"],
|
||||||
|
['nik'=>'6171010101010002','id_keluarga'=>1,'nama_lengkap'=>'Siti Rahma', 'jenis_kelamin'=>'P','tanggal_lahir'=>'1985-08-20','umur'=>39,'tempat_lahir'=>'Pontianak','status_keluarga'=>'istri', 'status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Islam','pekerjaan'=>'Ibu Rumah Tangga','penghasilan'=>0,'status_ekonomi'=>'miskin','pendidikan_terakhir'=>'SD','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sakit_ringan','lat'=>-0.027,'lng'=>109.344,'no_kk'=>'6171010101010001','kecamatan'=>'Pontianak Timur','kelurahan'=>'Dalam Bugis','nama_ri'=>"Masjid Jami'"],
|
||||||
|
['nik'=>'6171010101010003','id_keluarga'=>1,'nama_lengkap'=>'Budi Suryana', 'jenis_kelamin'=>'L','tanggal_lahir'=>'2012-03-10','umur'=>12,'tempat_lahir'=>'Pontianak','status_keluarga'=>'anak', 'status_perkawinan'=>'belum_kawin','status_hidup'=>'hidup','agama'=>'Islam','pekerjaan'=>'Pelajar','penghasilan'=>0,'status_ekonomi'=>'miskin','pendidikan_terakhir'=>'SMP','status_pendidikan'=>'sekolah','kondisi_kesehatan'=>'sehat','lat'=>-0.027,'lng'=>109.344,'no_kk'=>'6171010101010001','kecamatan'=>'Pontianak Timur','kelurahan'=>'Dalam Bugis','nama_ri'=>"Masjid Jami'"],
|
||||||
|
['nik'=>'6171010101010004','id_keluarga'=>2,'nama_lengkap'=>'Samsul Bahri', 'jenis_kelamin'=>'L','tanggal_lahir'=>'1975-11-22','umur'=>49,'tempat_lahir'=>'Sambas','status_keluarga'=>'kepala','status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Islam','pekerjaan'=>'Pedagang Kaki Lima','penghasilan'=>1200000,'status_ekonomi'=>'rentan','pendidikan_terakhir'=>'SMA_SMK','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sehat','lat'=>-0.0315,'lng'=>109.339,'no_kk'=>'6171010101010002','kecamatan'=>'Pontianak Kota','kelurahan'=>'Sungai Bangkong','nama_ri'=>"Masjid Jami'"],
|
||||||
|
['nik'=>'6171010101010005','id_keluarga'=>2,'nama_lengkap'=>'Dewi Kartika', 'jenis_kelamin'=>'P','tanggal_lahir'=>'1978-06-14','umur'=>46,'tempat_lahir'=>'Pontianak','status_keluarga'=>'istri', 'status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Islam','pekerjaan'=>'Buruh Cuci','penghasilan'=>500000,'status_ekonomi'=>'rentan','pendidikan_terakhir'=>'SD','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sehat','lat'=>-0.0315,'lng'=>109.339,'no_kk'=>'6171010101010002','kecamatan'=>'Pontianak Kota','kelurahan'=>'Sungai Bangkong','nama_ri'=>"Masjid Jami'"],
|
||||||
|
['nik'=>'6171010101010006','id_keluarga'=>3,'nama_lengkap'=>'Maria Magdalena', 'jenis_kelamin'=>'P','tanggal_lahir'=>'1970-12-03','umur'=>54,'tempat_lahir'=>'Pontianak','status_keluarga'=>'kepala','status_perkawinan'=>'cerai_mati','status_hidup'=>'hidup','agama'=>'Kristen','pekerjaan'=>'Penjual Gorengan','penghasilan'=>800000,'status_ekonomi'=>'miskin','pendidikan_terakhir'=>'SD','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sakit_berat','lat'=>-0.033,'lng'=>109.331,'no_kk'=>'6171010101010003','kecamatan'=>'Pontianak Selatan','kelurahan'=>'Akcaya','nama_ri'=>'Katedral'],
|
||||||
|
['nik'=>'6171010101010007','id_keluarga'=>3,'nama_lengkap'=>'Petrus Yulius', 'jenis_kelamin'=>'L','tanggal_lahir'=>'2005-04-17','umur'=>19,'tempat_lahir'=>'Pontianak','status_keluarga'=>'anak', 'status_perkawinan'=>'belum_kawin','status_hidup'=>'hidup','agama'=>'Kristen','pekerjaan'=>'Pelajar','penghasilan'=>0,'status_ekonomi'=>'miskin','pendidikan_terakhir'=>'SMA_SMK','status_pendidikan'=>'sekolah','kondisi_kesehatan'=>'sehat','lat'=>-0.033,'lng'=>109.331,'no_kk'=>'6171010101010003','kecamatan'=>'Pontianak Selatan','kelurahan'=>'Akcaya','nama_ri'=>'Katedral'],
|
||||||
|
['nik'=>'6171010101010009','id_keluarga'=>4,'nama_lengkap'=>'Hendra Wijaya', 'jenis_kelamin'=>'L','tanggal_lahir'=>'1988-02-11','umur'=>36,'tempat_lahir'=>'Pontianak','status_keluarga'=>'kepala','status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Buddha','pekerjaan'=>'Ojek Online','penghasilan'=>1500000,'status_ekonomi'=>'rentan','pendidikan_terakhir'=>'SMA_SMK','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sehat','lat'=>-0.049,'lng'=>109.326,'no_kk'=>'6171010101010004','kecamatan'=>'Pontianak Selatan','kelurahan'=>'Parit Tokaya','nama_ri'=>'Mujahidin'],
|
||||||
|
['nik'=>'6171010101010010','id_keluarga'=>5,'nama_lengkap'=>'Lim Ah Kow', 'jenis_kelamin'=>'L','tanggal_lahir'=>'1965-07-30','umur'=>59,'tempat_lahir'=>'Pontianak','status_keluarga'=>'kepala','status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Buddha','pekerjaan'=>'Tidak Bekerja','penghasilan'=>0,'status_ekonomi'=>'miskin','pendidikan_terakhir'=>'SD','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sakit_berat','lat'=>-0.022,'lng'=>109.337,'no_kk'=>'6171010101010005','kecamatan'=>'Pontianak Kota','kelurahan'=>'Darat Sekip','nama_ri'=>'Vihara'],
|
||||||
|
['nik'=>'6171010101010011','id_keluarga'=>5,'nama_lengkap'=>'Tan Siu Lan', 'jenis_kelamin'=>'P','tanggal_lahir'=>'1968-03-19','umur'=>56,'tempat_lahir'=>'Pontianak','status_keluarga'=>'istri', 'status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Buddha','pekerjaan'=>'Penjual Sayur','penghasilan'=>700000,'status_ekonomi'=>'miskin','pendidikan_terakhir'=>'SD','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sehat','lat'=>-0.022,'lng'=>109.337,'no_kk'=>'6171010101010005','kecamatan'=>'Pontianak Kota','kelurahan'=>'Darat Sekip','nama_ri'=>'Vihara'],
|
||||||
|
['nik'=>'6171010101010012','id_keluarga'=>6,'nama_lengkap'=>'Mulyadi', 'jenis_kelamin'=>'L','tanggal_lahir'=>'1979-10-05','umur'=>45,'tempat_lahir'=>'Singkawang','status_keluarga'=>'kepala','status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Islam','pekerjaan'=>'PNS Golongan III','penghasilan'=>4500000,'status_ekonomi'=>'mampu','pendidikan_terakhir'=>'S1','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sehat','lat'=>-0.0265,'lng'=>109.346,'no_kk'=>'6171010101010006','kecamatan'=>'Pontianak Timur','kelurahan'=>'Dalam Bugis','nama_ri'=>"Masjid Jami'"],
|
||||||
|
['nik'=>'6171010101010013','id_keluarga'=>7,'nama_lengkap'=>'Yohanes Kristian', 'jenis_kelamin'=>'L','tanggal_lahir'=>'1982-01-25','umur'=>43,'tempat_lahir'=>'Pontianak','status_keluarga'=>'kepala','status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Katolik','pekerjaan'=>'Sopir Angkot','penghasilan'=>1100000,'status_ekonomi'=>'rentan','pendidikan_terakhir'=>'SMA_SMK','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'sehat','lat'=>-0.034,'lng'=>109.330,'no_kk'=>'6171010101010007','kecamatan'=>'Pontianak Selatan','kelurahan'=>'Akcaya','nama_ri'=>'Katedral'],
|
||||||
|
['nik'=>'6171010101010014','id_keluarga'=>8,'nama_lengkap'=>'Tan Eng Huat', 'jenis_kelamin'=>'L','tanggal_lahir'=>'1960-08-08','umur'=>64,'tempat_lahir'=>'Pontianak','status_keluarga'=>'kepala','status_perkawinan'=>'kawin','status_hidup'=>'hidup','agama'=>'Konghucu','pekerjaan'=>'Pemulung','penghasilan'=>600000,'status_ekonomi'=>'miskin','pendidikan_terakhir'=>'SD','status_pendidikan'=>'lulus','kondisi_kesehatan'=>'disabilitas','lat'=>-0.020,'lng'=>109.4415,'no_kk'=>'6171010101010008','kecamatan'=>'Pontianak Kota','kelurahan'=>'Pontianak Kota','nama_ri'=>'Klenteng'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoPendudukDetail($nik) {
|
||||||
|
$list = demoPenduduk();
|
||||||
|
foreach ($list as $p) { if ($p['nik'] === $nik) {
|
||||||
|
$p['histori_bantuan'] = [['nama_bantuan'=>'Sembako Ramadan','tanggal'=>'2025-03-15','jumlah'=>1,'satuan'=>'paket','nama_ri'=>"Masjid Jami'",'status_penyaluran'=>'disalurkan']];
|
||||||
|
$p['riwayat_pendidikan'] = [];
|
||||||
|
$p['peristiwa'] = [];
|
||||||
|
return $p;
|
||||||
|
}}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoKeluarga() {
|
||||||
|
return [
|
||||||
|
['id'=>1,'no_kk'=>'6171010101010001','id_rumah_ibadah'=>1,'nama_kepala'=>'Ahmad Suryana', 'alamat'=>'Gg. Mawar No.5','rt'=>'002','rw'=>'004','kelurahan'=>'Dalam Bugis', 'kecamatan'=>'Pontianak Timur', 'lat'=>-0.027, 'lng'=>109.344, 'status_ekonomi'=>'miskin','nama_ri'=>"Masjid Jami'",'jumlah_anggota'=>3,'total_penghasilan'=>900000, 'total_bantuan'=>2],
|
||||||
|
['id'=>2,'no_kk'=>'6171010101010002','id_rumah_ibadah'=>1,'nama_kepala'=>'Samsul Bahri', 'alamat'=>'Jl. Veteran No.12','rt'=>'003','rw'=>'002','kelurahan'=>'Sungai Bangkong','kecamatan'=>'Pontianak Kota', 'lat'=>-0.0315,'lng'=>109.339,'status_ekonomi'=>'rentan','nama_ri'=>"Masjid Jami'",'jumlah_anggota'=>2,'total_penghasilan'=>1700000,'total_bantuan'=>0],
|
||||||
|
['id'=>3,'no_kk'=>'6171010101010003','id_rumah_ibadah'=>2,'nama_kepala'=>'Maria Magdalena', 'alamat'=>'Jl. Kemuning No.3','rt'=>'001','rw'=>'001','kelurahan'=>'Akcaya', 'kecamatan'=>'Pontianak Selatan','lat'=>-0.033, 'lng'=>109.331,'status_ekonomi'=>'miskin','nama_ri'=>'Katedral', 'jumlah_anggota'=>3,'total_penghasilan'=>800000, 'total_bantuan'=>1],
|
||||||
|
['id'=>4,'no_kk'=>'6171010101010004','id_rumah_ibadah'=>5,'nama_kepala'=>'Hendra Wijaya', 'alamat'=>'Jl. Ahmad Yani Gg.7','rt'=>'005','rw'=>'003','kelurahan'=>'Parit Tokaya','kecamatan'=>'Pontianak Selatan','lat'=>-0.049, 'lng'=>109.326,'status_ekonomi'=>'rentan','nama_ri'=>'Mujahidin', 'jumlah_anggota'=>2,'total_penghasilan'=>1500000,'total_bantuan'=>0],
|
||||||
|
['id'=>5,'no_kk'=>'6171010101010005','id_rumah_ibadah'=>3,'nama_kepala'=>'Lim Ah Kow', 'alamat'=>'Jl. Diponegoro Gg.2','rt'=>'001','rw'=>'002','kelurahan'=>'Darat Sekip', 'kecamatan'=>'Pontianak Kota', 'lat'=>-0.022, 'lng'=>109.337,'status_ekonomi'=>'miskin','nama_ri'=>'Vihara', 'jumlah_anggota'=>2,'total_penghasilan'=>700000, 'total_bantuan'=>1],
|
||||||
|
['id'=>6,'no_kk'=>'6171010101010006','id_rumah_ibadah'=>1,'nama_kepala'=>'Mulyadi', 'alamat'=>'Gg. Teratai No.8','rt'=>'004','rw'=>'003','kelurahan'=>'Dalam Bugis', 'kecamatan'=>'Pontianak Timur', 'lat'=>-0.0265,'lng'=>109.346,'status_ekonomi'=>'mampu', 'nama_ri'=>"Masjid Jami'",'jumlah_anggota'=>2,'total_penghasilan'=>4500000,'total_bantuan'=>0],
|
||||||
|
['id'=>7,'no_kk'=>'6171010101010007','id_rumah_ibadah'=>2,'nama_kepala'=>'Yohanes Kristian','alamat'=>'Jl. HOS Cokro No.4','rt'=>'002','rw'=>'001','kelurahan'=>'Akcaya', 'kecamatan'=>'Pontianak Selatan','lat'=>-0.034, 'lng'=>109.330,'status_ekonomi'=>'rentan','nama_ri'=>'Katedral', 'jumlah_anggota'=>2,'total_penghasilan'=>1100000,'total_bantuan'=>0],
|
||||||
|
['id'=>8,'no_kk'=>'6171010101010008','id_rumah_ibadah'=>4,'nama_kepala'=>'Tan Eng Huat', 'alamat'=>'Jl. Gusti Situt No.7','rt'=>'003','rw'=>'001','kelurahan'=>'Pontianak Kota','kecamatan'=>'Pontianak Kota','lat'=>-0.020, 'lng'=>109.4415,'status_ekonomi'=>'miskin','nama_ri'=>'Klenteng', 'jumlah_anggota'=>1,'total_penghasilan'=>600000, 'total_bantuan'=>1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoBantuan() {
|
||||||
|
return [
|
||||||
|
['id'=>1,'nama_bantuan'=>'Sembako Ramadan', 'kategori'=>'sembako', 'sumber'=>'Zakat Masjid', 'satuan'=>'paket', 'aktif'=>1],
|
||||||
|
['id'=>2,'nama_bantuan'=>'PKH (Prog. Keluarga Harapan)', 'kategori'=>'ekonomi', 'sumber'=>'APBN', 'satuan'=>'Rp', 'aktif'=>1],
|
||||||
|
['id'=>3,'nama_bantuan'=>'BPNT / Kartu Sembako', 'kategori'=>'sembako', 'sumber'=>'APBN', 'satuan'=>'bulan', 'aktif'=>1],
|
||||||
|
['id'=>4,'nama_bantuan'=>'Beasiswa Pendidikan', 'kategori'=>'pendidikan', 'sumber'=>'CSR Perusahaan', 'satuan'=>'paket/tahun','aktif'=>1],
|
||||||
|
['id'=>5,'nama_bantuan'=>'BPJS Kesehatan PBI', 'kategori'=>'kesehatan', 'sumber'=>'APBD', 'satuan'=>'bulan', 'aktif'=>1],
|
||||||
|
['id'=>6,'nama_bantuan'=>'Bantuan Sembako Gereja', 'kategori'=>'sembako', 'sumber'=>'Kolekte Gereja', 'satuan'=>'paket', 'aktif'=>1],
|
||||||
|
['id'=>7,'nama_bantuan'=>'Bantuan Vihara (Kwan Im)', 'kategori'=>'sembako', 'sumber'=>'Dana Sosial Vihara','satuan'=>'paket', 'aktif'=>1],
|
||||||
|
['id'=>8,'nama_bantuan'=>'Raskin / Beras Miskin', 'kategori'=>'sembako', 'sumber'=>'Bulog', 'satuan'=>'kg', 'aktif'=>1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoHistori() {
|
||||||
|
return [
|
||||||
|
['id'=>1,'nik'=>'6171010101010001','id_bantuan'=>1,'nama_lengkap'=>'Ahmad Suryana', 'nama_bantuan'=>'Sembako Ramadan', 'kategori'=>'sembako','tanggal'=>'2025-03-15','jumlah'=>1,'satuan'=>'paket','nama_ri'=>"Masjid Jami'",'status_penyaluran'=>'disalurkan'],
|
||||||
|
['id'=>2,'nik'=>'6171010101010001','id_bantuan'=>3,'nama_lengkap'=>'Ahmad Suryana', 'nama_bantuan'=>'BPNT', 'kategori'=>'sembako','tanggal'=>'2025-01-10','jumlah'=>1,'satuan'=>'bulan','nama_ri'=>"Masjid Jami'",'status_penyaluran'=>'disalurkan'],
|
||||||
|
['id'=>3,'nik'=>'6171010101010006','id_bantuan'=>6,'nama_lengkap'=>'Maria Magdalena', 'nama_bantuan'=>'Bantuan Sembako Gereja','kategori'=>'sembako','tanggal'=>'2025-02-20','jumlah'=>1,'satuan'=>'paket','nama_ri'=>'Katedral', 'status_penyaluran'=>'disalurkan'],
|
||||||
|
['id'=>4,'nik'=>'6171010101010010','id_bantuan'=>7,'nama_lengkap'=>'Lim Ah Kow', 'nama_bantuan'=>'Bantuan Vihara', 'kategori'=>'sembako','tanggal'=>'2025-04-01','jumlah'=>1,'satuan'=>'paket','nama_ri'=>'Vihara', 'status_penyaluran'=>'disalurkan'],
|
||||||
|
['id'=>5,'nik'=>'6171010101010014','id_bantuan'=>8,'nama_lengkap'=>'Tan Eng Huat', 'nama_bantuan'=>'Raskin', 'kategori'=>'sembako','tanggal'=>'2025-03-05','jumlah'=>10,'satuan'=>'kg', 'nama_ri'=>'Klenteng', 'status_penyaluran'=>'disalurkan'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoLaporan() {
|
||||||
|
return [
|
||||||
|
['id'=>1,'nama_pelapor'=>'Anonim', 'nik_terdampak'=>'6171010101010006','id_rumah_ibadah'=>2,'deskripsi'=>'Ibu Maria sakit keras sudah seminggu, tidak bisa berobat karena tidak ada biaya. Butuh bantuan medis segera.','lat'=>-0.0330,'lng'=>109.3311,'alamat_laporan'=>'Jl. Kemuning No.3, Akcaya','urgensi'=>'darurat','foto'=>null,'status'=>'diverifikasi','nama_ri'=>'Katedral','nama_terdampak'=>'Maria Magdalena','created_at'=>date('Y-m-d').' 08:00:00'],
|
||||||
|
['id'=>2,'nama_pelapor'=>'Eko Prasetyo', 'nik_terdampak'=>null,'id_rumah_ibadah'=>1,'deskripsi'=>'Ada keluarga baru pindah ke Gg. Melati yang sepertinya sangat membutuhkan bantuan. Anak-anak tidak sekolah.','lat'=>-0.0268,'lng'=>109.3448,'alamat_laporan'=>'Gg. Melati, Dalam Bugis','urgensi'=>'sedang','foto'=>null,'status'=>'pending','nama_ri'=>"Masjid Jami'",'nama_terdampak'=>null,'created_at'=>date('Y-m-d').' 10:00:00'],
|
||||||
|
['id'=>3,'nama_pelapor'=>'Anonim', 'nik_terdampak'=>'6171010101010014','id_rumah_ibadah'=>4,'deskripsi'=>'Pak Tan penyandang disabilitas, hidup sebatang kara dan sangat sulit mendapat penghasilan.','lat'=>-0.0200,'lng'=>109.3415,'alamat_laporan'=>'Jl. Gusti Situt No.7','urgensi'=>'tinggi','foto'=>null,'status'=>'diproses','nama_ri'=>'Klenteng Kwan Ti Miau','nama_terdampak'=>'Tan Eng Huat','created_at'=>date('Y-m-d').' 14:00:00'],
|
||||||
|
['id'=>4,'nama_pelapor'=>'Hendra', 'nik_terdampak'=>null,'id_rumah_ibadah'=>5,'deskripsi'=>'Balita 2 tahun kondisi gizi buruk. Orang tua tidak mampu memenuhi kebutuhan gizi anak.','lat'=>-0.0490,'lng'=>109.3260,'alamat_laporan'=>'Gg. Teratai, Parit Tokaya','urgensi'=>'darurat','foto'=>null,'status'=>'pending','nama_ri'=>'Masjid Mujahidin','nama_terdampak'=>null,'created_at'=>date('Y-m-d').' 09:00:00'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoRI() {
|
||||||
|
return [
|
||||||
|
['id'=>1,'id_user'=>2,'nama'=>"Masjid Jami' Pontianak", 'jenis'=>'Masjid', 'alamat'=>'Jl. Tanjungpura, Dalam Bugis', 'lat'=>-0.02596,'lng'=>109.34307,'radius_meter'=>350,'kontak'=>'0561-732001','kecamatan'=>'Pontianak Timur', 'kelurahan'=>'Dalam Bugis', 'aktif'=>1,'nama_pengurus'=>'Ahmad Fauzi','jumlah_kk'=>3,'jumlah_penduduk'=>8],
|
||||||
|
['id'=>2,'id_user'=>3,'nama'=>'Gereja Katedral Pontianak', 'jenis'=>'Gereja', 'alamat'=>'Jl. Patimura No.9, Akcaya', 'lat'=>-0.03200,'lng'=>109.33200,'radius_meter'=>300,'kontak'=>'0561-766000','kecamatan'=>'Pontianak Selatan','kelurahan'=>'Akcaya', 'aktif'=>1,'nama_pengurus'=>'Romo Benediktus','jumlah_kk'=>2,'jumlah_penduduk'=>5],
|
||||||
|
['id'=>3,'id_user'=>null,'nama'=>'Vihara Bodhisattva Karaniya Metta','jenis'=>'Vihara', 'alamat'=>'Jl. Diponegoro, Pontianak Kota','lat'=>-0.02100,'lng'=>109.33800,'radius_meter'=>300,'kontak'=>'0561-733456','kecamatan'=>'Pontianak Kota', 'kelurahan'=>'Darat Sekip', 'aktif'=>1,'nama_pengurus'=>null,'jumlah_kk'=>2,'jumlah_penduduk'=>4],
|
||||||
|
['id'=>4,'id_user'=>null,'nama'=>'Klenteng Kwan Ti Miau', 'jenis'=>'Klenteng','alamat'=>'Jl. Gusti Situt Mahmud', 'lat'=>-0.01900,'lng'=>109.34100,'radius_meter'=>280,'kontak'=>'0561-740123','kecamatan'=>'Pontianak Kota', 'kelurahan'=>'Pontianak Kota', 'aktif'=>1,'nama_pengurus'=>null,'jumlah_kk'=>1,'jumlah_penduduk'=>2],
|
||||||
|
['id'=>5,'id_user'=>2, 'nama'=>'Masjid Mujahidin Pontianak', 'jenis'=>'Masjid', 'alamat'=>'Jl. Ahmad Yani, Parit Tokaya', 'lat'=>-0.04800,'lng'=>109.32500,'radius_meter'=>400,'kontak'=>'0561-764567','kecamatan'=>'Pontianak Selatan','kelurahan'=>'Parit Tokaya', 'aktif'=>1,'nama_pengurus'=>'Ahmad Fauzi','jumlah_kk'=>1,'jumlah_penduduk'=>2],
|
||||||
|
['id'=>6,'id_user'=>3, 'nama'=>'Gereja GPIB Immanuel', 'jenis'=>'Gereja', 'alamat'=>'Jl. Rahadi Usman, Darat Sekip', 'lat'=>-0.02800,'lng'=>109.34500,'radius_meter'=>300,'kontak'=>'0561-735890','kecamatan'=>'Pontianak Kota', 'kelurahan'=>'Darat Sekip', 'aktif'=>1,'nama_pengurus'=>'Romo Benediktus','jumlah_kk'=>1,'jumlah_penduduk'=>1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoPetaPenduduk() {
|
||||||
|
return [
|
||||||
|
['nama_kepala'=>'Ahmad Suryana', 'alamat'=>'Gg. Mawar No.5', 'lat'=>-0.0270,'lng'=>109.3440,'status_ekonomi'=>'miskin','jumlah_anggota'=>3,'no_kk'=>'6171010101010001',"nama_ri"=>"Masjid Jami'",'total_bantuan'=>2,'kecamatan'=>'Pontianak Timur'],
|
||||||
|
['nama_kepala'=>'Samsul Bahri', 'alamat'=>'Jl. Veteran No.12', 'lat'=>-0.0315,'lng'=>109.3390,'status_ekonomi'=>'rentan','jumlah_anggota'=>2,'no_kk'=>'6171010101010002',"nama_ri"=>"Masjid Jami'",'total_bantuan'=>0,'kecamatan'=>'Pontianak Kota'],
|
||||||
|
['nama_kepala'=>'Maria Magdalena', 'alamat'=>'Jl. Kemuning No.3', 'lat'=>-0.0330,'lng'=>109.3310,'status_ekonomi'=>'miskin','jumlah_anggota'=>3,'no_kk'=>'6171010101010003','nama_ri'=>'Katedral','total_bantuan'=>1,'kecamatan'=>'Pontianak Selatan'],
|
||||||
|
['nama_kepala'=>'Hendra Wijaya', 'alamat'=>'Jl. Ahmad Yani Gg.7', 'lat'=>-0.0490,'lng'=>109.3260,'status_ekonomi'=>'rentan','jumlah_anggota'=>2,'no_kk'=>'6171010101010004','nama_ri'=>'Mujahidin','total_bantuan'=>0,'kecamatan'=>'Pontianak Selatan'],
|
||||||
|
['nama_kepala'=>'Lim Ah Kow', 'alamat'=>'Jl. Diponegoro Gg.2', 'lat'=>-0.0220,'lng'=>109.3370,'status_ekonomi'=>'miskin','jumlah_anggota'=>2,'no_kk'=>'6171010101010005','nama_ri'=>'Vihara','total_bantuan'=>1,'kecamatan'=>'Pontianak Kota'],
|
||||||
|
['nama_kepala'=>'Mulyadi', 'alamat'=>'Gg. Teratai No.8', 'lat'=>-0.0265,'lng'=>109.3460,'status_ekonomi'=>'mampu', 'jumlah_anggota'=>2,'no_kk'=>'6171010101010006',"nama_ri"=>"Masjid Jami'",'total_bantuan'=>0,'kecamatan'=>'Pontianak Timur'],
|
||||||
|
['nama_kepala'=>'Yohanes Kristian', 'alamat'=>'Jl. HOS Cokro No.4', 'lat'=>-0.0340,'lng'=>109.3300,'status_ekonomi'=>'rentan','jumlah_anggota'=>2,'no_kk'=>'6171010101010007','nama_ri'=>'Katedral','total_bantuan'=>0,'kecamatan'=>'Pontianak Selatan'],
|
||||||
|
['nama_kepala'=>'Tan Eng Huat', 'alamat'=>'Jl. Gusti Situt No.7', 'lat'=>-0.0200,'lng'=>109.3415,'status_ekonomi'=>'miskin','jumlah_anggota'=>1,'no_kk'=>'6171010101010008','nama_ri'=>'Klenteng','total_bantuan'=>1,'kecamatan'=>'Pontianak Kota'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoPetaRI() {
|
||||||
|
return [
|
||||||
|
["nama"=>"Masjid Jami' Pontianak", 'jenis'=>'Masjid', 'lat'=>-0.02596,'lng'=>109.34307,'radius_meter'=>350,'alamat'=>'Jl. Tanjungpura','kontak'=>'0561-732001'],
|
||||||
|
['nama'=>'Gereja Katedral Pontianak', 'jenis'=>'Gereja', 'lat'=>-0.03200,'lng'=>109.33200,'radius_meter'=>300,'alamat'=>'Jl. Patimura No.9','kontak'=>'0561-766000'],
|
||||||
|
['nama'=>'Vihara Bodhisattva Karaniya Metta', 'jenis'=>'Vihara', 'lat'=>-0.02100,'lng'=>109.33800,'radius_meter'=>300,'alamat'=>'Jl. Diponegoro','kontak'=>'0561-733456'],
|
||||||
|
['nama'=>'Klenteng Kwan Ti Miau', 'jenis'=>'Klenteng','lat'=>-0.01900,'lng'=>109.34100,'radius_meter'=>280,'alamat'=>'Jl. Gusti Situt','kontak'=>'0561-740123'],
|
||||||
|
['nama'=>'Masjid Mujahidin Pontianak', 'jenis'=>'Masjid', 'lat'=>-0.04800,'lng'=>109.32500,'radius_meter'=>400,'alamat'=>'Jl. Ahmad Yani','kontak'=>'0561-764567'],
|
||||||
|
['nama'=>'Gereja GPIB Immanuel', 'jenis'=>'Gereja', 'lat'=>-0.02800,'lng'=>109.34500,'radius_meter'=>300,'alamat'=>'Jl. Rahadi Usman','kontak'=>'0561-735890'],
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #080c14;
|
||||||
|
--bg2: #0d1220;
|
||||||
|
--bg3: #111827;
|
||||||
|
--bg4: #1a2236;
|
||||||
|
--bg5: #1e2a40;
|
||||||
|
--border: rgba(99,120,180,.15);
|
||||||
|
--border2: rgba(99,120,180,.25);
|
||||||
|
--text: #e8edf5;
|
||||||
|
--muted: #6b7a99;
|
||||||
|
--muted2: #8892aa;
|
||||||
|
|
||||||
|
--red: #ff4d6d; --red-bg: rgba(255,77,109,.1);
|
||||||
|
--orange: #ff8c42; --org-bg: rgba(255,140,66,.1);
|
||||||
|
--yellow: #ffd166; --yel-bg: rgba(255,209,102,.1);
|
||||||
|
--green: #06d6a0; --grn-bg: rgba(6,214,160,.1);
|
||||||
|
--blue: #4cc9f0; --blu-bg: rgba(76,201,240,.1);
|
||||||
|
--purple: #b48eff; --pur-bg: rgba(180,142,255,.1);
|
||||||
|
--pink: #ff6eb4; --pnk-bg: rgba(255,110,180,.1);
|
||||||
|
--accent: #4361ee; --acc-bg: rgba(67,97,238,.15);
|
||||||
|
--accent2: #7b5ea7;
|
||||||
|
|
||||||
|
--r4: 4px; --r8: 8px; --r12: 12px; --r16: 16px; --r24: 24px;
|
||||||
|
--sh: 0 4px 24px rgba(0,0,0,.4);
|
||||||
|
--sh2: 0 8px 40px rgba(0,0,0,.6);
|
||||||
|
--hdr: 56px;
|
||||||
|
--sb: 230px;
|
||||||
|
--trs: all .2s cubic-bezier(.4,0,.2,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html,body{height:100%;overflow:hidden;font-family:'Plus Jakarta Sans',sans-serif;font-size:13px;background:var(--bg);color:var(--text);line-height:1.5}
|
||||||
|
::-webkit-scrollbar{width:5px;height:5px}
|
||||||
|
::-webkit-scrollbar-track{background:transparent}
|
||||||
|
::-webkit-scrollbar-thumb{background:var(--bg5);border-radius:3px}
|
||||||
|
|
||||||
|
/* ═══ HEADER ═══ */
|
||||||
|
#app-header{
|
||||||
|
position:fixed;top:0;left:0;right:0;height:var(--hdr);
|
||||||
|
background:rgba(8,12,20,.92);backdrop-filter:blur(20px);
|
||||||
|
border-bottom:1px solid var(--border);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
padding:0 16px;z-index:1000;gap:12px;
|
||||||
|
}
|
||||||
|
.header-left,.header-right{display:flex;align-items:center;gap:10px}
|
||||||
|
.brand{display:flex;align-items:center;gap:10px}
|
||||||
|
.brand-logo{
|
||||||
|
width:36px;height:36px;background:linear-gradient(135deg,var(--accent),var(--accent2));
|
||||||
|
border-radius:10px;display:flex;align-items:center;justify-content:center;
|
||||||
|
font-size:18px;box-shadow:0 0 20px rgba(67,97,238,.3);flex-shrink:0;
|
||||||
|
}
|
||||||
|
.brand-title{font-size:14px;font-weight:800;letter-spacing:-.3px;color:var(--text)}
|
||||||
|
.brand-sub{font-size:10px;color:var(--muted);letter-spacing:.3px}
|
||||||
|
|
||||||
|
.stat-pills{display:flex;gap:6px;flex-wrap:wrap}
|
||||||
|
.spill{
|
||||||
|
display:flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;
|
||||||
|
font-size:11px;font-weight:700;border:1px solid transparent;cursor:default;
|
||||||
|
transition:var(--trs);
|
||||||
|
}
|
||||||
|
.spill.red{background:var(--red-bg);color:var(--red);border-color:rgba(255,77,109,.2)}
|
||||||
|
.spill.yellow{background:var(--yel-bg);color:var(--yellow);border-color:rgba(255,209,102,.2)}
|
||||||
|
.spill.green{background:var(--grn-bg);color:var(--green);border-color:rgba(6,214,160,.2)}
|
||||||
|
.spill.blue{background:var(--blu-bg);color:var(--blue);border-color:rgba(76,201,240,.2)}
|
||||||
|
.spill-dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:pulse 2s infinite}
|
||||||
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||||
|
|
||||||
|
.user-chip{
|
||||||
|
display:flex;align-items:center;gap:8px;padding:5px 12px;
|
||||||
|
background:var(--bg4);border:1px solid var(--border2);
|
||||||
|
border-radius:24px;cursor:pointer;transition:var(--trs);
|
||||||
|
}
|
||||||
|
.user-chip:hover{background:var(--bg5)}
|
||||||
|
.user-avatar{
|
||||||
|
width:26px;height:26px;border-radius:50%;
|
||||||
|
background:linear-gradient(135deg,var(--accent),var(--purple));
|
||||||
|
display:flex;align-items:center;justify-content:center;font-size:12px;
|
||||||
|
}
|
||||||
|
.user-name{font-size:12px;font-weight:600}
|
||||||
|
.role-badge{
|
||||||
|
font-size:9px;font-weight:700;padding:2px 6px;border-radius:8px;letter-spacing:.5px;
|
||||||
|
background:var(--acc-bg);color:var(--blue);border:1px solid rgba(76,201,240,.2);
|
||||||
|
}
|
||||||
|
.icon-btn{
|
||||||
|
width:36px;height:36px;background:var(--bg4);border:1px solid var(--border);
|
||||||
|
border-radius:10px;color:var(--muted);font-size:16px;cursor:pointer;
|
||||||
|
display:flex;align-items:center;justify-content:center;transition:var(--trs);
|
||||||
|
}
|
||||||
|
.icon-btn:hover{background:var(--bg5);color:var(--text);border-color:var(--border2)}
|
||||||
|
|
||||||
|
/* ═══ LAYOUT ═══ */
|
||||||
|
#app-layout{display:flex;height:calc(100vh - var(--hdr));margin-top:var(--hdr)}
|
||||||
|
|
||||||
|
/* ═══ SIDEBAR ═══ */
|
||||||
|
#sidebar{
|
||||||
|
width:var(--sb);background:var(--bg2);border-right:1px solid var(--border);
|
||||||
|
overflow-y:auto;flex-shrink:0;transition:width .25s cubic-bezier(.4,0,.2,1);
|
||||||
|
display:flex;flex-direction:column;
|
||||||
|
}
|
||||||
|
#sidebar.collapsed{width:0;overflow:hidden}
|
||||||
|
.sb-section{padding:8px 0}
|
||||||
|
.sb-label{
|
||||||
|
font-size:10px;font-weight:700;letter-spacing:1px;color:var(--muted);
|
||||||
|
padding:6px 16px 4px;text-transform:uppercase;
|
||||||
|
}
|
||||||
|
.nav-btn{
|
||||||
|
width:100%;display:flex;align-items:center;gap:10px;padding:9px 16px;
|
||||||
|
background:none;border:none;border-left:2px solid transparent;
|
||||||
|
color:var(--muted2);font-size:12.5px;font-weight:500;cursor:pointer;
|
||||||
|
text-align:left;transition:var(--trs);border-radius:0;position:relative;white-space:nowrap;
|
||||||
|
}
|
||||||
|
.nav-btn:hover{background:rgba(255,255,255,.04);color:var(--text);border-left-color:rgba(67,97,238,.4)}
|
||||||
|
.nav-btn.active{background:var(--acc-bg);color:var(--blue);border-left-color:var(--accent);font-weight:600}
|
||||||
|
.nav-icon{width:20px;text-align:center;font-size:15px;flex-shrink:0}
|
||||||
|
.nav-badge{
|
||||||
|
margin-left:auto;background:var(--red);color:#fff;border-radius:10px;
|
||||||
|
padding:1px 6px;font-size:10px;font-weight:700;
|
||||||
|
}
|
||||||
|
.nav-divider{border:none;border-top:1px solid var(--border);margin:4px 0}
|
||||||
|
.nav-kirim{color:var(--orange)!important}
|
||||||
|
.nav-kirim:hover{color:var(--orange)!important;background:var(--org-bg)!important}
|
||||||
|
|
||||||
|
/* ═══ MAIN ═══ */
|
||||||
|
#main-content{flex:1;overflow:hidden;display:flex;flex-direction:column}
|
||||||
|
.panel{display:none;height:100%;overflow-y:auto;padding:20px}
|
||||||
|
.panel.active{display:flex;flex-direction:column}
|
||||||
|
.panel-map.active{display:flex;flex-direction:column;padding:0;overflow:hidden}
|
||||||
|
|
||||||
|
/* ═══ PAGE HEADER ═══ */
|
||||||
|
.page-hdr{
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
flex-wrap:wrap;gap:10px;margin-bottom:20px;padding-bottom:16px;
|
||||||
|
border-bottom:1px solid var(--border);flex-shrink:0;
|
||||||
|
}
|
||||||
|
.page-hdr h2{font-size:18px;font-weight:800;letter-spacing:-.4px;display:flex;align-items:center;gap:8px}
|
||||||
|
.page-hdr h2 .ph-icon{font-size:20px}
|
||||||
|
.page-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||||
|
|
||||||
|
/* ═══ BUTTONS ═══ */
|
||||||
|
.btn{
|
||||||
|
display:inline-flex;align-items:center;gap:6px;padding:8px 16px;
|
||||||
|
border-radius:var(--r8);font-size:12.5px;font-weight:600;cursor:pointer;
|
||||||
|
border:none;transition:var(--trs);letter-spacing:-.1px;white-space:nowrap;
|
||||||
|
}
|
||||||
|
.btn-primary{background:var(--accent);color:#fff}
|
||||||
|
.btn-primary:hover{background:#3451d1;box-shadow:0 0 20px rgba(67,97,238,.3)}
|
||||||
|
.btn-secondary{background:var(--bg4);color:var(--text);border:1px solid var(--border2)}
|
||||||
|
.btn-secondary:hover{background:var(--bg5)}
|
||||||
|
.btn-success{background:var(--grn-bg);color:var(--green);border:1px solid rgba(6,214,160,.2)}
|
||||||
|
.btn-success:hover{background:rgba(6,214,160,.2)}
|
||||||
|
.btn-danger{background:var(--red-bg);color:var(--red);border:1px solid rgba(255,77,109,.2)}
|
||||||
|
.btn-danger:hover{background:rgba(255,77,109,.2)}
|
||||||
|
.btn-warning{background:var(--org-bg);color:var(--orange);border:1px solid rgba(255,140,66,.2)}
|
||||||
|
.btn-warning:hover{background:rgba(255,140,66,.2)}
|
||||||
|
.btn-sm{padding:5px 10px;font-size:11px;border-radius:var(--r4)}
|
||||||
|
.btn-xs{padding:3px 8px;font-size:10px;border-radius:var(--r4)}
|
||||||
|
.btn-icon{width:32px;height:32px;padding:0;justify-content:center;border-radius:var(--r8)}
|
||||||
|
|
||||||
|
/* ═══ CARDS ═══ */
|
||||||
|
.card{
|
||||||
|
background:var(--bg3);border:1px solid var(--border);
|
||||||
|
border-radius:var(--r12);padding:16px;
|
||||||
|
}
|
||||||
|
.card-title{font-size:13px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:6px}
|
||||||
|
|
||||||
|
/* ═══ DASHBOARD STATS ═══ */
|
||||||
|
.stats-grid{
|
||||||
|
display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
|
||||||
|
gap:12px;margin-bottom:20px;flex-shrink:0;
|
||||||
|
}
|
||||||
|
.stat-card{
|
||||||
|
background:var(--bg3);border:1px solid var(--border);border-radius:var(--r12);
|
||||||
|
padding:16px;cursor:pointer;transition:var(--trs);position:relative;overflow:hidden;
|
||||||
|
}
|
||||||
|
.stat-card::before{
|
||||||
|
content:'';position:absolute;top:0;left:0;right:0;height:3px;border-radius:var(--r12) var(--r12) 0 0;
|
||||||
|
}
|
||||||
|
.stat-card:hover{transform:translateY(-2px);box-shadow:var(--sh)}
|
||||||
|
.stat-card.red::before{background:var(--red)}
|
||||||
|
.stat-card.yellow::before{background:var(--yellow)}
|
||||||
|
.stat-card.green::before{background:var(--green)}
|
||||||
|
.stat-card.blue::before{background:var(--blue)}
|
||||||
|
.stat-card.orange::before{background:var(--orange)}
|
||||||
|
.stat-card.purple::before{background:var(--purple)}
|
||||||
|
.stat-num{font-size:30px;font-weight:800;line-height:1.1;font-family:'JetBrains Mono',monospace;margin-bottom:4px}
|
||||||
|
.stat-card.red .stat-num{color:var(--red)}
|
||||||
|
.stat-card.yellow .stat-num{color:var(--yellow)}
|
||||||
|
.stat-card.green .stat-num{color:var(--green)}
|
||||||
|
.stat-card.blue .stat-num{color:var(--blue)}
|
||||||
|
.stat-card.orange .stat-num{color:var(--orange)}
|
||||||
|
.stat-card.purple .stat-num{color:var(--purple)}
|
||||||
|
.stat-label{font-size:11px;color:var(--muted);font-weight:500}
|
||||||
|
.stat-trend{font-size:10px;margin-top:6px;display:flex;align-items:center;gap:3px}
|
||||||
|
.stat-card .stat-bg-icon{
|
||||||
|
position:absolute;right:-4px;bottom:-8px;font-size:54px;opacity:.05;
|
||||||
|
pointer-events:none;line-height:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;flex:1;min-height:0}
|
||||||
|
@media(max-width:900px){.dash-grid{grid-template-columns:1fr}}
|
||||||
|
|
||||||
|
/* ═══ PRIORITY LIST ═══ */
|
||||||
|
.prio-list{display:flex;flex-direction:column;gap:6px;overflow-y:auto;max-height:240px}
|
||||||
|
.prio-item{
|
||||||
|
display:flex;align-items:center;gap:10px;padding:10px 12px;
|
||||||
|
background:var(--bg4);border:1px solid var(--border);border-radius:var(--r8);
|
||||||
|
cursor:pointer;transition:var(--trs);
|
||||||
|
}
|
||||||
|
.prio-item:hover{background:var(--bg5);border-color:var(--border2)}
|
||||||
|
.prio-badge{
|
||||||
|
font-size:9px;font-weight:800;padding:3px 8px;border-radius:20px;
|
||||||
|
letter-spacing:.5px;white-space:nowrap;flex-shrink:0;
|
||||||
|
}
|
||||||
|
.pb-sangat-tinggi{background:rgba(255,77,109,.2);color:var(--red);border:1px solid rgba(255,77,109,.3)}
|
||||||
|
.pb-tinggi{background:var(--org-bg);color:var(--orange);border:1px solid rgba(255,140,66,.3)}
|
||||||
|
.pb-sedang{background:var(--yel-bg);color:var(--yellow);border:1px solid rgba(255,209,102,.3)}
|
||||||
|
.pb-rendah{background:var(--grn-bg);color:var(--green);border:1px solid rgba(6,214,160,.3)}
|
||||||
|
|
||||||
|
/* ═══ BAR CHART ═══ */
|
||||||
|
.bar-list{display:flex;flex-direction:column;gap:8px;overflow-y:auto}
|
||||||
|
.bar-row{display:grid;grid-template-columns:120px 1fr 48px;align-items:center;gap:8px;font-size:11px}
|
||||||
|
.bar-lbl{color:var(--muted2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}
|
||||||
|
.bar-track{height:14px;background:var(--bg4);border-radius:7px;overflow:hidden;position:relative}
|
||||||
|
.bar-fill{height:100%;border-radius:7px;transition:width .6s cubic-bezier(.4,0,.2,1);display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-size:9px;font-weight:700;color:rgba(255,255,255,.8);min-width:4px}
|
||||||
|
.bar-val{font-weight:700;font-family:'JetBrains Mono',monospace;color:var(--text);text-align:right;font-size:11px}
|
||||||
|
|
||||||
|
/* ═══ MAP ═══ */
|
||||||
|
.map-toolbar{
|
||||||
|
display:flex;align-items:center;flex-wrap:wrap;gap:8px;
|
||||||
|
padding:10px 14px;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0;
|
||||||
|
}
|
||||||
|
.map-toolbar h2{font-size:14px;font-weight:700;margin-right:4px}
|
||||||
|
.layer-toggles{display:flex;gap:4px;flex-wrap:wrap}
|
||||||
|
.layer-btn{
|
||||||
|
display:flex;align-items:center;gap:5px;padding:5px 10px;border-radius:20px;
|
||||||
|
border:1px solid var(--border2);background:var(--bg3);color:var(--muted2);
|
||||||
|
font-size:11px;font-weight:600;cursor:pointer;transition:var(--trs);
|
||||||
|
}
|
||||||
|
.layer-btn.active-red{background:var(--red-bg);color:var(--red);border-color:rgba(255,77,109,.3)}
|
||||||
|
.layer-btn.active-yellow{background:var(--yel-bg);color:var(--yellow);border-color:rgba(255,209,102,.3)}
|
||||||
|
.layer-btn.active-green{background:var(--grn-bg);color:var(--green);border-color:rgba(6,214,160,.3)}
|
||||||
|
.layer-btn.active-blue{background:var(--blu-bg);color:var(--blue);border-color:rgba(76,201,240,.3)}
|
||||||
|
.layer-btn.active-purple{background:var(--pur-bg);color:var(--purple);border-color:rgba(180,142,255,.3)}
|
||||||
|
.layer-btn.active-orange{background:var(--org-bg);color:var(--orange);border-color:rgba(255,140,66,.3)}
|
||||||
|
.legend{display:flex;align-items:center;gap:12px;margin-left:auto;font-size:11px;color:var(--muted)}
|
||||||
|
.legend-item{display:flex;align-items:center;gap:4px}
|
||||||
|
.legend-dot{width:8px;height:8px;border-radius:50%}
|
||||||
|
.map-filter-sel{
|
||||||
|
padding:5px 10px;background:var(--bg4);border:1px solid var(--border2);
|
||||||
|
border-radius:var(--r8);color:var(--text);font-size:12px;cursor:pointer;
|
||||||
|
}
|
||||||
|
#map{flex:1;min-height:0}
|
||||||
|
.leaflet-popup-content-wrapper{background:var(--bg3)!important;color:var(--text)!important;border:1px solid var(--border2)!important;border-radius:var(--r12)!important;box-shadow:var(--sh2)!important;padding:0!important}
|
||||||
|
.leaflet-popup-content{margin:0!important;padding:0!important}
|
||||||
|
.leaflet-popup-tip{background:var(--bg3)!important}
|
||||||
|
.leaflet-popup-close-button{color:var(--muted)!important;font-size:18px!important;top:8px!important;right:10px!important}
|
||||||
|
.map-popup{padding:14px;min-width:220px}
|
||||||
|
.mp-title{font-weight:800;font-size:13px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
|
||||||
|
.mp-row{display:flex;justify-content:space-between;align-items:center;padding:3px 0;font-size:11px;color:var(--muted2)}
|
||||||
|
.mp-row strong{color:var(--text);font-weight:600}
|
||||||
|
.mp-divider{border:none;border-top:1px solid var(--border);margin:8px 0}
|
||||||
|
.mp-actions{display:flex;gap:6px;margin-top:8px}
|
||||||
|
|
||||||
|
/* ═══ DATA TABLE ═══ */
|
||||||
|
.table-wrap{overflow-x:auto;border-radius:var(--r12);border:1px solid var(--border);flex:1;min-height:0}
|
||||||
|
.data-tbl{width:100%;border-collapse:collapse;font-size:12px}
|
||||||
|
.data-tbl th{
|
||||||
|
background:var(--bg4);color:var(--muted);font-weight:700;font-size:10.5px;
|
||||||
|
letter-spacing:.5px;text-transform:uppercase;padding:10px 14px;
|
||||||
|
border-bottom:1px solid var(--border);white-space:nowrap;text-align:left;
|
||||||
|
position:sticky;top:0;
|
||||||
|
}
|
||||||
|
.data-tbl td{padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:middle}
|
||||||
|
.data-tbl tr:last-child td{border-bottom:none}
|
||||||
|
.data-tbl tbody tr{transition:background .12s}
|
||||||
|
.data-tbl tbody tr:hover td{background:rgba(255,255,255,.03)}
|
||||||
|
.data-tbl tbody tr.row-miskin td:first-child{border-left:3px solid var(--red)}
|
||||||
|
.data-tbl tbody tr.row-rentan td:first-child{border-left:3px solid var(--yellow)}
|
||||||
|
.data-tbl tbody tr.row-mampu td:first-child{border-left:3px solid var(--green)}
|
||||||
|
.mono{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted2)}
|
||||||
|
|
||||||
|
/* ═══ BADGES ═══ */
|
||||||
|
.bdg{display:inline-flex;align-items:center;gap:3px;padding:3px 9px;border-radius:20px;font-size:10px;font-weight:700;letter-spacing:.3px;white-space:nowrap}
|
||||||
|
.bdg-miskin{background:var(--red-bg);color:var(--red)}
|
||||||
|
.bdg-rentan{background:var(--yel-bg);color:var(--yellow)}
|
||||||
|
.bdg-mampu{background:var(--grn-bg);color:var(--green)}
|
||||||
|
.bdg-sehat{background:var(--grn-bg);color:var(--green)}
|
||||||
|
.bdg-sakit-berat,.bdg-sakit_berat{background:var(--red-bg);color:var(--red)}
|
||||||
|
.bdg-sakit-ringan,.bdg-sakit_ringan{background:var(--yel-bg);color:var(--yellow)}
|
||||||
|
.bdg-disabilitas{background:var(--pur-bg);color:var(--purple)}
|
||||||
|
.bdg-pending{background:var(--yel-bg);color:var(--yellow)}
|
||||||
|
.bdg-diverifikasi{background:var(--blu-bg);color:var(--blue)}
|
||||||
|
.bdg-diproses{background:var(--org-bg);color:var(--orange)}
|
||||||
|
.bdg-selesai{background:var(--grn-bg);color:var(--green)}
|
||||||
|
.bdg-darurat{background:var(--red-bg);color:var(--red)}
|
||||||
|
.bdg-tinggi{background:var(--org-bg);color:var(--orange)}
|
||||||
|
.bdg-sedang{background:var(--yel-bg);color:var(--yellow)}
|
||||||
|
.bdg-rendah{background:var(--grn-bg);color:var(--green)}
|
||||||
|
|
||||||
|
/* ═══ SEARCH & FILTER BAR ═══ */
|
||||||
|
.filter-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex-shrink:0;margin-bottom:14px}
|
||||||
|
.search-wrap{position:relative;flex:1;min-width:180px;max-width:300px}
|
||||||
|
.search-wrap svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--muted);pointer-events:none}
|
||||||
|
.search-input{
|
||||||
|
width:100%;padding:8px 10px 8px 34px;background:var(--bg4);border:1px solid var(--border2);
|
||||||
|
border-radius:var(--r8);color:var(--text);font-size:12px;outline:none;transition:var(--trs);
|
||||||
|
font-family:'Plus Jakarta Sans',sans-serif;
|
||||||
|
}
|
||||||
|
.search-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--acc-bg)}
|
||||||
|
.filter-sel{
|
||||||
|
padding:8px 12px;background:var(--bg4);border:1px solid var(--border2);
|
||||||
|
border-radius:var(--r8);color:var(--text);font-size:12px;cursor:pointer;outline:none;
|
||||||
|
font-family:'Plus Jakarta Sans',sans-serif;transition:var(--trs);
|
||||||
|
}
|
||||||
|
.filter-sel:focus{border-color:var(--accent)}
|
||||||
|
|
||||||
|
/* ═══ MODALS ═══ */
|
||||||
|
.modal-overlay{
|
||||||
|
position:fixed;inset:0;background:rgba(0,0,0,.75);backdrop-filter:blur(8px);
|
||||||
|
display:flex;align-items:center;justify-content:center;z-index:2000;padding:20px;
|
||||||
|
}
|
||||||
|
.modal-overlay.hidden{display:none}
|
||||||
|
.modal{
|
||||||
|
background:var(--bg3);border:1px solid var(--border2);border-radius:var(--r16);
|
||||||
|
width:100%;max-width:520px;max-height:90vh;display:flex;flex-direction:column;
|
||||||
|
box-shadow:var(--sh2);animation:modalIn .2s cubic-bezier(.34,1.56,.64,1);
|
||||||
|
}
|
||||||
|
.modal.modal-lg{max-width:820px}
|
||||||
|
.modal.modal-xl{max-width:980px}
|
||||||
|
@keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}
|
||||||
|
.modal-hdr{
|
||||||
|
padding:16px 20px;border-bottom:1px solid var(--border);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;flex-shrink:0;
|
||||||
|
}
|
||||||
|
.modal-hdr h3{font-size:15px;font-weight:800;display:flex;align-items:center;gap:8px}
|
||||||
|
.modal-body{padding:20px;overflow-y:auto;flex:1}
|
||||||
|
.modal-footer{padding:14px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px;flex-shrink:0}
|
||||||
|
.btn-close-modal{
|
||||||
|
width:30px;height:30px;background:var(--bg5);border:none;border-radius:var(--r8);
|
||||||
|
color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;
|
||||||
|
font-size:16px;transition:var(--trs);
|
||||||
|
}
|
||||||
|
.btn-close-modal:hover{background:var(--red-bg);color:var(--red)}
|
||||||
|
|
||||||
|
/* ═══ FORMS ═══ */
|
||||||
|
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
|
||||||
|
.form-grid.cols3{grid-template-columns:1fr 1fr 1fr}
|
||||||
|
@media(max-width:640px){.form-grid,.form-grid.cols3{grid-template-columns:1fr}}
|
||||||
|
.form-group{margin-bottom:14px}
|
||||||
|
.form-label{display:block;font-size:11px;font-weight:700;color:var(--muted);margin-bottom:5px;letter-spacing:.3px;text-transform:uppercase}
|
||||||
|
.form-input,.form-select,.form-textarea{
|
||||||
|
width:100%;padding:9px 12px;background:var(--bg4);border:1px solid var(--border2);
|
||||||
|
border-radius:var(--r8);color:var(--text);font-size:12.5px;outline:none;
|
||||||
|
transition:var(--trs);font-family:'Plus Jakarta Sans',sans-serif;
|
||||||
|
}
|
||||||
|
.form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--acc-bg)}
|
||||||
|
.form-input.readonly{background:var(--bg5);color:var(--muted);cursor:not-allowed}
|
||||||
|
.form-textarea{resize:vertical;min-height:80px}
|
||||||
|
.form-hint{font-size:10.5px;color:var(--muted);margin-top:3px}
|
||||||
|
.form-divider{grid-column:1/-1;border:none;border-top:1px solid var(--border);margin:6px 0;padding-top:6px}
|
||||||
|
.form-section-title{grid-column:1/-1;font-size:11px;font-weight:700;color:var(--blue);text-transform:uppercase;letter-spacing:.5px;margin-bottom:-6px}
|
||||||
|
.alert-warn{
|
||||||
|
background:var(--yel-bg);border:1px solid rgba(255,209,102,.3);color:var(--yellow);
|
||||||
|
border-radius:var(--r8);padding:10px 12px;font-size:12px;margin-bottom:12px;
|
||||||
|
grid-column:1/-1;
|
||||||
|
}
|
||||||
|
.alert-err{background:var(--red-bg);border:1px solid rgba(255,77,109,.3);color:var(--red);border-radius:var(--r8);padding:10px 12px;font-size:12px}
|
||||||
|
.alert-ok{background:var(--grn-bg);border:1px solid rgba(6,214,160,.3);color:var(--green);border-radius:var(--r8);padding:10px 12px;font-size:12px}
|
||||||
|
|
||||||
|
/* ═══ LAPORAN CARDS ═══ */
|
||||||
|
.laporan-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}
|
||||||
|
.laporan-card{
|
||||||
|
background:var(--bg3);border:1px solid var(--border);border-radius:var(--r12);
|
||||||
|
padding:14px;transition:var(--trs);position:relative;overflow:hidden;
|
||||||
|
}
|
||||||
|
.laporan-card:hover{border-color:var(--border2);transform:translateY(-1px);box-shadow:var(--sh)}
|
||||||
|
.laporan-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
||||||
|
.laporan-card.darurat::before{background:var(--red)}
|
||||||
|
.laporan-card.tinggi::before{background:var(--orange)}
|
||||||
|
.laporan-card.sedang::before{background:var(--yellow)}
|
||||||
|
.laporan-card.rendah::before{background:var(--green)}
|
||||||
|
.lc-hdr{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:8px}
|
||||||
|
.lc-title{font-weight:700;font-size:13px}
|
||||||
|
.lc-desc{font-size:12px;color:var(--muted2);margin-bottom:8px;line-height:1.5}
|
||||||
|
.lc-meta{font-size:11px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap;margin-bottom:10px}
|
||||||
|
.lc-actions{display:flex;gap:6px;flex-wrap:wrap}
|
||||||
|
|
||||||
|
/* ═══ RI CARDS ═══ */
|
||||||
|
.ri-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
|
||||||
|
.ri-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r12);padding:16px;transition:var(--trs)}
|
||||||
|
.ri-card:hover{border-color:var(--border2);box-shadow:var(--sh)}
|
||||||
|
.ri-icon-big{font-size:36px;margin-bottom:10px;line-height:1}
|
||||||
|
.ri-name{font-weight:800;font-size:14px;margin-bottom:4px}
|
||||||
|
.ri-type{font-size:11px;color:var(--muted);margin-bottom:10px}
|
||||||
|
.ri-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}
|
||||||
|
.ri-stat{background:var(--bg4);border-radius:var(--r8);padding:8px;text-align:center}
|
||||||
|
.ri-stat-num{font-size:20px;font-weight:800;font-family:'JetBrains Mono',monospace}
|
||||||
|
.ri-stat-lbl{font-size:10px;color:var(--muted)}
|
||||||
|
|
||||||
|
/* ═══ DETAIL PANEL (slide-in) ═══ */
|
||||||
|
.detail-panel{
|
||||||
|
position:fixed;right:0;top:var(--hdr);bottom:0;width:400px;
|
||||||
|
background:var(--bg2);border-left:1px solid var(--border2);
|
||||||
|
z-index:900;overflow-y:auto;transform:translateX(100%);
|
||||||
|
transition:transform .3s cubic-bezier(.4,0,.2,1);box-shadow:var(--sh2);
|
||||||
|
}
|
||||||
|
.detail-panel.open{transform:translateX(0)}
|
||||||
|
.dp-hdr{
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
padding:16px 20px;border-bottom:1px solid var(--border);position:sticky;top:0;
|
||||||
|
background:var(--bg2);backdrop-filter:blur(10px);z-index:1;
|
||||||
|
}
|
||||||
|
.dp-body{padding:20px}
|
||||||
|
.dp-section{margin-bottom:20px}
|
||||||
|
.dp-section-title{font-size:10px;font-weight:700;color:var(--muted);letter-spacing:1px;text-transform:uppercase;margin-bottom:10px}
|
||||||
|
.dp-field{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px}
|
||||||
|
.dp-field:last-child{border-bottom:none}
|
||||||
|
.dp-field-lbl{color:var(--muted2)}
|
||||||
|
.dp-field-val{font-weight:600;text-align:right;max-width:60%}
|
||||||
|
|
||||||
|
/* ═══ CHAT ═══ */
|
||||||
|
.chat-layout{display:flex;flex-direction:column;flex:1;min-height:0}
|
||||||
|
.chat-rooms{display:flex;gap:6px;padding-bottom:12px;flex-shrink:0;overflow-x:auto}
|
||||||
|
.room-btn{
|
||||||
|
padding:6px 14px;border-radius:20px;border:1px solid var(--border2);
|
||||||
|
background:var(--bg4);color:var(--muted2);font-size:12px;font-weight:600;
|
||||||
|
cursor:pointer;transition:var(--trs);white-space:nowrap;
|
||||||
|
}
|
||||||
|
.room-btn.active{background:var(--acc-bg);color:var(--blue);border-color:rgba(67,97,238,.3)}
|
||||||
|
.chat-messages{
|
||||||
|
flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:10px;
|
||||||
|
padding:16px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r12);margin-bottom:12px;
|
||||||
|
}
|
||||||
|
.chat-bubble{max-width:72%;display:flex;flex-direction:column}
|
||||||
|
.chat-bubble.mine{align-self:flex-end;align-items:flex-end}
|
||||||
|
.chat-bubble.other{align-self:flex-start}
|
||||||
|
.chat-sender{font-size:10px;font-weight:700;color:var(--muted);margin-bottom:3px;padding:0 4px}
|
||||||
|
.chat-text{
|
||||||
|
padding:10px 14px;border-radius:var(--r12);font-size:12.5px;line-height:1.5;
|
||||||
|
word-break:break-word;
|
||||||
|
}
|
||||||
|
.chat-bubble.mine .chat-text{background:var(--accent);color:#fff;border-bottom-right-radius:4px}
|
||||||
|
.chat-bubble.other .chat-text{background:var(--bg4);border-bottom-left-radius:4px}
|
||||||
|
.chat-time{font-size:10px;color:var(--muted);margin-top:3px;padding:0 4px}
|
||||||
|
.chat-input-row{display:flex;gap:8px;flex-shrink:0}
|
||||||
|
.chat-input-row input{flex:1}
|
||||||
|
.chat-empty{text-align:center;color:var(--muted);padding:32px;font-size:13px}
|
||||||
|
|
||||||
|
/* ═══ LOG TABLE ═══ */
|
||||||
|
.log-badge{
|
||||||
|
font-size:10px;font-weight:700;padding:2px 8px;border-radius:4px;
|
||||||
|
background:var(--bg5);color:var(--muted2);font-family:'JetBrains Mono',monospace;
|
||||||
|
}
|
||||||
|
.log-badge.create{background:var(--grn-bg);color:var(--green)}
|
||||||
|
.log-badge.update{background:var(--blu-bg);color:var(--blue)}
|
||||||
|
.log-badge.delete{background:var(--red-bg);color:var(--red)}
|
||||||
|
.log-badge.login{background:var(--pur-bg);color:var(--purple)}
|
||||||
|
|
||||||
|
/* ═══ TOAST ═══ */
|
||||||
|
#toast-container{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none}
|
||||||
|
.toast-item{
|
||||||
|
display:flex;align-items:center;gap:10px;padding:12px 16px;
|
||||||
|
background:var(--bg3);border:1px solid var(--border2);border-radius:var(--r12);
|
||||||
|
min-width:240px;max-width:380px;box-shadow:var(--sh2);pointer-events:auto;
|
||||||
|
animation:toastIn .3s cubic-bezier(.34,1.56,.64,1);font-size:12.5px;
|
||||||
|
}
|
||||||
|
.toast-item.out{animation:toastOut .3s ease forwards}
|
||||||
|
@keyframes toastIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}
|
||||||
|
@keyframes toastOut{to{opacity:0;transform:translateX(40px)}}
|
||||||
|
.toast-icon{font-size:18px;flex-shrink:0}
|
||||||
|
.toast-item.success{border-left:3px solid var(--green)}
|
||||||
|
.toast-item.error{border-left:3px solid var(--red)}
|
||||||
|
.toast-item.info{border-left:3px solid var(--blue)}
|
||||||
|
.toast-item.warning{border-left:3px solid var(--yellow)}
|
||||||
|
|
||||||
|
/* ═══ EMPTY STATE ═══ */
|
||||||
|
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px;color:var(--muted);text-align:center;flex:1}
|
||||||
|
.empty-state .es-icon{font-size:48px;margin-bottom:12px;opacity:.5}
|
||||||
|
.empty-state h3{font-size:15px;font-weight:700;margin-bottom:6px;color:var(--muted2)}
|
||||||
|
.empty-state p{font-size:12px}
|
||||||
|
|
||||||
|
/* ═══ LOADING ═══ */
|
||||||
|
.skeleton{background:linear-gradient(90deg,var(--bg4) 25%,var(--bg5) 50%,var(--bg4) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:var(--r4)}
|
||||||
|
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
||||||
|
.loading-row td{padding:10px 14px}.loading-cell{height:14px;border-radius:4px}
|
||||||
|
|
||||||
|
/* ═══ MISC ═══ */
|
||||||
|
.hidden{display:none!important}
|
||||||
|
.info-box{background:var(--bg4);border:1px solid var(--border2);border-left:3px solid var(--blue);border-radius:var(--r8);padding:10px 14px;font-size:12px;color:var(--muted2);margin-bottom:12px}
|
||||||
|
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||||||
|
@media(max-width:800px){.two-col{grid-template-columns:1fr}}
|
||||||
|
.scrollable{overflow-y:auto;flex:1;min-height:0}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const Api = (() => {
|
||||||
|
// Deteksi path API secara otomatis
|
||||||
|
const BASE = (() => {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const dir = path.substring(0, path.lastIndexOf('/') + 1);
|
||||||
|
return dir + 'api/index.php';
|
||||||
|
})();
|
||||||
|
|
||||||
|
async function req(method, path, body=null) {
|
||||||
|
// path contoh: '/penduduk' → '?r=penduduk' | '/penduduk/123' → '?r=penduduk/123'
|
||||||
|
const cleanPath = path.replace(/^\//, '');
|
||||||
|
const url = `${BASE}?r=${cleanPath}`;
|
||||||
|
const opts = {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'same-origin',
|
||||||
|
};
|
||||||
|
if (body) opts.body = JSON.stringify(body);
|
||||||
|
|
||||||
|
const res = await fetch(url, opts);
|
||||||
|
const text = await res.text();
|
||||||
|
let json;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
// PHP mengembalikan HTML/teks error bukan JSON
|
||||||
|
const preview = text.substring(0, 200);
|
||||||
|
throw new Error(`Server error ${res.status} — bukan JSON: ${preview}`);
|
||||||
|
}
|
||||||
|
if (!json.success) throw new Error(json.message || 'Terjadi kesalahan');
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: path => req('GET', path),
|
||||||
|
post: (path, b) => req('POST', path, b),
|
||||||
|
put: (path, b) => req('PUT', path, b),
|
||||||
|
delete: path => req('DELETE', path),
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
/* ════════════════════════════════════════
|
||||||
|
GLOBAL HELPERS
|
||||||
|
════════════════════════════════════════ */
|
||||||
|
const ge = id => document.getElementById(id);
|
||||||
|
const gv = id => (document.getElementById(id)?.value ?? '').trim();
|
||||||
|
const sv = (id, val) => { const el = document.getElementById(id); if (el) el.value = val ?? ''; };
|
||||||
|
const rp = n => n > 0 ? 'Rp ' + parseInt(n).toLocaleString('id-ID') : '—';
|
||||||
|
const cap = s => s ? s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ') : '—';
|
||||||
|
const bdg = (val, _cls) => {
|
||||||
|
const v = val || '';
|
||||||
|
const css = v.replace(/_/g,'-').toLowerCase();
|
||||||
|
return `<span class="bdg bdg-${css}">${v.replace(/_/g,' ').toUpperCase()}</span>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════
|
||||||
|
ROLE CONFIG
|
||||||
|
1 = Admin, 2 = Pengurus RI, 3 = Pimpinan, 4 = Masyarakat
|
||||||
|
════════════════════════════════════════ */
|
||||||
|
const ROLE_MENUS = {
|
||||||
|
1: ['dashboard','peta','penduduk','keluarga','ri','bantuan','prioritas','laporan','chat','log'],
|
||||||
|
2: ['dashboard','peta','penduduk','keluarga','ri','bantuan','prioritas','laporan','chat'],
|
||||||
|
3: ['dashboard','peta','penduduk','keluarga','laporan'],
|
||||||
|
4: ['dashboard','peta'],
|
||||||
|
99: ['dashboard','peta'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_LABELS = {
|
||||||
|
1: { icon:'⚙️', label:'Administrator', color:'#e74c3c' },
|
||||||
|
2: { icon:'🕌', label:'Pengurus Rumah Ibadah', color:'#27ae60' },
|
||||||
|
3: { icon:'🏛️', label:'Pimpinan Daerah', color:'#2980b9' },
|
||||||
|
4: { icon:'👤', label:'Masyarakat', color:'#8e44ad' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════
|
||||||
|
APP CONTROLLER
|
||||||
|
════════════════════════════════════════ */
|
||||||
|
const App = (() => {
|
||||||
|
let _user = null, _autoTimer = null;
|
||||||
|
|
||||||
|
const currentUser = () => _user;
|
||||||
|
const userRole = () => _user?.id_role ?? 99;
|
||||||
|
const canEdit = () => [1,2].includes(userRole());
|
||||||
|
|
||||||
|
/* ── Login ── */
|
||||||
|
async function doLogin() {
|
||||||
|
const username = gv('login-user'), password = gv('login-pass');
|
||||||
|
const errEl = ge('login-err');
|
||||||
|
const btnMasuk = ge('btn-masuk-submit');
|
||||||
|
errEl.classList.add('hidden');
|
||||||
|
if (btnMasuk) { btnMasuk.disabled = true; btnMasuk.textContent = 'Memproses...'; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('api/index.php?r=auth', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
|
||||||
|
let json;
|
||||||
|
const text = await res.text();
|
||||||
|
try { json = JSON.parse(text); }
|
||||||
|
catch { throw new Error(`Respons server tidak valid (bukan JSON). Cek error PHP di server.`); }
|
||||||
|
|
||||||
|
if (!json.success) throw new Error(json.message || 'Login gagal');
|
||||||
|
|
||||||
|
_user = json.data;
|
||||||
|
setUserUI(_user);
|
||||||
|
closeModal('modal-login');
|
||||||
|
ge('login-pass').value = '';
|
||||||
|
toast(`Selamat datang, ${_user.nama_lengkap}! 👋`, 'success');
|
||||||
|
|
||||||
|
// Arahkan ke panel default sesuai role
|
||||||
|
const defaultPanel = { 1:'dashboard', 2:'dashboard', 3:'dashboard', 4:'dashboard' };
|
||||||
|
switchPanel(defaultPanel[_user.id_role] || 'dashboard');
|
||||||
|
startAutoRefresh();
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
errEl.textContent = e.message || 'Username atau password salah';
|
||||||
|
errEl.classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
if (btnMasuk) { btnMasuk.disabled = false; btnMasuk.textContent = 'Masuk →'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await fetch('api/index.php?r=auth/logout', { method:'DELETE', credentials:'same-origin' });
|
||||||
|
} catch {}
|
||||||
|
stopAutoRefresh();
|
||||||
|
_user = null;
|
||||||
|
setUserUI(null);
|
||||||
|
toast('Anda telah logout', 'info');
|
||||||
|
switchPanel('dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Apply UI per role ── */
|
||||||
|
function setUserUI(u) {
|
||||||
|
if (u) {
|
||||||
|
const info = ROLE_LABELS[u.id_role] || { icon:'👤', label: u.nama_role, color:'#555' };
|
||||||
|
ge('user-name-el').textContent = u.nama_lengkap;
|
||||||
|
ge('user-avatar-el').textContent = info.icon;
|
||||||
|
ge('user-role-el').textContent = info.label.toUpperCase();
|
||||||
|
ge('user-info').classList.remove('hidden');
|
||||||
|
ge('btn-login').classList.add('hidden');
|
||||||
|
applyRoleUI(u.id_role);
|
||||||
|
|
||||||
|
// Tampilkan role banner
|
||||||
|
showRoleBanner(u.id_role, u.nama_lengkap, info);
|
||||||
|
} else {
|
||||||
|
ge('user-info').classList.add('hidden');
|
||||||
|
ge('btn-login').classList.remove('hidden');
|
||||||
|
applyRoleUI(99);
|
||||||
|
hideRoleBanner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRoleBanner(role, nama, info) {
|
||||||
|
let banner = ge('role-banner');
|
||||||
|
if (!banner) {
|
||||||
|
banner = document.createElement('div');
|
||||||
|
banner.id = 'role-banner';
|
||||||
|
banner.style.cssText = `
|
||||||
|
position:fixed; top:0; left:0; right:0; z-index:9999;
|
||||||
|
padding:6px 16px; font-size:12px; font-weight:600;
|
||||||
|
display:flex; align-items:center; gap:8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
`;
|
||||||
|
document.body.prepend(banner);
|
||||||
|
}
|
||||||
|
banner.style.background = info.color;
|
||||||
|
banner.style.color = '#fff';
|
||||||
|
banner.innerHTML = `
|
||||||
|
${info.icon} Login sebagai: <strong>${nama}</strong>
|
||||||
|
| Role: <strong>${info.label}</strong>
|
||||||
|
| <span style="opacity:0.8;font-size:11px">${roleDescription(role)}</span>
|
||||||
|
`;
|
||||||
|
// Geser konten ke bawah sedikit
|
||||||
|
document.body.style.paddingTop = '30px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideRoleBanner() {
|
||||||
|
const banner = ge('role-banner');
|
||||||
|
if (banner) banner.remove();
|
||||||
|
document.body.style.paddingTop = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function roleDescription(role) {
|
||||||
|
return {
|
||||||
|
1: 'Akses penuh — kelola semua data & pengguna',
|
||||||
|
2: 'Kelola penduduk, bantuan & verifikasi laporan',
|
||||||
|
3: 'Read-only — monitoring & dashboard',
|
||||||
|
4: 'Hanya bisa melihat peta & kirim laporan',
|
||||||
|
}[role] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRoleUI(role) {
|
||||||
|
const allowed = ROLE_MENUS[role] || ROLE_MENUS[99];
|
||||||
|
|
||||||
|
// Sembunyikan/tampilkan nav menu
|
||||||
|
document.querySelectorAll('.nav-btn[data-panel]').forEach(btn => {
|
||||||
|
const panel = btn.dataset.panel;
|
||||||
|
btn.style.display = allowed.includes(panel) ? '' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sembunyikan tombol aksi edit/tambah/salur untuk non-editor
|
||||||
|
const canEditRole = [1,2].includes(role);
|
||||||
|
document.querySelectorAll('[id^="btn-tambah"],[id^="btn-salur"],[id^="btn-edit"]')
|
||||||
|
.forEach(el => el.style.display = canEditRole ? '' : 'none');
|
||||||
|
|
||||||
|
// Khusus Admin: tampilkan menu log
|
||||||
|
const logBtn = document.querySelector('[data-panel="log"]');
|
||||||
|
if (logBtn) logBtn.style.display = role === 1 ? '' : 'none';
|
||||||
|
|
||||||
|
// Kirim Laporan hanya untuk masyarakat & umum
|
||||||
|
const kirimBtn = ge('nav-kirim-laporan');
|
||||||
|
if (kirimBtn) kirimBtn.style.display = [4,99].includes(role) ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Panel routing ── */
|
||||||
|
let _cur = 'dashboard';
|
||||||
|
|
||||||
|
function switchPanel(name) {
|
||||||
|
const role = userRole();
|
||||||
|
const allowed = ROLE_MENUS[role] || ROLE_MENUS[99];
|
||||||
|
|
||||||
|
// Jika panel tidak boleh diakses role ini, redirect ke dashboard
|
||||||
|
if (!allowed.includes(name)) {
|
||||||
|
toast('Anda tidak punya akses ke halaman ini', 'error');
|
||||||
|
name = 'dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
ge(`panel-${name}`)?.classList.add('active');
|
||||||
|
document.querySelector(`[data-panel="${name}"]`)?.classList.add('active');
|
||||||
|
_cur = name;
|
||||||
|
|
||||||
|
if (name === 'peta') setTimeout(() => MapApp.init(), 80);
|
||||||
|
else loadPanel(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCurrentPanel() { loadPanel(_cur); }
|
||||||
|
|
||||||
|
function loadPanel(n) {
|
||||||
|
({ dashboard: Dashboard.load, penduduk: Penduduk.load, keluarga: Keluarga.load,
|
||||||
|
bantuan: Bantuan.load, laporan: LaporanPanel.load, ri: RI.load,
|
||||||
|
prioritas: PrioritasPanel.load, chat: Chat.init, log: LogPanel.load })[n]?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auto refresh ── */
|
||||||
|
function startAutoRefresh() {
|
||||||
|
stopAutoRefresh();
|
||||||
|
_autoTimer = setInterval(() => {
|
||||||
|
if (_cur === 'dashboard') Dashboard.load();
|
||||||
|
if (_cur === 'laporan') LaporanPanel.load();
|
||||||
|
updateHeaderStats();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
function stopAutoRefresh() { clearInterval(_autoTimer); }
|
||||||
|
|
||||||
|
/* ── Modals ── */
|
||||||
|
function showModal(id) { ge(id)?.classList.remove('hidden'); }
|
||||||
|
function closeModal(id) { ge(id)?.classList.add('hidden'); }
|
||||||
|
|
||||||
|
/* ── Toast ── */
|
||||||
|
function toast(msg, type='info', dur=3800) {
|
||||||
|
const icons = { success:'✅', error:'❌', info:'ℹ️', warning:'⚠️' };
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = `toast-item ${type}`;
|
||||||
|
el.innerHTML = `<span class="toast-icon">${icons[type]||'ℹ️'}</span><span>${msg}</span>`;
|
||||||
|
ge('toast-container').appendChild(el);
|
||||||
|
setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 300); }, dur);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header stats ── */
|
||||||
|
async function updateHeaderStats() {
|
||||||
|
try {
|
||||||
|
const d = await Api.get('/dashboard');
|
||||||
|
const s = d.statistik;
|
||||||
|
ge('hp-miskin-n').textContent = s.total_miskin;
|
||||||
|
ge('hp-rentan-n').textContent = s.total_rentan;
|
||||||
|
ge('hp-mampu-n').textContent = s.total_mampu;
|
||||||
|
ge('hp-lap-n').textContent = s.laporan_pending;
|
||||||
|
const nb = ge('nb-laporan');
|
||||||
|
s.laporan_pending > 0
|
||||||
|
? (nb.textContent = s.laporan_pending, nb.classList.remove('hidden'))
|
||||||
|
: nb.classList.add('hidden');
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Events ── */
|
||||||
|
ge('btn-toggle-sidebar').addEventListener('click', () => {
|
||||||
|
ge('sidebar').classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if (e.target.classList.contains('modal-overlay')) e.target.classList.add('hidden');
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape')
|
||||||
|
document.querySelectorAll('.modal-overlay:not(.hidden)').forEach(m => m.classList.add('hidden'));
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── Init ── */
|
||||||
|
(async function init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('api/index.php?r=auth/me', { credentials:'same-origin' });
|
||||||
|
const text = await res.text();
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if (json.success) { _user = json.data; setUserUI(_user); startAutoRefresh(); }
|
||||||
|
} catch {}
|
||||||
|
Dashboard.load();
|
||||||
|
updateHeaderStats();
|
||||||
|
})();
|
||||||
|
|
||||||
|
return { currentUser, userRole, canEdit, doLogin, logout, switchPanel,
|
||||||
|
showModal, closeModal, toast, updateHeaderStats };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
const Bantuan = (() => {
|
||||||
|
let jenisList = [];
|
||||||
|
|
||||||
|
const DEMO_JENIS = [
|
||||||
|
{ id:1, nama_bantuan:'Sembako Ramadan', kategori:'sembako', sumber:'Zakat Masjid', satuan:'paket', aktif:1 },
|
||||||
|
{ id:2, nama_bantuan:'PKH (Prog. Keluarga Harapan)', kategori:'ekonomi', sumber:'APBN', satuan:'Rp', aktif:1 },
|
||||||
|
{ id:3, nama_bantuan:'BPNT / Kartu Sembako', kategori:'sembako', sumber:'APBN', satuan:'bulan', aktif:1 },
|
||||||
|
{ id:4, nama_bantuan:'Beasiswa Pendidikan', kategori:'pendidikan', sumber:'CSR Perusahaan', satuan:'paket/tahun',aktif:1 },
|
||||||
|
{ id:5, nama_bantuan:'BPJS Kesehatan PBI', kategori:'kesehatan', sumber:'APBD', satuan:'bulan', aktif:1 },
|
||||||
|
{ id:6, nama_bantuan:'Bantuan Sembako Gereja', kategori:'sembako', sumber:'Kolekte Gereja', satuan:'paket', aktif:1 },
|
||||||
|
{ id:7, nama_bantuan:'Bantuan Vihara (Kwan Im)',kategori:'sembako', sumber:'Dana Sosial Vihara',satuan:'paket', aktif:1 },
|
||||||
|
{ id:8, nama_bantuan:'Raskin / Beras Miskin', kategori:'sembako', sumber:'Bulog', satuan:'kg', aktif:1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEMO_HISTORI = [
|
||||||
|
{ id:1, nama_lengkap:'Ahmad Suryana', nama_bantuan:'Sembako Ramadan', tanggal:'2025-03-15', jumlah:1, satuan:'paket', nama_ri:"Masjid Jami'", status_penyaluran:'disalurkan' },
|
||||||
|
{ id:2, nama_lengkap:'Ahmad Suryana', nama_bantuan:'BPNT', tanggal:'2025-01-10', jumlah:1, satuan:'bulan', nama_ri:"Masjid Jami'", status_penyaluran:'disalurkan' },
|
||||||
|
{ id:3, nama_lengkap:'Maria Magdalena', nama_bantuan:'Bantuan Sembako Gereja', tanggal:'2025-02-20', jumlah:1, satuan:'paket', nama_ri:'Katedral', status_penyaluran:'disalurkan' },
|
||||||
|
{ id:4, nama_lengkap:'Lim Ah Kow', nama_bantuan:'Bantuan Vihara', tanggal:'2025-04-01', jumlah:1, satuan:'paket', nama_ri:'Vihara', status_penyaluran:'disalurkan' },
|
||||||
|
{ id:5, nama_lengkap:'Tan Eng Huat', nama_bantuan:'Raskin', tanggal:'2025-03-05', jumlah:10, satuan:'kg', nama_ri:'Klenteng', status_penyaluran:'disalurkan' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const KAT_COLOR = { sembako:'#06d6a0', ekonomi:'#4361ee', pendidikan:'#4cc9f0', kesehatan:'#ffd166', lainnya:'#6b7a99' };
|
||||||
|
const KAT_ICON = { sembako:'🛒', ekonomi:'💰', pendidikan:'📚', kesehatan:'🏥', lainnya:'📦' };
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try { jenisList = await Api.get('/bantuan'); } catch { jenisList = DEMO_JENIS; }
|
||||||
|
renderJenis();
|
||||||
|
loadHistori();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJenis() {
|
||||||
|
const el = ge('bantuan-jenis-list');
|
||||||
|
el.innerHTML = jenisList.map(b => `
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg4);border:1px solid var(--border);border-radius:8px;margin-bottom:6px;border-left:3px solid ${KAT_COLOR[b.kategori]||'#888'}">
|
||||||
|
<span style="font-size:20px">${KAT_ICON[b.kategori]||'📦'}</span>
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="font-weight:700;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${b.nama_bantuan}</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted)">${b.kategori} · ${b.sumber||'—'} · per ${b.satuan||'unit'}</div>
|
||||||
|
</div>
|
||||||
|
${App.canEdit() ? `<button class="btn btn-danger btn-xs" onclick="Bantuan.hapusJenis(${b.id})">🗑</button>` : ''}
|
||||||
|
</div>`).join('') || '<p style="color:var(--muted);font-size:12px;padding:12px">Belum ada data bantuan</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHistori() {
|
||||||
|
let data;
|
||||||
|
try { data = await Api.get('/histori?status=disalurkan'); } catch { data = DEMO_HISTORI; }
|
||||||
|
ge('bantuan-histori-list').innerHTML = data.map(h => `
|
||||||
|
<div style="padding:10px 12px;background:var(--bg4);border:1px solid var(--border);border-radius:8px;margin-bottom:6px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:flex-start">
|
||||||
|
<strong style="font-size:12.5px">${h.nama_lengkap}</strong>
|
||||||
|
<span class="bdg bdg-${h.status_penyaluran||'disalurkan'}" style="font-size:9px">${h.status_penyaluran}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted2);margin-top:3px">${h.nama_bantuan} · ${h.jumlah} ${h.satuan||''}</div>
|
||||||
|
<div style="font-size:10px;color:var(--muted);margin-top:2px">🕌 ${h.nama_ri||'—'} · 📅 ${h.tanggal?.substring(0,10)||'—'}</div>
|
||||||
|
</div>`).join('') || '<p style="color:var(--muted);font-size:12px;padding:12px">Belum ada riwayat</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFormJenis() {
|
||||||
|
['fjb-nama','fjb-satuan','fjb-sumber','fjb-deskripsi'].forEach(id => sv(id,''));
|
||||||
|
sv('fjb-kategori','sembako');
|
||||||
|
App.showModal('modal-jenis-bantuan');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveJenis() {
|
||||||
|
const nama = gv('fjb-nama');
|
||||||
|
if (!nama) { App.toast('Nama bantuan wajib diisi','error'); return; }
|
||||||
|
const body = { nama_bantuan: nama, kategori: gv('fjb-kategori'), sumber: gv('fjb-sumber'), satuan: gv('fjb-satuan'), deskripsi: gv('fjb-deskripsi') };
|
||||||
|
try {
|
||||||
|
await Api.post('/bantuan', body);
|
||||||
|
App.toast('Jenis bantuan ditambahkan ✅','success');
|
||||||
|
App.closeModal('modal-jenis-bantuan');
|
||||||
|
load();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hapusJenis(id) {
|
||||||
|
if (!confirm('Hapus jenis bantuan ini?')) return;
|
||||||
|
try { await Api.delete(`/bantuan/${id}`); App.toast('Dihapus','success'); load(); }
|
||||||
|
catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showFormSalur(nikPrefill = '', namaPrefill = '') {
|
||||||
|
sv('fs-bantuan', '');
|
||||||
|
sv('fs-nik', nikPrefill);
|
||||||
|
sv('fs-jumlah', '1');
|
||||||
|
sv('fs-ket', '');
|
||||||
|
ge('fs-nik-info').classList.add('hidden');
|
||||||
|
ge('fs-tgl').value = new Date().toISOString().substring(0,10);
|
||||||
|
// Isi dropdown jenis bantuan
|
||||||
|
if (!jenisList.length) { try { jenisList = await Api.get('/bantuan'); } catch { jenisList = DEMO_JENIS; } }
|
||||||
|
ge('fs-bantuan').innerHTML = jenisList.map(b => `<option value="${b.id}">${KAT_ICON[b.kategori]||'📦'} ${b.nama_bantuan}</option>`).join('');
|
||||||
|
App.showModal('modal-salur');
|
||||||
|
if (nikPrefill) { setTimeout(() => cekPenerima(nikPrefill), 200); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickSalur(nik, nama) { showFormSalur(nik, nama); }
|
||||||
|
|
||||||
|
async function cekPenerima(nik) {
|
||||||
|
if (nik.length < 16) { ge('fs-nik-info').classList.add('hidden'); return; }
|
||||||
|
try {
|
||||||
|
const info = await Api.get(`/histori/${nik}/cek`);
|
||||||
|
const box = ge('fs-nik-info');
|
||||||
|
if (info.total > 0) {
|
||||||
|
box.innerHTML = `✅ Sudah dibantu <strong>${info.total}x</strong> · ${info.jenis_bantuan||'—'} · Terakhir: ${info.terakhir_dibantu?.substring(0,10)||'—'}`;
|
||||||
|
box.className = 'alert-ok';
|
||||||
|
} else {
|
||||||
|
box.innerHTML = `⚠️ Belum pernah menerima bantuan`;
|
||||||
|
box.className = 'alert-warn';
|
||||||
|
}
|
||||||
|
box.classList.remove('hidden');
|
||||||
|
} catch {
|
||||||
|
ge('fs-nik-info').classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function salurkan() {
|
||||||
|
const nik = gv('fs-nik');
|
||||||
|
if (!nik || nik.length !== 16) { App.toast('NIK penerima harus 16 digit','error'); return; }
|
||||||
|
if (!gv('fs-bantuan')) { App.toast('Pilih jenis bantuan','error'); return; }
|
||||||
|
if (!gv('fs-tgl')) { App.toast('Tanggal wajib diisi','error'); return; }
|
||||||
|
const body = { nik, id_bantuan: gv('fs-bantuan'), tanggal: gv('fs-tgl'), jumlah: gv('fs-jumlah'), keterangan: gv('fs-ket') };
|
||||||
|
try {
|
||||||
|
await Api.post('/histori', body);
|
||||||
|
App.toast('Bantuan berhasil dicatat ✅','success');
|
||||||
|
App.closeModal('modal-salur');
|
||||||
|
load();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { load, showFormJenis, saveJenis, hapusJenis, showFormSalur, quickSalur, cekPenerima, salurkan };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
const Chat = (() => {
|
||||||
|
let currentRoom = null, pollTimer = null;
|
||||||
|
|
||||||
|
const DEMO_ROOMS = [
|
||||||
|
{ id:1, nama:'# Koordinasi Umum' },
|
||||||
|
{ id:2, nama:'# Bantuan Darurat' },
|
||||||
|
{ id:3, nama:'# Distribusi Sembako' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEMO_MSGS = {
|
||||||
|
null: [
|
||||||
|
{ id:1, id_user:2, nama_lengkap:'Ahmad Fauzi', pesan:'Salam rekan-rekan semua. Ada keluarga di Gg. Mawar yang sangat butuh bantuan sembako segera.', created_at:'2025-05-13 08:00:00' },
|
||||||
|
{ id:2, id_user:3, nama_lengkap:'Romo Benediktus',pesan:'Baik. Kami di Katedral juga sedang tangani kasus Ibu Maria yang sakit keras. Mohon dikoordinasikan.', created_at:'2025-05-13 08:05:00' },
|
||||||
|
{ id:3, id_user:2, nama_lengkap:'Ahmad Fauzi', pesan:'Insya Allah kita distribusi bersama hari Jumat. Tolong daftarkan semua ke sistem ya.', created_at:'2025-05-13 08:10:00' },
|
||||||
|
{ id:4, id_user:3, nama_lengkap:'Romo Benediktus',pesan:'Setuju. Saya sudah verifikasi laporan Ibu Maria. Status diproses.', created_at:'2025-05-13 08:15:00' },
|
||||||
|
],
|
||||||
|
1: [{ id:5, id_user:2, nama_lengkap:'Ahmad Fauzi', pesan:'Koordinasi umum aktif. Silakan gunakan channel ini untuk diskusi lintas RI.', created_at:'2025-05-13 09:00:00' }],
|
||||||
|
2: [{ id:6, id_user:3, nama_lengkap:'Romo Benediktus', pesan:'Ada kasus darurat di Akcaya — balita gizi buruk. Butuh respons cepat.', created_at:'2025-05-14 07:00:00' }],
|
||||||
|
3: [{ id:7, id_user:2, nama_lengkap:'Ahmad Fauzi', pesan:'Jadwal distribusi sembako Ramadan: 15 Maret 2025, mulai pukul 08.00 di halaman masjid.', created_at:'2025-05-12 16:00:00' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
// Load rooms
|
||||||
|
let rooms = [];
|
||||||
|
try { const d = await Api.get('/chat/rooms'); rooms = d?.length ? d : DEMO_ROOMS; }
|
||||||
|
catch { rooms = DEMO_ROOMS; }
|
||||||
|
|
||||||
|
const container = ge('chat-rooms');
|
||||||
|
container.innerHTML = `<button class="room-btn active" data-room="" onclick="Chat.loadRoom('',this)"># Umum</button>`;
|
||||||
|
rooms.forEach(r => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'room-btn';
|
||||||
|
btn.dataset.room = r.id;
|
||||||
|
btn.textContent = r.nama;
|
||||||
|
btn.onclick = function() { Chat.loadRoom(r.id, this); };
|
||||||
|
container.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
await loadRoom(null, container.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRoom(roomId, btn) {
|
||||||
|
currentRoom = roomId || null;
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
document.querySelectorAll('.room-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
if (btn) btn.classList.add('active');
|
||||||
|
await fetchMessages();
|
||||||
|
pollTimer = setInterval(fetchMessages, 5000); // poll tiap 5 detik
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMessages() {
|
||||||
|
let msgs = [];
|
||||||
|
try {
|
||||||
|
const url = currentRoom ? `/chat?room=${currentRoom}` : '/chat';
|
||||||
|
msgs = await Api.get(url);
|
||||||
|
} catch {
|
||||||
|
msgs = DEMO_MSGS[currentRoom] || DEMO_MSGS[null];
|
||||||
|
}
|
||||||
|
renderMessages(msgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMessages(msgs) {
|
||||||
|
const box = ge('chat-messages');
|
||||||
|
const user = App.currentUser();
|
||||||
|
if (!msgs?.length) {
|
||||||
|
box.innerHTML = '<div class="chat-empty">💬 Belum ada pesan di channel ini</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prevScroll = box.scrollHeight - box.scrollTop;
|
||||||
|
box.innerHTML = msgs.map(m => {
|
||||||
|
const mine = user && m.id_user == user.id;
|
||||||
|
return `<div class="chat-bubble ${mine ? 'mine' : 'other'}">
|
||||||
|
${!mine ? `<div class="chat-sender">${m.nama_lengkap || 'User'}</div>` : ''}
|
||||||
|
<div class="chat-text">${escHtml(m.pesan)}</div>
|
||||||
|
<div class="chat-time">${(m.created_at||'').substring(11,16)}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
// Scroll ke bawah jika sudah di bawah
|
||||||
|
if (prevScroll < 200) box.scrollTop = box.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
const inp = ge('chat-input');
|
||||||
|
const pesan = inp.value.trim();
|
||||||
|
if (!pesan) return;
|
||||||
|
if (!App.currentUser()) { App.toast('Harus login untuk mengirim pesan','warning'); App.showModal('modal-login'); return; }
|
||||||
|
inp.value = '';
|
||||||
|
try {
|
||||||
|
await Api.post('/chat', { pesan, id_room: currentRoom });
|
||||||
|
await fetchMessages();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { init, loadRoom, send };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
const Dashboard = (() => {
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const d = await Api.get('/dashboard');
|
||||||
|
const s = d.statistik;
|
||||||
|
sv2('d-miskin', s.total_miskin); sv2('d-rentan', s.total_rentan);
|
||||||
|
sv2('d-mampu', s.total_mampu); sv2('d-kk', s.total_kk);
|
||||||
|
sv2('d-belum', d.belum_dibantu); sv2('d-darurat', s.laporan_pending);
|
||||||
|
sv2('hp-miskin-n', s.total_miskin); sv2('hp-rentan-n', s.total_rentan);
|
||||||
|
sv2('hp-mampu-n', s.total_mampu); sv2('hp-lap-n', s.laporan_pending);
|
||||||
|
ge('dash-updated').textContent = 'Update: ' + new Date().toLocaleTimeString('id-ID');
|
||||||
|
renderPrio(d.prioritas_darurat);
|
||||||
|
renderBar('d-kec-chart', d.sebaran_kecamatan, r => ({ label: r.kecamatan||'?', val: +r.miskin+ +r.rentan, color:'#ff4d6d', sub:`${r.miskin} miskin` }));
|
||||||
|
renderBar('d-ri-chart', d.beban_per_ri, r => ({ label: r.nama, val: +r.kk_miskin, color:'#4361ee', sub:`${r.jumlah_kk} KK` }));
|
||||||
|
renderBar('d-trend-chart', d.trend_bantuan, r => ({ label: r.bulan, val: +r.jumlah, color:'#06d6a0', sub:`${r.jumlah}x` }));
|
||||||
|
} catch { loadDemo(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDemo() {
|
||||||
|
sv2('d-miskin',6); sv2('d-rentan',4); sv2('d-mampu',2);
|
||||||
|
sv2('d-kk',8); sv2('d-belum',4); sv2('d-darurat',2);
|
||||||
|
sv2('hp-miskin-n',6); sv2('hp-rentan-n',4); sv2('hp-mampu-n',2); sv2('hp-lap-n',2);
|
||||||
|
ge('dash-updated').textContent = 'Data demo (offline)';
|
||||||
|
renderPrio([
|
||||||
|
{ nama_lengkap:'Maria Magdalena', umur:54, kondisi_kesehatan:'sakit_berat', kecamatan:'Pontianak Selatan', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI', nik:'6171010101010006' },
|
||||||
|
{ nama_lengkap:'Tan Eng Huat', umur:65, kondisi_kesehatan:'disabilitas', kecamatan:'Pontianak Kota', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI', nik:'6171010101010014' },
|
||||||
|
{ nama_lengkap:'Lim Ah Kow', umur:59, kondisi_kesehatan:'sakit_berat', kecamatan:'Pontianak Kota', jumlah_dibantu:1, level_prioritas:'TINGGI', nik:'6171010101010010' },
|
||||||
|
]);
|
||||||
|
renderBar('d-kec-chart',[{kecamatan:'Pontianak Timur',miskin:2,rentan:1},{kecamatan:'Pontianak Selatan',miskin:2,rentan:2},{kecamatan:'Pontianak Kota',miskin:2,rentan:1}],
|
||||||
|
r=>({label:r.kecamatan,val:+r.miskin+ +r.rentan,color:'#ff4d6d',sub:`${r.miskin} miskin`}));
|
||||||
|
renderBar('d-ri-chart',[{nama:"Masjid Jami'",jumlah_kk:3,kk_miskin:2},{nama:'Katedral',jumlah_kk:2,kk_miskin:2},{nama:'Mujahidin',jumlah_kk:1,kk_miskin:1}],
|
||||||
|
r=>({label:r.nama,val:+r.kk_miskin,color:'#4361ee',sub:`${r.jumlah_kk} KK`}));
|
||||||
|
renderBar('d-trend-chart',[{bulan:'2025-01',jumlah:2},{bulan:'2025-02',jumlah:1},{bulan:'2025-03',jumlah:3},{bulan:'2025-04',jumlah:1}],
|
||||||
|
r=>({label:r.bulan,val:+r.jumlah,color:'#06d6a0',sub:`${r.jumlah}x`}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPrio(list) {
|
||||||
|
const el = ge('d-prio-list');
|
||||||
|
if (!list?.length) { el.innerHTML = '<div class="empty-state"><div class="es-icon">✅</div><p>Tidak ada kasus darurat</p></div>'; return; }
|
||||||
|
const cls = { 'SANGAT TINGGI':'pb-sangat-tinggi','TINGGI':'pb-tinggi','SEDANG':'pb-sedang','RENDAH':'pb-rendah' };
|
||||||
|
el.innerHTML = list.map(p => `
|
||||||
|
<div class="prio-item" onclick="Bantuan.quickSalur('${p.nik}','${p.nama_lengkap}')">
|
||||||
|
<span class="prio-badge ${cls[p.level_prioritas]||'pb-rendah'}">${p.level_prioritas}</span>
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="font-weight:700;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${p.nama_lengkap}</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted)">${cap(p.kondisi_kesehatan)} · ${p.kecamatan} · dibantu ${p.jumlah_dibantu}x</div>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:18px;flex-shrink:0">→</span>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBar(elId, data, mapper) {
|
||||||
|
const el = ge(elId);
|
||||||
|
if (!data?.length) { el.innerHTML = '<p style="color:var(--muted);font-size:12px;padding:12px">Tidak ada data</p>'; return; }
|
||||||
|
const mapped = data.map(mapper);
|
||||||
|
const max = Math.max(...mapped.map(m => m.val), 1);
|
||||||
|
el.innerHTML = mapped.map(m => {
|
||||||
|
const pct = Math.round(m.val / max * 100);
|
||||||
|
return `<div class="bar-row">
|
||||||
|
<div class="bar-lbl" title="${m.label}">${m.label.slice(0,16)}${m.label.length>16?'…':''}</div>
|
||||||
|
<div class="bar-track"><div class="bar-fill" style="width:${pct}%;background:${m.color}"></div></div>
|
||||||
|
<div class="bar-val">${m.val}<span style="font-size:9px;color:var(--muted);margin-left:3px">${m.sub?.replace(/^\d+ /,'')}</span></div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sv2(id, val) { const el = ge(id); if (el) el.textContent = val ?? '—'; }
|
||||||
|
return { load };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
const Keluarga = (() => {
|
||||||
|
let all = [], editId = null;
|
||||||
|
|
||||||
|
const DEMO = [
|
||||||
|
{ id:1, no_kk:'6171010101010001', nama_kepala:'Ahmad Suryana', kecamatan:'Pontianak Timur', kelurahan:'Dalam Bugis', jumlah_anggota:3, total_penghasilan:900000, nama_ri:"Masjid Jami'", status_ekonomi:'miskin', total_bantuan:2 },
|
||||||
|
{ id:2, no_kk:'6171010101010002', nama_kepala:'Samsul Bahri', kecamatan:'Pontianak Kota', kelurahan:'Sungai Bangkong',jumlah_anggota:2,total_penghasilan:1700000, nama_ri:"Masjid Jami'", status_ekonomi:'rentan', total_bantuan:0 },
|
||||||
|
{ id:3, no_kk:'6171010101010003', nama_kepala:'Maria Magdalena', kecamatan:'Pontianak Selatan', kelurahan:'Akcaya', jumlah_anggota:3, total_penghasilan:800000, nama_ri:'Katedral', status_ekonomi:'miskin', total_bantuan:1 },
|
||||||
|
{ id:4, no_kk:'6171010101010004', nama_kepala:'Hendra Wijaya', kecamatan:'Pontianak Selatan', kelurahan:'Parit Tokaya', jumlah_anggota:2, total_penghasilan:1500000, nama_ri:'Mujahidin', status_ekonomi:'rentan', total_bantuan:0 },
|
||||||
|
{ id:5, no_kk:'6171010101010005', nama_kepala:'Lim Ah Kow', kecamatan:'Pontianak Kota', kelurahan:'Darat Sekip', jumlah_anggota:2, total_penghasilan:700000, nama_ri:'Vihara', status_ekonomi:'miskin', total_bantuan:1 },
|
||||||
|
{ id:6, no_kk:'6171010101010006', nama_kepala:'Mulyadi', kecamatan:'Pontianak Timur', kelurahan:'Dalam Bugis', jumlah_anggota:2, total_penghasilan:4500000, nama_ri:"Masjid Jami'", status_ekonomi:'mampu', total_bantuan:0 },
|
||||||
|
{ id:7, no_kk:'6171010101010007', nama_kepala:'Yohanes Kristian',kecamatan:'Pontianak Selatan', kelurahan:'Akcaya', jumlah_anggota:2, total_penghasilan:1100000, nama_ri:'Katedral', status_ekonomi:'rentan', total_bantuan:0 },
|
||||||
|
{ id:8, no_kk:'6171010101010008', nama_kepala:'Tan Eng Huat', kecamatan:'Pontianak Kota', kelurahan:'Pontianak Kota',jumlah_anggota:1,total_penghasilan:600000, nama_ri:'Klenteng', status_ekonomi:'miskin', total_bantuan:1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try { all = await Api.get('/keluarga'); } catch { all = DEMO; }
|
||||||
|
applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(q) { applyFilter(); }
|
||||||
|
|
||||||
|
function applyFilter() {
|
||||||
|
const q = gv('kf-search');
|
||||||
|
const ek = gv('kf-ekonomi');
|
||||||
|
const data = all.filter(k =>
|
||||||
|
(!q || k.nama_kepala?.toLowerCase().includes(q.toLowerCase()) || k.no_kk?.includes(q)) &&
|
||||||
|
(!ek || k.status_ekonomi === ek)
|
||||||
|
);
|
||||||
|
render(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
const tb = ge('tbody-keluarga');
|
||||||
|
if (!data.length) { tb.innerHTML = '<tr><td colspan="9"><div class="empty-state"><div class="es-icon">🏠</div><h3>Tidak ada data</h3></div></td></tr>'; return; }
|
||||||
|
tb.innerHTML = data.map(k => `
|
||||||
|
<tr class="row-${k.status_ekonomi}">
|
||||||
|
<td class="mono">${k.no_kk}</td>
|
||||||
|
<td><strong>${k.nama_kepala}</strong><div style="font-size:10px;color:var(--muted)">${k.kelurahan||''}</div></td>
|
||||||
|
<td>${k.kecamatan||'—'}</td>
|
||||||
|
<td style="text-align:center;font-weight:700;font-size:15px">${k.jumlah_anggota||0}</td>
|
||||||
|
<td class="mono" style="font-size:11px">${rp(k.total_penghasilan)}</td>
|
||||||
|
<td>${k.nama_ri||'—'}</td>
|
||||||
|
<td>${bdg(k.status_ekonomi,'')}</td>
|
||||||
|
<td style="text-align:center">
|
||||||
|
${k.total_bantuan > 0
|
||||||
|
? `<span class="bdg bdg-sehat">✅ ${k.total_bantuan}x</span>`
|
||||||
|
: `<span class="bdg bdg-darurat">❌ Belum</span>`}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="display:flex;gap:4px">
|
||||||
|
<button class="btn btn-secondary btn-xs" onclick="Keluarga.showAnggota(${k.id})" title="Lihat anggota">👥</button>
|
||||||
|
${App.canEdit() ? `
|
||||||
|
<button class="btn btn-secondary btn-xs" onclick="Keluarga.showForm(${k.id})" title="Edit">✏️</button>
|
||||||
|
<button class="btn btn-danger btn-xs" onclick="Keluarga.hapus(${k.id})" title="Hapus">🗑</button>` : ''}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showForm(id = null) {
|
||||||
|
editId = id;
|
||||||
|
ge('mk-title').textContent = id ? '✏️ Edit Data Keluarga' : '🏠 Tambah Kartu Keluarga';
|
||||||
|
ge('fk-nokk').readOnly = !!id;
|
||||||
|
// Load RI dropdown
|
||||||
|
try {
|
||||||
|
const riList = await Api.get('/rumah-ibadah');
|
||||||
|
ge('fk-ri').innerHTML = '<option value="">— Pilih Rumah Ibadah —</option>' +
|
||||||
|
riList.map(r => `<option value="${r.id}">${r.nama}</option>`).join('');
|
||||||
|
} catch {
|
||||||
|
ge('fk-ri').innerHTML = '<option value="1">Masjid Jami\' Pontianak</option>';
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
const k = all.find(x => x.id == id) || {};
|
||||||
|
sv('fk-nokk', k.no_kk); sv('fk-kepala', k.nama_kepala);
|
||||||
|
sv('fk-alamat', k.alamat); sv('fk-kecamatan', k.kecamatan);
|
||||||
|
sv('fk-kelurahan', k.kelurahan); sv('fk-ekonomi', k.status_ekonomi || 'rentan');
|
||||||
|
sv('fk-lat', k.lat); sv('fk-lng', k.lng);
|
||||||
|
if (k.id_rumah_ibadah) sv('fk-ri', k.id_rumah_ibadah);
|
||||||
|
} else {
|
||||||
|
['fk-nokk','fk-kepala','fk-alamat','fk-kecamatan','fk-kelurahan','fk-lat','fk-lng'].forEach(i => sv(i,''));
|
||||||
|
sv('fk-ekonomi','rentan');
|
||||||
|
}
|
||||||
|
App.showModal('modal-keluarga');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const nokk = gv('fk-nokk');
|
||||||
|
if (!nokk || nokk.length !== 16) { App.toast('No. KK harus 16 digit','error'); return; }
|
||||||
|
if (!gv('fk-kepala')) { App.toast('Nama kepala wajib diisi','error'); return; }
|
||||||
|
const body = {
|
||||||
|
no_kk: nokk, nama_kepala: gv('fk-kepala'),
|
||||||
|
id_rumah_ibadah: gv('fk-ri') || null,
|
||||||
|
alamat: gv('fk-alamat'), kecamatan: gv('fk-kecamatan'),
|
||||||
|
kelurahan: gv('fk-kelurahan'), status_ekonomi: gv('fk-ekonomi'),
|
||||||
|
lat: gv('fk-lat') || null, lng: gv('fk-lng') || null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (editId) { await Api.put(`/keluarga/${editId}`, body); App.toast('Data KK diperbarui ✅','success'); }
|
||||||
|
else { await Api.post('/keluarga', body); App.toast('KK ditambahkan ✅','success'); }
|
||||||
|
App.closeModal('modal-keluarga');
|
||||||
|
load();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hapus(id) {
|
||||||
|
if (!confirm('Hapus data keluarga ini?\nPastikan semua anggota sudah dipindah atau dihapus.')) return;
|
||||||
|
try { await Api.delete(`/keluarga/${id}`); App.toast('Data KK dihapus','success'); load(); }
|
||||||
|
catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showAnggota(id) {
|
||||||
|
const k = all.find(x => x.id == id);
|
||||||
|
let anggota = [];
|
||||||
|
try {
|
||||||
|
const detail = await Api.get(`/keluarga/${id}`);
|
||||||
|
anggota = detail.anggota || [];
|
||||||
|
} catch {
|
||||||
|
anggota = [];
|
||||||
|
}
|
||||||
|
if (!anggota.length) { App.toast('Tidak ada data anggota (atau belum terhubung ke API)','info'); return; }
|
||||||
|
// Tampilkan dalam alert atau panel detail
|
||||||
|
const info = anggota.map(a => `• ${a.nama_lengkap} (${a.umur}th) — ${cap(a.status_keluarga)} — ${a.status_ekonomi}`).join('\n');
|
||||||
|
alert(`Anggota KK: ${k?.nama_kepala}\n\n${info}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { load, search, applyFilter, showForm, save, hapus, showAnggota };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
const LaporanPanel = (() => {
|
||||||
|
const DEMO = [
|
||||||
|
{ id:1, nama_pelapor:'Anonim', deskripsi:'Ibu Maria sakit keras sudah seminggu, tidak bisa berobat karena tidak ada biaya. Butuh bantuan medis segera.', urgensi:'darurat', status:'diverifikasi', alamat_laporan:'Jl. Kemuning No.3, Akcaya', created_at:'2025-05-01T08:00:00', nama_ri:'Gereja Katedral' },
|
||||||
|
{ id:2, nama_pelapor:'Eko Prasetyo', deskripsi:'Ada keluarga baru pindah ke Gg. Melati yang sepertinya sangat membutuhkan bantuan. Anak-anak tidak sekolah.', urgensi:'sedang', status:'pending', alamat_laporan:'Gg. Melati, Dalam Bugis', created_at:'2025-05-10T10:00:00', nama_ri:"Masjid Jami'" },
|
||||||
|
{ id:3, nama_pelapor:'Anonim', deskripsi:'Pak Tan penyandang disabilitas, hidup sebatang kara dan sangat sulit mendapat penghasilan. Perlu perhatian.', urgensi:'tinggi', status:'diproses', alamat_laporan:'Jl. Gusti Situt No.7', created_at:'2025-05-12T14:00:00', nama_ri:'Klenteng Kwan Ti Miau' },
|
||||||
|
{ id:4, nama_pelapor:'Hendra', deskripsi:'Balita 2 tahun kondisi gizi buruk. Orang tua tidak mampu memenuhi kebutuhan gizi anak.', urgensi:'darurat', status:'pending', alamat_laporan:'Gg. Teratai, Parit Tokaya', created_at:'2025-05-15T09:00:00', nama_ri:'Masjid Mujahidin' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const URG = { darurat:'🔴', tinggi:'🟠', sedang:'🟡', rendah:'🟢' };
|
||||||
|
const URG_COLOR = { darurat:'var(--red)', tinggi:'var(--orange)', sedang:'var(--yellow)', rendah:'var(--green)' };
|
||||||
|
const STATUS_LABEL = { pending:'⏳ Pending', diverifikasi:'✅ Diverifikasi', diproses:'🔄 Diproses', selesai:'🏁 Selesai' };
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const st = gv('lf-status');
|
||||||
|
const ur = gv('lf-urgensi');
|
||||||
|
let url = '/laporan';
|
||||||
|
const p = []; if(st) p.push(`status=${st}`); if(ur) p.push(`urgensi=${ur}`);
|
||||||
|
if(p.length) url += '?' + p.join('&');
|
||||||
|
let data;
|
||||||
|
try { data = await Api.get(url); } catch { data = DEMO; }
|
||||||
|
if(st) data = data.filter(l => l.status === st);
|
||||||
|
if(ur) data = data.filter(l => l.urgensi === ur);
|
||||||
|
render(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
const el = ge('laporan-grid');
|
||||||
|
if (!data.length) {
|
||||||
|
el.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="es-icon">✅</div><h3>Tidak ada laporan</h3><p>Semua laporan sudah ditangani</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = data.map(l => `
|
||||||
|
<div class="laporan-card ${l.urgensi}">
|
||||||
|
<div class="lc-hdr">
|
||||||
|
<div>
|
||||||
|
<span class="bdg bdg-${l.urgensi}" style="margin-bottom:6px;display:inline-block">${URG[l.urgensi]||'📋'} ${(l.urgensi||'').toUpperCase()}</span>
|
||||||
|
<div class="lc-title">#${l.id} — ${l.nama_pelapor||'Anonim'}</div>
|
||||||
|
</div>
|
||||||
|
<span class="bdg bdg-${l.status}">${STATUS_LABEL[l.status]||l.status}</span>
|
||||||
|
</div>
|
||||||
|
<div class="lc-desc">${l.deskripsi}</div>
|
||||||
|
<div class="lc-meta">
|
||||||
|
<span>📍 ${l.alamat_laporan||'—'}</span>
|
||||||
|
<span>🕌 ${l.nama_ri||'—'}</span>
|
||||||
|
<span>📅 ${(l.created_at||'').substring(0,10)}</span>
|
||||||
|
</div>
|
||||||
|
${App.canEdit() && l.status !== 'selesai' ? `
|
||||||
|
<div class="lc-actions">
|
||||||
|
${l.status === 'pending' ? `<button class="btn btn-secondary btn-sm" onclick="LaporanPanel.updateStatus(${l.id},'diverifikasi')">✅ Verifikasi</button>` : ''}
|
||||||
|
${l.status === 'diverifikasi' ? `<button class="btn btn-warning btn-sm" onclick="LaporanPanel.updateStatus(${l.id},'diproses')">🔄 Proses</button>` : ''}
|
||||||
|
${l.status !== 'selesai' ? `<button class="btn btn-success btn-sm" onclick="LaporanPanel.updateStatus(${l.id},'selesai')">🏁 Selesai</button>` : ''}
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="LaporanPanel.hapus(${l.id})">🗑</button>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStatus(id, status) {
|
||||||
|
try {
|
||||||
|
await Api.put(`/laporan/${id}`, { status });
|
||||||
|
App.toast(`Status laporan diubah: ${STATUS_LABEL[status]||status}`, 'success');
|
||||||
|
load(); App.updateHeaderStats();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hapus(id) {
|
||||||
|
if (!confirm('Hapus laporan ini?')) return;
|
||||||
|
try { await Api.delete(`/laporan/${id}`); App.toast('Laporan dihapus','success'); load(); }
|
||||||
|
catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showKirimModal() {
|
||||||
|
['fl-nama','fl-hp','fl-nik','fl-alamat','fl-deskripsi'].forEach(id => sv(id,''));
|
||||||
|
sv('fl-urgensi','sedang');
|
||||||
|
App.showModal('modal-laporan');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kirim() {
|
||||||
|
if (!gv('fl-deskripsi')) { App.toast('Deskripsi kondisi wajib diisi','error'); return; }
|
||||||
|
const body = {
|
||||||
|
nama_pelapor: gv('fl-nama') || null,
|
||||||
|
no_hp_pelapor: gv('fl-hp') || null,
|
||||||
|
nik_terdampak: gv('fl-nik') || null,
|
||||||
|
urgensi: gv('fl-urgensi'),
|
||||||
|
alamat_laporan:gv('fl-alamat'),
|
||||||
|
deskripsi: gv('fl-deskripsi'),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await Api.post('/laporan', body);
|
||||||
|
App.closeModal('modal-laporan');
|
||||||
|
App.toast('Laporan berhasil dikirim! Terima kasih 🙏','success');
|
||||||
|
load(); App.updateHeaderStats();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { load, updateStatus, hapus, showKirimModal, kirim };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const LogPanel = (() => {
|
||||||
|
const DEMO = [
|
||||||
|
{ created_at:'2025-05-14 10:32:00', nama_lengkap:'Ahmad Fauzi', aksi:'create_histori_bantuan', tabel:'histori_bantuan', id_record:'5', ip_address:'192.168.1.10' },
|
||||||
|
{ created_at:'2025-05-14 09:15:00', nama_lengkap:'Romo Benediktus', aksi:'update_laporan', tabel:'laporan', id_record:'1', ip_address:'192.168.1.20' },
|
||||||
|
{ created_at:'2025-05-13 08:10:00', nama_lengkap:'Ahmad Fauzi', aksi:'update_penduduk', tabel:'penduduk', id_record:'6171010101010006', ip_address:'192.168.1.10' },
|
||||||
|
{ created_at:'2025-05-13 08:00:00', nama_legendkap:'Ahmad Fauzi', aksi:'login', tabel:'user', id_record:'2', ip_address:'192.168.1.10' },
|
||||||
|
{ created_at:'2025-05-12 16:45:00', nama_lengkap:'Administrator', aksi:'create_penduduk', tabel:'penduduk', id_record:'6171010101010014', ip_address:'127.0.0.1' },
|
||||||
|
{ created_at:'2025-05-12 14:00:00', nama_lengkap:'Administrator', aksi:'delete_penduduk', tabel:'penduduk', id_record:'6171999999999999', ip_address:'127.0.0.1' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
let data;
|
||||||
|
try { data = await Api.get('/log'); } catch { data = DEMO; }
|
||||||
|
render(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAksiClass(aksi) {
|
||||||
|
if (aksi?.startsWith('create')) return 'create';
|
||||||
|
if (aksi?.startsWith('update')) return 'update';
|
||||||
|
if (aksi?.startsWith('delete')) return 'delete';
|
||||||
|
if (aksi === 'login' || aksi === 'logout') return 'login';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
const tb = ge('tbody-log');
|
||||||
|
if (!data.length) { tb.innerHTML = '<tr><td colspan="6"><div class="empty-state"><div class="es-icon">📋</div><h3>Tidak ada log</h3></div></td></tr>'; return; }
|
||||||
|
tb.innerHTML = data.map(l => `
|
||||||
|
<tr>
|
||||||
|
<td class="mono" style="font-size:11px;white-space:nowrap">${(l.created_at||'').substring(0,16)}</td>
|
||||||
|
<td style="font-weight:600">${l.nama_lengkap||'—'}</td>
|
||||||
|
<td><span class="log-badge ${getAksiClass(l.aksi)}">${l.aksi}</span></td>
|
||||||
|
<td style="color:var(--muted2);font-size:11px">${l.tabel||'—'}</td>
|
||||||
|
<td class="mono" style="font-size:10px;color:var(--muted);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${(l.id_record||'').substring(0,20)}</td>
|
||||||
|
<td style="font-size:11px;color:var(--muted)">${l.ip_address||'—'}</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { load };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
const MapApp = (() => {
|
||||||
|
let map, clusterLayer, riLayer, laporanLayer, radiusLayer;
|
||||||
|
let allPenduduk = [], allRI = [], allLaporan = [];
|
||||||
|
let layerState = { penduduk:true, ri:true, laporan:false, radius:true };
|
||||||
|
let ready = false;
|
||||||
|
|
||||||
|
const COLOR = { miskin:'#ff4d6d', rentan:'#ffd166', mampu:'#06d6a0' };
|
||||||
|
const RI_EMOJI = { Masjid:'🕌', Gereja:'⛪', Vihara:'🛕', Pura:'🛕', Klenteng:'🏮', Lainnya:'🏛️' };
|
||||||
|
|
||||||
|
function mkIcon(color, emoji='') {
|
||||||
|
return L.divIcon({
|
||||||
|
className:'',
|
||||||
|
html:`<div style="width:30px;height:30px;border-radius:50%;background:${color};display:flex;align-items:center;justify-content:center;font-size:13px;box-shadow:0 2px 8px rgba(0,0,0,.5);border:2px solid rgba(255,255,255,.25)">${emoji||'👤'}</div>`,
|
||||||
|
iconSize:[30,30],iconAnchor:[15,15],popupAnchor:[0,-16]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function riIcon(emoji) {
|
||||||
|
return L.divIcon({
|
||||||
|
className:'',
|
||||||
|
html:`<div style="font-size:28px;filter:drop-shadow(0 2px 6px rgba(0,0,0,.6));line-height:1">${emoji}</div>`,
|
||||||
|
iconSize:[34,34],iconAnchor:[17,17],popupAnchor:[0,-18]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (ready) { setTimeout(()=>map.invalidateSize(),100); return; }
|
||||||
|
ready = true;
|
||||||
|
|
||||||
|
map = L.map('map',{ center:[-0.028,109.338],zoom:13,zoomControl:true });
|
||||||
|
|
||||||
|
const tiles = {
|
||||||
|
'Dark': L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{attribution:'© CARTO',maxZoom:19}),
|
||||||
|
'Street': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OSM',maxZoom:19}),
|
||||||
|
'Satellite': L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{attribution:'© Esri',maxZoom:19}),
|
||||||
|
};
|
||||||
|
tiles['Dark'].addTo(map);
|
||||||
|
L.control.layers(tiles,{},{collapsed:false,position:'topright'}).addTo(map);
|
||||||
|
|
||||||
|
clusterLayer = L.markerClusterGroup({ maxClusterRadius:60, showCoverageOnHover:false });
|
||||||
|
riLayer = L.layerGroup();
|
||||||
|
laporanLayer = L.layerGroup();
|
||||||
|
radiusLayer = L.layerGroup();
|
||||||
|
|
||||||
|
map.addLayer(clusterLayer);
|
||||||
|
map.addLayer(riLayer);
|
||||||
|
map.addLayer(radiusLayer);
|
||||||
|
|
||||||
|
// Koordinat kursor
|
||||||
|
const coordDiv = L.control({position:'bottomleft'});
|
||||||
|
coordDiv.onAdd = ()=>{
|
||||||
|
const d=L.DomUtil.create('div');
|
||||||
|
d.style.cssText='background:rgba(8,12,20,.85);color:#aaa;padding:4px 12px;border-radius:6px;font-size:11px;font-family:JetBrains Mono,monospace;border:1px solid rgba(255,255,255,.1)';
|
||||||
|
map.on('mousemove',e=>d.innerHTML=`${e.latlng.lat.toFixed(6)}, ${e.latlng.lng.toFixed(6)}`);
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
coordDiv.addTo(map);
|
||||||
|
|
||||||
|
loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAll() {
|
||||||
|
try {
|
||||||
|
const [gP, gR, gL] = await Promise.all([
|
||||||
|
Api.get('/peta?layer=penduduk'),
|
||||||
|
Api.get('/peta?layer=rumah_ibadah'),
|
||||||
|
Api.get('/peta?layer=laporan'),
|
||||||
|
]);
|
||||||
|
allPenduduk = gP.features || [];
|
||||||
|
allRI = gR.features || [];
|
||||||
|
allLaporan = gL.features || [];
|
||||||
|
} catch {
|
||||||
|
// Demo data
|
||||||
|
allPenduduk = buildDemoPenduduk();
|
||||||
|
allRI = buildDemoRI();
|
||||||
|
allLaporan = buildDemoLaporan();
|
||||||
|
}
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAll() {
|
||||||
|
renderPenduduk(allPenduduk);
|
||||||
|
renderRI(allRI);
|
||||||
|
renderLaporan(allLaporan);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPenduduk(features) {
|
||||||
|
clusterLayer.clearLayers();
|
||||||
|
features.forEach(f => {
|
||||||
|
const p = f.properties;
|
||||||
|
const [lng,lat] = f.geometry.coordinates;
|
||||||
|
const icon = mkIcon(COLOR[p.status_ekonomi]||COLOR.rentan);
|
||||||
|
L.marker([lat,lng],{icon}).bindPopup(`
|
||||||
|
<div class="map-popup">
|
||||||
|
<div class="mp-title">🏠 ${p.nama_kepala||p.nama_lengkap}</div>
|
||||||
|
<hr class="mp-divider">
|
||||||
|
<div class="mp-row"><span>No. KK</span><strong class="mono">${p.no_kk||p.nik||'—'}</strong></div>
|
||||||
|
<div class="mp-row"><span>Alamat</span><strong>${p.alamat||'—'}</strong></div>
|
||||||
|
<div class="mp-row"><span>Anggota</span><strong>${p.jumlah_anggota||1} jiwa</strong></div>
|
||||||
|
<div class="mp-row"><span>Status</span><strong style="color:${COLOR[p.status_ekonomi]}">${(p.status_ekonomi||'').toUpperCase()}</strong></div>
|
||||||
|
<div class="mp-row"><span>Bantuan</span><strong>${p.total_bantuan||0}x diterima</strong></div>
|
||||||
|
<div class="mp-row"><span>Rumah Ibadah</span><strong>${p.nama_ri||'—'}</strong></div>
|
||||||
|
</div>`).addTo(clusterLayer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRI(features) {
|
||||||
|
riLayer.clearLayers(); radiusLayer.clearLayers();
|
||||||
|
features.forEach(f => {
|
||||||
|
const r = f.properties;
|
||||||
|
const [lng,lat] = f.geometry.coordinates;
|
||||||
|
const emoji = RI_EMOJI[r.jenis]||'🏛️';
|
||||||
|
L.marker([lat,lng],{icon:riIcon(emoji)}).bindPopup(`
|
||||||
|
<div class="map-popup">
|
||||||
|
<div class="mp-title">${emoji} ${r.nama}</div>
|
||||||
|
<div class="mp-row"><span>Jenis</span><strong>${r.jenis}</strong></div>
|
||||||
|
<div class="mp-row"><span>Alamat</span><strong>${r.alamat||'—'}</strong></div>
|
||||||
|
<div class="mp-row"><span>Kontak</span><strong>${r.kontak||'—'}</strong></div>
|
||||||
|
<div class="mp-row"><span>Radius</span><strong>${r.radius_meter}m</strong></div>
|
||||||
|
</div>`).addTo(riLayer);
|
||||||
|
L.circle([lat,lng],{
|
||||||
|
radius:r.radius_meter,color:'#4361ee',fillColor:'#4361ee',
|
||||||
|
fillOpacity:.06,weight:1.5,dashArray:'6 4'
|
||||||
|
}).addTo(radiusLayer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLaporan(features) {
|
||||||
|
laporanLayer.clearLayers();
|
||||||
|
const UC = { darurat:'#ff4d6d', tinggi:'#ff8c42', sedang:'#ffd166', rendah:'#06d6a0' };
|
||||||
|
const UE = { darurat:'🚨', tinggi:'⚠️', sedang:'📋', rendah:'📝' };
|
||||||
|
features.forEach(f => {
|
||||||
|
const l = f.properties;
|
||||||
|
const [lng,lat] = f.geometry.coordinates;
|
||||||
|
const icon = mkIcon(UC[l.urgensi]||'#888', UE[l.urgensi]||'📋');
|
||||||
|
L.marker([lat,lng],{icon}).bindPopup(`
|
||||||
|
<div class="map-popup">
|
||||||
|
<div class="mp-title">${UE[l.urgensi]||'📋'} Laporan ${(l.urgensi||'').toUpperCase()}</div>
|
||||||
|
<hr class="mp-divider">
|
||||||
|
<div style="font-size:12px;color:#aaa;margin-bottom:8px">${l.deskripsi||'—'}</div>
|
||||||
|
<div class="mp-row"><span>Alamat</span><strong>${l.alamat_laporan||'—'}</strong></div>
|
||||||
|
<div class="mp-row"><span>Status</span><strong>${l.status||'—'}</strong></div>
|
||||||
|
</div>`).addTo(laporanLayer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilter() {
|
||||||
|
const status = ge('map-status-filter')?.value;
|
||||||
|
const data = status ? allPenduduk.filter(f => f.properties.status_ekonomi === status) : allPenduduk;
|
||||||
|
renderPenduduk(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLayer(name, btn) {
|
||||||
|
layerState[name] = !layerState[name];
|
||||||
|
const on = layerState[name];
|
||||||
|
const layerMap = { penduduk:clusterLayer, ri:riLayer, laporan:laporanLayer, radius:radiusLayer };
|
||||||
|
const activeCls = { penduduk:'active-green', ri:'active-blue', laporan:'active-purple', radius:'active-orange' };
|
||||||
|
on ? map.addLayer(layerMap[name]) : map.removeLayer(layerMap[name]);
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.toggle(activeCls[name], on);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demo seed data (offline mode)
|
||||||
|
function buildDemoPenduduk() {
|
||||||
|
const seeds = [
|
||||||
|
{ nama_kepala:'Ahmad Suryana', alamat:'Gg. Mawar No.5', lat:-0.027, lng:109.344, status_ekonomi:'miskin', jumlah_anggota:3, no_kk:'6171010101010001', nama_ri:"Masjid Jami'", total_bantuan:2 },
|
||||||
|
{ nama_kepala:'Samsul Bahri', alamat:'Jl. Veteran No.12', lat:-0.0315, lng:109.339, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010002', nama_ri:"Masjid Jami'", total_bantuan:0 },
|
||||||
|
{ nama_kepala:'Maria Magdalena',alamat:'Jl. Kemuning No.3', lat:-0.033, lng:109.331, status_ekonomi:'miskin', jumlah_anggota:3, no_kk:'6171010101010003', nama_ri:'Katedral', total_bantuan:1 },
|
||||||
|
{ nama_kepala:'Hendra Wijaya', alamat:'Jl. Ahmad Yani', lat:-0.049, lng:109.326, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010004', nama_ri:'Mujahidin', total_bantuan:0 },
|
||||||
|
{ nama_kepala:'Lim Ah Kow', alamat:'Jl. Diponegoro', lat:-0.022, lng:109.337, status_ekonomi:'miskin', jumlah_anggota:2, no_kk:'6171010101010005', nama_ri:'Vihara', total_bantuan:1 },
|
||||||
|
{ nama_kepala:'Mulyadi', alamat:'Gg. Teratai No.8', lat:-0.0265, lng:109.346, status_ekonomi:'mampu', jumlah_anggota:2, no_kk:'6171010101010006', nama_ri:"Masjid Jami'", total_bantuan:0 },
|
||||||
|
{ nama_kepala:'Yohanes Kristian',alamat:'Jl. HOS Cokro', lat:-0.034, lng:109.330, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010007', nama_ri:'Katedral', total_bantuan:0 },
|
||||||
|
{ nama_kepala:'Tan Eng Huat', alamat:'Jl. Gusti Situt', lat:-0.020, lng:109.4415,status_ekonomi:'miskin', jumlah_anggota:1, no_kk:'6171010101010008', nama_ri:'Klenteng', total_bantuan:1 },
|
||||||
|
];
|
||||||
|
return seeds.map(s=>({type:'Feature',geometry:{type:'Point',coordinates:[s.lng,s.lat]},properties:s}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDemoRI() {
|
||||||
|
const ri = [
|
||||||
|
{ nama:"Masjid Jami' Pontianak", jenis:'Masjid', lat:-0.02596, lng:109.34307, radius_meter:350, alamat:'Jl. Tanjungpura', kontak:'0561-732001' },
|
||||||
|
{ nama:'Gereja Katedral Pontianak', jenis:'Gereja', lat:-0.03200, lng:109.33200, radius_meter:300, alamat:'Jl. Patimura No.9', kontak:'0561-766000' },
|
||||||
|
{ nama:'Vihara Bodhisattva Karaniya Metta', jenis:'Vihara', lat:-0.02100, lng:109.33800, radius_meter:300, alamat:'Jl. Diponegoro', kontak:'0561-733456' },
|
||||||
|
{ nama:'Klenteng Kwan Ti Miau', jenis:'Klenteng', lat:-0.01900, lng:109.34100, radius_meter:280, alamat:'Jl. Gusti Situt', kontak:'0561-740123' },
|
||||||
|
{ nama:'Masjid Mujahidin Pontianak', jenis:'Masjid', lat:-0.04800, lng:109.32500, radius_meter:400, alamat:'Jl. Ahmad Yani', kontak:'0561-764567' },
|
||||||
|
{ nama:'Gereja GPIB Immanuel', jenis:'Gereja', lat:-0.02800, lng:109.34500, radius_meter:300, alamat:'Jl. Rahadi Usman', kontak:'0561-735890' },
|
||||||
|
];
|
||||||
|
return ri.map(r=>({type:'Feature',geometry:{type:'Point',coordinates:[r.lng,r.lat]},properties:r}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDemoLaporan() {
|
||||||
|
return [
|
||||||
|
{ type:'Feature',geometry:{type:'Point',coordinates:[109.3311,-0.0330]},properties:{ deskripsi:'Ibu Maria sakit keras, tidak bisa berobat', urgensi:'darurat', status:'diverifikasi', alamat_laporan:'Jl. Kemuning No.3' }},
|
||||||
|
{ type:'Feature',geometry:{type:'Point',coordinates:[109.3415,-0.0200]},properties:{ deskripsi:'Pak Tan penyandang disabilitas butuh bantuan', urgensi:'tinggi', status:'diproses', alamat_laporan:'Jl. Gusti Situt No.7' }},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { init, applyFilter, toggleLayer, loadAll };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
const Penduduk = (() => {
|
||||||
|
let all = [], filtered = [], editNIK = null;
|
||||||
|
|
||||||
|
const DEMO = [
|
||||||
|
{ nik:'6171010101010001', nama_lengkap:'Ahmad Suryana', umur:44, status_keluarga:'kepala', pekerjaan:'Buruh Harian', penghasilan:900000, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMP', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
|
||||||
|
{ nik:'6171010101010002', nama_lengkap:'Siti Rahma', umur:39, status_keluarga:'istri', pekerjaan:'Ibu RT', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_ringan',pendidikan_terakhir:'SD', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
|
||||||
|
{ nik:'6171010101010003', nama_lengkap:'Budi Suryana', umur:12, status_keluarga:'anak', pekerjaan:'Pelajar', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMP', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
|
||||||
|
{ nik:'6171010101010004', nama_lengkap:'Samsul Bahri', umur:49, status_keluarga:'kepala', pekerjaan:'Pedagang Kaki Lima',penghasilan:1200000,status_ekonomi:'rentan', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMA_SMK', no_kk:'6171010101010002', kecamatan:'Pontianak Kota' },
|
||||||
|
{ nik:'6171010101010005', nama_lengkap:'Dewi Kartika', umur:46, status_keluarga:'istri', pekerjaan:'Buruh Cuci', penghasilan:500000, status_ekonomi:'rentan', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SD', no_kk:'6171010101010002', kecamatan:'Pontianak Kota' },
|
||||||
|
{ nik:'6171010101010006', nama_lengkap:'Maria Magdalena', umur:54, status_keluarga:'kepala', pekerjaan:'Penjual Gorengan',penghasilan:800000, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', pendidikan_terakhir:'SD', no_kk:'6171010101010003', kecamatan:'Pontianak Selatan' },
|
||||||
|
{ nik:'6171010101010007', nama_lengkap:'Petrus Yulius', umur:19, status_keluarga:'anak', pekerjaan:'Pelajar', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMA_SMK', no_kk:'6171010101010003', kecamatan:'Pontianak Selatan' },
|
||||||
|
{ nik:'6171010101010010', nama_lengkap:'Lim Ah Kow', umur:59, status_keluarga:'kepala', pekerjaan:'Tidak Bekerja', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', pendidikan_terakhir:'SD', no_kk:'6171010101010005', kecamatan:'Pontianak Kota' },
|
||||||
|
{ nik:'6171010101010012', nama_lengkap:'Mulyadi', umur:45, status_keluarga:'kepala', pekerjaan:'PNS Gol. III', penghasilan:4500000, status_ekonomi:'mampu', kondisi_kesehatan:'sehat', pendidikan_terakhir:'S1', no_kk:'6171010101010006', kecamatan:'Pontianak Timur' },
|
||||||
|
{ nik:'6171010101010014', nama_lengkap:'Tan Eng Huat', umur:65, status_keluarga:'kepala', pekerjaan:'Pemulung', penghasilan:600000, status_ekonomi:'miskin', kondisi_kesehatan:'disabilitas', pendidikan_terakhir:'SD', no_kk:'6171010101010008', kecamatan:'Pontianak Kota' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try { all = await Api.get('/penduduk'); } catch { all = DEMO; }
|
||||||
|
filtered = [...all];
|
||||||
|
render(filtered);
|
||||||
|
ge('pf-count').textContent = `${all.length} total`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(q) { applyFilter(q); }
|
||||||
|
function applyFilter(qOverride) {
|
||||||
|
const q = qOverride ?? gv('pf-search');
|
||||||
|
const ek = gv('pf-ekonomi');
|
||||||
|
const ke = gv('pf-kesehatan');
|
||||||
|
filtered = all.filter(p =>
|
||||||
|
(!q || p.nama_lengkap?.toLowerCase().includes(q.toLowerCase()) || p.nik?.includes(q)) &&
|
||||||
|
(!ek || p.status_ekonomi === ek) &&
|
||||||
|
(!ke || p.kondisi_kesehatan === ke)
|
||||||
|
);
|
||||||
|
render(filtered);
|
||||||
|
ge('pf-count').textContent = `${filtered.length} / ${all.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendidikanLabel = { tidak_sekolah:'Tdk Sekolah', SD:'SD', SMP:'SMP', SMA_SMK:'SMA/SMK', D1_D3:'D3', S1:'S1', S2_S3:'S2/S3' };
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
const tb = ge('tbody-penduduk');
|
||||||
|
if (!data.length) { tb.innerHTML = '<tr><td colspan="10"><div class="empty-state"><div class="es-icon">🔍</div><h3>Tidak ada data</h3><p>Coba ubah filter pencarian</p></div></td></tr>'; return; }
|
||||||
|
tb.innerHTML = data.map(p => `
|
||||||
|
<tr class="row-${p.status_ekonomi}">
|
||||||
|
<td class="mono">${p.nik}</td>
|
||||||
|
<td><strong>${p.nama_lengkap}</strong><div style="font-size:10px;color:var(--muted)">${p.kecamatan||''}</div></td>
|
||||||
|
<td>${p.umur||'?'} th</td>
|
||||||
|
<td>${cap(p.status_keluarga)}</td>
|
||||||
|
<td>${p.pekerjaan||'—'}</td>
|
||||||
|
<td style="white-space:nowrap;font-family:'JetBrains Mono',monospace;font-size:11px">${rp(p.penghasilan)}</td>
|
||||||
|
<td>${bdg(p.status_ekonomi,'')}</td>
|
||||||
|
<td>${bdg(p.kondisi_kesehatan,'')}</td>
|
||||||
|
<td>${pendidikanLabel[p.pendidikan_terakhir]||p.pendidikan_terakhir||'—'}</td>
|
||||||
|
<td>
|
||||||
|
<div style="display:flex;gap:4px">
|
||||||
|
<button class="btn btn-secondary btn-xs" onclick="Penduduk.showDetail('${p.nik}')" title="Detail">👁</button>
|
||||||
|
<button class="btn btn-secondary btn-xs" onclick="Penduduk.showForm('${p.nik}')" title="Edit" ${!App.canEdit()?'style="display:none"':''}>✏️</button>
|
||||||
|
<button class="btn btn-danger btn-xs" onclick="Penduduk.hapus('${p.nik}')" title="Hapus" ${!App.canEdit()?'style="display:none"':''}>🗑</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showForm(nik = null) {
|
||||||
|
editNIK = nik;
|
||||||
|
ge('mp-title').textContent = nik ? `✏️ Edit — ${nik}` : '➕ Tambah Penduduk Baru';
|
||||||
|
ge('fp-nik').readOnly = !!nik;
|
||||||
|
await loadKKDropdown();
|
||||||
|
if (nik) {
|
||||||
|
try {
|
||||||
|
const p = await Api.get(`/penduduk/${nik}`);
|
||||||
|
fillForm(p);
|
||||||
|
} catch {
|
||||||
|
const p = all.find(x => x.nik === nik);
|
||||||
|
if (p) fillForm(p);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
['fp-nik','fp-nama','fp-tempat','fp-pekerjaan','fp-ket-sehat'].forEach(id => sv(id,''));
|
||||||
|
sv('fp-penghasilan','0'); sv('fp-umur','');
|
||||||
|
ge('fp-warn').classList.add('hidden');
|
||||||
|
}
|
||||||
|
App.showModal('modal-penduduk');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm(p) {
|
||||||
|
sv('fp-nik', p.nik); sv('fp-nama', p.nama_lengkap);
|
||||||
|
sv('fp-jkel', p.jenis_kelamin); sv('fp-tgl', p.tanggal_lahir?.substring(0,10));
|
||||||
|
sv('fp-umur', p.umur ? `${p.umur} tahun` : '');
|
||||||
|
sv('fp-tempat', p.tempat_lahir); sv('fp-agama', p.agama);
|
||||||
|
sv('fp-status-kk', p.status_keluarga); sv('fp-kawin', p.status_perkawinan);
|
||||||
|
sv('fp-hidup', p.status_hidup); sv('fp-ekonomi', p.status_ekonomi);
|
||||||
|
sv('fp-pekerjaan', p.pekerjaan); sv('fp-penghasilan', p.penghasilan || 0);
|
||||||
|
sv('fp-pendidikan', p.pendidikan_terakhir); sv('fp-stat-pend', p.status_pendidikan);
|
||||||
|
sv('fp-kesehatan', p.kondisi_kesehatan); sv('fp-ket-sehat', p.keterangan_kesehatan);
|
||||||
|
if (p.id_keluarga) sv('fp-kk', p.id_keluarga);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadKKDropdown() {
|
||||||
|
const sel = ge('fp-kk');
|
||||||
|
try {
|
||||||
|
const list = await Api.get('/keluarga');
|
||||||
|
sel.innerHTML = list.map(k => `<option value="${k.id}">${k.no_kk} — ${k.nama_kepala}</option>`).join('');
|
||||||
|
} catch {
|
||||||
|
sel.innerHTML = '<option value="1">Demo — Ahmad Suryana</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hitungUmur() {
|
||||||
|
const tgl = gv('fp-tgl');
|
||||||
|
if (!tgl) return;
|
||||||
|
const umur = Math.floor((Date.now() - new Date(tgl)) / (365.25*24*3600*1000));
|
||||||
|
sv('fp-umur', `${umur} tahun`);
|
||||||
|
// Validasi
|
||||||
|
const pend = gv('fp-pendidikan'), stat = gv('fp-stat-pend');
|
||||||
|
const warns = [];
|
||||||
|
if (umur >= 7 && umur <= 15 && stat === 'tidak_sekolah')
|
||||||
|
warns.push(`⚠️ Usia ${umur} tahun — seharusnya masih wajib belajar (SD/SMP)`);
|
||||||
|
if (umur >= 16 && umur <= 18 && stat === 'tidak_sekolah' && ['tidak_sekolah','SD'].includes(pend))
|
||||||
|
warns.push(`⚠️ Usia ${umur} tahun — disarankan melanjutkan ke SMA/SMK`);
|
||||||
|
const wEl = ge('fp-warn');
|
||||||
|
if (warns.length) { wEl.innerHTML = warns.join('<br>'); wEl.classList.remove('hidden'); }
|
||||||
|
else wEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const nik = gv('fp-nik');
|
||||||
|
if (!nik || nik.length !== 16 || !/^\d+$/.test(nik)) { App.toast('NIK harus 16 digit angka','error'); return; }
|
||||||
|
if (!gv('fp-nama')) { App.toast('Nama lengkap wajib diisi','error'); return; }
|
||||||
|
if (!gv('fp-tgl')) { App.toast('Tanggal lahir wajib diisi','error'); return; }
|
||||||
|
const body = {
|
||||||
|
nik, id_keluarga: gv('fp-kk'),
|
||||||
|
nama_lengkap: gv('fp-nama'), jenis_kelamin: gv('fp-jkel'),
|
||||||
|
tanggal_lahir: gv('fp-tgl'), tempat_lahir: gv('fp-tempat'),
|
||||||
|
agama: gv('fp-agama'), status_keluarga: gv('fp-status-kk'),
|
||||||
|
status_perkawinan: gv('fp-kawin'), status_hidup: gv('fp-hidup'),
|
||||||
|
pekerjaan: gv('fp-pekerjaan'), penghasilan: +gv('fp-penghasilan')||0,
|
||||||
|
status_ekonomi: gv('fp-ekonomi'), pendidikan_terakhir: gv('fp-pendidikan'),
|
||||||
|
status_pendidikan: gv('fp-stat-pend'), kondisi_kesehatan: gv('fp-kesehatan'),
|
||||||
|
keterangan_kesehatan: gv('fp-ket-sehat'),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (editNIK) { await Api.put(`/penduduk/${editNIK}`, body); App.toast('Data diperbarui ✅','success'); }
|
||||||
|
else { await Api.post('/penduduk', body); App.toast('Penduduk ditambahkan ✅','success'); }
|
||||||
|
App.closeModal('modal-penduduk');
|
||||||
|
await load();
|
||||||
|
App.updateHeaderStats();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hapus(nik) {
|
||||||
|
if (!confirm(`Hapus data penduduk NIK ${nik}?\nTindakan ini tidak dapat dibatalkan.`)) return;
|
||||||
|
try {
|
||||||
|
await Api.delete(`/penduduk/${nik}`);
|
||||||
|
App.toast('Data dihapus','success');
|
||||||
|
load(); App.updateHeaderStats();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showDetail(nik) {
|
||||||
|
ge('dp-title').textContent = 'Memuat…';
|
||||||
|
ge('detail-panel').classList.add('open');
|
||||||
|
let p;
|
||||||
|
try { p = await Api.get(`/penduduk/${nik}`); }
|
||||||
|
catch { p = all.find(x => x.nik === nik); }
|
||||||
|
if (!p) { App.toast('Gagal memuat detail','error'); return; }
|
||||||
|
ge('dp-title').textContent = p.nama_lengkap;
|
||||||
|
const histori = p.histori_bantuan || [];
|
||||||
|
ge('dp-body').innerHTML = `
|
||||||
|
<div class="dp-section">
|
||||||
|
<div class="dp-section-title">Identitas</div>
|
||||||
|
${field('NIK', `<span class="mono">${p.nik}</span>`)}
|
||||||
|
${field('Jenis Kelamin', p.jenis_kelamin === 'L' ? 'Laki-laki' : 'Perempuan')}
|
||||||
|
${field('Umur', `${p.umur||'?'} tahun`)}
|
||||||
|
${field('Tempat Lahir', p.tempat_lahir||'—')}
|
||||||
|
${field('Agama', p.agama||'—')}
|
||||||
|
${field('Status KK', cap(p.status_keluarga))}
|
||||||
|
${field('Perkawinan', cap(p.status_perkawinan))}
|
||||||
|
</div>
|
||||||
|
<div class="dp-section">
|
||||||
|
<div class="dp-section-title">Sosial Ekonomi</div>
|
||||||
|
${field('Pekerjaan', p.pekerjaan||'—')}
|
||||||
|
${field('Penghasilan', rp(p.penghasilan))}
|
||||||
|
${field('Status Ekonomi', bdg(p.status_ekonomi,''))}
|
||||||
|
${field('Pendidikan', p.pendidikan_terakhir?.replace('_','/')||'—')}
|
||||||
|
${field('Kesehatan', bdg(p.kondisi_kesehatan,''))}
|
||||||
|
</div>
|
||||||
|
<div class="dp-section">
|
||||||
|
<div class="dp-section-title">Lokasi</div>
|
||||||
|
${field('Kelurahan', p.kelurahan||'—')}
|
||||||
|
${field('Kecamatan', p.kecamatan||'—')}
|
||||||
|
${field('Alamat', p.alamat||'—')}
|
||||||
|
${field('Rumah Ibadah', p.nama_ri||'—')}
|
||||||
|
</div>
|
||||||
|
<div class="dp-section">
|
||||||
|
<div class="dp-section-title">Riwayat Bantuan (${histori.length}x)</div>
|
||||||
|
${histori.length ? histori.map(h=>`
|
||||||
|
<div style="background:var(--bg4);border-radius:8px;padding:8px 10px;margin-bottom:6px;font-size:12px">
|
||||||
|
<strong>${h.nama_bantuan}</strong><span style="color:var(--muted);font-size:11px;float:right">${h.tanggal?.substring(0,10)}</span>
|
||||||
|
<div style="color:var(--muted);font-size:11px">${h.nama_ri||''} · ${h.jumlah} ${h.satuan||''}</div>
|
||||||
|
</div>`).join('') : '<p style="color:var(--muted);font-size:12px">Belum pernah menerima bantuan</p>'}
|
||||||
|
</div>
|
||||||
|
${App.canEdit() ? `
|
||||||
|
<div style="display:flex;gap:8px;margin-top:8px">
|
||||||
|
<button class="btn btn-primary" style="flex:1" onclick="Bantuan.quickSalur('${p.nik}','${p.nama_lengkap}')">📦 Salurkan Bantuan</button>
|
||||||
|
<button class="btn btn-secondary" onclick="Penduduk.showForm('${p.nik}')">✏️ Edit</button>
|
||||||
|
</div>` : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function field(lbl, val) {
|
||||||
|
return `<div class="dp-field"><span class="dp-field-lbl">${lbl}</span><span class="dp-field-val">${val}</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetail() { ge('detail-panel').classList.remove('open'); }
|
||||||
|
|
||||||
|
return { load, search, applyFilter, showForm, hitungUmur, save, hapus, showDetail, closeDetail };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
const PrioritasPanel = (() => {
|
||||||
|
let all = [];
|
||||||
|
|
||||||
|
const DEMO = [
|
||||||
|
{ nik:'6171010101010006', nama_lengkap:'Maria Magdalena', umur:54, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', kecamatan:'Pontianak Selatan', nama_ri:'Katedral', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI' },
|
||||||
|
{ nik:'6171010101010014', nama_lengkap:'Tan Eng Huat', umur:65, status_ekonomi:'miskin', kondisi_kesehatan:'disabilitas', kecamatan:'Pontianak Kota', nama_ri:'Klenteng', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI' },
|
||||||
|
{ nik:'6171010101010010', nama_lengkap:'Lim Ah Kow', umur:59, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', kecamatan:'Pontianak Kota', nama_ri:'Vihara', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI' },
|
||||||
|
{ nik:'6171010101010002', nama_lengkap:'Siti Rahma', umur:39, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_ringan',kecamatan:'Pontianak Timur', nama_ri:"Masjid Jami'", jumlah_dibantu:2, level_prioritas:'TINGGI' },
|
||||||
|
{ nik:'6171010101010001', nama_lengkap:'Ahmad Suryana', umur:44, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', kecamatan:'Pontianak Timur', nama_ri:"Masjid Jami'", jumlah_dibantu:2, level_prioritas:'SEDANG' },
|
||||||
|
{ nik:'6171010101010005', nama_lengkap:'Dewi Kartika', umur:46, status_ekonomi:'rentan', kondisi_kesehatan:'sehat', kecamatan:'Pontianak Kota', nama_ri:"Masjid Jami'", jumlah_dibantu:0, level_prioritas:'SEDANG' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRIO_CLS = { 'SANGAT TINGGI':'pb-sangat-tinggi', 'TINGGI':'pb-tinggi', 'SEDANG':'pb-sedang', 'RENDAH':'pb-rendah' };
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try { all = await Api.get('/prioritas'); } catch { all = DEMO; }
|
||||||
|
render(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
const tb = ge('tbody-prioritas');
|
||||||
|
if (!data.length) { tb.innerHTML = '<tr><td colspan="10"><div class="empty-state"><div class="es-icon">✅</div><h3>Tidak ada warga prioritas</h3></div></td></tr>'; return; }
|
||||||
|
tb.innerHTML = data.map(p => `
|
||||||
|
<tr>
|
||||||
|
<td class="mono">${p.nik}</td>
|
||||||
|
<td><strong>${p.nama_lengkap}</strong></td>
|
||||||
|
<td>${p.umur} th</td>
|
||||||
|
<td>${bdg(p.status_ekonomi,'')}</td>
|
||||||
|
<td>${bdg(p.kondisi_kesehatan,'')}</td>
|
||||||
|
<td>${p.kecamatan||'—'}</td>
|
||||||
|
<td style="font-size:11px">${p.nama_ri||'—'}</td>
|
||||||
|
<td style="text-align:center">
|
||||||
|
${p.jumlah_dibantu > 0
|
||||||
|
? `<span style="font-weight:700;color:var(--green)">${p.jumlah_dibantu}x</span>`
|
||||||
|
: `<span style="font-weight:700;color:var(--red)">Belum</span>`}
|
||||||
|
</td>
|
||||||
|
<td><span class="prio-badge ${PRIO_CLS[p.level_prioritas]||''}">${p.level_prioritas}</span></td>
|
||||||
|
<td>
|
||||||
|
${App.canEdit() ? `
|
||||||
|
<button class="btn btn-primary btn-xs" onclick="Bantuan.quickSalur('${p.nik}','${p.nama_lengkap}')">📦 Bantu</button>
|
||||||
|
` : '—'}
|
||||||
|
</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCSV() {
|
||||||
|
if (!all.length) { App.toast('Tidak ada data untuk diekspor','warning'); return; }
|
||||||
|
const hdr = ['NIK','Nama','Umur','Ekonomi','Kesehatan','Kecamatan','Rumah Ibadah','Dibantu','Prioritas'];
|
||||||
|
const rows = all.map(p => [p.nik,p.nama_lengkap,p.umur,p.status_ekonomi,p.kondisi_kesehatan,p.kecamatan,p.nama_ri,p.jumlah_dibantu,p.level_prioritas]);
|
||||||
|
const csv = '\uFEFF' + [hdr,...rows].map(r => r.map(v => `"${v||''}"`).join(',')).join('\n');
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8'}));
|
||||||
|
a.download = `prioritas-bantuan-${new Date().toISOString().substring(0,10)}.csv`;
|
||||||
|
a.click();
|
||||||
|
App.toast('File CSV berhasil diunduh ✅','success');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { load, exportCSV };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
const RI = (() => {
|
||||||
|
let all = [], editId = null;
|
||||||
|
|
||||||
|
const DEMO = [
|
||||||
|
{ id:1, nama:"Masjid Jami' Pontianak", jenis:'Masjid', kecamatan:'Pontianak Timur', kontak:'0561-732001', radius_meter:350, jumlah_kk:3, jumlah_penduduk:8, lat:-0.02596, lng:109.34307 },
|
||||||
|
{ id:2, nama:'Gereja Katedral Pontianak', jenis:'Gereja', kecamatan:'Pontianak Selatan', kontak:'0561-766000', radius_meter:300, jumlah_kk:2, jumlah_penduduk:5, lat:-0.03200, lng:109.33200 },
|
||||||
|
{ id:3, nama:'Vihara Bodhisattva Karaniya Metta', jenis:'Vihara', kecamatan:'Pontianak Kota', kontak:'0561-733456', radius_meter:300, jumlah_kk:2, jumlah_penduduk:4, lat:-0.02100, lng:109.33800 },
|
||||||
|
{ id:4, nama:'Klenteng Kwan Ti Miau', jenis:'Klenteng', kecamatan:'Pontianak Kota', kontak:'0561-740123', radius_meter:280, jumlah_kk:1, jumlah_penduduk:2, lat:-0.01900, lng:109.34100 },
|
||||||
|
{ id:5, nama:'Masjid Mujahidin Pontianak', jenis:'Masjid', kecamatan:'Pontianak Selatan', kontak:'0561-764567', radius_meter:400, jumlah_kk:1, jumlah_penduduk:2, lat:-0.04800, lng:109.32500 },
|
||||||
|
{ id:6, nama:'Gereja GPIB Immanuel', jenis:'Gereja', kecamatan:'Pontianak Kota', kontak:'0561-735890', radius_meter:300, jumlah_kk:1, jumlah_penduduk:1, lat:-0.02800, lng:109.34500 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const JENIS_EMOJI = { Masjid:'🕌', Gereja:'⛪', Vihara:'🛕', Pura:'🛕', Klenteng:'🏮', Lainnya:'🏛️' };
|
||||||
|
const JENIS_COLOR = { Masjid:'#06d6a0', Gereja:'#4cc9f0', Vihara:'#ffd166', Pura:'#f97316', Klenteng:'#b48eff', Lainnya:'#6b7a99' };
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try { all = await Api.get('/rumah-ibadah'); } catch { all = DEMO; }
|
||||||
|
render(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
ge('ri-grid').innerHTML = data.map(r => `
|
||||||
|
<div class="ri-card" style="border-top:3px solid ${JENIS_COLOR[r.jenis]||'#6b7a99'}">
|
||||||
|
<div class="ri-icon-big">${JENIS_EMOJI[r.jenis]||'🏛️'}</div>
|
||||||
|
<div class="ri-name">${r.nama}</div>
|
||||||
|
<div class="ri-type">${r.jenis} · ${r.kecamatan||'—'}</div>
|
||||||
|
<div class="ri-stats">
|
||||||
|
<div class="ri-stat">
|
||||||
|
<div class="ri-stat-num" style="color:${JENIS_COLOR[r.jenis]}">${r.jumlah_kk||0}</div>
|
||||||
|
<div class="ri-stat-lbl">KK Binaan</div>
|
||||||
|
</div>
|
||||||
|
<div class="ri-stat">
|
||||||
|
<div class="ri-stat-num" style="color:var(--blue)">${r.jumlah_penduduk||0}</div>
|
||||||
|
<div class="ri-stat-lbl">Jiwa</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted);margin-bottom:10px">
|
||||||
|
📞 ${r.kontak||'—'} · ⭕ Radius ${r.radius_meter}m
|
||||||
|
</div>
|
||||||
|
${App.canEdit() ? `
|
||||||
|
<div style="display:flex;gap:6px">
|
||||||
|
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="RI.showForm(${r.id})">✏️ Edit</button>
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="RI.hapus(${r.id})">🗑</button>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showForm(id = null) {
|
||||||
|
editId = id;
|
||||||
|
ge('mri-title').textContent = id ? '✏️ Edit Rumah Ibadah' : '🕌 Tambah Rumah Ibadah';
|
||||||
|
if (id) {
|
||||||
|
const r = all.find(x => x.id == id) || {};
|
||||||
|
sv('fri-nama', r.nama); sv('fri-jenis', r.jenis);
|
||||||
|
sv('fri-radius', r.radius_meter||300); sv('fri-lat', r.lat);
|
||||||
|
sv('fri-lng', r.lng); sv('fri-kec', r.kecamatan);
|
||||||
|
sv('fri-kontak', r.kontak); sv('fri-alamat', r.alamat);
|
||||||
|
} else {
|
||||||
|
['fri-nama','fri-kec','fri-kontak','fri-alamat'].forEach(i => sv(i,''));
|
||||||
|
sv('fri-radius','300'); sv('fri-lat',''); sv('fri-lng','');
|
||||||
|
}
|
||||||
|
App.showModal('modal-ri');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!gv('fri-nama')) { App.toast('Nama wajib diisi','error'); return; }
|
||||||
|
if (!gv('fri-lat') || !gv('fri-lng')) { App.toast('Koordinat (lat/lng) wajib diisi','error'); return; }
|
||||||
|
const body = {
|
||||||
|
nama: gv('fri-nama'), jenis: gv('fri-jenis'),
|
||||||
|
radius_meter: +gv('fri-radius')||300,
|
||||||
|
lat: +gv('fri-lat'), lng: +gv('fri-lng'),
|
||||||
|
kecamatan: gv('fri-kec'), kontak: gv('fri-kontak'), alamat: gv('fri-alamat'),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (editId) { await Api.put(`/rumah-ibadah/${editId}`, body); App.toast('Data diperbarui ✅','success'); }
|
||||||
|
else { await Api.post('/rumah-ibadah', body); App.toast('Rumah ibadah ditambahkan ✅','success'); }
|
||||||
|
App.closeModal('modal-ri'); load();
|
||||||
|
} catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hapus(id) {
|
||||||
|
if (!confirm('Nonaktifkan rumah ibadah ini?')) return;
|
||||||
|
try { await Api.delete(`/rumah-ibadah/${id}`); App.toast('Dinonaktifkan','success'); load(); }
|
||||||
|
catch(e) { App.toast(e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { load, showForm, save, hapus, getAll: () => all };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
define('APP_NAME', 'WebGIS Kemiskinan Pontianak');
|
||||||
|
define('APP_VERSION', '3.0.0');
|
||||||
|
define('TIMEZONE', 'Asia/Makassar');
|
||||||
|
define('ROLE_ADMIN', 1);
|
||||||
|
define('ROLE_PENGURUS', 2);
|
||||||
|
define('ROLE_PIMPINAN', 3);
|
||||||
|
define('ROLE_MASYARAKAT', 4);
|
||||||
|
define('SESSION_NAME', 'webgis_session');
|
||||||
|
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
|
||||||
|
date_default_timezone_set(TIMEZONE);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
define('DB_HOST', getenv('DB_HOST'));
|
||||||
|
define('DB_NAME', getenv('DB_NAME'));
|
||||||
|
define('DB_USER', getenv('DB_USER'));
|
||||||
|
define('DB_PASS', getenv('DB_PASSWORD'));
|
||||||
|
define('DB_CHAR', getenv('DB_CHAR') ?: 'utf8mb4');
|
||||||
|
|
||||||
|
function getDB(): PDO {
|
||||||
|
static $pdo = null;
|
||||||
|
if ($pdo === null) {
|
||||||
|
$dsn = "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHAR;
|
||||||
|
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
// includes/helpers.php
|
||||||
|
require_once __DIR__ . '/../config/app.php';
|
||||||
|
require_once __DIR__ . '/../config/database.php';
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
HTTP Response helpers
|
||||||
|
============================================================ */
|
||||||
|
function jsonSuccess($data = [], string $message = 'OK', int $code = 200): void {
|
||||||
|
http_response_code($code);
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode(['success' => true, 'message' => $message, 'data' => $data], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonError(string $message = 'Error', int $code = 400, array $errors = []): void {
|
||||||
|
http_response_code($code);
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode(['success' => false, 'message' => $message, 'errors' => $errors], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Input sanitization
|
||||||
|
============================================================ */
|
||||||
|
function sanitize(string $val): string {
|
||||||
|
return htmlspecialchars(strip_tags(trim($val)), ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBody(): array {
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$data = json_decode($raw, true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function required(array $data, array $fields): array {
|
||||||
|
$errors = [];
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
if (!isset($data[$f]) || trim((string)$data[$f]) === '') {
|
||||||
|
$errors[] = "Field '$f' wajib diisi";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Session / Auth
|
||||||
|
============================================================ */
|
||||||
|
function startSession(): void {
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_name(SESSION_NAME);
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentUser(): ?array {
|
||||||
|
startSession();
|
||||||
|
return $_SESSION['user'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAuth(array $allowedRoles = []): array {
|
||||||
|
$user = currentUser();
|
||||||
|
if (!$user) jsonError('Unauthorized - silakan login', 401);
|
||||||
|
if (!empty($allowedRoles) && !in_array($user['id_role'], $allowedRoles)) {
|
||||||
|
jsonError('Forbidden - hak akses tidak cukup', 403);
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
function login(string $username, string $password): array|false {
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT u.*, r.nama_role FROM user u JOIN role r ON u.id_role = r.id
|
||||||
|
WHERE u.username = ? AND u.aktif = 1 LIMIT 1"
|
||||||
|
);
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
if (!$user || !password_verify($password, $user['password_hash'])) return false;
|
||||||
|
|
||||||
|
// Update last_login
|
||||||
|
$db->prepare("UPDATE user SET last_login = NOW() WHERE id = ?")->execute([$user['id']]);
|
||||||
|
unset($user['password_hash']);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Activity Log
|
||||||
|
============================================================ */
|
||||||
|
function logActivity(string $aksi, string $tabel = '', string $idRecord = '',
|
||||||
|
array $dataLama = [], array $dataBaru = []): void {
|
||||||
|
try {
|
||||||
|
$user = currentUser();
|
||||||
|
$db = getDB();
|
||||||
|
$db->prepare(
|
||||||
|
"INSERT INTO log_aktivitas (id_user, aksi, tabel, id_record, data_lama, data_baru, ip_address)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
)->execute([
|
||||||
|
$user['id'] ?? null,
|
||||||
|
$aksi, $tabel, $idRecord,
|
||||||
|
$dataLama ? json_encode($dataLama, JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
$dataBaru ? json_encode($dataBaru, JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
$_SERVER['REMOTE_ADDR'] ?? null,
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) { /* silent */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Haversine distance (km)
|
||||||
|
============================================================ */
|
||||||
|
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||||
|
$R = 6371;
|
||||||
|
$dLat = deg2rad($lat2 - $lat1);
|
||||||
|
$dLng = deg2rad($lng2 - $lng1);
|
||||||
|
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
|
||||||
|
return $R * 2 * asin(sqrt($a));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Pagination helper
|
||||||
|
============================================================ */
|
||||||
|
function paginate(array $data, int $page, int $perPage = 20): array {
|
||||||
|
$total = count($data);
|
||||||
|
$pages = (int)ceil($total / $perPage);
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
return [
|
||||||
|
'items' => array_slice($data, $offset, $perPage),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'pages' => $pages,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Validasi umur vs pendidikan
|
||||||
|
============================================================ */
|
||||||
|
function validasiPendidikan(int $umur, string $pendidikanTerakhir, string $statusPendidikan): array {
|
||||||
|
$warnings = [];
|
||||||
|
if ($umur >= 7 && $umur <= 15) {
|
||||||
|
if ($statusPendidikan === 'tidak_sekolah') {
|
||||||
|
$warnings[] = "Anak usia $umur tahun seharusnya masih sekolah (wajib belajar 9 tahun)";
|
||||||
|
}
|
||||||
|
if (!in_array($pendidikanTerakhir, ['SD','SMP','tidak_sekolah'])) {
|
||||||
|
$warnings[] = "Pendidikan terakhir tidak sesuai untuk usia $umur tahun";
|
||||||
|
}
|
||||||
|
} elseif ($umur >= 16 && $umur <= 18) {
|
||||||
|
if ($statusPendidikan === 'tidak_sekolah' && in_array($pendidikanTerakhir, ['tidak_sekolah','SD'])) {
|
||||||
|
$warnings[] = "Remaja usia $umur tahun disarankan melanjutkan ke SMA/SMK";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Upload foto helper
|
||||||
|
============================================================ */
|
||||||
|
function uploadFoto(array $file, string $subfolder = 'foto_laporan'): string|false {
|
||||||
|
$allowed = ['image/jpeg','image/png','image/webp'];
|
||||||
|
if (!in_array($file['type'], $allowed)) return false;
|
||||||
|
if ($file['size'] > 5 * 1024 * 1024) return false;
|
||||||
|
|
||||||
|
$dir = UPLOAD_DIR . $subfolder . '/';
|
||||||
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||||
|
|
||||||
|
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||||
|
$filename = uniqid('img_', true) . '.' . strtolower($ext);
|
||||||
|
$dest = $dir . $filename;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $dest)) return false;
|
||||||
|
return $subfolder . '/' . $filename;
|
||||||
|
}
|
||||||
@@ -0,0 +1,812 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGIS Kemiskinan — Kota Pontianak</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🕌</text></svg>">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"/>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<header id="app-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<button id="btn-toggle-sidebar" class="icon-btn" title="Toggle Sidebar">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-logo">🕌</div>
|
||||||
|
<div>
|
||||||
|
<div class="brand-title">WebGIS Kemiskinan Pontianak</div>
|
||||||
|
<div class="brand-sub">Sistem Pengentasan Kemiskinan · Berbasis Rumah Ibadah</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="stat-pills">
|
||||||
|
<span class="spill red" id="hp-miskin" title="Klik lihat data miskin" onclick="App.switchPanel('penduduk');setTimeout(()=>document.getElementById('pf-ekonomi').value='miskin'&&Penduduk.applyFilter(),300)">
|
||||||
|
<span class="spill-dot"></span><span id="hp-miskin-n">—</span> Miskin
|
||||||
|
</span>
|
||||||
|
<span class="spill yellow" id="hp-rentan"><span class="spill-dot"></span><span id="hp-rentan-n">—</span> Rentan</span>
|
||||||
|
<span class="spill green" id="hp-mampu"><span class="spill-dot"></span><span id="hp-mampu-n">—</span> Mampu</span>
|
||||||
|
<span class="spill blue" id="hp-lap" title="Laporan pending" onclick="App.switchPanel('laporan')">
|
||||||
|
<span class="spill-dot"></span><span id="hp-lap-n">—</span> Laporan
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="user-info" class="user-chip hidden" onclick="App.toggleUserMenu()">
|
||||||
|
<div class="user-avatar" id="user-avatar-el">👤</div>
|
||||||
|
<span class="user-name" id="user-name-el">—</span>
|
||||||
|
<span class="role-badge" id="user-role-el">—</span>
|
||||||
|
<button class="btn btn-danger btn-sm" style="margin-left:4px" onclick="event.stopPropagation();App.logout()">Keluar</button>
|
||||||
|
</div>
|
||||||
|
<button id="btn-login" class="btn btn-primary" onclick="App.showModal('modal-login')">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M15 3h6v18h-6M10 17l5-5-5-5M15 12H3"/></svg>
|
||||||
|
Masuk
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- LAYOUT -->
|
||||||
|
<div id="app-layout">
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside id="sidebar">
|
||||||
|
<div class="sb-section">
|
||||||
|
<div class="sb-label">Menu Utama</div>
|
||||||
|
<button class="nav-btn active" data-panel="dashboard" onclick="App.switchPanel('dashboard')"><span class="nav-icon">📊</span>Dashboard</button>
|
||||||
|
<button class="nav-btn" data-panel="peta" onclick="App.switchPanel('peta')"><span class="nav-icon">🗺️</span>Peta Sebaran</button>
|
||||||
|
</div>
|
||||||
|
<div class="sb-section">
|
||||||
|
<div class="sb-label">Data</div>
|
||||||
|
<button class="nav-btn" data-panel="penduduk" onclick="App.switchPanel('penduduk')"><span class="nav-icon">👥</span>Penduduk</button>
|
||||||
|
<button class="nav-btn" data-panel="keluarga" onclick="App.switchPanel('keluarga')"><span class="nav-icon">🏠</span>Keluarga (KK)</button>
|
||||||
|
<button class="nav-btn" data-panel="ri" onclick="App.switchPanel('ri')"><span class="nav-icon">🕌</span>Rumah Ibadah</button>
|
||||||
|
</div>
|
||||||
|
<div class="sb-section">
|
||||||
|
<div class="sb-label">Bantuan Sosial</div>
|
||||||
|
<button class="nav-btn" data-panel="bantuan" onclick="App.switchPanel('bantuan')"><span class="nav-icon">🎁</span>Manaj. Bantuan</button>
|
||||||
|
<button class="nav-btn" data-panel="prioritas" onclick="App.switchPanel('prioritas')"><span class="nav-icon">🔥</span>Prioritas</button>
|
||||||
|
<button class="nav-btn" data-panel="laporan" onclick="App.switchPanel('laporan')"><span class="nav-icon">🚨</span>Laporan<span class="nav-badge hidden" id="nb-laporan">0</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="sb-section">
|
||||||
|
<div class="sb-label">Sistem</div>
|
||||||
|
<button class="nav-btn" data-panel="chat" onclick="App.switchPanel('chat')"><span class="nav-icon">💬</span>Koordinasi</button>
|
||||||
|
<button class="nav-btn" data-panel="log" onclick="App.switchPanel('log')"><span class="nav-icon">📋</span>Log Aktivitas</button>
|
||||||
|
</div>
|
||||||
|
<div class="nav-divider"></div>
|
||||||
|
<button class="nav-btn nav-kirim" id="nav-kirim-laporan" onclick="LaporanPanel.showKirimModal()"><span class="nav-icon">📣</span>Kirim Laporan</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- MAIN -->
|
||||||
|
<main id="main-content">
|
||||||
|
|
||||||
|
<!-- ═══ DASHBOARD ═══ -->
|
||||||
|
<section id="panel-dashboard" class="panel active">
|
||||||
|
<div class="page-hdr">
|
||||||
|
<h2><span class="ph-icon">📊</span>Dashboard Kemiskinan Pontianak</h2>
|
||||||
|
<div class="page-actions">
|
||||||
|
<span id="dash-updated" style="font-size:11px;color:var(--muted)">—</span>
|
||||||
|
<button class="btn btn-secondary" onclick="Dashboard.load()">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-grid" id="dash-stats">
|
||||||
|
<div class="stat-card red" onclick="App.switchPanel('penduduk')">
|
||||||
|
<div class="stat-num" id="d-miskin">—</div>
|
||||||
|
<div class="stat-label">Penduduk Miskin</div>
|
||||||
|
<div class="stat-bg-icon">😔</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card yellow">
|
||||||
|
<div class="stat-num" id="d-rentan">—</div>
|
||||||
|
<div class="stat-label">Rentan / Hampir Miskin</div>
|
||||||
|
<div class="stat-bg-icon">⚠️</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card green">
|
||||||
|
<div class="stat-num" id="d-mampu">—</div>
|
||||||
|
<div class="stat-label">Penduduk Mampu</div>
|
||||||
|
<div class="stat-bg-icon">✅</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card blue" onclick="App.switchPanel('keluarga')">
|
||||||
|
<div class="stat-num" id="d-kk">—</div>
|
||||||
|
<div class="stat-label">Kartu Keluarga</div>
|
||||||
|
<div class="stat-bg-icon">🏠</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card orange" onclick="App.switchPanel('prioritas')">
|
||||||
|
<div class="stat-num" id="d-belum">—</div>
|
||||||
|
<div class="stat-label">Belum Pernah Dibantu</div>
|
||||||
|
<div class="stat-bg-icon">📦</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card purple" onclick="App.switchPanel('laporan')">
|
||||||
|
<div class="stat-num" id="d-darurat">—</div>
|
||||||
|
<div class="stat-label">Laporan Aktif</div>
|
||||||
|
<div class="stat-bg-icon">🚨</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dash-grid scrollable">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">🔥 Prioritas Darurat <span style="font-size:11px;font-weight:400;color:var(--muted);margin-left:auto">klik untuk bantu</span></div>
|
||||||
|
<div class="prio-list" id="d-prio-list"><div class="empty-state"><div class="es-icon">⏳</div><p>Memuat data…</p></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">🏘️ Sebaran per Kecamatan</div>
|
||||||
|
<div class="bar-list" id="d-kec-chart"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">🕌 Beban per Rumah Ibadah</div>
|
||||||
|
<div class="bar-list" id="d-ri-chart"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">📦 Tren Bantuan 6 Bulan</div>
|
||||||
|
<div class="bar-list" id="d-trend-chart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ PETA ═══ -->
|
||||||
|
<section id="panel-peta" class="panel panel-map">
|
||||||
|
<div class="map-toolbar">
|
||||||
|
<h2>🗺️ Peta Sebaran</h2>
|
||||||
|
<select class="map-filter-sel" id="map-status-filter" onchange="MapApp.applyFilter()">
|
||||||
|
<option value="">Semua Status</option>
|
||||||
|
<option value="miskin">Hanya Miskin</option>
|
||||||
|
<option value="rentan">Hanya Rentan</option>
|
||||||
|
<option value="mampu">Hanya Mampu</option>
|
||||||
|
</select>
|
||||||
|
<div class="layer-toggles">
|
||||||
|
<button class="layer-btn active-green" id="lb-penduduk" onclick="MapApp.toggleLayer('penduduk',this)">👥 Penduduk</button>
|
||||||
|
<button class="layer-btn active-blue" id="lb-ri" onclick="MapApp.toggleLayer('ri',this)">🕌 Rumah Ibadah</button>
|
||||||
|
<button class="layer-btn" id="lb-laporan" onclick="MapApp.toggleLayer('laporan',this)">🚨 Laporan</button>
|
||||||
|
<button class="layer-btn active-purple" id="lb-radius" onclick="MapApp.toggleLayer('radius',this)">⭕ Radius</button>
|
||||||
|
</div>
|
||||||
|
<div class="legend">
|
||||||
|
<span class="legend-item"><span class="legend-dot" style="background:var(--red)"></span>Miskin</span>
|
||||||
|
<span class="legend-item"><span class="legend-dot" style="background:var(--yellow)"></span>Rentan</span>
|
||||||
|
<span class="legend-item"><span class="legend-dot" style="background:var(--green)"></span>Mampu</span>
|
||||||
|
<span class="legend-item"><span class="legend-dot" style="background:var(--purple)"></span>Laporan</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="map"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ PENDUDUK ═══ -->
|
||||||
|
<section id="panel-penduduk" class="panel">
|
||||||
|
<div class="page-hdr">
|
||||||
|
<h2><span class="ph-icon">👥</span>Data Penduduk</h2>
|
||||||
|
<div class="page-actions">
|
||||||
|
<button class="btn btn-primary" id="btn-tambah-penduduk" onclick="Penduduk.showForm()">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
|
Tambah Penduduk
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<div class="search-wrap">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
<input class="search-input" type="search" id="pf-search" placeholder="Cari nama atau NIK…" oninput="Penduduk.search(this.value)">
|
||||||
|
</div>
|
||||||
|
<select class="filter-sel" id="pf-ekonomi" onchange="Penduduk.applyFilter()">
|
||||||
|
<option value="">Semua Status Ekonomi</option>
|
||||||
|
<option value="miskin">🔴 Miskin</option>
|
||||||
|
<option value="rentan">🟡 Rentan</option>
|
||||||
|
<option value="mampu">🟢 Mampu</option>
|
||||||
|
</select>
|
||||||
|
<select class="filter-sel" id="pf-kesehatan" onchange="Penduduk.applyFilter()">
|
||||||
|
<option value="">Semua Kondisi</option>
|
||||||
|
<option value="sehat">Sehat</option>
|
||||||
|
<option value="sakit_ringan">Sakit Ringan</option>
|
||||||
|
<option value="sakit_berat">Sakit Berat</option>
|
||||||
|
<option value="disabilitas">Disabilitas</option>
|
||||||
|
</select>
|
||||||
|
<span id="pf-count" style="font-size:11px;color:var(--muted);white-space:nowrap"></span>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-tbl">
|
||||||
|
<thead><tr>
|
||||||
|
<th>NIK</th><th>Nama Lengkap</th><th>Umur</th>
|
||||||
|
<th>Status KK</th><th>Pekerjaan</th><th>Penghasilan</th>
|
||||||
|
<th>Ekonomi</th><th>Kesehatan</th><th>Pendidikan</th><th>Aksi</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody id="tbody-penduduk"><tr><td colspan="10" style="text-align:center;padding:32px;color:var(--muted)">Memuat data…</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ KELUARGA ═══ -->
|
||||||
|
<section id="panel-keluarga" class="panel">
|
||||||
|
<div class="page-hdr">
|
||||||
|
<h2><span class="ph-icon">🏠</span>Data Keluarga (KK)</h2>
|
||||||
|
<div class="page-actions">
|
||||||
|
<button class="btn btn-primary" id="btn-tambah-kk" onclick="Keluarga.showForm()">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
|
Tambah KK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<div class="search-wrap">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
<input class="search-input" type="search" id="kf-search" placeholder="Cari nama kepala / No. KK…" oninput="Keluarga.search(this.value)">
|
||||||
|
</div>
|
||||||
|
<select class="filter-sel" id="kf-ekonomi" onchange="Keluarga.applyFilter()">
|
||||||
|
<option value="">Semua Status</option>
|
||||||
|
<option value="miskin">🔴 Miskin</option>
|
||||||
|
<option value="rentan">🟡 Rentan</option>
|
||||||
|
<option value="mampu">🟢 Mampu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-tbl">
|
||||||
|
<thead><tr>
|
||||||
|
<th>No. KK</th><th>Kepala Keluarga</th><th>Kecamatan</th>
|
||||||
|
<th>Anggota</th><th>Total Penghasilan</th><th>Rumah Ibadah</th>
|
||||||
|
<th>Status</th><th>Bantuan</th><th>Aksi</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody id="tbody-keluarga"><tr><td colspan="9" style="text-align:center;padding:32px;color:var(--muted)">Memuat data…</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ BANTUAN ═══ -->
|
||||||
|
<section id="panel-bantuan" class="panel">
|
||||||
|
<div class="page-hdr">
|
||||||
|
<h2><span class="ph-icon">🎁</span>Manajemen Bantuan</h2>
|
||||||
|
<div class="page-actions">
|
||||||
|
<button class="btn btn-secondary" id="btn-tambah-bantuan" onclick="Bantuan.showFormJenis()">+ Jenis Baru</button>
|
||||||
|
<button class="btn btn-primary" id="btn-salur-bantuan" onclick="Bantuan.showFormSalur()">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
|
Salurkan Bantuan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="two-col scrollable">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">📋 Jenis Bantuan Tersedia</div>
|
||||||
|
<div id="bantuan-jenis-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">📜 Riwayat Penyaluran Terbaru</div>
|
||||||
|
<div id="bantuan-histori-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ LAPORAN ═══ -->
|
||||||
|
<section id="panel-laporan" class="panel">
|
||||||
|
<div class="page-hdr">
|
||||||
|
<h2><span class="ph-icon">🚨</span>Laporan Masyarakat</h2>
|
||||||
|
<div class="page-actions">
|
||||||
|
<select class="filter-sel" id="lf-status" onchange="LaporanPanel.load()">
|
||||||
|
<option value="">Semua Status</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="diverifikasi">Diverifikasi</option>
|
||||||
|
<option value="diproses">Diproses</option>
|
||||||
|
<option value="selesai">Selesai</option>
|
||||||
|
</select>
|
||||||
|
<select class="filter-sel" id="lf-urgensi" onchange="LaporanPanel.load()">
|
||||||
|
<option value="">Semua Urgensi</option>
|
||||||
|
<option value="darurat">🔴 Darurat</option>
|
||||||
|
<option value="tinggi">🟠 Tinggi</option>
|
||||||
|
<option value="sedang">🟡 Sedang</option>
|
||||||
|
<option value="rendah">🟢 Rendah</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-primary" onclick="LaporanPanel.showKirimModal()">📣 Kirim Laporan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="laporan-grid scrollable" id="laporan-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ RUMAH IBADAH ═══ -->
|
||||||
|
<section id="panel-ri" class="panel">
|
||||||
|
<div class="page-hdr">
|
||||||
|
<h2><span class="ph-icon">🕌</span>Rumah Ibadah</h2>
|
||||||
|
<div class="page-actions">
|
||||||
|
<button class="btn btn-primary" id="btn-tambah-ri" onclick="RI.showForm()">+ Tambah</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ri-grid scrollable" id="ri-grid"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ PRIORITAS ═══ -->
|
||||||
|
<section id="panel-prioritas" class="panel">
|
||||||
|
<div class="page-hdr">
|
||||||
|
<h2><span class="ph-icon">🔥</span>Daftar Prioritas Bantuan</h2>
|
||||||
|
<div class="page-actions">
|
||||||
|
<button class="btn btn-secondary" onclick="PrioritasPanel.load()">⟳ Refresh</button>
|
||||||
|
<button class="btn btn-success" onclick="PrioritasPanel.exportCSV()">⬇ Export CSV</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:var(--muted);margin-bottom:14px;flex-shrink:0">
|
||||||
|
Urutan: <strong style="color:var(--red)">SANGAT TINGGI</strong> (miskin+sakit) →
|
||||||
|
<strong style="color:var(--orange)">TINGGI</strong> (miskin+belum dibantu) →
|
||||||
|
<strong style="color:var(--yellow)">SEDANG</strong> → <strong style="color:var(--green)">RENDAH</strong>
|
||||||
|
</p>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-tbl">
|
||||||
|
<thead><tr><th>NIK</th><th>Nama</th><th>Umur</th><th>Ekonomi</th><th>Kesehatan</th><th>Kecamatan</th><th>Rumah Ibadah</th><th>Dibantu</th><th>Prioritas</th><th>Aksi</th></tr></thead>
|
||||||
|
<tbody id="tbody-prioritas"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ CHAT ═══ -->
|
||||||
|
<section id="panel-chat" class="panel">
|
||||||
|
<div class="page-hdr"><h2><span class="ph-icon">💬</span>Koordinasi Pengurus</h2></div>
|
||||||
|
<div class="chat-rooms" id="chat-rooms">
|
||||||
|
<button class="room-btn active" data-room="" onclick="Chat.loadRoom('',this)"># Umum</button>
|
||||||
|
</div>
|
||||||
|
<div class="chat-messages" id="chat-messages"><div class="chat-empty">⏳ Memuat pesan…</div></div>
|
||||||
|
<div class="chat-input-row">
|
||||||
|
<input class="form-input" type="text" id="chat-input" placeholder="Ketik pesan koordinasi… (Enter untuk kirim)" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();Chat.send()}">
|
||||||
|
<button class="btn btn-primary" onclick="Chat.send()">Kirim ➤</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ LOG ═══ -->
|
||||||
|
<section id="panel-log" class="panel">
|
||||||
|
<div class="page-hdr">
|
||||||
|
<h2><span class="ph-icon">📋</span>Log Aktivitas Sistem</h2>
|
||||||
|
<button class="btn btn-secondary" onclick="LogPanel.load()">⟳ Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-tbl">
|
||||||
|
<thead><tr><th>Waktu</th><th>User</th><th>Aksi</th><th>Tabel</th><th>ID Record</th><th>IP</th></tr></thead>
|
||||||
|
<tbody id="tbody-log"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════ MODALS ══════════════════════ -->
|
||||||
|
|
||||||
|
<!-- LOGIN -->
|
||||||
|
<div id="modal-login" class="modal-overlay hidden">
|
||||||
|
<div class="modal" style="max-width:420px">
|
||||||
|
<div class="modal-hdr">
|
||||||
|
<h3>🔐 Masuk ke Sistem</h3>
|
||||||
|
<button class="btn-close-modal" onclick="App.closeModal('modal-login')">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="info-box" style="margin-bottom:16px">
|
||||||
|
<b>Akun Demo:</b><br>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace;font-size:11px">
|
||||||
|
admin / admin123 → Administrator<br>
|
||||||
|
pengurus_jami / admin123 → Pengurus RI<br>
|
||||||
|
pimpinan / admin123 → Pimpinan (read-only)<br>
|
||||||
|
warga1 / admin123 → Masyarakat
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input class="form-input" type="text" id="login-user" placeholder="admin" onkeydown="if(event.key==='Enter')document.getElementById('login-pass').focus()">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input class="form-input" type="password" id="login-pass" placeholder="••••••••" onkeydown="if(event.key==='Enter')App.doLogin()">
|
||||||
|
</div>
|
||||||
|
<div id="login-err" class="alert-err hidden"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="App.closeModal('modal-login')">Batal</button>
|
||||||
|
<button id="btn-masuk-submit" class="btn btn-primary" onclick="App.doLogin()">Masuk →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PENDUDUK FORM -->
|
||||||
|
<div id="modal-penduduk" class="modal-overlay hidden">
|
||||||
|
<div class="modal modal-lg">
|
||||||
|
<div class="modal-hdr">
|
||||||
|
<h3 id="mp-title">➕ Tambah Penduduk</h3>
|
||||||
|
<button class="btn-close-modal" onclick="App.closeModal('modal-penduduk')">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-section-title">Identitas</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">NIK (16 digit) *</label>
|
||||||
|
<input class="form-input" type="text" id="fp-nik" maxlength="16" placeholder="6171…">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kartu Keluarga *</label>
|
||||||
|
<select class="form-select" id="fp-kk"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nama Lengkap *</label>
|
||||||
|
<input class="form-input" type="text" id="fp-nama" placeholder="Nama sesuai KTP">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Jenis Kelamin</label>
|
||||||
|
<select class="form-select" id="fp-jkel">
|
||||||
|
<option value="L">Laki-laki</option>
|
||||||
|
<option value="P">Perempuan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Tanggal Lahir *</label>
|
||||||
|
<input class="form-input" type="date" id="fp-tgl" onchange="Penduduk.hitungUmur()">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Umur (otomatis)</label>
|
||||||
|
<input class="form-input readonly" type="text" id="fp-umur" readonly placeholder="Isi tanggal lahir dulu">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Tempat Lahir</label>
|
||||||
|
<input class="form-input" type="text" id="fp-tempat" placeholder="Kota kelahiran">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Agama</label>
|
||||||
|
<select class="form-select" id="fp-agama">
|
||||||
|
<option>Islam</option><option>Kristen</option><option>Katolik</option>
|
||||||
|
<option>Hindu</option><option>Buddha</option><option>Konghucu</option><option>Lainnya</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-divider"></div>
|
||||||
|
<div class="form-section-title">Status & Pekerjaan</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Status dalam KK</label>
|
||||||
|
<select class="form-select" id="fp-status-kk">
|
||||||
|
<option value="kepala">Kepala Keluarga</option>
|
||||||
|
<option value="istri">Istri/Suami</option>
|
||||||
|
<option value="anak">Anak</option>
|
||||||
|
<option value="menantu">Menantu</option>
|
||||||
|
<option value="cucu">Cucu</option>
|
||||||
|
<option value="orang_tua">Orang Tua</option>
|
||||||
|
<option value="lainnya">Lainnya</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Status Perkawinan</label>
|
||||||
|
<select class="form-select" id="fp-kawin">
|
||||||
|
<option value="belum_kawin">Belum Kawin</option>
|
||||||
|
<option value="kawin">Kawin</option>
|
||||||
|
<option value="cerai_hidup">Cerai Hidup</option>
|
||||||
|
<option value="cerai_mati">Cerai Mati</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Status Hidup</label>
|
||||||
|
<select class="form-select" id="fp-hidup">
|
||||||
|
<option value="hidup">Hidup</option>
|
||||||
|
<option value="meninggal">Meninggal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Status Ekonomi</label>
|
||||||
|
<select class="form-select" id="fp-ekonomi">
|
||||||
|
<option value="miskin">🔴 Miskin</option>
|
||||||
|
<option value="rentan">🟡 Rentan</option>
|
||||||
|
<option value="mampu">🟢 Mampu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Pekerjaan</label>
|
||||||
|
<input class="form-input" type="text" id="fp-pekerjaan" placeholder="Buruh, Pedagang, dll">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Penghasilan/Bulan (Rp)</label>
|
||||||
|
<input class="form-input" type="number" id="fp-penghasilan" min="0" placeholder="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-divider"></div>
|
||||||
|
<div class="form-section-title">Pendidikan & Kesehatan</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Pendidikan Terakhir</label>
|
||||||
|
<select class="form-select" id="fp-pendidikan">
|
||||||
|
<option value="tidak_sekolah">Tidak Sekolah</option>
|
||||||
|
<option value="SD">SD/Sederajat</option>
|
||||||
|
<option value="SMP">SMP/Sederajat</option>
|
||||||
|
<option value="SMA_SMK">SMA/SMK/Sederajat</option>
|
||||||
|
<option value="D1_D3">D1/D2/D3</option>
|
||||||
|
<option value="S1">S1/D4</option>
|
||||||
|
<option value="S2_S3">S2/S3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Status Pendidikan</label>
|
||||||
|
<select class="form-select" id="fp-stat-pend">
|
||||||
|
<option value="sekolah">Masih Sekolah</option>
|
||||||
|
<option value="lulus">Lulus</option>
|
||||||
|
<option value="tidak_sekolah">Tidak Sekolah</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kondisi Kesehatan</label>
|
||||||
|
<select class="form-select" id="fp-kesehatan">
|
||||||
|
<option value="sehat">✅ Sehat</option>
|
||||||
|
<option value="sakit_ringan">🟡 Sakit Ringan</option>
|
||||||
|
<option value="sakit_berat">🔴 Sakit Berat</option>
|
||||||
|
<option value="disabilitas">🟣 Disabilitas</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Keterangan Kesehatan</label>
|
||||||
|
<input class="form-input" type="text" id="fp-ket-sehat" placeholder="Jika ada catatan khusus">
|
||||||
|
</div>
|
||||||
|
<div class="alert-warn hidden" id="fp-warn"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="App.closeModal('modal-penduduk')">Batal</button>
|
||||||
|
<button class="btn btn-primary" onclick="Penduduk.save()">💾 Simpan Data</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KELUARGA FORM -->
|
||||||
|
<div id="modal-keluarga" class="modal-overlay hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-hdr">
|
||||||
|
<h3 id="mk-title">🏠 Tambah Kartu Keluarga</h3>
|
||||||
|
<button class="btn-close-modal" onclick="App.closeModal('modal-keluarga')">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nomor KK (16 digit) *</label>
|
||||||
|
<input class="form-input" type="text" id="fk-nokk" maxlength="16" placeholder="6171…">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nama Kepala Keluarga *</label>
|
||||||
|
<input class="form-input" type="text" id="fk-kepala">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Rumah Ibadah Terdekat</label>
|
||||||
|
<select class="form-select" id="fk-ri"><option value="">— Pilih Rumah Ibadah —</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Alamat</label>
|
||||||
|
<input class="form-input" type="text" id="fk-alamat">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kecamatan</label>
|
||||||
|
<input class="form-input" type="text" id="fk-kecamatan">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kelurahan</label>
|
||||||
|
<input class="form-input" type="text" id="fk-kelurahan">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Status Ekonomi KK</label>
|
||||||
|
<select class="form-select" id="fk-ekonomi">
|
||||||
|
<option value="miskin">🔴 Miskin</option>
|
||||||
|
<option value="rentan" selected>🟡 Rentan</option>
|
||||||
|
<option value="mampu">🟢 Mampu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Latitude</label>
|
||||||
|
<input class="form-input" type="number" id="fk-lat" step="any" placeholder="-0.028">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Longitude</label>
|
||||||
|
<input class="form-input" type="number" id="fk-lng" step="any" placeholder="109.338">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="App.closeModal('modal-keluarga')">Batal</button>
|
||||||
|
<button class="btn btn-primary" onclick="Keluarga.save()">💾 Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SALUR BANTUAN -->
|
||||||
|
<div id="modal-salur" class="modal-overlay hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-hdr">
|
||||||
|
<h3>📦 Salurkan Bantuan</h3>
|
||||||
|
<button class="btn-close-modal" onclick="App.closeModal('modal-salur')">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">NIK Penerima *</label>
|
||||||
|
<input class="form-input" type="text" id="fs-nik" maxlength="16" placeholder="16 digit NIK" oninput="Bantuan.cekPenerima(this.value)">
|
||||||
|
<div id="fs-nik-info" class="alert-ok hidden" style="margin-top:6px"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Jenis Bantuan *</label>
|
||||||
|
<select class="form-select" id="fs-bantuan"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Tanggal Penyaluran *</label>
|
||||||
|
<input class="form-input" type="date" id="fs-tgl">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Jumlah</label>
|
||||||
|
<input class="form-input" type="number" id="fs-jumlah" value="1" min="0.1" step="0.1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Keterangan</label>
|
||||||
|
<input class="form-input" type="text" id="fs-ket" placeholder="Catatan tambahan (opsional)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="App.closeModal('modal-salur')">Batal</button>
|
||||||
|
<button class="btn btn-primary" onclick="Bantuan.salurkan()">✅ Catat Penyaluran</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAMBAH JENIS BANTUAN -->
|
||||||
|
<div id="modal-jenis-bantuan" class="modal-overlay hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-hdr">
|
||||||
|
<h3>🎁 Tambah Jenis Bantuan</h3>
|
||||||
|
<button class="btn-close-modal" onclick="App.closeModal('modal-jenis-bantuan')">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nama Bantuan *</label>
|
||||||
|
<input class="form-input" type="text" id="fjb-nama" placeholder="Sembako Ramadan, Beasiswa, dll">
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kategori *</label>
|
||||||
|
<select class="form-select" id="fjb-kategori">
|
||||||
|
<option value="sembako">Sembako</option>
|
||||||
|
<option value="pendidikan">Pendidikan</option>
|
||||||
|
<option value="kesehatan">Kesehatan</option>
|
||||||
|
<option value="ekonomi">Ekonomi</option>
|
||||||
|
<option value="lainnya">Lainnya</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Satuan</label>
|
||||||
|
<input class="form-input" type="text" id="fjb-satuan" placeholder="paket, kg, Rp, bulan">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Sumber Dana</label>
|
||||||
|
<input class="form-input" type="text" id="fjb-sumber" placeholder="Zakat, APBD, CSR, Donasi">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Deskripsi</label>
|
||||||
|
<textarea class="form-textarea" id="fjb-deskripsi" rows="3" placeholder="Keterangan singkat tentang bantuan ini"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="App.closeModal('modal-jenis-bantuan')">Batal</button>
|
||||||
|
<button class="btn btn-primary" onclick="Bantuan.saveJenis()">💾 Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LAPORAN KIRIM -->
|
||||||
|
<div id="modal-laporan" class="modal-overlay hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-hdr">
|
||||||
|
<h3>📣 Kirim Laporan Kondisi Darurat</h3>
|
||||||
|
<button class="btn-close-modal" onclick="App.closeModal('modal-laporan')">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="info-box" style="margin-bottom:14px">Anda bisa melapor secara anonim. Laporan akan segera ditindaklanjuti oleh pengurus rumah ibadah terdekat.</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nama Pelapor (opsional)</label>
|
||||||
|
<input class="form-input" type="text" id="fl-nama" placeholder="Biarkan kosong jika anonim">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">No. HP (opsional)</label>
|
||||||
|
<input class="form-input" type="text" id="fl-hp" placeholder="Untuk follow-up">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">NIK Warga Terdampak (opsional)</label>
|
||||||
|
<input class="form-input" type="text" id="fl-nik" maxlength="16" placeholder="Kosongkan jika tidak tahu">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Tingkat Urgensi *</label>
|
||||||
|
<select class="form-select" id="fl-urgensi">
|
||||||
|
<option value="rendah">🟢 Rendah — Perlu perhatian</option>
|
||||||
|
<option value="sedang" selected>🟡 Sedang — Perlu bantuan segera</option>
|
||||||
|
<option value="tinggi">🟠 Tinggi — Kondisi mendesak</option>
|
||||||
|
<option value="darurat">🔴 DARURAT — Butuh bantuan segera!</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Alamat / Lokasi</label>
|
||||||
|
<input class="form-input" type="text" id="fl-alamat" placeholder="Nama jalan, gang, kelurahan">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Deskripsi Kondisi *</label>
|
||||||
|
<textarea class="form-textarea" id="fl-deskripsi" rows="4" placeholder="Jelaskan kondisi warga yang membutuhkan bantuan…"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="App.closeModal('modal-laporan')">Batal</button>
|
||||||
|
<button class="btn btn-primary" onclick="LaporanPanel.kirim()">📤 Kirim Laporan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DETAIL PENDUDUK (SLIDE PANEL) -->
|
||||||
|
<div id="detail-panel" class="detail-panel">
|
||||||
|
<div class="dp-hdr">
|
||||||
|
<span id="dp-title" style="font-weight:800;font-size:14px">Detail Penduduk</span>
|
||||||
|
<button class="btn-close-modal" onclick="Penduduk.closeDetail()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="dp-body" id="dp-body"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RI FORM -->
|
||||||
|
<div id="modal-ri" class="modal-overlay hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-hdr">
|
||||||
|
<h3 id="mri-title">🕌 Tambah Rumah Ibadah</h3>
|
||||||
|
<button class="btn-close-modal" onclick="App.closeModal('modal-ri')">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Nama Rumah Ibadah *</label>
|
||||||
|
<input class="form-input" type="text" id="fri-nama" placeholder="Masjid Al-Ikhlas, Gereja Santa Maria, dll">
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Jenis *</label>
|
||||||
|
<select class="form-select" id="fri-jenis">
|
||||||
|
<option>Masjid</option><option>Gereja</option><option>Vihara</option>
|
||||||
|
<option>Pura</option><option>Klenteng</option><option>Lainnya</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Radius Wilayah (meter)</label>
|
||||||
|
<input class="form-input" type="number" id="fri-radius" value="300" min="100" max="2000">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Latitude *</label>
|
||||||
|
<input class="form-input" type="number" id="fri-lat" step="any" placeholder="-0.028">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Longitude *</label>
|
||||||
|
<input class="form-input" type="number" id="fri-lng" step="any" placeholder="109.338">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kecamatan</label>
|
||||||
|
<input class="form-input" type="text" id="fri-kec">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Kontak</label>
|
||||||
|
<input class="form-input" type="text" id="fri-kontak" placeholder="No. telepon">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Alamat</label>
|
||||||
|
<input class="form-input" type="text" id="fri-alamat">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="App.closeModal('modal-ri')">Batal</button>
|
||||||
|
<button class="btn btn-primary" onclick="RI.save()">💾 Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOAST CONTAINER -->
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
|
<!-- SCRIPTS -->
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
|
||||||
|
<script src="assets/js/api.js"></script>
|
||||||
|
<script src="assets/js/map.js"></script>
|
||||||
|
<script src="assets/js/dashboard.js"></script>
|
||||||
|
<script src="assets/js/penduduk.js"></script>
|
||||||
|
<script src="assets/js/keluarga.js"></script>
|
||||||
|
<script src="assets/js/bantuan.js"></script>
|
||||||
|
<script src="assets/js/laporan.js"></script>
|
||||||
|
<script src="assets/js/ri.js"></script>
|
||||||
|
<script src="assets/js/prioritas.js"></script>
|
||||||
|
<script src="assets/js/chat.js"></script>
|
||||||
|
<script src="assets/js/log.js"></script>
|
||||||
|
<script src="assets/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
-- ================================================================
|
||||||
|
-- WebGIS Pengentasan Kemiskinan — Schema Database
|
||||||
|
-- Kota Pontianak, Kalimantan Barat
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
SET time_zone = "+07:00";
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS `webgis_kemiskinan`
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE `webgis_kemiskinan`;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL ROLE
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `role` (
|
||||||
|
`id` TINYINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`nama_role` VARCHAR(50) NOT NULL,
|
||||||
|
`deskripsi` VARCHAR(255) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_role_nama` (`nama_role`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO `role` (`id`, `nama_role`, `deskripsi`) VALUES
|
||||||
|
(1, 'admin', 'Administrator sistem, akses penuh'),
|
||||||
|
(2, 'pengurus', 'Pengurus rumah ibadah, input & verifikasi data'),
|
||||||
|
(3, 'pimpinan', 'Pimpinan daerah, read-only & dashboard'),
|
||||||
|
(4, 'masyarakat', 'Warga umum, laporan & lihat data terbatas');
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL USER
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `user` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`id_role` TINYINT UNSIGNED NOT NULL,
|
||||||
|
`username` VARCHAR(50) NOT NULL,
|
||||||
|
`password_hash` VARCHAR(255) NOT NULL,
|
||||||
|
`nama_lengkap` VARCHAR(100) NOT NULL,
|
||||||
|
`email` VARCHAR(100) DEFAULT NULL,
|
||||||
|
`no_hp` VARCHAR(20) DEFAULT NULL,
|
||||||
|
`aktif` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`last_login` DATETIME DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_username` (`username`),
|
||||||
|
KEY `fk_user_role` (`id_role`),
|
||||||
|
CONSTRAINT `fk_user_role` FOREIGN KEY (`id_role`) REFERENCES `role` (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL RUMAH IBADAH
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `rumah_ibadah` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`id_user` INT UNSIGNED DEFAULT NULL COMMENT 'Pengurus PJ',
|
||||||
|
`nama` VARCHAR(150) NOT NULL,
|
||||||
|
`jenis` ENUM('Masjid','Gereja','Vihara','Pura','Klenteng','Lainnya') NOT NULL,
|
||||||
|
`alamat` TEXT DEFAULT NULL,
|
||||||
|
`lat` DECIMAL(10,7) NOT NULL,
|
||||||
|
`lng` DECIMAL(10,7) NOT NULL,
|
||||||
|
`radius_meter` SMALLINT UNSIGNED NOT NULL DEFAULT 300,
|
||||||
|
`kontak` VARCHAR(50) DEFAULT NULL,
|
||||||
|
`kecamatan` VARCHAR(80) DEFAULT NULL,
|
||||||
|
`kelurahan` VARCHAR(80) DEFAULT NULL,
|
||||||
|
`aktif` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_ri_user` (`id_user`),
|
||||||
|
CONSTRAINT `fk_ri_user` FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL KELUARGA (KK)
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `keluarga` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`no_kk` CHAR(16) NOT NULL COMMENT 'Nomor Kartu Keluarga',
|
||||||
|
`id_rumah_ibadah` INT UNSIGNED DEFAULT NULL COMMENT 'RI terdekat / pengelola',
|
||||||
|
`nama_kepala` VARCHAR(100) NOT NULL,
|
||||||
|
`alamat` TEXT DEFAULT NULL,
|
||||||
|
`rt` VARCHAR(5) DEFAULT NULL,
|
||||||
|
`rw` VARCHAR(5) DEFAULT NULL,
|
||||||
|
`kelurahan` VARCHAR(80) DEFAULT NULL,
|
||||||
|
`kecamatan` VARCHAR(80) DEFAULT NULL,
|
||||||
|
`lat` DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
`lng` DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
`status_ekonomi` ENUM('miskin','rentan','mampu') NOT NULL DEFAULT 'rentan',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_no_kk` (`no_kk`),
|
||||||
|
KEY `fk_kk_ri` (`id_rumah_ibadah`),
|
||||||
|
CONSTRAINT `fk_kk_ri` FOREIGN KEY (`id_rumah_ibadah`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL PENDUDUK
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `penduduk` (
|
||||||
|
`nik` CHAR(16) NOT NULL,
|
||||||
|
`id_keluarga` INT UNSIGNED NOT NULL,
|
||||||
|
`nama_lengkap` VARCHAR(100) NOT NULL,
|
||||||
|
`jenis_kelamin` ENUM('L','P') NOT NULL,
|
||||||
|
`tanggal_lahir` DATE NOT NULL,
|
||||||
|
-- umur dihitung di query: TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE())
|
||||||
|
`tempat_lahir` VARCHAR(80) DEFAULT NULL,
|
||||||
|
`status_keluarga` ENUM('kepala','istri','anak','menantu','cucu','orang_tua','lainnya') NOT NULL DEFAULT 'anak',
|
||||||
|
`status_perkawinan` ENUM('belum_kawin','kawin','cerai_hidup','cerai_mati') NOT NULL DEFAULT 'belum_kawin',
|
||||||
|
`status_hidup` ENUM('hidup','meninggal') NOT NULL DEFAULT 'hidup',
|
||||||
|
`agama` ENUM('Islam','Kristen','Katolik','Hindu','Buddha','Konghucu','Lainnya') DEFAULT NULL,
|
||||||
|
`pekerjaan` VARCHAR(80) DEFAULT NULL,
|
||||||
|
`penghasilan` INT UNSIGNED DEFAULT 0 COMMENT 'Per bulan, rupiah',
|
||||||
|
`status_ekonomi` ENUM('miskin','rentan','mampu') NOT NULL DEFAULT 'rentan',
|
||||||
|
-- Pendidikan
|
||||||
|
`pendidikan_terakhir` ENUM('tidak_sekolah','SD','SMP','SMA_SMK','D1_D3','S1','S2_S3') DEFAULT 'tidak_sekolah',
|
||||||
|
`status_pendidikan` ENUM('sekolah','tidak_sekolah','lulus') DEFAULT 'tidak_sekolah',
|
||||||
|
-- Kesehatan
|
||||||
|
`kondisi_kesehatan` ENUM('sehat','sakit_ringan','sakit_berat','disabilitas') DEFAULT 'sehat',
|
||||||
|
`keterangan_kesehatan` VARCHAR(255) DEFAULT NULL,
|
||||||
|
-- Koordinat individual (bisa beda dari rumah)
|
||||||
|
`lat` DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
`lng` DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
-- Timestamps
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`nik`),
|
||||||
|
KEY `fk_pddk_kk` (`id_keluarga`),
|
||||||
|
KEY `idx_status_ekonomi` (`status_ekonomi`),
|
||||||
|
KEY `idx_status_hidup` (`status_hidup`),
|
||||||
|
CONSTRAINT `fk_pddk_kk` FOREIGN KEY (`id_keluarga`) REFERENCES `keluarga` (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL RIWAYAT PENDIDIKAN / PELATIHAN
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `riwayat_pendidikan` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`nik` CHAR(16) NOT NULL,
|
||||||
|
`jenis` ENUM('formal','pelatihan','kursus') NOT NULL DEFAULT 'formal',
|
||||||
|
`nama_institusi` VARCHAR(150) DEFAULT NULL,
|
||||||
|
`jurusan` VARCHAR(100) DEFAULT NULL,
|
||||||
|
`tahun_masuk` YEAR DEFAULT NULL,
|
||||||
|
`tahun_lulus` YEAR DEFAULT NULL,
|
||||||
|
`ijazah` TINYINT(1) DEFAULT 0,
|
||||||
|
`keterangan` TEXT DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_riwpend_nik` (`nik`),
|
||||||
|
CONSTRAINT `fk_riwpend_nik` FOREIGN KEY (`nik`) REFERENCES `penduduk` (`nik`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL JENIS BANTUAN (master)
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `bantuan` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`nama_bantuan` VARCHAR(100) NOT NULL,
|
||||||
|
`kategori` ENUM('sembako','pendidikan','kesehatan','ekonomi','lainnya') NOT NULL DEFAULT 'lainnya',
|
||||||
|
`sumber` VARCHAR(100) DEFAULT NULL COMMENT 'APBD, CSR, Zakat, dll',
|
||||||
|
`deskripsi` TEXT DEFAULT NULL,
|
||||||
|
`satuan` VARCHAR(50) DEFAULT NULL COMMENT 'karung, paket, Rp, dll',
|
||||||
|
`aktif` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL HISTORI BANTUAN
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `histori_bantuan` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`nik` CHAR(16) NOT NULL,
|
||||||
|
`id_bantuan` INT UNSIGNED NOT NULL,
|
||||||
|
`id_rumah_ibadah` INT UNSIGNED DEFAULT NULL,
|
||||||
|
`id_user` INT UNSIGNED DEFAULT NULL COMMENT 'Petugas penyalur',
|
||||||
|
`tanggal` DATE NOT NULL,
|
||||||
|
`jumlah` DECIMAL(10,2) DEFAULT 1,
|
||||||
|
`keterangan` TEXT DEFAULT NULL,
|
||||||
|
`status_penyaluran` ENUM('direncanakan','disalurkan','ditolak') NOT NULL DEFAULT 'disalurkan',
|
||||||
|
`bukti_foto` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_hb_nik` (`nik`),
|
||||||
|
KEY `fk_hb_bantuan` (`id_bantuan`),
|
||||||
|
KEY `fk_hb_ri` (`id_rumah_ibadah`),
|
||||||
|
KEY `fk_hb_user` (`id_user`),
|
||||||
|
CONSTRAINT `fk_hb_nik` FOREIGN KEY (`nik`) REFERENCES `penduduk` (`nik`),
|
||||||
|
CONSTRAINT `fk_hb_bantuan` FOREIGN KEY (`id_bantuan`) REFERENCES `bantuan` (`id`),
|
||||||
|
CONSTRAINT `fk_hb_ri` FOREIGN KEY (`id_rumah_ibadah`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT `fk_hb_user` FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL LAPORAN MASYARAKAT
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `laporan` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`nama_pelapor` VARCHAR(100) DEFAULT NULL,
|
||||||
|
`no_hp_pelapor` VARCHAR(20) DEFAULT NULL,
|
||||||
|
`nik_terdampak` CHAR(16) DEFAULT NULL COMMENT 'Penduduk yg dilaporkan (opsional)',
|
||||||
|
`id_rumah_ibadah` INT UNSIGNED DEFAULT NULL,
|
||||||
|
`deskripsi` TEXT NOT NULL,
|
||||||
|
`lat` DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
`lng` DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
`alamat_laporan` TEXT DEFAULT NULL,
|
||||||
|
`urgensi` ENUM('rendah','sedang','tinggi','darurat') NOT NULL DEFAULT 'sedang',
|
||||||
|
`foto` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`status` ENUM('pending','diverifikasi','diproses','selesai') NOT NULL DEFAULT 'pending',
|
||||||
|
`catatan_verifikasi` TEXT DEFAULT NULL,
|
||||||
|
`id_verifikator` INT UNSIGNED DEFAULT NULL,
|
||||||
|
`tgl_verifikasi` DATETIME DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_lap_nik` (`nik_terdampak`),
|
||||||
|
KEY `fk_lap_ri` (`id_rumah_ibadah`),
|
||||||
|
KEY `fk_lap_verif` (`id_verifikator`),
|
||||||
|
CONSTRAINT `fk_lap_nik` FOREIGN KEY (`nik_terdampak`) REFERENCES `penduduk` (`nik`) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT `fk_lap_ri` FOREIGN KEY (`id_rumah_ibadah`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT `fk_lap_verif` FOREIGN KEY (`id_verifikator`) REFERENCES `user` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL CHAT / KOMUNIKASI
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `chat` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`id_user` INT UNSIGNED NOT NULL,
|
||||||
|
`id_room` INT UNSIGNED DEFAULT NULL COMMENT 'NULL = channel umum',
|
||||||
|
`pesan` TEXT NOT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_chat_user` (`id_user`),
|
||||||
|
CONSTRAINT `fk_chat_user` FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Chat rooms
|
||||||
|
CREATE TABLE `chat_room` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`nama` VARCHAR(100) NOT NULL,
|
||||||
|
`deskripsi` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `chat` ADD CONSTRAINT `fk_chat_room` FOREIGN KEY (`id_room`) REFERENCES `chat_room` (`id`) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL LOG AKTIVITAS
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `log_aktivitas` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`id_user` INT UNSIGNED DEFAULT NULL,
|
||||||
|
`aksi` VARCHAR(100) NOT NULL COMMENT 'create_penduduk, update_bantuan, dll',
|
||||||
|
`tabel` VARCHAR(50) DEFAULT NULL,
|
||||||
|
`id_record` VARCHAR(50) DEFAULT NULL COMMENT 'PK record yg diubah',
|
||||||
|
`data_lama` JSON DEFAULT NULL,
|
||||||
|
`data_baru` JSON DEFAULT NULL,
|
||||||
|
`ip_address` VARCHAR(45) DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_log_user` (`id_user`),
|
||||||
|
KEY `idx_log_aksi` (`aksi`),
|
||||||
|
KEY `idx_log_created` (`created_at`),
|
||||||
|
CONSTRAINT `fk_log_user` FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- TABEL PERUBAHAN STATUS (kelahiran, kematian, dll)
|
||||||
|
-- ================================================================
|
||||||
|
CREATE TABLE `peristiwa_kependudukan` (
|
||||||
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`nik` CHAR(16) NOT NULL,
|
||||||
|
`jenis` ENUM('kelahiran','kematian','pernikahan','perceraian','pindah_masuk','pindah_keluar') NOT NULL,
|
||||||
|
`tanggal` DATE NOT NULL,
|
||||||
|
`keterangan` TEXT DEFAULT NULL,
|
||||||
|
`id_user` INT UNSIGNED DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_perk_nik` (`nik`),
|
||||||
|
KEY `fk_perk_user` (`id_user`),
|
||||||
|
CONSTRAINT `fk_perk_nik` FOREIGN KEY (`nik`) REFERENCES `penduduk` (`nik`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_perk_user` FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- SEED DATA — Users
|
||||||
|
-- ================================================================
|
||||||
|
-- Password: admin123 (bcrypt hash contoh)
|
||||||
|
INSERT INTO `user` (`id_role`,`username`,`password_hash`,`nama_lengkap`,`email`) VALUES
|
||||||
|
(1, 'admin', '$2y$10$9JkAyeakwNYcOvjtrxLeb.teP0iJCFQ9Eeru0LOfodabdjDyAXNTC', 'Administrator Sistem', 'admin@webgis-ptk.id'),
|
||||||
|
(2, 'pengurus_jami','$2y$10$9JkAyeakwNYcOvjtrxLeb.teP0iJCFQ9Eeru0LOfodabdjDyAXNTC', 'Ahmad Fauzi (Masjid Jami)', 'fauzi@webgis-ptk.id'),
|
||||||
|
(2, 'pengurus_ktdr','$2y$10$9JkAyeakwNYcOvjtrxLeb.teP0iJCFQ9Eeru0LOfodabdjDyAXNTC', 'Romo Benediktus (Katedral)', 'romo@webgis-ptk.id'),
|
||||||
|
(3, 'pimpinan', '$2y$10$9JkAyeakwNYcOvjtrxLeb.teP0iJCFQ9Eeru0LOfodabdjDyAXNTC', 'Camat Pontianak Kota', 'camat@webgis-ptk.id'),
|
||||||
|
(4, 'warga1', '$2y$10$9JkAyeakwNYcOvjtrxLeb.teP0iJCFQ9Eeru0LOfodabdjDyAXNTC', 'Budi Santoso', 'budi@gmail.com');
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- SEED DATA — Rumah Ibadah
|
||||||
|
-- ================================================================
|
||||||
|
INSERT INTO `rumah_ibadah` (`id_user`,`nama`,`jenis`,`alamat`,`lat`,`lng`,`radius_meter`,`kontak`,`kecamatan`,`kelurahan`) VALUES
|
||||||
|
(2, 'Masjid Jami\' Pontianak', 'Masjid', 'Jl. Tanjungpura, Dalam Bugis', -0.0259600, 109.3430700, 350, '0561-732001', 'Pontianak Timur', 'Dalam Bugis'),
|
||||||
|
(3, 'Gereja Katedral Pontianak', 'Gereja', 'Jl. Patimura No.9, Akcaya', -0.0320000, 109.3320000, 300, '0561-766000', 'Pontianak Selatan', 'Akcaya'),
|
||||||
|
(NULL,'Vihara Bodhisattva Karaniya Metta', 'Vihara', 'Jl. Diponegoro, Pontianak Kota', -0.0210000, 109.3380000, 300, '0561-733456', 'Pontianak Kota', 'Darat Sekip'),
|
||||||
|
(NULL,'Klenteng Kwan Ti Miau', 'Klenteng','Jl. Gusti Situt Mahmud', -0.0190000, 109.3410000, 280, '0561-740123', 'Pontianak Kota', 'Pontianak Kota'),
|
||||||
|
(2, 'Masjid Mujahidin Pontianak', 'Masjid', 'Jl. Ahmad Yani, Parit Tokaya', -0.0480000, 109.3250000, 400, '0561-764567', 'Pontianak Selatan', 'Parit Tokaya'),
|
||||||
|
(3, 'Gereja GPIB Immanuel', 'Gereja', 'Jl. Rahadi Usman, Darat Sekip', -0.0280000, 109.3450000, 300, '0561-735890', 'Pontianak Kota', 'Darat Sekip');
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- SEED DATA — Keluarga & Penduduk
|
||||||
|
-- ================================================================
|
||||||
|
INSERT INTO `keluarga` (`no_kk`,`id_rumah_ibadah`,`nama_kepala`,`alamat`,`rt`,`rw`,`kelurahan`,`kecamatan`,`lat`,`lng`,`status_ekonomi`) VALUES
|
||||||
|
('6171010101010001', 1, 'Ahmad Suryana', 'Gg. Mawar No.5', '002','004','Dalam Bugis', 'Pontianak Timur', -0.0270000, 109.3440000, 'miskin'),
|
||||||
|
('6171010101010002', 1, 'Samsul Bahri', 'Jl. Veteran No.12', '003','002','Sungai Bangkong','Pontianak Kota', -0.0315000, 109.3390000, 'rentan'),
|
||||||
|
('6171010101010003', 2, 'Maria Magdalena', 'Jl. Kemuning No.3', '001','001','Akcaya', 'Pontianak Selatan', -0.0330000, 109.3310000, 'miskin'),
|
||||||
|
('6171010101010004', 5, 'Hendra Wijaya', 'Jl. Ahmad Yani Gg. 7', '005','003','Parit Tokaya', 'Pontianak Selatan', -0.0490000, 109.3260000, 'rentan'),
|
||||||
|
('6171010101010005', 3, 'Lim Ah Kow', 'Jl. Diponegoro Gg. 2', '001','002','Darat Sekip', 'Pontianak Kota', -0.0220000, 109.3370000, 'miskin'),
|
||||||
|
('6171010101010006', 1, 'Mulyadi', 'Gg. Teratai No.8', '004','003','Dalam Bugis', 'Pontianak Timur', -0.0265000, 109.3460000, 'mampu'),
|
||||||
|
('6171010101010007', 2, 'Yohanes Kristian', 'Jl. Hos Cokroaminoto No.4','002','001','Akcaya', 'Pontianak Selatan', -0.0340000, 109.3300000, 'rentan'),
|
||||||
|
('6171010101010008', 4, 'Tan Eng Huat', 'Jl. Gusti Situt No.7', '003','001','Pontianak Kota', 'Pontianak Kota', -0.0200000, 109.3415000, 'miskin');
|
||||||
|
|
||||||
|
INSERT INTO `penduduk` (`nik`,`id_keluarga`,`nama_lengkap`,`jenis_kelamin`,`tanggal_lahir`,`tempat_lahir`,`status_keluarga`,`status_perkawinan`,`agama`,`pekerjaan`,`penghasilan`,`status_ekonomi`,`pendidikan_terakhir`,`status_pendidikan`,`kondisi_kesehatan`,`lat`,`lng`) VALUES
|
||||||
|
('6171010101010001', 1, 'Ahmad Suryana', 'L', '1980-05-15', 'Pontianak', 'kepala', 'kawin', 'Islam', 'Buruh harian', 900000, 'miskin', 'SMP', 'lulus', 'sehat', -0.0270000, 109.3440000),
|
||||||
|
('6171010101010002', 1, 'Siti Rahma', 'P', '1985-08-20', 'Pontianak', 'istri', 'kawin', 'Islam', 'Ibu rumah tangga', 0, 'miskin', 'SD', 'lulus', 'sakit_ringan',-0.0270000, 109.3440000),
|
||||||
|
('6171010101010003', 1, 'Budi Suryana', 'L', '2012-03-10', 'Pontianak', 'anak', 'belum_kawin', 'Islam', 'Pelajar', 0, 'miskin', 'SMP', 'sekolah', 'sehat', -0.0270000, 109.3440000),
|
||||||
|
('6171010101010004', 2, 'Samsul Bahri', 'L', '1975-11-22', 'Sambas', 'kepala', 'kawin', 'Islam', 'Pedagang kaki lima', 1200000, 'rentan', 'SMA_SMK', 'lulus', 'sehat', -0.0315000, 109.3390000),
|
||||||
|
('6171010101010005', 2, 'Dewi Kartika', 'P', '1978-06-14', 'Pontianak', 'istri', 'kawin', 'Islam', 'Buruh cuci', 500000, 'rentan', 'SD', 'lulus', 'sehat', -0.0315000, 109.3390000),
|
||||||
|
('6171010101010006', 3, 'Maria Magdalena', 'P', '1970-12-03', 'Pontianak', 'kepala', 'cerai_mati', 'Kristen', 'Penjual gorengan', 800000, 'miskin', 'SD', 'lulus', 'sakit_berat', -0.0330000, 109.3310000),
|
||||||
|
('6171010101010007', 3, 'Petrus Yulius', 'L', '2005-04-17', 'Pontianak', 'anak', 'belum_kawin', 'Kristen', 'Pelajar', 0, 'miskin', 'SMA_SMK', 'sekolah', 'sehat', -0.0330000, 109.3310000),
|
||||||
|
('6171010101010008', 3, 'Anastasia Dewi', 'P', '2010-09-28', 'Pontianak', 'anak', 'belum_kawin', 'Kristen', 'Pelajar', 0, 'miskin', 'SMP', 'sekolah', 'sehat', -0.0330000, 109.3310000),
|
||||||
|
('6171010101010009', 4, 'Hendra Wijaya', 'L', '1988-02-11', 'Pontianak', 'kepala', 'kawin', 'Buddha', 'Tukang ojek', 1500000, 'rentan', 'SMA_SMK', 'lulus', 'sehat', -0.0490000, 109.3260000),
|
||||||
|
('6171010101010010', 5, 'Lim Ah Kow', 'L', '1965-07-30', 'Pontianak', 'kepala', 'kawin', 'Buddha', 'Tidak bekerja', 0, 'miskin', 'SD', 'lulus', 'sakit_berat', -0.0220000, 109.3370000),
|
||||||
|
('6171010101010011', 5, 'Tan Siu Lan', 'P', '1968-03-19', 'Pontianak', 'istri', 'kawin', 'Buddha', 'Penjual sayur', 700000, 'miskin', 'SD', 'lulus', 'sehat', -0.0220000, 109.3370000),
|
||||||
|
('6171010101010012', 6, 'Mulyadi', 'L', '1979-10-05', 'Singkawang','kepala', 'kawin', 'Islam', 'PNS Golongan III', 4500000, 'mampu', 'S1', 'lulus', 'sehat', -0.0265000, 109.3460000),
|
||||||
|
('6171010101010013', 7, 'Yohanes Kristian', 'L', '1982-01-25', 'Pontianak', 'kepala', 'kawin', 'Katolik', 'Sopir angkot', 1100000, 'rentan', 'SMA_SMK', 'lulus', 'sehat', -0.0340000, 109.3300000),
|
||||||
|
('6171010101010014', 8, 'Tan Eng Huat', 'L', '1960-08-08', 'Pontianak', 'kepala', 'kawin', 'Konghucu','Pemulung', 600000, 'miskin', 'SD', 'lulus', 'disabilitas', -0.0200000, 109.3415000);
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- SEED DATA — Bantuan Master
|
||||||
|
-- ================================================================
|
||||||
|
INSERT INTO `bantuan` (`nama_bantuan`,`kategori`,`sumber`,`deskripsi`,`satuan`) VALUES
|
||||||
|
('Sembako Ramadan', 'sembako', 'Zakat Masjid', 'Paket sembako bulan Ramadan', 'paket'),
|
||||||
|
('PKH (Program Keluarga Harapan)', 'ekonomi', 'APBN', 'Bantuan tunai conditional', 'Rp'),
|
||||||
|
('BPNT / Kartu Sembako', 'sembako', 'APBN', 'Bantuan pangan non-tunai Rp200rb/bln', 'bulan'),
|
||||||
|
('Beasiswa Pendidikan', 'pendidikan', 'CSR Perusahaan', 'Beasiswa untuk anak sekolah', 'paket/tahun'),
|
||||||
|
('BPJS Kesehatan PBI', 'kesehatan', 'APBD', 'Iuran BPJS ditanggung pemerintah', 'bulan'),
|
||||||
|
('Bantuan Sembako Gereja', 'sembako', 'Kolekte Gereja', 'Paket bulanan dari gereja', 'paket'),
|
||||||
|
('Bantuan Vihara (Kwan Im)', 'sembako', 'Dana Sosial Vihara', 'Paket sembako bulanan', 'paket'),
|
||||||
|
('Raskin / BERAS MISKIN', 'sembako', 'Bulog', '10kg per bulan per KK', 'kg');
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- SEED DATA — Histori Bantuan
|
||||||
|
-- ================================================================
|
||||||
|
INSERT INTO `histori_bantuan` (`nik`,`id_bantuan`,`id_rumah_ibadah`,`id_user`,`tanggal`,`jumlah`,`status_penyaluran`) VALUES
|
||||||
|
('6171010101010001', 1, 1, 2, '2025-03-15', 1, 'disalurkan'),
|
||||||
|
('6171010101010001', 3, 1, 2, '2025-01-10', 1, 'disalurkan'),
|
||||||
|
('6171010101010006', 6, 2, 3, '2025-02-20', 1, 'disalurkan'),
|
||||||
|
('6171010101010010', 7, 3, 2, '2025-04-01', 1, 'disalurkan'),
|
||||||
|
('6171010101010014', 8, 4, 2, '2025-03-05', 10,'disalurkan');
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- SEED DATA — Laporan Masyarakat
|
||||||
|
-- ================================================================
|
||||||
|
INSERT INTO `laporan` (`nama_pelapor`,`nik_terdampak`,`id_rumah_ibadah`,`deskripsi`,`lat`,`lng`,`alamat_laporan`,`urgensi`,`status`) VALUES
|
||||||
|
('Anonim', '6171010101010006', 2, 'Ibu Maria sakit keras sudah seminggu, tidak bisa berobat karena tidak ada biaya', -0.0330, 109.3311, 'Jl. Kemuning No.3, Akcaya', 'darurat', 'diverifikasi'),
|
||||||
|
('Eko Prasetyo', NULL, 1, 'Ada keluarga baru pindah ke Gg. Melati yang sepertinya sangat membutuhkan bantuan', -0.0268, 109.3448, 'Gg. Melati, Dalam Bugis', 'sedang', 'pending'),
|
||||||
|
('Anonim', '6171010101010014', 4, 'Pak Tan penyandang disabilitas, hidup sebatang kara dan sulit mendapat penghasilan', -0.0200, 109.3415, 'Jl. Gusti Situt No.7', 'tinggi', 'diproses');
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- SEED DATA — Chat Room
|
||||||
|
-- ================================================================
|
||||||
|
INSERT INTO `chat_room` (`nama`,`deskripsi`) VALUES
|
||||||
|
('Koordinasi Umum', 'Channel koordinasi semua pengurus rumah ibadah'),
|
||||||
|
('Bantuan Darurat', 'Koordinasi penanganan kasus darurat & prioritas'),
|
||||||
|
('Distribusi Sembako', 'Jadwal & koordinasi distribusi sembako bulanan');
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- VIEW — Data lengkap penduduk (dengan umur otomatis)
|
||||||
|
-- ================================================================
|
||||||
|
CREATE OR REPLACE VIEW `v_penduduk_lengkap` AS
|
||||||
|
SELECT
|
||||||
|
p.nik,
|
||||||
|
p.nama_lengkap,
|
||||||
|
p.jenis_kelamin,
|
||||||
|
p.tanggal_lahir,
|
||||||
|
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||||
|
p.tempat_lahir,
|
||||||
|
p.status_keluarga,
|
||||||
|
p.status_perkawinan,
|
||||||
|
p.status_hidup,
|
||||||
|
p.agama,
|
||||||
|
p.pekerjaan,
|
||||||
|
p.penghasilan,
|
||||||
|
p.status_ekonomi,
|
||||||
|
p.pendidikan_terakhir,
|
||||||
|
p.status_pendidikan,
|
||||||
|
p.kondisi_kesehatan,
|
||||||
|
p.lat,
|
||||||
|
p.lng,
|
||||||
|
k.no_kk,
|
||||||
|
k.nama_kepala,
|
||||||
|
k.alamat,
|
||||||
|
k.rt,
|
||||||
|
k.rw,
|
||||||
|
k.kelurahan,
|
||||||
|
k.kecamatan,
|
||||||
|
k.status_ekonomi AS status_ekonomi_kk,
|
||||||
|
ri.nama AS nama_ri,
|
||||||
|
ri.jenis AS jenis_ri,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM `penduduk` p
|
||||||
|
JOIN `keluarga` k ON p.id_keluarga = k.id
|
||||||
|
LEFT JOIN `rumah_ibadah` ri ON k.id_rumah_ibadah = ri.id;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- VIEW — Statistik keluarga
|
||||||
|
-- ================================================================
|
||||||
|
CREATE OR REPLACE VIEW `v_statistik_keluarga` AS
|
||||||
|
SELECT
|
||||||
|
k.id,
|
||||||
|
k.no_kk,
|
||||||
|
k.nama_kepala,
|
||||||
|
k.status_ekonomi,
|
||||||
|
k.kecamatan,
|
||||||
|
k.kelurahan,
|
||||||
|
k.lat,
|
||||||
|
k.lng,
|
||||||
|
COUNT(p.nik) AS jumlah_anggota,
|
||||||
|
SUM(CASE WHEN p.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 1 ELSE 0 END) AS anggota_sakit,
|
||||||
|
SUM(p.penghasilan) AS total_penghasilan,
|
||||||
|
COUNT(hb.id) AS total_bantuan_diterima,
|
||||||
|
MAX(hb.tanggal) AS terakhir_dibantu,
|
||||||
|
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 `histori_bantuan` hb ON hb.nik = p.nik AND hb.status_penyaluran = 'disalurkan'
|
||||||
|
LEFT JOIN `rumah_ibadah` ri ON k.id_rumah_ibadah = ri.id
|
||||||
|
GROUP BY k.id;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- VIEW — Prioritas bantuan (miskin + sakit & belum dibantu)
|
||||||
|
-- ================================================================
|
||||||
|
CREATE OR REPLACE VIEW `v_prioritas_bantuan` AS
|
||||||
|
SELECT
|
||||||
|
p.nik,
|
||||||
|
p.nama_lengkap,
|
||||||
|
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||||
|
p.kondisi_kesehatan,
|
||||||
|
p.status_ekonomi,
|
||||||
|
k.nama_kepala AS kepala_keluarga,
|
||||||
|
k.alamat,
|
||||||
|
k.kecamatan,
|
||||||
|
k.lat,
|
||||||
|
k.lng,
|
||||||
|
ri.nama AS nama_ri,
|
||||||
|
COUNT(hb.id) AS jumlah_dibantu,
|
||||||
|
CASE
|
||||||
|
WHEN p.status_ekonomi = 'miskin' AND p.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'SANGAT TINGGI'
|
||||||
|
WHEN p.status_ekonomi = 'miskin' AND COUNT(hb.id) = 0 THEN 'TINGGI'
|
||||||
|
WHEN p.status_ekonomi = 'miskin' THEN 'SEDANG'
|
||||||
|
WHEN p.status_ekonomi = 'rentan' AND p.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'SEDANG'
|
||||||
|
ELSE 'RENDAH'
|
||||||
|
END AS level_prioritas
|
||||||
|
FROM `penduduk` p
|
||||||
|
JOIN `keluarga` k ON p.id_keluarga = k.id
|
||||||
|
LEFT JOIN `rumah_ibadah` ri ON k.id_rumah_ibadah = ri.id
|
||||||
|
LEFT JOIN `histori_bantuan` hb ON hb.nik = p.nik AND hb.status_penyaluran = 'disalurkan'
|
||||||
|
WHERE p.status_hidup = 'hidup'
|
||||||
|
AND p.status_ekonomi IN ('miskin','rentan')
|
||||||
|
GROUP BY p.nik;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$data = [];
|
||||||
|
$result = $conn->query("SELECT * FROM spbu");
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$data[] = $row;
|
||||||
|
}
|
||||||
|
echo json_encode($data);
|
||||||
|
?>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE DATABASE IF NOT EXISTS db_spbu;
|
||||||
|
USE db_spbu;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `spbu` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`nama_spbu` varchar(100) NOT NULL,
|
||||||
|
`no_wa` varchar(20) NOT NULL,
|
||||||
|
`status` enum('yes','no') NOT NULL DEFAULT 'no',
|
||||||
|
`latitude` double NOT NULL,
|
||||||
|
`longitude` double NOT NULL,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO `spbu` (`nama_spbu`, `no_wa`, `status`, `latitude`, `longitude`) VALUES
|
||||||
|
('SPBU Gajah Mada', '08115700101', 'yes', -0.0227, 109.3294),
|
||||||
|
('SPBU Ahmad Yani', '08115700102', 'no', -0.0350, 109.3350),
|
||||||
|
('SPBU Tanjungpura', '08115700103', 'yes', -0.0180, 109.3410);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$id = $conn->real_escape_string($_POST['id']);
|
||||||
|
$query = "DELETE FROM spbu WHERE id='$id'";
|
||||||
|
|
||||||
|
if ($conn->query($query)) {
|
||||||
|
echo "success";
|
||||||
|
} else {
|
||||||
|
echo "error: " . $conn->error;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Peta SPBU</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||||
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||||
|
<style>
|
||||||
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
body { font-family: 'Segoe UI', sans-serif; }
|
||||||
|
#map { height: 100vh; width: 100%; }
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 9999;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.modal-overlay.show { display: flex; }
|
||||||
|
|
||||||
|
.modal-box {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
width: 300px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.modal-box h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
color: #222;
|
||||||
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.modal-box label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin: 10px 0 3px;
|
||||||
|
}
|
||||||
|
.modal-box input, .modal-box select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.modal-box input:focus, .modal-box select:focus { border-color: #3498db; }
|
||||||
|
|
||||||
|
.modal-actions { display:flex; gap:8px; margin-top:16px; }
|
||||||
|
.btn-save {
|
||||||
|
flex:1; padding:9px;
|
||||||
|
background:#3498db; color:#fff;
|
||||||
|
border:none; border-radius:6px;
|
||||||
|
font-size:13px; font-weight:600; cursor:pointer;
|
||||||
|
}
|
||||||
|
.btn-save:hover { background:#2980b9; }
|
||||||
|
.btn-cancel {
|
||||||
|
flex:1; padding:9px;
|
||||||
|
background:#f0f0f0; color:#555;
|
||||||
|
border:none; border-radius:6px;
|
||||||
|
font-size:13px; font-weight:600; cursor:pointer;
|
||||||
|
}
|
||||||
|
.btn-cancel:hover { background:#e0e0e0; }
|
||||||
|
|
||||||
|
#info-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px; left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(20,20,20,0.8);
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 56px; left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(10px);
|
||||||
|
background: #2ecc71; color: #fff;
|
||||||
|
padding: 8px 22px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px; font-weight:600;
|
||||||
|
z-index: 9998;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all .3s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#toast.show { opacity:1; transform: translateX(-50%) translateY(0); }
|
||||||
|
#toast.error { background: #e74c3c; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
<div id="info-bar">🖱️ Klik peta untuk tambah SPBU · Klik marker untuk Edit/Hapus · Seret marker untuk pindah lokasi</div>
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
|
<!-- ===== MODAL TAMBAH ===== -->
|
||||||
|
<div class="modal-overlay" id="modalTambah">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3>➕ Tambah SPBU Baru</h3>
|
||||||
|
<input type="hidden" id="tLat">
|
||||||
|
<input type="hidden" id="tLng">
|
||||||
|
|
||||||
|
<label>Nama SPBU</label>
|
||||||
|
<input type="text" id="tNama" placeholder="Contoh: SPBU 64.751.01">
|
||||||
|
|
||||||
|
<label>No. WhatsApp</label>
|
||||||
|
<input type="text" id="tWa" placeholder="08xxxxxxxxxx">
|
||||||
|
|
||||||
|
<label>Status</label>
|
||||||
|
<select id="tStatus">
|
||||||
|
<option value="yes">🟢 Buka 24 Jam</option>
|
||||||
|
<option value="no">🔴 Tidak 24 Jam</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" id="btnBatalTambah">Batal</button>
|
||||||
|
<button class="btn-save" id="btnSimpanTambah">💾 Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== MODAL EDIT ===== -->
|
||||||
|
<div class="modal-overlay" id="modalEdit">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3>✏️ Edit Data SPBU</h3>
|
||||||
|
<input type="hidden" id="eId">
|
||||||
|
<input type="hidden" id="eLat">
|
||||||
|
<input type="hidden" id="eLng">
|
||||||
|
|
||||||
|
<label>Nama SPBU</label>
|
||||||
|
<input type="text" id="eNama" placeholder="Nama SPBU...">
|
||||||
|
|
||||||
|
<label>No. WhatsApp</label>
|
||||||
|
<input type="text" id="eWa" placeholder="08xxxxxxxxxx">
|
||||||
|
|
||||||
|
<label>Status</label>
|
||||||
|
<select id="eStatus">
|
||||||
|
<option value="yes">🟢 Buka 24 Jam</option>
|
||||||
|
<option value="no">🔴 Tidak 24 Jam</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" id="btnBatalEdit">Batal</button>
|
||||||
|
<button class="btn-save" id="btnSimpanEdit">💾 Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── PETA ──────────────────────────────────────────────────
|
||||||
|
const map = L.map('map').setView([-0.02, 109.34], 13);
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© OpenStreetMap contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// ── ICON ──────────────────────────────────────────────────
|
||||||
|
function buatIcon(warna) {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-${warna}.png`,
|
||||||
|
shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png',
|
||||||
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const ikonHijau = buatIcon('green');
|
||||||
|
const ikonMerah = buatIcon('red');
|
||||||
|
|
||||||
|
// ── TOAST ─────────────────────────────────────────────────
|
||||||
|
function toast(msg, tipe) {
|
||||||
|
const el = document.getElementById('toast');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.className = 'show' + (tipe === 'err' ? ' error' : '');
|
||||||
|
setTimeout(() => el.className = '', 2800);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LOAD DATA ─────────────────────────────────────────────
|
||||||
|
function loadData() {
|
||||||
|
fetch('ambil_data.php')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
map.eachLayer(l => { if (l instanceof L.Marker) map.removeLayer(l); });
|
||||||
|
|
||||||
|
data.forEach(d => {
|
||||||
|
const jam24 = d.status === 'yes';
|
||||||
|
const mk = L.marker([parseFloat(d.latitude), parseFloat(d.longitude)], {
|
||||||
|
icon: jam24 ? ikonHijau : ikonMerah,
|
||||||
|
draggable: true
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
mk.bindPopup(popupHTML(d, jam24));
|
||||||
|
|
||||||
|
// drag → pindah lokasi
|
||||||
|
mk.on('dragend', function(e) {
|
||||||
|
const p = e.target.getLatLng();
|
||||||
|
kirim('update.php', {
|
||||||
|
id: d.id, nama: d.nama_spbu, wa: d.no_wa,
|
||||||
|
status: d.status, lat: p.lat, lng: p.lng
|
||||||
|
}).then(r => {
|
||||||
|
if (r === 'success') { toast('📍 Lokasi dipindahkan'); loadData(); }
|
||||||
|
else toast('Gagal pindah: ' + r, 'err');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => toast('Gagal memuat data: ' + err, 'err'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POPUP HTML ────────────────────────────────────────────
|
||||||
|
function popupHTML(d, jam24) {
|
||||||
|
const bgBadge = jam24 ? '#d4edda' : '#f8d7da';
|
||||||
|
const clBadge = jam24 ? '#155724' : '#721c24';
|
||||||
|
const label = jam24 ? 'Buka 24 Jam' : 'Tidak 24 Jam';
|
||||||
|
return `
|
||||||
|
<div style="min-width:180px;font-family:'Segoe UI',sans-serif">
|
||||||
|
<b style="font-size:14px">${d.nama_spbu}</b><br>
|
||||||
|
<span style="color:#666;font-size:12px">📞 ${d.no_wa}</span><br>
|
||||||
|
<span style="display:inline-block;margin-top:5px;padding:2px 10px;
|
||||||
|
border-radius:12px;font-size:11px;font-weight:600;
|
||||||
|
background:${bgBadge};color:${clBadge}">⏰ ${label}</span>
|
||||||
|
<hr style="margin:8px 0;border:none;border-top:1px solid #eee">
|
||||||
|
<div style="display:flex;gap:6px">
|
||||||
|
<button onclick="bukaEdit(${d.id},'${esc(d.nama_spbu)}','${esc(d.no_wa)}','${d.status}',${d.latitude},${d.longitude})"
|
||||||
|
style="flex:1;padding:6px 0;background:#3498db;color:#fff;border:none;border-radius:5px;cursor:pointer;font-size:12px;font-weight:600">
|
||||||
|
✏️ Edit
|
||||||
|
</button>
|
||||||
|
<button onclick="hapus(${d.id})"
|
||||||
|
style="flex:1;padding:6px 0;background:#e74c3c;color:#fff;border:none;border-radius:5px;cursor:pointer;font-size:12px;font-weight:600">
|
||||||
|
🗑️ Hapus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) { return String(s).replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
||||||
|
|
||||||
|
// ── FETCH HELPER ──────────────────────────────────────────
|
||||||
|
function kirim(url, data) {
|
||||||
|
return fetch(url, { method:'POST', body: new URLSearchParams(data) })
|
||||||
|
.then(r => r.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── KLIK PETA → TAMBAH ────────────────────────────────────
|
||||||
|
map.on('click', function(e) {
|
||||||
|
document.getElementById('tLat').value = e.latlng.lat;
|
||||||
|
document.getElementById('tLng').value = e.latlng.lng;
|
||||||
|
document.getElementById('tNama').value = '';
|
||||||
|
document.getElementById('tWa').value = '';
|
||||||
|
document.getElementById('tStatus').value = 'yes';
|
||||||
|
document.getElementById('modalTambah').classList.add('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnBatalTambah').onclick = function() {
|
||||||
|
document.getElementById('modalTambah').classList.remove('show');
|
||||||
|
};
|
||||||
|
document.getElementById('modalTambah').onclick = function(e) {
|
||||||
|
if (e.target === this) this.classList.remove('show');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('btnSimpanTambah').onclick = function() {
|
||||||
|
const nama = document.getElementById('tNama').value.trim();
|
||||||
|
const wa = document.getElementById('tWa').value.trim();
|
||||||
|
const status = document.getElementById('tStatus').value;
|
||||||
|
const lat = document.getElementById('tLat').value;
|
||||||
|
const lng = document.getElementById('tLng').value;
|
||||||
|
|
||||||
|
if (!nama || !wa) { toast('Nama & No. WA wajib diisi', 'err'); return; }
|
||||||
|
|
||||||
|
kirim('simpan.php', { nama, wa, status, lat, lng }).then(r => {
|
||||||
|
if (r === 'success') {
|
||||||
|
document.getElementById('modalTambah').classList.remove('show');
|
||||||
|
toast('✅ SPBU berhasil ditambahkan');
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast('Gagal simpan: ' + r, 'err');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── EDIT ──────────────────────────────────────────────────
|
||||||
|
window.bukaEdit = function(id, nama, wa, status, lat, lng) {
|
||||||
|
document.getElementById('eId').value = id;
|
||||||
|
document.getElementById('eNama').value = nama;
|
||||||
|
document.getElementById('eWa').value = wa;
|
||||||
|
document.getElementById('eStatus').value = status;
|
||||||
|
document.getElementById('eLat').value = lat;
|
||||||
|
document.getElementById('eLng').value = lng;
|
||||||
|
document.getElementById('modalEdit').classList.add('show');
|
||||||
|
map.closePopup();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('btnBatalEdit').onclick = function() {
|
||||||
|
document.getElementById('modalEdit').classList.remove('show');
|
||||||
|
};
|
||||||
|
document.getElementById('modalEdit').onclick = function(e) {
|
||||||
|
if (e.target === this) this.classList.remove('show');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('btnSimpanEdit').onclick = function() {
|
||||||
|
const id = document.getElementById('eId').value;
|
||||||
|
const nama = document.getElementById('eNama').value.trim();
|
||||||
|
const wa = document.getElementById('eWa').value.trim();
|
||||||
|
const status = document.getElementById('eStatus').value;
|
||||||
|
const lat = document.getElementById('eLat').value;
|
||||||
|
const lng = document.getElementById('eLng').value;
|
||||||
|
|
||||||
|
if (!nama || !wa) { toast('Nama & No. WA wajib diisi', 'err'); return; }
|
||||||
|
|
||||||
|
kirim('update.php', { id, nama, wa, status, lat, lng }).then(r => {
|
||||||
|
if (r === 'success') {
|
||||||
|
document.getElementById('modalEdit').classList.remove('show');
|
||||||
|
toast('✅ Data berhasil diperbarui');
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast('Gagal update: ' + r, 'err');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── HAPUS ─────────────────────────────────────────────────
|
||||||
|
window.hapus = function(id) {
|
||||||
|
if (!confirm('Yakin hapus SPBU ini?')) return;
|
||||||
|
kirim('hapus.php', { id }).then(r => {
|
||||||
|
if (r === 'success') {
|
||||||
|
map.closePopup();
|
||||||
|
toast('🗑️ Data dihapus');
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
toast('Gagal hapus: ' + r, 'err');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── MULAI ─────────────────────────────────────────────────
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$user = getenv('DB_USER') ?: 'root';
|
||||||
|
$pass = getenv('DB_PASSWORD') !== false ? getenv('DB_PASSWORD') : '';
|
||||||
|
$db = getenv('DB_NAME') ?: 'db_spbu';
|
||||||
|
|
||||||
|
$conn = mysqli_connect($host, $user, $pass, $db);
|
||||||
|
|
||||||
|
if (!$conn) {
|
||||||
|
die("Koneksi database gagal: " . mysqli_connect_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
mysqli_set_charset($conn, 'utf8mb4');
|
||||||
|
?>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$nama = $conn->real_escape_string($_POST['nama']);
|
||||||
|
$wa = $conn->real_escape_string($_POST['wa']);
|
||||||
|
$status = $conn->real_escape_string($_POST['status']);
|
||||||
|
$lat = $conn->real_escape_string($_POST['lat']);
|
||||||
|
$lng = $conn->real_escape_string($_POST['lng']);
|
||||||
|
|
||||||
|
$query = "INSERT INTO spbu (nama_spbu, no_wa, status, latitude, longitude) VALUES ('$nama','$wa','$status','$lat','$lng')";
|
||||||
|
|
||||||
|
if ($conn->query($query)) {
|
||||||
|
echo "success";
|
||||||
|
} else {
|
||||||
|
echo "error: " . $conn->error;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$id = $conn->real_escape_string($_POST['id']);
|
||||||
|
$nama = $conn->real_escape_string($_POST['nama']);
|
||||||
|
$wa = $conn->real_escape_string($_POST['wa']);
|
||||||
|
$status = $conn->real_escape_string($_POST['status']);
|
||||||
|
$lat = $conn->real_escape_string($_POST['lat']);
|
||||||
|
$lng = $conn->real_escape_string($_POST['lng']);
|
||||||
|
|
||||||
|
$query = "UPDATE spbu SET nama_spbu='$nama', no_wa='$wa', status='$status', latitude='$lat', longitude='$lng' WHERE id='$id'";
|
||||||
|
|
||||||
|
if ($conn->query($query)) {
|
||||||
|
echo "success";
|
||||||
|
} else {
|
||||||
|
echo "error: " . $conn->error;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$user = getenv('DB_USER') ?: 'root';
|
||||||
|
$pass = getenv('DB_PASSWORD') !== false ? getenv('DB_PASSWORD') : '';
|
||||||
|
$db = getenv('DB_NAME') ?: 'webgis_db';
|
||||||
|
|
||||||
|
$conn = mysqli_connect($host, $user, $pass, $db);
|
||||||
|
|
||||||
|
if (!$conn) {
|
||||||
|
die(json_encode([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Koneksi database gagal: ' . mysqli_connect_error()
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
mysqli_set_charset($conn, 'utf8mb4');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
-- ================================================================
|
||||||
|
-- FILE : database.sql
|
||||||
|
-- DATABASE: webgis_db
|
||||||
|
-- Jalankan file ini di phpMyAdmin > tab SQL > klik Go
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS webgis_db
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE webgis_db;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- TABEL tb_jalan (Polyline) — ditambah kolom foto
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_jalan (
|
||||||
|
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
nama_jalan VARCHAR(200) NOT NULL COMMENT 'Nama ruas jalan',
|
||||||
|
status_jalan ENUM('Nasional','Provinsi','Kabupaten')
|
||||||
|
NOT NULL DEFAULT 'Kabupaten',
|
||||||
|
panjang_meter DECIMAL(15,2) NOT NULL DEFAULT 0,
|
||||||
|
geojson LONGTEXT NOT NULL,
|
||||||
|
keterangan TEXT,
|
||||||
|
foto VARCHAR(255) NULL COMMENT 'Path file foto jalan',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- TABEL tb_parsil (Polygon) — ditambah kolom foto
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_parsil (
|
||||||
|
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
nama_pemilik VARCHAR(200) NOT NULL,
|
||||||
|
no_sertifikat VARCHAR(100) NOT NULL,
|
||||||
|
status_kepemilikan ENUM('SHM','HGB','HGU','HP')
|
||||||
|
NOT NULL DEFAULT 'SHM',
|
||||||
|
luas_m2 DECIMAL(20,4) NOT NULL DEFAULT 0,
|
||||||
|
geojson LONGTEXT NOT NULL,
|
||||||
|
alamat TEXT,
|
||||||
|
foto VARCHAR(255) NULL COMMENT 'Path file foto parsil',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- TABEL tb_jalan_rusak (Point) — fitur baru
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS tb_jalan_rusak (
|
||||||
|
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
lokasi VARCHAR(200) NOT NULL COMMENT 'Nama/deskripsi lokasi kerusakan',
|
||||||
|
tingkat ENUM('Ringan','Sedang','Berat')
|
||||||
|
NOT NULL DEFAULT 'Sedang' COMMENT 'Tingkat kerusakan',
|
||||||
|
jenis VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'Jenis kerusakan: berlubang, retak, dll',
|
||||||
|
geojson LONGTEXT NOT NULL COMMENT 'Geometry Point format GeoJSON',
|
||||||
|
keterangan TEXT,
|
||||||
|
foto VARCHAR(255) NULL COMMENT 'Path file foto kerusakan',
|
||||||
|
tanggal_lapor DATE NOT NULL DEFAULT (CURRENT_DATE),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- CONTOH DATA tb_jalan
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
INSERT INTO tb_jalan (nama_jalan, status_jalan, panjang_meter, geojson, keterangan) VALUES
|
||||||
|
('Jl. Ahmad Yani','Nasional',1240.50,'{"type":"Feature","geometry":{"type":"LineString","coordinates":[[109.3272,-0.0207],[109.3290,-0.0215],[109.3310,-0.0225],[109.3335,-0.0238]]},"properties":{}}','Jalan Nasional utama kota Pontianak'),
|
||||||
|
('Jl. Gajah Mada','Provinsi',870.00,'{"type":"Feature","geometry":{"type":"LineString","coordinates":[[109.3250,-0.0260],[109.3265,-0.0270],[109.3280,-0.0285]]},"properties":{}}','Jalan Provinsi pusat kota'),
|
||||||
|
('Jl. Pahlawan','Kabupaten',540.75,'{"type":"Feature","geometry":{"type":"LineString","coordinates":[[109.3300,-0.0300],[109.3315,-0.0310],[109.3325,-0.0322]]},"properties":{}}','Jalan Kabupaten lingkungan');
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- CONTOH DATA tb_parsil
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
INSERT INTO tb_parsil (nama_pemilik, no_sertifikat, status_kepemilikan, luas_m2, geojson, alamat) VALUES
|
||||||
|
('Budi Santoso','SHM-001/PTK/2023','SHM',4500.00,'{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[109.3280,-0.0240],[109.3295,-0.0240],[109.3295,-0.0255],[109.3280,-0.0255],[109.3280,-0.0240]]]},"properties":{}}','Jl. Ahmad Yani No. 10, Pontianak'),
|
||||||
|
('PT. Maju Jaya','HGB-005/PTK/2022','HGB',12000.00,'{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[109.3310,-0.0220],[109.3330,-0.0220],[109.3330,-0.0240],[109.3310,-0.0240],[109.3310,-0.0220]]]},"properties":{}}','Kawasan Industri Pontianak');
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- CONTOH DATA tb_jalan_rusak
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
INSERT INTO tb_jalan_rusak (lokasi, tingkat, jenis, geojson, keterangan, tanggal_lapor) VALUES
|
||||||
|
('Depan SPBU Jl. Ahmad Yani','Berat','Berlubang','{"type":"Feature","geometry":{"type":"Point","coordinates":[109.3285,-0.0212]},"properties":{}}','Lubang besar diameter ±50cm, berbahaya bagi pengendara','2024-10-15'),
|
||||||
|
('Pertigaan Jl. Gajah Mada','Sedang','Retak','{"type":"Feature","geometry":{"type":"Point","coordinates":[109.3260,-0.0268]},"properties":{}}','Retak memanjang sepanjang 3 meter','2024-11-02');
|
||||||
@@ -0,0 +1,886 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGIS - Jalan, Parsil & Jalan Rusak</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css"/>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box;}
|
||||||
|
body{font-family:'Segoe UI',sans-serif;background:#1a1d23;color:#e0e0e0;height:100vh;display:flex;flex-direction:column;}
|
||||||
|
|
||||||
|
/* HEADER */
|
||||||
|
header{background:#111318;padding:0 16px;height:56px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #2a2d35;z-index:1000;flex-shrink:0;gap:10px;}
|
||||||
|
.header-brand{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:700;color:#fff;white-space:nowrap;}
|
||||||
|
.header-brand i{color:#4ade80;font-size:18px;}
|
||||||
|
.header-tabs{display:flex;gap:3px;flex-shrink:0;}
|
||||||
|
.tab-btn{padding:5px 13px;border-radius:6px;border:none;cursor:pointer;font-size:12px;font-weight:500;transition:all .2s;background:transparent;color:#888;white-space:nowrap;}
|
||||||
|
.tab-btn.active{background:#2a5bd7;color:#fff;}
|
||||||
|
.tab-btn:hover:not(.active){background:#2a2d35;color:#ddd;}
|
||||||
|
.dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px;vertical-align:middle;}
|
||||||
|
.dot-jalan{background:#f59e0b;}.dot-parsil{background:#4ade80;}.dot-rusak{background:#ef4444;}
|
||||||
|
.count-badge{background:#2a5bd733;color:#60a5fa;font-size:10px;padding:1px 6px;border-radius:10px;margin-left:5px;}
|
||||||
|
|
||||||
|
/* SEARCH BAR */
|
||||||
|
.search-wrap{flex:1;max-width:280px;position:relative;}
|
||||||
|
.search-wrap input{width:100%;padding:6px 10px 6px 32px;background:#1a1d23;border:1px solid #2a2d35;border-radius:6px;color:#e0e0e0;font-size:12px;outline:none;}
|
||||||
|
.search-wrap input:focus{border-color:#2a5bd7;}
|
||||||
|
.search-wrap i{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#555;font-size:12px;}
|
||||||
|
.search-wrap .btn-clear-s{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#555;cursor:pointer;font-size:11px;display:none;}
|
||||||
|
.search-wrap input:not(:placeholder-shown) ~ .btn-clear-s{display:block;}
|
||||||
|
|
||||||
|
/* LAYOUT */
|
||||||
|
.main-layout{display:flex;flex:1;overflow:hidden;}
|
||||||
|
|
||||||
|
/* SIDEBAR */
|
||||||
|
.sidebar{width:320px;background:#111318;border-right:1px solid #2a2d35;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden;}
|
||||||
|
.sidebar-header{padding:12px 14px;border-bottom:1px solid #2a2d35;display:flex;align-items:center;justify-content:space-between;}
|
||||||
|
.sidebar-title{font-size:12px;font-weight:600;color:#aaa;text-transform:uppercase;letter-spacing:.5px;}
|
||||||
|
.btn-tambah{background:#2a5bd7;color:#fff;border:none;border-radius:6px;padding:5px 11px;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:4px;}
|
||||||
|
.btn-tambah:hover{background:#3467e8;}
|
||||||
|
|
||||||
|
/* LIST */
|
||||||
|
.data-list{flex:1;overflow-y:auto;padding:7px;}
|
||||||
|
.data-list::-webkit-scrollbar{width:3px;}
|
||||||
|
.data-list::-webkit-scrollbar-thumb{background:#333;border-radius:2px;}
|
||||||
|
.data-card{background:#1a1d23;border:1px solid #2a2d35;border-radius:8px;padding:9px 11px;margin-bottom:6px;cursor:pointer;transition:all .15s;}
|
||||||
|
.data-card:hover{border-color:#3d4150;background:#1e2128;}
|
||||||
|
.data-card.selected{border-color:#2a5bd7;background:#1a2540;}
|
||||||
|
.card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;}
|
||||||
|
.card-name{font-size:13px;font-weight:600;color:#e0e0e0;flex:1;}
|
||||||
|
.card-badge{font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600;white-space:nowrap;flex-shrink:0;}
|
||||||
|
.badge-nasional{background:#7c3aed22;color:#a78bfa;border:1px solid #7c3aed44;}
|
||||||
|
.badge-provinsi{background:#05966922;color:#34d399;border:1px solid #05966944;}
|
||||||
|
.badge-kabupaten{background:#d9770622;color:#fbbf24;border:1px solid #d9770644;}
|
||||||
|
.badge-shm{background:#1d4ed822;color:#60a5fa;border:1px solid #1d4ed844;}
|
||||||
|
.badge-hgb{background:#7c3aed22;color:#a78bfa;border:1px solid #7c3aed44;}
|
||||||
|
.badge-hgu{background:#d9770622;color:#fbbf24;border:1px solid #d9770644;}
|
||||||
|
.badge-hp{background:#05966922;color:#34d399;border:1px solid #05966944;}
|
||||||
|
.badge-ringan{background:#16a34a22;color:#4ade80;border:1px solid #16a34a44;}
|
||||||
|
.badge-sedang{background:#d9770622;color:#fb923c;border:1px solid #d9770644;}
|
||||||
|
.badge-berat{background:#7f1d1d22;color:#f87171;border:1px solid #7f1d1d44;}
|
||||||
|
.card-meta{margin-top:4px;font-size:11px;color:#666;}
|
||||||
|
.card-meta span{margin-right:8px;}
|
||||||
|
.card-meta i{margin-right:3px;}
|
||||||
|
.card-thumb{width:100%;height:60px;object-fit:cover;border-radius:4px;margin-top:6px;border:1px solid #2a2d35;}
|
||||||
|
.card-actions{display:flex;gap:3px;margin-top:7px;}
|
||||||
|
.btn-sm{padding:4px 7px;border-radius:4px;border:none;cursor:pointer;font-size:11px;font-weight:500;transition:all .15s;}
|
||||||
|
.btn-zoom{background:#1e3a8a22;color:#60a5fa;border:1px solid #1e3a8a44;}
|
||||||
|
.btn-edit{background:#78350f22;color:#fbbf24;border:1px solid #78350f44;}
|
||||||
|
.btn-hapus{background:#7f1d1d22;color:#f87171;border:1px solid #7f1d1d44;}
|
||||||
|
.btn-sm:hover{filter:brightness(1.3);}
|
||||||
|
|
||||||
|
/* LEGEND */
|
||||||
|
.legend-box{padding:9px 14px;border-top:1px solid #2a2d35;font-size:11px;}
|
||||||
|
.legend-title{color:#888;margin-bottom:5px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;font-size:10px;}
|
||||||
|
.legend-item{display:flex;align-items:center;gap:5px;margin-bottom:3px;color:#aaa;}
|
||||||
|
.legend-line{width:20px;height:3px;border-radius:2px;}
|
||||||
|
.legend-poly{width:12px;height:12px;border-radius:2px;border:2px solid;}
|
||||||
|
.legend-point{width:10px;height:10px;border-radius:50%;border:2px solid;}
|
||||||
|
|
||||||
|
/* MAP */
|
||||||
|
#map{flex:1;}
|
||||||
|
|
||||||
|
/* MODAL */
|
||||||
|
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:9999;align-items:center;justify-content:center;}
|
||||||
|
.modal-overlay.show{display:flex;}
|
||||||
|
.modal{background:#111318;border:1px solid #2a2d35;border-radius:12px;padding:22px;width:440px;max-width:95vw;max-height:90vh;overflow-y:auto;}
|
||||||
|
.modal h3{font-size:15px;color:#fff;margin-bottom:16px;}
|
||||||
|
.form-group{margin-bottom:12px;}
|
||||||
|
.form-group label{display:block;font-size:11px;color:#888;margin-bottom:4px;font-weight:500;}
|
||||||
|
.form-group input,.form-group select,.form-group textarea{width:100%;padding:8px 10px;background:#1a1d23;border:1px solid #2a2d35;border-radius:6px;color:#e0e0e0;font-size:13px;outline:none;}
|
||||||
|
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:#2a5bd7;}
|
||||||
|
.form-group input[type=file]{padding:5px;color:#aaa;cursor:pointer;}
|
||||||
|
.readonly-val{padding:8px 10px;background:#0d0f14;border:1px solid #1e2128;border-radius:6px;color:#4ade80;font-size:13px;font-weight:600;}
|
||||||
|
.modal-actions{display:flex;gap:8px;margin-top:16px;justify-content:flex-end;}
|
||||||
|
.btn-primary{background:#2a5bd7;color:#fff;border:none;border-radius:6px;padding:8px 16px;font-size:13px;cursor:pointer;}
|
||||||
|
.btn-primary:hover{background:#3467e8;}
|
||||||
|
.btn-cancel{background:#2a2d35;color:#aaa;border:none;border-radius:6px;padding:8px 16px;font-size:13px;cursor:pointer;}
|
||||||
|
.foto-preview-wrap{margin-top:6px;}
|
||||||
|
.foto-preview-wrap img{width:100%;max-height:140px;object-fit:cover;border-radius:6px;border:1px solid #2a2d35;}
|
||||||
|
.foto-current{font-size:11px;color:#60a5fa;margin-bottom:4px;}
|
||||||
|
|
||||||
|
/* DRAW INFO */
|
||||||
|
.draw-info{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);background:#111318dd;color:#e0e0e0;padding:8px 16px;border-radius:8px;font-size:12px;z-index:999;border:1px solid #2a5bd7;display:none;pointer-events:none;}
|
||||||
|
.draw-info.show{display:block;}
|
||||||
|
|
||||||
|
/* EMPTY STATE */
|
||||||
|
.empty-state{text-align:center;padding:28px 16px;color:#555;}
|
||||||
|
.empty-state i{font-size:26px;margin-bottom:7px;display:block;}
|
||||||
|
.empty-state p{font-size:12px;}
|
||||||
|
|
||||||
|
/* LEAFLET POPUP */
|
||||||
|
.leaflet-popup-content-wrapper{background:#111318;border:1px solid #2a2d35;border-radius:8px;color:#e0e0e0;}
|
||||||
|
.leaflet-popup-tip{background:#111318;}
|
||||||
|
.popup-title{font-weight:600;font-size:13px;margin-bottom:5px;color:#fff;}
|
||||||
|
.popup-row{font-size:11px;color:#aaa;margin-bottom:2px;}
|
||||||
|
.popup-row span{color:#e0e0e0;}
|
||||||
|
.popup-foto{width:100%;max-height:100px;object-fit:cover;border-radius:4px;margin-top:6px;}
|
||||||
|
|
||||||
|
/* TOAST */
|
||||||
|
.toast{position:fixed;bottom:24px;right:24px;background:#1e2a1e;border:1px solid #4ade80;color:#4ade80;padding:10px 18px;border-radius:8px;font-size:13px;z-index:99999;transform:translateY(60px);opacity:0;transition:all .3s;}
|
||||||
|
.toast.show{transform:translateY(0);opacity:1;}
|
||||||
|
.toast.error{background:#2a1e1e;border-color:#f87171;color:#f87171;}
|
||||||
|
|
||||||
|
/* STAT BADGE */
|
||||||
|
.stat-row{display:flex;gap:6px;padding:8px 14px;border-top:1px solid #2a2d35;flex-shrink:0;}
|
||||||
|
.stat-item{flex:1;background:#1a1d23;border:1px solid #2a2d35;border-radius:6px;padding:5px 8px;text-align:center;}
|
||||||
|
.stat-item .sv{font-size:16px;font-weight:700;color:#e0e0e0;}
|
||||||
|
.stat-item .sl{font-size:9px;color:#666;margin-top:1px;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<header>
|
||||||
|
<div class="header-brand">
|
||||||
|
<i class="fas fa-map-marked-alt"></i>
|
||||||
|
WebGIS Jalan & Parsil
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="search-wrap">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
<input type="text" id="searchInput" placeholder="Cari data..." oninput="handleSearch(this.value)">
|
||||||
|
<button class="btn-clear-s" onclick="clearSearch()"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-tabs">
|
||||||
|
<button class="tab-btn active" id="tabJalan" onclick="switchTab('jalan')">
|
||||||
|
<span class="dot dot-jalan"></span>Jalan
|
||||||
|
<span class="count-badge" id="countJalan">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" id="tabParsil" onclick="switchTab('parsil')">
|
||||||
|
<span class="dot dot-parsil"></span>Parsil
|
||||||
|
<span class="count-badge" id="countParsil">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" id="tabRusak" onclick="switchTab('rusak')">
|
||||||
|
<span class="dot dot-rusak"></span>Jalan Rusak
|
||||||
|
<span class="count-badge" id="countRusak">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="main-layout">
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<span class="sidebar-title" id="sidebarTitle">Data Jalan</span>
|
||||||
|
<button class="btn-tambah" onclick="startDraw()">
|
||||||
|
<i class="fas fa-plus"></i> Tambah
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-list" id="listJalan"><div class="empty-state"><i class="fas fa-road"></i><p>Belum ada data jalan</p></div></div>
|
||||||
|
<div class="data-list" id="listParsil" style="display:none"><div class="empty-state"><i class="fas fa-map"></i><p>Belum ada data parsil</p></div></div>
|
||||||
|
<div class="data-list" id="listRusak" style="display:none"><div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>Belum ada data jalan rusak</p></div></div>
|
||||||
|
|
||||||
|
<!-- STAT -->
|
||||||
|
<div class="stat-row" id="statRow">
|
||||||
|
<div class="stat-item"><div class="sv" id="statJalan">0</div><div class="sl">Jalan</div></div>
|
||||||
|
<div class="stat-item"><div class="sv" id="statParsil">0</div><div class="sl">Parsil</div></div>
|
||||||
|
<div class="stat-item"><div class="sv" id="statRusak">0</div><div class="sl">Rusak</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LEGEND -->
|
||||||
|
<div class="legend-box" id="legendJalan">
|
||||||
|
<div class="legend-title">Keterangan Jalan</div>
|
||||||
|
<div class="legend-item"><div class="legend-line" style="background:#a78bfa"></div> Nasional</div>
|
||||||
|
<div class="legend-item"><div class="legend-line" style="background:#34d399"></div> Provinsi</div>
|
||||||
|
<div class="legend-item"><div class="legend-line" style="background:#fbbf24"></div> Kabupaten</div>
|
||||||
|
</div>
|
||||||
|
<div class="legend-box" id="legendParsil" style="display:none">
|
||||||
|
<div class="legend-title">Status Kepemilikan</div>
|
||||||
|
<div class="legend-item"><div class="legend-poly" style="background:#1d4ed822;border-color:#60a5fa"></div> SHM</div>
|
||||||
|
<div class="legend-item"><div class="legend-poly" style="background:#7c3aed22;border-color:#a78bfa"></div> HGB</div>
|
||||||
|
<div class="legend-item"><div class="legend-poly" style="background:#d9770622;border-color:#fbbf24"></div> HGU</div>
|
||||||
|
<div class="legend-item"><div class="legend-poly" style="background:#05966922;border-color:#34d399"></div> HP</div>
|
||||||
|
</div>
|
||||||
|
<div class="legend-box" id="legendRusak" style="display:none">
|
||||||
|
<div class="legend-title">Tingkat Kerusakan</div>
|
||||||
|
<div class="legend-item"><div class="legend-point" style="background:#4ade8044;border-color:#4ade80"></div> Ringan</div>
|
||||||
|
<div class="legend-item"><div class="legend-point" style="background:#fb923c44;border-color:#fb923c"></div> Sedang</div>
|
||||||
|
<div class="legend-item"><div class="legend-point" style="background:#f8717144;border-color:#f87171"></div> Berat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- draw hint -->
|
||||||
|
<div class="draw-info" id="drawInfo">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span id="drawInfoText">Klik pada peta untuk menggambar.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOAST -->
|
||||||
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
|
<!-- ===================== MODAL JALAN ===================== -->
|
||||||
|
<div class="modal-overlay" id="modalJalan">
|
||||||
|
<div class="modal">
|
||||||
|
<h3 id="modalJalanTitle"><i class="fas fa-road" style="color:#fbbf24;margin-right:8px"></i>Data Jalan</h3>
|
||||||
|
<div class="form-group"><label>Nama Jalan *</label><input type="text" id="jNama" placeholder="Contoh: Jl. Sudirman"></div>
|
||||||
|
<div class="form-group"><label>Status Jalan *</label>
|
||||||
|
<select id="jStatus"><option value="Nasional">Nasional</option><option value="Provinsi">Provinsi</option><option value="Kabupaten">Kabupaten</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label><i class="fas fa-ruler" style="margin-right:4px"></i>Panjang (otomatis)</label><div class="readonly-val" id="jPanjang">- meter</div></div>
|
||||||
|
<div class="form-group"><label>Keterangan</label><textarea id="jKet" rows="2" placeholder="Keterangan tambahan..."></textarea></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label><i class="fas fa-camera" style="margin-right:4px"></i>Foto Jalan</label>
|
||||||
|
<div class="foto-current" id="jFotoCurrent" style="display:none"><i class="fas fa-image"></i> Foto saat ini tersimpan</div>
|
||||||
|
<input type="file" id="jFoto" accept="image/jpeg,image/png,image/webp" onchange="previewFoto(this,'jFotoPreview')">
|
||||||
|
<div class="foto-preview-wrap" id="jFotoPreview" style="display:none"><img src="" alt="preview"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||||
|
<button class="btn-primary" onclick="saveJalan()"><i class="fas fa-save" style="margin-right:5px"></i>Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== MODAL PARSIL ===================== -->
|
||||||
|
<div class="modal-overlay" id="modalParsil">
|
||||||
|
<div class="modal">
|
||||||
|
<h3 id="modalParsilTitle"><i class="fas fa-map" style="color:#4ade80;margin-right:8px"></i>Data Parsil Tanah</h3>
|
||||||
|
<div class="form-group"><label>Nama Pemilik *</label><input type="text" id="pNama" placeholder="Nama lengkap pemilik"></div>
|
||||||
|
<div class="form-group"><label>Nomor Sertifikat *</label><input type="text" id="pNoSert" placeholder="Contoh: SHM-001/KEL/2024"></div>
|
||||||
|
<div class="form-group"><label>Status Kepemilikan *</label>
|
||||||
|
<select id="pStatus"><option value="SHM">SHM - Hak Milik</option><option value="HGB">HGB - Hak Guna Bangunan</option><option value="HGU">HGU - Hak Guna Usaha</option><option value="HP">HP - Hak Pakai</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label><i class="fas fa-vector-square" style="margin-right:4px"></i>Luas (otomatis)</label><div class="readonly-val" id="pLuas">- m²</div></div>
|
||||||
|
<div class="form-group"><label>Alamat</label><textarea id="pAlamat" rows="2" placeholder="Alamat lokasi parsil..."></textarea></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label><i class="fas fa-camera" style="margin-right:4px"></i>Foto Parsil</label>
|
||||||
|
<div class="foto-current" id="pFotoCurrent" style="display:none"><i class="fas fa-image"></i> Foto saat ini tersimpan</div>
|
||||||
|
<input type="file" id="pFoto" accept="image/jpeg,image/png,image/webp" onchange="previewFoto(this,'pFotoPreview')">
|
||||||
|
<div class="foto-preview-wrap" id="pFotoPreview" style="display:none"><img src="" alt="preview"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||||
|
<button class="btn-primary" onclick="saveParsil()"><i class="fas fa-save" style="margin-right:5px"></i>Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== MODAL JALAN RUSAK ===================== -->
|
||||||
|
<div class="modal-overlay" id="modalRusak">
|
||||||
|
<div class="modal">
|
||||||
|
<h3 id="modalRusakTitle"><i class="fas fa-exclamation-triangle" style="color:#ef4444;margin-right:8px"></i>Data Jalan Rusak</h3>
|
||||||
|
<div class="form-group"><label>Nama / Lokasi *</label><input type="text" id="rLokasi" placeholder="Contoh: Depan SPBU Jl. Ahmad Yani"></div>
|
||||||
|
<div class="form-group"><label>Tingkat Kerusakan *</label>
|
||||||
|
<select id="rTingkat">
|
||||||
|
<option value="Ringan">Ringan</option>
|
||||||
|
<option value="Sedang" selected>Sedang</option>
|
||||||
|
<option value="Berat">Berat</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label>Jenis Kerusakan</label><input type="text" id="rJenis" placeholder="Berlubang / Retak / Amblas..."></div>
|
||||||
|
<div class="form-group"><label>Tanggal Lapor</label><input type="date" id="rTanggal"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label><i class="fas fa-map-pin" style="margin-right:4px"></i>Koordinat (otomatis dari peta)</label>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<div class="readonly-val" id="rKoord" style="flex:1">- klik titik di peta -</div>
|
||||||
|
<button type="button" onclick="pickTitikPeta()" style="background:#1e3a8a;color:#60a5fa;border:1px solid #1e3a8a44;border-radius:6px;padding:7px 10px;font-size:12px;cursor:pointer;white-space:nowrap;flex-shrink:0"><i class="fas fa-map-marker-alt"></i> Pilih di Peta</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label>Keterangan</label><textarea id="rKet" rows="2" placeholder="Deskripsi kerusakan..."></textarea></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label><i class="fas fa-camera" style="margin-right:4px"></i>Foto Kerusakan</label>
|
||||||
|
<div class="foto-current" id="rFotoCurrent" style="display:none"><i class="fas fa-image"></i> Foto saat ini tersimpan</div>
|
||||||
|
<input type="file" id="rFoto" accept="image/jpeg,image/png,image/webp" onchange="previewFoto(this,'rFotoPreview')">
|
||||||
|
<div class="foto-preview-wrap" id="rFotoPreview" style="display:none"><img src="" alt="preview"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||||
|
<button class="btn-primary" onclick="saveRusak()"><i class="fas fa-save" style="margin-right:5px"></i>Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JS LIBRARIES -->
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||||
|
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// =============================================================
|
||||||
|
// 1. INIT MAP
|
||||||
|
// =============================================================
|
||||||
|
const map = L.map('map').setView([-0.02, 109.34], 13);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap contributors'}).addTo(map);
|
||||||
|
|
||||||
|
const layerJalan = L.layerGroup().addTo(map);
|
||||||
|
const layerParsil = L.layerGroup().addTo(map);
|
||||||
|
const layerRusak = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 2. WARNA & IKON
|
||||||
|
// =============================================================
|
||||||
|
const WARNA_JALAN = {
|
||||||
|
'Nasional': {color:'#a78bfa',weight:5,opacity:.9},
|
||||||
|
'Provinsi': {color:'#34d399',weight:4,opacity:.9},
|
||||||
|
'Kabupaten':{color:'#fbbf24',weight:3,opacity:.9}
|
||||||
|
};
|
||||||
|
const WARNA_PARSIL = {
|
||||||
|
'SHM':{color:'#60a5fa',fillColor:'#1d4ed8',fillOpacity:.25,weight:2},
|
||||||
|
'HGB':{color:'#a78bfa',fillColor:'#7c3aed',fillOpacity:.25,weight:2},
|
||||||
|
'HGU':{color:'#fbbf24',fillColor:'#d97706',fillOpacity:.25,weight:2},
|
||||||
|
'HP': {color:'#34d399',fillColor:'#059669',fillOpacity:.25,weight:2}
|
||||||
|
};
|
||||||
|
const WARNA_RUSAK = {
|
||||||
|
'Ringan':{color:'#4ade80',radius:8,fillColor:'#166534',fillOpacity:.8,weight:2},
|
||||||
|
'Sedang':{color:'#fb923c',radius:10,fillColor:'#c2410c',fillOpacity:.8,weight:2},
|
||||||
|
'Berat': {color:'#f87171',radius:12,fillColor:'#991b1b',fillOpacity:.8,weight:2}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 3. STATE
|
||||||
|
// =============================================================
|
||||||
|
let currentTab = 'jalan';
|
||||||
|
let editMode = false;
|
||||||
|
let editId = null;
|
||||||
|
let tempGeojson = null;
|
||||||
|
let tempPanjang = 0;
|
||||||
|
let tempLuas = 0;
|
||||||
|
let searchTimer = null;
|
||||||
|
|
||||||
|
const drawControl = new L.Control.Draw({
|
||||||
|
draw:{
|
||||||
|
polyline:{shapeOptions:{color:'#2a5bd7',weight:3}},
|
||||||
|
polygon: {shapeOptions:{color:'#4ade80',fillOpacity:.2}},
|
||||||
|
marker: {icon: L.divIcon({className:'',html:'<div style="width:18px;height:18px;border-radius:50%;background:#ef4444;border:3px solid #fff;box-shadow:0 2px 6px #0005"></div>',iconSize:[18,18],iconAnchor:[9,9]})},
|
||||||
|
rectangle:false, circle:false, circlemarker:false
|
||||||
|
},
|
||||||
|
edit:false
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 4. TOAST
|
||||||
|
// =============================================================
|
||||||
|
function toast(msg, type='ok'){
|
||||||
|
const t = document.getElementById('toast');
|
||||||
|
t.textContent = msg;
|
||||||
|
t.className = 'toast show' + (type==='error'?' error':'');
|
||||||
|
clearTimeout(t._timer);
|
||||||
|
t._timer = setTimeout(()=>t.classList.remove('show'), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 5. TAB SWITCH
|
||||||
|
// =============================================================
|
||||||
|
function switchTab(tab){
|
||||||
|
currentTab = tab;
|
||||||
|
['jalan','parsil','rusak'].forEach(t=>{
|
||||||
|
document.getElementById('tab'+cap(t)).classList.toggle('active', t===tab);
|
||||||
|
document.getElementById('list'+cap(t)).style.display = t===tab?'block':'none';
|
||||||
|
document.getElementById('legend'+cap(t)).style.display = t===tab?'block':'none';
|
||||||
|
});
|
||||||
|
document.getElementById('sidebarTitle').textContent =
|
||||||
|
tab==='jalan'?'Data Jalan':tab==='parsil'?'Parsil Tanah':'Jalan Rusak';
|
||||||
|
// clear search
|
||||||
|
document.getElementById('searchInput').value = '';
|
||||||
|
}
|
||||||
|
function cap(s){return s.charAt(0).toUpperCase()+s.slice(1);}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 6. SEARCH
|
||||||
|
// =============================================================
|
||||||
|
function handleSearch(q){
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
searchTimer = setTimeout(()=>{
|
||||||
|
if(q.trim()===''){
|
||||||
|
if(currentTab==='jalan') loadJalan();
|
||||||
|
else if(currentTab==='parsil') loadParsil();
|
||||||
|
else loadRusak();
|
||||||
|
} else {
|
||||||
|
if(currentTab==='jalan') loadJalan(q);
|
||||||
|
else if(currentTab==='parsil') loadParsil(q);
|
||||||
|
else loadRusak(q);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
function clearSearch(){
|
||||||
|
document.getElementById('searchInput').value='';
|
||||||
|
handleSearch('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 7. DRAW
|
||||||
|
// =============================================================
|
||||||
|
function pickTitikPeta(){
|
||||||
|
// Hide modal, activate marker draw, then re-show modal after click
|
||||||
|
document.getElementById('modalRusak').classList.remove('show');
|
||||||
|
map.addControl(drawControl);
|
||||||
|
const info = document.getElementById('drawInfo');
|
||||||
|
document.getElementById('drawInfoText').textContent = 'Klik lokasi jalan rusak di peta.';
|
||||||
|
info.classList.add('show');
|
||||||
|
new L.Draw.Marker(map, drawControl.options.draw.marker).enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDraw(){
|
||||||
|
editMode=false; editId=null;
|
||||||
|
map.addControl(drawControl);
|
||||||
|
const info=document.getElementById('drawInfo');
|
||||||
|
const txt=document.getElementById('drawInfoText');
|
||||||
|
if(currentTab==='jalan'){
|
||||||
|
new L.Draw.Polyline(map,drawControl.options.draw.polyline).enable();
|
||||||
|
txt.textContent='Klik untuk menambah titik jalan. Double-klik untuk selesai.';
|
||||||
|
} else if(currentTab==='parsil'){
|
||||||
|
new L.Draw.Polygon(map,drawControl.options.draw.polygon).enable();
|
||||||
|
txt.textContent='Klik untuk menggambar batas parsil. Klik titik awal untuk menutup.';
|
||||||
|
} else {
|
||||||
|
new L.Draw.Marker(map,drawControl.options.draw.marker).enable();
|
||||||
|
txt.textContent='Klik lokasi jalan rusak di peta.';
|
||||||
|
}
|
||||||
|
info.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on(L.Draw.Event.CREATED, function(e){
|
||||||
|
document.getElementById('drawInfo').classList.remove('show');
|
||||||
|
map.removeControl(drawControl);
|
||||||
|
const layer = e.layer;
|
||||||
|
const geoJSON = layer.toGeoJSON();
|
||||||
|
tempGeojson = JSON.stringify(geoJSON);
|
||||||
|
|
||||||
|
if(currentTab==='jalan' && e.layerType==='polyline'){
|
||||||
|
const lls = layer.getLatLngs();
|
||||||
|
tempPanjang = 0;
|
||||||
|
for(let i=0;i<lls.length-1;i++) tempPanjang+=lls[i].distanceTo(lls[i+1]);
|
||||||
|
document.getElementById('jPanjang').textContent = tempPanjang.toFixed(2)+' meter';
|
||||||
|
openModalJalan(false);
|
||||||
|
} else if(currentTab==='parsil' && e.layerType==='polygon'){
|
||||||
|
tempLuas = turf.area(geoJSON);
|
||||||
|
document.getElementById('pLuas').textContent = tempLuas.toFixed(2)+' m²';
|
||||||
|
openModalParsil(false);
|
||||||
|
} else if(currentTab==='rusak' && e.layerType==='marker'){
|
||||||
|
const ll = layer.getLatLng();
|
||||||
|
tempGeojson = JSON.stringify(layer.toGeoJSON());
|
||||||
|
document.getElementById('rKoord').textContent = ll.lat.toFixed(6)+', '+ll.lng.toFixed(6);
|
||||||
|
// If modal already open (re-pick mode), just update coords and re-show
|
||||||
|
document.getElementById('modalRusak').classList.add('show');
|
||||||
|
if(!editMode){
|
||||||
|
// fill fresh form
|
||||||
|
document.getElementById('rLokasi').value='';
|
||||||
|
document.getElementById('rTingkat').value='Sedang';
|
||||||
|
document.getElementById('rJenis').value='';
|
||||||
|
document.getElementById('rKet').value='';
|
||||||
|
document.getElementById('rTanggal').value=new Date().toISOString().slice(0,10);
|
||||||
|
resetFoto('rFoto','rFotoPreview','rFotoCurrent');
|
||||||
|
document.getElementById('modalRusakTitle').innerHTML='<i class="fas fa-exclamation-triangle" style="color:#ef4444;margin-right:8px"></i>Tambah Jalan Rusak';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 8. FOTO PREVIEW
|
||||||
|
// =============================================================
|
||||||
|
function previewFoto(input, previewId){
|
||||||
|
const wrap = document.getElementById(previewId);
|
||||||
|
const img = wrap.querySelector('img');
|
||||||
|
if(input.files && input.files[0]){
|
||||||
|
img.src = URL.createObjectURL(input.files[0]);
|
||||||
|
wrap.style.display='block';
|
||||||
|
} else {
|
||||||
|
wrap.style.display='none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 9. MODAL OPEN / CLOSE
|
||||||
|
// =============================================================
|
||||||
|
function openModalJalan(isEdit, data=null){
|
||||||
|
resetFoto('jFoto','jFotoPreview','jFotoCurrent');
|
||||||
|
if(isEdit && data){
|
||||||
|
document.getElementById('modalJalanTitle').innerHTML='<i class="fas fa-edit" style="color:#fbbf24;margin-right:8px"></i>Edit Data Jalan';
|
||||||
|
document.getElementById('jNama').value=data.nama_jalan;
|
||||||
|
document.getElementById('jStatus').value=data.status_jalan;
|
||||||
|
document.getElementById('jPanjang').textContent=parseFloat(data.panjang_meter).toFixed(2)+' meter';
|
||||||
|
document.getElementById('jKet').value=data.keterangan||'';
|
||||||
|
if(data.foto) document.getElementById('jFotoCurrent').style.display='block';
|
||||||
|
tempGeojson=data.geojson; tempPanjang=parseFloat(data.panjang_meter);
|
||||||
|
editMode=true; editId=data.id;
|
||||||
|
} else {
|
||||||
|
document.getElementById('modalJalanTitle').innerHTML='<i class="fas fa-road" style="color:#fbbf24;margin-right:8px"></i>Tambah Data Jalan';
|
||||||
|
document.getElementById('jNama').value='';
|
||||||
|
document.getElementById('jStatus').value='Nasional';
|
||||||
|
document.getElementById('jKet').value='';
|
||||||
|
editMode=false; editId=null;
|
||||||
|
}
|
||||||
|
document.getElementById('modalJalan').classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModalParsil(isEdit, data=null){
|
||||||
|
resetFoto('pFoto','pFotoPreview','pFotoCurrent');
|
||||||
|
if(isEdit && data){
|
||||||
|
document.getElementById('modalParsilTitle').innerHTML='<i class="fas fa-edit" style="color:#4ade80;margin-right:8px"></i>Edit Data Parsil';
|
||||||
|
document.getElementById('pNama').value=data.nama_pemilik;
|
||||||
|
document.getElementById('pNoSert').value=data.no_sertifikat;
|
||||||
|
document.getElementById('pStatus').value=data.status_kepemilikan;
|
||||||
|
document.getElementById('pLuas').textContent=parseFloat(data.luas_m2).toFixed(2)+' m²';
|
||||||
|
document.getElementById('pAlamat').value=data.alamat||'';
|
||||||
|
if(data.foto) document.getElementById('pFotoCurrent').style.display='block';
|
||||||
|
tempGeojson=data.geojson; tempLuas=parseFloat(data.luas_m2);
|
||||||
|
editMode=true; editId=data.id;
|
||||||
|
} else {
|
||||||
|
document.getElementById('modalParsilTitle').innerHTML='<i class="fas fa-map" style="color:#4ade80;margin-right:8px"></i>Tambah Parsil Tanah';
|
||||||
|
document.getElementById('pNama').value='';
|
||||||
|
document.getElementById('pNoSert').value='';
|
||||||
|
document.getElementById('pStatus').value='SHM';
|
||||||
|
document.getElementById('pAlamat').value='';
|
||||||
|
editMode=false; editId=null;
|
||||||
|
}
|
||||||
|
document.getElementById('modalParsil').classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModalRusak(isEdit, data=null){
|
||||||
|
resetFoto('rFoto','rFotoPreview','rFotoCurrent');
|
||||||
|
document.getElementById('rTanggal').value=new Date().toISOString().slice(0,10);
|
||||||
|
if(isEdit && data){
|
||||||
|
document.getElementById('modalRusakTitle').innerHTML='<i class="fas fa-edit" style="color:#ef4444;margin-right:8px"></i>Edit Jalan Rusak';
|
||||||
|
document.getElementById('rLokasi').value=data.lokasi;
|
||||||
|
document.getElementById('rTingkat').value=data.tingkat;
|
||||||
|
document.getElementById('rJenis').value=data.jenis||'';
|
||||||
|
document.getElementById('rTanggal').value=data.tanggal_lapor||'';
|
||||||
|
document.getElementById('rKet').value=data.keterangan||'';
|
||||||
|
if(data.foto) document.getElementById('rFotoCurrent').style.display='block';
|
||||||
|
// parse geojson to show coords
|
||||||
|
try{
|
||||||
|
const gj=JSON.parse(data.geojson);
|
||||||
|
const c=gj.geometry.coordinates;
|
||||||
|
document.getElementById('rKoord').textContent=c[1].toFixed(6)+', '+c[0].toFixed(6);
|
||||||
|
}catch(e){}
|
||||||
|
tempGeojson=data.geojson;
|
||||||
|
editMode=true; editId=data.id;
|
||||||
|
} else {
|
||||||
|
document.getElementById('modalRusakTitle').innerHTML='<i class="fas fa-exclamation-triangle" style="color:#ef4444;margin-right:8px"></i>Tambah Jalan Rusak';
|
||||||
|
document.getElementById('rLokasi').value='';
|
||||||
|
document.getElementById('rTingkat').value='Sedang';
|
||||||
|
document.getElementById('rJenis').value='';
|
||||||
|
document.getElementById('rKet').value='';
|
||||||
|
document.getElementById('rKoord').textContent='- klik titik di peta -';
|
||||||
|
editMode=false; editId=null;
|
||||||
|
}
|
||||||
|
document.getElementById('modalRusak').classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(){
|
||||||
|
['modalJalan','modalParsil','modalRusak'].forEach(id=>document.getElementById(id).classList.remove('show'));
|
||||||
|
tempGeojson=null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFoto(inputId, previewId, currentId){
|
||||||
|
document.getElementById(inputId).value='';
|
||||||
|
document.getElementById(previewId).style.display='none';
|
||||||
|
document.getElementById(currentId).style.display='none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 10. SAVE — JALAN (multipart/form-data)
|
||||||
|
// =============================================================
|
||||||
|
async function saveJalan(){
|
||||||
|
const nama = document.getElementById('jNama').value.trim();
|
||||||
|
if(!nama){ toast('Nama jalan wajib diisi!','error'); return; }
|
||||||
|
if(!tempGeojson && !editMode){ toast('Gambar polyline di peta terlebih dahulu!','error'); return; }
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('nama_jalan', nama);
|
||||||
|
fd.append('status_jalan', document.getElementById('jStatus').value);
|
||||||
|
fd.append('panjang_meter', tempPanjang);
|
||||||
|
fd.append('geojson', tempGeojson||'');
|
||||||
|
fd.append('keterangan', document.getElementById('jKet').value.trim());
|
||||||
|
const fotoFile = document.getElementById('jFoto').files[0];
|
||||||
|
if(fotoFile) fd.append('foto', fotoFile);
|
||||||
|
|
||||||
|
let url='jalan.php', method='POST';
|
||||||
|
if(editMode){ fd.append('id', editId); fd.append('_method','PUT'); }
|
||||||
|
|
||||||
|
try{
|
||||||
|
const res = await fetch(url,{method,body:fd});
|
||||||
|
const data = await res.json();
|
||||||
|
if(data.status==='ok'){ closeModal(); loadJalan(); toast(data.message); }
|
||||||
|
else toast('Error: '+data.message,'error');
|
||||||
|
}catch(e){ toast('Gagal konek ke server','error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 11. SAVE — PARSIL
|
||||||
|
// =============================================================
|
||||||
|
async function saveParsil(){
|
||||||
|
const nama = document.getElementById('pNama').value.trim();
|
||||||
|
const noSert = document.getElementById('pNoSert').value.trim();
|
||||||
|
if(!nama){ toast('Nama pemilik wajib diisi!','error'); return; }
|
||||||
|
if(!noSert){ toast('Nomor sertifikat wajib diisi!','error'); return; }
|
||||||
|
if(!tempGeojson && !editMode){ toast('Gambar polygon di peta terlebih dahulu!','error'); return; }
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('nama_pemilik', nama);
|
||||||
|
fd.append('no_sertifikat', noSert);
|
||||||
|
fd.append('status_kepemilikan', document.getElementById('pStatus').value);
|
||||||
|
fd.append('luas_m2', tempLuas);
|
||||||
|
fd.append('geojson', tempGeojson||'');
|
||||||
|
fd.append('alamat', document.getElementById('pAlamat').value.trim());
|
||||||
|
const fotoFile = document.getElementById('pFoto').files[0];
|
||||||
|
if(fotoFile) fd.append('foto', fotoFile);
|
||||||
|
|
||||||
|
if(editMode){ fd.append('id', editId); fd.append('_method','PUT'); }
|
||||||
|
|
||||||
|
try{
|
||||||
|
const res = await fetch('parsil.php',{method:'POST',body:fd});
|
||||||
|
const data = await res.json();
|
||||||
|
if(data.status==='ok'){ closeModal(); loadParsil(); toast(data.message); }
|
||||||
|
else toast('Error: '+data.message,'error');
|
||||||
|
}catch(e){ toast('Gagal konek ke server','error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 12. SAVE — JALAN RUSAK
|
||||||
|
// =============================================================
|
||||||
|
async function saveRusak(){
|
||||||
|
const lokasi = document.getElementById('rLokasi').value.trim();
|
||||||
|
if(!lokasi){ toast('Lokasi wajib diisi!','error'); return; }
|
||||||
|
if(!tempGeojson){ toast('Klik titik lokasi di peta terlebih dahulu!','error'); return; }
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('lokasi', lokasi);
|
||||||
|
fd.append('tingkat', document.getElementById('rTingkat').value);
|
||||||
|
fd.append('jenis', document.getElementById('rJenis').value.trim());
|
||||||
|
fd.append('tanggal_lapor', document.getElementById('rTanggal').value);
|
||||||
|
fd.append('geojson', tempGeojson);
|
||||||
|
fd.append('keterangan', document.getElementById('rKet').value.trim());
|
||||||
|
const fotoFile = document.getElementById('rFoto').files[0];
|
||||||
|
if(fotoFile) fd.append('foto', fotoFile);
|
||||||
|
|
||||||
|
if(editMode){ fd.append('id', editId); fd.append('_method','PUT'); }
|
||||||
|
|
||||||
|
try{
|
||||||
|
const res = await fetch('jalan_rusak.php',{method:'POST',body:fd});
|
||||||
|
const data = await res.json();
|
||||||
|
if(data.status==='ok'){ closeModal(); loadRusak(); toast(data.message); }
|
||||||
|
else toast('Error: '+data.message,'error');
|
||||||
|
}catch(e){ toast('Gagal konek ke server','error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 13. LOAD — JALAN
|
||||||
|
// =============================================================
|
||||||
|
async function loadJalan(q=''){
|
||||||
|
let res,json;
|
||||||
|
try{
|
||||||
|
const url = q ? `jalan.php?q=${encodeURIComponent(q)}` : 'jalan.php';
|
||||||
|
res = await fetch(url);
|
||||||
|
json = await res.json();
|
||||||
|
}catch(err){
|
||||||
|
document.getElementById('listJalan').innerHTML='<div class="empty-state"><i class="fas fa-exclamation-triangle" style="color:#f87171"></i><p style="color:#f87171">Gagal konek ke server. Pastikan XAMPP menyala & database sudah diimport.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(json.status==='error'){
|
||||||
|
document.getElementById('listJalan').innerHTML=`<div class="empty-state"><i class="fas fa-database" style="color:#f87171"></i><p style="color:#f87171">Error DB: ${json.message}</p></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {data} = json;
|
||||||
|
document.getElementById('countJalan').textContent = data.length;
|
||||||
|
document.getElementById('statJalan').textContent = data.length;
|
||||||
|
layerJalan.clearLayers();
|
||||||
|
const list = document.getElementById('listJalan');
|
||||||
|
if(!data.length){ list.innerHTML='<div class="empty-state"><i class="fas fa-road"></i><p>'+(q?'Tidak ada hasil pencarian':'Belum ada data jalan')+'</p></div>'; return; }
|
||||||
|
list.innerHTML='';
|
||||||
|
data.forEach(item=>{
|
||||||
|
const gj = JSON.parse(item.geojson);
|
||||||
|
const style = WARNA_JALAN[item.status_jalan]||WARNA_JALAN['Kabupaten'];
|
||||||
|
const layer = L.geoJSON(gj,{style}).addTo(layerJalan);
|
||||||
|
layer.bindPopup(`
|
||||||
|
<div class="popup-title">${item.nama_jalan}</div>
|
||||||
|
<div class="popup-row">Status: <span>${item.status_jalan}</span></div>
|
||||||
|
<div class="popup-row">Panjang: <span>${parseFloat(item.panjang_meter).toFixed(2)} m</span></div>
|
||||||
|
${item.keterangan?`<div class="popup-row">Ket: <span>${item.keterangan}</span></div>`:''}
|
||||||
|
${item.foto?`<img class="popup-foto" src="${item.foto}" alt="foto">`:''}
|
||||||
|
`);
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className='data-card';
|
||||||
|
card.innerHTML=`
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-name">${item.nama_jalan}</div>
|
||||||
|
<div class="card-badge badge-${item.status_jalan.toLowerCase()}">${item.status_jalan}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-meta"><span><i class="fas fa-ruler"></i>${parseFloat(item.panjang_meter).toFixed(0)} m</span></div>
|
||||||
|
${item.foto?`<img class="card-thumb" src="${item.foto}" alt="foto">`:''}
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn-sm btn-zoom" onclick="zoomJalan${item.id}()"><i class="fas fa-search-location"></i> Zoom</button>
|
||||||
|
<button class="btn-sm btn-edit" onclick="editJalan(${item.id})"><i class="fas fa-edit"></i> Edit</button>
|
||||||
|
<button class="btn-sm btn-hapus" onclick="hapusJalan(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
|
||||||
|
</div>`;
|
||||||
|
list.appendChild(card);
|
||||||
|
window[`zoomJalan${item.id}`]=()=>map.fitBounds(layer.getBounds(),{padding:[50,50]});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 14. LOAD — PARSIL
|
||||||
|
// =============================================================
|
||||||
|
async function loadParsil(q=''){
|
||||||
|
let res,json;
|
||||||
|
try{
|
||||||
|
const url = q ? `parsil.php?q=${encodeURIComponent(q)}` : 'parsil.php';
|
||||||
|
res = await fetch(url);
|
||||||
|
json = await res.json();
|
||||||
|
}catch(err){
|
||||||
|
document.getElementById('listParsil').innerHTML='<div class="empty-state"><i class="fas fa-exclamation-triangle" style="color:#f87171"></i><p style="color:#f87171">Gagal konek ke server.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(json.status==='error'){ document.getElementById('listParsil').innerHTML=`<div class="empty-state"><p style="color:#f87171">Error DB: ${json.message}</p></div>`; return; }
|
||||||
|
const {data} = json;
|
||||||
|
document.getElementById('countParsil').textContent = data.length;
|
||||||
|
document.getElementById('statParsil').textContent = data.length;
|
||||||
|
layerParsil.clearLayers();
|
||||||
|
const list = document.getElementById('listParsil');
|
||||||
|
if(!data.length){ list.innerHTML='<div class="empty-state"><i class="fas fa-map"></i><p>'+(q?'Tidak ada hasil pencarian':'Belum ada data parsil')+'</p></div>'; return; }
|
||||||
|
list.innerHTML='';
|
||||||
|
data.forEach(item=>{
|
||||||
|
const gj = JSON.parse(item.geojson);
|
||||||
|
const style = WARNA_PARSIL[item.status_kepemilikan]||WARNA_PARSIL['SHM'];
|
||||||
|
const layer = L.geoJSON(gj,{style}).addTo(layerParsil);
|
||||||
|
layer.bindPopup(`
|
||||||
|
<div class="popup-title">${item.nama_pemilik}</div>
|
||||||
|
<div class="popup-row">No. Sertifikat: <span>${item.no_sertifikat}</span></div>
|
||||||
|
<div class="popup-row">Status: <span>${item.status_kepemilikan}</span></div>
|
||||||
|
<div class="popup-row">Luas: <span>${parseFloat(item.luas_m2).toFixed(2)} m²</span></div>
|
||||||
|
${item.alamat?`<div class="popup-row">Alamat: <span>${item.alamat}</span></div>`:''}
|
||||||
|
${item.foto?`<img class="popup-foto" src="${item.foto}" alt="foto">`:''}
|
||||||
|
`);
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className='data-card';
|
||||||
|
card.innerHTML=`
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-name">${item.nama_pemilik}</div>
|
||||||
|
<div class="card-badge badge-${item.status_kepemilikan.toLowerCase()}">${item.status_kepemilikan}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
<span><i class="fas fa-vector-square"></i>${parseFloat(item.luas_m2).toFixed(0)} m²</span>
|
||||||
|
<span><i class="fas fa-certificate"></i>${item.no_sertifikat}</span>
|
||||||
|
</div>
|
||||||
|
${item.foto?`<img class="card-thumb" src="${item.foto}" alt="foto">`:''}
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn-sm btn-zoom" onclick="zoomParsil${item.id}()"><i class="fas fa-search-location"></i> Zoom</button>
|
||||||
|
<button class="btn-sm btn-edit" onclick="editParsil(${item.id})"><i class="fas fa-edit"></i> Edit</button>
|
||||||
|
<button class="btn-sm btn-hapus" onclick="hapusParsil(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
|
||||||
|
</div>`;
|
||||||
|
list.appendChild(card);
|
||||||
|
window[`zoomParsil${item.id}`]=()=>map.fitBounds(layer.getBounds(),{padding:[50,50]});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 15. LOAD — JALAN RUSAK
|
||||||
|
// =============================================================
|
||||||
|
async function loadRusak(q=''){
|
||||||
|
let res,json;
|
||||||
|
try{
|
||||||
|
const url = q ? `jalan_rusak.php?q=${encodeURIComponent(q)}` : 'jalan_rusak.php';
|
||||||
|
res = await fetch(url);
|
||||||
|
json = await res.json();
|
||||||
|
}catch(err){
|
||||||
|
document.getElementById('listRusak').innerHTML='<div class="empty-state"><i class="fas fa-exclamation-triangle" style="color:#f87171"></i><p style="color:#f87171">Gagal konek ke server.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(json.status==='error'){ document.getElementById('listRusak').innerHTML=`<div class="empty-state"><p style="color:#f87171">Error DB: ${json.message}</p></div>`; return; }
|
||||||
|
const {data} = json;
|
||||||
|
document.getElementById('countRusak').textContent = data.length;
|
||||||
|
document.getElementById('statRusak').textContent = data.length;
|
||||||
|
layerRusak.clearLayers();
|
||||||
|
const list = document.getElementById('listRusak');
|
||||||
|
if(!data.length){ list.innerHTML='<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>'+(q?'Tidak ada hasil pencarian':'Belum ada data jalan rusak')+'</p></div>'; return; }
|
||||||
|
list.innerHTML='';
|
||||||
|
data.forEach(item=>{
|
||||||
|
const gj = JSON.parse(item.geojson);
|
||||||
|
const cfg = WARNA_RUSAK[item.tingkat]||WARNA_RUSAK['Sedang'];
|
||||||
|
const coord = gj.geometry.coordinates; // [lng, lat]
|
||||||
|
const layer = L.circleMarker([coord[1],coord[0]], cfg).addTo(layerRusak);
|
||||||
|
layer.bindPopup(`
|
||||||
|
<div class="popup-title"><i class="fas fa-exclamation-triangle" style="color:#ef4444;margin-right:5px"></i>${item.lokasi}</div>
|
||||||
|
<div class="popup-row">Tingkat: <span style="color:${cfg.color};font-weight:700">${item.tingkat}</span></div>
|
||||||
|
${item.jenis?`<div class="popup-row">Jenis: <span>${item.jenis}</span></div>`:''}
|
||||||
|
<div class="popup-row">Dilaporkan: <span>${item.tanggal_lapor||'-'}</span></div>
|
||||||
|
${item.keterangan?`<div class="popup-row">Ket: <span>${item.keterangan}</span></div>`:''}
|
||||||
|
${item.foto?`<img class="popup-foto" src="${item.foto}" alt="foto">`:''}
|
||||||
|
`);
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className='data-card';
|
||||||
|
card.innerHTML=`
|
||||||
|
<div class="card-top">
|
||||||
|
<div class="card-name">${item.lokasi}</div>
|
||||||
|
<div class="card-badge badge-${item.tingkat.toLowerCase()}">${item.tingkat}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-meta">
|
||||||
|
${item.jenis?`<span><i class="fas fa-tools"></i>${item.jenis}</span>`:''}
|
||||||
|
<span><i class="fas fa-calendar"></i>${item.tanggal_lapor||'-'}</span>
|
||||||
|
</div>
|
||||||
|
${item.foto?`<img class="card-thumb" src="${item.foto}" alt="foto">`:''}
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn-sm btn-zoom" onclick="zoomRusak${item.id}()"><i class="fas fa-search-location"></i> Zoom</button>
|
||||||
|
<button class="btn-sm btn-edit" onclick="editRusak(${item.id})"><i class="fas fa-edit"></i> Edit</button>
|
||||||
|
<button class="btn-sm btn-hapus" onclick="hapusRusak(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
|
||||||
|
</div>`;
|
||||||
|
list.appendChild(card);
|
||||||
|
window[`zoomRusak${item.id}`]=()=>map.setView([coord[1],coord[0]],17);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 16. EDIT
|
||||||
|
// =============================================================
|
||||||
|
async function editJalan(id){
|
||||||
|
const res=await fetch(`jalan.php?id=${id}`);
|
||||||
|
const {data}=await res.json();
|
||||||
|
switchTab('jalan');
|
||||||
|
openModalJalan(true,data);
|
||||||
|
}
|
||||||
|
async function editParsil(id){
|
||||||
|
const res=await fetch(`parsil.php?id=${id}`);
|
||||||
|
const {data}=await res.json();
|
||||||
|
switchTab('parsil');
|
||||||
|
openModalParsil(true,data);
|
||||||
|
}
|
||||||
|
async function editRusak(id){
|
||||||
|
const res=await fetch(`jalan_rusak.php?id=${id}`);
|
||||||
|
const {data}=await res.json();
|
||||||
|
switchTab('rusak');
|
||||||
|
openModalRusak(true,data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 17. HAPUS
|
||||||
|
// =============================================================
|
||||||
|
async function hapusJalan(id){
|
||||||
|
if(!confirm('Yakin ingin menghapus data jalan ini?')) return;
|
||||||
|
const res=await fetch(`jalan.php?id=${id}`,{method:'DELETE'});
|
||||||
|
const data=await res.json();
|
||||||
|
if(data.status==='ok'){ loadJalan(); toast(data.message); }
|
||||||
|
else toast('Error: '+data.message,'error');
|
||||||
|
}
|
||||||
|
async function hapusParsil(id){
|
||||||
|
if(!confirm('Yakin ingin menghapus data parsil ini?')) return;
|
||||||
|
const res=await fetch(`parsil.php?id=${id}`,{method:'DELETE'});
|
||||||
|
const data=await res.json();
|
||||||
|
if(data.status==='ok'){ loadParsil(); toast(data.message); }
|
||||||
|
else toast('Error: '+data.message,'error');
|
||||||
|
}
|
||||||
|
async function hapusRusak(id){
|
||||||
|
if(!confirm('Yakin ingin menghapus data jalan rusak ini?')) return;
|
||||||
|
const res=await fetch(`jalan_rusak.php?id=${id}`,{method:'DELETE'});
|
||||||
|
const data=await res.json();
|
||||||
|
if(data.status==='ok'){ loadRusak(); toast(data.message); }
|
||||||
|
else toast('Error: '+data.message,'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 18. INIT
|
||||||
|
// =============================================================
|
||||||
|
loadJalan();
|
||||||
|
loadParsil();
|
||||||
|
loadRusak();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
// ===================================================
|
||||||
|
// jalan.php — CRUD Data Jalan (Polyline) + Upload Foto
|
||||||
|
// ===================================================
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
// Folder upload
|
||||||
|
$uploadDir = __DIR__ . '/uploads/jalan/';
|
||||||
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
||||||
|
|
||||||
|
// ---- Tangani DELETE via query string ----
|
||||||
|
if ($method === 'DELETE') {
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
if (!$id) {
|
||||||
|
$raw = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = (int)($raw['id'] ?? 0);
|
||||||
|
}
|
||||||
|
// Hapus foto lama jika ada
|
||||||
|
$row = $conn->query("SELECT foto FROM tb_jalan WHERE id=$id")->fetch_assoc();
|
||||||
|
if ($row && $row['foto'] && file_exists(__DIR__ . '/' . $row['foto'])) {
|
||||||
|
unlink(__DIR__ . '/' . $row['foto']);
|
||||||
|
}
|
||||||
|
$stmt = $conn->prepare("DELETE FROM tb_jalan WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','message'=>'Data jalan berhasil dihapus'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- GET ----
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$search = isset($_GET['q']) ? '%' . $conn->real_escape_string($_GET['q']) . '%' : null;
|
||||||
|
if (isset($_GET['id'])) {
|
||||||
|
$id = (int)$_GET['id'];
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM tb_jalan WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$data = $stmt->get_result()->fetch_assoc();
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$data]);
|
||||||
|
} elseif ($search) {
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM tb_jalan WHERE nama_jalan LIKE ? OR status_jalan LIKE ? OR keterangan LIKE ? ORDER BY created_at DESC");
|
||||||
|
$stmt->bind_param('sss', $search, $search, $search);
|
||||||
|
$stmt->execute();
|
||||||
|
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$rows]);
|
||||||
|
} else {
|
||||||
|
$result = $conn->query("SELECT * FROM tb_jalan ORDER BY created_at DESC");
|
||||||
|
$rows = $result->fetch_all(MYSQLI_ASSOC);
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$rows]);
|
||||||
|
}
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- POST (multipart/form-data untuk support upload foto) ----
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$nama = $conn->real_escape_string($_POST['nama_jalan'] ?? '');
|
||||||
|
$status = $conn->real_escape_string($_POST['status_jalan'] ?? 'Kabupaten');
|
||||||
|
$panjang = (float)($_POST['panjang_meter'] ?? 0);
|
||||||
|
$geojson = $conn->real_escape_string($_POST['geojson'] ?? '');
|
||||||
|
$ket = $conn->real_escape_string($_POST['keterangan'] ?? '');
|
||||||
|
$foto = '';
|
||||||
|
|
||||||
|
// Upload foto
|
||||||
|
if (!empty($_FILES['foto']['name'])) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg','jpeg','png','webp'];
|
||||||
|
if (in_array($ext, $allowed)) {
|
||||||
|
$fname = 'jalan_' . time() . '_' . rand(1000,9999) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $uploadDir . $fname)) {
|
||||||
|
$foto = 'uploads/jalan/' . $fname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO tb_jalan (nama_jalan,status_jalan,panjang_meter,geojson,keterangan,foto) VALUES (?,?,?,?,?,?)");
|
||||||
|
$stmt->bind_param('ssdsss', $nama, $status, $panjang, $geojson, $ket, $foto);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','id'=>$conn->insert_id,'message'=>'Data jalan berhasil disimpan'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PUT (multipart workaround via POST + _method) ----
|
||||||
|
if ($method === 'PUT' || ($method === 'POST' && ($_POST['_method'] ?? '') === 'PUT')) {
|
||||||
|
parse_str(file_get_contents('php://input'), $put);
|
||||||
|
$id = (int)($_POST['id'] ?? $put['id'] ?? 0);
|
||||||
|
$nama = $conn->real_escape_string($_POST['nama_jalan'] ?? $put['nama_jalan'] ?? '');
|
||||||
|
$status = $conn->real_escape_string($_POST['status_jalan'] ?? $put['status_jalan'] ?? 'Kabupaten');
|
||||||
|
$panjang = (float)($_POST['panjang_meter'] ?? $put['panjang_meter'] ?? 0);
|
||||||
|
$geojson = $conn->real_escape_string($_POST['geojson'] ?? $put['geojson'] ?? '');
|
||||||
|
$ket = $conn->real_escape_string($_POST['keterangan'] ?? $put['keterangan'] ?? '');
|
||||||
|
|
||||||
|
// Ambil foto lama
|
||||||
|
$row = $conn->query("SELECT foto FROM tb_jalan WHERE id=$id")->fetch_assoc();
|
||||||
|
$foto = $row['foto'] ?? '';
|
||||||
|
|
||||||
|
if (!empty($_FILES['foto']['name'])) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg','jpeg','png','webp'];
|
||||||
|
if (in_array($ext, $allowed)) {
|
||||||
|
// Hapus foto lama
|
||||||
|
if ($foto && file_exists(__DIR__ . '/' . $foto)) unlink(__DIR__ . '/' . $foto);
|
||||||
|
$fname = 'jalan_' . time() . '_' . rand(1000,9999) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $uploadDir . $fname)) {
|
||||||
|
$foto = 'uploads/jalan/' . $fname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE tb_jalan SET nama_jalan=?,status_jalan=?,panjang_meter=?,geojson=?,keterangan=?,foto=? WHERE id=?");
|
||||||
|
$stmt->bind_param('ssdsssi', $nama, $status, $panjang, $geojson, $ket, $foto, $id);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','message'=>'Data jalan berhasil diupdate'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status'=>'error','message'=>'Method not allowed']);
|
||||||
|
$conn->close();
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
// ===================================================
|
||||||
|
// jalan_rusak.php — CRUD Data Jalan Rusak (Point) + Upload Foto
|
||||||
|
// ===================================================
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$uploadDir = __DIR__ . '/uploads/jalan_rusak/';
|
||||||
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
||||||
|
|
||||||
|
// ---- DELETE ----
|
||||||
|
if ($method === 'DELETE') {
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
if (!$id) {
|
||||||
|
$raw = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = (int)($raw['id'] ?? 0);
|
||||||
|
}
|
||||||
|
$row = $conn->query("SELECT foto FROM tb_jalan_rusak WHERE id=$id")->fetch_assoc();
|
||||||
|
if ($row && $row['foto'] && file_exists(__DIR__ . '/' . $row['foto'])) unlink(__DIR__ . '/' . $row['foto']);
|
||||||
|
$stmt = $conn->prepare("DELETE FROM tb_jalan_rusak WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','message'=>'Data jalan rusak berhasil dihapus'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- GET ----
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$search = isset($_GET['q']) ? '%' . $conn->real_escape_string($_GET['q']) . '%' : null;
|
||||||
|
if (isset($_GET['id'])) {
|
||||||
|
$id = (int)$_GET['id'];
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM tb_jalan_rusak WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$data = $stmt->get_result()->fetch_assoc();
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$data]);
|
||||||
|
} elseif ($search) {
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM tb_jalan_rusak WHERE lokasi LIKE ? OR tingkat LIKE ? OR jenis LIKE ? OR keterangan LIKE ? ORDER BY created_at DESC");
|
||||||
|
$stmt->bind_param('ssss', $search, $search, $search, $search);
|
||||||
|
$stmt->execute();
|
||||||
|
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$rows]);
|
||||||
|
} else {
|
||||||
|
$result = $conn->query("SELECT * FROM tb_jalan_rusak ORDER BY created_at DESC");
|
||||||
|
$rows = $result->fetch_all(MYSQLI_ASSOC);
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$rows]);
|
||||||
|
}
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- POST ----
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$lokasi = $conn->real_escape_string($_POST['lokasi'] ?? '');
|
||||||
|
$tingkat = $conn->real_escape_string($_POST['tingkat'] ?? 'Sedang');
|
||||||
|
$jenis = $conn->real_escape_string($_POST['jenis'] ?? '');
|
||||||
|
$geojson = $conn->real_escape_string($_POST['geojson'] ?? '');
|
||||||
|
$ket = $conn->real_escape_string($_POST['keterangan'] ?? '');
|
||||||
|
$tgl = $conn->real_escape_string($_POST['tanggal_lapor'] ?? date('Y-m-d'));
|
||||||
|
$foto = '';
|
||||||
|
|
||||||
|
if (!empty($_FILES['foto']['name'])) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
if (in_array($ext, ['jpg','jpeg','png','webp'])) {
|
||||||
|
$fname = 'rusak_' . time() . '_' . rand(1000,9999) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $uploadDir . $fname))
|
||||||
|
$foto = 'uploads/jalan_rusak/' . $fname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO tb_jalan_rusak (lokasi,tingkat,jenis,geojson,keterangan,foto,tanggal_lapor) VALUES (?,?,?,?,?,?,?)");
|
||||||
|
$stmt->bind_param('sssssss', $lokasi, $tingkat, $jenis, $geojson, $ket, $foto, $tgl);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','id'=>$conn->insert_id,'message'=>'Data jalan rusak berhasil disimpan'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PUT ----
|
||||||
|
if ($method === 'PUT' || ($method === 'POST' && ($_POST['_method'] ?? '') === 'PUT')) {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$lokasi = $conn->real_escape_string($_POST['lokasi'] ?? '');
|
||||||
|
$tingkat = $conn->real_escape_string($_POST['tingkat'] ?? 'Sedang');
|
||||||
|
$jenis = $conn->real_escape_string($_POST['jenis'] ?? '');
|
||||||
|
$geojson = $conn->real_escape_string($_POST['geojson'] ?? '');
|
||||||
|
$ket = $conn->real_escape_string($_POST['keterangan'] ?? '');
|
||||||
|
$tgl = $conn->real_escape_string($_POST['tanggal_lapor'] ?? date('Y-m-d'));
|
||||||
|
$row = $conn->query("SELECT foto FROM tb_jalan_rusak WHERE id=$id")->fetch_assoc();
|
||||||
|
$foto = $row['foto'] ?? '';
|
||||||
|
|
||||||
|
if (!empty($_FILES['foto']['name'])) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
if (in_array($ext, ['jpg','jpeg','png','webp'])) {
|
||||||
|
if ($foto && file_exists(__DIR__ . '/' . $foto)) unlink(__DIR__ . '/' . $foto);
|
||||||
|
$fname = 'rusak_' . time() . '_' . rand(1000,9999) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $uploadDir . $fname))
|
||||||
|
$foto = 'uploads/jalan_rusak/' . $fname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE tb_jalan_rusak SET lokasi=?,tingkat=?,jenis=?,geojson=?,keterangan=?,foto=?,tanggal_lapor=? WHERE id=?");
|
||||||
|
$stmt->bind_param('sssssssi', $lokasi, $tingkat, $jenis, $geojson, $ket, $foto, $tgl, $id);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','message'=>'Data jalan rusak berhasil diupdate'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status'=>'error','message'=>'Method not allowed']);
|
||||||
|
$conn->close();
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
// ===================================================
|
||||||
|
// parsil.php — CRUD Data Parsil Tanah (Polygon) + Upload Foto
|
||||||
|
// ===================================================
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$uploadDir = __DIR__ . '/uploads/parsil/';
|
||||||
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
||||||
|
|
||||||
|
// ---- DELETE ----
|
||||||
|
if ($method === 'DELETE') {
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
if (!$id) {
|
||||||
|
$raw = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = (int)($raw['id'] ?? 0);
|
||||||
|
}
|
||||||
|
$row = $conn->query("SELECT foto FROM tb_parsil WHERE id=$id")->fetch_assoc();
|
||||||
|
if ($row && $row['foto'] && file_exists(__DIR__ . '/' . $row['foto'])) unlink(__DIR__ . '/' . $row['foto']);
|
||||||
|
$stmt = $conn->prepare("DELETE FROM tb_parsil WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','message'=>'Data parsil berhasil dihapus'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- GET ----
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$search = isset($_GET['q']) ? '%' . $conn->real_escape_string($_GET['q']) . '%' : null;
|
||||||
|
if (isset($_GET['id'])) {
|
||||||
|
$id = (int)$_GET['id'];
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM tb_parsil WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$data = $stmt->get_result()->fetch_assoc();
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$data]);
|
||||||
|
} elseif ($search) {
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM tb_parsil WHERE nama_pemilik LIKE ? OR no_sertifikat LIKE ? OR alamat LIKE ? ORDER BY created_at DESC");
|
||||||
|
$stmt->bind_param('sss', $search, $search, $search);
|
||||||
|
$stmt->execute();
|
||||||
|
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$rows]);
|
||||||
|
} else {
|
||||||
|
$result = $conn->query("SELECT * FROM tb_parsil ORDER BY created_at DESC");
|
||||||
|
$rows = $result->fetch_all(MYSQLI_ASSOC);
|
||||||
|
echo json_encode(['status'=>'ok','data'=>$rows]);
|
||||||
|
}
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- POST ----
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$nama = $conn->real_escape_string($_POST['nama_pemilik'] ?? '');
|
||||||
|
$noSert = $conn->real_escape_string($_POST['no_sertifikat'] ?? '');
|
||||||
|
$status = $conn->real_escape_string($_POST['status_kepemilikan'] ?? 'SHM');
|
||||||
|
$luas = (float)($_POST['luas_m2'] ?? 0);
|
||||||
|
$geojson = $conn->real_escape_string($_POST['geojson'] ?? '');
|
||||||
|
$alamat = $conn->real_escape_string($_POST['alamat'] ?? '');
|
||||||
|
$foto = '';
|
||||||
|
|
||||||
|
if (!empty($_FILES['foto']['name'])) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
if (in_array($ext, ['jpg','jpeg','png','webp'])) {
|
||||||
|
$fname = 'parsil_' . time() . '_' . rand(1000,9999) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $uploadDir . $fname))
|
||||||
|
$foto = 'uploads/parsil/' . $fname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO tb_parsil (nama_pemilik,no_sertifikat,status_kepemilikan,luas_m2,geojson,alamat,foto) VALUES (?,?,?,?,?,?,?)");
|
||||||
|
$stmt->bind_param('sssdsss', $nama, $noSert, $status, $luas, $geojson, $alamat, $foto);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','id'=>$conn->insert_id,'message'=>'Data parsil berhasil disimpan'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PUT ----
|
||||||
|
if ($method === 'PUT' || ($method === 'POST' && ($_POST['_method'] ?? '') === 'PUT')) {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$nama = $conn->real_escape_string($_POST['nama_pemilik'] ?? '');
|
||||||
|
$noSert = $conn->real_escape_string($_POST['no_sertifikat'] ?? '');
|
||||||
|
$status = $conn->real_escape_string($_POST['status_kepemilikan'] ?? 'SHM');
|
||||||
|
$luas = (float)($_POST['luas_m2'] ?? 0);
|
||||||
|
$geojson = $conn->real_escape_string($_POST['geojson'] ?? '');
|
||||||
|
$alamat = $conn->real_escape_string($_POST['alamat'] ?? '');
|
||||||
|
$row = $conn->query("SELECT foto FROM tb_parsil WHERE id=$id")->fetch_assoc();
|
||||||
|
$foto = $row['foto'] ?? '';
|
||||||
|
|
||||||
|
if (!empty($_FILES['foto']['name'])) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
if (in_array($ext, ['jpg','jpeg','png','webp'])) {
|
||||||
|
if ($foto && file_exists(__DIR__ . '/' . $foto)) unlink(__DIR__ . '/' . $foto);
|
||||||
|
$fname = 'parsil_' . time() . '_' . rand(1000,9999) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $uploadDir . $fname))
|
||||||
|
$foto = 'uploads/parsil/' . $fname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE tb_parsil SET nama_pemilik=?,no_sertifikat=?,status_kepemilikan=?,luas_m2=?,geojson=?,alamat=?,foto=? WHERE id=?");
|
||||||
|
$stmt->bind_param('sssdsssi', $nama, $noSert, $status, $luas, $geojson, $alamat, $foto, $id);
|
||||||
|
echo $stmt->execute()
|
||||||
|
? json_encode(['status'=>'ok','message'=>'Data parsil berhasil diupdate'])
|
||||||
|
: json_encode(['status'=>'error','message'=>$conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status'=>'error','message'=>'Method not allowed']);
|
||||||
|
$conn->close();
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,156 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<base target="_top">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<title>Peta Kepadatan Penduduk - Kota Pontianak</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#map { width: 100%; height: 100vh; }
|
||||||
|
.info { padding: 6px 8px; font: 14px/16px Arial, Helvetica, sans-serif; background: white; background: rgba(255,255,255,0.8); box-shadow: 0 0 15px rgba(0,0,0,0.2); border-radius: 5px; }
|
||||||
|
.info h4 { margin: 0 0 5px; color: #777; }
|
||||||
|
.legend { text-align: left; line-height: 18px; color: #555; }
|
||||||
|
.legend i { width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id='map'></div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="pontianak-kecamatan.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
// Koordinat pusat Kota Pontianak
|
||||||
|
const map = L.map('map').setView([-0.0271550562074066, 109.34544904782616], 12);
|
||||||
|
|
||||||
|
const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Info control: menampilkan info kecamatan saat hover
|
||||||
|
const info = L.control();
|
||||||
|
|
||||||
|
info.onAdd = function (map) {
|
||||||
|
this._div = L.DomUtil.create('div', 'info');
|
||||||
|
this.update();
|
||||||
|
return this._div;
|
||||||
|
};
|
||||||
|
|
||||||
|
info.update = function (props) {
|
||||||
|
const contents = props
|
||||||
|
? `<b>${props.name}</b><br />${props.density.toLocaleString('id-ID')} jiwa / km<sup>2</sup>`
|
||||||
|
: 'Arahkan kursor ke kecamatan';
|
||||||
|
this._div.innerHTML = `<h4>Kepadatan Penduduk<br>Kota Pontianak</h4>${contents}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
info.addTo(map);
|
||||||
|
|
||||||
|
// Warna berdasarkan kepadatan penduduk (jiwa/km²)
|
||||||
|
function getColor(d) {
|
||||||
|
return d > 10000 ? '#800026' :
|
||||||
|
d > 7000 ? '#BD0026' :
|
||||||
|
d > 5000 ? '#E31A1C' :
|
||||||
|
d > 3000 ? '#FC4E2A' :
|
||||||
|
d > 2000 ? '#FD8D3C' :
|
||||||
|
d > 1000 ? '#FEB24C' :
|
||||||
|
d > 500 ? '#FED976' : '#FFEDA0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function style(feature) {
|
||||||
|
return {
|
||||||
|
weight: 2,
|
||||||
|
opacity: 1,
|
||||||
|
color: 'white',
|
||||||
|
dashArray: '3',
|
||||||
|
fillOpacity: 0.7,
|
||||||
|
fillColor: getColor(feature.properties.density)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightFeature(e) {
|
||||||
|
const layer = e.target;
|
||||||
|
|
||||||
|
layer.setStyle({
|
||||||
|
weight: 5,
|
||||||
|
color: '#666',
|
||||||
|
dashArray: '',
|
||||||
|
fillOpacity: 0.7
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.bringToFront();
|
||||||
|
info.update(layer.feature.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* global kecamatanData */
|
||||||
|
const geojson = L.geoJson(kecamatanData, {
|
||||||
|
style,
|
||||||
|
onEachFeature
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
function resetHighlight(e) {
|
||||||
|
geojson.resetStyle(e.target);
|
||||||
|
info.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomToFeature(e) {
|
||||||
|
map.fitBounds(e.target.getBounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEachFeature(feature, layer) {
|
||||||
|
layer.on({
|
||||||
|
mouseover: highlightFeature,
|
||||||
|
mouseout: resetHighlight,
|
||||||
|
click: zoomToFeature
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
map.attributionControl.addAttribution('Data Kecamatan Kota Pontianak');
|
||||||
|
|
||||||
|
// Legend
|
||||||
|
const legend = L.control({position: 'bottomright'});
|
||||||
|
|
||||||
|
legend.onAdd = function (map) {
|
||||||
|
const div = L.DomUtil.create('div', 'info legend');
|
||||||
|
const grades = [0, 500, 1000, 2000, 3000, 5000, 7000, 10000];
|
||||||
|
const labels = [];
|
||||||
|
let from, to;
|
||||||
|
|
||||||
|
for (let i = 0; i < grades.length; i++) {
|
||||||
|
from = grades[i];
|
||||||
|
to = grades[i + 1];
|
||||||
|
|
||||||
|
labels.push(
|
||||||
|
`<i style="background:${getColor(from + 1)}"></i> ${from.toLocaleString('id-ID')}${to ? `–${to.toLocaleString('id-ID')}` : '+'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.innerHTML = labels.join('<br>');
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
|
||||||
|
legend.addTo(map);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$result = $conn->query("SELECT * FROM spbu");
|
||||||
|
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$data[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($data);
|
||||||
|
?>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE DATABASE IF NOT EXISTS db_spbu
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE db_spbu;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS spbu (
|
||||||
|
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
nama_spbu VARCHAR(100) NOT NULL,
|
||||||
|
no_wa VARCHAR(20) NOT NULL,
|
||||||
|
status ENUM('yes','no') NOT NULL DEFAULT 'no',
|
||||||
|
latitude DOUBLE NOT NULL,
|
||||||
|
longitude DOUBLE NOT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO spbu (nama_spbu, no_wa, status, latitude, longitude) VALUES
|
||||||
|
('SPBU Gajah Mada', '08115700101', 'yes', -0.0227, 109.3294),
|
||||||
|
('SPBU Ahmad Yani', '08115700102', 'no', -0.0350, 109.3350),
|
||||||
|
('SPBU Tanjungpura', '08115700103', 'yes', -0.0180, 109.3410);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$id = $_POST['id'];
|
||||||
|
|
||||||
|
$query = "DELETE FROM spbu WHERE id='$id'";
|
||||||
|
|
||||||
|
if ($conn->query($query)) {
|
||||||
|
echo "success";
|
||||||
|
} else {
|
||||||
|
echo "error";
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Peta SPBU - Layer Control</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||||
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: 'Segoe UI', sans-serif; }
|
||||||
|
#map { height: 100vh; width: 100%; }
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2) !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 10px 14px !important;
|
||||||
|
min-width: 210px !important;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
margin: 6px 0 !important;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: #333 !important;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// =========================================================
|
||||||
|
// INISIALISASI PETA
|
||||||
|
// =========================================================
|
||||||
|
const map = L.map('map').setView([-0.02, 109.34], 13);
|
||||||
|
|
||||||
|
// Base layer
|
||||||
|
const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© OpenStreetMap contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LAYER GROUPS - dua kelompok berdasarkan status 24 jam
|
||||||
|
// =========================================================
|
||||||
|
const layerSPBU24Jam = L.layerGroup(); // status = 'yes'
|
||||||
|
const layerSPBUNon24Jam = L.layerGroup(); // status = 'no'
|
||||||
|
|
||||||
|
// Tampilkan kedua layer secara default
|
||||||
|
layerSPBU24Jam.addTo(map);
|
||||||
|
layerSPBUNon24Jam.addTo(map);
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LAYERS CONTROL
|
||||||
|
// =========================================================
|
||||||
|
const baseMaps = {
|
||||||
|
"🗺️ OpenStreetMap": osm
|
||||||
|
};
|
||||||
|
|
||||||
|
const overlayMaps = {
|
||||||
|
"🟢 SPBU Buka 24 Jam" : layerSPBU24Jam,
|
||||||
|
"🔴 SPBU Tidak Buka 24 Jam" : layerSPBUNon24Jam
|
||||||
|
};
|
||||||
|
|
||||||
|
L.control.layers(baseMaps, overlayMaps, {
|
||||||
|
collapsed: false,
|
||||||
|
position: 'topright'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// ICON HELPER
|
||||||
|
// =========================================================
|
||||||
|
function buatIcon(warna) {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-${warna}.png`,
|
||||||
|
shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png',
|
||||||
|
iconSize: [25, 41],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconHijau = buatIcon('green');
|
||||||
|
const iconMerah = buatIcon('red');
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// LOAD & RENDER DATA
|
||||||
|
// =========================================================
|
||||||
|
function loadData() {
|
||||||
|
layerSPBU24Jam.clearLayers();
|
||||||
|
layerSPBUNon24Jam.clearLayers();
|
||||||
|
|
||||||
|
fetch('ambil_data.php')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
data.forEach(d => {
|
||||||
|
const is24Jam = d.status === 'yes';
|
||||||
|
const icon = is24Jam ? iconHijau : iconMerah;
|
||||||
|
|
||||||
|
const marker = L.marker([d.latitude, d.longitude], {
|
||||||
|
icon: icon,
|
||||||
|
draggable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.bindPopup(`
|
||||||
|
<div style="min-width:190px">
|
||||||
|
<b style="font-size:14px">${d.nama_spbu}</b><br>
|
||||||
|
<span style="color:#666">📞 ${d.no_wa}</span><br>
|
||||||
|
<span style="
|
||||||
|
display:inline-block; margin-top:5px; padding:2px 10px;
|
||||||
|
border-radius:12px; font-size:11px; font-weight:600;
|
||||||
|
background:${is24Jam ? '#d4edda' : '#f8d7da'};
|
||||||
|
color:${is24Jam ? '#155724' : '#721c24'}">
|
||||||
|
⏰ ${is24Jam ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam'}
|
||||||
|
</span>
|
||||||
|
<hr style="margin:8px 0; border:none; border-top:1px solid #eee">
|
||||||
|
<button onclick="editData(${d.id},'${d.nama_spbu}','${d.no_wa}','${d.status}',${d.latitude},${d.longitude})"
|
||||||
|
style="padding:4px 10px; background:#3498db; color:#fff; border:none; border-radius:4px; cursor:pointer; font-size:12px; margin-right:4px">
|
||||||
|
✏️ Edit
|
||||||
|
</button>
|
||||||
|
<button onclick="hapusData(${d.id})"
|
||||||
|
style="padding:4px 10px; background:#e74c3c; color:#fff; border:none; border-radius:4px; cursor:pointer; font-size:12px">
|
||||||
|
🗑️ Hapus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
marker.on('dragend', function(e) {
|
||||||
|
const pos = e.target.getLatLng();
|
||||||
|
fetch('update.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
id: d.id, nama: d.nama_spbu, wa: d.no_wa,
|
||||||
|
status: d.status, lat: pos.lat, lng: pos.lng
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(r => { if(r === "success") { alert("Lokasi diupdate"); loadData(); } });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Masukkan ke layer group yang sesuai
|
||||||
|
if (is24Jam) {
|
||||||
|
layerSPBU24Jam.addLayer(marker);
|
||||||
|
} else {
|
||||||
|
layerSPBUNon24Jam.addLayer(marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// KLIK PETA → TAMBAH DATA BARU
|
||||||
|
// =========================================================
|
||||||
|
map.on('click', function(e) {
|
||||||
|
const lat = e.latlng.lat;
|
||||||
|
const lng = e.latlng.lng;
|
||||||
|
|
||||||
|
const popupContent = `
|
||||||
|
<div style="min-width:200px">
|
||||||
|
<b>➕ Tambah SPBU Baru</b>
|
||||||
|
<hr style="margin:6px 0; border:none; border-top:1px solid #eee">
|
||||||
|
<form id="formSPBU" style="font-size:13px">
|
||||||
|
Nama SPBU:<br>
|
||||||
|
<input type="text" name="nama" required
|
||||||
|
style="width:100%; padding:4px 6px; margin:3px 0 8px; border:1px solid #ccc; border-radius:4px"><br>
|
||||||
|
No WhatsApp:<br>
|
||||||
|
<input type="text" name="wa" required
|
||||||
|
style="width:100%; padding:4px 6px; margin:3px 0 8px; border:1px solid #ccc; border-radius:4px"><br>
|
||||||
|
Status 24 Jam:<br>
|
||||||
|
<select name="status"
|
||||||
|
style="width:100%; padding:4px 6px; margin:3px 0 10px; border:1px solid #ccc; border-radius:4px">
|
||||||
|
<option value="yes">🟢 Buka 24 Jam</option>
|
||||||
|
<option value="no">🔴 Tidak Buka 24 Jam</option>
|
||||||
|
</select><br>
|
||||||
|
<button type="submit"
|
||||||
|
style="width:100%; padding:6px; background:#2ecc71; color:#fff; border:none; border-radius:4px; cursor:pointer; font-weight:600">
|
||||||
|
💾 Simpan
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
L.popup().setLatLng(e.latlng).setContent(popupContent).openOn(map);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const form = document.getElementById("formSPBU");
|
||||||
|
if (!form) return;
|
||||||
|
form.onsubmit = function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const fd = new FormData(this);
|
||||||
|
fd.append("lat", lat);
|
||||||
|
fd.append("lng", lng);
|
||||||
|
fetch('simpan.php', { method: 'POST', body: fd })
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(r => {
|
||||||
|
if(r === "success") { alert("Data berhasil disimpan"); map.closePopup(); loadData(); }
|
||||||
|
else { alert("Gagal menyimpan"); }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// EDIT DATA
|
||||||
|
// =========================================================
|
||||||
|
function editData(id, nama, wa, status, lat, lng) {
|
||||||
|
const newNama = prompt("Nama SPBU:", nama);
|
||||||
|
const newWa = prompt("No WhatsApp:", wa);
|
||||||
|
const newStatus = prompt("Status 24 Jam (yes/no):", status);
|
||||||
|
if (newNama && newWa && newStatus) {
|
||||||
|
fetch('update.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: new URLSearchParams({ id, nama: newNama, wa: newWa, status: newStatus, lat, lng })
|
||||||
|
})
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(r => { if(r === "success") { alert("Data diupdate"); map.closePopup(); loadData(); } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// HAPUS DATA
|
||||||
|
// =========================================================
|
||||||
|
function hapusData(id) {
|
||||||
|
if (confirm("Yakin mau hapus data SPBU ini?")) {
|
||||||
|
fetch('hapus.php', { method: 'POST', body: new URLSearchParams({ id }) })
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(r => { if(r === "success") { alert("Data dihapus"); map.closePopup(); loadData(); } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$user = getenv('DB_USER') ?: 'root';
|
||||||
|
$pass = getenv('DB_PASSWORD') !== false ? getenv('DB_PASSWORD') : '';
|
||||||
|
$db = getenv('DB_NAME') ?: 'db_spbu';
|
||||||
|
|
||||||
|
$conn = mysqli_connect($host, $user, $pass, $db);
|
||||||
|
|
||||||
|
if (!$conn) {
|
||||||
|
die("Koneksi database gagal: " . mysqli_connect_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
mysqli_set_charset($conn, 'utf8mb4');
|
||||||
|
?>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$nama = $_POST['nama'];
|
||||||
|
$wa = $_POST['wa'];
|
||||||
|
$status = $_POST['status'];
|
||||||
|
$lat = $_POST['lat'];
|
||||||
|
$lng = $_POST['lng'];
|
||||||
|
|
||||||
|
$query = "INSERT INTO spbu (nama_spbu, no_wa, status, latitude, longitude)
|
||||||
|
VALUES ('$nama','$wa','$status','$lat','$lng')";
|
||||||
|
|
||||||
|
if ($conn->query($query)) {
|
||||||
|
echo "success";
|
||||||
|
} else {
|
||||||
|
echo "error";
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$id = $_POST['id'];
|
||||||
|
$nama = $_POST['nama'];
|
||||||
|
$wa = $_POST['wa'];
|
||||||
|
$status = $_POST['status'];
|
||||||
|
$lat = $_POST['lat'];
|
||||||
|
$lng = $_POST['lng'];
|
||||||
|
|
||||||
|
$query = "UPDATE spbu SET
|
||||||
|
nama_spbu='$nama',
|
||||||
|
no_wa='$wa',
|
||||||
|
status='$status',
|
||||||
|
latitude='$lat',
|
||||||
|
longitude='$lng'
|
||||||
|
WHERE id='$id'";
|
||||||
|
|
||||||
|
if ($conn->query($query)) {
|
||||||
|
echo "success";
|
||||||
|
} else {
|
||||||
|
echo "error";
|
||||||
|
}
|
||||||
|
?>
|
||||||
Reference in New Issue
Block a user