Compare commits

...

2 Commits

Author SHA1 Message Date
envicy d950565c7c Menyelesaikan conflict: menghapus file zip mengikuti server 2026-06-11 22:35:14 +07:00
envicy ce26f9fac6 Update 2026-06-11 22:29:16 +07:00
14 changed files with 1933 additions and 2015 deletions
-84
View File
@@ -1,84 +0,0 @@
# 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.
@@ -1,196 +0,0 @@
<?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,
]);
@@ -1,171 +0,0 @@
<?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);
@@ -59,42 +59,34 @@ if ($currentUser && $currentUser['role'] === 'pengelola') {
}
}
if ($lat != 0 && $lng != 0) {
$upload_dir = "../../uploads/";
if (!file_exists($upload_dir)) {
// Handle file upload
$upload_dir = dirname(dirname(__DIR__)) . '/uploads/';
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
}
$foto_rumah = null;
$foto_kk = null;
function uploadFile($fileKey, $upload_dir) {
function uploadFileCreate($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)) {
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
if (in_array($fileExtension, $allowedExtensions)) {
$newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\\._-]/', '_', $fileName);
if (move_uploaded_file($fileTmpPath, $upload_dir . $newFileName)) {
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";
$foto_rumah = uploadFileCreate('foto_rumah', $upload_dir);
$foto_kk = uploadFileCreate('foto_kk', $upload_dir);
$val_foto_rumah = $foto_rumah ? "'" . $conn->real_escape_string($foto_rumah) . "'" : "NULL";
$val_foto_kk = $foto_kk ? "'" . $conn->real_escape_string($foto_kk) . "'" : "NULL";
if ($lat != 0 && $lng != 0) {
$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)";
@@ -81,7 +81,7 @@ if ($id > 0) {
$old_foto_kk = $row['foto_kk'];
}
$upload_dir = "../../uploads/";
$upload_dir = dirname(dirname(__DIR__)) . '/uploads/';
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
File diff suppressed because it is too large Load Diff
@@ -77,7 +77,7 @@
if (dashboardBtn) dashboardBtn.style.display = 'flex';
// Show user management button in sidebar for Admin
const adminDivider = document.getElementById('sidebarAdminDivider');
const adminDivider = document.getElementById('adminNavLabel');
if (window.currentUser.role === 'admin') {
if (menuUsersBtn) menuUsersBtn.style.display = 'flex';
if (adminDivider) adminDivider.style.display = 'block';
@@ -93,7 +93,7 @@
document.getElementById('authLoginBtn').addEventListener('click', showLoginModal);
if (menuUsersBtn) menuUsersBtn.style.display = 'none';
if (dashboardBtn) dashboardBtn.style.display = 'none';
const adminDivider = document.getElementById('sidebarAdminDivider');
const adminDivider = document.getElementById('adminNavLabel');
if (adminDivider) adminDivider.style.display = 'none';
}
}
@@ -9,7 +9,8 @@ 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);
const zoomContainer = document.querySelector('.zoom-controls');
if (zoomContainer) zoomContainer.appendChild(geoBtn);
let userMarker = null;
@@ -444,8 +444,8 @@ function addMiskinMarker(item) {
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"
<a href="uploads/${d.foto_rumah}" target="_blank">
<img src="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>
@@ -456,8 +456,8 @@ function addMiskinMarker(item) {
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"
<a href="uploads/${d.foto_kk}" target="_blank">
<img src="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>
@@ -590,8 +590,8 @@ window.openEditMiskinModal = function(id) {
<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;"
<a href="uploads/${d.foto_rumah}" target="_blank">
<img src="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>
@@ -603,8 +603,8 @@ window.openEditMiskinModal = function(id) {
<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;"
<a href="uploads/${d.foto_kk}" target="_blank">
<img src="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>
+73 -91
View File
@@ -1,5 +1,8 @@
// ============================================================
// WebGIS Kemiskinan Pontianak — map.js (Redesign)
// ============================================================
// Inisialisasi Peta
// Koordinat awal: [-0.0263, 109.3425] (Pontianak)
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
// ===== Toast Notification =====
@@ -7,86 +10,94 @@ 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(); });
// ===== Zoom Controls =====
document.getElementById('zoomInBtn').addEventListener('click', () => map.zoomIn());
document.getElementById('zoomOutBtn').addEventListener('click', () => map.zoomOut());
// Base Map dari OpenStreetMap
// ===== Base Map =====
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// Inisialisasi FeatureGroups untuk masing-masing layer
// ===== Feature Groups =====
const rumahIbadahLayer = L.featureGroup().addTo(map);
const pendudukMiskinLayer = L.featureGroup().addTo(map);
const geoJsonLayer = L.featureGroup().addTo(map);
// ===== Sidebar Collapse =====
const collapseBtn = document.getElementById('sidebarCollapseBtn');
const collapseIcon = document.getElementById('collapseIcon');
if (collapseBtn) {
collapseBtn.addEventListener('click', function() {
const collapsed = document.body.classList.toggle('sidebar-collapsed');
if (collapseIcon) collapseIcon.className = collapsed ? 'fas fa-chevron-right' : 'fas fa-chevron-left';
setTimeout(() => map.invalidateSize(), 320);
});
}
// ===== Search =====
const searchInput = document.getElementById('searchInput');
const searchClear = document.getElementById('searchClear');
const searchResultsEl = document.getElementById('searchResults');
searchInput.addEventListener('focus', function() {
searchInput.addEventListener('focus', () => {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
});
searchInput.addEventListener('input', function() {
const val = this.value.toLowerCase();
const val = this.value.toLowerCase().trim();
if (val.length > 0) {
searchClear.style.visibility = 'visible';
window.showSearchResults(val);
} else {
searchClear.style.visibility = 'hidden';
document.getElementById('searchResults').style.display = 'none';
searchResultsEl.style.display = 'none';
}
});
searchClear.addEventListener('click', function() {
searchInput.value = '';
searchClear.style.visibility = 'hidden';
document.getElementById('searchResults').style.display = 'none';
searchResultsEl.style.display = 'none';
searchInput.focus();
});
document.addEventListener('click', function(e) {
if (!e.target.closest('.search-bar') && !e.target.closest('#searchResults')) {
searchResultsEl.style.display = 'none';
}
});
window.showSearchResults = function(query) {
const resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = '';
searchResultsEl.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 (layer.ibadahData?.nama?.toLowerCase().includes(query) && layer instanceof L.Marker) {
results.push({ type: 'Rumah Ibadah', icon: '🕌', nama: layer.ibadahData.nama, layer });
}
});
}
if (results.length === 0) {
resultsContainer.innerHTML = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
searchResultsEl.innerHTML = '<div class="search-result-item" style="color:#94a3b8;">Tidak ada hasil ditemukan</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.className = 'search-result-item';
item.innerHTML = `<span>${res.icon}</span><span><strong style="font-size:10px; color:#94a3b8; text-transform:uppercase;">${res.type}</strong><br>${res.nama}</span>`;
item.addEventListener('click', () => {
if (res.layer instanceof L.Marker) {
map.setView(res.layer.getLatLng(), 17);
@@ -94,38 +105,40 @@ window.showSearchResults = function(query) {
map.fitBounds(res.layer.getBounds());
}
res.layer.openPopup();
resultsContainer.style.display = 'none';
searchResultsEl.style.display = 'none';
searchInput.value = res.nama;
searchClear.style.visibility = 'visible';
});
resultsContainer.appendChild(item);
searchResultsEl.appendChild(item);
});
}
resultsContainer.style.display = 'block';
searchResultsEl.style.display = 'block';
};
// UI Logic: Custom Layer Control Toggle
// ===== Layer Panel 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';
const isOpen = layerPanel.classList.toggle('show');
layerBtn.classList.toggle('active', isOpen);
});
// Sembunyikan layer panel saat klik di peta
map.on('click', function() {
layerPanel.style.display = 'none';
layerPanel.classList.remove('show');
layerBtn.classList.remove('active');
});
// Logic untuk Toggle Layer Visibility
// ===== 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;
@@ -134,87 +147,58 @@ window.toggleSubLayer = function(subId, iconEl) {
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');
}
const el = layer.getElement?.();
if (el) el.style.display = checked.includes(layer.miskinData.kategori_bantuan) ? '' : 'none';
});
}
};
// --- Modal Logic ---
// ===== 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;
document.getElementById('modalTitle').textContent = title;
document.getElementById('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);
const oldBtn = document.getElementById('modalSaveBtn');
const newBtn = oldBtn.cloneNode(true);
oldBtn.parentNode.replaceChild(newBtn, oldBtn);
newBtn.addEventListener('click', saveCallback);
};
window.closeModal = function() {
unifiedModal.classList.remove('show');
};
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;
document.getElementById('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() {
const oldBtn = document.getElementById('confirmYesBtn');
const newBtn = oldBtn.cloneNode(true);
oldBtn.parentNode.replaceChild(newBtn, oldBtn);
newBtn.addEventListener('click', function() {
confirmModal.classList.remove('show');
confirmCallback();
});
};
window.closeConfirmModal = function() {
confirmModal.classList.remove('show');
};
window.closeConfirmModal = function() { confirmModal.classList.remove('show'); };
// --- Action Menu Logic (replaced by left sidebar) ---
window.currentAddMode = null; // 'rumah_ibadah' atau 'miskin_click'
// ===== Add Mode =====
window.currentAddMode = null;
window.activateAddMode = function(mode) {
// Toggle: jika mode yang sama diklik lagi, batalkan
if (window.currentAddMode === mode) {
window.deactivateAddMode();
return;
}
if (window.currentAddMode === mode) { window.deactivateAddMode(); return; }
window.currentAddMode = mode;
// Tooltip
const tooltips = {
'rumah_ibadah': 'Klik untuk Tambah Rumah Ibadah'
};
const tooltips = { 'rumah_ibadah': 'Klik peta 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';
};
@@ -224,13 +208,11 @@ window.deactivateAddMode = function() {
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
};
// --- Custom Tooltip Cursor ---
// ===== Cursor Tooltip =====
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';
+157 -90
View File
@@ -16,8 +16,8 @@
let activePanel = null;
// ---- Elements ----
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
// removed
// removed
const leftSidebar = document.getElementById('leftSidebar');
const rightPanel = document.getElementById('rightPanel');
const rightPanelTitle = document.getElementById('rightPanelTitle');
@@ -25,19 +25,7 @@
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 collapse handled in map.js
// ---- Sidebar Buttons ----
document.querySelectorAll('.sidebar-btn').forEach(btn => {
@@ -85,6 +73,7 @@
switch (type) {
case 'ibadah': renderIbadahPanel(); break;
case 'miskin': renderMiskinPanel(); break;
case 'log_bantuan': renderLogBantuanPanel(); break;
}
}
@@ -238,6 +227,59 @@
}
}
// ==========================
// ===== Log Bantuan Panel ==
// ==========================
function renderLogBantuanPanel() {
rightPanelTitle.textContent = '📋 Log Bantuan';
rightPanelActions.innerHTML = '';
const all = [];
if (typeof pendudukMiskinLayer !== 'undefined') {
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;
}
rightPanelBody.innerHTML = '';
const canLog = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
// Hint strip
const hint = document.createElement('div');
hint.className = 'panel-hint';
hint.innerHTML = `<i class="fas fa-info-circle"></i> ${canLog ? 'Klik penduduk untuk lihat atau tambah log bantuan.' : 'Klik penduduk untuk melihat riwayat log bantuan.'}`;
rightPanelBody.appendChild(hint);
// Summary badge
const summary = document.createElement('div');
summary.className = 'panel-section-title';
summary.textContent = `${all.length} Kepala Keluarga`;
rightPanelBody.appendChild(summary);
all.forEach(d => {
const isMakan = d.kategori_bantuan === 'Makan';
const card = document.createElement('div');
card.className = 'log-person-card';
const badgeHtml = isMakan
? '<span class="data-card-badge badge-orange">Bantuan Makan</span>'
: '<span class="data-card-badge badge-blue">Pemberdayaan</span>';
const avatarCls = isMakan ? 'avatar-makan' : 'avatar-pmbr';
card.innerHTML = `
<div class="log-person-avatar ${avatarCls}">🏠</div>
<div class="log-person-info">
<div class="log-person-name">${escHtml(d.nama)}</div>
<div class="log-person-meta">
<span>👨‍👩‍👧 ${d.jumlah_jiwa || '-'} jiwa</span>
${badgeHtml}
</div>
</div>
<i class="fas fa-chevron-right log-person-arrow"></i>`;
card.addEventListener('click', () => window.openLogBantuan(d.id, d.nama));
rightPanelBody.appendChild(card);
});
}
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'));
@@ -344,115 +386,140 @@
}
});
// ==========================
// ===== 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;
// ==========================================
// ===== Log Bantuan Modal ==================
// ==========================================
window.openLogBantuan = function (miskinId, nama) {
_logMiskinId = miskinId;
logTitle.textContent = `Log Bantuan — ${nama}`;
logModal.classList.add('show');
const modal = document.getElementById('logBantuanModal');
document.getElementById('logBantuanTitle').textContent = '📋 Log Bantuan — ' + nama;
document.getElementById('logBantuanMiskinId').value = miskinId;
modal.classList.add('show');
loadLogBantuan(miskinId);
const isAuth = !!window.currentUser;
const formContainer = document.querySelector('#logBantuanModal .modal-body > div:last-child');
const modalFooter = document.querySelector('#logBantuanModal .modal-footer');
// Tampilkan form tambah hanya untuk admin/pengelola
const canLog = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
document.getElementById('logBantuanFormSection').style.display = canLog ? 'block' : 'none';
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);
// Isi dropdown ibadah jika pengelola (auto-select ibadahnya)
if (canLog) populateLogIbadahSelect();
};
window.closeLogBantuanModal = function () {
logModal.classList.remove('show');
_logMiskinId = null;
document.getElementById('logBantuanModal').classList.remove('show');
};
function fetchLogBantuan(miskinId) {
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Memuat...</div>';
fetch(`api/log_bantuan/read.php?miskin_id=${miskinId}`)
function loadLogBantuan(miskinId) {
const listEl = document.getElementById('logBantuanList');
listEl.innerHTML = '<div class="panel-empty">Memuat log...</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>';
.then(res => {
if (res.status !== 'success' || !res.data.length) {
listEl.innerHTML = '<div class="panel-empty">Belum ada log bantuan tercatat.</div>';
return;
}
logList.innerHTML = '';
data.data.forEach(log => {
const tgl = new Date(log.tanggal).toLocaleDateString('id-ID', { day:'2-digit', month:'short', year:'numeric' });
listEl.innerHTML = '';
res.data.forEach(log => {
const item = document.createElement('div');
item.className = 'log-item';
const tgl = new Date(log.tanggal).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' });
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);
<div class="log-item-header">
<span class="log-tipe-badge"><i class="fas fa-hand-holding-heart" style="margin-right:4px;font-size:10px;"></i>${escHtml(log.tipe_bantuan)}</span>
<span class="log-tgl"><i class="fas fa-calendar-alt"></i>${tgl}</span>
</div>
<div class="log-ibadah"><span>🕌</span> ${escHtml(log.nama_ibadah)} <span class="log-jenis">${escHtml(log.jenis_ibadah)}</span></div>
${log.keterangan ? `<div class="log-ket"><i class="fas fa-quote-left" style="margin-right:4px;font-size:9px;opacity:0.5;"></i>${escHtml(log.keterangan)}</div>` : ''}
`;
listEl.appendChild(item);
});
})
.catch(() => {
logList.innerHTML = '<div class="panel-empty" style="padding:15px; color:#ef4444;">Gagal memuat data</div>';
listEl.innerHTML = '<div class="panel-empty" style="color:var(--danger);">Gagal memuat data log.</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;
function populateLogIbadahSelect() {
const sel = document.getElementById('logIbadahId');
sel.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>';
if (window.currentUser && window.currentUser.role === 'pengelola' && window.currentUser.ibadah_id) {
// Pengelola hanya bisa pilih ibadah miliknya — fetch dari layer
const id = window.currentUser.ibadah_id;
let found = false;
if (typeof rumahIbadahLayer !== 'undefined') {
rumahIbadahLayer.eachLayer(l => {
if (l.ibadahData) {
const opt = document.createElement('option');
opt.value = l.ibadahData.id;
opt.textContent = l.ibadahData.nama;
if (l.ibadahData.id == id) { opt.selected = true; found = true; }
sel.appendChild(opt);
}
});
}
} else {
// Admin bisa pilih semua
if (typeof rumahIbadahLayer !== 'undefined') {
rumahIbadahLayer.eachLayer(l => {
if (l.ibadahData) {
const opt = document.createElement('option');
opt.value = l.ibadahData.id;
opt.textContent = l.ibadahData.nama;
sel.appendChild(opt);
}
});
}
}
}
if (!ibadah_id) { alert('Pilih rumah ibadah terlebih dahulu!'); return; }
if (!tanggal) { alert('Tanggal harus diisi!'); return; }
document.getElementById('btnSaveLog').addEventListener('click', function () {
const miskinId = parseInt(document.getElementById('logBantuanMiskinId').value);
const ibadahId = parseInt(document.getElementById('logIbadahId').value);
const tipe = document.getElementById('logTipeBantuan').value.trim();
const tanggal = document.getElementById('logTanggal').value;
const keterangan = document.getElementById('logKeterangan').value.trim();
if (!ibadahId || !tipe || !tanggal) {
window.showToast('Lengkapi semua field wajib', 'error');
return;
}
const btn = this;
btn.disabled = true;
btn.textContent = 'Menyimpan...';
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 })
body: JSON.stringify({ miskin_id: miskinId, ibadah_id: ibadahId, tipe_bantuan: tipe, tanggal, keterangan })
})
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
.then(res => {
btn.disabled = false;
btn.textContent = 'Simpan Log';
if (res.status === 'success') {
window.showToast('Log bantuan berhasil disimpan', 'success');
document.getElementById('logTipeBantuan').value = '';
document.getElementById('logTanggal').value = '';
document.getElementById('logKeterangan').value = '';
fetchLogBantuan(_logMiskinId);
loadLogBantuan(miskinId);
} else {
alert(data.message || 'Gagal menyimpan');
window.showToast(res.message || 'Gagal menyimpan log', 'error');
}
})
.catch(() => {
btn.disabled = false;
btn.textContent = 'Simpan Log';
window.showToast('Terjadi kesalahan koneksi', 'error');
});
});
// Close on overlay click
document.getElementById('logBantuanModal').addEventListener('click', function(e) {
if (e.target === this) window.closeLogBantuanModal();
});
// ---- Utility ----
function escHtml(str) {
if (!str) return '';
+159 -239
View File
@@ -3,104 +3,105 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pemetaan Kemiskinan</title>
<title>WebGIS Kemiskinan Pontianak</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">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- FontAwesome (Icons) -->
<!-- FontAwesome -->
<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;">
<!-- ====== LEFT SIDEBAR ====== -->
<nav class="left-sidebar" id="leftSidebar">
<!-- Brand -->
<div class="sidebar-brand">
<div class="sidebar-brand-icon">🗺️</div>
<div class="sidebar-brand-text">
<div class="sidebar-brand-title">WebGIS Kemiskinan</div>
<div class="sidebar-brand-sub">Kota Pontianak</div>
</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>
<!-- Nav -->
<div class="sidebar-nav">
<div class="sidebar-nav-label">Peta</div>
<button class="sidebar-btn" id="menuIbadah" data-panel="ibadah">
<span class="btn-icon">🕌</span>
<span class="btn-label">Rumah Ibadah</span>
</button>
<button class="sidebar-btn" id="menuMiskin" data-panel="miskin">
<span class="btn-icon">🏠</span>
<span class="btn-label">Penduduk Miskin</span>
</button>
<button class="sidebar-btn" id="menuLogBantuan" data-panel="log_bantuan">
<span class="btn-icon">📋</span>
<span class="btn-label">Log Bantuan</span>
</button>
<div class="sidebar-divider"></div>
<div class="sidebar-nav-label" id="adminNavLabel" style="display:none;">Admin</div>
<button class="sidebar-btn" id="menuUsers" style="display:none;">
<span class="btn-icon">👥</span>
<span class="btn-label">Pengguna</span>
</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>
<!-- ====== TOP BAR ====== -->
<div class="top-bar" id="topBar">
<!-- Search -->
<div style="position:relative; flex:1; max-width:380px;">
<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>
<div id="searchResults" style="display:none; position:absolute; top:calc(100% + 6px); left:0; right:0;"></div>
</div>
<!-- Custom Layer Control Button -->
<button class="custom-layer-btn" id="layerBtn" title="Atur Layer">
<i class="fas fa-layer-group fa-lg"></i>
<!-- Layer Toggle -->
<button class="top-btn" id="layerBtn" title="Layer Peta">
<i class="fas fa-layer-group"></i>
</button>
<div>
<!-- Custom Layer Control Panel -->
<!-- Auth -->
<div id="authWidget">
<button class="btn-login" id="authLoginBtn">
<i class="fas fa-sign-in-alt"></i> Login
</button>
</div>
</div>
<!-- ====== LAYER PANEL ====== -->
<div class="custom-layer-panel" id="layerPanel">
<h3>Daftar Layer</h3>
<div class="layer-panel-title"><i class="fas fa-layer-group" style="color:var(--primary)"></i> Layer Peta</div>
<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>
<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">
@@ -113,22 +114,18 @@
</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 class="geojson-section">
<div id="geoJsonFileList" style="display:none;">
<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 style="margin-top:8px; padding-top:8px; border-top:1px dashed var(--border);">
<label style="font-size:11px; display:block; margin-bottom:4px; color:var(--text-secondary);">Import GeoJSON:</label>
<input type="file" id="fileGeoJson" accept=".geojson,.json" style="font-size:11px; max-width:100%;">
</div>
</div>
</div>
</div>
</div><!-- /custom-layer-panel -->
<!-- Right Data Panel -->
<!-- ====== RIGHT DATA PANEL ====== -->
<div class="right-panel" id="rightPanel">
<div class="right-panel-header">
<h3 id="rightPanelTitle">Data</h3>
@@ -138,24 +135,25 @@
<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 -->
<!-- ====== MAP ====== -->
<div id="map"></div>
<!-- Unified Modal -->
<!-- ====== ZOOM CONTROLS ====== -->
<div class="zoom-controls">
<button class="zoom-btn" id="zoomInBtn" title="Zoom In"><i class="fas fa-plus"></i></button>
<button class="zoom-btn" id="zoomOutBtn" title="Zoom Out"><i class="fas fa-minus"></i></button>
</div>
<!-- ====== UNIFIED MODAL ====== -->
<div id="unifiedModal" class="unified-modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Judul Form</h3>
<h3 id="modalTitle">Form</h3>
<span class="modal-close" onclick="closeModal()">&times;</span>
</div>
<div class="modal-body" id="modalBody">
<!-- Konten dinamis -->
</div>
<div class="modal-body" id="modalBody"></div>
<div class="modal-footer" id="modalFooter">
<button id="modalSaveBtn" class="btn-save">Simpan</button>
<button class="btn-cancel" onclick="closeModal()">Batal</button>
@@ -163,102 +161,47 @@
</div>
</div>
<!-- Confirm Modal -->
<!-- ====== CONFIRM MODAL ====== -->
<div id="confirmModal" class="unified-modal">
<div class="modal-content" style="width: 300px;">
<div class="modal-content" style="width:320px;">
<div class="modal-header">
<h3>Konfirmasi</h3>
<h3>Konfirmasi Hapus</h3>
</div>
<div class="modal-body">
<p id="confirmMessage"></p>
<p id="confirmMessage" style="font-size:14px; color:var(--text-secondary);"></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>
<div class="modal-footer">
<button id="confirmYesBtn" class="btn-save" style="background:var(--danger);">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 -->
<!-- ====== LOGIN MODAL ====== -->
<div id="loginModal" class="unified-modal">
<div class="modal-content" style="width: 360px;">
<div class="modal-content" style="width:380px;">
<div class="modal-header">
<h3>Login Ke Sistem</h3>
<h3>Masuk 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>
<label>Username</label>
<input type="text" id="loginUsername" placeholder="Masukkan username">
</div>
<div class="form-group">
<label for="loginPassword">Password</label>
<label>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 id="loginErrorMsg" style="display:none; color:var(--danger); font-size:12px; font-weight:600;"></div>
</div>
<div class="modal-footer">
<button id="loginSubmitBtn" class="btn-save" style="width: 100%;">Masuk</button>
<button id="loginSubmitBtn" class="btn-save" style="width:100%;">Masuk</button>
</div>
</div>
</div>
<!-- Dashboard Modal -->
<!-- ====== DASHBOARD MODAL ====== -->
<div id="dashboardModal" class="unified-modal">
<div class="modal-content">
<div class="modal-header">
@@ -269,29 +212,28 @@
</div>
</div>
<!-- User Management Modal -->
<!-- ====== USER MANAGEMENT MODAL ====== -->
<div id="userManagementModal" class="unified-modal">
<div class="modal-content" style="width: 700px; max-width: 95%;">
<div class="modal-content" id="userManagementModal" style="width:min(720px,95vw);">
<div class="modal-header">
<h3>Manajemen Pengguna (Admin)</h3>
<h3>Manajemen Pengguna</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>
<div class="modal-body" style="gap:14px;">
<div style="background:var(--surface-2); border:1px solid var(--border); border-radius:10px; padding:14px; display:flex; flex-direction:column; gap:10px;">
<h4 id="userFormTitle" style="font-size:13px; font-weight:700; color:var(--text-primary);">Tambah User Baru</h4>
<input type="hidden" id="manageUserId" value="">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<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">
<label>Password</label>
<input type="password" id="managePassword" placeholder="Kosongkan jika tidak diubah">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
<div class="form-group">
<label>Role</label>
<select id="manageRole">
@@ -300,108 +242,86 @@
</select>
</div>
<div class="form-group" id="manageIbadahGroup">
<label>Rumah Ibadah yang Dikelola</label>
<select id="manageIbadahId">
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
</select>
<label>Rumah Ibadah</label>
<select id="manageIbadahId"></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 style="display:flex; gap:8px; justify-content:flex-end;">
<button id="btnCancelUserEdit" class="btn-cancel" style="display:none;">Batal</button>
<button id="btnSaveUser" class="btn-save">Simpan</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;">
<div style="max-height:200px; overflow-y:auto; border:1px solid var(--border); border-radius:10px;">
<table style="width:100%; border-collapse:collapse; font-size:13px;">
<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 style="background:var(--surface-2); border-bottom:1px solid var(--border); color:var(--text-secondary); position:sticky; top:0; z-index:10;">
<th style="padding:10px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.4px;">Username</th>
<th style="padding:10px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.4px;">Role</th>
<th style="padding:10px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.4px;">Rumah Ibadah</th>
<th style="padding:10px 14px; text-align:center; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.4px;">Aksi</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- Diisi dinamis -->
</tbody>
<tbody id="userTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast Notification Container -->
<!-- ====== TOAST ====== -->
<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.
<!-- ====== LOG BANTUAN MODAL ====== -->
<div id="logBantuanModal" class="unified-modal">
<div class="modal-content modal-log-bantuan">
<div class="modal-header">
<h3 id="logBantuanTitle">📋 Log Bantuan</h3>
<span class="modal-close" onclick="closeLogBantuanModal()">&times;</span>
</div>
<div class="modal-body" style="gap:0; padding:0; overflow:hidden; display:flex; flex-direction:column; height:100%;">
<!-- Form tambah log -->
<div id="logBantuanFormSection" class="log-form-section">
<div class="log-form-title"><i class="fas fa-plus-circle"></i> Tambah Log Baru</div>
<input type="hidden" id="logBantuanMiskinId">
<div class="log-form-grid">
<div class="form-group">
<label>Rumah Ibadah <span style="color:var(--danger)">*</span></label>
<select id="logIbadahId"><option value="">-- Pilih --</option></select>
</div>
<div class="form-group">
<label>Jenis Bantuan <span style="color:var(--danger)">*</span></label>
<input type="text" id="logTipeBantuan" placeholder="cth: Sembako, Uang Tunai…">
</div>
<div class="form-group">
<label>Tanggal <span style="color:var(--danger)">*</span></label>
<input type="date" id="logTanggal">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label>Keterangan</label>
<input type="text" id="logKeterangan" placeholder="Catatan tambahan (opsional)">
</div>
</div>
<div style="display:flex; justify-content:flex-end;">
<button id="btnSaveLog" class="btn-save"><i class="fas fa-save"></i> Simpan Log</button>
</div>
</div>
<!-- Daftar log -->
<div class="log-list-section">
<div class="log-list-title"><i class="fas fa-history"></i> Riwayat Bantuan</div>
<div id="logBantuanList"></div>
</div>
</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 -->
<!-- ====== SCRIPTS ====== -->
<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>
@@ -0,0 +1,4 @@
# Cegah eksekusi script di folder uploads
Options -ExecCGI
AddHandler cgi-script .php .php3 .php4 .php5 .phtml .pl .py .jsp .asp .sh .cgi
php_flag engine off
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB