Initial commit

This commit is contained in:
envicy
2026-06-11 16:37:05 +07:00
commit 503e9421a9
45 changed files with 13370 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# WebGIS Pemetaan Kemiskinan Kota Pontianak
Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, sebaran penduduk miskin, log bantuan, dan rumah ibadah di Kota Pontianak.
> [!WARNING]
> **DISCLAIMER — DATA DUMMY / BUKAN DATA NYATA**
>
> Seluruh data yang ditampilkan dalam aplikasi ini, termasuk namun tidak terbatas pada:
> - Data nama, lokasi, dan kategori **penduduk miskin**
> - **Foto rumah** dan **Foto Kartu Keluarga (KK)** yang diunggah
> - Data **rumah ibadah** beserta radius jangkauannya
> - **Log bantuan** yang tercatat di sistem
>
> adalah **data fiktif / dummy** yang dibuat semata-mata untuk keperluan **demonstrasi, pengembangan, dan pengujian sistem** ini.
> Data tersebut **tidak mewakili kondisi, identitas, atau situasi nyata** dari individu maupun lokasi mana pun di Kota Pontianak.
## Fitur Utama
- **Pemetaan Interaktif**: Visualisasi peta dengan Leaflet.js yang berpusat di Kota Pontianak.
- **Manajemen Data Spasial**:
- **Rumah Ibadah**: Lokasi titik rumah ibadah (Masjid, Gereja, Vihara, dll) beserta jangkauan radius jemaah/bantuan.
- **Penduduk Miskin**: Pemetaan warga kurang mampu dan integrasi log/riwayat bantuan sosial yang disalurkan melalui rumah ibadah terdekat.
- **Pencarian Cepat**: Cari data rumah ibadah langsung dari bilah pencarian.
- **Impor GeoJSON Eksternal**: Pengguna dapat menambahkan file GeoJSON dari perangkat lokal ke dalam peta.
## Persyaratan Sistem
- **Server Web**: Apache (misal menggunakan XAMPP atau Laragon).
- **Bahasa Pemrograman**: PHP 7.4 ke atas.
- **Database**: MySQL versi 5.7+ atau MariaDB versi 10.2+ (Wajib mendukung fitur fungsi spasial seperti `ST_GeomFromGeoJSON` dan `ST_AsGeoJSON`).
## Cara Instalasi & Penggunaan
1. **Unduh/Clone Repositori**:
Tempatkan folder `webgis-poverty-pontianak` di dalam direktori root server web Anda (misalnya `C:/xampp/htdocs/webgis/webgis-poverty-pontianak`).
2. **Setup Database**:
- Buka phpMyAdmin atau MySQL client pilihan Anda.
- Buat database baru bernama `webgis`:
```sql
CREATE DATABASE webgis;
```
- Import file `database.sql` ke dalam database `webgis` tersebut.
3. **Konfigurasi Koneksi**:
- Buka file [api/db_connect.php](api/db_connect.php).
- Sesuaikan konfigurasi host, user, password, dan database sesuai dengan setup database MySQL lokal Anda:
```php
$host = "localhost";
$user = "root";
$pass = "";
$db = "webgis";
```
4. **Jalankan Aplikasi**:
- Aktifkan modul **Apache** dan **MySQL** di Control Panel XAMPP Anda.
- Buka browser dan navigasikan ke alamat:
[http://localhost/webgis/webgis-poverty-pontianak/index.php](http://localhost/webgis/webgis-poverty-pontianak/index.php)
## Akun Akses Default (Login)
Anda dapat masuk ke dalam sistem menggunakan akun simulasi berikut:
- **Akun Administrator (Admin)**:
- **Username**: `admin`
- **Password**: `password`
- **Hak Akses**: Mengelola data spasial rumah ibadah, penduduk miskin, mengelola akun pengelola rumah ibadah, dan manajemen pengguna secara penuh.
- **Akun Pengelola Rumah Ibadah**:
- **Username**: `pengelola_mujahidin`
- **Password**: `password`
- **Hak Akses**: Mengelola (tambah, edit, hapus, impor, geser lokasi) data penduduk miskin, serta menginput log bantuan sosial yang disalurkan melalui masjid kelolaannya (Masjid Raya Mujahidin).
## Struktur Folder
- `api/` — Kumpulan REST API endpoint untuk operasi CRUD masing-masing fitur.
- `rumah_ibadah/`, `penduduk_miskin/`, `log_bantuan/`
- `assets/` — File aset frontend.
- `css/` — Gaya UI (custom style.css).
- `js/` — Logika frontend peta dan pengelolaan interaksi panel data.
- `features/` — Modul fitur Leaflet (manajemen layer, geolokasi, dll).
- `index.php` — Halaman utama interface WebGIS.
- `database.sql` — Skema database beserta **data dummy** Pontianak untuk pengetesan awal. Seluruh data di dalamnya bersifat fiktif dan tidak merepresentasikan data nyata.
- `uploads/` — Folder penyimpanan foto yang diunggah. File yang sudah ada merupakan contoh dummy semata.
@@ -0,0 +1,56 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function isLoggedIn() {
return isset($_SESSION['user_id']);
}
function getCurrentUser() {
if (!isLoggedIn()) {
return null;
}
return [
'id' => $_SESSION['user_id'],
'username' => $_SESSION['username'],
'role' => $_SESSION['role'],
'ibadah_id' => $_SESSION['ibadah_id'] ? intval($_SESSION['ibadah_id']) : null
];
}
function requireLogin() {
if (!isLoggedIn()) {
header('HTTP/1.1 401 Unauthorized');
echo json_encode(['status' => 'error', 'message' => 'Unauthorized: Silakan login terlebih dahulu.']);
exit;
}
}
function requireAdmin() {
requireLogin();
if ($_SESSION['role'] !== 'admin') {
header('HTTP/1.1 403 Forbidden');
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Akses ditolak. Hanya Admin yang diizinkan.']);
exit;
}
}
function requireAdminOrPengelola() {
requireLogin();
if ($_SESSION['role'] !== 'admin' && $_SESSION['role'] !== 'pengelola') {
header('HTTP/1.1 403 Forbidden');
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Akses ditolak.']);
exit;
}
}
function canManageIbadah($ibadah_id) {
if (!isLoggedIn()) return false;
if ($_SESSION['role'] === 'admin') return true;
if ($_SESSION['role'] === 'pengelola') {
return intval($_SESSION['ibadah_id']) === intval($ibadah_id);
}
return false;
}
?>
@@ -0,0 +1,27 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (isset($_SESSION['user_id'])) {
echo json_encode([
'status' => 'success',
'isLoggedIn' => true,
'data' => [
'id' => intval($_SESSION['user_id']),
'username' => $_SESSION['username'],
'role' => $_SESSION['role'],
'ibadah_id' => $_SESSION['ibadah_id'] ? intval($_SESSION['ibadah_id']) : null,
'nama_ibadah' => $_SESSION['nama_ibadah'] ?? null
]
]);
} else {
echo json_encode([
'status' => 'success',
'isLoggedIn' => false,
'data' => null
]);
}
?>
@@ -0,0 +1,196 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireLogin();
include_once '../db_connect.php';
$user = getCurrentUser();
$isPengelola = $user['role'] === 'pengelola';
$ibadahId = $isPengelola ? intval($user['ibadah_id']) : null;
// ── Helper: Haversine distance in meters ──────────────────────────────────
function haversine($lat1, $lng1, $lat2, $lng2) {
$R = 6371000;
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat/2)*sin($dLat/2)
+ cos(deg2rad($lat1))*cos(deg2rad($lat2))*sin($dLng/2)*sin($dLng/2);
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
}
// ── 1. Rumah Ibadah ───────────────────────────────────────────────────────
if ($isPengelola && $ibadahId) {
$riRes = $conn->query("SELECT * FROM rumah_ibadah WHERE id=$ibadahId");
} else {
$riRes = $conn->query("SELECT * FROM rumah_ibadah");
}
$ibadahList = [];
while ($row = $riRes->fetch_assoc()) $ibadahList[] = $row;
$totalIbadah = count($ibadahList);
// Distribusi per jenis
$jenisDist = [];
foreach ($ibadahList as $ib) {
$j = $ib['jenis'] ?? 'Lainnya';
$jenisDist[$j] = ($jenisDist[$j] ?? 0) + 1;
}
// ── 2. Penduduk Miskin ────────────────────────────────────────────────────
$pmRes = $conn->query("SELECT * FROM penduduk_miskin");
$allMiskin = [];
while ($row = $pmRes->fetch_assoc()) $allMiskin[] = $row;
// Filter: pengelola hanya lihat yang dalam radius ibadahnya
if ($isPengelola && $ibadahId && !empty($ibadahList)) {
$ib = $ibadahList[0];
$filteredMiskin = array_filter($allMiskin, function($m) use ($ib) {
return haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius'];
});
$filteredMiskin = array_values($filteredMiskin);
} else {
$filteredMiskin = $allMiskin;
}
$totalMiskin = count($filteredMiskin);
$totalJiwa = array_sum(array_column($filteredMiskin, 'jumlah_jiwa'));
// Kategori bantuan
$kategoriDist = [];
foreach ($filteredMiskin as $m) {
$k = $m['kategori_bantuan'] ?? 'Lainnya';
$kategoriDist[$k] = ($kategoriDist[$k] ?? 0) + 1;
}
// Terurus vs Tidak (dalam radius salah satu ibadah)
$terurusCount = 0;
$tidakTerurusCount = 0;
foreach ($filteredMiskin as $m) {
$inAnyRadius = false;
foreach ($ibadahList as $ib) {
if (haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius']) {
$inAnyRadius = true;
break;
}
}
if ($inAnyRadius) $terurusCount++;
else $tidakTerurusCount++;
}
// Jika admin: hitung seluruh miskin dalam radius seluruh ibadah
if (!$isPengelola) {
$allIbadahRes = $conn->query("SELECT * FROM rumah_ibadah");
$allIbadahList = [];
while ($row = $allIbadahRes->fetch_assoc()) $allIbadahList[] = $row;
$terurusCount = 0;
$tidakTerurusCount = 0;
foreach ($allMiskin as $m) {
$inAnyRadius = false;
foreach ($allIbadahList as $ib) {
if (haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius']) {
$inAnyRadius = true;
break;
}
}
if ($inAnyRadius) $terurusCount++;
else $tidakTerurusCount++;
}
}
// ── 3. Log Bantuan ────────────────────────────────────────────────────────
if ($isPengelola && $ibadahId) {
$logRes = $conn->query("
SELECT lb.*, ri.nama as nama_ibadah, ri.jenis as jenis_ibadah, pm.nama as nama_kk
FROM log_bantuan lb
JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id
JOIN penduduk_miskin pm ON pm.id = lb.miskin_id
WHERE lb.ibadah_id = $ibadahId
ORDER BY lb.tanggal DESC, lb.id DESC
LIMIT 10
");
$totalLogRes = $conn->query("SELECT COUNT(*) as cnt FROM log_bantuan WHERE ibadah_id=$ibadahId");
} else {
$logRes = $conn->query("
SELECT lb.*, ri.nama as nama_ibadah, ri.jenis as jenis_ibadah, pm.nama as nama_kk
FROM log_bantuan lb
JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id
JOIN penduduk_miskin pm ON pm.id = lb.miskin_id
ORDER BY lb.tanggal DESC, lb.id DESC
LIMIT 10
");
$totalLogRes = $conn->query("SELECT COUNT(*) as cnt FROM log_bantuan");
}
$recentLogs = [];
while ($row = $logRes->fetch_assoc()) $recentLogs[] = $row;
$totalLog = 0;
if ($totalLogRes && $trow = $totalLogRes->fetch_assoc()) $totalLog = (int)$trow['cnt'];
// Distribusi tipe bantuan dari log
$tipeDist = [];
if ($isPengelola && $ibadahId) {
$tipeRes = $conn->query("SELECT tipe_bantuan, COUNT(*) as cnt FROM log_bantuan WHERE ibadah_id=$ibadahId GROUP BY tipe_bantuan");
} else {
$tipeRes = $conn->query("SELECT tipe_bantuan, COUNT(*) as cnt FROM log_bantuan GROUP BY tipe_bantuan");
}
while ($row = $tipeRes->fetch_assoc()) $tipeDist[$row['tipe_bantuan']] = (int)$row['cnt'];
// Bantuan per bulan (6 bulan terakhir)
if ($isPengelola && $ibadahId) {
$bulanRes = $conn->query("
SELECT DATE_FORMAT(tanggal, '%Y-%m') as bulan, COUNT(*) as cnt
FROM log_bantuan
WHERE ibadah_id=$ibadahId AND tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
GROUP BY bulan ORDER BY bulan ASC
");
} else {
$bulanRes = $conn->query("
SELECT DATE_FORMAT(tanggal, '%Y-%m') as bulan, COUNT(*) as cnt
FROM log_bantuan
WHERE tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
GROUP BY bulan ORDER BY bulan ASC
");
}
$bantuanPerBulan = [];
while ($row = $bulanRes->fetch_assoc()) $bantuanPerBulan[] = $row;
// ── 4. Ibadah Tipe (admin only: performa per ibadah) ─────────────────────
$ibadahPerforma = [];
if (!$isPengelola) {
$perfRes = $conn->query("
SELECT ri.id, ri.nama, ri.jenis, ri.radius,
COUNT(DISTINCT lb.id) as total_log,
COUNT(DISTINCT lb.miskin_id) as total_penerima
FROM rumah_ibadah ri
LEFT JOIN log_bantuan lb ON lb.ibadah_id = ri.id
GROUP BY ri.id
ORDER BY total_log DESC
");
while ($row = $perfRes->fetch_assoc()) $ibadahPerforma[] = $row;
}
// ── Output ────────────────────────────────────────────────────────────────
echo json_encode([
'status' => 'success',
'role' => $user['role'],
'ibadah_id' => $ibadahId,
'stats' => [
'total_ibadah' => $totalIbadah,
'total_miskin' => $totalMiskin,
'total_jiwa' => $totalJiwa,
'total_log' => $totalLog,
'terurus' => $terurusCount,
'tidak_terurus' => $tidakTerurusCount,
'pct_terurus' => $totalMiskin > 0 ? round($terurusCount / $totalMiskin * 100, 1) : 0,
],
'distribusi' => [
'jenis_ibadah' => $jenisDist,
'kategori' => $kategoriDist,
'tipe_bantuan' => $tipeDist,
'per_bulan' => $bantuanPerBulan,
],
'ibadah_list' => $ibadahList,
'ibadah_performa' => $ibadahPerforma,
'recent_logs' => $recentLogs,
]);
@@ -0,0 +1,171 @@
<?php
/**
* stats_extended.php
* Dashboard terintegrasi: webgis_poverty + jalan + spbu + penduduk choropleth
*/
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireLogin();
// ── Helper: buat koneksi terpisah ─────────────────────────────────────────
function makeConn($dbName) {
$c = new mysqli("localhost", "root", "", $dbName);
if ($c->connect_error) return null;
$c->set_charset("utf8mb4");
return $c;
}
// ── Koneksi ke masing-masing database ────────────────────────────────────
$connPoverty = makeConn("webgis_poverty"); // atau "webgis" sesuai db.sql
$connJalan = makeConn("jalan");
$connSpbu = makeConn("spbu");
// ── 1. Data dari webgis_poverty (kemiskinan) ─────────────────────────────
$poverty = [
'total_miskin' => 0,
'total_jiwa' => 0,
'total_ibadah' => 0,
'total_log' => 0,
'terurus' => 0,
'tidak_terurus' => 0,
'pct_terurus' => 0,
'distribusi' => ['kategori' => [], 'tipe_bantuan' => [], 'per_bulan' => []],
];
if ($connPoverty) {
// Total KK miskin & jiwa
$r = $connPoverty->query("SELECT COUNT(*) as c, COALESCE(SUM(jumlah_jiwa),0) as j FROM penduduk_miskin");
if ($r && $row = $r->fetch_assoc()) {
$poverty['total_miskin'] = (int)$row['c'];
$poverty['total_jiwa'] = (int)$row['j'];
}
// Total rumah ibadah
$r = $connPoverty->query("SELECT COUNT(*) as c FROM rumah_ibadah");
if ($r && $row = $r->fetch_assoc()) $poverty['total_ibadah'] = (int)$row['c'];
// Total log bantuan
$r = $connPoverty->query("SELECT COUNT(*) as c FROM log_bantuan");
if ($r && $row = $r->fetch_assoc()) $poverty['total_log'] = (int)$row['c'];
// Kategori bantuan
$r = $connPoverty->query("SELECT kategori_bantuan, COUNT(*) as c FROM penduduk_miskin GROUP BY kategori_bantuan");
if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['kategori'][$row['kategori_bantuan']] = (int)$row['c'];
// Tipe bantuan dari log
$r = $connPoverty->query("SELECT tipe_bantuan, COUNT(*) as c FROM log_bantuan GROUP BY tipe_bantuan");
if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['tipe_bantuan'][$row['tipe_bantuan']] = (int)$row['c'];
// Bantuan per bulan (6 bulan terakhir)
$r = $connPoverty->query("
SELECT DATE_FORMAT(tanggal,'%Y-%m') as bulan, COUNT(*) as cnt
FROM log_bantuan
WHERE tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
GROUP BY bulan ORDER BY bulan ASC
");
if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['per_bulan'][] = $row;
// Terurus vs tidak (radius sederhana: ada di radius salah satu ibadah)
$ibadahRes = $connPoverty->query("SELECT lat, lng, radius FROM rumah_ibadah");
$ibadahArr = [];
if ($ibadahRes) while ($row = $ibadahRes->fetch_assoc()) $ibadahArr[] = $row;
$miskinRes = $connPoverty->query("SELECT lat, lng FROM penduduk_miskin");
if ($miskinRes) {
while ($m = $miskinRes->fetch_assoc()) {
$inRadius = false;
foreach ($ibadahArr as $ib) {
$dlat = deg2rad($ib['lat'] - $m['lat']);
$dlng = deg2rad($ib['lng'] - $m['lng']);
$a = sin($dlat/2)**2 + cos(deg2rad($m['lat']))*cos(deg2rad($ib['lat']))*sin($dlng/2)**2;
$dist = 6371000 * 2 * atan2(sqrt($a), sqrt(1-$a));
if ($dist <= (float)$ib['radius']) { $inRadius = true; break; }
}
if ($inRadius) $poverty['terurus']++; else $poverty['tidak_terurus']++;
}
$tot = $poverty['total_miskin'];
$poverty['pct_terurus'] = $tot > 0 ? round($poverty['terurus'] / $tot * 100, 1) : 0;
}
$connPoverty->close();
}
// ── 2. Data Jalan ─────────────────────────────────────────────────────────
$jalan = [
'total_jalan' => 0,
'total_panjang_m' => 0,
'per_status' => [],
'total_parsil' => 0,
'total_luas_m2' => 0,
'per_kepemilikan' => [],
];
if ($connJalan) {
$r = $connJalan->query("SELECT COUNT(*) as c, COALESCE(SUM(panjang_meter),0) as p FROM jalan");
if ($r && $row = $r->fetch_assoc()) {
$jalan['total_jalan'] = (int)$row['c'];
$jalan['total_panjang_m'] = (float)$row['p'];
}
$r = $connJalan->query("SELECT status_jalan, COUNT(*) as c FROM jalan GROUP BY status_jalan");
if ($r) while ($row = $r->fetch_assoc()) $jalan['per_status'][$row['status_jalan']] = (int)$row['c'];
$r = $connJalan->query("SELECT COUNT(*) as c, COALESCE(SUM(luas_m2),0) as l FROM parsil_tanah");
if ($r && $row = $r->fetch_assoc()) {
$jalan['total_parsil'] = (int)$row['c'];
$jalan['total_luas_m2'] = (float)$row['l'];
}
$r = $connJalan->query("SELECT status_kepemilikan, COUNT(*) as c FROM parsil_tanah GROUP BY status_kepemilikan");
if ($r) while ($row = $r->fetch_assoc()) $jalan['per_kepemilikan'][$row['status_kepemilikan']] = (int)$row['c'];
$connJalan->close();
}
// ── 3. Data SPBU ──────────────────────────────────────────────────────────
$spbu = [
'total_spbu' => 0,
'buka_24_jam' => 0,
'tidak_24_jam' => 0,
'list' => [],
];
if ($connSpbu) {
$r = $connSpbu->query("SELECT COUNT(*) as c FROM spbu");
if ($r && $row = $r->fetch_assoc()) $spbu['total_spbu'] = (int)$row['c'];
$r = $connSpbu->query("SELECT SUM(buka_24_jam=1) as buka, SUM(buka_24_jam=0) as tidak FROM spbu");
if ($r && $row = $r->fetch_assoc()) {
$spbu['buka_24_jam'] = (int)$row['buka'];
$spbu['tidak_24_jam'] = (int)$row['tidak'];
}
$r = $connSpbu->query("SELECT nama_spbu, nomor_wa, buka_24_jam, latitude, longitude FROM spbu ORDER BY nama_spbu");
if ($r) while ($row = $r->fetch_assoc()) $spbu['list'][] = $row;
$connSpbu->close();
}
// ── 4. Data Penduduk (choropleth — static dari pontianak.json) ────────────
// Data kepadatan per kecamatan sudah ada di file JS, ringkasan di sini
$penduduk = [
'kecamatan' => [
['nama' => 'Pontianak Barat', 'density' => 9.331],
['nama' => 'Pontianak Kota', 'density' => 8.102],
['nama' => 'Pontianak Timur', 'density' => 9.196],
['nama' => 'Pontianak Selatan', 'density' => 5.673],
['nama' => 'Pontianak Utara', 'density' => 3.642],
['nama' => 'Pontianak Tenggara', 'density' => 3.103],
]
];
$penduduk['total_kecamatan'] = count($penduduk['kecamatan']);
$penduduk['rata_density'] = round(array_sum(array_column($penduduk['kecamatan'],'density')) / $penduduk['total_kecamatan'], 3);
$penduduk['terpadat'] = $penduduk['kecamatan'][array_search(max(array_column($penduduk['kecamatan'],'density')), array_column($penduduk['kecamatan'],'density'))]['nama'];
// ── Output ────────────────────────────────────────────────────────────────
echo json_encode([
'status' => 'success',
'poverty' => $poverty,
'jalan' => $jalan,
'spbu' => $spbu,
'penduduk' => $penduduk,
], JSON_UNESCAPED_UNICODE);
@@ -0,0 +1,17 @@
<?php
$host = "localhost";
$user = "root";
$pass = "";
$db = "webgis";
// Coba koneksi ke server dan pilih database
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die("Koneksi gagal: " . $conn->connect_error);
}
// Set charset
$conn->set_charset("utf8mb4");
// Jika di-include oleh file API, biarkan $conn tersedia
?>
@@ -0,0 +1,37 @@
<?php
header('Content-Type: application/json');
require_once '../auth_helper.php';
requireAdminOrPengelola();
require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
$miskin_id = intval($data['miskin_id'] ?? 0);
$ibadah_id = intval($data['ibadah_id'] ?? 0);
$tipe_bantuan = trim($data['tipe_bantuan'] ?? '');
$tanggal = trim($data['tanggal'] ?? '');
$keterangan = trim($data['keterangan'] ?? '');
if ($_SESSION['role'] === 'pengelola') {
if ($ibadah_id !== intval($_SESSION['ibadah_id'])) {
header('HTTP/1.1 403 Forbidden');
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Anda tidak diizinkan membuat log untuk rumah ibadah lain.']);
exit;
}
}
if (!$miskin_id || !$ibadah_id || !$tipe_bantuan || !$tanggal) {
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
exit;
}
$sql = "INSERT INTO log_bantuan (miskin_id, ibadah_id, tipe_bantuan, tanggal, keterangan) VALUES (?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param('iisss', $miskin_id, $ibadah_id, $tipe_bantuan, $tanggal, $keterangan);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'id' => $stmt->insert_id]);
} else {
echo json_encode(['status' => 'error', 'message' => $conn->error]);
}
$conn->close();
@@ -0,0 +1,37 @@
<?php
header('Content-Type: application/json');
require_once '../db_connect.php';
$miskin_id = isset($_GET['miskin_id']) ? intval($_GET['miskin_id']) : 0;
if (!$miskin_id) {
echo json_encode(['status' => 'error', 'message' => 'miskin_id diperlukan']);
exit;
}
$sql = "
SELECT
lb.id,
lb.tipe_bantuan,
lb.tanggal,
lb.keterangan,
ri.nama AS nama_ibadah,
ri.jenis AS jenis_ibadah
FROM log_bantuan lb
JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id
WHERE lb.miskin_id = ?
ORDER BY lb.tanggal DESC, lb.id DESC
";
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $miskin_id);
$stmt->execute();
$result = $stmt->get_result();
$data = [];
while ($row = $result->fetch_assoc()) {
$data[] = $row;
}
echo json_encode(['status' => 'success', 'data' => $data]);
$conn->close();
+55
View File
@@ -0,0 +1,55 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once 'db_connect.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
if (empty($username) || empty($password)) {
echo json_encode(['status' => 'error', 'message' => 'Username dan password wajib diisi']);
exit;
}
// Cari user di database
$sql = "SELECT u.*, ri.nama as nama_ibadah
FROM users u
LEFT JOIN rumah_ibadah ri ON u.ibadah_id = ri.id
WHERE u.username = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param('s', $username);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 1) {
$user = $result->fetch_assoc();
// Verifikasi password
if (password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['ibadah_id'] = $user['ibadah_id'];
$_SESSION['nama_ibadah'] = $user['nama_ibadah'];
echo json_encode([
'status' => 'success',
'message' => 'Login berhasil',
'data' => [
'id' => intval($user['id']),
'username' => $user['username'],
'role' => $user['role'],
'ibadah_id' => $user['ibadah_id'] ? intval($user['ibadah_id']) : null,
'nama_ibadah' => $user['nama_ibadah']
]
]);
exit;
}
}
echo json_encode(['status' => 'error', 'message' => 'Username atau password salah']);
$conn->close();
?>
+21
View File
@@ -0,0 +1,21 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION = array();
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
session_destroy();
echo json_encode(['status' => 'success', 'message' => 'Logout berhasil']);
?>
@@ -0,0 +1,83 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdminOrPengelola();
include_once '../db_connect.php';
// Ambil info ibadah pengelola jika role pengelola
$currentUser = getCurrentUser();
$ibadahFilter = null; // null = admin (tidak dibatasi)
if ($currentUser && $currentUser['role'] === 'pengelola') {
$ibadah_id = $currentUser['ibadah_id'];
if (!$ibadah_id) {
echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']);
exit;
}
$res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id");
if (!$res || $res->num_rows === 0) {
echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']);
exit;
}
$ibadahFilter = $res->fetch_assoc();
}
$data = json_decode(file_get_contents("php://input"));
if (is_array($data)) {
$conn->begin_transaction();
$inserted = 0;
$skipped = 0;
$errors = [];
foreach ($data as $item) {
if (isset($item->lat) && isset($item->lng) && $item->lat !== '' && $item->lng !== '') {
$nama = $conn->real_escape_string($item->nama ?? '');
$kategori = $conn->real_escape_string($item->kategori_bantuan ?? 'Makan');
$jumlah_jiwa = (int)($item->jumlah_jiwa ?? 1);
$lat = (float)$item->lat;
$lng = (float)$item->lng;
// Jika pengelola, cek apakah item berada dalam radius ibadahnya
if ($ibadahFilter !== null) {
$R = 6371000;
$lat1 = deg2rad((float)$ibadahFilter['lat']);
$lat2 = deg2rad($lat);
$dLat = $lat2 - $lat1;
$dLng = deg2rad($lng - (float)$ibadahFilter['lng']);
$a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2);
$dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a));
if ($dist > (float)$ibadahFilter['radius']) {
$skipped++;
continue; // Lewati item di luar radius
}
}
$query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng)
VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng)";
if (!$conn->query($query)) {
$errors[] = $conn->error;
} else {
$inserted++;
}
}
}
if (empty($errors) && $inserted > 0) {
$conn->commit();
$msg = "Berhasil mengimpor $inserted data penduduk miskin.";
if ($skipped > 0) $msg .= " ($skipped data di luar radius diabaikan.)";
echo json_encode(["status" => "success", "message" => $msg]);
} else if ($inserted === 0) {
$conn->rollback();
$msg = $skipped > 0
? "Semua $skipped data berada di luar radius rumah ibadah Anda, tidak ada yang diimpor."
: "Tidak ada data valid yang diimpor.";
echo json_encode(["status" => "error", "message" => $msg]);
} else {
$conn->rollback();
echo json_encode(["status" => "error", "message" => "Gagal mengimpor beberapa data. Transaksi dibatalkan.", "errors" => $errors]);
}
} else {
echo json_encode(["status" => "error", "message" => "Format data salah."]);
}
?>
@@ -0,0 +1,109 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdminOrPengelola();
include_once '../db_connect.php';
$nama = '';
$kategori = 'Makan';
$jumlah_jiwa = 1;
$lat = 0.0;
$lng = 0.0;
if (!empty($_POST)) {
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
$kategori = $conn->real_escape_string($_POST['kategori_bantuan'] ?? 'Makan');
$jumlah_jiwa = (int)($_POST['jumlah_jiwa'] ?? 1);
$lat = (float)($_POST['lat'] ?? 0);
$lng = (float)($_POST['lng'] ?? 0);
} else {
$data = json_decode(file_get_contents("php://input"));
if ($data) {
$nama = $conn->real_escape_string($data->nama ?? '');
$kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan');
$jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1);
$lat = (float)($data->lat ?? 0);
$lng = (float)($data->lng ?? 0);
}
}
// Validasi pengelola: hanya boleh tambah jika lokasi berada dalam radius ibadah yang dikelolanya
$currentUser = getCurrentUser();
if ($currentUser && $currentUser['role'] === 'pengelola') {
$ibadah_id = $currentUser['ibadah_id'];
if (!$ibadah_id) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']);
exit;
}
// Ambil data ibadah (lat, lng, radius)
$res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id");
if (!$res || $res->num_rows === 0) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']);
exit;
}
$ib = $res->fetch_assoc();
// Hitung jarak menggunakan Haversine formula
$R = 6371000; // meter
$lat1 = deg2rad((float)$ib['lat']);
$lat2 = deg2rad($lat);
$dLat = $lat2 - $lat1;
$dLng = deg2rad($lng - (float)$ib['lng']);
$a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2);
$dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a));
if ($dist > (float)$ib['radius']) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Lokasi penduduk di luar radius rumah ibadah Anda. Hanya boleh menambah penduduk dalam radius yang dikelola.']);
exit;
}
}
if ($lat != 0 && $lng != 0) {
$upload_dir = "../../uploads/";
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$foto_rumah = null;
$foto_kk = null;
function uploadFile($fileKey, $upload_dir) {
if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) {
$fileTmpPath = $_FILES[$fileKey]['tmp_name'];
$fileName = $_FILES[$fileKey]['name'];
$fileNameCmps = explode(".", $fileName);
$fileExtension = strtolower(end($fileNameCmps));
$allowedfileExtensions = array('jpg', 'jpeg', 'png', 'gif');
if (in_array($fileExtension, $allowedfileExtensions)) {
$newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\._-]/', '_', $fileName);
$dest_path = $upload_dir . $newFileName;
if (move_uploaded_file($fileTmpPath, $dest_path)) {
return $newFileName;
}
}
}
return null;
}
$foto_rumah = uploadFile('foto_rumah', $upload_dir);
$foto_kk = uploadFile('foto_kk', $upload_dir);
$val_foto_rumah = $foto_rumah ? "'$foto_rumah'" : "NULL";
$val_foto_kk = $foto_kk ? "'$foto_kk'" : "NULL";
$query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng, foto_rumah, foto_kk)
VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng, $val_foto_rumah, $val_foto_kk)";
if ($conn->query($query)) {
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Berhasil ditambahkan."]);
} else {
echo json_encode(["status" => "error", "message" => "Gagal: " . $conn->error]);
}
} else {
echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]);
}
?>
@@ -0,0 +1,65 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdminOrPengelola();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->id)) {
$id = (int)$data->id;
// Validasi pengelola: hanya boleh hapus penduduk dalam radius ibadah yang dikelolanya
$currentUser = getCurrentUser();
if ($currentUser && $currentUser['role'] === 'pengelola') {
$ibadah_id = $currentUser['ibadah_id'];
if (!$ibadah_id) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']);
exit;
}
// Ambil koordinat penduduk
$mRes = $conn->query("SELECT lat, lng FROM penduduk_miskin WHERE id=$id");
if (!$mRes || $mRes->num_rows === 0) {
echo json_encode(['status' => 'error', 'message' => 'Data penduduk tidak ditemukan.']);
exit;
}
$mRow = $mRes->fetch_assoc();
$pLat = (float)$mRow['lat'];
$pLng = (float)$mRow['lng'];
// Ambil data ibadah
$iRes = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id");
if (!$iRes || $iRes->num_rows === 0) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']);
exit;
}
$ib = $iRes->fetch_assoc();
// Hitung jarak Haversine
$R = 6371000;
$lat1 = deg2rad((float)$ib['lat']);
$lat2 = deg2rad($pLat);
$dLat = $lat2 - $lat1;
$dLng = deg2rad($pLng - (float)$ib['lng']);
$a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2);
$dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a));
if ($dist > (float)$ib['radius']) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Penduduk ini berada di luar radius rumah ibadah Anda. Tidak diizinkan menghapus.']);
exit;
}
}
$query = "DELETE FROM penduduk_miskin WHERE id=$id";
if($conn->query($query)) {
echo json_encode(["status" => "success", "message" => "Berhasil dihapus."]);
} else {
echo json_encode(["status" => "error", "message" => "Gagal hapus: " . $conn->error]);
}
} else {
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
}
?>
@@ -0,0 +1,28 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT * FROM penduduk_miskin";
$result = $conn->query($query);
$arr = array();
if($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$item = array(
"id" => $row['id'],
"nama" => $row['nama'],
"kategori_bantuan" => $row['kategori_bantuan'],
"jumlah_jiwa" => isset($row['jumlah_jiwa']) ? (int)$row['jumlah_jiwa'] : 1,
"lat" => (float)$row['lat'],
"lng" => (float)$row['lng'],
"foto_rumah" => $row['foto_rumah'],
"foto_kk" => $row['foto_kk']
);
array_push($arr, $item);
}
echo json_encode(["status" => "success", "data" => $arr]);
} else {
echo json_encode(["status" => "success", "data" => []]);
}
?>
@@ -0,0 +1,148 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdminOrPengelola();
include_once '../db_connect.php';
$id = 0;
$nama = '';
$kategori = 'Makan';
$jumlah_jiwa = 1;
$lat = 0.0;
$lng = 0.0;
if (!empty($_POST)) {
$id = (int)($_POST['id'] ?? 0);
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
$kategori = $conn->real_escape_string($_POST['kategori_bantuan'] ?? 'Makan');
$jumlah_jiwa = (int)($_POST['jumlah_jiwa'] ?? 1);
$lat = (float)($_POST['lat'] ?? 0);
$lng = (float)($_POST['lng'] ?? 0);
} else {
$data = json_decode(file_get_contents("php://input"));
if ($data) {
$id = (int)($data->id ?? 0);
$nama = $conn->real_escape_string($data->nama ?? '');
$kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan');
$jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1);
$lat = (float)($data->lat ?? 0);
$lng = (float)($data->lng ?? 0);
}
}
// Validasi pengelola: hanya boleh edit penduduk yang lokasinya dalam radius ibadah yang dikelolanya
$currentUser = getCurrentUser();
if ($currentUser && $currentUser['role'] === 'pengelola') {
$ibadah_id = $currentUser['ibadah_id'];
if (!$ibadah_id) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']);
exit;
}
$res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id");
if (!$res || $res->num_rows === 0) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']);
exit;
}
$ib = $res->fetch_assoc();
// Gunakan koordinat penduduk yang sedang diedit (dari DB jika lat/lng 0, atau dari input)
$checkLat = $lat;
$checkLng = $lng;
if ($checkLat == 0 && $checkLng == 0 && $id > 0) {
$mRes = $conn->query("SELECT lat, lng FROM penduduk_miskin WHERE id=$id");
if ($mRes && $mRow = $mRes->fetch_assoc()) {
$checkLat = (float)$mRow['lat'];
$checkLng = (float)$mRow['lng'];
}
}
$R = 6371000;
$lat1 = deg2rad((float)$ib['lat']);
$lat2 = deg2rad($checkLat);
$dLat = $lat2 - $lat1;
$dLng = deg2rad($checkLng - (float)$ib['lng']);
$a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2);
$dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a));
if ($dist > (float)$ib['radius']) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Penduduk ini berada di luar radius rumah ibadah Anda. Tidak diizinkan mengedit.']);
exit;
}
}
if ($id > 0) {
// Get current files to delete them if updated
$old_foto_rumah = null;
$old_foto_kk = null;
$res = $conn->query("SELECT foto_rumah, foto_kk FROM penduduk_miskin WHERE id=$id");
if ($res && $row = $res->fetch_assoc()) {
$old_foto_rumah = $row['foto_rumah'];
$old_foto_kk = $row['foto_kk'];
}
$upload_dir = "../../uploads/";
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$foto_rumah = null;
$foto_kk = null;
function uploadFile($fileKey, $upload_dir) {
if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) {
$fileTmpPath = $_FILES[$fileKey]['tmp_name'];
$fileName = $_FILES[$fileKey]['name'];
$fileNameCmps = explode(".", $fileName);
$fileExtension = strtolower(end($fileNameCmps));
$allowedfileExtensions = array('jpg', 'jpeg', 'png', 'gif');
if (in_array($fileExtension, $allowedfileExtensions)) {
$newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\._-]/', '_', $fileName);
$dest_path = $upload_dir . $newFileName;
if (move_uploaded_file($fileTmpPath, $dest_path)) {
return $newFileName;
}
}
}
return null;
}
$foto_rumah = uploadFile('foto_rumah', $upload_dir);
$foto_kk = uploadFile('foto_kk', $upload_dir);
// Build update fields
$update_fields = [
"nama='$nama'",
"kategori_bantuan='$kategori'",
"jumlah_jiwa=$jumlah_jiwa",
"lat=$lat",
"lng=$lng"
];
if ($foto_rumah !== null) {
$update_fields[] = "foto_rumah='$foto_rumah'";
if ($old_foto_rumah && file_exists($upload_dir . $old_foto_rumah)) {
unlink($upload_dir . $old_foto_rumah);
}
}
if ($foto_kk !== null) {
$update_fields[] = "foto_kk='$foto_kk'";
if ($old_foto_kk && file_exists($upload_dir . $old_foto_kk)) {
unlink($upload_dir . $old_foto_kk);
}
}
$query = "UPDATE penduduk_miskin SET " . implode(", ", $update_fields) . " WHERE id=$id";
if ($conn->query($query)) {
echo json_encode(["status" => "success", "message" => "Update berhasil."]);
} else {
echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]);
}
} else {
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
}
?>
@@ -0,0 +1,28 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if (!empty($data->lat) && !empty($data->lng)) {
$nama = $conn->real_escape_string($data->nama ?? '');
$jenis = $conn->real_escape_string($data->jenis ?? 'Masjid');
$alamat = $conn->real_escape_string($data->alamat ?? '');
$radius = (float)($data->radius ?? 500);
$lat = (float)$data->lat;
$lng = (float)$data->lng;
$query = "INSERT INTO rumah_ibadah (nama, jenis, alamat, radius, lat, lng)
VALUES ('$nama', '$jenis', '$alamat', $radius, $lat, $lng)";
if ($conn->query($query)) {
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Rumah Ibadah berhasil ditambahkan."]);
} else {
echo json_encode(["status" => "error", "message" => "Gagal menambahkan: " . $conn->error]);
}
} else {
echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]);
}
?>
@@ -0,0 +1,22 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->id)) {
$id = (int)$data->id;
$query = "DELETE FROM rumah_ibadah WHERE id=$id";
if($conn->query($query)) {
echo json_encode(["status" => "success", "message" => "Berhasil dihapus."]);
} else {
echo json_encode(["status" => "error", "message" => "Gagal hapus: " . $conn->error]);
}
} else {
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
}
?>
@@ -0,0 +1,27 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT * FROM rumah_ibadah";
$result = $conn->query($query);
$arr = array();
if($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$item = array(
"id" => $row['id'],
"nama" => $row['nama'],
"jenis" => $row['jenis'],
"alamat" => $row['alamat'],
"radius" => (float)$row['radius'],
"lat" => (float)$row['lat'],
"lng" => (float)$row['lng']
);
array_push($arr, $item);
}
echo json_encode(["status" => "success", "data" => $arr]);
} else {
echo json_encode(["status" => "success", "data" => []]);
}
?>
@@ -0,0 +1,30 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if (!empty($data->id)) {
$id = (int)$data->id;
$nama = $conn->real_escape_string($data->nama ?? '');
$jenis = $conn->real_escape_string($data->jenis ?? 'Masjid');
$alamat = $conn->real_escape_string($data->alamat ?? '');
$radius = (float)($data->radius ?? 500);
$lat = (float)$data->lat;
$lng = (float)$data->lng;
$query = "UPDATE rumah_ibadah
SET nama='$nama', jenis='$jenis', alamat='$alamat', radius=$radius, lat=$lat, lng=$lng
WHERE id=$id";
if ($conn->query($query)) {
echo json_encode(["status" => "success", "message" => "Update berhasil."]);
} else {
echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]);
}
} else {
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
}
?>
@@ -0,0 +1,47 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireAdmin();
require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
$role = trim($data['role'] ?? 'pengelola');
$ibadah_id = !empty($data['ibadah_id']) ? intval($data['ibadah_id']) : null;
if (empty($username) || empty($password)) {
echo json_encode(['status' => 'error', 'message' => 'Username dan password wajib diisi']);
exit;
}
if ($role !== 'admin' && $role !== 'pengelola') {
echo json_encode(['status' => 'error', 'message' => 'Role tidak valid']);
exit;
}
// Cek apakah username sudah ada
$stmt = $conn->prepare("SELECT id FROM users WHERE username = ?");
$stmt->bind_param('s', $username);
$stmt->execute();
if ($stmt->get_result()->num_rows > 0) {
echo json_encode(['status' => 'error', 'message' => 'Username sudah digunakan']);
exit;
}
// Hash password
$hashed_password = password_hash($password, PASSWORD_BCRYPT);
// Insert user baru
$stmt = $conn->prepare("INSERT INTO users (username, password, role, ibadah_id) VALUES (?, ?, ?, ?)");
$stmt->bind_param('sssi', $username, $hashed_password, $role, $ibadah_id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'User berhasil ditambahkan', 'id' => $stmt->insert_id]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menambahkan user: ' . $conn->error]);
}
$conn->close();
?>
@@ -0,0 +1,30 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireAdmin();
require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
$id = intval($data['id'] ?? 0);
if (!$id) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
exit;
}
if ($id === intval($_SESSION['user_id'])) {
echo json_encode(['status' => 'error', 'message' => 'Anda tidak bisa menghapus akun Anda sendiri']);
exit;
}
$stmt = $conn->prepare("DELETE FROM users WHERE id = ?");
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'User berhasil dihapus']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus user: ' . $conn->error]);
}
$conn->close();
?>
@@ -0,0 +1,31 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireAdmin();
require_once '../db_connect.php';
$sql = "SELECT u.id, u.username, u.role, u.ibadah_id, ri.nama as nama_ibadah
FROM users u
LEFT JOIN rumah_ibadah ri ON u.ibadah_id = ri.id
ORDER BY u.id DESC";
$result = $conn->query($sql);
$arr = array();
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$item = array(
"id" => intval($row['id']),
"username" => $row['username'],
"role" => $row['role'],
"ibadah_id" => $row['ibadah_id'] ? intval($row['ibadah_id']) : null,
"nama_ibadah" => $row['nama_ibadah'] ?? '-'
);
array_push($arr, $item);
}
echo json_encode(["status" => "success", "data" => $arr]);
} else {
echo json_encode(["status" => "success", "data" => []]);
}
$conn->close();
?>
@@ -0,0 +1,52 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireAdmin();
require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
$id = intval($data['id'] ?? 0);
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
$role = trim($data['role'] ?? 'pengelola');
$ibadah_id = !empty($data['ibadah_id']) ? intval($data['ibadah_id']) : null;
if (!$id || empty($username)) {
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
exit;
}
if ($role !== 'admin' && $role !== 'pengelola') {
echo json_encode(['status' => 'error', 'message' => 'Role tidak valid']);
exit;
}
// Cek apakah username sudah ada di user lain
$stmt = $conn->prepare("SELECT id FROM users WHERE username = ? AND id != ?");
$stmt->bind_param('si', $username, $id);
$stmt->execute();
if ($stmt->get_result()->num_rows > 0) {
echo json_encode(['status' => 'error', 'message' => 'Username sudah digunakan oleh user lain']);
exit;
}
if (!empty($password)) {
// Hash new password
$hashed_password = password_hash($password, PASSWORD_BCRYPT);
$stmt = $conn->prepare("UPDATE users SET username = ?, password = ?, role = ?, ibadah_id = ? WHERE id = ?");
$stmt->bind_param('ssssi', $username, $hashed_password, $role, $ibadah_id, $id);
} else {
// Keep existing password
$stmt = $conn->prepare("UPDATE users SET username = ?, role = ?, ibadah_id = ? WHERE id = ?");
$stmt->bind_param('sssi', $username, $role, $ibadah_id, $id);
}
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'User berhasil diperbarui']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal memperbarui user: ' . $conn->error]);
}
$conn->close();
?>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,383 @@
// --- Fitur Autentikasi dan Manajemen User (RBAC) ---
(function() {
window.currentUser = null;
// Elements
const authWidget = document.getElementById('authWidget');
const loginModal = document.getElementById('loginModal');
const loginUsernameInput = document.getElementById('loginUsername');
const loginPasswordInput = document.getElementById('loginPassword');
const loginSubmitBtn = document.getElementById('loginSubmitBtn');
const loginErrorMsg = document.getElementById('loginErrorMsg');
const closeLoginModal = document.getElementById('closeLoginModal');
// User Management Modal Elements
const userManagementModal = document.getElementById('userManagementModal');
const closeUserManagementModal = document.getElementById('closeUserManagementModal');
const menuUsersBtn = document.getElementById('menuUsers');
const userTableBody = document.getElementById('userTableBody');
const btnSaveUser = document.getElementById('btnSaveUser');
const btnCancelUserEdit = document.getElementById('btnCancelUserEdit');
const userFormTitle = document.getElementById('userFormTitle');
const manageUserId = document.getElementById('manageUserId');
const manageUsername = document.getElementById('manageUsername');
const managePassword = document.getElementById('managePassword');
const manageRole = document.getElementById('manageRole');
const manageIbadahId = document.getElementById('manageIbadahId');
const manageIbadahGroup = document.getElementById('manageIbadahGroup');
// Startup Session Check
function checkSession() {
fetch('api/check_session.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.isLoggedIn) {
window.currentUser = data.data;
} else {
window.currentUser = null;
}
updateAuthUI();
refreshAllLayers();
})
.catch(err => {
console.error("Session check failed:", err);
window.currentUser = null;
updateAuthUI();
});
}
// Update UI based on logged-in state
function updateAuthUI() {
if (!authWidget) return;
const dashboardBtn = document.getElementById('menuDashboard');
if (window.currentUser) {
// Logged In state
const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola';
const extraLabel = window.currentUser.nama_ibadah ? ` - ${window.currentUser.nama_ibadah}` : '';
authWidget.innerHTML = `
<div class="user-profile-pill">
<div class="user-profile-info">
<div class="user-profile-name">👤 ${escHtml(window.currentUser.username)}</div>
<div class="user-profile-role">${roleLabel}${escHtml(extraLabel)}</div>
</div>
<button class="btn-profile-logout" id="authLogoutBtn" title="Logout">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
`;
document.getElementById('authLogoutBtn').addEventListener('click', logout);
// Show Dashboard button for all logged-in users
if (dashboardBtn) dashboardBtn.style.display = 'flex';
// Show user management button in sidebar for Admin
const adminDivider = document.getElementById('sidebarAdminDivider');
if (window.currentUser.role === 'admin') {
if (menuUsersBtn) menuUsersBtn.style.display = 'flex';
if (adminDivider) adminDivider.style.display = 'block';
} else {
if (menuUsersBtn) menuUsersBtn.style.display = 'none';
if (adminDivider) adminDivider.style.display = 'none';
}
} else {
// Logged Out state
authWidget.innerHTML = `
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
`;
document.getElementById('authLoginBtn').addEventListener('click', showLoginModal);
if (menuUsersBtn) menuUsersBtn.style.display = 'none';
if (dashboardBtn) dashboardBtn.style.display = 'none';
const adminDivider = document.getElementById('sidebarAdminDivider');
if (adminDivider) adminDivider.style.display = 'none';
}
}
function refreshAllLayers() {
if (typeof loadSpbu === 'function') loadSpbu();
if (typeof loadJalan === 'function') loadJalan();
if (typeof loadParsil === 'function') loadParsil();
if (typeof loadRumahIbadah === 'function') loadRumahIbadah();
if (typeof loadPendudukMiskin === 'function') loadPendudukMiskin();
if (window.refreshActivePanel) window.refreshActivePanel();
}
// Login Modals logic
function showLoginModal() {
loginUsernameInput.value = '';
loginPasswordInput.value = '';
loginErrorMsg.style.display = 'none';
loginModal.classList.add('show');
}
function hideLoginModal() {
loginModal.classList.remove('show');
}
if (closeLoginModal) {
closeLoginModal.addEventListener('click', hideLoginModal);
}
loginSubmitBtn.addEventListener('click', function() {
const username = loginUsernameInput.value.trim();
const password = loginPasswordInput.value;
if (!username || !password) {
loginErrorMsg.textContent = 'Username dan password wajib diisi';
loginErrorMsg.style.display = 'block';
return;
}
fetch('api/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
window.currentUser = data.data;
hideLoginModal();
updateAuthUI();
refreshAllLayers();
showToast('Login berhasil', 'success');
} else {
loginErrorMsg.textContent = data.message || 'Login gagal';
loginErrorMsg.style.display = 'block';
}
})
.catch(err => {
console.error(err);
loginErrorMsg.textContent = 'Koneksi ke server terputus';
loginErrorMsg.style.display = 'block';
});
});
function logout() {
fetch('api/logout.php')
.then(res => res.json())
.then(data => {
window.currentUser = null;
updateAuthUI();
refreshAllLayers();
showToast('Logout berhasil', 'success');
})
.catch(err => {
console.error(err);
showToast('Gagal logout', 'error');
});
}
// User Management Modal Logic
if (menuUsersBtn) {
menuUsersBtn.addEventListener('click', function() {
userManagementModal.classList.add('show');
loadUsers();
populateIbadahDropdown();
resetUserForm();
});
}
// Dashboard Button
const menuDashboardBtn = document.getElementById('menuDashboard');
if (menuDashboardBtn) {
menuDashboardBtn.addEventListener('click', function() {
if (typeof window.openDashboard === 'function') window.openDashboard();
});
}
if (closeUserManagementModal) {
closeUserManagementModal.addEventListener('click', function() {
userManagementModal.classList.remove('show');
});
}
// Show/hide ibadah dropdown based on selected role in form
if (manageRole) {
manageRole.addEventListener('change', function() {
if (this.value === 'admin') {
manageIbadahGroup.style.display = 'none';
manageIbadahId.value = '';
} else {
manageIbadahGroup.style.display = 'flex';
}
});
}
function populateIbadahDropdown() {
if (!manageIbadahId) return;
manageIbadahId.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>';
if (typeof ibadahDataList !== 'undefined') {
ibadahDataList.forEach(ib => {
const opt = document.createElement('option');
opt.value = ib.id;
opt.textContent = `${ib.jenis || '🕌'} ${ib.nama}`;
manageIbadahId.appendChild(opt);
});
}
}
function loadUsers() {
if (!userTableBody) return;
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#aaa;">Memuat user...</td></tr>';
fetch('api/users/read.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
userTableBody.innerHTML = '';
if (data.data.length === 0) {
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#aaa;">Belum ada user</td></tr>';
return;
}
data.data.forEach(user => {
const tr = document.createElement('tr');
tr.style.borderBottom = '1px solid #e2e8f0';
const roleBadge = user.role === 'admin'
? '<span class="data-card-badge badge-red">Admin</span>'
: '<span class="data-card-badge badge-blue">Pengelola</span>';
tr.innerHTML = `
<td style="padding:10px 12px; font-weight:600;">${escHtml(user.username)}</td>
<td style="padding:10px 12px;">${roleBadge}</td>
<td style="padding:10px 12px; color:#64748b;">${escHtml(user.nama_ibadah)}</td>
<td style="padding:10px 12px; text-align:center; display:flex; gap:6px; justify-content:center;">
<button class="btn-card-action btn-card-edit" title="Edit User" onclick="editUser(${JSON.stringify(user).replace(/"/g, '&quot;')})">
<i class="fas fa-pen"></i>
</button>
<button class="btn-card-action btn-card-delete" title="Hapus User" onclick="deleteUser(${user.id})">
<i class="fas fa-trash"></i>
</button>
</td>
`;
userTableBody.appendChild(tr);
});
}
})
.catch(err => {
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#ef4444;">Gagal memuat user</td></tr>';
});
}
window.editUser = function(user) {
userFormTitle.textContent = 'Edit Pengguna';
manageUserId.value = user.id;
manageUsername.value = user.username;
managePassword.value = ''; // Password cleared in edit
manageRole.value = user.role;
if (user.role === 'admin') {
manageIbadahGroup.style.display = 'none';
manageIbadahId.value = '';
} else {
manageIbadahGroup.style.display = 'flex';
manageIbadahId.value = user.ibadah_id || '';
}
btnCancelUserEdit.style.display = 'inline-block';
};
window.deleteUser = function(id) {
if (confirm('Apakah Anda yakin ingin menghapus user ini?')) {
fetch('api/users/delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
showToast(data.message, 'success');
loadUsers();
resetUserForm();
} else {
showToast(data.message || 'Gagal menghapus user', 'error');
}
})
.catch(err => {
showToast('Koneksi terputus', 'error');
});
}
};
btnSaveUser.addEventListener('click', function() {
const id = manageUserId.value;
const username = manageUsername.value.trim();
const password = managePassword.value;
const role = manageRole.value;
const ibadah_id = manageIbadahId.value;
if (!username) {
alert('Username wajib diisi');
return;
}
if (!id && !password) {
alert('Password wajib diisi untuk user baru');
return;
}
const payload = { username, role, ibadah_id };
if (id) payload.id = id;
if (password) payload.password = password;
const endpoint = id ? 'api/users/update.php' : 'api/users/create.php';
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
showToast(data.message, 'success');
loadUsers();
resetUserForm();
} else {
alert(data.message || 'Gagal menyimpan user');
}
})
.catch(err => {
alert('Koneksi terputus');
});
});
btnCancelUserEdit.addEventListener('click', resetUserForm);
function resetUserForm() {
userFormTitle.textContent = 'Tambah User Baru';
manageUserId.value = '';
manageUsername.value = '';
managePassword.value = '';
manageRole.value = 'pengelola';
manageIbadahGroup.style.display = 'flex';
manageIbadahId.value = '';
btnCancelUserEdit.style.display = 'none';
}
// Helper functions
function escHtml(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Toast Notification helper
function showToast(message, type = 'success') {
if (typeof window.showToast === 'function') {
window.showToast(message, type);
return;
}
// Fallback alert
alert(message);
}
// Run session check on initialization
checkSession();
})();
@@ -0,0 +1,468 @@
// ===== Dashboard Feature (Terintegrasi) =====
// Menghubungkan: webgis-poverty-pontianak, jalan, penduduk, spbu
(function () {
'use strict';
// ── State ──────────────────────────────────────────────────────────────
let dashboardData = null;
let activeTab = 'kemiskinan';
// ── Modal Element ──────────────────────────────────────────────────────
const modal = document.getElementById('dashboardModal');
if (!modal) return;
// ── Open / Close ───────────────────────────────────────────────────────
window.openDashboard = function () {
modal.classList.add('show');
if (!dashboardData) loadDashboard();
else renderTab(activeTab);
};
window.closeDashboard = function () {
modal.classList.remove('show');
};
document.getElementById('closeDashboardModal')?.addEventListener('click', window.closeDashboard);
modal.addEventListener('click', function (e) {
if (e.target === modal) window.closeDashboard();
});
// ── Load Data ──────────────────────────────────────────────────────────
function loadDashboard() {
const body = document.getElementById('dashboardBody');
body.innerHTML = `
<div class="db-loading">
<div class="db-spinner"></div>
<span>Memuat data dashboard terintegrasi…</span>
</div>`;
fetch('api/dashboard/stats_extended.php')
.then(r => r.json())
.then(data => {
if (data.status !== 'success') throw new Error(data.message || 'Gagal memuat');
dashboardData = data;
renderShell();
renderTab(activeTab);
})
.catch(err => {
body.innerHTML = `<div class="db-error">❌ Gagal memuat data: ${err.message}</div>`;
});
}
// ── Shell (tab bar + container) ────────────────────────────────────────
function renderShell() {
const body = document.getElementById('dashboardBody');
const tabs = [
{ id: 'kemiskinan', label: '🏠 Kemiskinan' },
{ id: 'jalan', label: '🛣️ Jalan' },
{ id: 'spbu', label: '⛽ SPBU' },
{ id: 'penduduk', label: '👥 Penduduk' },
];
body.innerHTML = `
<div class="db-tab-bar" id="dbTabBar">
${tabs.map(t => `
<button class="db-tab-btn${t.id === activeTab ? ' active' : ''}"
data-tab="${t.id}">${t.label}</button>
`).join('')}
</div>
<div id="dbTabContent" class="db-tab-content"></div>`;
document.getElementById('dbTabBar').addEventListener('click', function (e) {
const btn = e.target.closest('[data-tab]');
if (!btn) return;
activeTab = btn.dataset.tab;
document.querySelectorAll('.db-tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === activeTab));
renderTab(activeTab);
});
}
// ── Route to tab ───────────────────────────────────────────────────────
function renderTab(tab) {
const content = document.getElementById('dbTabContent');
if (!content || !dashboardData) return;
if (tab === 'kemiskinan') renderKemiskinan(content, dashboardData.poverty);
else if (tab === 'jalan') renderJalan(content, dashboardData.jalan);
else if (tab === 'spbu') renderSpbu(content, dashboardData.spbu);
else if (tab === 'penduduk') renderPenduduk(content, dashboardData.penduduk);
}
// ══════════════════════════════════════════════════════════════════════
// Tab 1: Kemiskinan
// ══════════════════════════════════════════════════════════════════════
function renderKemiskinan(el, d) {
const pctColor = d.pct_terurus >= 70 ? '#22c55e' : d.pct_terurus >= 40 ? '#f59e0b' : '#ef4444';
el.innerHTML = `
<div class="db-kpi-grid">
<div class="db-kpi-card kpi-ibadah">
<div class="db-kpi-icon">🕌</div>
<div class="db-kpi-val">${d.total_ibadah}</div>
<div class="db-kpi-label">Rumah Ibadah</div>
</div>
<div class="db-kpi-card kpi-miskin">
<div class="db-kpi-icon">🏠</div>
<div class="db-kpi-val">${d.total_miskin}</div>
<div class="db-kpi-label">KK Miskin</div>
</div>
<div class="db-kpi-card kpi-jiwa">
<div class="db-kpi-icon">👥</div>
<div class="db-kpi-val">${d.total_jiwa}</div>
<div class="db-kpi-label">Total Jiwa</div>
</div>
<div class="db-kpi-card kpi-log">
<div class="db-kpi-icon">📋</div>
<div class="db-kpi-val">${d.total_log}</div>
<div class="db-kpi-label">Log Bantuan</div>
</div>
</div>
${d.total_miskin > 0 ? `
<div class="db-section">
<div class="db-section-title">📊 Cakupan Penanganan</div>
<div class="db-coverage-row">
<div class="db-coverage-labels">
<span style="color:#22c55e">✅ Terurus: <strong>${d.terurus}</strong></span>
<span style="color:#ef4444">⚠️ Tidak Terurus: <strong>${d.tidak_terurus}</strong></span>
<span style="color:${pctColor}; font-weight:700">${d.pct_terurus}%</span>
</div>
<div class="db-progress-bar">
<div class="db-progress-fill" style="width:${d.pct_terurus}%; background:${pctColor};"></div>
</div>
</div>
</div>` : ''}
<div class="db-charts-grid">
<div class="db-chart-card">
<div class="db-chart-title">📦 Kategori Bantuan</div>
<canvas id="chartKategori" height="180"></canvas>
</div>
<div class="db-chart-card">
<div class="db-chart-title">🎁 Tipe Bantuan</div>
<canvas id="chartTipe" height="180"></canvas>
</div>
<div class="db-chart-card db-chart-wide">
<div class="db-chart-title">📅 Aktivitas Bantuan (6 Bulan)</div>
<canvas id="chartBulan" height="140"></canvas>
</div>
</div>`;
setTimeout(() => {
drawPieChart('chartKategori', d.distribusi.kategori, ['#6366f1','#f59e0b','#22c55e','#ef4444','#06b6d4']);
drawPieChart('chartTipe', d.distribusi.tipe_bantuan, ['#4f46e5','#16a34a','#dc2626','#d97706','#0891b2','#7c3aed']);
drawBarChart('chartBulan', d.distribusi.per_bulan);
}, 50);
}
// ══════════════════════════════════════════════════════════════════════
// Tab 2: Jalan
// ══════════════════════════════════════════════════════════════════════
function renderJalan(el, d) {
const panjangKm = (d.total_panjang_m / 1000).toFixed(2);
const luasHa = (d.total_luas_m2 / 10000).toFixed(2);
el.innerHTML = `
<div class="db-kpi-grid">
<div class="db-kpi-card" style="--kpi-color:#3b82f6">
<div class="db-kpi-icon">🛣️</div>
<div class="db-kpi-val">${d.total_jalan}</div>
<div class="db-kpi-label">Ruas Jalan</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#0ea5e9">
<div class="db-kpi-icon">📏</div>
<div class="db-kpi-val">${panjangKm} km</div>
<div class="db-kpi-label">Total Panjang</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#8b5cf6">
<div class="db-kpi-icon">🗂️</div>
<div class="db-kpi-val">${d.total_parsil}</div>
<div class="db-kpi-label">Persil Tanah</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#6d28d9">
<div class="db-kpi-icon">📐</div>
<div class="db-kpi-val">${luasHa} ha</div>
<div class="db-kpi-label">Total Luas Persil</div>
</div>
</div>
<div class="db-charts-grid">
<div class="db-chart-card">
<div class="db-chart-title">🛣️ Jalan per Status</div>
<canvas id="chartStatusJalan" height="180"></canvas>
</div>
<div class="db-chart-card">
<div class="db-chart-title">📋 Persil per Kepemilikan</div>
<canvas id="chartKepemilikan" height="180"></canvas>
</div>
</div>
${Object.keys(d.per_status).length > 0 ? `
<div class="db-section">
<div class="db-section-title">🛣️ Rincian Status Jalan</div>
<div class="db-table-wrap">
<table class="db-table">
<thead><tr><th>Status</th><th>Jumlah Ruas</th></tr></thead>
<tbody>
${Object.entries(d.per_status).map(([k,v]) => `
<tr><td>${esc(k)}</td><td><span class="db-badge">${v}</span></td></tr>
`).join('')}
</tbody>
</table>
</div>
</div>` : '<div class="db-empty">Belum ada data jalan tersimpan di database.</div>'}`;
setTimeout(() => {
drawPieChart('chartStatusJalan', d.per_status, ['#3b82f6','#0ea5e9','#38bdf8']);
drawPieChart('chartKepemilikan', d.per_kepemilikan, ['#8b5cf6','#a78bfa','#c4b5fd','#7c3aed']);
}, 50);
}
// ══════════════════════════════════════════════════════════════════════
// Tab 3: SPBU
// ══════════════════════════════════════════════════════════════════════
function renderSpbu(el, d) {
el.innerHTML = `
<div class="db-kpi-grid">
<div class="db-kpi-card" style="--kpi-color:#f97316">
<div class="db-kpi-icon">⛽</div>
<div class="db-kpi-val">${d.total_spbu}</div>
<div class="db-kpi-label">Total SPBU</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#22c55e">
<div class="db-kpi-icon">🕐</div>
<div class="db-kpi-val">${d.buka_24_jam}</div>
<div class="db-kpi-label">Buka 24 Jam</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#ef4444">
<div class="db-kpi-icon">🕙</div>
<div class="db-kpi-val">${d.tidak_24_jam}</div>
<div class="db-kpi-label">Tidak 24 Jam</div>
</div>
</div>
<div class="db-charts-grid">
<div class="db-chart-card">
<div class="db-chart-title">⏰ Status Operasional</div>
<canvas id="chartSpbuOps" height="180"></canvas>
</div>
</div>
${d.list.length > 0 ? `
<div class="db-section">
<div class="db-section-title">📍 Daftar SPBU</div>
<div class="db-table-wrap">
<table class="db-table">
<thead>
<tr>
<th>#</th>
<th>Nama SPBU</th>
<th>Nomor WA</th>
<th>Operasional</th>
<th>Koordinat</th>
</tr>
</thead>
<tbody>
${d.list.map((s, i) => `
<tr>
<td>${i+1}</td>
<td><strong>${esc(s.nama_spbu)}</strong></td>
<td>${esc(s.nomor_wa)}</td>
<td>${s.buka_24_jam == 1
? '<span class="db-badge" style="background:#22c55e">24 Jam</span>'
: '<span class="db-badge" style="background:#f59e0b">Terbatas</span>'}</td>
<td style="font-size:11px;color:#888">${parseFloat(s.latitude).toFixed(5)}, ${parseFloat(s.longitude).toFixed(5)}</td>
</tr>`).join('')}
</tbody>
</table>
</div>
</div>` : '<div class="db-empty">Belum ada data SPBU tersimpan di database.</div>'}`;
setTimeout(() => {
drawPieChart('chartSpbuOps', { 'Buka 24 Jam': d.buka_24_jam, 'Tidak 24 Jam': d.tidak_24_jam }, ['#22c55e','#ef4444']);
}, 50);
}
// ══════════════════════════════════════════════════════════════════════
// Tab 4: Penduduk (Choropleth Kecamatan)
// ══════════════════════════════════════════════════════════════════════
function renderPenduduk(el, d) {
const maxDensity = Math.max(...d.kecamatan.map(k => k.density));
el.innerHTML = `
<div class="db-kpi-grid">
<div class="db-kpi-card" style="--kpi-color:#0891b2">
<div class="db-kpi-icon">🗺️</div>
<div class="db-kpi-val">${d.total_kecamatan}</div>
<div class="db-kpi-label">Kecamatan</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#06b6d4">
<div class="db-kpi-icon">📊</div>
<div class="db-kpi-val">${d.rata_density.toLocaleString('id-ID')}</div>
<div class="db-kpi-label">Rata-rata Kepadatan (jiwa/km²)</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#0e7490">
<div class="db-kpi-icon">🏆</div>
<div class="db-kpi-val" style="font-size:14px">${esc(d.terpadat)}</div>
<div class="db-kpi-label">Kecamatan Terpadat</div>
</div>
</div>
<div class="db-section">
<div class="db-section-title">👥 Kepadatan Penduduk per Kecamatan (jiwa/km²)</div>
${d.kecamatan.map(k => {
const pct = ((k.density / maxDensity) * 100).toFixed(1);
const color = k.density >= 8 ? '#ef4444' : k.density >= 5 ? '#f59e0b' : '#22c55e';
return `
<div style="margin-bottom:10px">
<div style="display:flex; justify-content:space-between; font-size:13px; margin-bottom:3px">
<span>${esc(k.nama)}</span>
<strong style="color:${color}">${k.density.toLocaleString('id-ID')} jiwa/km²</strong>
</div>
<div class="db-progress-bar">
<div class="db-progress-fill" style="width:${pct}%; background:${color};"></div>
</div>
</div>`;
}).join('')}
<div style="margin-top:8px; font-size:11px; color:#888; display:flex; gap:16px">
<span>🟢 &lt;5 Rendah</span>
<span>🟡 58 Sedang</span>
<span>🔴 ≥8 Tinggi</span>
</div>
</div>
<div class="db-charts-grid">
<div class="db-chart-card db-chart-wide">
<div class="db-chart-title">📊 Kepadatan per Kecamatan</div>
<canvas id="chartDensity" height="160"></canvas>
</div>
</div>`;
setTimeout(() => {
drawDensityBar('chartDensity', d.kecamatan);
}, 50);
}
// ══════════════════════════════════════════════════════════════════════
// Chart Helpers
// ══════════════════════════════════════════════════════════════════════
function drawPieChart(canvasId, dataObj, colors) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const entries = Object.entries(dataObj || {}).filter(([,v]) => v > 0);
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (entries.length === 0) {
ctx.fillStyle = '#aaa'; ctx.font = '13px Inter'; ctx.textAlign = 'center';
ctx.fillText('Belum ada data', canvas.width/2, canvas.height/2);
return;
}
const labels = entries.map(([k]) => k);
const values = entries.map(([,v]) => v);
const total = values.reduce((a,b) => a+b, 0);
const cx = canvas.width/2, cy = canvas.height * 0.42;
const r = Math.min(cx, cy) * 0.72;
let startAngle = -Math.PI/2;
values.forEach((v,i) => {
const slice = (v/total)*2*Math.PI;
ctx.beginPath(); ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, startAngle, startAngle+slice);
ctx.closePath();
ctx.fillStyle = colors[i % colors.length];
ctx.fill(); ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.stroke();
startAngle += slice;
});
ctx.fillStyle='#333'; ctx.font='bold 15px Inter'; ctx.textAlign='center';
ctx.fillText(total, cx, cy+5);
const legY = cy+r+14, legItemW = 92, perRow = Math.floor(canvas.width/legItemW);
labels.forEach((lbl,i) => {
const row=Math.floor(i/perRow), col=i%perRow;
const lx=(canvas.width - Math.min(labels.length,perRow)*legItemW)/2 + col*legItemW;
const ly=legY+row*17;
ctx.fillStyle=colors[i%colors.length]; ctx.fillRect(lx,ly,10,10);
ctx.fillStyle='#555'; ctx.font='10px Inter'; ctx.textAlign='left';
const shortLbl = lbl.length>11 ? lbl.slice(0,10)+'…' : lbl;
ctx.fillText(`${shortLbl} (${values[i]})`, lx+13, ly+9);
});
}
function drawBarChart(canvasId, perBulan) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!perBulan || perBulan.length === 0) {
ctx.fillStyle='#aaa'; ctx.font='13px Inter'; ctx.textAlign='center';
ctx.fillText('Belum ada data bantuan', canvas.width/2, canvas.height/2);
return;
}
const padL=36, padR=12, padT=12, padB=28;
const W=canvas.width-padL-padR, H=canvas.height-padT-padB;
const values=perBulan.map(d=>parseInt(d.cnt)), maxVal=Math.max(...values,1);
const barW=W/perBulan.length*0.6, gap=W/perBulan.length;
ctx.strokeStyle='#e5e7eb'; ctx.lineWidth=1;
for (let i=0;i<=4;i++){
const y=padT+H-(i/4)*H;
ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(padL+W,y); ctx.stroke();
ctx.fillStyle='#9ca3af'; ctx.font='9px Inter'; ctx.textAlign='right';
ctx.fillText(Math.round((i/4)*maxVal), padL-4, y+3);
}
perBulan.forEach((d,i)=>{
const bh=(parseInt(d.cnt)/maxVal)*H, bx=padL+i*gap+(gap-barW)/2, by=padT+H-bh;
const grad=ctx.createLinearGradient(0,by,0,padT+H);
grad.addColorStop(0,'#6366f1'); grad.addColorStop(1,'#a5b4fc');
ctx.fillStyle=grad;
ctx.beginPath();
if(ctx.roundRect) ctx.roundRect(bx,by,barW,bh,[4,4,0,0]); else ctx.rect(bx,by,barW,bh);
ctx.fill();
ctx.fillStyle='#4f46e5'; ctx.font='bold 10px Inter'; ctx.textAlign='center';
ctx.fillText(d.cnt, bx+barW/2, by-4);
const ml=d.bulan ? d.bulan.slice(5)+'/'+d.bulan.slice(2,4) : '';
ctx.fillStyle='#6b7280'; ctx.font='9px Inter';
ctx.fillText(ml, bx+barW/2, padT+H+14);
});
}
function drawDensityBar(canvasId, kecamatan) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const padL=130, padR=50, padT=12, padB=12;
const W=canvas.width-padL-padR, H=canvas.height-padT-padB;
const maxVal=Math.max(...kecamatan.map(k=>k.density),1);
const barH=Math.floor(H/kecamatan.length)-6;
kecamatan.forEach((k,i) => {
const bw=(k.density/maxVal)*W;
const by=padT+i*(barH+6);
const color=k.density>=8?'#ef4444':k.density>=5?'#f59e0b':'#22c55e';
// Label kecamatan
ctx.fillStyle='#374151'; ctx.font='11px Inter'; ctx.textAlign='right';
ctx.fillText(k.nama, padL-6, by+barH/2+4);
// Bar
ctx.fillStyle=color+'33';
ctx.fillRect(padL, by, W, barH);
const grad=ctx.createLinearGradient(padL,0,padL+bw,0);
grad.addColorStop(0,color); grad.addColorStop(1,color+'99');
ctx.fillStyle=grad;
if(ctx.roundRect) ctx.roundRect(padL,by,bw,barH,[0,4,4,0]); else ctx.fillRect(padL,by,bw,barH);
ctx.fill();
// Value
ctx.fillStyle=color; ctx.font='bold 11px Inter'; ctx.textAlign='left';
ctx.fillText(k.density.toFixed(3), padL+bw+5, by+barH/2+4);
});
}
// ── Utility ───────────────────────────────────────────────────────────
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
})();
@@ -0,0 +1,181 @@
// --- Fitur Import GeoJSON ---
const fileGeoJson = document.getElementById('fileGeoJson');
let geoJsonLayers = {};
let geoJsonCounter = 0;
window.toggleGeoJsonMenu = function() {
const list = document.getElementById('geoJsonFileList');
const icon = document.getElementById('geoJsonToggleIcon');
if (list.style.display === 'none') {
list.style.display = 'block';
icon.className = 'fas fa-minus';
} else {
list.style.display = 'none';
icon.className = 'fas fa-plus';
}
};
// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
function isMiskinFeature(feature) {
if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
const props = feature.properties || {};
const keys = Object.keys(props).map(k => k.toLowerCase());
// Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
return hasNama && !isIbadah && !isSpbu;
}
fileGeoJson.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
try {
const geoJsonData = JSON.parse(event.target.result);
const fileName = file.name;
// Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
const features = geoJsonData.type === 'FeatureCollection'
? geoJsonData.features
: [geoJsonData];
const miskinFeatures = features.filter(f => isMiskinFeature(f));
const otherFeatures = features.filter(f => !isMiskinFeature(f));
// ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
if (miskinFeatures.length > 0) {
const bulkData = miskinFeatures.map(f => {
const props = f.properties || {};
const coords = f.geometry.coordinates;
return {
nama: props.nama || props.name || props.penduduk || 'Data Impor',
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
lat: parseFloat(coords[1]),
lng: parseFloat(coords[0])
};
});
fetch('api/penduduk_miskin/bulk_create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bulkData)
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
// Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
if (typeof loadPendudukMiskin === 'function') {
loadPendudukMiskin();
}
if (typeof window.refreshActivePanel === 'function') {
window.refreshActivePanel();
}
showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
} else {
showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
}
})
.catch(err => {
console.error(err);
showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
});
}
// ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
if (otherFeatures.length > 0) {
const layerId = 'gj_' + (++geoJsonCounter);
const individualLayer = L.featureGroup();
const otherGeoJson = {
type: 'FeatureCollection',
features: otherFeatures
};
L.geoJSON(otherGeoJson, {
pointToLayer: function(feature, latlng) {
const props = feature.properties || {};
let emoji = props.emoji;
if (!emoji) {
const text = JSON.stringify(props).toLowerCase();
if (text.includes('spbu')) emoji = '⛽';
else if (text.includes('masjid')) emoji = '🕌';
else if (text.includes('gereja')) emoji = '⛪';
else if (text.includes('vihara')) emoji = '🪷';
else if (text.includes('pura')) emoji = '🛕';
else if (text.includes('kelenteng')) emoji = '🏮';
else emoji = '📍';
}
let cls = 'miskin-out';
if (emoji === '⛽') cls = 'spbu-24';
else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
const icon = L.divIcon({
className: '',
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>${emoji}</span></div></div>`,
iconSize: [38, 38],
iconAnchor: [19, 38],
popupAnchor: [0, -40]
});
return L.marker(latlng, { icon: icon });
},
onEachFeature: function(feature, layer) {
if (feature.properties) {
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0; font-size:12px; margin: 0;">';
for (let key in feature.properties) {
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
}
popupContent += '</ul>';
layer.bindPopup(popupContent);
}
},
style: function(feature) {
return {
color: (feature.properties && feature.properties.color) || '#3388ff',
weight: 2,
fillOpacity: 0.4
};
}
}).addTo(individualLayer);
individualLayer.addTo(map);
geoJsonLayers[layerId] = individualLayer;
// Tambahkan checkbox ke UI
const container = document.getElementById('geoJsonLayersContainer');
const label = document.createElement('label');
label.className = 'layer-option';
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
label.querySelector('input').addEventListener('change', function(e) {
if (e.target.checked) {
map.addLayer(geoJsonLayers[layerId]);
} else {
map.removeLayer(geoJsonLayers[layerId]);
}
});
container.appendChild(label);
if (miskinFeatures.length === 0) {
showToast('File GeoJSON berhasil dimuat!', 'success');
}
}
// Reset input file agar bisa import file yang sama
fileGeoJson.value = '';
} catch (error) {
showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
console.error(error);
}
};
reader.readAsText(file);
});
@@ -0,0 +1,30 @@
// --- Fitur Geolocation ---
// Tambahkan tombol di dalam wadah zoom control
const geoBtn = document.createElement('button');
geoBtn.className = 'custom-layer-btn';
geoBtn.style.position = 'relative';
geoBtn.style.top = '0';
geoBtn.style.left = '0';
geoBtn.style.right = 'auto';
geoBtn.innerHTML = '<i class="fas fa-crosshairs fa-lg"></i>';
geoBtn.title = 'Lokasi Saya';
document.querySelector('.custom-zoom-control').appendChild(geoBtn);
let userMarker = null;
geoBtn.addEventListener('click', function() {
map.locate({setView: true, maxZoom: 16});
});
map.on('locationfound', function(e) {
if (userMarker) {
map.removeLayer(userMarker);
}
userMarker = L.marker(e.latlng).addTo(map)
.bindPopup("Anda berada di sini!").openPopup();
});
map.on('locationerror', function(e) {
alert("Gagal mendapatkan lokasi Anda.");
});
@@ -0,0 +1,916 @@
// --- Fitur Pemetaan Kemiskinan ---
// Emoji per jenis ibadah
const IBADAH_EMOJI_MAP = {
'Masjid': '🕌',
'Gereja': '⛪',
'Pura': '🛕',
'Vihara': '🪷',
'Kelenteng': '🏮',
};
// Emoji Bubble Icon builder
function makeIbadahIcon(jenis) {
const emot = IBADAH_EMOJI_MAP[jenis] || '🕌';
return L.divIcon({
className: '',
html: `<div class="emoji-marker"><div class="bubble ibadah"><span>${emot}</span></div></div>`,
iconSize: [38, 38],
iconAnchor: [19, 38],
popupAnchor: [0, -40]
});
}
function makeMiskinIcon(inRadius) {
const cls = inRadius ? 'miskin-in' : 'miskin-out';
return L.divIcon({
className: '',
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>🏠</span></div></div>`,
iconSize: [38, 38],
iconAnchor: [19, 38],
popupAnchor: [0, -40]
});
}
let ibadahDataList = [];
let miskinMarkerList = []; // Simpan referensi marker untuk update warna
// Helper: cek apakah koordinat (lat, lng) berada dalam radius ibadah pengelola yang login
function isInMyIbadahRadius(lat, lng) {
if (!window.currentUser || window.currentUser.role !== 'pengelola') return true; // admin selalu bisa
const myIbadahId = window.currentUser.ibadah_id;
if (!myIbadahId) return false;
const ib = ibadahDataList.find(i => i.id == myIbadahId);
if (!ib) return false;
const dist = L.latLng(lat, lng).distanceTo(L.latLng(ib.lat, ib.lng));
return dist <= ib.radius;
}
window.isInMyIbadahRadius = isInMyIbadahRadius;
let isResizing = false;
let resizingCircle = null;
let resizingIbadah = null;
map.on('mousemove', function(e) {
if (isResizing && resizingCircle && resizingIbadah) {
const center = resizingCircle.getLatLng();
const newRadius = center.distanceTo(e.latlng);
resizingCircle.setRadius(newRadius);
resizingIbadah.radius = newRadius;
// Update data radius di marker juga
rumahIbadahLayer.eachLayer(function(layer) {
if (layer.ibadahData && layer.ibadahData.id === resizingIbadah.id && layer instanceof L.Marker) {
layer.ibadahData.radius = newRadius;
}
});
updateSemuaWarnaMiskin();
}
});
map.on('mouseup', function(e) {
if (isResizing) {
isResizing = false;
map.dragging.enable();
if (resizingIbadah) {
updateIbadah(resizingIbadah.id, resizingIbadah.nama, resizingIbadah.jenis, resizingIbadah.alamat, resizingIbadah.radius, resizingIbadah.lat, resizingIbadah.lng);
}
resizingCircle = null;
resizingIbadah = null;
}
});
// --- Rumah Ibadah ---
function loadRumahIbadah() {
rumahIbadahLayer.clearLayers();
ibadahDataList = [];
fetch('api/rumah_ibadah/read.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.data) {
ibadahDataList = data.data;
data.data.forEach(item => {
addIbadahMarker(item);
});
updateSemuaWarnaMiskin();
}
if (window.refreshActivePanel) window.refreshActivePanel();
});
}
function addIbadahMarker(item) {
const lat = parseFloat(item.lat);
const lng = parseFloat(item.lng);
const radius = parseFloat(item.radius) || 100; // fallback if invalid
if (isNaN(lat) || isNaN(lng)) return;
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
const marker = L.marker([lat, lng], { icon: makeIbadahIcon(item.jenis), draggable: isAdmin });
// Lingkaran radius
const circle = L.circle([lat, lng], {
radius: radius,
color: '#ff7800',
weight: 2,
fillColor: '#ff7800',
fillOpacity: 0.2
});
circle.on('mousemove', function(e) {
if (!isAdmin || isResizing) return;
const center = circle.getLatLng();
const radius = circle.getRadius();
const dist = center.distanceTo(e.latlng);
const metersPerPixel = map.distance(map.containerPointToLatLng([0,0]), map.containerPointToLatLng([0,1]));
const tolerance = Math.max(radius * 0.1, metersPerPixel * 15); // toleransi 15px atau 10%
if (Math.abs(dist - radius) <= tolerance) {
if (circle._path) circle._path.style.cursor = 'ew-resize';
circle.nearEdge = true;
} else {
if (circle._path) circle._path.style.cursor = 'pointer';
circle.nearEdge = false;
}
});
circle.on('mousedown', function(e) {
if (isAdmin && circle.nearEdge) {
map.dragging.disable();
isResizing = true;
resizingCircle = circle;
resizingIbadah = item;
L.DomEvent.stopPropagation(e);
}
});
marker.ibadahData = item;
marker.circleLayer = circle;
const d = item;
const emot = IBADAH_EMOJI_MAP[d.jenis] || '🕌';
let actionButtons = '';
if (isAdmin) {
actionButtons = `
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditIbadahModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteIbadah(${d.id})">Hapus</button>
</div>
`;
}
const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 160px;">
<h4 style="margin:0 0 5px 0;">${emot} ${d.nama}</h4>
<p style="margin: 0 0 3px 0;"><b>Jenis:</b> ${d.jenis || 'Masjid'}</p>
<p style="margin: 0 0 3px 0;"><b>Alamat:</b> ${d.alamat}</p>
<p style="margin: 0 0 10px 0;"><b>Radius:</b> ${d.radius} m</p>
${actionButtons}
</div>
`;
marker.bindPopup(popupContent);
marker.on('dragend', function(e) {
const newPos = marker.getLatLng();
circle.setLatLng(newPos);
item.lat = newPos.lat;
item.lng = newPos.lng;
// Update ke DB
updateIbadah(item.id, item.nama, item.jenis, item.alamat, item.radius, newPos.lat, newPos.lng);
});
rumahIbadahLayer.addLayer(circle);
rumahIbadahLayer.addLayer(marker);
}
// Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta)
map.on('contextmenu', function(e) {
const isAdminOrPengelola = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
if (!isAdminOrPengelola) return;
const canAddHere = isInMyIbadahRadius(e.latlng.lat, e.latlng.lng);
if (!canAddHere) {
// Pengelola: lokasi di luar radius ibadahnya
const warnContent = `
<div class="popup-form">
<h4>⚠️ Di Luar Radius</h4>
<p style="font-size:12px; color:#e11d48; margin:4px 0 0;">Anda hanya dapat menambah penduduk miskin di dalam radius rumah ibadah yang Anda kelola.</p>
</div>
`;
L.popup().setLatLng(e.latlng).setContent(warnContent).openOn(map);
return;
}
const popupContent = `
<div class="popup-form">
<h4>Tambah Titik</h4>
<button class="btn-danger" style="margin-top:5px;" onclick="formAddMiskin(${e.latlng.lat}, ${e.latlng.lng})">Tambah Penduduk Miskin</button>
</div>
`;
L.popup().setLatLng(e.latlng).setContent(popupContent).openOn(map);
});
// Map Click -> Form Add Rumah Ibadah
map.on('click', function(e) {
if (window.currentAddMode === 'rumah_ibadah') {
const lat = e.latlng.lat;
const lng = e.latlng.lng;
// Reverse Geocoding
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
.then(res => res.json())
.then(geoData => {
const alamat = geoData.display_name || 'Alamat tidak ditemukan';
const bodyHTML = `
<div class="form-group">
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama Rumah Ibadah</label>
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
</div>
<div class="form-group">
<label>Jenis</label>
<select id="modalIbadahJenis">
<option value="Masjid">🕌 Masjid</option>
<option value="Gereja">⛪ Gereja</option>
<option value="Pura">🛕 Pura</option>
<option value="Vihara">🪷 Vihara</option>
<option value="Kelenteng">🏮 Kelenteng</option>
</select>
</div>
<div class="form-group">
<label>Alamat (Auto)</label>
<textarea id="modalIbadahAlamat" rows="3">${alamat}</textarea>
</div>
<div class="form-group">
<label>Radius (m)</label>
<input type="number" id="modalIbadahRadius" value="500">
</div>
`;
openModal("Tambah Rumah Ibadah", bodyHTML, function() {
window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius');
});
window.deactivateAddMode();
})
.catch(err => {
// Tetap buka modal meskipun gagal mendapatkan alamat
const alamat = '';
const bodyHTML = `
<div class="form-group">
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama Rumah Ibadah</label>
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
</div>
<div class="form-group">
<label>Jenis</label>
<select id="modalIbadahJenis">
<option value="Masjid">🕌 Masjid</option>
<option value="Gereja">⛪ Gereja</option>
<option value="Pura">🛕 Pura</option>
<option value="Vihara">🪷 Vihara</option>
<option value="Kelenteng">🏮 Kelenteng</option>
</select>
</div>
<div class="form-group">
<label>Alamat (Manual)</label>
<textarea id="modalIbadahAlamat" rows="3" placeholder="Masukkan alamat manual">${alamat}</textarea>
</div>
<div class="form-group">
<label>Radius (m)</label>
<input type="number" id="modalIbadahRadius" value="500">
</div>
`;
openModal("Tambah Rumah Ibadah", bodyHTML, function() {
window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius');
});
window.deactivateAddMode();
});
}
});
window.saveNewIbadah = function(lat, lng, namaId, jenisId, alamatId, radiusId) {
const nama = document.getElementById(namaId).value;
const jenis = document.getElementById(jenisId).value;
const alamat = document.getElementById(alamatId).value;
const radius = document.getElementById(radiusId).value;
if (!nama) { alert("Nama Rumah Ibadah harus diisi!"); return; }
fetch('api/rumah_ibadah/create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, jenis, alamat, radius, lat, lng })
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') { closeModal(); loadRumahIbadah(); }
else { alert(data.message); }
});
};
window.openEditIbadahModal = function(id) {
let d = null;
rumahIbadahLayer.eachLayer(function(layer) {
if (layer.ibadahData && layer.ibadahData.id == id) d = layer.ibadahData;
});
if (!d) return;
const jenisOpts = ['Masjid','Gereja','Pura','Vihara','Kelenteng'].map(j =>
`<option value="${j}" ${d.jenis === j ? 'selected' : ''}>${IBADAH_EMOJI_MAP[j] || ''} ${j}</option>`
).join('');
const bodyHTML = `
<div class="form-group">
<label>Nama Rumah Ibadah</label>
<input type="text" id="editIbadahNama" value="${d.nama}">
</div>
<div class="form-group">
<label>Jenis</label>
<select id="editIbadahJenis">${jenisOpts}</select>
</div>
<div class="form-group">
<label>Alamat</label>
<textarea id="editIbadahAlamat" rows="2">${d.alamat}</textarea>
</div>
<div class="form-group">
<label>Radius (m)</label>
<input type="number" id="editIbadahRadius" value="${d.radius}" oninput="previewRadius(${d.id}, this.value)">
</div>
`;
map.closePopup();
openModal("Edit Rumah Ibadah", bodyHTML, function() {
window.saveEditIbadah(d.id, d.lat, d.lng, 'editIbadahNama', 'editIbadahJenis', 'editIbadahAlamat', 'editIbadahRadius');
});
};
window.previewRadius = function(id, newRadius) {
rumahIbadahLayer.eachLayer(function(layer) {
// layer ini bisa marker atau circle, kita cek yang punya circleLayer (marker)
if (layer.ibadahData && layer.ibadahData.id === id && layer.circleLayer) {
layer.circleLayer.setRadius(parseFloat(newRadius));
layer.ibadahData.radius = parseFloat(newRadius);
updateSemuaWarnaMiskin(); // Update warna miskin real-time
}
});
};
window.saveEditIbadah = function(id, lat, lng, namaId, jenisId, alamatId, radiusId) {
const nama = document.getElementById(namaId).value;
const jenis = document.getElementById(jenisId).value;
const alamat = document.getElementById(alamatId).value;
const radius = document.getElementById(radiusId).value;
updateIbadah(id, nama, jenis, alamat, radius, lat, lng);
};
function updateIbadah(id, nama, jenis, alamat, radius, lat, lng) {
fetch('api/rumah_ibadah/update.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, jenis, alamat, radius, lat, lng })
}).then(res => res.json()).then(data => {
if(data.status === 'success') {
if (typeof closeModal === 'function') closeModal();
map.closePopup();
loadRumahIbadah();
}
});
}
window.deleteIbadah = function(id) {
openConfirmModal("Yakin hapus rumah ibadah ini?", function() {
fetch('api/rumah_ibadah/delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
}).then(res => res.json()).then(data => {
if(data.status === 'success') { map.closePopup(); loadRumahIbadah(); }
});
});
};
// --- Penduduk Miskin ---
function loadPendudukMiskin() {
pendudukMiskinLayer.clearLayers();
miskinMarkerList = [];
fetch('api/penduduk_miskin/read.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.data) {
data.data.forEach(item => {
addMiskinMarker(item);
});
updateSemuaWarnaMiskin();
}
if (window.refreshActivePanel) window.refreshActivePanel();
});
}
function addMiskinMarker(item) {
const lat = parseFloat(item.lat);
const lng = parseFloat(item.lng);
if (isNaN(lat) || isNaN(lng)) return;
// Pengelola hanya bisa manage marker yang ada di dalam radius ibadahnya
const isAdminOrPengelola = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
const canManage = isAdminOrPengelola && isInMyIbadahRadius(lat, lng);
const marker = L.marker([lat, lng], { icon: makeMiskinIcon(false), draggable: canManage });
marker.miskinData = item;
const d = item;
let actionButtons = '';
if (canManage) {
actionButtons = `
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditMiskinModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteMiskin(${d.id})">Hapus</button>
`;
}
let photoLinksHtml = '';
if (d.foto_rumah) {
photoLinksHtml += `
<div style="margin: 5px 0;">
<b style="font-size:11px;">Foto Rumah:</b><br>
<a href="/poverty/uploads/${d.foto_rumah}" target="_blank">
<img src="/poverty/uploads/${d.foto_rumah}" alt="Foto Rumah"
style="max-width:150px; max-height:100px; border-radius:4px; margin-top:3px; border:1px solid #ddd; object-fit:cover; cursor:pointer;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
<span style="display:none; font-size:11px; color:#ef4444;">❌ Foto tidak dapat dimuat</span>
</a>
</div>`;
}
if (d.foto_kk) {
photoLinksHtml += `
<div style="margin: 5px 0;">
<b style="font-size:11px;">Foto KK:</b><br>
<a href="/poverty/uploads/${d.foto_kk}" target="_blank">
<img src="/poverty/uploads/${d.foto_kk}" alt="Foto KK"
style="max-width:150px; max-height:100px; border-radius:4px; margin-top:3px; border:1px solid #ddd; object-fit:cover; cursor:pointer;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
<span style="display:none; font-size:11px; color:#ef4444;">❌ Foto tidak dapat dimuat</span>
</a>
</div>`;
}
const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 165px;">
<h4 style="margin:0 0 5px 0;">🏠 ${d.nama}</h4>
<p style="margin: 0 0 3px 0;"><b>Bantuan:</b> ${d.kategori_bantuan}</p>
<p style="margin: 0 0 5px 0;"><b>Jiwa:</b> ${d.jumlah_jiwa || '-'}</p>
${photoLinksHtml}
<div style="display:flex; gap:5px; flex-wrap:wrap; margin-top:8px;">
<button style="padding:4px 8px; background:#6366f1; color:white; border:none; border-radius:3px; cursor:pointer; font-size:11px;" onclick="openLogBantuan(${d.id}, '${d.nama.replace(/'/g, "\\'")}');">📋 Log Bantuan</button>
${actionButtons}
</div>
</div>
`;
marker.bindPopup(popupContent);
marker.on('dragend', function(e) {
const newPos = marker.getLatLng();
item.lat = newPos.lat;
item.lng = newPos.lng;
updateSemuaWarnaMiskin(); // Update warna segera
// Simpan ke DB
fetch('api/penduduk_miskin/update.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
});
});
miskinMarkerList.push(marker);
pendudukMiskinLayer.addLayer(marker);
}
window.formAddMiskin = function(lat, lng) {
map.closePopup();
const bodyHTML = `
<div class="form-group">
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama Kepala Keluarga</label>
<input type="text" id="miskinNama" placeholder="Nama Kepala Keluarga">
</div>
<div class="form-group">
<label>Kategori Bantuan</label>
<select id="miskinKategori">
<option value="Makan">Bantuan Makan</option>
<option value="Pemberdayaan">Pemberdayaan</option>
</select>
</div>
<div class="form-group">
<label>Jumlah Jiwa</label>
<input type="number" id="miskinJiwa" value="1" min="1">
</div>
<div class="form-group">
<label>Foto Rumah</label>
<input type="file" id="miskinFotoRumah" accept="image/*">
</div>
<div class="form-group">
<label>Foto Kartu Keluarga (KK)</label>
<input type="file" id="miskinFotoKK" accept="image/*">
</div>
`;
openModal("Tambah Penduduk Miskin", bodyHTML, function() {
window.saveNewMiskin(lat, lng);
});
};
window.saveNewMiskin = function(lat, lng) {
const nama = document.getElementById('miskinNama').value;
const kategori_bantuan = document.getElementById('miskinKategori').value;
const jumlah_jiwa = document.getElementById('miskinJiwa').value;
const fotoRumahInput = document.getElementById('miskinFotoRumah');
const fotoKKInput = document.getElementById('miskinFotoKK');
if (!nama) { alert('Nama kepala keluarga harus diisi!'); return; }
const formData = new FormData();
formData.append('nama', nama);
formData.append('kategori_bantuan', kategori_bantuan);
formData.append('jumlah_jiwa', jumlah_jiwa);
formData.append('lat', lat);
formData.append('lng', lng);
if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]);
if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]);
fetch('api/penduduk_miskin/create.php', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
closeModal();
loadPendudukMiskin();
} else { alert(data.message); }
});
};
window.openEditMiskinModal = function(id) {
let d = null;
pendudukMiskinLayer.eachLayer(function(layer) {
if (layer.miskinData && layer.miskinData.id == id) d = layer.miskinData;
});
if (!d) return;
const bodyHTML = `
<div class="form-group">
<label>Nama Kepala Keluarga</label>
<input type="text" id="editMiskinNama" value="${d.nama}">
</div>
<div class="form-group">
<label>Kategori Bantuan</label>
<select id="editMiskinKategori">
<option value="Makan" ${d.kategori_bantuan === 'Makan' ? 'selected' : ''}>Bantuan Makan</option>
<option value="Pemberdayaan" ${d.kategori_bantuan === 'Pemberdayaan' ? 'selected' : ''}>Pemberdayaan</option>
</select>
</div>
<div class="form-group">
<label>Jumlah Jiwa</label>
<input type="number" id="editMiskinJiwa" value="${d.jumlah_jiwa || 1}" min="1">
</div>
<div class="form-group">
<label>Foto Rumah</label>
<input type="file" id="editMiskinFotoRumah" accept="image/*">
${d.foto_rumah ? `
<div style="margin-top:5px;">
<a href="/poverty/uploads/${d.foto_rumah}" target="_blank">
<img src="/poverty/uploads/${d.foto_rumah}" alt="Foto Rumah" style="max-width:120px; max-height:80px; border-radius:4px; border:1px solid #ddd; object-fit:cover;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
<span style="display:none; font-size:11px; color:#ef4444;">❌ File gambar rusak atau tidak ditemukan</span>
</a>
<div style="font-size:11px;color:#888;margin-top:2px;">Upload baru untuk mengganti</div>
</div>` : ''}
</div>
<div class="form-group">
<label>Foto Kartu Keluarga (KK)</label>
<input type="file" id="editMiskinFotoKK" accept="image/*">
${d.foto_kk ? `
<div style="margin-top:5px;">
<a href="/poverty/uploads/${d.foto_kk}" target="_blank">
<img src="/poverty/uploads/${d.foto_kk}" alt="Foto KK" style="max-width:120px; max-height:80px; border-radius:4px; border:1px solid #ddd; object-fit:cover;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
<span style="display:none; font-size:11px; color:#ef4444;">❌ File gambar rusak atau tidak ditemukan</span>
</a>
<div style="font-size:11px;color:#888;margin-top:2px;">Upload baru untuk mengganti</div>
</div>` : ''}
</div>
`;
map.closePopup();
openModal("Edit Penduduk Miskin", bodyHTML, function() {
window.saveEditMiskin(d.id, d.lat, d.lng, 'editMiskinNama', 'editMiskinKategori', 'editMiskinJiwa');
});
};
window.saveEditMiskin = function(id, lat, lng, namaId, kategoriId, jiwaId) {
const nama = document.getElementById(namaId).value;
const kategori_bantuan = document.getElementById(kategoriId).value;
const jumlah_jiwa = jiwaId ? document.getElementById(jiwaId).value : 1;
const fotoRumahInput = document.getElementById('editMiskinFotoRumah');
const fotoKKInput = document.getElementById('editMiskinFotoKK');
const formData = new FormData();
formData.append('id', id);
formData.append('nama', nama);
formData.append('kategori_bantuan', kategori_bantuan);
formData.append('jumlah_jiwa', jumlah_jiwa);
formData.append('lat', lat);
formData.append('lng', lng);
if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]);
if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]);
fetch('api/penduduk_miskin/update.php', {
method: 'POST',
body: formData
}).then(res => res.json()).then(data => {
if(data.status === 'success') { closeModal(); loadPendudukMiskin(); }
});
};
window.deleteMiskin = function(id) {
openConfirmModal("Yakin hapus penduduk miskin ini?", function() {
fetch('api/penduduk_miskin/delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
}).then(res => res.json()).then(data => {
if(data.status === 'success') { map.closePopup(); loadPendudukMiskin(); }
});
});
};
// Logika dinamis warna: Merah jika di dalam salah satu radius Rumah Ibadah, Hijau jika di luar
function updateSemuaWarnaMiskin() {
miskinMarkerList.forEach(marker => {
let inRadius = false;
const pLatlng = L.latLng(marker.miskinData.lat, marker.miskinData.lng);
for (let i = 0; i < ibadahDataList.length; i++) {
const ibadah = ibadahDataList[i];
const iLatlng = L.latLng(ibadah.lat, ibadah.lng);
const dist = pLatlng.distanceTo(iLatlng); // meter
if (dist <= ibadah.radius) {
inRadius = true;
break; // Jika sudah masuk satu radius, langsung merah
}
}
if (inRadius) {
marker.setIcon(makeMiskinIcon(true));
} else {
marker.setIcon(makeMiskinIcon(false));
}
});
}
// ==========================
// ===== Import Feature =====
// ==========================
let parsedImportData = [];
window.openImportMiskinModal = function() {
parsedImportData = []; // Reset data
const bodyHTML = `
<div class="form-group">
<label>Pilih File (CSV, JSON, atau GeoJSON)</label>
<input type="file" id="importMiskinFile" accept=".csv,.json,.geojson" style="display:block; margin-bottom:10px; width:100%;">
<div style="font-size:12px; color:#666; margin-bottom:15px; line-height:1.4;">
<strong>Format CSV yang didukung:</strong><br>
Header minimal berisi: <code>nama</code>, <code>kategori_bantuan</code> (atau <code>kategori</code>), <code>jumlah_jiwa</code>, <code>lat</code>, <code>lng</code>.<br>
Pemisah: Koma (<code>,</code>), Titik Koma (<code>;</code>), atau Tab.
</div>
</div>
<div id="importPreviewContainer" style="display:none; margin-top: 15px;">
<h4 style="margin:0 0 8px 0; font-size: 13px; color: #333; display: flex; justify-content: space-between;">
<span>Pratinjau Data (<span id="importPreviewCount">0</span> item)</span>
<span id="importPreviewStatus" style="font-weight: normal; font-size: 11px;"></span>
</h4>
<div style="max-height:180px; overflow-y:auto; border:1px solid #e2e8f0; border-radius:6px; background:#f8fafc;">
<table style="width:100%; border-collapse:collapse; font-size:11px; text-align:left;">
<thead>
<tr style="background:#f1f5f9; border-bottom:1px solid #cbd5e1; color:#475569; position: sticky; top: 0;">
<th style="padding:6px 8px;">Kepala Keluarga</th>
<th style="padding:6px 8px;">Kategori</th>
<th style="padding:6px 8px;">Jiwa</th>
<th style="padding:6px 8px;">Koordinat</th>
</tr>
</thead>
<tbody id="importPreviewTableBody" style="color:#334155;"></tbody>
</table>
</div>
</div>
<div id="importErrorMsg" style="display:none; margin-top:10px; color:#ef4444; font-size:12px; font-weight: 500;"></div>
`;
map.closePopup();
openModal("Impor Penduduk Miskin", bodyHTML, function() {
if (parsedImportData.length === 0) {
showToast('Silakan pilih file terlebih dahulu atau pastikan data valid.', 'warning');
return;
}
// Kirim data ke API bulk_create.php
fetch('api/penduduk_miskin/bulk_create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsedImportData)
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
closeModal();
loadPendudukMiskin();
showToast(data.message, 'success');
} else {
showToast(data.message || 'Gagal mengimpor data.', 'error');
}
})
.catch(err => {
console.error(err);
showToast('Terjadi kesalahan saat menghubungi server.', 'error');
});
});
// Register file input listener
document.getElementById('importMiskinFile').addEventListener('change', handleImportFileChange);
};
function parseCSV(text) {
const lines = text.split(/\r?\n/);
if (lines.length < 2) return [];
const firstLine = lines[0];
let delimiter = ',';
if (firstLine.includes(';')) delimiter = ';';
else if (firstLine.includes('\t')) delimiter = '\t';
const headers = firstLine.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, '').toLowerCase());
const results = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
let fields = [];
let currentField = '';
let insideQuote = false;
for (let j = 0; j < line.length; j++) {
const char = line[j];
if (char === '"' || char === "'") {
insideQuote = !insideQuote;
} else if (char === delimiter && !insideQuote) {
fields.push(currentField.trim());
currentField = '';
} else {
currentField += char;
}
}
fields.push(currentField.trim());
fields = fields.map(f => f.replace(/^["']|["']$/g, ''));
const row = {};
headers.forEach((header, index) => {
row[header] = fields[index] || '';
});
results.push(row);
}
return results;
}
function handleImportFileChange(e) {
const file = e.target.files[0];
const previewContainer = document.getElementById('importPreviewContainer');
const previewCount = document.getElementById('importPreviewCount');
const previewTableBody = document.getElementById('importPreviewTableBody');
const errorMsg = document.getElementById('importErrorMsg');
if (!file) {
previewContainer.style.display = 'none';
parsedImportData = [];
return;
}
errorMsg.style.display = 'none';
previewContainer.style.display = 'none';
previewTableBody.innerHTML = '';
parsedImportData = [];
const reader = new FileReader();
reader.onload = function(event) {
try {
const text = event.target.result;
let rawData = [];
if (file.name.endsWith('.csv')) {
rawData = parseCSV(text);
} else if (file.name.endsWith('.json') || file.name.endsWith('.geojson')) {
const json = JSON.parse(text);
if (json.type === 'FeatureCollection' && Array.isArray(json.features)) {
rawData = json.features.map(f => {
const props = f.properties || {};
const coords = f.geometry && f.geometry.coordinates;
return {
nama: props.nama || props.name || props.penduduk || '',
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
jumlah_jiwa: props.jumlah_jiwa || props.jumlah || props.jiwa || 1,
lat: coords ? coords[1] : (props.lat || props.latitude),
lng: coords ? coords[0] : (props.lng || props.longitude)
};
});
} else if (Array.isArray(json)) {
rawData = json;
} else {
throw new Error("Format JSON tidak didukung.");
}
}
if (rawData.length === 0) {
errorMsg.textContent = "File kosong atau format tidak didukung.";
errorMsg.style.display = 'block';
return;
}
const validRows = [];
let invalidCount = 0;
rawData.forEach(row => {
const nama = row.nama || row.name || row.penduduk || row['nama lengkap'] || row.fullname || '';
const kategori_bantuan = row.kategori_bantuan || row.kategori || row.bantuan || row['kategori bantuan'] || 'Makan';
const jumlah_jiwa = parseInt(row.jumlah_jiwa || row.jumlah || row.jiwa || row['jumlah jiwa'] || 1, 10) || 1;
const lat = parseFloat(row.lat || row.latitude || row.y);
const lng = parseFloat(row.lng || row.longitude || row.long || row.x);
const isValid = !isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0 && nama.trim() !== '';
if (isValid) {
validRows.push({ nama, kategori_bantuan, jumlah_jiwa, lat, lng });
} else {
invalidCount++;
}
});
parsedImportData = validRows;
if (validRows.length === 0) {
errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama_kk/nama, lat, dan lng terisi).";
errorMsg.style.display = 'block';
return;
}
validRows.forEach(row => {
const tr = document.createElement('tr');
tr.style.borderBottom = '1px solid #e2e8f0';
tr.innerHTML = `
<td style="padding:6px 8px; font-weight: 500;">${row.nama}</td>
<td style="padding:6px 8px;">${row.kategori_bantuan}</td>
<td style="padding:6px 8px;">${row.jumlah_jiwa}</td>
<td style="padding:6px 8px; color: #64748b; font-family: monospace;">${row.lat.toFixed(5)}, ${row.lng.toFixed(5)}</td>
`;
previewTableBody.appendChild(tr);
});
previewCount.textContent = validRows.length;
previewContainer.style.display = 'block';
const statusEl = document.getElementById('importPreviewStatus');
if (statusEl) {
if (invalidCount > 0) {
statusEl.innerHTML = `<span style="color:#e11d48;"><i class="fas fa-exclamation-triangle"></i> ${invalidCount} baris tidak valid diabaikan</span>`;
} else {
statusEl.innerHTML = `<span style="color:#16a34a;"><i class="fas fa-check-circle"></i> Semua data valid</span>`;
}
}
} catch (err) {
console.error(err);
errorMsg.textContent = "Gagal memproses file. Pastikan format file sesuai.";
errorMsg.style.display = 'block';
}
};
reader.readAsText(file);
}
// Initial Load
loadRumahIbadah();
loadPendudukMiskin();
+242
View File
@@ -0,0 +1,242 @@
// Inisialisasi Peta
// Koordinat awal: [-0.0263, 109.3425] (Pontianak)
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
// ===== Toast Notification =====
window.showToast = function(message, type = 'success', duration = 3500) {
const icons = { success: '✅', error: '❌', info: '️', warning: '⚠️' };
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `<span class="toast-icon">${icons[type] || '️'}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('hiding');
toast.addEventListener('animationend', () => toast.remove());
}, duration);
};
// Custom Zoom Control Logic
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); });
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); });
// Base Map dari OpenStreetMap
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Inisialisasi FeatureGroups untuk masing-masing layer
const rumahIbadahLayer = L.featureGroup().addTo(map);
const pendudukMiskinLayer = L.featureGroup().addTo(map);
const geoJsonLayer = L.featureGroup().addTo(map);
const searchInput = document.getElementById('searchInput');
const searchClear = document.getElementById('searchClear');
searchInput.addEventListener('focus', function() {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
});
searchInput.addEventListener('input', function() {
const val = this.value.toLowerCase();
if (val.length > 0) {
searchClear.style.visibility = 'visible';
window.showSearchResults(val);
} else {
searchClear.style.visibility = 'hidden';
document.getElementById('searchResults').style.display = 'none';
}
});
searchClear.addEventListener('click', function() {
searchInput.value = '';
searchClear.style.visibility = 'hidden';
document.getElementById('searchResults').style.display = 'none';
searchInput.focus();
});
window.showSearchResults = function(query) {
const resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = '';
let results = [];
if (typeof rumahIbadahLayer !== 'undefined') {
rumahIbadahLayer.eachLayer(layer => {
if (layer.ibadahData && layer.ibadahData.nama && layer.ibadahData.nama.toLowerCase().includes(query)) {
if(layer instanceof L.Marker) {
results.push({ type: 'Rumah Ibadah', nama: layer.ibadahData.nama, layer: layer });
}
}
});
}
if (results.length === 0) {
resultsContainer.innerHTML = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
} else {
results.forEach(res => {
const item = document.createElement('div');
item.style.padding = '10px 15px';
item.style.cursor = 'pointer';
item.style.borderBottom = '1px solid #eee';
item.style.fontSize = '14px';
item.innerHTML = `<strong>${res.type}:</strong> ${res.nama}`;
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f8f9fa');
item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white');
item.addEventListener('click', () => {
if (res.layer instanceof L.Marker) {
map.setView(res.layer.getLatLng(), 17);
} else if (res.layer.getBounds) {
map.fitBounds(res.layer.getBounds());
}
res.layer.openPopup();
resultsContainer.style.display = 'none';
});
resultsContainer.appendChild(item);
});
}
resultsContainer.style.display = 'block';
};
// UI Logic: Custom Layer Control Toggle
const layerBtn = document.getElementById('layerBtn');
const layerPanel = document.getElementById('layerPanel');
layerBtn.addEventListener('click', function() {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
layerPanel.style.display = layerPanel.style.display === 'block' ? 'none' : 'block';
});
// Sembunyikan layer panel saat klik di peta
map.on('click', function() {
layerPanel.style.display = 'none';
});
// Logic untuk Toggle Layer Visibility
document.getElementById('layerRumahIbadah').addEventListener('change', function(e) {
e.target.checked ? map.addLayer(rumahIbadahLayer) : map.removeLayer(rumahIbadahLayer);
});
document.getElementById('layerMiskin').addEventListener('change', function(e) {
e.target.checked ? map.addLayer(pendudukMiskinLayer) : map.removeLayer(pendudukMiskinLayer);
});
// --- Sub-layer Toggle (expand/collapse) ---
window.toggleSubLayer = function(subId, iconEl) {
const sub = document.getElementById(subId);
if (!sub) return;
const isHidden = sub.style.display === 'none';
sub.style.display = isHidden ? '' : 'none';
iconEl.classList.toggle('collapsed', !isHidden);
};
// --- Sub-layer Filter ---
window.applySubFilter = function(type) {
if (type === 'miskin') {
const checked = [...document.querySelectorAll('.sub-miskin:checked')].map(el => el.value);
pendudukMiskinLayer.eachLayer(layer => {
if (!layer.miskinData) return;
if (checked.includes(layer.miskinData.kategori_bantuan)) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
} else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
}
});
}
};
// --- Modal Logic ---
const unifiedModal = document.getElementById('unifiedModal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
window.openModal = function(title, bodyHTML, saveCallback) {
modalTitle.textContent = title;
modalBody.innerHTML = bodyHTML;
unifiedModal.classList.add('show');
// Ambil elemen tombol save terbaru dari DOM
const currentSaveBtn = document.getElementById('modalSaveBtn');
// Clone untuk menghapus semua event listener lama
const newSaveBtn = currentSaveBtn.cloneNode(true);
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
newSaveBtn.addEventListener('click', saveCallback);
};
window.closeModal = function() {
unifiedModal.classList.remove('show');
};
const confirmModal = document.getElementById('confirmModal');
const confirmMessage = document.getElementById('confirmMessage');
window.openConfirmModal = function(msg, confirmCallback) {
confirmMessage.textContent = msg;
confirmModal.classList.add('show');
const currentYesBtn = document.getElementById('confirmYesBtn');
const newYesBtn = currentYesBtn.cloneNode(true);
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
newYesBtn.addEventListener('click', function() {
confirmModal.classList.remove('show');
confirmCallback();
});
};
window.closeConfirmModal = function() {
confirmModal.classList.remove('show');
};
// --- Action Menu Logic (replaced by left sidebar) ---
window.currentAddMode = null; // 'rumah_ibadah' atau 'miskin_click'
window.activateAddMode = function(mode) {
// Toggle: jika mode yang sama diklik lagi, batalkan
if (window.currentAddMode === mode) {
window.deactivateAddMode();
return;
}
window.currentAddMode = mode;
// Tooltip
const tooltips = {
'rumah_ibadah': 'Klik untuk Tambah Rumah Ibadah'
};
if (window.cursorTooltip && tooltips[mode]) window.cursorTooltip.textContent = tooltips[mode];
// Ubah kursor map menjadi crosshair
document.getElementById('map').style.cursor = 'crosshair';
};
window.deactivateAddMode = function() {
window.currentAddMode = null;
document.getElementById('map').style.cursor = '';
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
};
// --- Custom Tooltip Cursor ---
window.cursorTooltip = document.createElement('div');
window.cursorTooltip.className = 'custom-cursor-tooltip';
document.body.appendChild(window.cursorTooltip);
// Gunakan document-level mousemove agar selalu terpicu
// bahkan saat Leaflet Draw overlay aktif menangkap event map
document.addEventListener('mousemove', function(e) {
if (window.currentAddMode) {
window.cursorTooltip.style.display = 'block';
window.cursorTooltip.style.left = e.pageX + 15 + 'px';
window.cursorTooltip.style.top = e.pageY + 15 + 'px';
} else {
window.cursorTooltip.style.display = 'none';
}
});
+465
View File
@@ -0,0 +1,465 @@
// ===== Panel Management =====
// Mengelola Left Sidebar Toggle + Right Panel + Layer Control
(function () {
// ---- Konstanta Jenis Ibadah ----
const IBADAH_EMOJI = {
'Masjid': '🕌',
'Gereja': '⛪',
'Pura': '🛕',
'Vihara': '🪷',
'Kelenteng': '🏮',
};
window.IBADAH_EMOJI = IBADAH_EMOJI;
// ---- State ----
let activePanel = null;
// ---- Elements ----
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
const leftSidebar = document.getElementById('leftSidebar');
const rightPanel = document.getElementById('rightPanel');
const rightPanelTitle = document.getElementById('rightPanelTitle');
const rightPanelActions = document.getElementById('rightPanelActions');
const rightPanelBody = document.getElementById('rightPanelBody');
const rightPanelClose = document.getElementById('rightPanelClose');
// ---- Sidebar Toggle ----
sidebarToggleBtn.addEventListener('click', function () {
const isOpen = leftSidebar.style.display !== 'none';
if (isOpen) {
leftSidebar.style.display = 'none';
sidebarToggleBtn.classList.remove('open');
sidebarToggleIcon.className = 'fas fa-chevron-right';
} else {
leftSidebar.style.display = 'flex';
sidebarToggleBtn.classList.add('open');
sidebarToggleIcon.className = 'fas fa-chevron-left';
}
});
// ---- Sidebar Buttons ----
document.querySelectorAll('.sidebar-btn').forEach(btn => {
btn.addEventListener('click', function () {
const panel = this.dataset.panel;
if (activePanel === panel) {
closePanel();
} else {
openPanel(panel);
}
});
});
rightPanelClose.addEventListener('click', closePanel);
// ---- Open / Close Panel ----
function openPanel(panel) {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
activePanel = panel;
rightPanel.classList.add('open');
document.body.classList.add('right-panel-open');
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
const activeBtn = document.querySelector(`.sidebar-btn[data-panel="${panel}"]`);
if (activeBtn) activeBtn.classList.add('active');
renderPanel(panel);
}
function closePanel() {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
activePanel = null;
rightPanel.classList.remove('open');
document.body.classList.remove('right-panel-open');
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
}
window.refreshActivePanel = function () {
if (activePanel && activePanel !== 'layer') renderPanel(activePanel);
};
// ---- Render Panel by Type ----
function renderPanel(type) {
rightPanelBody.innerHTML = '<div class="panel-empty">Memuat data...</div>';
rightPanelActions.innerHTML = '';
switch (type) {
case 'ibadah': renderIbadahPanel(); break;
case 'miskin': renderMiskinPanel(); break;
}
}
// ==========================
// ===== Ibadah Panel =======
// ==========================
function renderIbadahPanel() {
rightPanelTitle.textContent = '🕌 Rumah Ibadah';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
if (isAdmin) {
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddIbadah">
<i class="fas fa-plus"></i> Tambah Ibadah
</button>`;
document.getElementById('btnPanelAddIbadah').addEventListener('click', () => {
window.activateAddMode('rumah_ibadah');
});
} else {
rightPanelActions.innerHTML = '';
}
const items = [];
rumahIbadahLayer.eachLayer(l => {
if (l.ibadahData && l instanceof L.Marker) items.push(l.ibadahData);
});
if (!items.length) {
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data rumah ibadah</div>';
return;
}
rightPanelBody.innerHTML = '';
items.forEach(d => {
const emoji = IBADAH_EMOJI[d.jenis] || '🕌';
const card = document.createElement('div');
card.className = 'data-card';
let actionButtons = '';
if (isAdmin) {
actionButtons = `
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
`;
}
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon card-icon-ibadah">${emoji}</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">${escHtml(d.jenis || 'Masjid')} • 📡 ${d.radius} m</div>
</div>
${actionButtons}
</div>`;
if (isAdmin) {
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditIbadahModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteIbadah(d.id); });
}
card.addEventListener('click', () => flyToIbadah(d.id));
rightPanelBody.appendChild(card);
});
}
function flyToIbadah(id) {
rumahIbadahLayer.eachLayer(l => {
if (l.ibadahData && l.ibadahData.id == id && l instanceof L.Marker) {
map.setView(l.getLatLng(), 17); l.openPopup();
}
});
}
// ==========================
// ===== Miskin Panel =======
// ==========================
function renderMiskinPanel() {
rightPanelTitle.textContent = '🏠 Penduduk Miskin';
const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
if (canManage) {
const isPengelola = window.currentUser.role === 'pengelola';
const addLabel = isPengelola
? '<i class="fas fa-plus"></i> Tambah <span style="font-size:10px;opacity:0.8;">(dalam radius)</span>'
: '<i class="fas fa-plus"></i> Tambah';
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelTambahMiskin">
${addLabel}
</button>
<button class="btn-panel-import" id="btnPanelImportMiskin">
<i class="fas fa-file-import"></i> Impor
</button>`;
document.getElementById('btnPanelTambahMiskin').addEventListener('click', () => {
window.currentAddMode = 'miskin_click';
document.getElementById('map').style.cursor = 'crosshair';
if (window.cursorTooltip) {
const tipText = isPengelola
? 'Klik di dalam radius ibadah Anda untuk tambah penduduk'
: 'Klik di peta untuk tentukan lokasi penduduk miskin';
window.cursorTooltip.textContent = tipText;
}
});
document.getElementById('btnPanelImportMiskin').addEventListener('click', () => {
if (typeof window.openImportMiskinModal === 'function') {
window.openImportMiskinModal();
} else {
alert('Fitur impor belum siap.');
}
});
} else {
rightPanelActions.innerHTML = '';
}
const all = [];
pendudukMiskinLayer.eachLayer(l => { if (l.miskinData) all.push(l.miskinData); });
if (!all.length) {
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data penduduk miskin</div>';
return;
}
const inRadius = (d) => {
if (typeof ibadahDataList === 'undefined') return false;
for (const ib of ibadahDataList) {
const dist = L.latLng(d.lat, d.lng).distanceTo(L.latLng(ib.lat, ib.lng));
if (dist <= ib.radius) return true;
}
return false;
};
rightPanelBody.innerHTML = '';
const terurusItems = all.filter(d => inRadius(d));
const tidakItems = all.filter(d => !inRadius(d));
if (terurusItems.length) {
const h1 = document.createElement('div');
h1.className = 'panel-section-title';
h1.innerHTML = `✅ Terurus <span style="font-weight:400;text-transform:none;font-size:10px;">(${terurusItems.length})</span>`;
rightPanelBody.appendChild(h1);
terurusItems.forEach(d => rightPanelBody.appendChild(buildMiskinCard(d, true)));
}
if (tidakItems.length) {
const h2 = document.createElement('div');
h2.className = 'panel-section-title';
h2.innerHTML = `⚠️ Tidak Terurus <span style="font-weight:400;text-transform:none;font-size:10px;">(${tidakItems.length})</span>`;
rightPanelBody.appendChild(h2);
tidakItems.forEach(d => rightPanelBody.appendChild(buildMiskinCard(d, false)));
}
}
function buildMiskinCard(d, terurus) {
// Pengelola hanya boleh edit/hapus penduduk yang dalam radius ibadahnya
const isPengelolaOrAdmin = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
const canManage = isPengelolaOrAdmin && (typeof window.isInMyIbadahRadius === 'function'
? window.isInMyIbadahRadius(d.lat, d.lng)
: true);
const iconCls = terurus ? 'card-icon-miskin-in' : 'card-icon-miskin-out';
const badge = d.kategori_bantuan === 'Makan'
? '<span class="data-card-badge badge-orange">Bantuan Makan</span>'
: '<span class="data-card-badge badge-blue">Pemberdayaan</span>';
const card = document.createElement('div');
card.className = 'data-card';
card.dataset.miskinId = d.id;
let actionButtons = '';
if (canManage) {
actionButtons = `
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
`;
}
let fotoHtml = '';
if (d.foto_rumah || d.foto_kk) {
fotoHtml += `<div class="expand-row-photos" style="display:flex; gap:10px; margin-top:8px; justify-content: space-around;">`;
if (d.foto_rumah) {
fotoHtml += `
<div style="text-align:center;">
<span style="font-size:10px; display:block; font-weight:500; color:#555;">Foto Rumah</span>
<a href="uploads/${d.foto_rumah}" target="_blank">
<img src="uploads/${d.foto_rumah}" style="width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid #ddd; margin-top:3px;" />
</a>
</div>`;
}
if (d.foto_kk) {
fotoHtml += `
<div style="text-align:center;">
<span style="font-size:10px; display:block; font-weight:500; color:#555;">Foto KK</span>
<a href="uploads/${d.foto_kk}" target="_blank">
<img src="uploads/${d.foto_kk}" style="width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid #ddd; margin-top:3px;" />
</a>
</div>`;
}
fotoHtml += `</div>`;
}
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon ${iconCls}">🏠</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">👨‍👩‍👧 ${d.jumlah_jiwa || '-'} jiwa</div>
${badge}
</div>
${actionButtons}
</div>
<div class="data-card-expand">
<div class="expand-row"><span>Kepala Keluarga</span><span>${escHtml(d.nama)}</span></div>
<div class="expand-row"><span>Kategori Bantuan</span><span>${escHtml(d.kategori_bantuan)}</span></div>
<div class="expand-row"><span>Jumlah Jiwa</span><span>${d.jumlah_jiwa || '-'}</span></div>
<div class="expand-row"><span>Status</span><span>${terurus ? 'Terurus ✅' : 'Tidak Terurus ⚠️'}</span></div>
${fotoHtml}
<button class="btn-log-bantuan" data-id="${d.id}" data-nama="${escHtml(d.nama)}">
<i class="fas fa-history"></i> Lihat Log Bantuan
</button>
</div>`;
if (canManage) {
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditMiskinModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteMiskin(d.id); });
}
card.querySelector('.btn-log-bantuan').addEventListener('click', (e) => { e.stopPropagation(); openLogBantuan(d.id, d.nama); });
card.querySelector('.data-card-main').addEventListener('click', () => {
card.classList.toggle('expanded');
flyToMiskin(d.id);
});
return card;
}
function flyToMiskin(id) {
pendudukMiskinLayer.eachLayer(l => {
if (l.miskinData && l.miskinData.id == id) {
map.setView(l.getLatLng(), 17); l.openPopup();
}
});
}
// ---- Hook: klik peta saat mode miskin_click ----
// Ditangkap oleh kemiskinan.js via contextmenu — mode ini pakai klik kiri
map.on('click', function(e) {
if (window.currentAddMode === 'miskin_click') {
window.currentAddMode = null;
document.getElementById('map').style.cursor = '';
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
// Buka form tambah miskin
if (typeof window.formAddMiskin === 'function') {
window.formAddMiskin(e.latlng.lat, e.latlng.lng);
}
}
});
// ==========================
// ===== Log Bantuan Modal ==
// ==========================
const logModal = document.getElementById('logBantuanModal');
const logTitle = document.getElementById('logBantuanTitle');
const logList = document.getElementById('logBantuanList');
const logSaveBtn = document.getElementById('logSaveBtn');
const logIbadahSel = document.getElementById('logIbadahId');
let _logMiskinId = null;
window.openLogBantuan = function (miskinId, nama) {
_logMiskinId = miskinId;
logTitle.textContent = `Log Bantuan — ${nama}`;
logModal.classList.add('show');
const isAuth = !!window.currentUser;
const formContainer = document.querySelector('#logBantuanModal .modal-body > div:last-child');
const modalFooter = document.querySelector('#logBantuanModal .modal-footer');
if (isAuth) {
if (formContainer) formContainer.style.display = 'block';
if (modalFooter) modalFooter.style.display = 'flex';
} else {
if (formContainer) formContainer.style.display = 'none';
if (modalFooter) modalFooter.style.display = 'none';
}
// Isi dropdown ibadah
logIbadahSel.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>';
if (typeof ibadahDataList !== 'undefined') {
ibadahDataList.forEach(ib => {
const opt = document.createElement('option');
opt.value = ib.id;
opt.textContent = `${IBADAH_EMOJI[ib.jenis] || '🕌'} ${ib.nama}`;
logIbadahSel.appendChild(opt);
});
}
if (window.currentUser && window.currentUser.role === 'pengelola') {
logIbadahSel.value = window.currentUser.ibadah_id;
logIbadahSel.disabled = true;
} else {
logIbadahSel.disabled = false;
}
// Set tanggal default hari ini
document.getElementById('logTanggal').valueAsDate = new Date();
fetchLogBantuan(miskinId);
};
window.closeLogBantuanModal = function () {
logModal.classList.remove('show');
_logMiskinId = null;
};
function fetchLogBantuan(miskinId) {
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Memuat...</div>';
fetch(`api/log_bantuan/read.php?miskin_id=${miskinId}`)
.then(r => r.json())
.then(data => {
if (!data.data || !data.data.length) {
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Belum ada riwayat bantuan</div>';
return;
}
logList.innerHTML = '';
data.data.forEach(log => {
const tgl = new Date(log.tanggal).toLocaleDateString('id-ID', { day:'2-digit', month:'short', year:'numeric' });
const item = document.createElement('div');
item.className = 'log-item';
item.innerHTML = `
<div class="log-dot"></div>
<div class="log-info">
<div class="log-type">${escHtml(log.tipe_bantuan)}</div>
<div class="log-meta">${IBADAH_EMOJI[log.jenis_ibadah] || '🕌'} ${escHtml(log.nama_ibadah)}${tgl}</div>
${log.keterangan ? `<div class="log-keterangan">${escHtml(log.keterangan)}</div>` : ''}
</div>`;
logList.appendChild(item);
});
})
.catch(() => {
logList.innerHTML = '<div class="panel-empty" style="padding:15px; color:#ef4444;">Gagal memuat data</div>';
});
}
logSaveBtn.addEventListener('click', function () {
const ibadah_id = logIbadahSel.value;
const tipe_bantuan = document.getElementById('logTipeBantuan').value;
const tanggal = document.getElementById('logTanggal').value;
const keterangan = document.getElementById('logKeterangan').value;
if (!ibadah_id) { alert('Pilih rumah ibadah terlebih dahulu!'); return; }
if (!tanggal) { alert('Tanggal harus diisi!'); return; }
fetch('api/log_bantuan/create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ miskin_id: _logMiskinId, ibadah_id, tipe_bantuan, tanggal, keterangan })
})
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
document.getElementById('logKeterangan').value = '';
fetchLogBantuan(_logMiskinId);
} else {
alert(data.message || 'Gagal menyimpan');
}
});
});
// ---- Utility ----
function escHtml(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ---- Expose ----
window.openRightPanel = openPanel;
})();
+90
View File
@@ -0,0 +1,90 @@
-- Database schema for WebGIS Pemetaan Kemiskinan Pontianak
CREATE DATABASE IF NOT EXISTS webgis;
USE webgis;
-- 1. Tabel rumah_ibadah
DROP TABLE IF EXISTS `log_bantuan`;
DROP TABLE IF EXISTS `rumah_ibadah`;
CREATE TABLE `rumah_ibadah` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama` varchar(255) NOT NULL,
`jenis` varchar(100) NOT NULL,
`alamat` text,
`radius` double NOT NULL DEFAULT 0,
`lat` double NOT NULL,
`lng` double NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. Tabel penduduk_miskin
DROP TABLE IF EXISTS `penduduk_miskin`;
CREATE TABLE `penduduk_miskin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama` varchar(255) NOT NULL,
`kategori_bantuan` varchar(255) NOT NULL,
`jumlah_jiwa` int(11) NOT NULL DEFAULT 1,
`lat` double NOT NULL,
`lng` double NOT NULL,
`foto_rumah` varchar(255) DEFAULT NULL,
`foto_kk` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. Tabel log_bantuan
CREATE TABLE `log_bantuan` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`miskin_id` int(11) NOT NULL,
`ibadah_id` int(11) NOT NULL,
`tipe_bantuan` varchar(255) NOT NULL,
`tanggal` date NOT NULL,
`keterangan` text,
PRIMARY KEY (`id`),
KEY `fk_log_miskin` (`miskin_id`),
KEY `fk_log_ibadah` (`ibadah_id`),
CONSTRAINT `fk_log_ibadah` FOREIGN KEY (`ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_log_miskin` FOREIGN KEY (`miskin_id`) REFERENCES `penduduk_miskin` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data Contoh (Sample Data) untuk Pontianak
-- Data Contoh Rumah Ibadah
INSERT INTO `rumah_ibadah` (`nama`, `jenis`, `alamat`, `radius`, `lat`, `lng`) VALUES
('Masjid Raya Mujahidin', 'Masjid', 'Jl. Jenderal Ahmad Yani, Akcaya, Kec. Pontianak Sel.', 500, -0.0435, 109.3440),
('Gereja Katedral Santo Yosef', 'Gereja', 'Jl. Patria Tama, Darat Sekip, Kec. Pontianak Kota', 500, -0.0278, 109.3370),
('Vihara Bodhisatva Karaniya Metta', 'Vihara', 'Jl. Kapten Marsan, Darat Sekip, Kec. Pontianak Kota', 500, -0.0232, 109.3432);
-- Data Contoh Penduduk Miskin
INSERT INTO `penduduk_miskin` (`nama`, `kategori_bantuan`, `jumlah_jiwa`, `lat`, `lng`) VALUES
('Budi Santoso', 'Makan', 4, -0.0420, 109.3415),
('Siti Aminah', 'Pemberdayaan', 3, -0.0285, 109.3395),
('Ahmad Subarjo', 'Makan', 5, -0.0245, 109.3448);
-- Data Contoh Log Bantuan
INSERT INTO `log_bantuan` (`miskin_id`, `ibadah_id`, `tipe_bantuan`, `tanggal`, `keterangan`) VALUES
(1, 1, 'Sembako', '2026-05-15', 'Pembagian paket sembako bulanan'),
(2, 2, 'Pendidikan', '2026-05-20', 'Bantuan biaya sekolah anak'),
(3, 1, 'Uang Tunai', '2026-06-01', 'Santunan tunai darurat');
-- 7. Tabel users
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(100) NOT NULL UNIQUE,
`password` varchar(255) NOT NULL,
`role` enum('admin', 'pengelola') NOT NULL,
`ibadah_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_users_ibadah` FOREIGN KEY (`ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data Contoh Pengguna (Default Users)
-- Password admin: password
-- Password pengelola: password (terhubung ke Masjid Raya Mujahidin, ibadah_id = 1)
INSERT INTO `users` (`username`, `password`, `role`, `ibadah_id`) VALUES
('admin', '$2y$10$qqKxivn4646czFu/ffwa/uZxJrkj3T9xmsrRbyxJbHx6jllpOOfDO', 'admin', NULL),
('pengelola_mujahidin', '$2y$10$qqKxivn4646czFu/ffwa/uZxJrkj3T9xmsrRbyxJbHx6jllpOOfDO', 'pengelola', 1);
+408
View File
@@ -0,0 +1,408 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pemetaan Kemiskinan</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- FontAwesome (Icons) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<!-- UI Container over map -->
<div class="ui-container">
<!-- Search Bar -->
<div class="search-bar">
<div class="search-icon">
<i class="fas fa-search"></i>
</div>
<input type="text" class="search-input" id="searchInput" placeholder="Cari rumah ibadah...">
<button class="search-clear" id="searchClear">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Search Results -->
<div id="searchResults" style="display: none; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto;">
</div>
</div>
<!-- Custom Zoom Control -->
<div class="custom-zoom-control" style="position: absolute; top: 20px; left: 20px; z-index: 1000; display: flex; flex-direction: column; gap: 5px;">
<button class="custom-layer-btn" id="zoomInBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom In"><i class="fas fa-plus"></i></button>
<button class="custom-layer-btn" id="zoomOutBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom Out"><i class="fas fa-minus"></i></button>
</div>
<!-- Sidebar Toggle Button -->
<button class="sidebar-toggle-btn" id="sidebarToggleBtn" title="Menu">
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button>
<!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
<button class="sidebar-btn" id="menuIbadah" data-panel="ibadah" title="Rumah Ibadah">
<span class="sidebar-emoji">🕌</span>
<span class="sidebar-label">Ibadah</span>
</button>
<button class="sidebar-btn" id="menuMiskin" data-panel="miskin" title="Penduduk Miskin">
<span class="sidebar-emoji">🏠</span>
<span class="sidebar-label">Miskin</span>
</button>
<button class="sidebar-btn" id="menuDashboard" style="display:none;" title="Dashboard">
<span class="sidebar-emoji">📊</span>
<span class="sidebar-label">Dashboard</span>
</button>
<div class="sidebar-divider" id="sidebarAdminDivider" style="display:none;"></div>
<button class="sidebar-btn" id="menuUsers" style="display:none;" title="Manajemen Pengguna">
<span class="sidebar-emoji">👥</span>
<span class="sidebar-label">User</span>
</button>
</nav>
<!-- Custom Layer Control Button -->
<button class="custom-layer-btn" id="layerBtn" title="Atur Layer">
<i class="fas fa-layer-group fa-lg"></i>
</button>
<div>
<!-- Custom Layer Control Panel -->
<div class="custom-layer-panel" id="layerPanel">
<h3>Daftar Layer</h3>
<div class="layer-section-title">Pemetaan Kemiskinan</div>
<!-- Rumah Ibadah -->
<label class="layer-option">
<input type="checkbox" id="layerRumahIbadah" checked> Rumah Ibadah
</label>
<!-- Penduduk Miskin -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerMiskin" checked> Penduduk Miskin
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subMiskin', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subMiskin" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-miskin" value="Makan" checked onchange="applySubFilter('miskin')"> Bantuan Makan
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-miskin" value="Pemberdayaan" checked onchange="applySubFilter('miskin')"> Pemberdayaan
</label>
</div>
</div>
<!-- GeoJSON Eksternal -->
<div class="layer-group" style="margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px;">
<div class="layer-group-header" onclick="window.toggleGeoJsonMenu()" style="cursor:pointer; display:flex; justify-content:space-between; align-items:center; font-size:14px; margin-bottom:8px; font-weight: 500;">
<span><i class="fas fa-folder"></i> GeoJSON Eksternal</span>
<i id="geoJsonToggleIcon" class="fas fa-plus"></i>
</div>
<div id="geoJsonFileList" style="display:none; padding-left:15px; margin-bottom:10px;">
<div id="geoJsonLayersContainer"></div>
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ddd;">
<label style="font-size: 12px; display:block; margin-bottom: 5px;">Import GeoJSON Baru:</label>
<input type="file" id="fileGeoJson" accept=".geojson,.json" style="font-size: 12px; max-width: 100%;">
</div>
</div>
</div>
</div><!-- /custom-layer-panel -->
<!-- Right Data Panel -->
<div class="right-panel" id="rightPanel">
<div class="right-panel-header">
<h3 id="rightPanelTitle">Data</h3>
<button class="right-panel-close" id="rightPanelClose"><i class="fas fa-times"></i></button>
</div>
<div class="right-panel-actions" id="rightPanelActions"></div>
<div class="right-panel-body" id="rightPanelBody">
<div class="panel-empty">Pilih menu di kiri untuk menampilkan data</div>
</div>
</div><!-- /right-panel -->
</div>
<!-- Map Container -->
<div id="map"></div>
<!-- Unified Modal -->
<div id="unifiedModal" class="unified-modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Judul Form</h3>
<span class="modal-close" onclick="closeModal()">&times;</span>
</div>
<div class="modal-body" id="modalBody">
<!-- Konten dinamis -->
</div>
<div class="modal-footer" id="modalFooter">
<button id="modalSaveBtn" class="btn-save">Simpan</button>
<button class="btn-cancel" onclick="closeModal()">Batal</button>
</div>
</div>
</div>
<!-- Confirm Modal -->
<div id="confirmModal" class="unified-modal">
<div class="modal-content" style="width: 300px;">
<div class="modal-header">
<h3>Konfirmasi</h3>
</div>
<div class="modal-body">
<p id="confirmMessage"></p>
</div>
<div class="modal-footer" style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="confirmYesBtn" class="btn-save" style="background-color: #dc3545;">Ya, Hapus</button>
<button class="btn-cancel" onclick="closeConfirmModal()">Batal</button>
</div>
</div>
</div>
<!-- Log Bantuan Modal -->
<div id="logBantuanModal" class="unified-modal">
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
<div class="modal-header">
<h3 id="logBantuanTitle">Log Bantuan</h3>
<span class="modal-close" onclick="closeLogBantuanModal()">&times;</span>
</div>
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
<!-- Riwayat -->
<div style="padding:15px; border-bottom:1px solid #eee;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
</div>
</div>
<!-- Form Tambah -->
<div style="padding:15px;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
<div class="form-group">
<label>Rumah Ibadah</label>
<select id="logIbadahId"></select>
</div>
<div class="form-group">
<label>Tipe Bantuan</label>
<select id="logTipeBantuan">
<option value="Sembako">Sembako</option>
<option value="Uang Tunai">Uang Tunai</option>
<option value="Pakaian">Pakaian</option>
<option value="Pendidikan">Pendidikan</option>
<option value="Kesehatan">Kesehatan</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="form-group">
<label>Tanggal</label>
<input type="date" id="logTanggal">
</div>
<div class="form-group">
<label>Keterangan (opsional)</label>
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
</div>
</div>
</div>
<!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget">
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
</div>
<!-- Login Modal -->
<div id="loginModal" class="unified-modal">
<div class="modal-content" style="width: 360px;">
<div class="modal-header">
<h3>Login Ke Sistem</h3>
<span class="modal-close" id="closeLoginModal">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="loginUsername">Username</label>
<input type="text" id="loginUsername" placeholder="Masukkan username">
</div>
<div class="form-group">
<label for="loginPassword">Password</label>
<input type="password" id="loginPassword" placeholder="Masukkan password">
</div>
<div id="loginErrorMsg" style="display:none; color:#ef4444; font-size:12px; font-weight:500;"></div>
</div>
<div class="modal-footer">
<button id="loginSubmitBtn" class="btn-save" style="width: 100%;">Masuk</button>
</div>
</div>
</div>
<!-- Dashboard Modal -->
<div id="dashboardModal" class="unified-modal">
<div class="modal-content">
<div class="modal-header">
<h3>📊 Dashboard Informasi</h3>
<span class="modal-close" id="closeDashboardModal">&times;</span>
</div>
<div id="dashboardBody"></div>
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="unified-modal">
<div class="modal-content" style="width: 700px; max-width: 95%;">
<div class="modal-header">
<h3>Manajemen Pengguna (Admin)</h3>
<span class="modal-close" id="closeUserManagementModal">&times;</span>
</div>
<div class="modal-body" style="gap: 15px;">
<!-- Form Tambah/Edit User -->
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
<input type="hidden" id="manageUserId" value="">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Username</label>
<input type="text" id="manageUsername" placeholder="Username">
</div>
<div class="form-group">
<label>Password (Kosongkan jika tidak diubah)</label>
<input type="password" id="managePassword" placeholder="Password">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Role</label>
<select id="manageRole">
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group" id="manageIbadahGroup">
<label>Rumah Ibadah yang Dikelola</label>
<select id="manageIbadahId">
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
</select>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
</div>
</div>
<!-- Tabel Daftar User -->
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
<th style="padding: 10px 12px;">Username</th>
<th style="padding: 10px 12px;">Role</th>
<th style="padding: 10px 12px;">Rumah Ibadah</th>
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- Diisi dinamis -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer"></div>
<!-- Disclaimer Banner -->
<div id="disclaimerBanner" style="
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 2000;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
color: #f1f5f9;
border: 1px solid #f59e0b;
border-left: 4px solid #f59e0b;
border-radius: 10px;
padding: 12px 16px;
max-width: 520px;
width: calc(100% - 40px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
display: flex;
align-items: flex-start;
gap: 12px;
font-family: 'Inter', sans-serif;
animation: slideUpBanner 0.4s ease;
">
<span style="font-size: 20px; flex-shrink:0; margin-top:1px;">⚠️</span>
<div style="flex:1;">
<div style="font-size: 13px; font-weight: 700; color: #fbbf24; margin-bottom: 4px; letter-spacing: 0.3px;">DISCLAIMER DATA DUMMY</div>
<div style="font-size: 12px; line-height: 1.6; color: #cbd5e1;">
Seluruh data pada aplikasi ini termasuk <strong style="color:#fde68a;">nama kepala keluarga, foto rumah, dan foto Kartu Keluarga (KK)</strong> merupakan <strong style="color:#fde68a;">data fiktif / dummy</strong> yang hanya digunakan untuk keperluan demonstrasi dan pengujian sistem. Bukan data nyata.
</div>
</div>
<button onclick="document.getElementById('disclaimerBanner').style.display='none'" style="
background: transparent;
border: 1px solid #475569;
color: #94a3b8;
border-radius: 6px;
padding: 3px 8px;
cursor: pointer;
font-size: 11px;
flex-shrink: 0;
margin-top: 1px;
transition: all 0.2s;
" onmouseover="this.style.borderColor='#f59e0b';this.style.color='#f59e0b'" onmouseout="this.style.borderColor='#475569';this.style.color='#94a3b8'">Tutup</button>
</div>
<style>
@keyframes slideUpBanner {
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
</style>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Main Map Initialization -->
<script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen -->
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
<script src="assets/js/features/kemiskinan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
<!-- Core Auth Feature -->
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
<!-- Dashboard Feature -->
<script src="assets/js/features/dashboard.js?v=<?= time() ?>"></script>
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
nama,kategori_bantuan,jumlah_jiwa,lat,lng
Budi Santoso,Makan,4,-0.025,109.340
Siti Aminah,Pemberdayaan,3,-0.028,109.345
Andi Wijaya,Makan,5,-0.024,109.341
1 nama kategori_bantuan jumlah_jiwa lat lng
2 Budi Santoso Makan 4 -0.025 109.340
3 Siti Aminah Pemberdayaan 3 -0.028 109.345
4 Andi Wijaya Makan 5 -0.024 109.341
@@ -0,0 +1,29 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"nama": "Joko Widodo",
"kategori_bantuan": "Pemberdayaan",
"jumlah_jiwa": 2
},
"geometry": {
"type": "Point",
"coordinates": [109.343, -0.027]
}
},
{
"type": "Feature",
"properties": {
"nama": "Mega Wati",
"kategori_bantuan": "Makan",
"jumlah_jiwa": 6
},
"geometry": {
"type": "Point",
"coordinates": [109.344, -0.029]
}
}
]
}