add files

This commit is contained in:
luthfihadinugroho79
2026-06-10 19:42:59 +07:00
parent 7bf3c98e59
commit e950468760
3541 changed files with 0 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
# WebGIS Pontianak v2.0
## Sistem Informasi Geografis Penduduk Miskin Kota Pontianak
---
## 📁 Struktur Proyek
```
webgis-v2/
├── index.php # Entry point (auto-redirect by role)
├── login.php # Halaman login (semua role)
├── daftar.php # Registrasi akun masyarakat
├── dashboard/
│ ├── masyarakat.php # Dashboard: Masyarakat
│ ├── pengurus.php # Dashboard: Pengurus Rumah Ibadah
│ └── pemerintah.php # Dashboard: Pemerintah (read-only)
├── api/
│ ├── auth.php # Login, logout, register, session
│ ├── penduduk.php # CRUD data KK & GeoJSON
│ ├── anggota.php # CRUD anggota keluarga & pendidikan
│ ├── rumah_ibadah.php # CRUD rumah ibadah & GeoJSON
│ ├── laporan.php # Laporan masyarakat & verifikasi
│ ├── bantuan.php # Histori bantuan & statistik
│ ├── pesan.php # Komunikasi antar rumah ibadah
│ └── notifikasi.php # Notifikasi real-time
├── includes/
│ └── config.php # DB config, helpers, session
├── assets/
│ ├── css/style.css # Global stylesheet
│ ├── js/app.js # Global JS utilities
│ └── uploads/ # Foto & dokumen upload
└── database_baru.sql # Schema database lengkap
```
---
## 🚀 Cara Install
### 1. Import Database
```sql
CREATE DATABASE webgis_pontianak CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE webgis_pontianak;
-- Import file: database_baru.sql
```
### 2. Konfigurasi Database
Edit `includes/config.php`:
```php
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // sesuaikan
define('DB_PASS', ''); // sesuaikan
define('DB_NAME', 'webgis_pontianak');
```
### 3. Buat Password Default
Jalankan script PHP ini sekali untuk update password `password123`:
```php
<?php
require 'includes/config.php';
$hash = password_hash('password123', PASSWORD_DEFAULT);
$conn->query("UPDATE users SET password_hash='$hash'");
echo "Password updated!";
?>
```
### 4. Akses Aplikasi
Buka `http://localhost/webgis-v2/` di browser.
---
## 👤 Akun Default
| Username | Password | Role |
|----------------|---------------|---------------------|
| `admin` | password123 | Admin |
| `pengurus_mj` | password123 | Pengurus Masjid Jami|
| `pengurus_mm` | password123 | Pengurus Mujahidin |
| `pengurus_gk` | password123 | Pengurus Katedral |
| `pemda_ptk` | password123 | Pemerintah |
| `ahmad_yusuf` | password123 | Masyarakat |
---
## 🔑 Fitur Per Role
### 👤 Masyarakat
- Login / Daftar akun mandiri
- Lihat peta sebaran penduduk
- **Kirim laporan:** kelahiran, kematian, pindah, dll
- Lihat data keluarga & anggota (dengan umur otomatis dari tanggal lahir)
- Lihat riwayat pendidikan anggota keluarga
- Lihat histori bantuan yang diterima keluarga
- Terima notifikasi real-time
### 🕌 Pengurus Rumah Ibadah
- Semua fitur di atas, plus:
- **Manajemen penduduk binaan:** tambah, edit, hapus KK
- **Manajemen anggota keluarga** beserta riwayat pendidikan
- **Verifikasi laporan** masyarakat → otomatis update status anggota
- **Catat bantuan:** detail lengkap dengan kondisi, foto, nominal
- Histori bantuan dengan **rumah ibadah mitra**
- **Kirim/terima pesan** antar rumah ibadah
- Tambah rumah ibadah baru ke sistem
- Notifikasi auto saat laporan masuk ke wilayah binaan
### 🏛️ Pemerintah (Read-only)
- Dashboard analytics lengkap dengan Chart.js
- KPI: total KK, jiwa, sangat miskin, miskin, bantuan, ibadah
- **Grafik:** distribusi per kecamatan, status ekonomi, bantuan per jenis, kontribusi ibadah, status laporan
- Peta GIS interaktif dengan filter
- Tabel penduduk lengkap + **export CSV**
- Transparansi penyaluran bantuan
- Monitoring laporan masyarakat
---
## 🗺️ Fitur GIS
- **Peta Leaflet** dengan tile OpenStreetMap
- Marker per penduduk (warna berbeda per status ekonomi)
- Marker per rumah ibadah (ikon sesuai jenis)
- **Radius layanan** tiap rumah ibadah (lingkaran)
- **Auto-assign:** laporan masyarakat otomatis diteruskan ke rumah ibadah yang radius-nya mencakup lokasi KK
- **Haversine:** jarak KK ke rumah ibadah terdekat dihitung otomatis saat input
---
## 📡 API Endpoints
```
GET api/auth.php?action=stats → Statistik publik (login page)
POST api/auth.php → Login
POST api/auth.php?action=register → Register masyarakat
GET api/auth.php?action=logout → Logout
GET api/penduduk.php?action=geojson → GeoJSON semua penduduk
GET api/penduduk.php?action=dashboard → Statistik dashboard
POST api/penduduk.php → Tambah KK (multipart)
PUT api/penduduk.php → Update KK
GET api/anggota.php?action=list&id_penduduk=X → List anggota KK
POST api/anggota.php → Tambah anggota
PUT api/anggota.php → Update/ubah status hidup
GET api/rumah_ibadah.php?action=geojson → GeoJSON ibadah
GET api/rumah_ibadah.php?action=list → List ibadah (dropdown)
POST api/rumah_ibadah.php → Tambah ibadah
GET api/laporan.php?action=list → List laporan (filter wilayah auto)
POST api/laporan.php?action=kirim → Kirim laporan (masyarakat)
PUT api/laporan.php?action=verifikasi → Verifikasi laporan (pengurus)
GET api/bantuan.php?action=list → Histori bantuan
GET api/bantuan.php?action=statistik → Statistik bantuan
POST api/bantuan.php → Catat bantuan baru
PUT api/bantuan.php → Update bantuan
GET api/pesan.php?action=inbox → Pesan masuk (pengurus)
GET api/pesan.php?action=outbox → Pesan terkirim
POST api/pesan.php → Kirim pesan antar ibadah
PUT api/pesan.php?action=inbox → Balas pesan
GET api/notifikasi.php → Semua notifikasi user
PUT api/notifikasi.php → Tandai baca
```
---
## 🔧 Kebutuhan Server
- PHP 7.4+ dengan ekstensi `mysqli`, `session`
- MySQL / MariaDB 5.7+
- Apache/Nginx dengan `mod_rewrite`
- PHP `fileinfo` untuk upload gambar
---
## 📦 Dependensi Eksternal (CDN — tidak perlu install)
- Leaflet.js 1.9.4 (peta interaktif)
- Chart.js 4.4.0 (grafik dashboard pemerintah)
- Google Fonts: Sora
---
*WebGIS Pontianak v2.0 — Sistem Terintegrasi Penanggulangan Kemiskinan*
+193
View File
@@ -0,0 +1,193 @@
<?php
// api/anggota.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'list';
// ── GET ───────────────────────────────────────────────────
if ($method === 'GET') {
if ($action === 'list' && isset($_GET['id_penduduk'])) {
$pid = (int)$_GET['id_penduduk'];
$res = $conn->query("SELECT *,
TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE()) AS umur,
CASE
WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) < 6 THEN 'Balita'
WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) < 13 THEN 'Anak-anak'
WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) < 18 THEN 'Remaja'
WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) < 60 THEN 'Dewasa'
ELSE 'Lansia'
END AS kelompok_usia
FROM anggota_keluarga WHERE id_penduduk=$pid ORDER BY id");
$data = [];
while ($r = $res->fetch_assoc()) {
// Tambahkan riwayat pendidikan tiap anggota
$pend = $conn->query("SELECT * FROM riwayat_pendidikan WHERE id_anggota={$r['id']} ORDER BY tahun_masuk DESC");
$pendList = [];
while ($p = $pend->fetch_assoc()) $pendList[] = $p;
$r['riwayat_pendidikan'] = $pendList;
$data[] = $r;
}
jsonResponse($data);
} elseif ($action === 'get' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$row = $conn->query("SELECT *,
TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) AS umur
FROM anggota_keluarga WHERE id=$id")->fetch_assoc();
if ($row) {
$pend = $conn->query("SELECT * FROM riwayat_pendidikan WHERE id_anggota=$id ORDER BY tahun_masuk DESC");
$pendList = [];
while ($p = $pend->fetch_assoc()) $pendList[] = $p;
$row['riwayat_pendidikan'] = $pendList;
}
jsonResponse($row ?: ['error'=>'Tidak ditemukan']);
} elseif ($action === 'statistik_usia') {
// Distribusi usia seluruh anggota keluarga
$res = $conn->query("SELECT
COUNT(*) AS total,
SUM(CASE WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) < 6 THEN 1 ELSE 0 END) AS balita,
SUM(CASE WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) BETWEEN 6 AND 12 THEN 1 ELSE 0 END) AS anak,
SUM(CASE WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) BETWEEN 13 AND 17 THEN 1 ELSE 0 END) AS remaja,
SUM(CASE WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) BETWEEN 18 AND 59 THEN 1 ELSE 0 END) AS dewasa,
SUM(CASE WHEN TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) >= 60 THEN 1 ELSE 0 END) AS lansia
FROM anggota_keluarga WHERE status_hidup='hidup'");
jsonResponse($res->fetch_assoc());
} elseif ($action === 'statistik_pendidikan') {
$res = $conn->query("SELECT rp.jenjang, COUNT(*) AS jumlah
FROM riwayat_pendidikan rp
JOIN anggota_keluarga ak ON rp.id_anggota=ak.id
WHERE rp.status_pendidikan='lulus' OR rp.status_pendidikan='putus_sekolah'
GROUP BY rp.jenjang ORDER BY jumlah DESC");
$data=[];
while($r=$res->fetch_assoc())$data[]=$r;
jsonResponse($data);
}
}
// ── POST: tambah anggota ──────────────────────────────────
if ($method === 'POST') {
requireLogin(['admin','pengurus_ibadah','masyarakat']);
$d = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$id_penduduk= (int)($d['id_penduduk']??0);
$nik = esc($conn,$d['nik']??'');
$nama = esc($conn,$d['nama']??'');
$hub = esc($conn,$d['hubungan']??'Anak');
$jk = esc($conn,$d['jenis_kelamin']??'L');
$tgl = esc($conn,$d['tanggal_lahir']??'2000-01-01');
$tmp = esc($conn,$d['tempat_lahir']??'');
$agm = esc($conn,$d['agama']??'Islam');
$pkj = esc($conn,$d['pekerjaan']??'');
$hp = esc($conn,$d['no_hp']??'');
if (!$id_penduduk || !$nama)
jsonResponse(['success'=>false,'error'=>'ID penduduk dan nama wajib diisi'],400);
$sql = "INSERT INTO anggota_keluarga
(id_penduduk,nik,nama,hubungan,jenis_kelamin,tanggal_lahir,tempat_lahir,agama,pekerjaan,no_hp)
VALUES ($id_penduduk,'$nik','$nama','$hub','$jk','$tgl','$tmp','$agm','$pkj','$hp')";
if ($conn->query($sql)) {
$aid = $conn->insert_id;
// Tambah riwayat pendidikan jika ada
$pendList = $d['riwayat_pendidikan'] ?? [];
foreach ($pendList as $pd) {
$jj = esc($conn,$pd['jenjang']??'');
$ni = esc($conn,$pd['nama_institusi']??'');
$jur= esc($conn,$pd['jurusan']??'');
$tm = (int)($pd['tahun_masuk']??0)?:'NULL';
$tl = (int)($pd['tahun_lulus']??0)?:'NULL';
$sp = esc($conn,$pd['status_pendidikan']??'lulus');
$ket= esc($conn,$pd['keterangan']??'');
if ($jj) {
$conn->query("INSERT INTO riwayat_pendidikan
(id_anggota,jenjang,nama_institusi,jurusan,tahun_masuk,tahun_lulus,status_pendidikan,keterangan)
VALUES ($aid,'$jj','$ni','$jur',$tm,$tl,'$sp','$ket')");
}
}
// Update jumlah_anggota di KK
$conn->query("UPDATE penduduk SET jumlah_anggota=(SELECT COUNT(*) FROM anggota_keluarga WHERE id_penduduk=$id_penduduk) WHERE id=$id_penduduk");
logAktivitas($conn,'tambah_anggota','anggota_keluarga',$aid,"KK:$id_penduduk - $nama");
jsonResponse(['success'=>true,'id'=>$aid,'message'=>'Anggota keluarga berhasil ditambahkan']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── PUT: update anggota / status hidup ───────────────────
if ($method === 'PUT') {
requireLogin(['admin','pengurus_ibadah']);
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)$d['id'];
$action2 = $d['action'] ?? 'update';
if ($action2 === 'status') {
// Update status hidup saja (dari verifikasi laporan)
$status = esc($conn,$d['status_hidup']??'hidup');
$sql = "UPDATE anggota_keluarga SET status_hidup='$status' WHERE id=$id";
if ($conn->query($sql)) {
logAktivitas($conn,'update_status_anggota','anggota_keluarga',$id,"Status: $status");
jsonResponse(['success'=>true,'message'=>'Status diperbarui']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
} else {
// Full update
$nama = esc($conn,$d['nama']??'');
$hub = esc($conn,$d['hubungan']??'');
$tgl = esc($conn,$d['tanggal_lahir']??'');
$agm = esc($conn,$d['agama']??'');
$pkj = esc($conn,$d['pekerjaan']??'');
$hp = esc($conn,$d['no_hp']??'');
$sh = esc($conn,$d['status_hidup']??'hidup');
$sql = "UPDATE anggota_keluarga SET nama='$nama',hubungan='$hub',tanggal_lahir='$tgl',
agama='$agm',pekerjaan='$pkj',no_hp='$hp',status_hidup='$sh' WHERE id=$id";
if ($conn->query($sql)) {
// Update pendidikan jika ada
if (!empty($d['riwayat_pendidikan'])) {
$conn->query("DELETE FROM riwayat_pendidikan WHERE id_anggota=$id");
foreach ($d['riwayat_pendidikan'] as $pd) {
$jj=$conn->real_escape_string($pd['jenjang']??'');
$ni=$conn->real_escape_string($pd['nama_institusi']??'');
$tm=(int)($pd['tahun_masuk']??0)?:'NULL';
$tl=(int)($pd['tahun_lulus']??0)?:'NULL';
$sp=$conn->real_escape_string($pd['status_pendidikan']??'lulus');
if($jj)$conn->query("INSERT INTO riwayat_pendidikan (id_anggota,jenjang,nama_institusi,tahun_masuk,tahun_lulus,status_pendidikan) VALUES ($id,'$jj','$ni',$tm,$tl,'$sp')");
}
}
logAktivitas($conn,'edit_anggota','anggota_keluarga',$id,"Edit: $nama");
jsonResponse(['success'=>true,'message'=>'Data anggota diperbarui']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
}
// ── DELETE ────────────────────────────────────────────────
if ($method === 'DELETE') {
requireLogin(['admin','pengurus_ibadah']);
$id = (int)($_GET['id']??0);
$row = $conn->query("SELECT id_penduduk FROM anggota_keluarga WHERE id=$id")->fetch_assoc();
if ($conn->query("DELETE FROM anggota_keluarga WHERE id=$id")) {
if ($row) {
$pid = (int)$row['id_penduduk'];
$conn->query("UPDATE penduduk SET jumlah_anggota=(SELECT COUNT(*) FROM anggota_keluarga WHERE id_penduduk=$pid) WHERE id=$pid");
}
logAktivitas($conn,'hapus_anggota','anggota_keluarga',$id,'');
jsonResponse(['success'=>true,'message'=>'Data dihapus']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
?>
+309
View File
@@ -0,0 +1,309 @@
<?php
// api/auth.php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
startSession();
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
// ── GET: statistik publik untuk halaman login ─────────────
if ($method === 'GET' && $action === 'stats') {
$kk = $conn->query("SELECT COUNT(*) c FROM penduduk")->fetch_assoc()['c'];
$ibadah = $conn->query("SELECT COUNT(*) c FROM rumah_ibadah")->fetch_assoc()['c'];
$bantuan = $conn->query("SELECT COUNT(*) c FROM bantuan")->fetch_assoc()['c'];
jsonResponse(['kk'=>(int)$kk,'ibadah'=>(int)$ibadah,'bantuan'=>(int)$bantuan]);
}
// ── GET: cek sesi aktif ───────────────────────────────────
if ($method === 'GET' && $action === 'me') {
if (!empty($_SESSION['user_id'])) {
jsonResponse(['logged_in'=>true,'user'=>currentUser()]);
}
jsonResponse(['logged_in'=>false]);
}
// ── GET: logout ───────────────────────────────────────────
if ($method === 'GET' && $action === 'logout') {
$_SESSION = [];
session_destroy();
header('Location: ../login.php');
exit;
}
// ── GET: daftar pengurus pending (admin only) ─────────────
if ($method === 'GET' && $action === 'pending_list') {
requireLogin(['admin']);
$res = $conn->query("SELECT id,nama_lengkap,username,no_hp,email,nama_ibadah,jenis_ibadah,
latitude,longitude,alamat,kelurahan,kecamatan,radius,status,catatan_admin,created_at
FROM pengurus_pending ORDER BY created_at DESC");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
}
// ── GET: daftar rumah ibadah tersedia untuk pendaftar ─────
if ($method === 'GET' && $action === 'ibadah_list') {
$res = $conn->query("SELECT id, nama, jenis, alamat, kelurahan, kecamatan FROM rumah_ibadah ORDER BY nama");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
}
// ── POST: login ───────────────────────────────────────────
if ($method === 'POST' && ($action === '' || $action === 'login')) {
$raw = file_get_contents('php://input');
$d = json_decode($raw, true);
if (!$d) $d = $_POST;
$username = trim($d['username'] ?? '');
$password = $d['password'] ?? '';
$roleReq = $d['role'] ?? '';
if (!$username || !$password) {
jsonResponse(['success'=>false,'error'=>'Username dan kata sandi wajib diisi.'], 400);
}
$uname = $conn->real_escape_string($username);
$row = $conn->query(
"SELECT * FROM users WHERE (username='$uname' OR no_hp='$uname') AND is_active=1 LIMIT 1"
)->fetch_assoc();
if (!$row || !password_verify($password, $row['password_hash'])) {
jsonResponse(['success'=>false,'error'=>'Username atau kata sandi salah.'], 401);
}
// Validasi role jika dipilih
if ($roleReq && $row['role'] !== 'admin' && $row['role'] !== $roleReq) {
$roleLabel = ['masyarakat'=>'Masyarakat','pengurus_ibadah'=>'Pengurus Ibadah','pemerintah'=>'Pemerintah'];
jsonResponse(['success'=>false,'error'=>'Akun ini bukan akun '.($roleLabel[$roleReq] ?? $roleReq).'. Pilih tab yang sesuai.'], 403);
}
// Set session
session_regenerate_id(true);
$_SESSION['user_id'] = $row['id'];
$_SESSION['nama'] = $row['nama_lengkap'];
$_SESSION['role'] = $row['role'];
$_SESSION['id_rumah_ibadah'] = $row['id_rumah_ibadah'];
$_SESSION['id_penduduk'] = $row['id_penduduk'];
$_SESSION['login_time'] = time();
$uid = (int)$row['id'];
$conn->query("UPDATE users SET last_login=NOW() WHERE id=$uid");
logAktivitas($conn,'login','users',$uid,'Login dari '.$_SERVER['REMOTE_ADDR']);
$redirectMap = [
'admin' => 'dashboard/pengurus.php',
'pengurus_ibadah' => 'dashboard/pengurus.php',
'masyarakat' => 'dashboard/masyarakat.php',
'pemerintah' => 'dashboard/pemerintah.php',
];
$redirect = $redirectMap[$row['role']] ?? 'index.php';
jsonResponse(['success'=>true,'role'=>$row['role'],'redirect'=>$redirect,'nama'=>$row['nama_lengkap']]);
}
// ── POST: register masyarakat ─────────────────────────────
if ($method === 'POST' && $action === 'register') {
$raw = file_get_contents('php://input');
$d = json_decode($raw, true);
if (!$d) $d = $_POST;
$nama = trim($d['nama_lengkap'] ?? '');
$username = trim($d['username'] ?? '');
$no_hp = trim($d['no_hp'] ?? '');
$email = trim($d['email'] ?? '');
$password = $d['password'] ?? '';
$no_kk = trim($d['no_kk'] ?? '');
if (!$nama) jsonResponse(['success'=>false,'error'=>'Nama lengkap wajib diisi.'], 400);
if (!$username) jsonResponse(['success'=>false,'error'=>'Username wajib diisi.'], 400);
if (!$no_hp) jsonResponse(['success'=>false,'error'=>'Nomor HP wajib diisi.'], 400);
if (!$password) jsonResponse(['success'=>false,'error'=>'Kata sandi wajib diisi.'], 400);
if (strlen($username) < 4) jsonResponse(['success'=>false,'error'=>'Username minimal 4 karakter.'], 400);
if (strlen($password) < 6) jsonResponse(['success'=>false,'error'=>'Kata sandi minimal 6 karakter.'], 400);
if (preg_match('/\s/', $username)) jsonResponse(['success'=>false,'error'=>'Username tidak boleh mengandung spasi.'], 400);
$uname_e = $conn->real_escape_string($username);
$hp_e = $conn->real_escape_string($no_hp);
$cekUser = $conn->query("SELECT id FROM users WHERE username='$uname_e' LIMIT 1")->fetch_assoc();
if ($cekUser) jsonResponse(['success'=>false,'error'=>'Username sudah digunakan, coba username lain.'], 409);
$cekHp = $conn->query("SELECT id FROM users WHERE no_hp='$hp_e' LIMIT 1")->fetch_assoc();
if ($cekHp) jsonResponse(['success'=>false,'error'=>'Nomor HP sudah terdaftar.'], 409);
$hash = password_hash($password, PASSWORD_DEFAULT);
$nama_e = $conn->real_escape_string($nama);
$email_e = $conn->real_escape_string($email);
$hash_e = $conn->real_escape_string($hash);
$id_penduduk = 'NULL';
if ($no_kk) {
$kk_e = $conn->real_escape_string($no_kk);
$kk = $conn->query("SELECT id FROM penduduk WHERE no_kk='$kk_e' LIMIT 1")->fetch_assoc();
if ($kk) $id_penduduk = (int)$kk['id'];
}
$sql = "INSERT INTO users (nama_lengkap, username, no_hp, email, password_hash, role, id_penduduk)
VALUES ('$nama_e', '$uname_e', '$hp_e', '$email_e', '$hash_e', 'masyarakat', $id_penduduk)";
if ($conn->query($sql)) {
$newId = $conn->insert_id;
logAktivitas($conn,'register','users',$newId,"Daftar baru: $username");
jsonResponse(['success'=>true,'message'=>'Akun berhasil dibuat. Silakan login dengan username dan kata sandi Anda.']);
} else {
jsonResponse(['success'=>false,'error'=>'Gagal membuat akun: '.$conn->error], 500);
}
}
// ── POST: register pengurus ibadah (langsung aktif, tanpa pending) ────────
if ($method === 'POST' && $action === 'register_pengurus') {
$raw = file_get_contents('php://input');
$d = json_decode($raw, true);
if (!$d) $d = $_POST;
// Validasi bidang akun
$nama = trim($d['nama_lengkap'] ?? '');
$username = trim($d['username'] ?? '');
$no_hp = trim($d['no_hp'] ?? '');
$email = trim($d['email'] ?? '');
$password = $d['password'] ?? '';
// Validasi bidang rumah ibadah
$nama_ibadah = trim($d['nama_ibadah'] ?? '');
$jenis = $d['jenis_ibadah'] ?? 'Masjid';
$lat = (float)($d['latitude'] ?? 0);
$lon = (float)($d['longitude'] ?? 0);
$alamat = trim($d['alamat'] ?? '');
$kelurahan = trim($d['kelurahan'] ?? '');
$kecamatan = trim($d['kecamatan'] ?? '');
$radius = max(50, min(2000, (int)($d['radius'] ?? 300)));
// Cek wajib
if (!$nama) jsonResponse(['success'=>false,'error'=>'Nama lengkap wajib diisi.'], 400);
if (!$username) jsonResponse(['success'=>false,'error'=>'Username wajib diisi.'], 400);
if (!$no_hp) jsonResponse(['success'=>false,'error'=>'Nomor HP wajib diisi.'], 400);
if (!$password) jsonResponse(['success'=>false,'error'=>'Kata sandi wajib diisi.'], 400);
if (!$nama_ibadah) jsonResponse(['success'=>false,'error'=>'Nama rumah ibadah wajib diisi.'], 400);
if (!$lat || !$lon) jsonResponse(['success'=>false,'error'=>'Lokasi rumah ibadah wajib ditentukan.'], 400);
if (strlen($username) < 4) jsonResponse(['success'=>false,'error'=>'Username minimal 4 karakter.'], 400);
if (strlen($password) < 6) jsonResponse(['success'=>false,'error'=>'Kata sandi minimal 6 karakter.'], 400);
if (preg_match('/\s/', $username)) jsonResponse(['success'=>false,'error'=>'Username tidak boleh mengandung spasi.'], 400);
$valid_jenis = ['Masjid','Gereja','Vihara','Pura','Klenteng','Lainnya'];
if (!in_array($jenis, $valid_jenis)) $jenis = 'Masjid';
$uname_e = $conn->real_escape_string($username);
$hp_e = $conn->real_escape_string($no_hp);
// Cek duplikat di users
if ($conn->query("SELECT id FROM users WHERE username='$uname_e' LIMIT 1")->fetch_assoc())
jsonResponse(['success'=>false,'error'=>'Username sudah digunakan.'], 409);
if ($conn->query("SELECT id FROM users WHERE no_hp='$hp_e' LIMIT 1")->fetch_assoc())
jsonResponse(['success'=>false,'error'=>'Nomor HP sudah terdaftar.'], 409);
// Siapkan data
$hash = password_hash($password, PASSWORD_DEFAULT);
$nama_e = $conn->real_escape_string($nama);
$email_e = $conn->real_escape_string($email);
$hash_e = $conn->real_escape_string($hash);
$nibd_e = $conn->real_escape_string($nama_ibadah);
$jenis_e = $conn->real_escape_string($jenis);
$alamat_e = $conn->real_escape_string($alamat);
$kel_e = $conn->real_escape_string($kelurahan);
$kec_e = $conn->real_escape_string($kecamatan);
// Buat rumah ibadah dan akun pengguna dalam transaksi agar atomic
$conn->begin_transaction();
$ok = $conn->query("INSERT INTO rumah_ibadah (nama,jenis,kontak,email,latitude,longitude,alamat,kelurahan,kecamatan,radius)
VALUES ('$nibd_e','$jenis_e','$hp_e','$email_e',$lat,$lon,'$alamat_e','$kel_e','$kec_e',$radius)");
if (!$ok) {
$conn->rollback();
jsonResponse(['success'=>false,'error'=>'Gagal membuat rumah ibadah: '.$conn->error], 500);
}
$ibadahId = $conn->insert_id;
$ok = $conn->query("INSERT INTO users (nama_lengkap,username,no_hp,email,password_hash,role,id_rumah_ibadah,is_active)
VALUES ('$nama_e','$uname_e','$hp_e','$email_e','$hash_e','pengurus_ibadah',$ibadahId,1)");
if (!$ok) {
$conn->rollback();
jsonResponse(['success'=>false,'error'=>'Gagal membuat akun pengurus: '.$conn->error], 500);
}
$userId = $conn->insert_id;
$conn->commit();
logAktivitas($conn,'register_pengurus','users',$userId,"Pengurus baru langsung aktif: $username, ibadah $ibadahId");
jsonResponse([
'success' => true,
'message' => 'Pendaftaran berhasil. Akun pengurus dan rumah ibadah telah dibuat dan aktif.',
'user_id' => (int)$userId,
'ibadah_id' => (int)$ibadahId
]);
}
// ── PUT: approve / reject pengurus pending (admin) ────────
if ($method === 'PUT' && $action === 'approve_pengurus') {
requireLogin(['admin']);
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id'] ?? 0);
$status = $d['status'] ?? ''; // 'approved' | 'rejected'
$catatan = $conn->real_escape_string($d['catatan'] ?? '');
if (!$id || !in_array($status, ['approved','rejected']))
jsonResponse(['success'=>false,'error'=>'Parameter tidak valid.'], 400);
$pending = $conn->query("SELECT * FROM pengurus_pending WHERE id=$id AND status='pending' LIMIT 1")->fetch_assoc();
if (!$pending) jsonResponse(['success'=>false,'error'=>'Data pendaftaran tidak ditemukan.'], 404);
if ($status === 'approved') {
// Buat rumah ibadah baru
$ni = $conn->real_escape_string($pending['nama_ibadah']);
$ji = $conn->real_escape_string($pending['jenis_ibadah']);
$hp = $conn->real_escape_string($pending['no_hp']);
$em = $conn->real_escape_string($pending['email']);
$al = $conn->real_escape_string($pending['alamat']);
$ke = $conn->real_escape_string($pending['kelurahan']);
$kc = $conn->real_escape_string($pending['kecamatan']);
$lat = (float)$pending['latitude'];
$lon = (float)$pending['longitude'];
$rad = (int)$pending['radius'];
$conn->query("INSERT INTO rumah_ibadah (nama,jenis,kontak,email,latitude,longitude,alamat,kelurahan,kecamatan,radius)
VALUES ('$ni','$ji','$hp','$em',$lat,$lon,'$al','$ke','$kc',$rad)");
$ibadahId = $conn->insert_id;
// Buat user pengurus
$un = $conn->real_escape_string($pending['username']);
$nm = $conn->real_escape_string($pending['nama_lengkap']);
$pw = $conn->real_escape_string($pending['password_hash']);
$eme = $conn->real_escape_string($pending['email']);
$hpe = $conn->real_escape_string($pending['no_hp']);
$conn->query("INSERT INTO users (nama_lengkap,username,no_hp,email,password_hash,role,id_rumah_ibadah,is_active)
VALUES ('$nm','$un','$hpe','$eme','$pw','pengurus_ibadah',$ibadahId,1)");
$userId = $conn->insert_id;
logAktivitas($conn,'approve_pengurus','pengurus_pending',$id,"Approved → user $userId, ibadah $ibadahId");
}
$conn->query("UPDATE pengurus_pending SET status='$status', catatan_admin='$catatan' WHERE id=$id");
jsonResponse([
'success' => true,
'message' => $status === 'approved' ? 'Pendaftaran disetujui. Akun dan rumah ibadah berhasil dibuat.' : 'Pendaftaran ditolak.'
]);
}
// Fallback
jsonResponse(['error'=>'Endpoint tidak valid'], 404);
?>
+158
View File
@@ -0,0 +1,158 @@
<?php
// api/bantuan.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
startSession();
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'list';
// ── GET ───────────────────────────────────────────────────
if ($method === 'GET') {
if ($action === 'list') {
$where = "WHERE 1=1";
if (isset($_GET['id_penduduk']) && $_GET['id_penduduk'])
$where .= " AND b.id_penduduk=".(int)$_GET['id_penduduk'];
if (!empty($_SESSION['role']) && $_SESSION['role']==='pengurus_ibadah') {
$iid = (int)$_SESSION['id_rumah_ibadah'];
$where .= " AND b.id_rumah_ibadah=$iid";
}
if (isset($_GET['id_ibadah']) && $_GET['id_ibadah'])
$where .= " AND b.id_rumah_ibadah=".(int)$_GET['id_ibadah'];
$res = $conn->query("SELECT b.*,
p.nama_kepala_keluarga,p.alamat,p.kelurahan,p.kecamatan,
kb.nama AS jenis_bantuan, kb.sumber AS sumber_bantuan,
ri.nama AS nama_ibadah, rim.nama AS nama_mitra,
u.nama_lengkap AS dicatat_oleh
FROM bantuan b
JOIN penduduk p ON b.id_penduduk=p.id
JOIN kategori_bantuan kb ON b.id_kategori_bantuan=kb.id
LEFT JOIN rumah_ibadah ri ON b.id_rumah_ibadah=ri.id
LEFT JOIN rumah_ibadah rim ON b.id_rumah_ibadah_mitra=rim.id
LEFT JOIN users u ON b.id_pengurus_input=u.id
$where ORDER BY b.tanggal_bantuan DESC LIMIT 500");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
} elseif ($action === 'kategori') {
$res = $conn->query("SELECT * FROM kategori_bantuan ORDER BY sumber,nama");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
} elseif ($action === 'statistik') {
$where = "WHERE status_bantuan='disalurkan'";
if (!empty($_SESSION['role']) && $_SESSION['role'] === 'pengurus_ibadah') {
$iid = (int)($_SESSION['id_rumah_ibadah'] ?? 0);
$where .= " AND id_rumah_ibadah=$iid";
}
if (isset($_GET['id_ibadah']) && $_GET['id_ibadah']) {
$where .= " AND id_rumah_ibadah=".(int)$_GET['id_ibadah'];
}
$total = $conn->query("SELECT COUNT(*) c, COALESCE(SUM(jumlah_nominal),0) nom FROM bantuan $where")->fetch_assoc();
$per_jenis = [];
$res = $conn->query("SELECT kb.nama,kb.sumber,COUNT(*) jumlah,COALESCE(SUM(b.jumlah_nominal),0) nominal
FROM bantuan b JOIN kategori_bantuan kb ON b.id_kategori_bantuan=kb.id
$where GROUP BY kb.id ORDER BY jumlah DESC");
while ($r = $res->fetch_assoc()) $per_jenis[] = $r;
$per_ibadah = [];
$res2 = $conn->query("SELECT ri.nama,COUNT(b.id) jumlah FROM bantuan b
JOIN rumah_ibadah ri ON b.id_rumah_ibadah=ri.id
$where GROUP BY ri.id ORDER BY jumlah DESC");
while ($r = $res2->fetch_assoc()) $per_ibadah[] = $r;
jsonResponse([
'total_bantuan' => (int)$total['c'],
'total_nominal' => (float)$total['nom'],
'per_jenis' => $per_jenis,
'per_ibadah' => $per_ibadah,
]);
}
}
// ── POST: tambah bantuan ──────────────────────────────────
if ($method === 'POST') {
requireLogin(['admin','pengurus_ibadah']);
$d = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$id_penduduk = (int)($d['id_penduduk']??0);
$id_kategori = (int)($d['id_kategori_bantuan']??0);
$id_ibadah = (int)($d['id_rumah_ibadah']??$_SESSION['id_rumah_ibadah']??0) ?: 'NULL';
$id_mitra = (int)($d['id_rumah_ibadah_mitra']??0) ?: 'NULL';
$donatur = esc($conn,$d['nama_donatur']??'');
$tgl = esc($conn,$d['tanggal_bantuan']??date('Y-m-d'));
$nominal = (float)($d['jumlah_nominal']??0) ?: 'NULL';
$bentuk = esc($conn,$d['bentuk_bantuan']??'');
$kondisi = esc($conn,$d['kondisi_saat_diberikan']??'');
$catatan = esc($conn,$d['catatan_tambahan']??'');
$uid = (int)$_SESSION['user_id'];
$status = esc($conn,$d['status_bantuan']??'disalurkan');
$foto = uploadFile('foto_dokumentasi','bantuan');
$foto_e = $foto ? esc($conn,$foto) : '';
$sql = "INSERT INTO bantuan
(id_penduduk,id_kategori_bantuan,id_rumah_ibadah,id_rumah_ibadah_mitra,nama_donatur,
tanggal_bantuan,jumlah_nominal,bentuk_bantuan,kondisi_saat_diberikan,catatan_tambahan,
foto_dokumentasi,status_bantuan,id_pengurus_input)
VALUES ($id_penduduk,$id_kategori,$id_ibadah,$id_mitra,'$donatur','$tgl',$nominal,
'$bentuk','$kondisi','$catatan','$foto_e','$status',$uid)";
if ($conn->query($sql)) {
$bid = $conn->insert_id;
// Notif ke user masyarakat yang terdaftar pada KK ini
$user_kk = $conn->query("SELECT id FROM users WHERE id_penduduk=$id_penduduk AND role='masyarakat'")->fetch_assoc();
if ($user_kk) {
kirimNotifikasi($conn,(int)$user_kk['id'],
'Bantuan Baru Diterima',
"Keluarga Anda menerima bantuan: $bentuk pada ".date('d/m/Y',strtotime($tgl)),
'bantuan_baru',$bid);
}
logAktivitas($conn,'tambah_bantuan','bantuan',$bid,"KK ID:$id_penduduk - $bentuk");
jsonResponse(['success'=>true,'id'=>$bid,'message'=>'Bantuan berhasil dicatat']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── PUT: update bantuan ───────────────────────────────────
if ($method === 'PUT') {
requireLogin(['admin','pengurus_ibadah']);
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)$d['id'];
$bentuk = esc($conn,$d['bentuk_bantuan']??'');
$catatan = esc($conn,$d['catatan_tambahan']??'');
$status = esc($conn,$d['status_bantuan']??'disalurkan');
$mitra = (int)($d['id_rumah_ibadah_mitra']??0) ?: 'NULL';
$sql = "UPDATE bantuan SET bentuk_bantuan='$bentuk',catatan_tambahan='$catatan',
status_bantuan='$status',id_rumah_ibadah_mitra=$mitra WHERE id=$id";
if ($conn->query($sql)) {
logAktivitas($conn,'edit_bantuan','bantuan',$id,'');
jsonResponse(['success'=>true,'message'=>'Bantuan diperbarui']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── DELETE ────────────────────────────────────────────────
if ($method === 'DELETE') {
requireLogin(['admin','pengurus_ibadah']);
$id = (int)($_GET['id']??0);
if ($conn->query("DELETE FROM bantuan WHERE id=$id")) {
logAktivitas($conn,'hapus_bantuan','bantuan',$id,'');
jsonResponse(['success'=>true,'message'=>'Data dihapus']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
?>
+162
View File
@@ -0,0 +1,162 @@
<?php
// api/chat.php — Chat antara masyarakat dan rumah ibadah
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
startSession();
requireLogin(['masyarakat','pengurus_ibadah','admin']);
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'conversations';
$role = $_SESSION['role'] ?? '';
$uid = (int)$_SESSION['user_id'];
$myIbadah = (int)($_SESSION['id_rumah_ibadah'] ?? 0);
// ── GET: daftar percakapan ──────────────────────────────────
if ($method === 'GET' && $action === 'conversations') {
if ($role === 'masyarakat') {
// Percakapan masyarakat (grouped by rumah ibadah)
$res = $conn->query("
SELECT c.id_rumah_ibadah,
ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah,
MAX(c.created_at) AS last_time,
COUNT(*) AS total_pesan,
SUM(c.is_read=0 AND c.pengirim_role='pengurus_ibadah') AS unread
FROM chat_masyarakat c
JOIN rumah_ibadah ri ON c.id_rumah_ibadah=ri.id
WHERE c.id_user_masyarakat=$uid
GROUP BY c.id_rumah_ibadah
ORDER BY last_time DESC
");
} else {
// Percakapan pengurus (grouped by masyarakat user)
$res = $conn->query("
SELECT c.id_user_masyarakat,
u.nama_lengkap AS nama_masyarakat,
MAX(c.created_at) AS last_time,
COUNT(*) AS total_pesan,
SUM(c.is_read=0 AND c.pengirim_role='masyarakat') AS unread
FROM chat_masyarakat c
JOIN users u ON c.id_user_masyarakat=u.id
WHERE c.id_rumah_ibadah=$myIbadah
GROUP BY c.id_user_masyarakat
ORDER BY last_time DESC
");
}
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
}
// ── GET: pesan dalam percakapan ────────────────────────────
if ($method === 'GET' && $action === 'messages') {
if ($role === 'masyarakat') {
$id_ibadah = (int)($_GET['id_rumah_ibadah'] ?? 0);
if (!$id_ibadah) jsonResponse(['error'=>'id_rumah_ibadah diperlukan'], 400);
// Tandai pesan dari pengurus sebagai dibaca
$conn->query("UPDATE chat_masyarakat SET is_read=1
WHERE id_user_masyarakat=$uid AND id_rumah_ibadah=$id_ibadah
AND pengirim_role='pengurus_ibadah' AND is_read=0");
$res = $conn->query("
SELECT c.*, ri.nama AS nama_ibadah, u.nama_lengkap AS nama_pengirim
FROM chat_masyarakat c
JOIN rumah_ibadah ri ON c.id_rumah_ibadah=ri.id
JOIN users u ON c.id_pengirim=u.id
WHERE c.id_user_masyarakat=$uid AND c.id_rumah_ibadah=$id_ibadah
ORDER BY c.created_at ASC LIMIT 200
");
} else {
// Pengurus: lihat chat dengan masyarakat tertentu
$id_user = (int)($_GET['id_user_masyarakat'] ?? 0);
if (!$id_user) jsonResponse(['error'=>'id_user_masyarakat diperlukan'], 400);
// Tandai pesan dari masyarakat sebagai dibaca
$conn->query("UPDATE chat_masyarakat SET is_read=1
WHERE id_user_masyarakat=$id_user AND id_rumah_ibadah=$myIbadah
AND pengirim_role='masyarakat' AND is_read=0");
$res = $conn->query("
SELECT c.*, ri.nama AS nama_ibadah, u.nama_lengkap AS nama_pengirim
FROM chat_masyarakat c
JOIN rumah_ibadah ri ON c.id_rumah_ibadah=ri.id
JOIN users u ON c.id_pengirim=u.id
WHERE c.id_user_masyarakat=$id_user AND c.id_rumah_ibadah=$myIbadah
ORDER BY c.created_at ASC LIMIT 200
");
}
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
}
// ── GET: daftar rumah ibadah (untuk masyarakat memilih tujuan) ──
if ($method === 'GET' && $action === 'ibadah_list') {
$res = $conn->query("SELECT id, nama, jenis, kecamatan FROM rumah_ibadah ORDER BY nama");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
}
// ── GET: hitung unread ──────────────────────────────────────
if ($method === 'GET' && $action === 'unread') {
if ($role === 'masyarakat') {
$r = $conn->query("SELECT COUNT(*) AS n FROM chat_masyarakat
WHERE id_user_masyarakat=$uid AND pengirim_role='pengurus_ibadah' AND is_read=0")
->fetch_assoc();
} else {
$r = $conn->query("SELECT COUNT(*) AS n FROM chat_masyarakat
WHERE id_rumah_ibadah=$myIbadah AND pengirim_role='masyarakat' AND is_read=0")
->fetch_assoc();
}
jsonResponse(['unread' => (int)($r['n'] ?? 0)]);
}
// ── POST: kirim pesan ──────────────────────────────────────
if ($method === 'POST') {
$d = json_decode(file_get_contents('php://input'), true);
$isi = trim(esc($conn, $d['isi'] ?? ''));
if (!$isi) jsonResponse(['success'=>false,'error'=>'Pesan tidak boleh kosong'], 400);
if ($role === 'masyarakat') {
$id_ibadah = (int)($d['id_rumah_ibadah'] ?? 0);
if (!$id_ibadah) jsonResponse(['success'=>false,'error'=>'Rumah ibadah tidak dipilih'], 400);
$sql = "INSERT INTO chat_masyarakat
(id_user_masyarakat, id_rumah_ibadah, id_pengirim, pengirim_role, isi_pesan)
VALUES ($uid, $id_ibadah, $uid, 'masyarakat', '$isi')";
if ($conn->query($sql)) {
$cid = $conn->insert_id;
// Notif ke pengurus rumah ibadah
$peng = $conn->query("SELECT id FROM users WHERE id_rumah_ibadah=$id_ibadah AND role='pengurus_ibadah'");
while ($p = $peng->fetch_assoc()) {
kirimNotifikasi($conn, (int)$p['id'], 'Pesan Chat Baru', 'Ada pesan baru dari masyarakat.', 'chat_baru', $cid);
}
jsonResponse(['success'=>true, 'id'=>$cid]);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error], 500);
}
} else {
// Pengurus membalas ke masyarakat
$id_user_mas = (int)($d['id_user_masyarakat'] ?? 0);
if (!$id_user_mas || !$myIbadah) jsonResponse(['success'=>false,'error'=>'Data tidak lengkap'], 400);
$sql = "INSERT INTO chat_masyarakat
(id_user_masyarakat, id_rumah_ibadah, id_pengirim, pengirim_role, isi_pesan)
VALUES ($id_user_mas, $myIbadah, $uid, 'pengurus_ibadah', '$isi')";
if ($conn->query($sql)) {
$cid = $conn->insert_id;
// Notif ke masyarakat
kirimNotifikasi($conn, $id_user_mas, 'Pesan dari Rumah Ibadah', 'Rumah ibadah mengirim pesan baru.', 'chat_baru', $cid);
jsonResponse(['success'=>true, 'id'=>$cid]);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error], 500);
}
}
}
?>
+146
View File
@@ -0,0 +1,146 @@
<?php
// api/laporan.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
startSession();
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'list';
// ── GET ───────────────────────────────────────────────────
if ($method === 'GET') {
if ($action === 'list') {
$where = "WHERE 1=1";
// Filter wilayah untuk pengurus ibadah
if (!empty($_SESSION['role']) && $_SESSION['role'] === 'pengurus_ibadah') {
$iid = (int)$_SESSION['id_rumah_ibadah'];
$where .= " AND ls.id_rumah_ibadah=$iid";
}
if (isset($_GET['status']) && $_GET['status'])
$where .= " AND ls.status_verifikasi='".esc($conn,$_GET['status'])."'";
if (isset($_GET['jenis']) && $_GET['jenis'])
$where .= " AND ls.jenis_laporan='".esc($conn,$_GET['jenis'])."'";
$res = $conn->query("SELECT ls.*,
p.nama_kepala_keluarga, p.alamat AS alamat_kk, p.kelurahan, p.kecamatan,
u.nama_lengkap AS nama_pelapor, u.no_hp AS hp_pelapor,
ri.nama AS nama_ibadah,
up.nama_lengkap AS nama_pengurus
FROM laporan_status ls
LEFT JOIN penduduk p ON ls.id_penduduk=p.id
LEFT JOIN users u ON ls.id_pelapor_user=u.id
LEFT JOIN rumah_ibadah ri ON ls.id_rumah_ibadah=ri.id
LEFT JOIN users up ON ls.id_pengurus_verif=up.id
$where ORDER BY ls.created_at DESC LIMIT 200");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
} elseif ($action === 'get' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$row = $conn->query("SELECT ls.*,
p.nama_kepala_keluarga,p.alamat AS alamat_kk,
u.nama_lengkap AS nama_pelapor,
ri.nama AS nama_ibadah
FROM laporan_status ls
LEFT JOIN penduduk p ON ls.id_penduduk=p.id
LEFT JOIN users u ON ls.id_pelapor_user=u.id
LEFT JOIN rumah_ibadah ri ON ls.id_rumah_ibadah=ri.id
WHERE ls.id=$id")->fetch_assoc();
jsonResponse($row ?: ['error'=>'Tidak ditemukan']);
}
}
// ── POST: buat laporan baru (dari masyarakat) ─────────────
if ($method === 'POST' && $action === 'kirim') {
startSession();
$d = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$id_penduduk = (int)($d['id_penduduk']??0) ?: 'NULL';
$id_anggota = (int)($d['id_anggota']??0) ?: 'NULL';
$jenis = esc($conn,$d['jenis_laporan']??'lainnya');
$deskripsi = esc($conn,$d['deskripsi']??'');
$nama = esc($conn,$d['nama_terkait']??'');
$tgl = esc($conn,$d['tanggal_kejadian']??date('Y-m-d'));
$uid = isset($_SESSION['user_id'])?(int)$_SESSION['user_id']:'NULL';
if (!$deskripsi) jsonResponse(['success'=>false,'error'=>'Deskripsi wajib diisi'],400);
// Auto-assign rumah ibadah berdasarkan lokasi KK
$id_ibadah = 'NULL';
if ($id_penduduk !== 'NULL') {
$kk = $conn->query("SELECT latitude,longitude FROM penduduk WHERE id=$id_penduduk")->fetch_assoc();
if ($kk) {
$matches = findIbadahInRadius($conn,(float)$kk['latitude'],(float)$kk['longitude']);
if ($matches && $matches[0]) $id_ibadah = (int)$matches[0]['id'];
}
}
$foto = uploadFile('lampiran','laporan');
$foto_e = $foto ? esc($conn,$foto) : '';
$sql = "INSERT INTO laporan_status
(id_penduduk,id_anggota,id_pelapor_user,jenis_laporan,deskripsi,nama_terkait,
tanggal_kejadian,lampiran_foto,id_rumah_ibadah)
VALUES ($id_penduduk,$id_anggota,$uid,'$jenis','$deskripsi','$nama','$tgl','$foto_e',$id_ibadah)";
if ($conn->query($sql)) {
$lid = $conn->insert_id;
// Notifikasi ke pengurus rumah ibadah
if ($id_ibadah !== 'NULL') {
$pengurus = $conn->query("SELECT id FROM users WHERE id_rumah_ibadah=$id_ibadah AND role='pengurus_ibadah'");
while ($p = $pengurus->fetch_assoc()) {
kirimNotifikasi($conn,(int)$p['id'],
"Laporan Baru: ".ucfirst(str_replace('_',' ',$d['jenis_laporan'])),
"Ada laporan baru masuk mengenai: $deskripsi",
'laporan_masuk',$lid);
}
}
logAktivitas($conn,'kirim_laporan','laporan_status',$lid,"Jenis: $jenis");
jsonResponse(['success'=>true,'id'=>$lid,'message'=>'Laporan berhasil dikirim. Terima kasih!']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── PUT: verifikasi laporan (oleh pengurus) ───────────────
if ($method === 'PUT' && $action === 'verifikasi') {
requireLogin(['admin','pengurus_ibadah']);
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)$d['id'];
$status = esc($conn,$d['status_verifikasi']??'diproses');
$catatan= esc($conn,$d['catatan_pengurus']??'');
$uid = (int)$_SESSION['user_id'];
$sql = "UPDATE laporan_status SET status_verifikasi='$status',
catatan_pengurus='$catatan',id_pengurus_verif=$uid,diverifikasi_at=NOW()
WHERE id=$id";
if ($conn->query($sql)) {
// Jika disetujui → update status anggota keluarga
if ($status === 'disetujui') {
$lap = $conn->query("SELECT * FROM laporan_status WHERE id=$id")->fetch_assoc();
if ($lap['id_anggota'] && in_array($lap['jenis_laporan'],['kematian','pindah_keluar'])) {
$new_status = $lap['jenis_laporan']==='kematian'?'meninggal':'pindah';
$aid = (int)$lap['id_anggota'];
$conn->query("UPDATE anggota_keluarga SET status_hidup='$new_status' WHERE id=$aid");
}
// Notif ke pelapor
if ($lap['id_pelapor_user']) {
kirimNotifikasi($conn,(int)$lap['id_pelapor_user'],
'Laporan Disetujui','Laporan Anda telah diverifikasi dan disetujui oleh pengurus.','verifikasi',$id);
}
}
logAktivitas($conn,'verifikasi_laporan','laporan_status',$id,"Status: $status");
jsonResponse(['success'=>true,'message'=>'Status laporan diperbarui']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
?>
+33
View File
@@ -0,0 +1,33 @@
<?php
// api/notifikasi.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
requireLogin();
startSession();
$uid = (int)$_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$only_unread = isset($_GET['unread']) ? "AND is_read=0" : "";
$res = $conn->query("SELECT * FROM notifikasi WHERE id_user=$uid $only_unread ORDER BY created_at DESC LIMIT 50");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
$unread = $conn->query("SELECT COUNT(*) c FROM notifikasi WHERE id_user=$uid AND is_read=0")->fetch_assoc()['c'];
jsonResponse(['notifikasi'=>$data,'unread'=>(int)$unread]);
}
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)($d['id']??0);
if ($id) {
$conn->query("UPDATE notifikasi SET is_read=1 WHERE id=$id AND id_user=$uid");
} else {
$conn->query("UPDATE notifikasi SET is_read=1 WHERE id_user=$uid");
}
jsonResponse(['success'=>true]);
}
?>
+222
View File
@@ -0,0 +1,222 @@
<?php
// api/penduduk.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'geojson';
function buildFeature($row, $conn) {
$pid = (int)$row['id'];
// Anggota keluarga + umur otomatis
$ak = $conn->query("SELECT *,
TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) AS umur
FROM anggota_keluarga WHERE id_penduduk=$pid ORDER BY id");
$anggota = [];
while ($a = $ak->fetch_assoc()) {
// Pendidikan terakhir
$pend = $conn->query("SELECT jenjang,nama_institusi,tahun_lulus,status_pendidikan
FROM riwayat_pendidikan WHERE id_anggota={$a['id']}
ORDER BY tahun_masuk DESC LIMIT 1")->fetch_assoc();
$a['pendidikan_terakhir'] = $pend;
$anggota[] = $a;
}
$row['anggota'] = $anggota;
// Histori bantuan
$bq = $conn->query("SELECT b.*,kb.nama AS jenis_bantuan,ri.nama AS nama_ibadah,rim.nama AS nama_mitra
FROM bantuan b
JOIN kategori_bantuan kb ON b.id_kategori_bantuan=kb.id
LEFT JOIN rumah_ibadah ri ON b.id_rumah_ibadah=ri.id
LEFT JOIN rumah_ibadah rim ON b.id_rumah_ibadah_mitra=rim.id
WHERE b.id_penduduk=$pid ORDER BY b.tanggal_bantuan DESC");
$bantuan = [];
while ($b = $bq->fetch_assoc()) $bantuan[] = $b;
$row['histori_bantuan'] = $bantuan;
$row['jumlah_bantuan'] = count($bantuan);
return [
'type' => 'Feature',
'geometry' => ['type'=>'Point','coordinates'=>[(float)$row['longitude'],(float)$row['latitude']]],
'properties' => $row
];
}
// ── GET ───────────────────────────────────────────────────
if ($method === 'GET') {
if ($action === 'geojson') {
$where = "WHERE 1=1";
if (isset($_GET['status_ekonomi']) && $_GET['status_ekonomi'] !== 'all')
$where .= " AND p.status_ekonomi='".esc($conn,$_GET['status_ekonomi'])."'";
if (isset($_GET['id_ibadah']) && $_GET['id_ibadah'])
$where .= " AND p.id_rumah_ibadah=".(int)$_GET['id_ibadah'];
if (isset($_GET['kecamatan']) && $_GET['kecamatan'])
$where .= " AND p.kecamatan='".esc($conn,$_GET['kecamatan'])."'";
$res = $conn->query("SELECT p.*,ri.nama AS nama_ibadah,ri.jenis AS jenis_ibadah
FROM penduduk p
LEFT JOIN rumah_ibadah ri ON p.id_rumah_ibadah=ri.id
$where ORDER BY p.id DESC");
$features = [];
while ($row = $res->fetch_assoc()) $features[] = buildFeature($row, $conn);
jsonResponse(['type'=>'FeatureCollection','features'=>$features]);
} elseif ($action === 'get' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$row = $conn->query("SELECT p.*,ri.nama AS nama_ibadah
FROM penduduk p LEFT JOIN rumah_ibadah ri ON p.id_rumah_ibadah=ri.id
WHERE p.id=$id")->fetch_assoc();
jsonResponse($row ? buildFeature($row,$conn) : ['error'=>'Tidak ditemukan']);
} elseif ($action === 'dashboard') {
$where = 'WHERE 1=1';
$ibadahFilter = '';
$laporanFilter = '';
if (!empty($_SESSION['role']) && $_SESSION['role'] === 'pengurus_ibadah') {
$iid = (int)($_SESSION['id_rumah_ibadah'] ?? 0);
$where .= " AND p.id_rumah_ibadah=$iid";
$ibadahFilter = " WHERE id=$iid";
$laporanFilter = " WHERE id_rumah_ibadah=$iid";
}
$total = $conn->query("SELECT COUNT(*) c FROM penduduk $where")->fetch_assoc()['c'];
$jiwa = $conn->query("SELECT COALESCE(SUM(jumlah_anggota),0) c FROM penduduk $where")->fetch_assoc()['c'];
$ibadah = $conn->query("SELECT COUNT(*) c FROM rumah_ibadah$ibadahFilter")->fetch_assoc()['c'];
$bantuan = $conn->query("SELECT COUNT(*) c FROM bantuan" . ($laporanFilter ? " WHERE id_rumah_ibadah=$iid" : ''))->fetch_assoc()['c'];
$pending = $conn->query("SELECT COUNT(*) c FROM laporan_status" . ($laporanFilter ? " WHERE status_verifikasi='pending' AND id_rumah_ibadah=$iid" : " WHERE status_verifikasi='pending'") )->fetch_assoc()['c'];
$sm = $conn->query("SELECT COUNT(*) c FROM penduduk $where AND status_ekonomi='sangat_miskin'")->fetch_assoc()['c'];
$miskin = $conn->query("SELECT COUNT(*) c FROM penduduk $where AND status_ekonomi='miskin'")->fetch_assoc()['c'];
$rentan = $conn->query("SELECT COUNT(*) c FROM penduduk $where AND status_ekonomi='rentan'")->fetch_assoc()['c'];
// Per kecamatan
$kecRes = $conn->query("SELECT kecamatan,COUNT(*) AS jumlah_kk,SUM(jumlah_anggota) AS jiwa
FROM penduduk $where GROUP BY kecamatan ORDER BY jumlah_kk DESC");
$perkec = [];
while ($r = $kecRes->fetch_assoc()) $perkec[] = $r;
jsonResponse([
'total_kk' => (int)$total,
'total_jiwa' => (int)$jiwa,
'total_ibadah' => (int)$ibadah,
'total_bantuan' => (int)$bantuan,
'pending_laporan'=> (int)$pending,
'sangat_miskin' => (int)$sm,
'miskin' => (int)$miskin,
'rentan' => (int)$rentan,
'per_kecamatan' => $perkec,
]);
}
}
// ── POST: tambah penduduk ─────────────────────────────────
if ($method === 'POST') {
requireLogin(['admin','pengurus_ibadah','masyarakat']);
$nama = esc($conn,$_POST['nama_kepala_keluarga']??'');
$no_kk = esc($conn,$_POST['no_kk']??'');
$nik = esc($conn,$_POST['nik_kepala']??'');
$lat = (float)($_POST['latitude']??0);
$lon = (float)($_POST['longitude']??0);
$alamat = esc($conn,$_POST['alamat']??'');
$rt = esc($conn,$_POST['rt']??'');
$rw = esc($conn,$_POST['rw']??'');
$kel = esc($conn,$_POST['kelurahan']??'');
$kec = esc($conn,$_POST['kecamatan']??'');
$jml = (int)($_POST['jumlah_anggota']??1);
$sekon = esc($conn,$_POST['status_ekonomi']??'miskin');
$shun = esc($conn,$_POST['status_hunian']??'milik_sendiri');
$nearest = findNearestIbadah($conn,$lat,$lon);
$id_ibadah = $nearest?$nearest['id']:'NULL';
$jarak = $nearest?$nearest['jarak']:0;
$sql = "INSERT INTO penduduk (no_kk,nama_kepala_keluarga,nik_kepala,latitude,longitude,
alamat,rt,rw,kelurahan,kecamatan,jumlah_anggota,id_rumah_ibadah,jarak_ke_ibadah,
status_ekonomi,status_hunian)
VALUES ('$no_kk','$nama','$nik',$lat,$lon,'$alamat','$rt','$rw','$kel','$kec',
$jml,$id_ibadah,$jarak,'$sekon','$shun')";
if ($conn->query($sql)) {
$pid = $conn->insert_id;
// Simpan anggota keluarga
$anggota = json_decode($_POST['anggota']??'[]', true);
foreach ($anggota as $a) {
$anm = esc($conn,$a['nama']??'');
$hub = esc($conn,$a['hubungan']??'Anak');
$jk = esc($conn,$a['jenis_kelamin']??'L');
$tgl = esc($conn,$a['tanggal_lahir']??'2000-01-01');
$tmp = esc($conn,$a['tempat_lahir']??'');
$agm = esc($conn,$a['agama']??'Islam');
$pkj = esc($conn,$a['pekerjaan']??'');
$nik2 = esc($conn,$a['nik']??'');
$conn->query("INSERT INTO anggota_keluarga
(id_penduduk,nik,nama,hubungan,jenis_kelamin,tanggal_lahir,tempat_lahir,agama,pekerjaan)
VALUES ($pid,'$nik2','$anm','$hub','$jk','$tgl','$tmp','$agm','$pkj')");
$aid = $conn->insert_id;
// Riwayat pendidikan per anggota
$pendList = json_decode($a['pendidikan']??'[]', true);
foreach ($pendList as $pd) {
$jj = esc($conn,$pd['jenjang']??'');
$ni = esc($conn,$pd['nama_institusi']??'');
$tm = (int)($pd['tahun_masuk']??0)?:'NULL';
$tl = (int)($pd['tahun_lulus']??0)?:'NULL';
$sp = esc($conn,$pd['status_pendidikan']??'lulus');
$conn->query("INSERT INTO riwayat_pendidikan (id_anggota,jenjang,nama_institusi,tahun_masuk,tahun_lulus,status_pendidikan)
VALUES ($aid,'$jj','$ni',$tm,$tl,'$sp')");
}
}
// Upload foto/dokumen
$foto = uploadFile('foto','penduduk');
logAktivitas($conn,'tambah_penduduk','penduduk',$pid,"Tambah KK: $nama");
jsonResponse(['success'=>true,'id'=>$pid,'message'=>'Data berhasil disimpan','nearest_ibadah'=>$nearest]);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── PUT: update penduduk ──────────────────────────────────
if ($method === 'PUT') {
requireLogin(['admin','pengurus_ibadah']);
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)$d['id'];
$nama = esc($conn,$d['nama_kepala_keluarga']??'');
$lat = (float)($d['latitude']??0);
$lon = (float)($d['longitude']??0);
$alam = esc($conn,$d['alamat']??'');
$jml = (int)($d['jumlah_anggota']??1);
$se = esc($conn,$d['status_ekonomi']??'miskin');
$nearest = findNearestIbadah($conn,$lat,$lon);
$id_ibadah = $nearest?$nearest['id']:'NULL';
$jarak = $nearest?$nearest['jarak']:0;
$sql = "UPDATE penduduk SET nama_kepala_keluarga='$nama',latitude=$lat,longitude=$lon,
alamat='$alam',jumlah_anggota=$jml,status_ekonomi='$se',
id_rumah_ibadah=$id_ibadah,jarak_ke_ibadah=$jarak WHERE id=$id";
if ($conn->query($sql)) {
logAktivitas($conn,'edit_penduduk','penduduk',$id,"Edit KK: $nama");
jsonResponse(['success'=>true,'message'=>'Data diperbarui']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── DELETE ────────────────────────────────────────────────
if ($method === 'DELETE') {
requireLogin(['admin']);
$id = (int)($_GET['id']??0);
if ($conn->query("DELETE FROM penduduk WHERE id=$id")) {
logAktivitas($conn,'hapus_penduduk','penduduk',$id,'');
jsonResponse(['success'=>true,'message'=>'Data dihapus']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
?>
+108
View File
@@ -0,0 +1,108 @@
<?php
// api/pesan.php — komunikasi antar rumah ibadah
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
requireLogin(['admin','pengurus_ibadah']);
startSession();
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'inbox';
$myIbadah = (int)($_SESSION['id_rumah_ibadah']??0);
$uid = (int)$_SESSION['user_id'];
// ── GET: inbox ─────────────────────────────────────────────
if ($method === 'GET' && $action === 'inbox') {
$res = $conn->query("SELECT pai.*,
ri_k.nama AS dari_ibadah, ri_k.jenis AS jenis_dari,
ri_t.nama AS kepada_ibadah,
p.nama_kepala_keluarga, p.alamat AS alamat_kk,
u.nama_lengkap AS nama_pengirim
FROM pesan_antar_ibadah pai
JOIN rumah_ibadah ri_k ON pai.id_pengirim_ibadah=ri_k.id
JOIN rumah_ibadah ri_t ON pai.id_penerima_ibadah=ri_t.id
LEFT JOIN penduduk p ON pai.id_penduduk=p.id
LEFT JOIN users u ON pai.id_pengirim_user=u.id
WHERE pai.id_penerima_ibadah=$myIbadah
ORDER BY pai.created_at DESC LIMIT 100");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
}
// ── GET: outbox ────────────────────────────────────────────
if ($method === 'GET' && $action === 'outbox') {
$res = $conn->query("SELECT pai.*,
ri_t.nama AS kepada_ibadah,
p.nama_kepala_keluarga,
u.nama_lengkap AS nama_pengirim
FROM pesan_antar_ibadah pai
JOIN rumah_ibadah ri_t ON pai.id_penerima_ibadah=ri_t.id
LEFT JOIN penduduk p ON pai.id_penduduk=p.id
LEFT JOIN users u ON pai.id_pengirim_user=u.id
WHERE pai.id_pengirim_ibadah=$myIbadah
ORDER BY pai.created_at DESC LIMIT 100");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
}
// ── POST: kirim pesan ──────────────────────────────────────
if ($method === 'POST') {
$d = json_decode(file_get_contents('php://input'), true);
$to_ibadah = (int)($d['id_penerima_ibadah']??0);
$id_penduduk = (int)($d['id_penduduk']??0) ?: 'NULL';
$perihal = esc($conn,$d['perihal']??'');
$isi = esc($conn,$d['isi_pesan']??'');
if (!$to_ibadah||!$perihal||!$isi)
jsonResponse(['success'=>false,'error'=>'Data tidak lengkap'],400);
$sql = "INSERT INTO pesan_antar_ibadah
(id_pengirim_ibadah,id_penerima_ibadah,id_penduduk,id_pengirim_user,perihal,isi_pesan)
VALUES ($myIbadah,$to_ibadah,$id_penduduk,$uid,'$perihal','$isi')";
if ($conn->query($sql)) {
$pid2 = $conn->insert_id;
// Notif ke pengurus penerima
$peng = $conn->query("SELECT id FROM users WHERE id_rumah_ibadah=$to_ibadah AND role='pengurus_ibadah'");
while ($p = $peng->fetch_assoc()) {
kirimNotifikasi($conn,(int)$p['id'],"Pesan Baru dari Rumah Ibadah","$perihal",'pesan_baru',$pid2);
}
logAktivitas($conn,'kirim_pesan','pesan_antar_ibadah',$pid2,"Ke ibadah ID:$to_ibadah");
jsonResponse(['success'=>true,'id'=>$pid2,'message'=>'Pesan berhasil dikirim']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── PUT: balas / update status ─────────────────────────────
if ($method === 'PUT') {
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)$d['id'];
$status = esc($conn,$d['status_pesan']??'dibaca');
$balas = esc($conn,$d['pesan_balasan']??'');
$sql = "UPDATE pesan_antar_ibadah SET status_pesan='$status',
pesan_balasan='$balas',id_penerima_user=$uid,dibalas_at=NOW()
WHERE id=$id AND id_penerima_ibadah=$myIbadah";
if ($conn->query($sql)) {
// Notif ke pengirim asal
$pesan = $conn->query("SELECT id_pengirim_user FROM pesan_antar_ibadah WHERE id=$id")->fetch_assoc();
if ($pesan && $pesan['id_pengirim_user']) {
kirimNotifikasi($conn,(int)$pesan['id_pengirim_user'],
'Pesan Dibalas','Permintaan bantuan Anda telah direspons.','pesan_baru',$id);
}
jsonResponse(['success'=>true,'message'=>'Pesan diperbarui']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
?>
<?php
// Tambahkan file api/notifikasi.php inline
?>
+132
View File
@@ -0,0 +1,132 @@
<?php
// api/rumah_ibadah.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once '../includes/config.php';
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? 'geojson';
// ── GET ───────────────────────────────────────────────────
if ($method === 'GET') {
if ($action === 'geojson') {
$where = "WHERE 1=1";
if (!empty($_SESSION['role']) && $_SESSION['role'] === 'pengurus_ibadah') {
$iid = (int)($_SESSION['id_rumah_ibadah'] ?? 0);
$where .= " AND ri.id=$iid";
}
if (isset($_GET['id_ibadah']) && $_GET['id_ibadah']) {
$where .= " AND ri.id=".(int)$_GET['id_ibadah'];
}
$res = $conn->query("SELECT ri.*,
(SELECT COUNT(*) FROM penduduk p WHERE p.id_rumah_ibadah=ri.id) AS jumlah_kk,
(SELECT COUNT(*) FROM bantuan b WHERE b.id_rumah_ibadah=ri.id) AS jumlah_bantuan,
(SELECT COUNT(*) FROM laporan_status ls WHERE ls.id_rumah_ibadah=ri.id AND ls.status_verifikasi='pending') AS laporan_pending
FROM rumah_ibadah ri $where ORDER BY ri.id");
$features = [];
while ($row = $res->fetch_assoc()) {
$features[] = [
'type' => 'Feature',
'geometry' => ['type'=>'Point','coordinates'=>[(float)$row['longitude'],(float)$row['latitude']]],
'properties' => $row
];
}
jsonResponse(['type'=>'FeatureCollection','features'=>$features]);
} elseif ($action === 'list') {
$res = $conn->query("SELECT id, nama, jenis, kelurahan, kecamatan FROM rumah_ibadah ORDER BY nama");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
} elseif ($action === 'get' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$r = $conn->query("SELECT * FROM rumah_ibadah WHERE id=$id")->fetch_assoc();
jsonResponse($r ?: ['error'=>'Tidak ditemukan']);
} elseif ($action === 'pengurus' && isset($_GET['id'])) {
// Daftar pengurus rumah ibadah tertentu
$id = (int)$_GET['id'];
$res = $conn->query("SELECT id,nama_lengkap,email,no_hp FROM users WHERE id_rumah_ibadah=$id AND role='pengurus_ibadah'");
$data = [];
while ($r = $res->fetch_assoc()) $data[] = $r;
jsonResponse($data);
}
}
// ── POST ──────────────────────────────────────────────────
if ($method === 'POST') {
requireLogin(['admin','pengurus_ibadah']);
$d = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$nama = esc($conn,$d['nama']??'');
$jenis = esc($conn,$d['jenis']??'Masjid');
$kontak = esc($conn,$d['kontak']??'');
$email = esc($conn,$d['email']??'');
$lat = (float)($d['latitude']??0);
$lon = (float)($d['longitude']??0);
$alamat = esc($conn,$d['alamat']??'');
$kel = esc($conn,$d['kelurahan']??'');
$kec = esc($conn,$d['kecamatan']??'');
$radius = (int)($d['radius']??300);
$sql = "INSERT INTO rumah_ibadah (nama,jenis,kontak,email,latitude,longitude,alamat,kelurahan,kecamatan,radius)
VALUES ('$nama','$jenis','$kontak','$email',$lat,$lon,'$alamat','$kel','$kec',$radius)";
if ($conn->query($sql)) {
$id = $conn->insert_id;
logAktivitas($conn,'tambah_rumah_ibadah','rumah_ibadah',$id,"Tambah: $nama");
jsonResponse(['success'=>true,'id'=>$id,'message'=>'Rumah ibadah berhasil ditambahkan']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── PUT ───────────────────────────────────────────────────
if ($method === 'PUT') {
requireLogin(['admin','pengurus_ibadah']);
$d = json_decode(file_get_contents('php://input'), true);
$id = (int)$d['id'];
$nama = esc($conn,$d['nama']??'');
$jenis = esc($conn,$d['jenis']??'');
$kontak = esc($conn,$d['kontak']??'');
$email = esc($conn,$d['email']??'');
$lat = (float)($d['latitude']??0);
$lon = (float)($d['longitude']??0);
$alamat = esc($conn,$d['alamat']??'');
$kel = esc($conn,$d['kelurahan']??'');
$kec = esc($conn,$d['kecamatan']??'');
$radius = (int)($d['radius']??300);
// Jika pengurus_ibadah, pastikan hanya boleh mengubah rumah ibadah miliknya
if (!empty($_SESSION['role']) && $_SESSION['role'] === 'pengurus_ibadah') {
$myIbadah = (int)($_SESSION['id_rumah_ibadah'] ?? 0);
if ($myIbadah !== $id) {
jsonResponse(['success'=>false,'error'=>'Tidak diizinkan mengubah rumah ibadah lain.'], 403);
}
}
$sql = "UPDATE rumah_ibadah SET nama='$nama',jenis='$jenis',kontak='$kontak',email='$email',
latitude=$lat,longitude=$lon,alamat='$alamat',kelurahan='$kel',kecamatan='$kec',radius=$radius
WHERE id=$id";
if ($conn->query($sql)) {
logAktivitas($conn,'edit_rumah_ibadah','rumah_ibadah',$id,"Edit: $nama");
jsonResponse(['success'=>true,'message'=>'Data diperbarui']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
// ── DELETE ────────────────────────────────────────────────
if ($method === 'DELETE') {
requireLogin(['admin']);
$id = (int)($_GET['id']??0);
if ($conn->query("DELETE FROM rumah_ibadah WHERE id=$id")) {
logAktivitas($conn,'hapus_rumah_ibadah','rumah_ibadah',$id,'');
jsonResponse(['success'=>true,'message'=>'Data dihapus']);
} else {
jsonResponse(['success'=>false,'error'=>$conn->error],500);
}
}
?>
+433
View File
@@ -0,0 +1,433 @@
/* assets/css/style.css — WebGIS Pontianak v2.0 */
/* ── RESET & BASE ─────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #080c14;
--surface: #0f1522;
--card: #141b2d;
--card2: #1a2235;
--border: rgba(99,179,237,0.12);
--accent: #3b82f6;
--accent2: #06b6d4;
--green: #10b981;
--red: #ef4444;
--amber: #f59e0b;
--purple: #8b5cf6;
--pink: #ec4899;
--text: #e2e8f0;
--text2: #7c93b5;
--glow: rgba(59,130,246,0.2);
--radius: 12px;
--radius-lg: 18px;
--shadow: 0 8px 32px rgba(0,0,0,0.4);
--shadow-lg: 0 24px 64px rgba(0,0,0,0.5);
}
html { scroll-behavior: smooth; }
body {
font-family: 'Sora', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ── SCROLLBAR ────────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(99,179,237,0.2); border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: rgba(99,179,237,0.35); }
/* ── TYPOGRAPHY ───────────────────────────────────────── */
h1 { font-size: 28px; font-weight: 800; letter-spacing: -0.5px; }
h2 { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
h3 { font-size: 16px; font-weight: 700; }
h4 { font-size: 13px; font-weight: 700; }
p { font-size: 14px; color: var(--text2); }
small { font-size: 11px; color: var(--text2); }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* ── CARDS ────────────────────────────────────────────── */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px;
transition: border-color 0.2s, transform 0.2s;
}
.card:hover { border-color: rgba(99,179,237,0.25); }
.card-sm { padding: 14px; border-radius: var(--radius); }
/* ── BUTTONS ──────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 9px 18px;
border-radius: var(--radius);
border: none;
font-family: inherit;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
text-decoration: none;
}
.btn-sm { padding: 6px 12px; font-size: 11px; border-radius: 8px; }
.btn-lg { padding: 13px 24px; font-size: 15px; }
.btn-full { width: 100%; }
.btn-primary {
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: #fff;
box-shadow: 0 4px 16px var(--glow);
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 8px 24px var(--glow); }
.btn-primary:active { transform: translateY(0); }
.btn-success { background: linear-gradient(135deg, var(--green), #059669); color: #fff; }
.btn-success:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(16,185,129,0.3); }
.btn-danger { background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.25); color: var(--red); }
.btn-danger:hover { background: rgba(239,68,68,0.2); }
.btn-ghost { background: rgba(255,255,255,0.04); border: 1px solid var(--border); color: var(--text2); }
.btn-ghost:hover { background: rgba(255,255,255,0.08); color: var(--text); }
.btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none !important; box-shadow: none !important; }
/* ── BADGES ───────────────────────────────────────────── */
.badge {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border-radius: 6px;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.3px;
white-space: nowrap;
}
.badge-blue { background: rgba(59,130,246,0.15); color: #93c5fd; }
.badge-green { background: rgba(16,185,129,0.15); color: #6ee7b7; }
.badge-amber { background: rgba(245,158,11,0.15); color: #fcd34d; }
.badge-red { background: rgba(239,68,68,0.15); color: #fca5a5; }
.badge-purple { background: rgba(139,92,246,0.15); color: #c4b5fd; }
.badge-gray { background: rgba(255,255,255,0.07); color: var(--text2); }
/* ── FORMS ────────────────────────────────────────────── */
.form-group { margin-bottom: 16px; }
.form-group label {
display: block;
font-size: 11px;
font-weight: 700;
color: var(--text2);
margin-bottom: 6px;
letter-spacing: 0.3px;
text-transform: uppercase;
}
.form-control {
width: 100%;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
color: var(--text);
padding: 10px 12px;
border-radius: var(--radius);
font-family: inherit;
font-size: 13px;
transition: all 0.2s;
outline: none;
}
.form-control:focus {
border-color: var(--accent);
background: rgba(59,130,246,0.06);
box-shadow: 0 0 0 3px var(--glow);
}
.form-control::placeholder { color: rgba(124,147,181,0.4); }
.form-control option { background: var(--card); color: var(--text); }
textarea.form-control { resize: vertical; min-height: 80px; }
/* ── TABLES ───────────────────────────────────────────── */
.table-wrap { overflow-x: auto; border-radius: var(--radius-lg); border: 1px solid var(--border); }
table.datatable {
width: 100%;
border-collapse: collapse;
}
table.datatable thead tr {
background: rgba(255,255,255,0.03);
border-bottom: 1px solid var(--border);
}
table.datatable th {
padding: 10px 14px;
text-align: left;
font-size: 10px;
font-weight: 800;
color: var(--text2);
letter-spacing: 0.5px;
text-transform: uppercase;
white-space: nowrap;
}
table.datatable td {
padding: 11px 14px;
font-size: 12px;
border-bottom: 1px solid rgba(255,255,255,0.04);
vertical-align: middle;
}
table.datatable tbody tr:hover td { background: rgba(59,130,246,0.03); }
table.datatable tbody tr:last-child td { border-bottom: none; }
/* ── MODALS ───────────────────────────────────────────── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
z-index: 500;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
animation: fadeIn 0.2s;
}
@keyframes fadeIn { from { opacity: 0; } }
.modal-overlay.hidden { display: none; }
.modal {
background: var(--card);
border: 1px solid var(--border);
border-radius: 20px;
width: 90%;
max-width: 560px;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
animation: modalSlide 0.3s cubic-bezier(0.23,1,0.32,1);
}
@keyframes modalSlide { from { opacity: 0; transform: scale(0.95); } }
.modal-lg { max-width: 720px; }
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
background: var(--card);
z-index: 2;
}
.modal-title { font-size: 15px; font-weight: 800; }
.modal-close {
background: rgba(255,255,255,0.05);
border: none;
color: var(--text2);
width: 30px;
height: 30px;
border-radius: 8px;
cursor: pointer;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.modal-close:hover { background: rgba(239,68,68,0.2); color: var(--red); }
.modal-body { padding: 20px 24px; }
.modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border);
display: flex;
gap: 10px;
justify-content: flex-end;
}
/* ── ALERTS / TOASTS ──────────────────────────────────── */
.alert {
padding: 12px 16px;
border-radius: var(--radius);
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.alert-success { background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); color: #6ee7b7; }
.alert-error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); color: #fca5a5; }
.alert-info { background: rgba(59,130,246,0.1); border: 1px solid rgba(59,130,246,0.3); color: #93c5fd; }
.alert-warning { background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3); color: #fcd34d; }
.toast-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
}
.toast {
padding: 12px 18px;
border-radius: var(--radius);
font-size: 13px;
font-weight: 600;
max-width: 320px;
animation: toastIn 0.3s cubic-bezier(0.23,1,0.32,1);
pointer-events: none;
}
@keyframes toastIn { from { opacity: 0; transform: translateY(16px); } }
.toast-success { background: #065f46; border: 1px solid var(--green); color: #6ee7b7; }
.toast-error { background: #7f1d1d; border: 1px solid var(--red); color: #fca5a5; }
.toast-info { background: #1e3a5f; border: 1px solid var(--accent); color: #93c5fd; }
.toast-warning { background: #78350f; border: 1px solid var(--amber); color: #fcd34d; }
/* ── LEAFLET OVERRIDES ────────────────────────────────── */
.leaflet-popup-content-wrapper {
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
padding: 0;
}
.leaflet-popup-content { margin: 0; }
.leaflet-popup-tip-container { display: none; }
.popup-wrap { padding: 12px 14px; min-width: 200px; font-family: 'Sora', sans-serif; }
.popup-nama { font-size: 14px; font-weight: 800; margin-bottom: 8px; color: #1e293b; }
.popup-row { font-size: 12px; margin-bottom: 4px; color: #475569; }
.popup-row strong { color: #1e293b; }
.popup-actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.popup-btn {
padding: 5px 12px;
border-radius: 7px;
border: none;
font-size: 11px;
font-weight: 700;
cursor: pointer;
font-family: 'Sora', sans-serif;
transition: all 0.15s;
}
.pb-blue { background: #3b82f6; color: #fff; }
.pb-green { background: #10b981; color: #fff; }
.pb-amber { background: #f59e0b; color: #fff; }
.pb-blue:hover { background: #2563eb; }
.pb-green:hover { background: #059669; }
.pb-amber:hover { background: #d97706; }
/* ── LOADERS ──────────────────────────────────────────── */
.spinner {
display: inline-block;
width: 18px; height: 18px;
border: 2px solid rgba(255,255,255,0.2);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
vertical-align: middle;
}
.spinner-dark {
border-color: rgba(0,0,0,0.1);
border-top-color: var(--accent);
}
@keyframes spin { to { transform: rotate(360deg); } }
.skeleton {
background: linear-gradient(90deg,
rgba(255,255,255,0.04) 25%,
rgba(255,255,255,0.08) 50%,
rgba(255,255,255,0.04) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 6px;
}
@keyframes shimmer { to { background-position: -200% 0; } }
/* ── UTILS ────────────────────────────────────────────── */
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-muted { color: var(--text2); }
.text-accent { color: var(--accent); }
.text-green { color: var(--green); }
.text-red { color: var(--red); }
.text-amber { color: var(--amber); }
.font-bold { font-weight: 700; }
.font-heavy { font-weight: 800; }
.d-flex { display: flex; }
.d-grid { display: grid; }
.align-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-sm { gap: 8px; }
.gap-md { gap: 14px; }
.gap-lg { gap: 20px; }
.mt-sm { margin-top: 8px; }
.mt-md { margin-top: 16px; }
.mt-lg { margin-top: 24px; }
.mb-sm { margin-bottom: 8px; }
.mb-md { margin-bottom: 16px; }
.mb-lg { margin-bottom: 24px; }
.p-sm { padding: 8px; }
.p-md { padding: 16px; }
.p-lg { padding: 24px; }
.hidden { display: none !important; }
.invisible { visibility: hidden; }
.w-full { width: 100%; }
.h-full { height: 100%; }
/* ── EMPTY STATE ──────────────────────────────────────── */
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text2);
}
.empty-state .es-icon { font-size: 40px; margin-bottom: 12px; }
.empty-state h4 { font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 6px; }
.empty-state p { font-size: 12px; }
/* ── DIVIDERS ─────────────────────────────────────────── */
.section-label {
font-size: 10px;
font-weight: 800;
color: var(--text2);
letter-spacing: 1px;
text-transform: uppercase;
padding: 10px 0 6px;
border-bottom: 1px solid var(--border);
margin-bottom: 12px;
}
hr.divider { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
/* ── GRID HELPERS ─────────────────────────────────────── */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
.grid-4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; }
@media (max-width: 768px) {
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
.hide-mobile { display: none !important; }
}
@media (max-width: 1024px) {
.grid-4 { grid-template-columns: 1fr 1fr; }
.grid-3 { grid-template-columns: 1fr 1fr; }
.hide-tablet { display: none !important; }
}
/* ── ANIMATIONS ───────────────────────────────────────── */
@keyframes fadeIn { from { opacity: 0; } }
@keyframes slideUp { from { opacity: 0; transform: translateY(16px); } }
@keyframes slideDown { from { opacity: 0; transform: translateY(-16px); } }
@keyframes scaleIn { from { opacity: 0; transform: scale(0.95); } }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
@keyframes bounce { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
.animate-fade-in { animation: fadeIn 0.3s; }
.animate-slide-up { animation: slideUp 0.3s cubic-bezier(0.23,1,0.32,1); }
.animate-scale-in { animation: scaleIn 0.3s cubic-bezier(0.23,1,0.32,1); }
.animate-pulse { animation: pulse 2s infinite; }
+344
View File
@@ -0,0 +1,344 @@
// assets/js/app.js — WebGIS Pontianak v2.0 Global Utilities
// ── TOAST SYSTEM ──────────────────────────────────────────
const Toast = (() => {
let container = null;
function getContainer() {
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
}
return container;
}
function show(message, type = 'info', duration = 3500) {
const t = document.createElement('div');
const icons = { success: '✅', error: '❌', info: '️', warning: '⚠️' };
t.className = `toast toast-${type}`;
t.textContent = `${icons[type] || ''} ${message}`;
const c = getContainer();
c.appendChild(t);
setTimeout(() => {
t.style.opacity = '0';
t.style.transform = 'translateY(12px)';
t.style.transition = 'all 0.3s';
setTimeout(() => t.remove(), 300);
}, duration);
return t;
}
return { show, success: m => show(m,'success'), error: m => show(m,'error'), info: m => show(m,'info'), warning: m => show(m,'warning') };
})();
// ── API HELPER ────────────────────────────────────────────
const API = {
async get(url) {
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
async post(url, data, isFormData = false) {
const opts = { method: 'POST' };
if (isFormData) { opts.body = data; }
else { opts.headers = { 'Content-Type': 'application/json' }; opts.body = JSON.stringify(data); }
const res = await fetch(url, opts);
return res.json();
},
async put(url, data) {
const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
return res.json();
},
async delete(url) {
const res = await fetch(url, { method: 'DELETE' });
return res.json();
}
};
// ── DATE & TIME HELPERS ───────────────────────────────────
const DateTime = {
format(dateStr, options = {}) {
if (!dateStr) return '—';
const defaults = { day: 'numeric', month: 'short', year: 'numeric' };
return new Date(dateStr).toLocaleDateString('id-ID', { ...defaults, ...options });
},
formatFull(dateStr) {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleString('id-ID', {
day: 'numeric', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
},
ago(dateStr) {
if (!dateStr) return '—';
const diff = Date.now() - new Date(dateStr).getTime();
const m = Math.floor(diff / 60000);
if (m < 1) return 'Baru saja';
if (m < 60) return `${m} menit lalu`;
const h = Math.floor(m / 60);
if (h < 24) return `${h} jam lalu`;
const d = Math.floor(h / 24);
if (d < 7) return `${d} hari lalu`;
return DateTime.format(dateStr);
},
age(dob) {
if (!dob) return 0;
return Math.floor((Date.now() - new Date(dob).getTime()) / (365.25 * 24 * 3600 * 1000));
},
today() {
return new Date().toISOString().split('T')[0];
}
};
// ── NUMBER HELPERS ────────────────────────────────────────
const Num = {
currency(val, prefix = 'Rp') {
if (!val && val !== 0) return '—';
const n = parseFloat(val);
if (n >= 1e9) return `${prefix}${(n/1e9).toFixed(1)}M`;
if (n >= 1e6) return `${prefix}${(n/1e6).toFixed(1)}jt`;
if (n >= 1e3) return `${prefix}${(n/1e3).toFixed(0)}rb`;
return `${prefix}${n.toLocaleString('id-ID')}`;
},
format(val) {
if (!val && val !== 0) return '—';
return parseFloat(val).toLocaleString('id-ID');
},
pct(val, total) {
if (!total) return '0%';
return `${((val / total) * 100).toFixed(1)}%`;
}
};
// ── BADGE HELPERS ─────────────────────────────────────────
const Badge = {
statusEkonomi(se) {
const map = {
sangat_miskin: { cls: 'badge-red', label: 'Sangat Miskin' },
miskin: { cls: 'badge-amber', label: 'Miskin' },
rentan: { cls: 'badge-purple', label: 'Rentan' },
mampu: { cls: 'badge-green', label: 'Mampu' },
};
const v = map[se] || { cls: 'badge-gray', label: se };
return `<span class="badge ${v.cls}">${v.label}</span>`;
},
statusVerif(s) {
const map = {
pending: { cls: 'badge-amber', label: '⏳ Pending' },
diproses: { cls: 'badge-blue', label: '🔄 Diproses' },
disetujui: { cls: 'badge-green', label: '✅ Disetujui' },
ditolak: { cls: 'badge-red', label: '❌ Ditolak' },
};
const v = map[s] || { cls: 'badge-gray', label: s };
return `<span class="badge ${v.cls}">${v.label}</span>`;
},
statusBantuan(s) {
const map = {
disalurkan: { cls: 'badge-green', label: '✅ Disalurkan' },
ditunda: { cls: 'badge-amber', label: '⏳ Ditunda' },
dibatalkan: { cls: 'badge-red', label: '❌ Dibatalkan' },
};
const v = map[s] || { cls: 'badge-gray', label: s };
return `<span class="badge ${v.cls}">${v.label}</span>`;
},
jenisLaporan(j) {
const map = {
kelahiran: '🍼 Kelahiran',
kematian: '⚰️ Kematian',
pindah_masuk: '📦 Pindah Masuk',
pindah_keluar: '🚚 Pindah Keluar',
pernikahan: '💒 Pernikahan',
perceraian: '📋 Perceraian',
perubahan_data:'📝 Perubahan Data',
lainnya: '📌 Lainnya',
};
return map[j] || j;
},
jenisIbadah(j) {
const map = { Masjid:'🕌', Gereja:'⛪', Vihara:'🏯', Pura:'🛕', Klenteng:'⛩️' };
return `${map[j] || '🏢'} ${j}`;
}
};
// ── MAP ICON FACTORY ──────────────────────────────────────
const MapIcon = {
make(color, emoji, size = 30) {
return L.divIcon({
className: '',
html: `<div style="width:${size}px;height:${size}px;background:${color};border-radius:50%;
display:flex;align-items:center;justify-content:center;font-size:${size*0.5}px;
box-shadow:0 3px 10px rgba(0,0,0,0.5);border:2px solid rgba(255,255,255,0.25)">${emoji}</div>`,
iconSize: [size, size],
iconAnchor: [size/2, size/2],
popupAnchor: [0, -size/2]
});
},
forIbadah(jenis) {
const map = {
masjid: ['#3b82f6','🕌'],
gereja: ['#8b5cf6','⛪'],
vihara: ['#f59e0b','🏯'],
pura: ['#10b981','🛕'],
klenteng: ['#06b6d4','⛩️'],
};
const [color, emoji] = map[(jenis||'').toLowerCase()] || ['#64748b','🏢'];
return this.make(color, emoji);
},
forPenduduk(statusEkonomi) {
const map = {
sangat_miskin: ['#ef4444','⚠️'],
miskin: ['#f97316','🔸'],
rentan: ['#eab308','🔶'],
mampu: ['#22c55e','🔷'],
};
const [color, emoji] = map[statusEkonomi] || ['#64748b','📍'];
return this.make(color, emoji);
}
};
// ── MODAL MANAGER ─────────────────────────────────────────
const Modal = {
open(id) { document.getElementById(id)?.classList.remove('hidden'); },
close(id) { document.getElementById(id)?.classList.add('hidden'); },
toggle(id){ document.getElementById(id)?.classList.toggle('hidden'); },
};
// ── LOADING BUTTON ────────────────────────────────────────
function setLoading(btn, loading, label = '') {
if (loading) {
btn._origLabel = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = `<div class="spinner"></div> ${label || 'Memproses...'}`;
} else {
btn.disabled = false;
btn.innerHTML = btn._origLabel || label;
}
}
// ── FORM SERIALIZER ───────────────────────────────────────
function serializeForm(formId) {
const form = document.getElementById(formId);
if (!form) return {};
const data = {};
form.querySelectorAll('input, select, textarea').forEach(el => {
if (el.name && !el.disabled) {
if (el.type === 'checkbox') data[el.name] = el.checked;
else if (el.type === 'radio') { if (el.checked) data[el.name] = el.value; }
else data[el.name] = el.value;
}
});
return data;
}
// ── DEBOUNCE ──────────────────────────────────────────────
function debounce(fn, delay = 300) {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); };
}
// ── CSV EXPORT ────────────────────────────────────────────
function exportToCSV(data, filename = 'export.csv') {
if (!data.length) { Toast.warning('Tidak ada data untuk diekspor'); return; }
const headers = Object.keys(data[0]);
const rows = [headers, ...data.map(r => headers.map(h => `"${String(r[h] ?? '').replace(/"/g, '""')}"` ))];
const csv = rows.map(r => r.join(',')).join('\n');
const a = document.createElement('a');
a.href = 'data:text/csv;charset=utf-8,\uFEFF' + encodeURIComponent(csv);
a.download = filename;
a.click();
}
// ── CONFIRM DIALOG ────────────────────────────────────────
function confirmAction(message, onConfirm) {
// Buat modal konfirmasi sederhana
const id = 'confirm-' + Date.now();
const el = document.createElement('div');
el.className = 'modal-overlay animate-fade-in';
el.id = id;
el.innerHTML = `
<div class="modal" style="max-width:380px">
<div class="modal-header">
<div class="modal-title">⚠️ Konfirmasi</div>
</div>
<div class="modal-body">
<p style="font-size:14px;color:var(--text);line-height:1.6">${message}</p>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="document.getElementById('${id}').remove()">Batal</button>
<button class="btn btn-danger" id="${id}-ok">Ya, Lanjutkan</button>
</div>
</div>`;
document.body.appendChild(el);
document.getElementById(`${id}-ok`).onclick = () => { el.remove(); onConfirm(); };
}
// ── HAVERSINE (client-side) ───────────────────────────────
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371000;
const phi1 = lat1 * Math.PI / 180, phi2 = lat2 * Math.PI / 180;
const dphi = (lat2 - lat1) * Math.PI / 180;
const dl = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dphi/2)**2 + Math.cos(phi1)*Math.cos(phi2)*Math.sin(dl/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
// ── GEOLOCATION ───────────────────────────────────────────
function getCurrentLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) { reject(new Error('Geolocation tidak didukung')); return; }
navigator.geolocation.getCurrentPosition(
pos => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
err => reject(err),
{ enableHighAccuracy: true, timeout: 8000 }
);
});
}
// ── NOTIFIKASI POLLING ────────────────────────────────────
const NotifPoller = {
interval: null,
callbacks: [],
start(intervalMs = 30000) {
this.poll();
this.interval = setInterval(() => this.poll(), intervalMs);
},
stop() {
if (this.interval) clearInterval(this.interval);
},
async poll() {
try {
const d = await API.get('../api/notifikasi.php?unread=1');
this.callbacks.forEach(cb => cb(d));
} catch (e) { /* silent */ }
},
onChange(cb) { this.callbacks.push(cb); }
};
// ── STORAGE HELPER (sessionStorage) ──────────────────────
const Store = {
set(key, val) { try { sessionStorage.setItem(key, JSON.stringify(val)); } catch(e){} },
get(key) { try { return JSON.parse(sessionStorage.getItem(key)); } catch(e){ return null; } },
remove(key) { sessionStorage.removeItem(key); },
};
// ── INIT ON DOM READY ─────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', e => {
if (e.target === overlay) overlay.classList.add('hidden');
});
});
// Auto-focus first input in opened modals
document.querySelectorAll('.modal-overlay .modal-close').forEach(btn => {
btn.addEventListener('click', () => {
btn.closest('.modal-overlay')?.classList.add('hidden');
});
});
});
+613
View File
@@ -0,0 +1,613 @@
<?php
require_once 'includes/config.php';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daftar Akun — WebGIS Pontianak</title>
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>
:root{
--bg:#080c14;--surface:#0f1522;--card:#141b2d;--border:rgba(99,179,237,0.12);
--accent:#3b82f6;--accent2:#06b6d4;--green:#10b981;--red:#ef4444;--amber:#f59e0b;
--text:#e2e8f0;--text2:#7c93b5;--glow:rgba(59,130,246,0.25);
--purple:#8b5cf6;--purple-glow:rgba(139,92,246,0.25);
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Sora',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.bg-grid{position:fixed;inset:0;background-image:linear-gradient(rgba(59,130,246,0.04) 1px,transparent 1px),linear-gradient(90deg,rgba(59,130,246,0.04) 1px,transparent 1px);background-size:40px 40px;pointer-events:none}
.orb{position:fixed;border-radius:50%;filter:blur(80px);pointer-events:none}
.o1{width:500px;height:500px;background:rgba(59,130,246,0.06);top:-200px;right:-100px}
.o2{width:400px;height:400px;background:rgba(6,182,212,0.05);bottom:-100px;left:-100px}
.wrap{position:relative;z-index:10;width:100%;max-width:560px;background:var(--card);border:1px solid var(--border);border-radius:24px;overflow:hidden;box-shadow:0 40px 80px rgba(0,0,0,0.5);animation:wrapIn 0.6s cubic-bezier(0.23,1,0.32,1)}
@keyframes wrapIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:none}}
.wrap-head{background:linear-gradient(135deg,#0d1929,#060e1a);padding:22px 28px;text-align:center;border-bottom:1px solid var(--border)}
.head-title{font-size:18px;font-weight:800;margin-bottom:3px}
.head-sub{font-size:11px;color:var(--text2)}
/* ROLE TABS */
.role-tabs{display:flex;gap:0;padding:12px 16px 0;border-bottom:1px solid var(--border);background:rgba(0,0,0,0.2)}
.rtab{flex:1;padding:9px 6px;border:none;background:none;font-family:'Sora',sans-serif;font-size:11px;font-weight:700;color:var(--text2);cursor:pointer;border-bottom:2px solid transparent;transition:all 0.2s;display:flex;align-items:center;justify-content:center;gap:5px}
.rtab.active{color:var(--accent);border-bottom-color:var(--accent)}
.rtab.active.pengurus-tab{color:var(--purple);border-bottom-color:var(--purple)}
.form-body{padding:20px 28px}
/* STEP BAR */
.step-bar{display:flex;gap:0;margin-bottom:18px;background:rgba(255,255,255,0.04);border:1px solid var(--border);border-radius:12px;padding:4px}
.step{flex:1;padding:6px;border-radius:9px;text-align:center;font-size:9px;font-weight:700;color:var(--text2);transition:all 0.2s}
.step.active{background:var(--accent);color:#fff;box-shadow:0 4px 12px var(--glow)}
.step.active.p-step{background:var(--purple);box-shadow:0 4px 12px var(--purple-glow)}
.step.done{background:rgba(16,185,129,0.15);color:var(--green)}
/* FIELDS */
.fl{margin-bottom:11px}
.fl label{display:block;font-size:10px;font-weight:800;color:var(--text2);margin-bottom:4px;letter-spacing:0.4px;text-transform:uppercase}
.fl input,.fl select{width:100%;background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text);padding:9px 11px;border-radius:10px;font-family:'Sora',sans-serif;font-size:12px;transition:all 0.2s;outline:none}
.fl input:focus,.fl select:focus{border-color:var(--accent);background:rgba(59,130,246,0.05);box-shadow:0 0 0 3px var(--glow)}
.fl input::placeholder{color:rgba(124,147,181,0.4)}
.fl .hint{font-size:9px;color:var(--text2);margin-top:3px;min-height:13px}
.fl select option{background:#0d1424}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.pw-wrap{position:relative}
.pw-wrap input{padding-right:38px}
.pw-toggle{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text2);cursor:pointer;font-size:14px;padding:2px}
.strength-bar{height:3px;border-radius:2px;background:rgba(255,255,255,0.08);margin-top:4px;overflow:hidden}
.strength-fill{height:100%;border-radius:2px;transition:all 0.3s;width:0}
/* MAP */
.map-section{margin-bottom:11px}
.map-section label{display:block;font-size:10px;font-weight:800;color:var(--text2);margin-bottom:4px;letter-spacing:0.4px;text-transform:uppercase}
#loc-map{height:220px;border-radius:10px;border:1px solid var(--border);overflow:hidden;cursor:crosshair}
.map-info{background:rgba(59,130,246,0.06);border:1px solid rgba(59,130,246,0.18);border-radius:8px;padding:8px 10px;font-size:10px;color:rgba(147,197,253,0.85);margin-top:6px;line-height:1.6;display:flex;align-items:flex-start;gap:6px}
.map-info.has-loc{background:rgba(16,185,129,0.06);border-color:rgba(16,185,129,0.2);color:rgba(110,231,183,0.9)}
.map-coords{display:flex;gap:6px;margin-top:6px}
.map-coords .fl{flex:1;margin-bottom:0}
/* RADIUS SLIDER */
.radius-section{margin-bottom:11px}
.radius-section label{display:block;font-size:10px;font-weight:800;color:var(--text2);margin-bottom:4px;letter-spacing:0.4px;text-transform:uppercase}
.radius-ctrl{display:flex;align-items:center;gap:10px;background:rgba(255,255,255,0.04);border:1px solid var(--border);border-radius:10px;padding:10px 12px}
.radius-ctrl input[type=range]{flex:1;-webkit-appearance:none;height:4px;border-radius:2px;background:linear-gradient(to right,var(--purple) 0%,var(--purple) var(--pct,30%),rgba(255,255,255,0.1) var(--pct,30%),rgba(255,255,255,0.1) 100%);outline:none;cursor:pointer}
.radius-ctrl input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--purple);box-shadow:0 2px 8px var(--purple-glow);cursor:pointer}
.radius-val{font-size:13px;font-weight:800;color:var(--purple);min-width:52px;text-align:right}
/* ALERTS */
.alert-box{padding:9px 12px;border-radius:10px;font-size:11px;margin-bottom:10px;display:none;align-items:center;gap:7px}
.alert-box.show{display:flex}
.alert-err{background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);color:#fca5a5}
.alert-ok{background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.3);color:#6ee7b7}
/* BUTTONS */
.btn{width:100%;padding:11px;border-radius:11px;border:none;font-family:'Sora',sans-serif;font-size:12px;font-weight:700;cursor:pointer;transition:all 0.2s;display:flex;align-items:center;justify-content:center;gap:7px;text-decoration:none}
.btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;box-shadow:0 8px 24px var(--glow)}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 12px 32px var(--glow)}
.btn-primary:disabled{opacity:0.6;cursor:not-allowed;transform:none}
.btn-purple{background:linear-gradient(135deg,var(--purple),#a78bfa);color:#fff;box-shadow:0 8px 24px var(--purple-glow)}
.btn-purple:hover{transform:translateY(-1px);box-shadow:0 12px 32px var(--purple-glow)}
.btn-purple:disabled{opacity:0.6;cursor:not-allowed;transform:none}
.btn-ghost{background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text2)}
.btn-ghost:hover{background:rgba(255,255,255,0.08);color:var(--text)}
.btn-row{display:flex;gap:8px}
.btn-row .btn{flex:1}
.spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.info-box{background:rgba(59,130,246,0.06);border:1px solid rgba(59,130,246,0.18);border-radius:9px;padding:9px 11px;font-size:10px;color:rgba(147,197,253,0.85);margin-bottom:11px;line-height:1.6}
.info-box.purple{background:rgba(139,92,246,0.06);border-color:rgba(139,92,246,0.18);color:rgba(196,181,253,0.9)}
.form-foot{padding:12px 28px;border-top:1px solid var(--border);text-align:center;font-size:11px;color:var(--text2)}
.form-foot a{color:var(--accent);text-decoration:none;font-weight:600}
/* SUCCESS */
.success-wrap{text-align:center;padding:8px 0}
.success-icon{font-size:48px;margin-bottom:12px;animation:pop 0.5s cubic-bezier(0.23,1,0.32,1)}
@keyframes pop{from{transform:scale(0)}to{transform:scale(1)}}
.success-title{font-size:17px;font-weight:800;margin-bottom:7px}
.success-sub{font-size:12px;color:var(--text2);margin-bottom:16px;line-height:1.6}
.success-list{background:rgba(16,185,129,0.07);border:1px solid rgba(16,185,129,0.2);border-radius:11px;padding:12px;text-align:left;font-size:10px;color:var(--text2);line-height:2.1;margin-bottom:18px}
.success-list b{color:var(--green)}
.success-warn{background:rgba(245,158,11,0.07);border:1px solid rgba(245,158,11,0.2);border-radius:11px;padding:12px;text-align:left;font-size:10px;color:rgba(253,230,138,0.85);line-height:2;margin-bottom:18px}
.loc-btn{width:100%;padding:8px;border-radius:8px;border:1px dashed rgba(139,92,246,0.3);background:rgba(139,92,246,0.05);color:rgba(196,181,253,0.8);font-family:'Sora',sans-serif;font-size:10px;font-weight:700;cursor:pointer;transition:all 0.2s;margin-bottom:7px}
.loc-btn:hover{background:rgba(139,92,246,0.12);border-color:rgba(139,92,246,0.5)}
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="orb o1"></div>
<div class="orb o2"></div>
<div class="wrap">
<div class="wrap-head">
<div class="head-title">📋 Daftar Akun</div>
<div class="head-sub">WebGIS Pontianak — Pilih jenis akun yang ingin Anda buat</div>
</div>
<!-- ROLE TABS -->
<div class="role-tabs">
<button class="rtab active" id="tab-masy" onclick="switchRole('masyarakat')">
👤 Masyarakat
</button>
<button class="rtab pengurus-tab" id="tab-peng" onclick="switchRole('pengurus')">
🕌 Pengurus Ibadah
</button>
</div>
<!-- ══════════════ FORM MASYARAKAT ══════════════ -->
<div id="form-masyarakat">
<div class="form-body">
<div class="step-bar" id="masy-steps">
<div class="step active" id="ms1">① Akun</div>
<div class="step" id="ms2">② Data Diri</div>
<div class="step" id="ms3">③ Selesai</div>
</div>
<div id="m-alert" class="alert-box"></div>
<!-- M-Step 1 -->
<div id="m-s1">
<div class="fl">
<label>Username *</label>
<input type="text" id="m-username" placeholder="Minimal 4 karakter, tanpa spasi" oninput="checkUname('m')">
<div class="hint" id="m-uname-hint"></div>
</div>
<div class="fl">
<label>Kata Sandi *</label>
<div class="pw-wrap">
<input type="password" id="m-password" placeholder="Minimal 6 karakter" oninput="checkPw('m')">
<button type="button" class="pw-toggle" onclick="togPw('m-password')">👁️</button>
</div>
<div class="strength-bar"><div class="strength-fill" id="m-pw-str"></div></div>
<div class="hint" id="m-pw-hint"></div>
</div>
<div class="fl">
<label>Konfirmasi Kata Sandi *</label>
<div class="pw-wrap">
<input type="password" id="m-confirm" placeholder="Ulangi kata sandi" oninput="checkConfirm('m')">
<button type="button" class="pw-toggle" onclick="togPw('m-confirm')">👁️</button>
</div>
<div class="hint" id="m-confirm-hint"></div>
</div>
<button class="btn btn-primary" onclick="mNext()">Lanjut →</button>
</div>
<!-- M-Step 2 -->
<div id="m-s2" style="display:none">
<div class="info-box">💡 Data ini untuk menghubungkan akun dengan data keluarga dalam sistem.</div>
<div class="fl"><label>Nama Lengkap *</label><input type="text" id="m-nama" placeholder="Nama lengkap sesuai KTP"></div>
<div class="fl"><label>Nomor HP / WhatsApp *</label><input type="tel" id="m-hp" placeholder="08xx-xxxx-xxxx"></div>
<div class="fl"><label>Email (opsional)</label><input type="email" id="m-email" placeholder="email@contoh.com"></div>
<div class="fl">
<label>Nomor KK (opsional)</label>
<input type="text" id="m-nokk" placeholder="16 digit nomor Kartu Keluarga" maxlength="16" inputmode="numeric">
<div class="hint">Jika diisi, akun Anda terhubung otomatis ke data keluarga.</div>
</div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="mBack()">← Kembali</button>
<button class="btn btn-primary" id="m-btn-reg" onclick="doRegisterMasy()">✅ Buat Akun</button>
</div>
</div>
<!-- M-Step 3 Sukses -->
<div id="m-s3" style="display:none">
<div class="success-wrap">
<div class="success-icon">🎉</div>
<div class="success-title">Akun Berhasil Dibuat!</div>
<div class="success-sub">Selamat datang di WebGIS Pontianak.<br>Anda siap berkontribusi untuk komunitas.</div>
<div class="success-list">
<b>Yang bisa Anda lakukan:</b><br>
📢 Laporkan kelahiran, kematian, kepindahan<br>
🗺️ Lihat peta sebaran penduduk sekitar<br>
🤝 Pantau riwayat bantuan keluarga<br>
📍 Bagikan lokasi Anda di peta
</div>
<a href="login.php" class="btn btn-primary">🚀 Masuk Sekarang</a>
</div>
</div>
</div>
<div class="form-foot" id="m-foot">Sudah punya akun? <a href="login.php">Masuk di sini</a></div>
</div>
<!-- ══════════════ FORM PENGURUS ══════════════ -->
<div id="form-pengurus" style="display:none">
<div class="form-body">
<div class="step-bar" id="peng-steps">
<div class="step active p-step" id="ps1">① Akun</div>
<div class="step" id="ps2">② Rumah Ibadah</div>
<div class="step" id="ps3">③ Lokasi</div>
<div class="step" id="ps4">④ Selesai</div>
</div>
<div id="p-alert" class="alert-box"></div>
<!-- P-Step 1: Data Akun -->
<div id="p-s1">
<div class="info-box purple">🕌 Daftar sebagai pengurus rumah ibadah baru. Akun dan data rumah ibadah akan dibuat langsung setelah pendaftaran.</div>
<div class="fl"><label>Nama Lengkap *</label><input type="text" id="p-nama" placeholder="Nama lengkap pengurus"></div>
<div class="fl">
<label>Username *</label>
<input type="text" id="p-username" placeholder="Minimal 4 karakter" oninput="checkUname('p')">
<div class="hint" id="p-uname-hint"></div>
</div>
<div class="form-grid">
<div class="fl"><label>Nomor HP *</label><input type="tel" id="p-hp" placeholder="08xx-xxxx-xxxx"></div>
<div class="fl"><label>Email</label><input type="email" id="p-email" placeholder="email@..."></div>
</div>
<div class="fl">
<label>Kata Sandi *</label>
<div class="pw-wrap">
<input type="password" id="p-password" placeholder="Minimal 6 karakter" oninput="checkPw('p')">
<button type="button" class="pw-toggle" onclick="togPw('p-password')">👁️</button>
</div>
<div class="strength-bar"><div class="strength-fill" id="p-pw-str"></div></div>
<div class="hint" id="p-pw-hint"></div>
</div>
<div class="fl">
<label>Konfirmasi Kata Sandi *</label>
<div class="pw-wrap">
<input type="password" id="p-confirm" placeholder="Ulangi kata sandi" oninput="checkConfirm('p')">
<button type="button" class="pw-toggle" onclick="togPw('p-confirm')">👁️</button>
</div>
<div class="hint" id="p-confirm-hint"></div>
</div>
<button class="btn btn-purple" onclick="pNext1()">Lanjut →</button>
</div>
<!-- P-Step 2: Info Rumah Ibadah -->
<div id="p-s2" style="display:none">
<div class="fl"><label>Nama Rumah Ibadah *</label><input type="text" id="p-nama-ibadah" placeholder="Masjid Al-Ikhlas / GBI Pontianak..."></div>
<div class="form-grid">
<div class="fl">
<label>Jenis *</label>
<select id="p-jenis">
<option>Masjid</option><option>Gereja</option><option>Vihara</option>
<option>Pura</option><option>Klenteng</option><option>Lainnya</option>
</select>
</div>
<div class="fl"><label>Kelurahan</label><input type="text" id="p-kel" placeholder="Auto dari peta"></div>
</div>
<div class="fl"><label>Kecamatan</label><input type="text" id="p-kec" placeholder="Auto dari peta"></div>
<!-- Radius Slider -->
<div class="radius-section">
<label>Radius Binaan (meter)</label>
<div class="radius-ctrl">
<input type="range" id="p-radius-range" min="50" max="2000" value="300"
oninput="updateRadiusDisplay('p',this.value)">
<span class="radius-val" id="p-radius-val">300 m</span>
</div>
<div class="hint" style="margin-top:5px">Jangkauan wilayah binaan rumah ibadah di peta</div>
</div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="pBack1()">← Kembali</button>
<button class="btn btn-purple" onclick="pNext2()">Lanjut →</button>
</div>
</div>
<!-- P-Step 3: Lokasi via Peta -->
<div id="p-s3" style="display:none">
<div class="map-section">
<label>📍 Klik peta untuk tentukan lokasi rumah ibadah *</label>
<button class="loc-btn" id="btn-gps" onclick="getGPS()">📡 Gunakan lokasi GPS saya sebagai titik awal</button>
<div id="loc-map"></div>
<div class="map-info" id="map-info">
🖱️ Klik di peta untuk menandai lokasi rumah ibadah. Alamat akan terisi otomatis.
</div>
</div>
<div class="map-coords">
<div class="fl"><label>Latitude</label><input class="finput" type="text" id="p-lat" readonly placeholder="—"></div>
<div class="fl"><label>Longitude</label><input class="finput" type="text" id="p-lng" readonly placeholder="—"></div>
</div>
<div class="fl"><label>Alamat Lengkap</label><input type="text" id="p-alamat" placeholder="Diisi otomatis dari peta"></div>
<div class="btn-row" style="margin-top:6px">
<button class="btn btn-ghost" onclick="pBack2()">← Kembali</button>
<button class="btn btn-purple" id="p-btn-reg" onclick="doRegisterPengurus()">📨 Kirim Pendaftaran</button>
</div>
</div>
<!-- P-Step 4: Sukses -->
<div id="p-s4" style="display:none">
<div class="success-wrap">
<div class="success-icon">✅</div>
<div class="success-title">Akun Berhasil Dibuat!</div>
<div class="success-sub">Akun pengurus dan rumah ibadah telah berhasil dibuat.<br>Silakan login untuk mulai menggunakan layanan.</div>
<div class="success-warn">
🏠 <b style="color:var(--accent)">Akun Anda sudah aktif</b><br>
Username dan kata sandi yang Anda daftarkan sudah bisa digunakan untuk login.<br>
Jika ingin mengelola radius atau data rumah ibadah, masuk ke dashboard pengurus.
</div>
<a href="login.php" class="btn btn-purple">🚀 Masuk Sekarang</a>
</div>
</div>
</div>
<div class="form-foot" id="p-foot">Sudah punya akun? <a href="login.php">Masuk di sini</a></div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ── State ────────────────────────────────────────────────────
const ST = {
m: { unameOk:false, pwOk:false, confirmOk:false },
p: { unameOk:false, pwOk:false, confirmOk:false }
};
let locMap = null, locMarker = null, locCircle = null;
let selectedLat = null, selectedLng = null;
// ── Role switch ──────────────────────────────────────────────
function switchRole(role) {
document.getElementById('form-masyarakat').style.display = role==='masyarakat'?'':'none';
document.getElementById('form-pengurus').style.display = role==='pengurus'?'':'none';
document.getElementById('tab-masy').className = 'rtab' + (role==='masyarakat'?' active':'');
document.getElementById('tab-peng').className = 'rtab pengurus-tab' + (role==='pengurus'?' active':'');
if (role==='pengurus' && !locMap) setTimeout(initMap, 100);
}
// ── Shared helpers ───────────────────────────────────────────
function togPw(id){const i=document.getElementById(id);i.type=i.type==='password'?'text':'password';}
function showAlert(prefix, msg, type='err'){
const el=document.getElementById(prefix+'-alert');
el.className='alert-box show alert-'+type;
el.innerHTML=(type==='err'?'❌ ':'✅ ')+msg;
setTimeout(()=>el.classList.remove('show'),5000);
}
function checkUname(prefix){
const val=document.getElementById(prefix+'-username').value.trim();
const hint=document.getElementById(prefix+'-uname-hint');
if(!val){hint.textContent='';ST[prefix].unameOk=false;return;}
if(val.length<4){hint.style.color='#fca5a5';hint.textContent='❌ Minimal 4 karakter';ST[prefix].unameOk=false;return;}
if(/\s/.test(val)){hint.style.color='#fca5a5';hint.textContent='❌ Tidak boleh ada spasi';ST[prefix].unameOk=false;return;}
if(!/^[a-zA-Z0-9_]+$/.test(val)){hint.style.color='#fca5a5';hint.textContent='❌ Hanya huruf, angka, dan _';ST[prefix].unameOk=false;return;}
ST[prefix].unameOk=true;hint.style.color='#6ee7b7';hint.textContent='✅ Username valid';
}
function checkPw(prefix){
const val=document.getElementById(prefix+'-password').value;
const fill=document.getElementById(prefix+'-pw-str');
const hint=document.getElementById(prefix+'-pw-hint');
if(!val){fill.style.width='0';hint.textContent='';ST[prefix].pwOk=false;return;}
let s=0;
if(val.length>=6)s++;if(val.length>=10)s++;if(/[A-Z]/.test(val))s++;
if(/[0-9]/.test(val))s++;if(/[^A-Za-z0-9]/.test(val))s++;
const C=['','#ef4444','#f97316','#f59e0b','#10b981','#3b82f6'];
const L=['','Sangat Lemah','Lemah','Cukup','Kuat','Sangat Kuat'];
fill.style.width=(s*20)+'%';fill.style.background=C[s]||'#ef4444';
hint.style.color=C[s]||'#ef4444';hint.textContent='Kekuatan: '+(L[s]||'Sangat Lemah');
ST[prefix].pwOk=s>=2&&val.length>=6;
const cv=document.getElementById(prefix+'-confirm').value;if(cv)checkConfirm(prefix);
}
function checkConfirm(prefix){
const val=document.getElementById(prefix+'-confirm').value;
const hint=document.getElementById(prefix+'-confirm-hint');
const pw=document.getElementById(prefix+'-password').value;
if(!val){hint.textContent='';ST[prefix].confirmOk=false;return;}
if(val===pw){hint.style.color='#6ee7b7';hint.textContent='✅ Kata sandi cocok';ST[prefix].confirmOk=true;}
else{hint.style.color='#fca5a5';hint.textContent='❌ Kata sandi tidak cocok';ST[prefix].confirmOk=false;}
}
// ── RADIUS SLIDER DISPLAY ────────────────────────────────────
function updateRadiusDisplay(prefix, val){
document.getElementById(prefix+'-radius-val').textContent = val + ' m';
// Update slider gradient
const min=50, max=2000;
const pct = ((val-min)/(max-min)*100).toFixed(1);
document.getElementById(prefix+'-radius-range').style.setProperty('--pct', pct+'%');
// Update lingkaran di peta jika sudah ada
if(prefix==='p' && locCircle){
locCircle.setRadius(parseInt(val));
}
}
// ── MASYARAKAT STEPS ─────────────────────────────────────────
function mNext(){
if(!ST.m.unameOk){showAlert('m','Username tidak valid — periksa kembali');return;}
if(!ST.m.pwOk){showAlert('m','Kata sandi terlalu lemah (minimal 6 karakter)');return;}
if(!ST.m.confirmOk){showAlert('m','Konfirmasi kata sandi tidak cocok');return;}
show('m-s1','m-s2');
setStep('ms','ms1','done');setStep('ms','ms2','active');
}
function mBack(){show('m-s2','m-s1');setStep('ms','ms1','active');setStep('ms','ms2','');}
// ── PENGURUS STEPS ───────────────────────────────────────────
function pNext1(){
const nama=document.getElementById('p-nama').value.trim();
const hp=document.getElementById('p-hp').value.trim();
if(!nama){showAlert('p','Nama lengkap wajib diisi');return;}
if(!hp){showAlert('p','Nomor HP wajib diisi');return;}
if(!ST.p.unameOk){showAlert('p','Username tidak valid');return;}
if(!ST.p.pwOk){showAlert('p','Kata sandi terlalu lemah');return;}
if(!ST.p.confirmOk){showAlert('p','Konfirmasi kata sandi tidak cocok');return;}
show('p-s1','p-s2');
setStep('ps','ps1','done');setStep('ps','ps2','active p-step');
}
function pBack1(){show('p-s2','p-s1');setStep('ps','ps1','active p-step');setStep('ps','ps2','');}
function pNext2(){
const ni=document.getElementById('p-nama-ibadah').value.trim();
if(!ni){showAlert('p','Nama rumah ibadah wajib diisi');return;}
show('p-s2','p-s3');
setStep('ps','ps2','done');setStep('ps','ps3','active p-step');
if(!locMap)initMap();else locMap.invalidateSize();
}
function pBack2(){show('p-s3','p-s2');setStep('ps','ps2','active p-step');setStep('ps','ps3','');}
function show(hide,show){document.getElementById(hide).style.display='none';document.getElementById(show).style.display='block';}
function setStep(bar,id,cls){
document.getElementById(id).className='step '+(cls||'');
}
// ── PETA REVERSE GEOCODING ───────────────────────────────────
function initMap(){
if(locMap)return;
locMap = L.map('loc-map',{zoomControl:true}).setView([-0.0236,109.3282],13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
attribution:'© OpenStreetMap',maxZoom:19
}).addTo(locMap);
locMap.on('click', e => placeMarker(e.latlng.lat, e.latlng.lng));
}
function placeMarker(lat,lng){
selectedLat=lat; selectedLng=lng;
const rad=parseInt(document.getElementById('p-radius-range').value)||300;
if(locMarker) locMarker.setLatLng([lat,lng]);
else locMarker=L.marker([lat,lng],{draggable:true}).addTo(locMap)
.on('dragend',e=>{const p=e.target.getLatLng();placeMarker(p.lat,p.lng);});
if(locCircle) locCircle.setLatLng([lat,lng]).setRadius(rad);
else locCircle=L.circle([lat,lng],{radius:rad,color:'#8b5cf6',fillColor:'#8b5cf6',fillOpacity:0.08,weight:1.5,dashArray:'5,5'}).addTo(locMap);
document.getElementById('p-lat').value=lat.toFixed(7);
document.getElementById('p-lng').value=lng.toFixed(7);
const info=document.getElementById('map-info');
info.className='map-info';
info.textContent='⏳ Mengambil informasi alamat...';
reverseGeocode(lat,lng);
}
async function reverseGeocode(lat,lng){
try{
const url=`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1&accept-language=id`;
const r=await fetch(url,{headers:{'Accept-Language':'id'}});
const d=await r.json();
const a=d.address||{};
// Susun alamat
const road = a.road||a.pedestrian||a.footway||'';
const hn = a.house_number||'';
const alamat = [road+(hn?' '+hn:''), a.suburb||a.neighbourhood||''].filter(Boolean).join(', ');
const kel = a.village||a.suburb||a.neighbourhood||a.quarter||'';
const kec = a.city_district||a.municipality||a.town||'';
document.getElementById('p-alamat').value=alamat||d.display_name?.split(',').slice(0,3).join(',').trim()||'';
document.getElementById('p-kel').value=kel;
document.getElementById('p-kec').value=kec;
const info=document.getElementById('map-info');
info.className='map-info has-loc';
info.innerHTML=`✅ <span><b>${d.display_name?.split(',').slice(0,3).join(', ')}</b><br><small>Klik/geser marker untuk ubah lokasi</small></span>`;
}catch(e){
document.getElementById('map-info').textContent='⚠️ Gagal mengambil alamat. Isi manual di bawah.';
}
}
function getGPS(){
if(!navigator.geolocation){showAlert('p','Browser tidak mendukung GPS');return;}
const btn=document.getElementById('btn-gps');
btn.textContent='📡 Mengambil lokasi...';btn.disabled=true;
navigator.geolocation.getCurrentPosition(pos=>{
const {latitude:lat,longitude:lng}=pos.coords;
if(locMap)locMap.setView([lat,lng],17);
placeMarker(lat,lng);
btn.textContent='📡 Gunakan lokasi GPS saya sebagai titik awal';btn.disabled=false;
},err=>{
showAlert('p','GPS tidak tersedia: '+err.message);
btn.textContent='📡 Gunakan lokasi GPS saya sebagai titik awal';btn.disabled=false;
},{enableHighAccuracy:true,timeout:10000});
}
// ── REGISTER MASYARAKAT ──────────────────────────────────────
async function doRegisterMasy(){
const nama=document.getElementById('m-nama').value.trim();
const hp =document.getElementById('m-hp').value.trim();
if(!nama){showAlert('m','Nama lengkap wajib diisi');return;}
if(!hp) {showAlert('m','Nomor HP wajib diisi');return;}
const btn=document.getElementById('m-btn-reg');
btn.disabled=true;btn.innerHTML='<div class="spinner"></div> Memproses...';
const payload={
username: document.getElementById('m-username').value.trim(),
password: document.getElementById('m-password').value,
nama_lengkap: nama,
no_hp: hp,
email: document.getElementById('m-email').value.trim(),
no_kk: document.getElementById('m-nokk').value.trim()
};
try{
const res=await fetch('api/auth.php?action=register',{
method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)
});
const ct=res.headers.get('content-type')||'';
if(!ct.includes('application/json')){throw new Error('Server mengembalikan respon tidak valid.');}
const data=await res.json();
if(data.success){
document.getElementById('m-s2').style.display='none';
document.getElementById('m-s3').style.display='block';
document.getElementById('m-foot').style.display='none';
setStep('ms','ms2','done');setStep('ms','ms3','active');
}else{
showAlert('m',data.error||'Gagal membuat akun. Coba lagi.');
btn.disabled=false;btn.innerHTML='✅ Buat Akun';
}
}catch(err){
showAlert('m','Terjadi kesalahan: '+err.message);
btn.disabled=false;btn.innerHTML='✅ Buat Akun';
}
}
// ── REGISTER PENGURUS ────────────────────────────────────────
async function doRegisterPengurus(){
if(!selectedLat||!selectedLng){showAlert('p','Klik peta untuk menentukan lokasi rumah ibadah');return;}
const btn=document.getElementById('p-btn-reg');
btn.disabled=true;btn.innerHTML='<div class="spinner"></div> Mengirim...';
const payload={
nama_lengkap: document.getElementById('p-nama').value.trim(),
username: document.getElementById('p-username').value.trim(),
no_hp: document.getElementById('p-hp').value.trim(),
email: document.getElementById('p-email').value.trim(),
password: document.getElementById('p-password').value,
nama_ibadah: document.getElementById('p-nama-ibadah').value.trim(),
jenis_ibadah: document.getElementById('p-jenis').value,
latitude: selectedLat,
longitude: selectedLng,
alamat: document.getElementById('p-alamat').value.trim(),
kelurahan: document.getElementById('p-kel').value.trim(),
kecamatan: document.getElementById('p-kec').value.trim(),
radius: parseInt(document.getElementById('p-radius-range').value)||300
};
try{
const res=await fetch('api/auth.php?action=register_pengurus',{
method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)
});
const ct=res.headers.get('content-type')||'';
if(!ct.includes('application/json')){throw new Error('Server mengembalikan respon tidak valid.');}
const data=await res.json();
if(data.success){
document.getElementById('p-s3').style.display='none';
document.getElementById('p-s4').style.display='block';
document.getElementById('p-foot').style.display='none';
setStep('ps','ps3','done');setStep('ps','ps4','active p-step');
}else{
showAlert('p',data.error||'Gagal mendaftar. Coba lagi.');
btn.disabled=false;btn.innerHTML='📨 Kirim Pendaftaran';
}
}catch(err){
showAlert('p','Terjadi kesalahan: '+err.message);
btn.disabled=false;btn.innerHTML='📨 Kirim Pendaftaran';
}
}
// Inisialisasi radius display
updateRadiusDisplay('p', document.getElementById('p-radius-range').value);
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+876
View File
@@ -0,0 +1,876 @@
<?php
require_once '../includes/config.php';
requireLogin(['pemerintah','admin']);
$user = currentUser();
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Pemerintah — WebGIS Pontianak</title>
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
:root{
--bg:#05080e;--surface:#0a0f1a;--card:#0f1625;--card2:#131d2e;
--border:rgba(99,179,237,0.09);--accent:#3b82f6;--accent2:#06b6d4;
--green:#10b981;--red:#ef4444;--amber:#f59e0b;--purple:#8b5cf6;--pink:#ec4899;
--text:#dde6f0;--text2:#607694;--glow:rgba(59,130,246,0.15);
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Sora',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
/* TOP BAR */
.topbar{
background:var(--surface);
border-bottom:1px solid var(--border);
padding:0 24px;height:60px;
display:flex;align-items:center;justify-content:space-between;
position:sticky;top:0;z-index:200;
box-shadow:0 2px 24px rgba(0,0,0,0.5);
}
.brand{display:flex;align-items:center;gap:12px}
.brand-icon{
width:38px;height:38px;
background:linear-gradient(135deg,#1d4ed8,#0369a1);
border-radius:11px;display:flex;align-items:center;justify-content:center;font-size:20px;
box-shadow:0 4px 16px rgba(29,78,216,0.3);
}
.brand-name{font-size:15px;font-weight:800;letter-spacing:-0.3px}
.brand-sub{font-size:10px;color:var(--text2);letter-spacing:0.3px}
.ro-badge{
padding:4px 10px;
background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.25);
color:#fca5a5;font-size:10px;font-weight:800;letter-spacing:0.5px;
border-radius:6px;
}
.topbar-nav{display:flex;gap:2px}
.nt{padding:6px 14px;border-radius:8px;border:none;background:transparent;color:var(--text2);font-family:'Sora',sans-serif;font-size:11px;font-weight:700;cursor:pointer;transition:all 0.2s;letter-spacing:0.3px}
.nt.active{background:rgba(59,130,246,0.12);color:var(--accent)}
.nt:hover:not(.active){background:rgba(255,255,255,0.04);color:var(--text)}
.topbar-right{display:flex;align-items:center;gap:10px}
.uc{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,0.04);border:1px solid var(--border);padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600}
.logout{background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.2);color:var(--red);padding:5px 12px;border-radius:8px;font-family:'Sora',sans-serif;font-size:11px;font-weight:700;cursor:pointer;transition:all 0.2s}
.logout:hover{background:rgba(239,68,68,0.2)}
/* PAGES */
.page{display:none;flex:1;height:calc(100vh - 60px)}
.page.active{display:flex}
/* === OVERVIEW === */
.overview{width:100%;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:20px}
/* KPI ROW */
.kpi-row{display:grid;grid-template-columns:repeat(6,1fr);gap:12px}
.kpi{
background:var(--card2);border:1px solid var(--border);
border-radius:14px;padding:16px 14px;
position:relative;overflow:hidden;
transition:border-color 0.2s,transform 0.2s;
}
.kpi:hover{border-color:var(--accent);transform:translateY(-2px)}
.kpi::before{
content:'';position:absolute;top:0;left:0;right:0;height:2px;
background:var(--kpi-color,var(--accent));
}
.kpi-icon{font-size:22px;margin-bottom:8px}
.kpi-num{font-size:26px;font-weight:800;line-height:1;margin-bottom:4px;color:var(--kpi-color,var(--text))}
.kpi-label{font-size:9px;font-weight:800;color:var(--text2);text-transform:uppercase;letter-spacing:0.6px}
.kpi-sub{font-size:10px;color:var(--text2);margin-top:3px}
/* CHARTS GRID */
.charts-row{display:grid;grid-template-columns:2fr 1fr;gap:16px}
.charts-row-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px}
.chart-card{
background:var(--card2);border:1px solid var(--border);
border-radius:16px;padding:20px;
}
.cc-title{font-size:13px;font-weight:800;margin-bottom:4px}
.cc-sub{font-size:11px;color:var(--text2);margin-bottom:16px}
.chart-wrap{position:relative}
/* KECAMATAN TABLE */
.kec-table{width:100%;border-collapse:collapse}
.kec-table th{padding:8px 12px;text-align:left;font-size:9px;font-weight:800;color:var(--text2);letter-spacing:0.5px;border-bottom:1px solid var(--border);background:rgba(255,255,255,0.02)}
.kec-table td{padding:10px 12px;font-size:11px;border-bottom:1px solid rgba(255,255,255,0.04)}
.kec-table tr:hover td{background:rgba(59,130,246,0.04)}
.bar-cell{display:flex;align-items:center;gap:8px}
.bar{height:6px;border-radius:3px;background:linear-gradient(90deg,var(--accent),var(--accent2));min-width:4px;transition:width 0.5s}
/* BANTUAN STREAM */
.stream{display:flex;flex-direction:column;gap:8px;max-height:320px;overflow-y:auto}
.stream-item{
background:rgba(16,185,129,0.05);border:1px solid rgba(16,185,129,0.12);
border-radius:10px;padding:10px 12px;
display:flex;align-items:flex-start;gap:10px;
}
.si-dot{width:8px;height:8px;border-radius:50%;background:var(--green);margin-top:3px;flex-shrink:0}
.si-body{flex:1}
.si-title{font-size:11px;font-weight:700;margin-bottom:2px}
.si-meta{font-size:10px;color:var(--text2)}
/* BADGE */
.badge{padding:2px 8px;border-radius:5px;font-size:9px;font-weight:800;letter-spacing:0.3px}
.bb{background:rgba(59,130,246,.15);color:#93c5fd}
.bg{background:rgba(16,185,129,.15);color:#6ee7b7}
.ba{background:rgba(245,158,11,.15);color:#fcd34d}
.br{background:rgba(239,68,68,.15);color:#fca5a5}
.bp{background:rgba(139,92,246,.15);color:#c4b5fd}
.bgr{background:rgba(255,255,255,.07);color:var(--text2)}
/* === MAP PAGE === */
.map-layout{display:flex;width:100%}
.map-panel{
width:320px;min-width:320px;
background:var(--surface);border-right:1px solid var(--border);
display:flex;flex-direction:column;overflow-y:auto;
}
.map-main{flex:1;position:relative}
#map{width:100%;height:100%}
.map-filter{padding:14px;border-bottom:1px solid var(--border)}
.map-filter label{font-size:10px;font-weight:800;color:var(--text2);display:block;margin-bottom:5px;letter-spacing:0.5px}
.map-filter select{width:100%;background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:9px;font-family:'Sora',sans-serif;font-size:11px;outline:none;margin-bottom:8px}
/* === LAPORAN PAGE === */
.laporan-overview{width:100%;overflow-y:auto;padding:24px}
/* === PENDUDUK PAGE === */
.penduduk-page{width:100%;overflow-y:auto;padding:24px}
.ptable-wrap{background:var(--card2);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:16px}
.ptable{width:100%;border-collapse:collapse}
.ptable th{padding:10px 14px;text-align:left;font-size:9px;font-weight:800;color:var(--text2);letter-spacing:0.5px;border-bottom:1px solid var(--border);background:rgba(255,255,255,0.02)}
.ptable td{padding:10px 14px;font-size:11px;border-bottom:1px solid rgba(255,255,255,0.04);color:var(--text2)}
.ptable td:first-child{font-weight:700;color:var(--text)}
.ptable tr:hover td{background:rgba(59,130,246,0.03)}
/* FILTER ROW */
.filter-row{display:flex;gap:10px;flex-wrap:wrap}
.fi{background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:9px;font-family:'Sora',sans-serif;font-size:11px;outline:none}
.fi:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--glow)}
.search-fi{flex:1;min-width:200px}
/* PULSE DOT */
.pulse{
display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--green);
box-shadow:0 0 0 0 rgba(16,185,129,0.4);
animation:pulse 2s infinite;
}
@keyframes pulse{
0%{box-shadow:0 0 0 0 rgba(16,185,129,0.4)}
70%{box-shadow:0 0 0 8px rgba(16,185,129,0)}
100%{box-shadow:0 0 0 0 rgba(16,185,129,0)}
}
/* SCROLL */
::-webkit-scrollbar{width:4px;height:4px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(99,179,237,0.2);border-radius:4px}
@media(max-width:1100px){
.kpi-row{grid-template-columns:repeat(3,1fr)}
.charts-row{grid-template-columns:1fr}
.charts-row-3{grid-template-columns:1fr 1fr}
}
@keyframes mi{from{opacity:0;transform:scale(.96)}}
@media(max-width:700px){
.kpi-row{grid-template-columns:repeat(2,1fr)}
.topbar-nav{display:none}
}
</style>
</head>
<body>
<!-- TOP BAR -->
<div class="topbar">
<div class="brand">
<div class="brand-icon">🏛️</div>
<div>
<div class="brand-name">Dashboard Pemerintah</div>
<div class="brand-sub">KOTA PONTIANAK · SISTEM PEMANTAUAN KEMISKINAN</div>
</div>
<div class="ro-badge">READ ONLY</div>
</div>
<div class="topbar-nav">
<button class="nt active" onclick="showPage('overview',this)">📊 Overview</button>
<button class="nt" onclick="showPage('peta',this)">🗺️ Peta GIS</button>
<button class="nt" onclick="showPage('penduduk',this)">👥 Penduduk</button>
<button class="nt" onclick="showPage('bantuan',this)">🤝 Bantuan</button>
<button class="nt" onclick="showPage('laporan',this)">📢 Laporan</button>
</div>
<div class="topbar-right">
<div style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text2)">
<div class="pulse"></div> Live Update
</div>
<div id="clock" style="font-size:11px;color:var(--text2);font-weight:700;background:rgba(255,255,255,0.04);padding:5px 12px;border-radius:8px;border:1px solid var(--border)"></div>
<div class="uc">🏛️ <?= htmlspecialchars($user['nama']) ?></div>
<button class="logout" onclick="location.href='../api/auth.php?action=logout'">Keluar</button>
</div>
</div>
<!-- ======== PAGE: OVERVIEW ======== -->
<div class="page active" id="page-overview">
<div class="overview">
<!-- KPI ROW -->
<div class="kpi-row">
<div class="kpi" style="--kpi-color:var(--accent)">
<div class="kpi-icon">🏘️</div>
<div class="kpi-num" id="k-kk">—</div>
<div class="kpi-label">Total KK Terdaftar</div>
<div class="kpi-sub" id="k-kk-sub">memuat...</div>
</div>
<div class="kpi" style="--kpi-color:var(--accent2)">
<div class="kpi-icon">👥</div>
<div class="kpi-num" id="k-jiwa">—</div>
<div class="kpi-label">Total Jiwa</div>
<div class="kpi-sub">dalam database</div>
</div>
<div class="kpi" style="--kpi-color:var(--red)">
<div class="kpi-icon">⚠️</div>
<div class="kpi-num" id="k-sm">—</div>
<div class="kpi-label">Sangat Miskin</div>
<div class="kpi-sub" id="k-sm-pct">memuat...</div>
</div>
<div class="kpi" style="--kpi-color:var(--amber)">
<div class="kpi-icon">🟠</div>
<div class="kpi-num" id="k-miskin">—</div>
<div class="kpi-label">Miskin</div>
<div class="kpi-sub" id="k-miskin-pct">—</div>
</div>
<div class="kpi" style="--kpi-color:var(--green)">
<div class="kpi-icon">🤝</div>
<div class="kpi-num" id="k-bantuan">—</div>
<div class="kpi-label">Total Bantuan</div>
<div class="kpi-sub" id="k-bantuan-sub">memuat...</div>
</div>
<div class="kpi" style="--kpi-color:var(--purple)">
<div class="kpi-icon">🕌</div>
<div class="kpi-num" id="k-ibadah">—</div>
<div class="kpi-label">Rumah Ibadah Aktif</div>
<div class="kpi-sub">berkontribusi</div>
</div>
</div>
<!-- CHART ROW 1 -->
<div class="charts-row">
<div class="chart-card">
<div class="cc-title">📊 Distribusi Status Ekonomi per Kecamatan</div>
<div class="cc-sub">Jumlah KK berdasarkan tingkat kemiskinan di tiap kecamatan</div>
<div class="chart-wrap"><canvas id="chart-kecamatan" height="160"></canvas></div>
</div>
<div class="chart-card">
<div class="cc-title">🥧 Komposisi Status Ekonomi</div>
<div class="cc-sub">Persentase seluruh KK terdaftar</div>
<div class="chart-wrap"><canvas id="chart-donut" height="180"></canvas></div>
</div>
</div>
<!-- CHART ROW 2 -->
<div class="charts-row-3">
<div class="chart-card">
<div class="cc-title">🤝 Bantuan per Jenis</div>
<div class="cc-sub">Distribusi berdasarkan kategori</div>
<canvas id="chart-bantuan-jenis" height="180"></canvas>
</div>
<div class="chart-card">
<div class="cc-title">🕌 Kontribusi Rumah Ibadah</div>
<div class="cc-sub">Jumlah bantuan per rumah ibadah</div>
<canvas id="chart-ibadah-kontrib" height="180"></canvas>
</div>
<div class="chart-card">
<div class="cc-title">📢 Status Laporan Masyarakat</div>
<div class="cc-sub">Laporan masuk dan penyelesaian</div>
<canvas id="chart-laporan" height="180"></canvas>
</div>
</div>
<!-- BOTTOM ROW -->
<div class="charts-row">
<!-- Per Kecamatan Table -->
<div class="chart-card">
<div class="cc-title">📍 Rekap per Kecamatan</div>
<div class="cc-sub">Perbandingan data antar kecamatan</div>
<div style="overflow-x:auto">
<table class="kec-table" id="kec-table">
<thead><tr>
<th>KECAMATAN</th><th>KK</th><th>JIWA</th>
<th>SANGAT MISKIN</th><th>MISKIN</th><th>RENTAN</th><th>BANTUAN</th>
</tr></thead>
<tbody id="kec-tbody"></tbody>
</table>
</div>
</div>
<!-- Live stream bantuan -->
<div class="chart-card">
<div class="cc-title">⚡ Bantuan Terbaru</div>
<div class="cc-sub">Real-time penyaluran bantuan</div>
<div class="stream" id="live-stream">
<div style="text-align:center;padding:20px;color:var(--text2);font-size:12px">⏳ Memuat...</div>
</div>
</div>
</div>
</div>
</div>
<!-- ======== PAGE: PETA ======== -->
<div class="page" id="page-peta">
<div class="map-panel">
<div class="map-filter">
<label>STATUS EKONOMI</label>
<select id="mp-ekonomi" onchange="loadPetaPenduduk()">
<option value="all">Semua</option>
<option value="sangat_miskin">🔴 Sangat Miskin</option>
<option value="miskin">🟠 Miskin</option>
<option value="rentan">🟡 Rentan</option>
</select>
<label>KECAMATAN</label>
<select id="mp-kecamatan" onchange="loadPetaPenduduk()">
<option value="">Semua Kecamatan</option>
</select>
<label>TAMPILAN</label>
<div style="display:flex;gap:6px">
<button onclick="toggleML('ibadah')" style="flex:1;padding:6px;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text2);border-radius:8px;cursor:pointer;font-family:'Sora',sans-serif;font-size:10px;font-weight:700">🕌 Ibadah</button>
<button onclick="toggleML('radius')" style="flex:1;padding:6px;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text2);border-radius:8px;cursor:pointer;font-family:'Sora',sans-serif;font-size:10px;font-weight:700">⭕ Radius</button>
<button onclick="map.setView([-0.0263,109.3425],12)" style="flex:1;padding:6px;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text2);border-radius:8px;cursor:pointer;font-family:'Sora',sans-serif;font-size:10px;font-weight:700">🔄 Reset</button>
</div>
</div>
<div style="padding:12px 14px;border-bottom:1px solid var(--border)">
<div style="font-size:10px;font-weight:800;color:var(--text2);margin-bottom:8px;letter-spacing:0.5px">📍 LEGENDA</div>
<div style="font-size:10px;color:var(--text2);line-height:2">
<div>⚠️ Sangat Miskin &nbsp; 🟠 Miskin &nbsp; 🟡 Rentan</div>
<div>🕌 Masjid &nbsp; ⛪ Gereja &nbsp; 🏯 Vihara &nbsp; 🛕 Pura</div>
<div>⭕ Radius layanan binaan</div>
</div>
</div>
<div style="padding:12px 14px;overflow-y:auto;flex:1">
<div style="font-size:10px;font-weight:800;color:var(--text2);margin-bottom:8px;letter-spacing:0.5px">🕌 RUMAH IBADAH</div>
<div id="map-ibadah-list" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
</div>
<div class="map-main"><div id="map"></div></div>
</div>
<!-- ======== PAGE: PENDUDUK ======== -->
<div class="page" id="page-penduduk">
<div class="penduduk-page">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<div>
<div style="font-size:18px;font-weight:800">👥 Data Penduduk</div>
<div style="font-size:12px;color:var(--text2);margin-top:3px">Semua KK terdaftar dalam sistem (read-only)</div>
</div>
<button onclick="exportCSV()" style="padding:8px 16px;background:linear-gradient(135deg,var(--green),#059669);border:none;color:#fff;border-radius:9px;font-family:'Sora',sans-serif;font-size:11px;font-weight:700;cursor:pointer">📥 Export CSV</button>
</div>
<div class="filter-row" style="margin-bottom:12px">
<input type="text" id="q-penduduk" class="fi search-fi" placeholder="🔍 Cari nama / alamat / kelurahan..." oninput="filterTable()">
<select id="q-ekonomi" class="fi" onchange="loadPendudukFull()">
<option value="">Semua Ekonomi</option>
<option value="sangat_miskin">Sangat Miskin</option>
<option value="miskin">Miskin</option>
<option value="rentan">Rentan</option>
<option value="mampu">Mampu</option>
</select>
<select id="q-kecamatan" class="fi" onchange="loadPendudukFull()">
<option value="">Semua Kecamatan</option>
</select>
</div>
<div class="ptable-wrap">
<table class="ptable">
<thead><tr>
<th>#</th><th>KEPALA KELUARGA</th><th>NO. KK</th>
<th>ALAMAT</th><th>KECAMATAN</th>
<th>JIWA</th><th>STATUS EKONOMI</th>
<th>RUMAH IBADAH</th><th>BANTUAN</th><th>AKSI</th>
</tr></thead>
<tbody id="p-tbody"></tbody>
</table>
</div>
<div id="p-paging" style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;font-size:11px;color:var(--text2)"></div>
</div>
</div>
<!-- ======== PAGE: BANTUAN ======== -->
<div class="page" id="page-bantuan">
<div class="overview">
<div style="font-size:18px;font-weight:800;margin-bottom:4px">🤝 Penyaluran Bantuan</div>
<div style="font-size:12px;color:var(--text2);margin-bottom:16px">Transparansi bantuan yang disalurkan oleh rumah ibadah dan pemerintah</div>
<!-- Stats -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px">
<div class="kpi" style="--kpi-color:var(--green)"><div class="kpi-icon">🤝</div><div class="kpi-num" id="bt-total">—</div><div class="kpi-label">Total Penyaluran</div></div>
<div class="kpi" style="--kpi-color:var(--accent)"><div class="kpi-icon">💰</div><div class="kpi-num" id="bt-nominal">—</div><div class="kpi-label">Total Nominal (Rp)</div></div>
<div class="kpi" style="--kpi-color:var(--amber)"><div class="kpi-icon">🏛️</div><div class="kpi-num" id="bt-pem">—</div><div class="kpi-label">Dari Pemerintah</div></div>
<div class="kpi" style="--kpi-color:var(--purple)"><div class="kpi-icon">🕌</div><div class="kpi-num" id="bt-ibadah">—</div><div class="kpi-label">Dari Rumah Ibadah</div></div>
</div>
<!-- Table -->
<div style="background:var(--card2);border:1px solid var(--border);border-radius:14px;overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">
<div style="font-size:13px;font-weight:800">📋 Histori Lengkap Bantuan</div>
<div style="display:flex;gap:8px">
<select id="bt-filter-sumber" onchange="loadBantuanFull()" class="fi" style="font-size:10px">
<option value="">Semua Sumber</option>
<option value="pemerintah">Pemerintah</option>
<option value="rumah_ibadah">Rumah Ibadah</option>
<option value="donatur">Donatur</option>
</select>
</div>
</div>
<table class="ptable">
<thead><tr>
<th>PENERIMA</th><th>JENIS BANTUAN</th><th>SUMBER</th>
<th>RUMAH IBADAH</th><th>MITRA</th><th>TANGGAL</th>
<th>BENTUK</th><th>NOMINAL</th><th>STATUS</th>
</tr></thead>
<tbody id="bt-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- ======== PAGE: LAPORAN ======== -->
<div class="page" id="page-laporan">
<div class="overview">
<div style="font-size:18px;font-weight:800;margin-bottom:4px">📢 Laporan Masyarakat</div>
<div style="font-size:12px;color:var(--text2);margin-bottom:16px">Semua laporan yang dikirim masyarakat beserta status penanganannya</div>
<!-- Stats laporan -->
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:20px">
<div class="kpi" style="--kpi-color:var(--text2)"><div class="kpi-icon">📢</div><div class="kpi-num" id="lp-total">—</div><div class="kpi-label">Total Laporan</div></div>
<div class="kpi" style="--kpi-color:var(--amber)"><div class="kpi-icon">⏳</div><div class="kpi-num" id="lp-pending">—</div><div class="kpi-label">Pending</div></div>
<div class="kpi" style="--kpi-color:var(--accent)"><div class="kpi-icon">🔄</div><div class="kpi-num" id="lp-proses">—</div><div class="kpi-label">Diproses</div></div>
<div class="kpi" style="--kpi-color:var(--green)"><div class="kpi-icon">✅</div><div class="kpi-num" id="lp-setuju">—</div><div class="kpi-label">Disetujui</div></div>
<div class="kpi" style="--kpi-color:var(--red)"><div class="kpi-icon">❌</div><div class="kpi-num" id="lp-tolak">—</div><div class="kpi-label">Ditolak</div></div>
</div>
<div style="background:var(--card2);border:1px solid var(--border);border-radius:14px;overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;gap:8px">
<select id="lp-filter-status" onchange="loadLaporanFull()" class="fi" style="font-size:10px">
<option value="">Semua Status</option>
<option value="pending">Pending</option>
<option value="diproses">Diproses</option>
<option value="disetujui">Disetujui</option>
<option value="ditolak">Ditolak</option>
</select>
<select id="lp-filter-jenis" onchange="loadLaporanFull()" class="fi" style="font-size:10px">
<option value="">Semua Jenis</option>
<option value="kelahiran">Kelahiran</option>
<option value="kematian">Kematian</option>
<option value="pindah_masuk">Pindah Masuk</option>
<option value="pindah_keluar">Pindah Keluar</option>
</select>
</div>
<table class="ptable">
<thead><tr>
<th>JENIS</th><th>PELAPOR</th><th>TERKAIT</th>
<th>RUMAH IBADAH</th><th>TGL KEJADIAN</th>
<th>STATUS</th><th>DESKRIPSI</th>
</tr></thead>
<tbody id="lp-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- Modal: Detail Keluarga (Anggota) -->
<div id="m-detail-kk-pem" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:500;align-items:center;justify-content:center;backdrop-filter:blur(4px)">
<div style="background:var(--card);border:1px solid var(--border);border-radius:18px;width:90%;max-width:680px;max-height:90vh;overflow-y:auto;box-shadow:0 24px 64px rgba(0,0,0,.6);animation:mi .3s cubic-bezier(.23,1,.32,1)">
<div style="padding:18px 22px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;background:var(--card);z-index:2">
<div style="font-size:15px;font-weight:800" id="pem-detail-title">Detail Keluarga</div>
<button onclick="document.getElementById('m-detail-kk-pem').style.display='none'" style="background:rgba(255,255,255,.04);border:none;color:var(--text2);width:28px;height:28px;border-radius:7px;cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center">✕</button>
</div>
<div style="padding:18px 22px" id="pem-detail-body">
<div style="text-align:center;padding:30px;color:var(--text2)">⏳ Memuat data...</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ── CLOCK ──
function updateClock(){
const now=new Date();
document.getElementById('clock').textContent=now.toLocaleString('id-ID',{weekday:'short',day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'});
}
updateClock();setInterval(updateClock,1000);
// ── PAGES ──
function showPage(name,el){
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
document.querySelectorAll('.nt').forEach(t=>t.classList.remove('active'));
document.getElementById('page-'+name).classList.add('active');
if(el)el.classList.add('active');
if(name==='peta'){loadMapData();initMap()}
if(name==='penduduk')loadPendudukFull();
if(name==='bantuan'){loadBantuanFull();loadBantuanStats()}
if(name==='laporan')loadLaporanFull();
}
// ── COLORS ──
const COLORS={
accent:'#3b82f6',accent2:'#06b6d4',green:'#10b981',
amber:'#f59e0b',red:'#ef4444',purple:'#8b5cf6',pink:'#ec4899'
};
const chartDefaults={
plugins:{legend:{labels:{color:'#7c93b5',font:{family:'Sora',size:10}}}},
scales:{
x:{ticks:{color:'#7c93b5',font:{family:'Sora',size:10}},grid:{color:'rgba(255,255,255,0.04)'}},
y:{ticks:{color:'#7c93b5',font:{family:'Sora',size:10}},grid:{color:'rgba(255,255,255,0.04)'}}
}
};
// ── LOAD OVERVIEW ──
let charts={};
async function loadOverview(){
const d=await fetch('../api/penduduk.php?action=dashboard').then(r=>r.json());
const bs=await fetch('../api/bantuan.php?action=statistik').then(r=>r.json());
const total=d.total_kk||0;
document.getElementById('k-kk').textContent=total.toLocaleString('id');
document.getElementById('k-kk-sub').textContent=`${d.total_jiwa||0} jiwa total`;
document.getElementById('k-jiwa').textContent=(d.total_jiwa||0).toLocaleString('id');
document.getElementById('k-sm').textContent=(d.sangat_miskin||0).toLocaleString('id');
document.getElementById('k-sm-pct').textContent=total?`${((d.sangat_miskin/total)*100).toFixed(1)}% dari total`:'';
document.getElementById('k-miskin').textContent=(d.miskin||0).toLocaleString('id');
document.getElementById('k-miskin-pct').textContent=total?`${((d.miskin/total)*100).toFixed(1)}% dari total`:'';
document.getElementById('k-bantuan').textContent=(bs.total_bantuan||0).toLocaleString('id');
document.getElementById('k-bantuan-sub').textContent=`Rp${((bs.total_nominal||0)/1e6).toFixed(1)}jt disalurkan`;
document.getElementById('k-ibadah').textContent=(d.total_ibadah||0);
// ── CHART: Per Kecamatan (stacked bar) ──
const kec=d.per_kecamatan||[];
if(charts.kecamatan)charts.kecamatan.destroy();
charts.kecamatan=new Chart(document.getElementById('chart-kecamatan'),{
type:'bar',
data:{
labels:kec.map(k=>k.kecamatan||'-'),
datasets:[
{label:'KK',data:kec.map(k=>k.jumlah_kk),backgroundColor:COLORS.accent+'cc',borderRadius:4},
{label:'Jiwa',data:kec.map(k=>k.jiwa),backgroundColor:COLORS.accent2+'99',borderRadius:4},
]
},
options:{...chartDefaults,responsive:true,plugins:{...chartDefaults.plugins,legend:{labels:{color:'#7c93b5',font:{family:'Sora',size:10}}}}}
});
// ── CHART: Donut status ekonomi ──
if(charts.donut)charts.donut.destroy();
charts.donut=new Chart(document.getElementById('chart-donut'),{
type:'doughnut',
data:{
labels:['Sangat Miskin','Miskin','Rentan'],
datasets:[{data:[d.sangat_miskin||0,d.miskin||0,d.rentan||0],backgroundColor:[COLORS.red+'dd',COLORS.amber+'dd',COLORS.purple+'dd'],borderWidth:0,hoverOffset:6}]
},
options:{responsive:true,cutout:'70%',plugins:{legend:{position:'bottom',labels:{color:'#7c93b5',font:{family:'Sora',size:10},padding:10}}}}
});
// ── CHART: Bantuan per jenis ──
const topJenis=(bs.per_jenis||[]).slice(0,6);
if(charts.bJenis)charts.bJenis.destroy();
charts.bJenis=new Chart(document.getElementById('chart-bantuan-jenis'),{
type:'bar',
data:{
labels:topJenis.map(j=>j.nama.substring(0,15)),
datasets:[{label:'Jumlah',data:topJenis.map(j=>j.jumlah),backgroundColor:COLORS.green+'cc',borderRadius:4}]
},
options:{...chartDefaults,indexAxis:'y',responsive:true,plugins:{legend:{display:false}}}
});
// ── CHART: Kontribusi ibadah ──
const topIbadah=(bs.per_ibadah||[]).slice(0,6);
if(charts.ibadah)charts.ibadah.destroy();
charts.ibadah=new Chart(document.getElementById('chart-ibadah-kontrib'),{
type:'bar',
data:{
labels:topIbadah.map(i=>i.nama.substring(0,18)),
datasets:[{label:'Bantuan',data:topIbadah.map(i=>i.jumlah),backgroundColor:COLORS.purple+'cc',borderRadius:4}]
},
options:{...chartDefaults,indexAxis:'y',responsive:true,plugins:{legend:{display:false}}}
});
// ── CHART: Laporan ──
const lp=await fetch('../api/laporan.php?action=list').then(r=>r.json()).catch(()=>[]);
const lpCount={pending:0,diproses:0,disetujui:0,ditolak:0};
lp.forEach(l=>{if(lpCount[l.status_verifikasi]!==undefined)lpCount[l.status_verifikasi]++});
if(charts.laporan)charts.laporan.destroy();
charts.laporan=new Chart(document.getElementById('chart-laporan'),{
type:'doughnut',
data:{
labels:['Pending','Diproses','Disetujui','Ditolak'],
datasets:[{data:Object.values(lpCount),backgroundColor:[COLORS.amber+'dd',COLORS.accent+'dd',COLORS.green+'dd',COLORS.red+'dd'],borderWidth:0}]
},
options:{responsive:true,cutout:'65%',plugins:{legend:{position:'bottom',labels:{color:'#7c93b5',font:{family:'Sora',size:10},padding:8}}}}
});
// ── KECAMATAN TABLE ──
const tbody=document.getElementById('kec-tbody');
const maxKK=Math.max(...kec.map(k=>k.jumlah_kk),1);
tbody.innerHTML=kec.map(k=>`<tr>
<td style="font-weight:700;color:var(--text)">${k.kecamatan}</td>
<td><div class="bar-cell"><div class="bar" style="width:${(k.jumlah_kk/maxKK*80)}px"></div>${k.jumlah_kk}</div></td>
<td>${k.jiwa}</td>
<td style="color:var(--red)">${k.sangat_miskin||0}</td>
<td style="color:var(--amber)">${k.miskin||0}</td>
<td style="color:var(--purple)">${k.rentan||0}</td>
<td style="color:var(--green)">${k.bantuan||0}</td>
</tr>`).join('');
// Populate kecamatan dropdowns
kec.forEach(k=>{
['mp-kecamatan','q-kecamatan'].forEach(id=>{
const sel=document.getElementById(id);
if(sel&&!Array.from(sel.options).find(o=>o.value===k.kecamatan))
sel.add(new Option(k.kecamatan,k.kecamatan));
});
});
// ── LIVE STREAM ──
loadLiveStream();
}
async function loadLiveStream(){
const data=await fetch('../api/bantuan.php?action=list').then(r=>r.json()).catch(()=>[]);
const el=document.getElementById('live-stream');
if(!data.length){el.innerHTML='<div style="text-align:center;padding:20px;color:var(--text2);font-size:11px">Belum ada bantuan</div>';return}
el.innerHTML=data.slice(0,10).map(b=>`
<div class="stream-item">
<div class="si-dot"></div>
<div class="si-body">
<div class="si-title">${b.nama_kepala_keluarga} · ${b.jenis_bantuan}</div>
<div class="si-meta">📦 ${b.bentuk_bantuan.substring(0,50)} · 🕌 ${b.nama_ibadah||b.sumber_bantuan} · ${fDate(b.tanggal_bantuan)}</div>
</div>
</div>`).join('');
}
// ── MAP ──
let mapInited=false;let mmap;
const MLG={ibadah:null,penduduk:null,radius:null};const MLV={ibadah:true,radius:true};
function initMap(){
if(mapInited)return;mapInited=true;
mmap=L.map('map',{center:[-0.0263,109.3425],zoom:12,zoomControl:false});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap',maxZoom:19}).addTo(mmap);
L.control.zoom({position:'bottomright'}).addTo(mmap);
MLG.ibadah=L.layerGroup().addTo(mmap);
MLG.penduduk=L.layerGroup().addTo(mmap);
MLG.radius=L.layerGroup().addTo(mmap);
loadMapData();
}
function toggleML(k){if(MLV[k]){mmap.removeLayer(MLG[k]);MLV[k]=false}else{mmap.addLayer(MLG[k]);MLV[k]=true}}
function mkIco(c,e){return L.divIcon({className:'',html:`<div style="width:28px;height:28px;background:${c};border-radius:50%;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,.2)">${e}</div>`,iconSize:[28,28],iconAnchor:[14,14],popupAnchor:[0,-16]})}
const MICO={masjid:mkIco('#3b82f6','🕌'),gereja:mkIco('#8b5cf6','⛪'),vihara:mkIco('#f59e0b','🏯'),pura:mkIco('#10b981','🛕'),lainnya:mkIco('#64748b','🏢'),sangat_miskin:mkIco('#ef4444','⚠️'),miskin:mkIco('#f97316','🟠'),rentan:mkIco('#eab308','🟡'),mampu:mkIco('#22c55e','🟢')};
function iico(j){return MICO[(j||'').toLowerCase()]||MICO.lainnya}
function pico(se){return MICO[se]||MICO.miskin}
async function loadMapData(){
if(!mapInited)return;
// Ibadah
const gj=await fetch('../api/rumah_ibadah.php?action=geojson').then(r=>r.json());
MLG.ibadah.clearLayers();MLG.radius.clearLayers();
const ilist=document.getElementById('map-ibadah-list');ilist.innerHTML='';
(gj.features||[]).forEach(f=>{
const p=f.properties,[lng,lat]=f.geometry.coordinates;
L.circle([lat,lng],{radius:p.radius,color:'#3b82f6',fillColor:'#3b82f6',fillOpacity:.04,weight:1,dashArray:'5,5'}).addTo(MLG.radius);
L.marker([lat,lng],{icon:iico(p.jenis)}).bindPopup(`<b>${p.nama}</b><br><small>${p.jenis} · KK: ${p.jumlah_kk} · Bantuan: ${p.jumlah_bantuan}</small>`).addTo(MLG.ibadah);
ilist.innerHTML+=`<div onclick="mmap.flyTo([${lat},${lng}],15)" style="padding:8px 10px;background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:all .15s;margin-bottom:4px" onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
<div style="font-size:11px;font-weight:700">${p.nama}</div>
<div style="font-size:10px;color:var(--text2)">${p.jenis} · ${p.jumlah_kk} KK · ${p.jumlah_bantuan} bantuan</div>
</div>`;
});
loadPetaPenduduk();
}
async function loadPetaPenduduk(){
if(!mapInited)return;
const se=document.getElementById('mp-ekonomi').value;
const kec=document.getElementById('mp-kecamatan').value;
let url=`../api/penduduk.php?action=geojson`;
if(se!=='all')url+=`&status_ekonomi=${se}`;
if(kec)url+=`&kecamatan=${encodeURIComponent(kec)}`;
const gj=await fetch(url).then(r=>r.json());
MLG.penduduk.clearLayers();
(gj.features||[]).forEach(f=>{
const p=f.properties,[lng,lat]=f.geometry.coordinates;
L.marker([lat,lng],{icon:pico(p.status_ekonomi)}).bindPopup(`
<b>${p.nama_kepala_keluarga}</b><br>
<span style="font-size:11px">📍 ${p.alamat||'-'}<br>👥 ${p.jumlah_anggota} jiwa · ${seBadge(p.status_ekonomi)}<br>🕌 ${p.nama_ibadah||'-'}<br>🤝 ${p.jumlah_bantuan||0}x bantuan</span>
`).addTo(MLG.penduduk);
});
}
// ── PENDUDUK FULL TABLE ──
let pAll=[];let pPage=1;const pPerPage=30;
async function loadPendudukFull(){
const se=document.getElementById('q-ekonomi').value;
const kec=document.getElementById('q-kecamatan').value;
let url=`../api/penduduk.php?action=geojson`;
if(se)url+=`&status_ekonomi=${se}`;
if(kec)url+=`&kecamatan=${encodeURIComponent(kec)}`;
const gj=await fetch(url).then(r=>r.json());
pAll=gj.features||[];pPage=1;
renderPTable(pAll);
}
function filterTable(){
const q=document.getElementById('q-penduduk').value.toLowerCase();
const filtered=pAll.filter(f=>{const p=f.properties;return(p.nama_kepala_keluarga||'').toLowerCase().includes(q)||(p.alamat||'').toLowerCase().includes(q)||(p.kelurahan||'').toLowerCase().includes(q)});
renderPTable(filtered);
}
function renderPTable(data){
const start=(pPage-1)*pPerPage;const slice=data.slice(start,start+pPerPage);
const tbody=document.getElementById('p-tbody');
tbody.innerHTML=slice.map((f,i)=>{const p=f.properties;return`<tr>
<td style="color:var(--text2)">${start+i+1}</td>
<td>${p.nama_kepala_keluarga}</td>
<td style="font-size:10px;color:var(--text2)">${p.no_kk||'-'}</td>
<td style="color:var(--text2)">${p.alamat||'-'}</td>
<td>${p.kecamatan||'-'}</td>
<td style="text-align:center">${p.jumlah_anggota}</td>
<td>${seBadge(p.status_ekonomi)}</td>
<td style="font-size:10px;color:var(--text2)">${p.nama_ibadah||'-'}</td>
<td style="text-align:center;color:var(--green)">${p.jumlah_bantuan||0}x</td>
<td style="text-align:center"><button onclick="openDetailKKPem(${p.id})" style="padding:4px 10px;background:rgba(59,130,246,0.12);border:1px solid rgba(59,130,246,0.25);color:#93c5fd;border-radius:7px;cursor:pointer;font-family:'Sora',sans-serif;font-size:10px;font-weight:700">👥 Detail</button></td>
</tr>`}).join('');
const total=data.length;const pages=Math.ceil(total/pPerPage);
document.getElementById('p-paging').innerHTML=`
Menampilkan ${start+1}-${Math.min(start+pPerPage,total)} dari ${total} data
<div style="display:flex;gap:6px">
${pPage>1?`<button onclick="pPage--;loadPendudukFull()" style="padding:4px 10px;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text2);border-radius:7px;cursor:pointer;font-family:'Sora',sans-serif;font-size:10px">← Prev</button>`:''}
<span style="padding:4px 10px">Hal ${pPage}/${pages}</span>
${pPage<pages?`<button onclick="pPage++;loadPendudukFull()" style="padding:4px 10px;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text2);border-radius:7px;cursor:pointer;font-family:'Sora',sans-serif;font-size:10px">Next →</button>`:''}
</div>`;
}
// ── BANTUAN FULL ──
async function loadBantuanStats(){
const s=await fetch('../api/bantuan.php?action=statistik').then(r=>r.json());
document.getElementById('bt-total').textContent=(s.total_bantuan||0).toLocaleString('id');
const nom=s.total_nominal||0;
document.getElementById('bt-nominal').textContent=nom>1e6?`${(nom/1e6).toFixed(1)}jt`:nom.toLocaleString('id');
document.getElementById('bt-pem').textContent=(s.per_jenis||[]).filter(j=>j.sumber==='pemerintah').reduce((a,j)=>a+parseInt(j.jumlah),0);
document.getElementById('bt-ibadah').textContent=(s.per_jenis||[]).filter(j=>j.sumber==='rumah_ibadah').reduce((a,j)=>a+parseInt(j.jumlah),0);
}
async function loadBantuanFull(){
const sumber=document.getElementById('bt-filter-sumber').value;
const data=await fetch('../api/bantuan.php?action=list').then(r=>r.json()).catch(()=>[]);
const filtered=sumber?data.filter(b=>b.sumber_bantuan===sumber):data;
const tbody=document.getElementById('bt-tbody');
tbody.innerHTML=filtered.slice(0,100).map(b=>`<tr>
<td>${b.nama_kepala_keluarga}<br><span style="font-size:9px;color:var(--text2)">${b.kelurahan||''}</span></td>
<td><span class="badge bg">${b.jenis_bantuan.substring(0,20)}</span></td>
<td style="font-size:10px;color:var(--text2)">${b.sumber_bantuan}</td>
<td style="font-size:10px;color:var(--text2)">${b.nama_ibadah||'-'}</td>
<td style="font-size:10px;color:var(--text2)">${b.nama_mitra||'-'}</td>
<td style="font-size:10px">${fDate(b.tanggal_bantuan)}</td>
<td style="font-size:10px;color:var(--text2)">${b.bentuk_bantuan.substring(0,40)}</td>
<td style="color:var(--green);font-size:10px">${b.jumlah_nominal?'Rp'+Number(b.jumlah_nominal).toLocaleString('id'):'-'}</td>
<td>${svBadge(b.status_bantuan)}</td>
</tr>`).join('');
}
// ── LAPORAN FULL ──
async function loadLaporanFull(){
const status=document.getElementById('lp-filter-status').value;
const jenis=document.getElementById('lp-filter-jenis').value;
let url=`../api/laporan.php?action=list`;
if(status)url+=`&status=${status}`;
if(jenis)url+=`&jenis=${jenis}`;
const data=await fetch(url).then(r=>r.json()).catch(()=>[]);
const cnt={pending:0,diproses:0,disetujui:0,ditolak:0};
data.forEach(l=>{if(cnt[l.status_verifikasi]!==undefined)cnt[l.status_verifikasi]++});
document.getElementById('lp-total').textContent=data.length;
document.getElementById('lp-pending').textContent=cnt.pending;
document.getElementById('lp-proses').textContent=cnt.diproses;
document.getElementById('lp-setuju').textContent=cnt.disetujui;
document.getElementById('lp-tolak').textContent=cnt.ditolak;
const tbody=document.getElementById('lp-tbody');
tbody.innerHTML=data.slice(0,100).map(l=>`<tr>
<td>${jenisL(l.jenis_laporan)}</td>
<td style="font-size:10px">${l.nama_pelapor||'Anonim'}<br><span style="color:var(--text2)">${l.hp_pelapor||'-'}</span></td>
<td style="font-size:10px">${l.nama_terkait||l.nama_kepala_keluarga||'-'}</td>
<td style="font-size:10px;color:var(--text2)">${l.nama_ibadah||'-'}</td>
<td style="font-size:10px">${fDate(l.tanggal_kejadian)}</td>
<td>${svBadge(l.status_verifikasi)}</td>
<td style="font-size:10px;color:var(--text2);max-width:200px">${(l.deskripsi||'').substring(0,60)}${l.deskripsi&&l.deskripsi.length>60?'...':''}</td>
</tr>`).join('');
}
// ── EXPORT CSV ──
function exportCSV(){
const rows=[['No','Kepala Keluarga','No KK','Alamat','Kecamatan','Jiwa','Status Ekonomi','Rumah Ibadah','Bantuan']];
pAll.forEach((f,i)=>{const p=f.properties;rows.push([i+1,p.nama_kepala_keluarga,p.no_kk||'',p.alamat||'',p.kecamatan||'',p.jumlah_anggota,p.status_ekonomi,p.nama_ibadah||'',p.jumlah_bantuan||0])});
const csv=rows.map(r=>r.map(v=>`"${String(v).replace(/"/g,'""')}"`).join(',')).join('\n');
const a=document.createElement('a');
a.href='data:text/csv;charset=utf-8,\uFEFF'+encodeURIComponent(csv);
a.download=`penduduk_pontianak_${new Date().toISOString().split('T')[0]}.csv`;
a.click();
}
// ── DETAIL KK (Pemerintah) ──
async function openDetailKKPem(id){
const modal=document.getElementById('m-detail-kk-pem');
modal.style.display='flex';
document.getElementById('pem-detail-title').textContent='Detail Keluarga';
document.getElementById('pem-detail-body').innerHTML='<div style="text-align:center;padding:30px;color:var(--text2)">⏳ Memuat data...</div>';
const f=await fetch(`../api/penduduk.php?action=get&id=${id}`).then(r=>r.json()).catch(()=>null);
if(!f||!f.properties){document.getElementById('pem-detail-body').innerHTML='<p style="text-align:center;color:var(--red)">Gagal memuat data</p>';return}
const p=f.properties;
const anggota=p.anggota||[];
const bantuan=p.histori_bantuan||[];
document.getElementById('pem-detail-title').textContent=p.nama_kepala_keluarga;
document.getElementById('pem-detail-body').innerHTML=`
<div style="margin-bottom:14px">
<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
${seBadge(p.status_ekonomi)}
${p.nama_ibadah?`<span class="badge bb">🕌 ${p.nama_ibadah}</span>`:''}
<span class="badge bgr">👁️ View Only</span>
</div>
<div style="font-size:11px;color:var(--text2);line-height:1.9">
📋 No. KK: <b style="color:var(--text)">${p.no_kk||'-'}</b><br>
📍 ${p.alamat||'-'}${p.rt?` · RT${p.rt}/RW${p.rw}`:''}<br>
${p.kelurahan?`🏘️ Kel. ${p.kelurahan}, Kec. ${p.kecamatan||'-'}<br>`:''}
🏠 Hunian: <b style="color:var(--text)">${(p.status_hunian||'-').replace(/_/g,' ')}</b>
${p.jarak_ke_ibadah?`<br>📏 Jarak ke ibadah: ${p.jarak_ke_ibadah}m`:''}
</div>
</div>
<div style="font-size:10px;font-weight:800;color:var(--text2);letter-spacing:0.5px;text-transform:uppercase;padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:8px">👥 Anggota Keluarga (${anggota.length} jiwa)</div>
${anggota.length?anggota.map(a=>`
<div style="background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:9px;padding:10px;margin-bottom:7px">
<div style="display:flex;gap:9px;align-items:flex-start">
<div style="width:36px;height:36px;border-radius:9px;background:${a.jenis_kelamin==='L'?'rgba(59,130,246,0.15)':'rgba(236,72,153,0.15)'};display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0">${a.jenis_kelamin==='L'?'👦':'👧'}</div>
<div style="flex:1">
<div style="font-size:12px;font-weight:700;color:var(--text)">${a.nama} ${a.status_hidup&&a.status_hidup!=='hidup'?`<span style="padding:2px 6px;background:rgba(239,68,68,0.15);color:#fca5a5;border-radius:4px;font-size:9px;font-weight:800">${a.status_hidup}</span>`:''}</div>
<div style="font-size:10px;color:var(--text2);margin-top:2px">${a.hubungan} · ${a.umur||0} tahun · ${a.agama||'-'} · ${a.jenis_kelamin==='L'?'Laki-laki':'Perempuan'}</div>
${a.pendidikan_terakhir?`<div style="font-size:10px;color:var(--accent);margin-top:3px">📚 ${a.pendidikan_terakhir.jenjang}${a.pendidikan_terakhir.nama_institusi?' — '+a.pendidikan_terakhir.nama_institusi:''}${a.pendidikan_terakhir.status_pendidikan==='sedang_berjalan'?' (aktif)':''}</div>`:''}
${a.pekerjaan?`<div style="font-size:10px;color:var(--text2);margin-top:2px">💼 ${a.pekerjaan}</div>`:''}
${a.nik?`<div style="font-size:10px;color:var(--text2);margin-top:1px">🪪 NIK: ${a.nik}</div>`:''}
</div>
</div>
</div>`).join(''):'<div style="text-align:center;padding:12px;color:var(--text2);font-size:12px">Belum ada data anggota</div>'}
<div style="font-size:10px;font-weight:800;color:var(--text2);letter-spacing:0.5px;text-transform:uppercase;padding:10px 0 8px;border-top:1px solid var(--border);margin-top:8px">🤝 Riwayat Bantuan (${bantuan.length}x)</div>
${bantuan.length?bantuan.slice(0,5).map(b=>`
<div style="background:rgba(16,185,129,.05);border:1px solid rgba(16,185,129,.15);border-radius:9px;padding:10px;margin-bottom:7px">
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
<div style="font-size:12px;font-weight:700;color:var(--green)">${b.jenis_bantuan}</div>
<div style="font-size:10px;color:var(--text2)">${fDate(b.tanggal_bantuan)}</div>
</div>
<div style="font-size:11px;color:var(--text2)">📦 ${b.bentuk_bantuan}${b.jumlah_nominal?`<br>💰 Rp${Number(b.jumlah_nominal).toLocaleString('id')}`:''}</div>
<div style="margin-top:5px;padding:4px 8px;background:rgba(59,130,246,.06);border-radius:6px;font-size:10px;color:var(--text2)">🕌 ${b.nama_ibadah||b.sumber_bantuan||'-'}${b.nama_mitra?' + mitra: '+b.nama_mitra:''}</div>
</div>`).join(''):'<div style="text-align:center;padding:12px;color:var(--text2);font-size:12px">Belum ada bantuan tercatat</div>'}
<div style="margin-top:14px">
<button onclick="document.getElementById('m-detail-kk-pem').style.display='none'" style="width:100%;padding:9px;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text2);border-radius:9px;font-family:'Sora',sans-serif;font-size:12px;font-weight:700;cursor:pointer">Tutup</button>
</div>`;
// Tutup modal saat klik luar
modal.onclick=e=>{if(e.target===modal)modal.style.display='none'};
}
// ── HELPERS ──
function seBadge(se){const m={sangat_miskin:'br',miskin:'ba',rentan:'bp',mampu:'bg'};const l={sangat_miskin:'Sangat Miskin',miskin:'Miskin',rentan:'Rentan',mampu:'Mampu'};return`<span class="badge ${m[se]||'bb'}">${l[se]||se}</span>`}
function svBadge(s){const m={pending:'ba',diproses:'bb',disetujui:'bg',ditolak:'br',disalurkan:'bg',ditunda:'ba',dibatalkan:'br'};return`<span class="badge ${m[s]||'bb'}">${s}</span>`}
function jenisL(j){const m={kelahiran:'🍼 Kelahiran',kematian:'⚰️ Kematian',pindah_masuk:'📦 Pindah Masuk',pindah_keluar:'🚚 Pindah Keluar',pernikahan:'💒 Pernikahan',perubahan_data:'📝 Perubahan Data',lainnya:'📌 Lainnya'};return m[j]||j}
function fDate(d){if(!d)return'-';return new Date(d).toLocaleDateString('id-ID',{day:'numeric',month:'short',year:'numeric'})}
// ── INIT ──
loadOverview();
setInterval(()=>{loadLiveStream();},60000);
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
<?php
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASSWORD') ?: '');
define('DB_NAME', getenv('DB_NAME') ?: 'webgis_pontianak');
define('BASE_URL', '');
define('UPLOAD_DIR', __DIR__ . '/../assets/uploads/');
define('SESSION_TIMEOUT', 7200); // 2 jam
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
http_response_code(500);
die(json_encode(['error' => 'Koneksi database gagal: ' . $conn->connect_error]));
}
$conn->set_charset('utf8mb4');
// ── Session ──────────────────────────────────────────────
function startSession() {
if (session_status() === PHP_SESSION_NONE) {
if (!headers_sent()) {
session_set_cookie_params(['httponly' => true, 'samesite' => 'Strict']);
}
session_start();
}
}
function requireLogin($roles = []) {
startSession();
if (empty($_SESSION['user_id'])) {
if (isApiRequest()) {
http_response_code(401);
echo json_encode(['error' => 'Tidak terautentikasi']);
exit;
}
header('Location: login.php');
exit;
}
if (!empty($roles) && !in_array($_SESSION['role'], $roles)) {
if (isApiRequest()) {
http_response_code(403);
echo json_encode(['error' => 'Akses ditolak']);
exit;
}
header('Location: login.php?error=akses');
exit;
}
}
function isApiRequest() {
return strpos($_SERVER['REQUEST_URI'], '/api/') !== false
|| ($_SERVER['HTTP_ACCEPT'] ?? '') === 'application/json';
}
function currentUser() {
startSession();
return [
'id' => $_SESSION['user_id'] ?? null,
'nama' => $_SESSION['nama'] ?? '',
'role' => $_SESSION['role'] ?? '',
'ibadah'=> $_SESSION['id_rumah_ibadah'] ?? null,
];
}
// ── Haversine ────────────────────────────────────────────
function haversine($lat1, $lon1, $lat2, $lon2) {
$R = 6371000;
$phi1 = deg2rad($lat1); $phi2 = deg2rad($lat2);
$dphi = deg2rad($lat2 - $lat1);
$dl = deg2rad($lon2 - $lon1);
$a = sin($dphi/2)**2 + cos($phi1)*cos($phi2)*sin($dl/2)**2;
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
}
function findNearestIbadah($conn, $lat, $lon) {
$result = $conn->query("SELECT id, nama, latitude, longitude FROM rumah_ibadah");
$nearest = null; $minDist = PHP_INT_MAX;
while ($row = $result->fetch_assoc()) {
$dist = haversine($lat, $lon, $row['latitude'], $row['longitude']);
if ($dist < $minDist) { $minDist = $dist; $nearest = $row; }
}
return $nearest ? ['id' => $nearest['id'], 'nama' => $nearest['nama'], 'jarak' => round($minDist, 2)] : null;
}
function findIbadahInRadius($conn, $lat, $lon) {
// Cari rumah ibadah yang radius-nya mencakup koordinat ini
$result = $conn->query("SELECT id, nama, radius, latitude, longitude FROM rumah_ibadah");
$matches = [];
while ($row = $result->fetch_assoc()) {
$dist = haversine($lat, $lon, $row['latitude'], $row['longitude']);
if ($dist <= $row['radius']) $matches[] = ['id' => $row['id'], 'nama' => $row['nama'], 'jarak' => round($dist, 2)];
}
return $matches ?: [findNearestIbadah($conn, $lat, $lon)];
}
// ── Log aktivitas ─────────────────────────────────────────
function logAktivitas($conn, $aksi, $tabel = null, $id = null, $desc = null) {
startSession();
$uid = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 'NULL';
$aksi = $conn->real_escape_string($aksi);
$tbl = $conn->real_escape_string($tabel ?? '');
$desc = $conn->real_escape_string($desc ?? '');
$ip = $conn->real_escape_string($_SERVER['REMOTE_ADDR'] ?? '');
$id = $id ? (int)$id : 'NULL';
$conn->query("INSERT INTO log_aktivitas (id_user,aksi,tabel_terkait,id_terkait,deskripsi,ip_address)
VALUES ($uid,'$aksi','$tbl',$id,'$desc','$ip')");
}
// ── Notifikasi ────────────────────────────────────────────
function kirimNotifikasi($conn, $id_user, $judul, $isi, $tipe = 'info', $id_ref = null) {
$judul = $conn->real_escape_string($judul);
$isi = $conn->real_escape_string($isi);
$tipe = $conn->real_escape_string($tipe);
$id_ref = $id_ref ? (int)$id_ref : 'NULL';
$id_user = (int)$id_user;
$conn->query("INSERT INTO notifikasi (id_user,judul,isi,tipe,id_referensi)
VALUES ($id_user,'$judul','$isi','$tipe',$id_ref)");
}
// ── Upload helper ─────────────────────────────────────────
function uploadFile($fileKey, $prefix = 'file') {
if (empty($_FILES[$fileKey]['name'])) return null;
$dir = UPLOAD_DIR;
if (!is_dir($dir)) mkdir($dir, 0755, true);
$ext = strtolower(pathinfo($_FILES[$fileKey]['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','gif','pdf','webp'];
if (!in_array($ext, $allowed)) return null;
$name = $prefix . '_' . uniqid() . '.' . $ext;
if (move_uploaded_file($_FILES[$fileKey]['tmp_name'], $dir . $name)) {
return 'assets/uploads/' . $name;
}
return null;
}
// ── JSON response ─────────────────────────────────────────
function jsonResponse($data, $code = 200) {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// ── Escape shorthand ──────────────────────────────────────
function esc($conn, $val) { return $conn->real_escape_string($val ?? ''); }
?>
+21
View File
@@ -0,0 +1,21 @@
<?php
// index.php — Entry point, redirect berdasarkan role
require_once 'includes/config.php';
startSession();
if (empty($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$role = $_SESSION['role'] ?? '';
$redirectMap = [
'admin' => 'dashboard/pengurus.php',
'pengurus_ibadah' => 'dashboard/pengurus.php',
'masyarakat' => 'dashboard/masyarakat.php',
'pemerintah' => 'dashboard/pemerintah.php',
];
header('Location: ' . ($redirectMap[$role] ?? 'login.php'));
exit;
?>
+436
View File
@@ -0,0 +1,436 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Masuk WebGIS Pontianak</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #080c14;
--surface: #0f1522;
--card: #141b2d;
--border: rgba(99,179,237,0.12);
--accent: #3b82f6;
--accent2: #06b6d4;
--gold: #f59e0b;
--green: #10b981;
--red: #ef4444;
--text: #e2e8f0;
--text2: #7c93b5;
--glow: rgba(59,130,246,0.25);
}
*{box-sizing:border-box;margin:0;padding:0}
body{
font-family:'Sora',sans-serif;
background:var(--bg);
color:var(--text);
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
overflow:hidden;
position:relative;
}
/* Animated background */
.bg-grid{
position:fixed;inset:0;
background-image:
linear-gradient(rgba(59,130,246,0.04) 1px,transparent 1px),
linear-gradient(90deg,rgba(59,130,246,0.04) 1px,transparent 1px);
background-size:40px 40px;
animation:gridMove 20s linear infinite;
}
@keyframes gridMove{ to{background-position:40px 40px} }
.bg-orb{
position:fixed;
border-radius:50%;
filter:blur(80px);
pointer-events:none;
}
.orb1{width:600px;height:600px;background:rgba(59,130,246,0.08);top:-200px;right:-100px;animation:orbFloat 12s ease-in-out infinite;}
.orb2{width:400px;height:400px;background:rgba(6,182,212,0.06);bottom:-100px;left:-100px;animation:orbFloat 15s ease-in-out infinite reverse;}
.orb3{width:300px;height:300px;background:rgba(245,158,11,0.04);top:50%;left:50%;transform:translate(-50%,-50%);animation:orbFloat 18s ease-in-out infinite 3s;}
@keyframes orbFloat{0%,100%{transform:translate(0,0)}50%{transform:translate(30px,-30px)}}
/* Floating particles */
.particles{position:fixed;inset:0;pointer-events:none}
.particle{
position:absolute;
width:2px;height:2px;
background:var(--accent);
border-radius:50%;
opacity:0;
animation:particleFly 8s ease-in infinite;
}
@keyframes particleFly{
0%{opacity:0;transform:translateY(100vh) scale(0)}
10%{opacity:0.6}
90%{opacity:0.2}
100%{opacity:0;transform:translateY(-20px) scale(1)}
}
/* Main container */
.login-wrap{
position:relative;z-index:10;
display:grid;
grid-template-columns:1fr 1fr;
max-width:900px;width:95%;
min-height:560px;
border-radius:24px;
overflow:hidden;
box-shadow:0 40px 80px rgba(0,0,0,0.6), 0 0 0 1px var(--border);
animation:wrapIn 0.8s cubic-bezier(0.23,1,0.32,1) both;
}
@keyframes wrapIn{from{opacity:0;transform:translateY(30px) scale(0.97)}to{opacity:1;transform:none}}
/* Left panel */
.login-hero{
background:linear-gradient(135deg,#0d1929 0%,#0a1628 50%,#060e1a 100%);
padding:52px 48px;
display:flex;flex-direction:column;justify-content:space-between;
position:relative;overflow:hidden;
}
.hero-map{
position:absolute;inset:0;opacity:0.15;
background-image:radial-gradient(circle at 30% 60%, var(--accent) 0%, transparent 50%);
}
.hero-dots{
position:absolute;inset:0;
background-image:radial-gradient(circle,rgba(59,130,246,0.4) 1px,transparent 1px);
background-size:24px 24px;
opacity:0.3;
}
.brand{position:relative;z-index:2}
.brand-icon{
width:56px;height:56px;
background:linear-gradient(135deg,var(--accent),var(--accent2));
border-radius:16px;
display:flex;align-items:center;justify-content:center;
font-size:26px;
margin-bottom:20px;
box-shadow:0 8px 32px var(--glow);
}
.brand-name{font-size:22px;font-weight:800;letter-spacing:-0.5px;line-height:1.2}
.brand-sub{font-size:12px;color:var(--text2);margin-top:4px;letter-spacing:0.5px}
.hero-stats{
position:relative;z-index:2;
display:flex;flex-direction:column;gap:12px;
}
.hero-stat{
display:flex;align-items:center;gap:12px;
padding:12px 16px;
background:rgba(255,255,255,0.04);
border:1px solid rgba(255,255,255,0.06);
border-radius:12px;
backdrop-filter:blur(8px);
}
.hs-icon{font-size:20px}
.hs-num{font-size:18px;font-weight:700;color:var(--accent)}
.hs-label{font-size:11px;color:var(--text2);margin-top:1px}
.hero-footer{
position:relative;z-index:2;
font-size:11px;color:rgba(124,147,181,0.6);
}
/* Right panel */
.login-form-wrap{
background:var(--card);
padding:52px 48px;
display:flex;flex-direction:column;justify-content:center;
}
.form-title{font-size:26px;font-weight:800;letter-spacing:-0.5px;margin-bottom:4px}
.form-sub{font-size:13px;color:var(--text2);margin-bottom:32px}
/* Role tabs */
.role-tabs{
display:grid;grid-template-columns:1fr 1fr 1fr;
gap:6px;margin-bottom:28px;
background:rgba(255,255,255,0.03);
border:1px solid var(--border);
border-radius:14px;padding:5px;
}
.role-tab{
padding:8px 4px;
border-radius:10px;
border:none;background:transparent;
color:var(--text2);
font-family:'Sora',sans-serif;
font-size:11px;font-weight:600;
cursor:pointer;
transition:all 0.2s;
display:flex;flex-direction:column;align-items:center;gap:3px;
}
.role-tab .tab-icon{font-size:16px}
.role-tab.active{
background:var(--accent);
color:#fff;
box-shadow:0 4px 16px var(--glow);
}
.role-tab:not(.active):hover{background:rgba(255,255,255,0.05);color:var(--text)}
/* Form fields */
.field{margin-bottom:16px}
.field label{display:block;font-size:12px;font-weight:600;color:var(--text2);margin-bottom:6px;letter-spacing:0.3px}
.field-wrap{position:relative}
.field-icon{
position:absolute;left:14px;top:50%;transform:translateY(-50%);
font-size:16px;pointer-events:none;z-index:2;
}
.field input{
width:100%;
background:rgba(255,255,255,0.04);
border:1px solid var(--border);
color:var(--text);
padding:12px 14px 12px 42px;
border-radius:12px;
font-family:'Sora',sans-serif;
font-size:14px;
transition:all 0.2s;
outline:none;
}
.field input:focus{
border-color:var(--accent);
background:rgba(59,130,246,0.06);
box-shadow:0 0 0 3px var(--glow);
}
.field input::placeholder{color:rgba(124,147,181,0.4)}
.toggle-pw{
position:absolute;right:14px;top:50%;transform:translateY(-50%);
background:none;border:none;color:var(--text2);cursor:pointer;
font-size:16px;padding:2px;
}
.error-msg{
background:rgba(239,68,68,0.1);
border:1px solid rgba(239,68,68,0.3);
color:#fca5a5;
padding:10px 14px;border-radius:10px;
font-size:12px;margin-bottom:16px;
display:none;
}
.error-msg.show{display:block}
.btn-login{
width:100%;padding:14px;
background:linear-gradient(135deg,var(--accent),var(--accent2));
border:none;border-radius:12px;
color:#fff;
font-family:'Sora',sans-serif;
font-size:14px;font-weight:700;
cursor:pointer;
transition:all 0.2s;
box-shadow:0 8px 24px var(--glow);
position:relative;overflow:hidden;
}
.btn-login:hover{transform:translateY(-1px);box-shadow:0 12px 32px var(--glow)}
.btn-login:active{transform:translateY(0)}
.btn-login:disabled{opacity:0.6;cursor:not-allowed;transform:none}
.btn-login .spinner{
display:inline-block;width:16px;height:16px;
border:2px solid rgba(255,255,255,0.3);
border-top-color:#fff;
border-radius:50%;
animation:spin 0.7s linear infinite;
vertical-align:middle;margin-right:6px;
}
@keyframes spin{to{transform:rotate(360deg)}}
.form-links{
margin-top:20px;text-align:center;
font-size:12px;color:var(--text2);
}
.form-links a{color:var(--accent);text-decoration:none;font-weight:600}
.form-links a:hover{text-decoration:underline}
.register-hint{
margin-top:12px;
padding:12px;
background:rgba(16,185,129,0.06);
border:1px solid rgba(16,185,129,0.2);
border-radius:10px;
font-size:11px;color:rgba(16,185,129,0.8);
text-align:center;
}
@media(max-width:700px){
.login-wrap{grid-template-columns:1fr}
.login-hero{display:none}
}
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="bg-orb orb1"></div>
<div class="bg-orb orb2"></div>
<div class="bg-orb orb3"></div>
<div class="particles" id="particles"></div>
<div class="login-wrap">
<!-- Left hero -->
<div class="login-hero">
<div class="hero-map"></div>
<div class="hero-dots"></div>
<div class="brand">
<div class="brand-icon">🗺️</div>
<div class="brand-name">WebGIS Pontianak</div>
<div class="brand-sub">SISTEM PENDUDUK MISKIN TERINTEGRASI</div>
</div>
<div class="hero-stats">
<div class="hero-stat">
<div class="hs-icon">🏘️</div>
<div>
<div class="hs-num" id="h-kk"></div>
<div class="hs-label">Kepala Keluarga Terdaftar</div>
</div>
</div>
<div class="hero-stat">
<div class="hs-icon">🕌</div>
<div>
<div class="hs-num" id="h-ibadah"></div>
<div class="hs-label">Rumah Ibadah Aktif</div>
</div>
</div>
<div class="hero-stat">
<div class="hs-icon">🤝</div>
<div>
<div class="hs-num" id="h-bantuan"></div>
<div class="hs-label">Bantuan Tersalurkan</div>
</div>
</div>
</div>
<div class="hero-footer">© 2025 Pemerintah Kota Pontianak · WebGIS v2.0</div>
</div>
<!-- Right form -->
<div class="login-form-wrap">
<div class="form-title">Selamat datang 👋</div>
<div class="form-sub">Masuk sesuai peran Anda dalam sistem</div>
<div class="role-tabs">
<button class="role-tab active" onclick="setRole('masyarakat',this)" id="tab-mas">
<span class="tab-icon">👤</span>Masyarakat
</button>
<button class="role-tab" onclick="setRole('pengurus_ibadah',this)" id="tab-pen">
<span class="tab-icon">🕌</span>Pengurus
</button>
<button class="role-tab" onclick="setRole('pemerintah',this)" id="tab-pem">
<span class="tab-icon">🏛️</span>Pemerintah
</button>
</div>
<div id="error-box" class="error-msg"></div>
<form id="login-form" onsubmit="doLogin(event)">
<input type="hidden" id="role-input" value="masyarakat">
<div class="field">
<label>USERNAME / NO. HP</label>
<div class="field-wrap">
<span class="field-icon">👤</span>
<input type="text" id="username" placeholder="Masukkan username Anda" autocomplete="username" required>
</div>
</div>
<div class="field">
<label>KATA SANDI</label>
<div class="field-wrap">
<span class="field-icon">🔒</span>
<input type="password" id="password" placeholder="••••••••" autocomplete="current-password" required>
<button type="button" class="toggle-pw" onclick="togglePw()">👁️</button>
</div>
</div>
<button type="submit" class="btn-login" id="btn-login">Masuk ke Sistem</button>
</form>
<div class="form-links">
Belum punya akun? <a href="daftar.php">Daftar sebagai Masyarakat / Pengurus Rumah Ibadah</a>
</div>
<div class="register-hint" id="role-hint">
Masyarakat dapat mendaftar mandiri dan melaporkan kejadian di sekitar mereka.
</div>
</div>
</div>
<script>
// Particles
const pWrap = document.getElementById('particles');
for(let i=0;i<25;i++){
const p=document.createElement('div');
p.className='particle';
p.style.cssText=`left:${Math.random()*100}%;animation-delay:${Math.random()*8}s;animation-duration:${6+Math.random()*6}s;opacity:${Math.random()*0.5}`;
pWrap.appendChild(p);
}
// Load stats
fetch('api/auth.php?action=stats')
.then(r=>r.json()).then(d=>{
document.getElementById('h-kk').textContent=d.kk??'—';
document.getElementById('h-ibadah').textContent=d.ibadah??'—';
document.getElementById('h-bantuan').textContent=d.bantuan??'—';
}).catch(()=>{});
const hints={
masyarakat:'Masyarakat dapat mendaftar mandiri dan melaporkan kejadian di sekitar mereka.',
pengurus_ibadah:'Pengurus rumah ibadah mengelola data bantuan dan laporan warga binaan.',
pemerintah:'Pemerintah mendapat akses read-only untuk memantau semua data dan statistik.'
};
function setRole(role,el){
document.querySelectorAll('.role-tab').forEach(t=>t.classList.remove('active'));
el.classList.add('active');
document.getElementById('role-input').value=role;
document.getElementById('role-hint').textContent=hints[role]||'';
}
function togglePw(){
const i=document.getElementById('password');
i.type=i.type==='password'?'text':'password';
}
async function doLogin(e){
e.preventDefault();
const btn=document.getElementById('btn-login');
const errBox=document.getElementById('error-box');
btn.disabled=true;
btn.innerHTML='<span class="spinner"></span>Memverifikasi...';
errBox.classList.remove('show');
const body={
username:document.getElementById('username').value,
password:document.getElementById('password').value,
role:document.getElementById('role-input').value
};
try{
const res=await fetch('api/auth.php',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
});
const data=await res.json();
if(data.success){
btn.innerHTML='✅ Berhasil! Mengalihkan...';
window.location.href=data.redirect||'index.php';
} else {
errBox.textContent=data.error||'Username atau kata sandi salah.';
errBox.classList.add('show');
btn.disabled=false;
btn.innerHTML='Masuk ke Sistem';
}
} catch(err){
errBox.textContent='Terjadi kesalahan. Periksa koneksi Anda.';
errBox.classList.add('show');
btn.disabled=false;
btn.innerHTML='Masuk ke Sistem';
}
}
</script>
</body>
</html>
+96
View File
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Setup Password WebGIS Pontianak</title>
<style>
body{font-family:monospace;background:#0f172a;color:#e2e8f0;padding:40px;max-width:600px;margin:0 auto}
h2{color:#3b82f6;margin-bottom:20px}
.box{background:#1e293b;border:1px solid #334155;border-radius:10px;padding:20px;margin-bottom:16px}
.ok{color:#10b981}.err{color:#ef4444}.warn{color:#f59e0b}
table{width:100%;border-collapse:collapse;margin-top:12px}
td,th{padding:8px 12px;border:1px solid #334155;font-size:13px}
th{background:#0f172a;color:#7c93b5}
.btn{display:inline-block;margin-top:20px;padding:10px 24px;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:700;cursor:pointer;font-family:monospace;text-decoration:none}
.btn:hover{background:#2563eb}
.del{color:#ef4444;font-size:12px;margin-top:16px;padding:10px;background:#450a0a;border:1px solid #7f1d1d;border-radius:8px}
</style>
</head>
<body>
<?php
require_once 'includes/config.php';
$action = $_GET['action'] ?? '';
if ($action === 'setup') {
// Hash password "password123" yang benar
$hash = password_hash('password123', PASSWORD_DEFAULT);
$hash_e = $conn->real_escape_string($hash);
// Update semua user dengan hash yang valid
if ($conn->query("UPDATE users SET password_hash='$hash_e'")) {
echo '<div class="box"><span class="ok">✅ Semua password berhasil di-set ke: <strong>password123</strong></span></div>';
} else {
echo '<div class="box"><span class="err">❌ Gagal: '.$conn->error.'</span></div>';
}
}
?>
<h2>🔧 Setup Password WebGIS Pontianak</h2>
<?php
// Cek koneksi
if ($conn->connect_error) {
echo '<div class="box"><span class="err">❌ Koneksi DB gagal: '.$conn->connect_error.'</span></div>';
exit;
}
echo '<div class="box"><span class="ok">✅ Koneksi database berhasil</span></div>';
// Tampilkan semua user
$res = $conn->query("SELECT id, nama_lengkap, username, role, is_active FROM users ORDER BY id");
?>
<div class="box">
<b>Daftar Akun yang Tersedia:</b>
<table>
<tr><th>#</th><th>Nama</th><th>Username</th><th>Role</th><th>Aktif</th></tr>
<?php while($r = $res->fetch_assoc()): ?>
<tr>
<td><?= $r['id'] ?></td>
<td><?= htmlspecialchars($r['nama_lengkap']) ?></td>
<td><strong><?= htmlspecialchars($r['username']) ?></strong></td>
<td><?= $r['role'] ?></td>
<td><?= $r['is_active'] ? '<span class="ok">✅</span>' : '<span class="err">❌</span>' ?></td>
</tr>
<?php endwhile; ?>
</table>
</div>
<div class="box">
<b>Klik tombol di bawah untuk set semua password menjadi <span class="warn">password123</span>:</b><br><br>
<a class="btn" href="?action=setup">🔐 Set Semua Password → password123</a>
</div>
<div class="del">
⚠️ <strong>PENTING:</strong> Setelah setup selesai, hapus file <code>setup_password.php</code> ini dari server Anda demi keamanan!
</div>
<?php if ($action === 'setup'): ?>
<div class="box" style="margin-top:20px">
<b>✅ Sekarang bisa login dengan:</b>
<table>
<tr><th>Username</th><th>Password</th><th>Role</th></tr>
<tr><td>admin</td><td>password123</td><td>Admin</td></tr>
<tr><td>pengurus_mj</td><td>password123</td><td>Pengurus Ibadah</td></tr>
<tr><td>pengurus_mm</td><td>password123</td><td>Pengurus Ibadah</td></tr>
<tr><td>pengurus_gk</td><td>password123</td><td>Pengurus Ibadah</td></tr>
<tr><td>pemda_ptk</td><td>password123</td><td>Pemerintah</td></tr>
<tr><td>ahmad_yusuf</td><td>password123</td><td>Masyarakat</td></tr>
</table>
<br>
<a class="btn" href="login.php" style="background:#10b981">🚀 Pergi ke Halaman Login</a>
</div>
<?php endif; ?>
</body>
</html>
+59
View File
@@ -0,0 +1,59 @@
-- =========================================================
-- SQL PERUBAHAN: Fitur Radius Slider + Daftar Pengurus
-- =========================================================
-- 1. Tabel pengurus_pending: menyimpan pendaftaran pengurus
-- yang menunggu verifikasi admin sebelum dibuatkan user.
CREATE TABLE IF NOT EXISTS `pengurus_pending` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`nama_lengkap` VARCHAR(100) NOT NULL,
`username` VARCHAR(50) NOT NULL,
`no_hp` VARCHAR(20) NOT NULL,
`email` VARCHAR(100) DEFAULT NULL,
`password_hash` VARCHAR(255) NOT NULL,
-- Data rumah ibadah baru yang diusulkan
`nama_ibadah` VARCHAR(100) NOT NULL,
`jenis_ibadah` ENUM('Masjid','Gereja','Vihara','Pura','Klenteng','Lainnya') DEFAULT 'Masjid',
`latitude` DOUBLE NOT NULL,
`longitude` DOUBLE NOT NULL,
`alamat` TEXT DEFAULT NULL,
`kelurahan` VARCHAR(80) DEFAULT NULL,
`kecamatan` VARCHAR(80) DEFAULT NULL,
`radius` INT DEFAULT 300,
-- Status verifikasi
`status` ENUM('pending','approved','rejected') DEFAULT 'pending',
`catatan_admin` TEXT DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_username` (`username`),
UNIQUE KEY `uq_hp` (`no_hp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =========================================================
-- CATATAN: Tidak ada perubahan skema pada tabel yang ada.
-- Kolom `radius` sudah ada di tabel `rumah_ibadah`.
-- Fitur slider hanya mengubah nilai radius via PUT API
-- yang sudah tersedia di api/rumah_ibadah.php.
-- =========================================================
-- =========================================================
-- SQL PERUBAHAN: Fitur Chat Masyarakat ↔ Rumah Ibadah
-- =========================================================
CREATE TABLE IF NOT EXISTS `chat_masyarakat` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id_user_masyarakat` INT UNSIGNED NOT NULL COMMENT 'User dengan role masyarakat',
`id_rumah_ibadah` INT UNSIGNED NOT NULL,
`id_pengirim` INT UNSIGNED NOT NULL COMMENT 'User yang mengirim pesan ini',
`pengirim_role` ENUM('masyarakat','pengurus_ibadah') NOT NULL,
`isi_pesan` TEXT NOT NULL,
`is_read` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_chat_masyarakat` (`id_user_masyarakat`, `id_rumah_ibadah`),
INDEX `idx_chat_ibadah` (`id_rumah_ibadah`),
INDEX `idx_chat_unread` (`is_read`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Pesan chat antara masyarakat dan rumah ibadah';
-- =========================================================