add files
This commit is contained in:
+189
@@ -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*
|
||||
@@ -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
@@ -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);
|
||||
?>
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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]);
|
||||
}
|
||||
?>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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
|
||||
?>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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; }
|
||||
@@ -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
@@ -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
@@ -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 🟠 Miskin 🟡 Rentan</div>
|
||||
<div>🕌 Masjid ⛪ Gereja 🏯 Vihara 🛕 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
@@ -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 ?? ''); }
|
||||
?>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
-- =========================================================
|
||||
Reference in New Issue
Block a user