edit files

This commit is contained in:
2026-06-10 19:48:00 +07:00
parent 8d26d3842d
commit 13cc0c9bb7
63 changed files with 8750 additions and 0 deletions
+13
View File
@@ -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>
+182
View File
@@ -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');
}
}
+417
View File
@@ -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'],
];
}
+494
View File
@@ -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}
+40
View File
@@ -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),
};
})();
+278
View File
@@ -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>
&nbsp;|&nbsp; Role: <strong>${info.label}</strong>
&nbsp;|&nbsp; <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 };
})();
+132
View File
@@ -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||'—'} &nbsp;·&nbsp; 📅 ${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 };
})();
+99
View File
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
}
return { init, loadRoom, send };
})();
+69
View File
@@ -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 };
})();
+127
View File
@@ -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 };
})();
+96
View File
@@ -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 };
})();
+40
View File
@@ -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 };
})();
+199
View File
@@ -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 };
})();
+217
View File
@@ -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 };
})();
+59
View File
@@ -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 };
})();
+87
View File
@@ -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||'—'} &nbsp;·&nbsp; ⭕ 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 };
})();
+11
View File
@@ -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);
+19
View File
@@ -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;
}
+171
View File
@@ -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;
}
+812
View File
@@ -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 &nbsp;→ Administrator<br>
pengurus_jami / admin123 &nbsp;→ Pengurus RI<br>
pimpinan / admin123 &nbsp;→ Pimpinan (read-only)<br>
warga1 / admin123 &nbsp;→ 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>
+474
View File
@@ -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;
View File