Initial commit — WebGIS UAS GeoMiskin
Sistem WebGIS analisis sebaran penduduk miskin dan jangkauan rumah ibadah. Fitur: peta interaktif Leaflet.js, CRUD rumah ibadah & penduduk miskin, auto-kategorisasi haversine, heatmap, export PDF/CSV, dashboard analitik, manajemen pengguna (superadmin/admin), login sistem. Teknologi: PHP, MySQL, Leaflet.js, OpenStreetMap, Nominatim Geocoding, mPDF. Catatan: jalankan `composer install` di folder uas/ untuk install dependencies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(node -e \"const { chromium } = require\\('playwright'\\); chromium.launch\\(\\).then\\(b => { console.log\\('chromium ok'\\); b.close\\(\\); }\\).catch\\(e => console.log\\('err:', e.message\\)\\);\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Composer dependencies
|
||||||
|
uas/vendor/
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Uploaded files (user data - tidak perlu di-version)
|
||||||
|
/uploads/
|
||||||
|
uas/uploads/bukti/
|
||||||
|
uas/uploads/kemiskinan/
|
||||||
|
|
||||||
|
# PHP session / cache
|
||||||
|
*.log
|
||||||
|
*.cache
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# XAMPP / OS files
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Temp test files
|
||||||
|
uas/test_*.php
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$data_spbu = mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM spbu"), MYSQLI_ASSOC);
|
||||||
|
echo json_encode($data_spbu);
|
||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
-- ================================================================
|
||||||
|
-- DATABASE GABUNGAN: db_webgis
|
||||||
|
-- WebGIS SIG B - Tugas + UAS
|
||||||
|
-- Athallah Ghathfan Aqila | D1041231060 | Untan
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS db_webgis CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE db_webgis;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- BAGIAN TUGAS
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS spbu (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
nomor VARCHAR(50),
|
||||||
|
status_24jam ENUM('Ya','Tidak') DEFAULT 'Tidak',
|
||||||
|
lat DECIMAL(10,7) NOT NULL,
|
||||||
|
lng DECIMAL(10,7) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS jalan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_jalan VARCHAR(255) NOT NULL,
|
||||||
|
status_jalan ENUM('Nasional','Provinsi','Kabupaten') DEFAULT 'Kabupaten',
|
||||||
|
panjang_m DECIMAL(10,2),
|
||||||
|
geojson TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS persil (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nomor_persil VARCHAR(100) NOT NULL,
|
||||||
|
status_milik ENUM('SHM','HGB','HGU','HP') DEFAULT 'SHM',
|
||||||
|
luas_m2 DECIMAL(12,2),
|
||||||
|
geojson TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS masjid (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_masjid VARCHAR(255) NOT NULL,
|
||||||
|
pic VARCHAR(100),
|
||||||
|
alamat TEXT,
|
||||||
|
radius_m INT DEFAULT 300,
|
||||||
|
lat DECIMAL(10,7) NOT NULL,
|
||||||
|
lng DECIMAL(10,7) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS kemiskinan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_kk VARCHAR(255) NOT NULL,
|
||||||
|
jumlah_kk INT DEFAULT 1,
|
||||||
|
alamat TEXT,
|
||||||
|
lat DECIMAL(10,7) NOT NULL,
|
||||||
|
lng DECIMAL(10,7) NOT NULL,
|
||||||
|
pkh TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
bpnt TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
blt TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
foto VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS wilayah (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
tipe ENUM('RT','RW','Kelurahan','Kecamatan') DEFAULT 'RT',
|
||||||
|
geojson TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- BAGIAN UAS
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
nama VARCHAR(100),
|
||||||
|
role ENUM('superadmin','admin') DEFAULT 'admin',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rumah_ibadah (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
jenis ENUM('Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya') DEFAULT 'Masjid',
|
||||||
|
kontak VARCHAR(100),
|
||||||
|
alamat TEXT,
|
||||||
|
lat DECIMAL(10,7) NOT NULL,
|
||||||
|
lng DECIMAL(10,7) NOT NULL,
|
||||||
|
radius INT DEFAULT 300,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS penduduk_miskin (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
kk_nama VARCHAR(255) NOT NULL,
|
||||||
|
nik VARCHAR(20),
|
||||||
|
alamat TEXT,
|
||||||
|
lat DECIMAL(10,7) NOT NULL,
|
||||||
|
lng DECIMAL(10,7) NOT NULL,
|
||||||
|
jumlah_anggota INT DEFAULT 1,
|
||||||
|
rumah_ibadah_id INT,
|
||||||
|
jarak_meter DECIMAL(10,2),
|
||||||
|
status_bantuan ENUM('belum_dibantu','sudah_dibantu','menunggu_verifikasi') DEFAULT 'belum_dibantu',
|
||||||
|
jenis_bantuan VARCHAR(255),
|
||||||
|
tanggal_bantuan DATE,
|
||||||
|
bukti_file VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS anggota_keluarga (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
penduduk_id INT NOT NULL,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
hubungan VARCHAR(50),
|
||||||
|
umur INT,
|
||||||
|
pekerjaan VARCHAR(100),
|
||||||
|
keterangan TEXT,
|
||||||
|
FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ================================================================
|
||||||
|
-- DATA SAMPLE
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
INSERT IGNORE INTO spbu (nama, nomor, status_24jam, lat, lng) VALUES
|
||||||
|
('SPBU Gajahmada', '64.771.01', 'Ya', -0.0208, 109.3284),
|
||||||
|
('SPBU Ahmad Yani', '64.771.02', 'Ya', -0.0342, 109.3401),
|
||||||
|
('SPBU Tanjungpura', '64.771.03', 'Tidak', -0.0156, 109.3512),
|
||||||
|
('SPBU Sultan Hamid II', '64.771.04', 'Ya', -0.0470, 109.3350);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO masjid (nama_masjid, pic, alamat, radius_m, lat, lng) VALUES
|
||||||
|
('Masjid Raya Mujahidin', 'H. Ahmad Fauzi', 'Jl. Gajahmada, Pontianak Selatan', 450, -0.0261, 109.3256),
|
||||||
|
('Masjid Al-Muhajirin', 'Ustadz Mahmud', 'Jl. Sutoyo, Pontianak Kota', 300, -0.0189, 109.3478),
|
||||||
|
('Masjid Nurul Huda', 'Ustadz Fahmi', 'Jl. Ahmad Yani, Pontianak Timur', 350, -0.0387, 109.3398),
|
||||||
|
('Masjid Jami Sultan', 'H. Ridwan', 'Jl. Tanjungpura, Pontianak Barat', 400, -0.0142, 109.3332);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO kemiskinan (nama_kk, jumlah_kk, alamat, lat, lng, pkh, bpnt, blt) VALUES
|
||||||
|
('Ahmad Suryana', 4, 'Jl. Gajahmada Gg. 3', -0.0278, 109.3267, 1, 1, 0),
|
||||||
|
('Siti Rohana', 3, 'Jl. Sutoyo No. 8', -0.0201, 109.3461, 0, 1, 1),
|
||||||
|
('Budi Santoso', 5, 'Jl. Ahmad Yani No. 32',-0.0355, 109.3412, 1, 0, 0),
|
||||||
|
('Dewi Rahayu', 2, 'Jl. Tanjungpura No. 7',-0.0412, 109.3389, 0, 0, 1),
|
||||||
|
('Hendra Kusuma', 6, 'Jl. Imam Bonjol No. 21',-0.0167, 109.3502, 1, 1, 1),
|
||||||
|
('Ratna Wulandari', 3, 'Jl. Gusti Situt No. 5',-0.0301, 109.3220, 1, 0, 1),
|
||||||
|
('Suparman', 7, 'Jl. Pak Kasih No. 12', -0.0243, 109.3310, 0, 1, 0);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO users (username, password, nama, role) VALUES
|
||||||
|
('admin', '$2y$10$TKh8H1.PfGLmAdaQXDLZQO3L4LYKE/VNRN3hJ3oZpYb7U0u0EWxLe', 'Administrator', 'superadmin'),
|
||||||
|
('petugas', '$2y$10$TKh8H1.PfGLmAdaQXDLZQO3L4LYKE/VNRN3hJ3oZpYb7U0u0EWxLe', 'Petugas Lapangan', 'admin');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO rumah_ibadah (id, nama, jenis, kontak, alamat, lat, lng, radius) VALUES
|
||||||
|
(1, 'Masjid Raya Mujahidin', 'Masjid', '0561-123456', 'Jl. Ahmad Yani, Pontianak', -0.0262, 109.3429, 500),
|
||||||
|
(2, 'Gereja Katedral Santo Yosef', 'Gereja Katolik', '0561-234567', 'Jl. Pattimura, Pontianak', -0.0312, 109.3301, 400),
|
||||||
|
(3, 'Vihara Bodhisattva Karaniya Metta', 'Vihara', '0561-345678', 'Jl. Tanjungpura, Pontianak', -0.0355, 109.3344, 350),
|
||||||
|
(4, 'Pura Agung Nusantara', 'Pura', '0561-456789', 'Jl. Gajah Mada, Pontianak', -0.0409, 109.3221, 400),
|
||||||
|
(5, 'Musholla Al-Ikhlas', 'Musholla', NULL, 'Jl. Kom Yos Sudarso, Pontianak',-0.0195, 109.3553, 250);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO penduduk_miskin (id, kk_nama, nik, alamat, lat, lng, rumah_ibadah_id, jarak_meter, status_bantuan, jenis_bantuan, tanggal_bantuan) VALUES
|
||||||
|
(1, 'Ahmad Subandi', '6171011234560001', 'Gg. Melati No.3, Pontianak Selatan', -0.0290, 109.3410, 1, 312.5, 'sudah_dibantu', 'PKH', '2024-03-01'),
|
||||||
|
(2, 'Siti Rahayu', '6171021234560002', 'Jl. Budi Utomo Gg.5, Pontianak', -0.0278, 109.3450, 1, 245.0, 'belum_dibantu', NULL, NULL),
|
||||||
|
(3, 'Budi Santoso', '6171031234560003', 'Jl. Gusti Hamzah, Pontianak Utara', -0.0320, 109.3290, 2, 198.3, 'menunggu_verifikasi', NULL, NULL),
|
||||||
|
(4, 'Maria Yuliana', '6171041234560004', 'Jl. Purnama Gg.2, Pontianak', -0.0350, 109.3320, 3, 267.1, 'sudah_dibantu', 'BPNT + PKH', '2024-01-15'),
|
||||||
|
(5, 'Wati Sari', '6171051234560005', 'Jl. Kom Yos Sudarso, Pontianak Timur',-0.0188,109.3561, 5, 89.4, 'belum_dibantu', NULL, NULL);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan) VALUES
|
||||||
|
(1, 'Ahmad Subandi', 'Kepala Keluarga', 45, 'Buruh Harian'),
|
||||||
|
(1, 'Dewi Subandi', 'Istri', 40, 'Ibu Rumah Tangga'),
|
||||||
|
(1, 'Rizki Subandi', 'Anak', 18, 'Pelajar'),
|
||||||
|
(2, 'Siti Rahayu', 'Kepala Keluarga', 38, 'Pedagang Kecil'),
|
||||||
|
(2, 'Putri Rahayu', 'Anak', 12, 'Pelajar'),
|
||||||
|
(3, 'Budi Santoso', 'Kepala Keluarga', 52, 'Tidak Bekerja'),
|
||||||
|
(3, 'Sari Santoso', 'Istri', 48, 'Buruh Cuci'),
|
||||||
|
(4, 'Maria Yuliana', 'Kepala Keluarga', 41, 'Pedagang'),
|
||||||
|
(5, 'Wati Sari', 'Kepala Keluarga', 33, 'Buruh');
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
// vendor mPDF ada di folder webgus/vendor (sudah di-install sebelumnya)
|
||||||
|
require_once __DIR__ . '/../webgus/vendor/autoload.php';
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$allMasjid = mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM masjid"), MYSQLI_ASSOC);
|
||||||
|
$allKemiskinan = mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM kemiskinan"), MYSQLI_ASSOC);
|
||||||
|
|
||||||
|
function haversine($lat1, $lng1, $lat2, $lng2) {
|
||||||
|
$R = 6371000;
|
||||||
|
$phi1 = deg2rad($lat1); $phi2 = deg2rad($lat2);
|
||||||
|
$dp = deg2rad($lat2 - $lat1);
|
||||||
|
$dl = deg2rad($lng2 - $lng1);
|
||||||
|
$a = sin($dp/2)**2 + cos($phi1)*cos($phi2)*sin($dl/2)**2;
|
||||||
|
return (int)round(2 * $R * asin(sqrt($a)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analisis coverage
|
||||||
|
$kkCoverage = []; $masjidCoverage = [];
|
||||||
|
foreach ($allMasjid as $m) $masjidCoverage[$m['id']] = ['kk'=>[], 'jiwa'=>0];
|
||||||
|
foreach ($allKemiskinan as $kk) {
|
||||||
|
$kkCoverage[$kk['id']] = [];
|
||||||
|
foreach ($allMasjid as $m) {
|
||||||
|
$dist = haversine($kk['lat'], $kk['lng'], $m['lat'], $m['lng']);
|
||||||
|
if ($dist <= (int)$m['radius_m']) {
|
||||||
|
$kkCoverage[$kk['id']][] = $m;
|
||||||
|
$masjidCoverage[$m['id']]['kk'][] = $kk;
|
||||||
|
$masjidCoverage[$m['id']]['jiwa'] += (int)$kk['jumlah_kk'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalKK = count($allKemiskinan);
|
||||||
|
$totalJiwa = array_sum(array_column($allKemiskinan, 'jumlah_kk'));
|
||||||
|
$countT = 0; $jiwaT = 0;
|
||||||
|
foreach ($allKemiskinan as $kk) {
|
||||||
|
if (count($kkCoverage[$kk['id']]) > 0) { $countT++; $jiwaT += (int)$kk['jumlah_kk']; }
|
||||||
|
}
|
||||||
|
$countTT = $totalKK - $countT; $jiwaTT = $totalJiwa - $jiwaT;
|
||||||
|
$pct = $totalKK > 0 ? number_format($countT / $totalKK * 100, 1) : '0.0';
|
||||||
|
$pctTT = $totalKK > 0 ? number_format($countTT / $totalKK * 100, 1) : '0.0';
|
||||||
|
|
||||||
|
$bulan = ['','Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
|
||||||
|
$tgl = date('j') . ' ' . $bulan[(int)date('n')] . ' ' . date('Y');
|
||||||
|
|
||||||
|
$rowsMasjid = '';
|
||||||
|
foreach ($allMasjid as $i => $m) {
|
||||||
|
$cov = $masjidCoverage[$m['id']];
|
||||||
|
$bg = ($i % 2 === 0) ? '#ffffff' : '#f9fafb';
|
||||||
|
$rowsMasjid .= "<tr style='background:{$bg};'>
|
||||||
|
<td style='text-align:center;'>" . ($i+1) . "</td>
|
||||||
|
<td>{$m['nama_masjid']}</td>
|
||||||
|
<td style='text-align:center;'>{$m['radius_m']}</td>
|
||||||
|
<td style='text-align:center;'><b>" . count($cov['kk']) . "</b></td>
|
||||||
|
<td style='text-align:center;'><b>{$cov['jiwa']}</b></td>
|
||||||
|
</tr>";
|
||||||
|
}
|
||||||
|
if (!$rowsMasjid) $rowsMasjid = "<tr><td colspan='5' style='text-align:center;color:#999;'>Belum ada data masjid.</td></tr>";
|
||||||
|
|
||||||
|
$rowsKK = '';
|
||||||
|
foreach ($allKemiskinan as $i => $kk) {
|
||||||
|
$covers = $kkCoverage[$kk['id']];
|
||||||
|
$covered = count($covers) > 0;
|
||||||
|
$minDist = '-';
|
||||||
|
if (count($allMasjid) > 0) {
|
||||||
|
$dists = array_map(fn($m) => haversine($kk['lat'], $kk['lng'], $m['lat'], $m['lng']), $allMasjid);
|
||||||
|
$minDist = min($dists);
|
||||||
|
}
|
||||||
|
$statusTxt = $covered ? '<span style="color:#1a7a3e;font-weight:bold;">Terjangkau</span>' : '<span style="color:#c0392b;font-weight:bold;">Tidak Terjangkau</span>';
|
||||||
|
$masjidNames = $covered ? implode(', ', array_column($covers, 'nama_masjid')) : '-';
|
||||||
|
$bg = ($i % 2 === 0) ? '#ffffff' : '#f9fafb';
|
||||||
|
$pkhTxt = $kk['pkh'] ? '<span style="color:#155724;">✓</span>' : '-';
|
||||||
|
$bpntTxt = $kk['bpnt'] ? '<span style="color:#004085;">✓</span>' : '-';
|
||||||
|
$bltTxt = $kk['blt'] ? '<span style="color:#856404;">✓</span>' : '-';
|
||||||
|
$rowsKK .= "<tr style='background:{$bg};'>
|
||||||
|
<td style='text-align:center;'>" . ($i+1) . "</td>
|
||||||
|
<td>{$kk['nama_kk']}</td>
|
||||||
|
<td style='text-align:center;'>{$kk['jumlah_kk']}</td>
|
||||||
|
<td style='font-size:9px;'>{$kk['alamat']}</td>
|
||||||
|
<td style='text-align:center;'>{$pkhTxt}</td>
|
||||||
|
<td style='text-align:center;'>{$bpntTxt}</td>
|
||||||
|
<td style='text-align:center;'>{$bltTxt}</td>
|
||||||
|
<td style='text-align:center;'>{$statusTxt}</td>
|
||||||
|
<td style='font-size:9px;'>{$masjidNames}</td>
|
||||||
|
<td style='text-align:center;'>{$minDist}</td>
|
||||||
|
</tr>";
|
||||||
|
}
|
||||||
|
if (!$rowsKK) $rowsKK = "<tr><td colspan='10' style='text-align:center;color:#999;'>Belum ada data kemiskinan.</td></tr>";
|
||||||
|
|
||||||
|
$html = <<<HTML
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; font-size: 11px; color: #222; }
|
||||||
|
h1 { font-size: 15px; text-align: center; margin-bottom: 2px; color: #1e3a5f; }
|
||||||
|
.subtitle { text-align: center; font-size: 10px; color: #555; margin-bottom: 14px; }
|
||||||
|
.section { font-size: 12px; font-weight: bold; color: #1e3a5f; border-bottom: 2px solid #1e3a5f; padding-bottom: 3px; margin: 12px 0 7px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 10px; }
|
||||||
|
th { background: #1e3a5f; color: white; padding: 5px; text-align: center; }
|
||||||
|
td { padding: 4px 5px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
|
||||||
|
.sg { width: 100%; border-collapse: collapse; margin-bottom: 4px; }
|
||||||
|
.sc { width: 25%; padding: 7px; text-align: center; border: 1px solid #ddd; }
|
||||||
|
.sn { font-size: 20px; font-weight: bold; }
|
||||||
|
.sl { font-size: 9px; color: #666; margin-top: 1px; }
|
||||||
|
.g { color: #1a7a3e; } .r { color: #c0392b; }
|
||||||
|
</style>
|
||||||
|
<h1>Laporan Analisis Kemiskinan & Jangkauan Masjid</h1>
|
||||||
|
<div class="subtitle">Sistem WebGIS SIG B — Universitas Tanjungpura | Dicetak: {$tgl}</div>
|
||||||
|
<div class="section">Ringkasan Statistik</div>
|
||||||
|
<table class="sg">
|
||||||
|
<tr>
|
||||||
|
<td class="sc"><div class="sn">{$totalKK}</div><div class="sl">Total KK Miskin</div></td>
|
||||||
|
<td class="sc"><div class="sn">{$totalJiwa}</div><div class="sl">Total Jiwa</div></td>
|
||||||
|
<td class="sc"><div class="sn g">{$countT} ({$pct}%)</div><div class="sl">KK Terjangkau • {$jiwaT} jiwa</div></td>
|
||||||
|
<td class="sc"><div class="sn r">{$countTT} ({$pctTT}%)</div><div class="sl">KK Tidak Terjangkau • {$jiwaTT} jiwa</div></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="section">Statistik per Masjid</div>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th width="5%">No</th><th width="40%">Nama Masjid</th><th width="15%">Radius (m)</th><th width="20%">KK Terjangkau</th><th width="20%">Jiwa Terjangkau</th></tr></thead>
|
||||||
|
<tbody>{$rowsMasjid}</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="section">Daftar KK Miskin & Status Jangkauan</div>
|
||||||
|
<table>
|
||||||
|
<thead><tr>
|
||||||
|
<th width="4%">No</th><th width="14%">Nama KK</th><th width="5%">Jiwa</th>
|
||||||
|
<th width="15%">Alamat</th><th width="5%">PKH</th><th width="5%">BPNT</th><th width="5%">BLT</th>
|
||||||
|
<th width="10%">Status</th><th width="22%">Dijangkau Masjid</th><th width="15%">Jarak Terdekat (m)</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>{$rowsKK}</tbody>
|
||||||
|
</table>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mpdf = new \Mpdf\Mpdf(['mode'=>'utf-8','format'=>'A4-L','margin_top'=>14,'margin_bottom'=>14,'margin_left'=>12,'margin_right'=>12]);
|
||||||
|
$mpdf->SetTitle('Laporan Kemiskinan & Jangkauan Masjid');
|
||||||
|
$mpdf->WriteHTML($html);
|
||||||
|
$mpdf->Output('laporan_kemiskinan_' . date('Y-m-d') . '.pdf', 'D');
|
||||||
|
} catch (\Mpdf\MpdfException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo '<pre>Gagal membuat PDF: ' . htmlspecialchars($e->getMessage()) . '</pre>';
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'spbu' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM spbu"), MYSQLI_ASSOC),
|
||||||
|
'jalan' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM jalan"), MYSQLI_ASSOC),
|
||||||
|
'persil' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM persil"), MYSQLI_ASSOC),
|
||||||
|
'wilayah' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM wilayah"), MYSQLI_ASSOC),
|
||||||
|
];
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($data);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$type = $_POST['type'] ?? '';
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
$whitelist = ['spbu','kemiskinan','masjid','jalan','persil','wilayah'];
|
||||||
|
if (!in_array($type, $whitelist, true) || $id <= 0) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Request tidak valid.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$q = "DELETE FROM `{$type}` WHERE id = {$id}";
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) { echo json_encode(['status'=>'error']); exit; }
|
||||||
|
if (mysqli_query($conn, "DELETE FROM spbu WHERE id = $id")) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Portal WebGIS | Universitas Tanjungpura</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0f172a;
|
||||||
|
font-family: 'Inter', 'Segoe UI', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* subtle grid bg */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(59,130,246,.04) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(59,130,246,.04) 1px, transparent 1px);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 52px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.header .badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: rgba(59,130,246,.15);
|
||||||
|
border: 1px solid rgba(59,130,246,.3);
|
||||||
|
color: #93c5fd;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: .8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 5px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #f1f5f9;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
letter-spacing: -.5px;
|
||||||
|
}
|
||||||
|
.header h1 span {
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #64748b;
|
||||||
|
max-width: 440px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
width: 300px;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid rgba(255,255,255,.07);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 36px 28px 32px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform .22s ease, border-color .22s ease, box-shadow .22s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
border-color: rgba(255,255,255,.15);
|
||||||
|
box-shadow: 0 24px 60px rgba(0,0,0,.5);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.card-icon {
|
||||||
|
width: 76px;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.card p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.65;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.card-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
margin-top: 22px;
|
||||||
|
padding: 9px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity .18s;
|
||||||
|
}
|
||||||
|
.card-btn:hover { opacity: .85; }
|
||||||
|
|
||||||
|
/* Tugas card */
|
||||||
|
.card-tugas .card-icon { background: rgba(59,130,246,.15); color: #3b82f6; }
|
||||||
|
.card-tugas:hover { box-shadow: 0 24px 60px rgba(59,130,246,.12), 0 0 0 1px rgba(59,130,246,.2); border-color: rgba(59,130,246,.3); }
|
||||||
|
.card-tugas .card-btn { background: #3b82f6; color: white; }
|
||||||
|
|
||||||
|
/* UAS card */
|
||||||
|
.card-uas .card-icon { background: rgba(16,185,129,.15); color: #10b981; }
|
||||||
|
.card-uas:hover { box-shadow: 0 24px 60px rgba(16,185,129,.12), 0 0 0 1px rgba(16,185,129,.2); border-color: rgba(16,185,129,.3); }
|
||||||
|
.card-uas .card-btn { background: #10b981; color: white; }
|
||||||
|
|
||||||
|
/* footer */
|
||||||
|
.footer {
|
||||||
|
margin-top: 52px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color .18s;
|
||||||
|
}
|
||||||
|
.footer a:hover { color: #94a3b8; }
|
||||||
|
.footer .meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #334155;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<div class="badge"><i class="fas fa-map-marked-alt"></i> WebGIS SIG B</div>
|
||||||
|
<h1>Portal <span>WebGIS</span> Terpadu</h1>
|
||||||
|
<p>Pilih sistem informasi spasial yang ingin Anda akses. Jelajahi data geospasial Kota Pontianak dengan mudah dan interaktif.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
|
||||||
|
<a href="tugas.php" class="card card-tugas">
|
||||||
|
<div class="card-icon">
|
||||||
|
<i class="fas fa-layer-group"></i>
|
||||||
|
</div>
|
||||||
|
<h2>Tugas</h2>
|
||||||
|
<p>Pemetaan SPBU, Masjid, KK Miskin, Ruas Jalan, Persil Tanah, dan Batas Wilayah Administrasi Kota Pontianak.</p>
|
||||||
|
<span class="card-btn"><i class="fas fa-globe"></i> Buka Aplikasi</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="uas/" class="card card-uas">
|
||||||
|
<div class="card-icon">
|
||||||
|
<i class="fas fa-house-circle-check"></i>
|
||||||
|
</div>
|
||||||
|
<h2>UAS</h2>
|
||||||
|
<p>Sistem analisis spasial sebaran penduduk miskin dan jangkauan rumah ibadah dengan fitur manajemen data lengkap.</p>
|
||||||
|
<span class="card-btn"><i class="fas fa-globe"></i> Buka Aplikasi</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<a href="uas/login.php"><i class="fas fa-lock me-1"></i>Masuk sebagai Administrator</a>
|
||||||
|
<div class="meta">Athallah Ghathfan Aqila · D1041231060 · Dr. Ir. Yus Sholva · Universitas Tanjungpura</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
$conn = mysqli_connect('127.0.0.1', 'root', '', 'db_webgis');
|
||||||
|
if (!$conn) {
|
||||||
|
die(json_encode(['status' => 'error', 'message' => 'Koneksi database gagal: ' . mysqli_connect_error()]));
|
||||||
|
}
|
||||||
|
mysqli_set_charset($conn, 'utf8mb4');
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$n = mysqli_real_escape_string($conn, $_POST['nama_jalan'] ?? '');
|
||||||
|
$s = in_array($_POST['status_jalan']??'', ['Nasional','Provinsi','Kabupaten']) ? $_POST['status_jalan'] : 'Kabupaten';
|
||||||
|
$p = (float)($_POST['panjang'] ?? 0);
|
||||||
|
$g = mysqli_real_escape_string($conn, $_POST['geojson'] ?? '');
|
||||||
|
if (!$n || !$g) { echo json_encode(['status'=>'error','message'=>'Data tidak lengkap']); exit; }
|
||||||
|
$q = "INSERT INTO jalan (nama_jalan, status_jalan, panjang_m, geojson) VALUES ('$n', '$s', $p, '$g')";
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$n = mysqli_real_escape_string($conn, $_POST['nama_kk'] ?? '');
|
||||||
|
$j = max(1, (int)($_POST['jumlah_kk'] ?? 1));
|
||||||
|
$a = mysqli_real_escape_string($conn, $_POST['alamat'] ?? '');
|
||||||
|
$lt = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lg = (float)($_POST['lng'] ?? 0);
|
||||||
|
$pkh = (isset($_POST['pkh']) && $_POST['pkh'] == '1') ? 1 : 0;
|
||||||
|
$bpnt = (isset($_POST['bpnt']) && $_POST['bpnt'] == '1') ? 1 : 0;
|
||||||
|
$blt = (isset($_POST['blt']) && $_POST['blt'] == '1') ? 1 : 0;
|
||||||
|
if (!$n || !$lt || !$lg) { echo json_encode(['status'=>'error','message'=>'Data tidak lengkap']); exit; }
|
||||||
|
|
||||||
|
$fotoSql = 'NULL';
|
||||||
|
if (!empty($_FILES['foto']['name']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg', 'jpeg', 'png', 'webp'];
|
||||||
|
if (in_array($ext, $allowed) && $_FILES['foto']['size'] <= 5 * 1024 * 1024) {
|
||||||
|
$dir = __DIR__ . '/uploads/kemiskinan/';
|
||||||
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||||
|
$fname = 'kk_' . time() . '_' . rand(1000,9999) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $dir . $fname)) {
|
||||||
|
$fotoSql = "'" . mysqli_real_escape_string($conn, $fname) . "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$q = "INSERT INTO kemiskinan (nama_kk, jumlah_kk, alamat, lat, lng, pkh, bpnt, blt, foto)
|
||||||
|
VALUES ('$n', $j, '$a', $lt, $lg, $pkh, $bpnt, $blt, $fotoSql)";
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$n = mysqli_real_escape_string($conn, $_POST['nama_masjid'] ?? '');
|
||||||
|
$p = mysqli_real_escape_string($conn, $_POST['pic'] ?? '');
|
||||||
|
$a = mysqli_real_escape_string($conn, $_POST['alamat'] ?? '');
|
||||||
|
$r = max(50, min(5000, (int)($_POST['radius_m'] ?? 300)));
|
||||||
|
$lt = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lg = (float)($_POST['lng'] ?? 0);
|
||||||
|
if (!$n || !$lt || !$lg) { echo json_encode(['status'=>'error','message'=>'Data tidak lengkap']); exit; }
|
||||||
|
$q = "INSERT INTO masjid (nama_masjid, pic, alamat, radius_m, lat, lng) VALUES ('$n', '$p', '$a', $r, $lt, $lg)";
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$nomor = mysqli_real_escape_string($conn, $_POST['nomor_persil'] ?? '');
|
||||||
|
$status = in_array($_POST['status_milik']??'', ['SHM','HGB','HGU','HP']) ? $_POST['status_milik'] : 'SHM';
|
||||||
|
$luas = (float)($_POST['luas'] ?? 0);
|
||||||
|
$geojson = mysqli_real_escape_string($conn, $_POST['geojson'] ?? '');
|
||||||
|
if (!$nomor || !$geojson) { echo json_encode(['status'=>'error','message'=>'Data tidak lengkap']); exit; }
|
||||||
|
$q = "INSERT INTO persil (nomor_persil, status_milik, luas_m2, geojson) VALUES ('$nomor', '$status', $luas, '$geojson')";
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$n = mysqli_real_escape_string($conn, $_POST['nama'] ?? '');
|
||||||
|
$no = mysqli_real_escape_string($conn, $_POST['nomor'] ?? '');
|
||||||
|
$s = in_array($_POST['status'] ?? '', ['Ya','Tidak']) ? $_POST['status'] : 'Tidak';
|
||||||
|
$lt = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lg = (float)($_POST['lng'] ?? 0);
|
||||||
|
if (!$n || !$lt || !$lg) { echo json_encode(['status'=>'error','message'=>'Data tidak lengkap']); exit; }
|
||||||
|
$q = "INSERT INTO spbu (nama, nomor, status_24jam, lat, lng) VALUES ('$n', '$no', '$s', $lt, $lg)";
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$nama = mysqli_real_escape_string($conn, trim($_POST['nama'] ?? ''));
|
||||||
|
$tipe = in_array($_POST['tipe'] ?? '', ['RT','RW','Kelurahan','Kecamatan']) ? $_POST['tipe'] : 'RT';
|
||||||
|
$geojson = mysqli_real_escape_string($conn, $_POST['geojson'] ?? '');
|
||||||
|
if (!$nama || !$geojson) { echo json_encode(['status'=>'error','message'=>'Data tidak lengkap']); exit; }
|
||||||
|
$q = "INSERT INTO wilayah (nama, tipe, geojson) VALUES ('$nama', '$tipe', '$geojson')";
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,744 @@
|
|||||||
|
<?php include 'koneksi.php'; ?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>WebGIS Tugas SIG B | Universitas Tanjungpura</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
|
||||||
|
#topnav { background: linear-gradient(135deg, #1e3a5f 0%, #2d6a9f 100%); color: white; padding: 10px 16px; display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||||||
|
#topnav .brand { font-weight: 700; font-size: 15px; }
|
||||||
|
#topnav .meta { font-size: 11px; opacity: 0.75; margin-left: auto; }
|
||||||
|
#app { display: flex; flex: 1; overflow: hidden; }
|
||||||
|
#sidebar { width: 285px; background: #fff; border-right: 1px solid #dee2e6; overflow-y: auto; flex-shrink: 0; display: flex; flex-direction: column; }
|
||||||
|
.sb-section { padding: 12px; border-bottom: 1px solid #f0f4f8; }
|
||||||
|
.sb-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: #8c9ab0; margin-bottom: 8px; }
|
||||||
|
.stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; }
|
||||||
|
.stat-card { background: #f8f9ff; border: 1px solid #e9edf4; border-radius: 6px; padding: 7px 4px; text-align: center; }
|
||||||
|
.stat-num { font-size: 18px; font-weight: 800; line-height: 1.1; }
|
||||||
|
.stat-lbl { font-size: 9.5px; color: #6c757d; margin-top: 1px; }
|
||||||
|
.layer-row { display: flex; align-items: center; gap: 8px; padding: 5px 2px; cursor: pointer; border-radius: 4px; }
|
||||||
|
.layer-row:hover { background: #f8f9ff; }
|
||||||
|
.layer-dot { width: 13px; height: 13px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.layer-dot.sq { border-radius: 3px; }
|
||||||
|
.layer-dot.line { height: 4px; border-radius: 2px; margin: 5px 0; }
|
||||||
|
.layer-lbl { font-size: 12px; color: #334155; flex: 1; }
|
||||||
|
.layer-cnt { font-size: 11px; color: #94a3b8; background: #f1f5f9; border-radius: 8px; padding: 1px 6px; }
|
||||||
|
.btn-layer { width: 100%; text-align: left; font-size: 12px; margin-bottom: 4px; border-radius: 6px; }
|
||||||
|
#map { flex: 1; }
|
||||||
|
#pick-hint { position: fixed; top: 72px; left: 50%; transform: translateX(-50%); background: rgba(30,58,95,.92); color: white; padding: 8px 18px; border-radius: 20px; font-size: 13px; z-index: 3000; display: none; backdrop-filter: blur(4px); box-shadow: 0 4px 12px rgba(0,0,0,.2); }
|
||||||
|
.popup-title { font-weight: 700; font-size: 13px; margin-bottom: 5px; color: #1e3a5f; border-bottom: 1px solid #eee; padding-bottom: 4px; }
|
||||||
|
.popup-row { font-size: 12px; color: #334155; margin: 3px 0; display: flex; gap: 4px; }
|
||||||
|
.popup-row b { color: #64748b; min-width: 60px; }
|
||||||
|
.popup-btns { display: flex; gap: 6px; margin-top: 8px; }
|
||||||
|
.pbtn { font-size: 11px; padding: 3px 10px; border-radius: 4px; border: none; cursor: pointer; font-weight: 600; }
|
||||||
|
.pbtn.edit { background: #3b82f6; color: white; }
|
||||||
|
.pbtn.del { background: #ef4444; color: white; }
|
||||||
|
.modal-header { background: linear-gradient(135deg, #1e3a5f, #2d6a9f); color: white; }
|
||||||
|
.modal-header .btn-close { filter: invert(1); }
|
||||||
|
label.form-label { font-weight: 600; font-size: 13px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="topnav">
|
||||||
|
<i class="fas fa-map-marked-alt fa-lg"></i>
|
||||||
|
<span class="brand">WebGIS SIG B — Universitas Tanjungpura</span>
|
||||||
|
<span class="meta">Athallah Ghathfan Aqila | D1041231060 | Dr. Ir. Yus Sholva</span>
|
||||||
|
<a href="index.php" class="btn btn-sm btn-light ms-2" title="Kembali ke Portal">
|
||||||
|
<i class="fas fa-home text-primary me-1"></i>Portal
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-light ms-1" onclick="exportPDF()" title="Export PDF">
|
||||||
|
<i class="fas fa-file-pdf text-danger me-1"></i>PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<div id="sidebar">
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="sb-section">
|
||||||
|
<div class="sb-title">Ringkasan Data</div>
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-num text-primary" id="cnt-spbu">0</div>
|
||||||
|
<div class="stat-lbl">SPBU</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-num text-warning" id="cnt-jalan">0</div>
|
||||||
|
<div class="stat-lbl">Jalan</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-num" style="color:#8b5cf6" id="cnt-persil">0</div>
|
||||||
|
<div class="stat-lbl">Persil</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-num text-secondary" id="cnt-wilayah">0</div>
|
||||||
|
<div class="stat-lbl">Wilayah</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Controls -->
|
||||||
|
<div class="sb-section">
|
||||||
|
<div class="sb-title">Tampilkan Layer</div>
|
||||||
|
<div class="layer-row" onclick="toggleLayer('spbu')">
|
||||||
|
<input type="checkbox" id="chk-spbu" checked onclick="event.stopPropagation();toggleLayer('spbu')">
|
||||||
|
<div class="layer-dot" style="background:#3b82f6"></div>
|
||||||
|
<span class="layer-lbl">SPBU</span>
|
||||||
|
<span class="layer-cnt" id="lc-spbu">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="layer-row" onclick="toggleLayer('jalan')">
|
||||||
|
<input type="checkbox" id="chk-jalan" checked onclick="event.stopPropagation();toggleLayer('jalan')">
|
||||||
|
<div class="layer-dot line" style="background:#f59e0b;width:13px"></div>
|
||||||
|
<span class="layer-lbl">Ruas Jalan</span>
|
||||||
|
<span class="layer-cnt" id="lc-jalan">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="layer-row" onclick="toggleLayer('persil')">
|
||||||
|
<input type="checkbox" id="chk-persil" checked onclick="event.stopPropagation();toggleLayer('persil')">
|
||||||
|
<div class="layer-dot sq" style="background:#8b5cf6"></div>
|
||||||
|
<span class="layer-lbl">Persil Tanah</span>
|
||||||
|
<span class="layer-cnt" id="lc-persil">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="layer-row" onclick="toggleLayer('wilayah')">
|
||||||
|
<input type="checkbox" id="chk-wilayah" checked onclick="event.stopPropagation();toggleLayer('wilayah')">
|
||||||
|
<div class="layer-dot sq" style="background:#a855f7;opacity:.7"></div>
|
||||||
|
<span class="layer-lbl">Batas Wilayah</span>
|
||||||
|
<span class="layer-cnt" id="lc-wilayah">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="sb-section">
|
||||||
|
<div class="sb-title">Tambah Data</div>
|
||||||
|
<button class="btn btn-primary btn-sm btn-layer" onclick="openAdd('spbu')">
|
||||||
|
<i class="fas fa-gas-pump me-2"></i>Tambah SPBU
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning btn-sm btn-layer text-dark" onclick="openAdd('jalan')">
|
||||||
|
<i class="fas fa-road me-2"></i>Tambah Ruas Jalan
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-layer" style="background:#8b5cf6;color:white;border:none" onclick="openAdd('persil')">
|
||||||
|
<i class="fas fa-draw-polygon me-2"></i>Tambah Persil Tanah
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-layer" style="background:#a855f7;color:white;border:none" onclick="openAdd('wilayah')">
|
||||||
|
<i class="fas fa-map me-2"></i>Tambah Batas Wilayah
|
||||||
|
</button>
|
||||||
|
<hr class="my-2">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm btn-layer" onclick="geolocate()">
|
||||||
|
<i class="fas fa-crosshairs me-2"></i>Lokasi Saya Saat Ini
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="sb-section">
|
||||||
|
<div class="sb-title">Legenda Jalan</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<div style="width:18px;height:4px;border-radius:2px;background:#dc2626"></div>
|
||||||
|
<small class="text-muted">Jalan Nasional</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<div style="width:18px;height:4px;border-radius:2px;background:#f59e0b"></div>
|
||||||
|
<small class="text-muted">Jalan Provinsi</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<div style="width:18px;height:4px;border-radius:2px;background:#64748b"></div>
|
||||||
|
<small class="text-muted">Jalan Kabupaten</small>
|
||||||
|
</div>
|
||||||
|
<hr class="my-2">
|
||||||
|
<div class="sb-title">Legenda Persil</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<div class="layer-dot" style="background:#22c55e"></div>
|
||||||
|
<small class="text-muted">SHM</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<div class="layer-dot" style="background:#3b82f6"></div>
|
||||||
|
<small class="text-muted">HGB</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<div class="layer-dot" style="background:#f59e0b"></div>
|
||||||
|
<small class="text-muted">HGU</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<div class="layer-dot" style="background:#94a3b8"></div>
|
||||||
|
<small class="text-muted">HP</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pick-hint">
|
||||||
|
<i class="fas fa-map-pin me-2"></i>Klik peta untuk pilih lokasi
|
||||||
|
<button class="btn btn-sm btn-light ms-3 py-0" onclick="cancelPick()">Batal</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal SPBU -->
|
||||||
|
<div class="modal fade" id="modal-spbu" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="title-spbu"><i class="fas fa-gas-pump me-2"></i>Data SPBU</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="spbu-id">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Nama SPBU <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="spbu-nama" placeholder="Contoh: SPBU 64.771.01">
|
||||||
|
</div>
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Nomor SPBU</label>
|
||||||
|
<input type="text" class="form-control" id="spbu-nomor" placeholder="64.771.01">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Status 24 Jam</label>
|
||||||
|
<select class="form-select" id="spbu-status24">
|
||||||
|
<option value="Ya">Ya</option>
|
||||||
|
<option value="Tidak">Tidak</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Latitude <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" step="any" class="form-control" id="spbu-lat" placeholder="-0.0000">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Longitude <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" step="any" class="form-control" id="spbu-lng" placeholder="109.0000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-primary btn-sm w-100" onclick="startPick('spbu')">
|
||||||
|
<i class="fas fa-map-pin me-1"></i>Pilih Lokasi dari Peta
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveSpbu()"><i class="fas fa-save me-1"></i>Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Jalan -->
|
||||||
|
<div class="modal fade" id="modal-jalan" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="title-jalan"><i class="fas fa-road me-2"></i>Data Ruas Jalan</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="jalan-id">
|
||||||
|
<input type="hidden" id="jalan-geojson">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Nama Jalan <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="jalan-nama" placeholder="Contoh: Jl. Gajahmada">
|
||||||
|
</div>
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Status Jalan</label>
|
||||||
|
<select class="form-select" id="jalan-status">
|
||||||
|
<option>Nasional</option><option>Provinsi</option><option selected>Kabupaten</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Panjang (m)</label>
|
||||||
|
<input type="number" class="form-control" id="jalan-panjang" placeholder="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Jalur GeoJSON <span class="text-danger">*</span></label>
|
||||||
|
<button class="btn btn-warning btn-sm mb-2" onclick="startDraw('polyline','jalan')">
|
||||||
|
<i class="fas fa-pencil-alt me-1"></i>Gambar Jalur di Peta
|
||||||
|
</button>
|
||||||
|
<textarea class="form-control font-monospace" id="jalan-geojson-view" rows="3" placeholder='{"type":"LineString","coordinates":[[lng,lat],...]}' readonly></textarea>
|
||||||
|
<div id="jalan-geojson-status"></div>
|
||||||
|
<small class="text-muted">Klik ganda untuk selesai menggambar</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
|
||||||
|
<button class="btn btn-warning" onclick="saveJalan()"><i class="fas fa-save me-1"></i>Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Persil -->
|
||||||
|
<div class="modal fade" id="modal-persil" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="title-persil"><i class="fas fa-draw-polygon me-2"></i>Data Persil Tanah</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="persil-id">
|
||||||
|
<input type="hidden" id="persil-geojson">
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Nomor Persil <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="persil-nomor" placeholder="Contoh: P-2024-001">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Status Kepemilikan</label>
|
||||||
|
<select class="form-select" id="persil-status">
|
||||||
|
<option>SHM</option><option>HGB</option><option>HGU</option><option>HP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Luas (m²)</label>
|
||||||
|
<input type="number" class="form-control" id="persil-luas" placeholder="0">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Poligon GeoJSON <span class="text-danger">*</span></label>
|
||||||
|
<button class="btn btn-sm mb-2" style="background:#8b5cf6;color:white;border:none" onclick="startDraw('polygon','persil')">
|
||||||
|
<i class="fas fa-vector-square me-1"></i>Gambar Poligon di Peta
|
||||||
|
</button>
|
||||||
|
<textarea class="form-control font-monospace" id="persil-geojson-view" rows="3" placeholder='{"type":"Polygon","coordinates":[[[lng,lat],...]]}' readonly></textarea>
|
||||||
|
<div id="persil-geojson-status"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
|
||||||
|
<button class="btn btn-sm" style="background:#8b5cf6;color:white;border:none;padding:.375rem .75rem" onclick="savePersil()"><i class="fas fa-save me-1"></i>Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Wilayah -->
|
||||||
|
<div class="modal fade" id="modal-wilayah" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="title-wilayah"><i class="fas fa-map me-2"></i>Batas Wilayah Administrasi</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="wil-id">
|
||||||
|
<input type="hidden" id="wil-geojson">
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-8">
|
||||||
|
<label class="form-label">Nama Wilayah <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="wil-nama" placeholder="Contoh: RT 001 / Kelurahan Bansir">
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<label class="form-label">Tipe Wilayah</label>
|
||||||
|
<select class="form-select" id="wil-tipe">
|
||||||
|
<option>RT</option><option>RW</option><option>Kelurahan</option><option>Kecamatan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Batas GeoJSON <span class="text-danger">*</span></label>
|
||||||
|
<button class="btn btn-sm mb-2" style="background:#a855f7;color:white;border:none" onclick="startDraw('polygon','wilayah')">
|
||||||
|
<i class="fas fa-draw-polygon me-1"></i>Gambar Batas Wilayah
|
||||||
|
</button>
|
||||||
|
<textarea class="form-control font-monospace" id="wil-geojson-view" rows="3" placeholder='{"type":"Polygon","coordinates":[...]}' readonly></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
|
||||||
|
<button class="btn btn-sm" style="background:#a855f7;color:white;border:none;padding:.375rem .75rem" onclick="saveWilayah()"><i class="fas fa-save me-1"></i>Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast -->
|
||||||
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:9999">
|
||||||
|
<div id="toast-el" class="toast align-items-center text-white border-0" role="alert">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body fw-semibold" id="toast-msg">...</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||||
|
<script>
|
||||||
|
const map = L.map('map', { zoomControl: true }).setView([-0.025, 109.34], 13);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors | WebGIS SIG B Untan'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
const LG = {
|
||||||
|
spbu: L.layerGroup().addTo(map),
|
||||||
|
jalan: L.layerGroup().addTo(map),
|
||||||
|
persil: L.layerGroup().addTo(map),
|
||||||
|
wilayah: L.layerGroup().addTo(map),
|
||||||
|
};
|
||||||
|
|
||||||
|
let D = { spbu:[], jalan:[], persil:[], wilayah:[] };
|
||||||
|
let pickFor = null, drawTarget = null;
|
||||||
|
let userMarker = null, drawTempLayer = null;
|
||||||
|
|
||||||
|
const drawnItems = new L.FeatureGroup();
|
||||||
|
map.addLayer(drawnItems);
|
||||||
|
|
||||||
|
function mkIcon(bg, fa) {
|
||||||
|
return L.divIcon({
|
||||||
|
className: '',
|
||||||
|
html: `<div style="background:${bg};width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:2.5px solid white;box-shadow:0 2px 6px rgba(0,0,0,.35);color:white;font-size:13px"><i class="fas fa-${fa}"></i></div>`,
|
||||||
|
iconSize: [30,30], iconAnchor:[15,15], popupAnchor:[0,-15]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const ICONS = {
|
||||||
|
spbu: mkIcon('#3b82f6','gas-pump'),
|
||||||
|
me: mkIcon('#f59e0b','person-walking'),
|
||||||
|
};
|
||||||
|
const JCOL = { Nasional:'#dc2626', Provinsi:'#f59e0b', Kabupaten:'#64748b' };
|
||||||
|
const PCOL = { SHM:'#22c55e', HGB:'#3b82f6', HGU:'#f59e0b', HP:'#94a3b8' };
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const r = await fetch('get_all_data.php');
|
||||||
|
D = await r.json();
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAll() {
|
||||||
|
Object.values(LG).forEach(l => l.clearLayers());
|
||||||
|
renderSpbu(); renderJalan(); renderPersil(); renderWilayah();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSpbu() {
|
||||||
|
D.spbu.forEach(d => {
|
||||||
|
if (!d.lat||!d.lng) return;
|
||||||
|
L.marker([d.lat,d.lng],{icon:ICONS.spbu})
|
||||||
|
.bindPopup(`<div class="popup-title"><i class="fas fa-gas-pump me-1 text-primary"></i>${d.nama}</div>
|
||||||
|
<div class="popup-row"><b>Nomor</b>${d.nomor||'-'}</div>
|
||||||
|
<div class="popup-row"><b>24 Jam</b>${d.status_24jam}</div>
|
||||||
|
<div class="popup-btns">
|
||||||
|
<button class="pbtn edit" onclick="openEdit('spbu',${d.id})">Edit</button>
|
||||||
|
<button class="pbtn del" onclick="delItem('spbu',${d.id})">Hapus</button>
|
||||||
|
</div>`)
|
||||||
|
.addTo(LG.spbu);
|
||||||
|
});
|
||||||
|
document.getElementById('lc-spbu').textContent = D.spbu.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJalan() {
|
||||||
|
D.jalan.forEach(d => {
|
||||||
|
if (!d.geojson) return;
|
||||||
|
try {
|
||||||
|
L.geoJSON(JSON.parse(d.geojson), {
|
||||||
|
style: { color: JCOL[d.status_jalan]||'#64748b', weight: 5, opacity: 0.85 }
|
||||||
|
}).bindPopup(`<div class="popup-title"><i class="fas fa-road me-1"></i>${d.nama_jalan}</div>
|
||||||
|
<div class="popup-row"><b>Status</b>${d.status_jalan}</div>
|
||||||
|
<div class="popup-row"><b>Panjang</b>${d.panjang_m||'-'} m</div>
|
||||||
|
<div class="popup-btns">
|
||||||
|
<button class="pbtn edit" onclick="openEdit('jalan',${d.id})">Edit</button>
|
||||||
|
<button class="pbtn del" onclick="delItem('jalan',${d.id})">Hapus</button>
|
||||||
|
</div>`)
|
||||||
|
.addTo(LG.jalan);
|
||||||
|
} catch(e){}
|
||||||
|
});
|
||||||
|
document.getElementById('lc-jalan').textContent = D.jalan.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPersil() {
|
||||||
|
D.persil.forEach(d => {
|
||||||
|
if (!d.geojson) return;
|
||||||
|
try {
|
||||||
|
const c = PCOL[d.status_milik]||'#8b5cf6';
|
||||||
|
L.geoJSON(JSON.parse(d.geojson), {
|
||||||
|
style: { color: c, fillColor: c, fillOpacity: 0.3, weight: 2 }
|
||||||
|
}).bindPopup(`<div class="popup-title"><i class="fas fa-draw-polygon me-1" style="color:#8b5cf6"></i>${d.nomor_persil}</div>
|
||||||
|
<div class="popup-row"><b>Status</b>${d.status_milik}</div>
|
||||||
|
<div class="popup-row"><b>Luas</b>${d.luas_m2||'-'} m²</div>
|
||||||
|
<div class="popup-btns">
|
||||||
|
<button class="pbtn edit" onclick="openEdit('persil',${d.id})">Edit</button>
|
||||||
|
<button class="pbtn del" onclick="delItem('persil',${d.id})">Hapus</button>
|
||||||
|
</div>`)
|
||||||
|
.addTo(LG.persil);
|
||||||
|
} catch(e){}
|
||||||
|
});
|
||||||
|
document.getElementById('lc-persil').textContent = D.persil.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWilayah() {
|
||||||
|
D.wilayah.forEach(d => {
|
||||||
|
if (!d.geojson) return;
|
||||||
|
try {
|
||||||
|
L.geoJSON(JSON.parse(d.geojson), {
|
||||||
|
style: { color:'#a855f7', fillColor:'#a855f7', fillOpacity:0.08, weight:2, dashArray:'6 4' }
|
||||||
|
}).bindPopup(`<div class="popup-title"><i class="fas fa-map me-1" style="color:#a855f7"></i>${d.nama}</div>
|
||||||
|
<div class="popup-row"><b>Tipe</b>${d.tipe}</div>
|
||||||
|
<div class="popup-btns">
|
||||||
|
<button class="pbtn edit" onclick="openEdit('wilayah',${d.id})">Edit</button>
|
||||||
|
<button class="pbtn del" onclick="delItem('wilayah',${d.id})">Hapus</button>
|
||||||
|
</div>`)
|
||||||
|
.addTo(LG.wilayah);
|
||||||
|
} catch(e){}
|
||||||
|
});
|
||||||
|
document.getElementById('lc-wilayah').textContent = D.wilayah.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
['spbu','jalan','persil','wilayah'].forEach(k => {
|
||||||
|
document.getElementById('cnt-'+k).textContent = D[k]?.length ?? 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLayer(name) {
|
||||||
|
const chk = document.getElementById('chk-'+name);
|
||||||
|
if (chk.checked) map.addLayer(LG[name]); else map.removeLayer(LG[name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPick(type) {
|
||||||
|
pickFor = type;
|
||||||
|
document.getElementById('pick-hint').style.display = 'block';
|
||||||
|
map.getContainer().style.cursor = 'crosshair';
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('modal-'+type))?.hide();
|
||||||
|
}
|
||||||
|
function cancelPick() {
|
||||||
|
pickFor = null;
|
||||||
|
document.getElementById('pick-hint').style.display = 'none';
|
||||||
|
map.getContainer().style.cursor = '';
|
||||||
|
}
|
||||||
|
map.on('click', async function(e) {
|
||||||
|
if (!pickFor) return;
|
||||||
|
const {lat, lng} = e.latlng;
|
||||||
|
const t = pickFor;
|
||||||
|
document.getElementById(t+'-lat').value = lat.toFixed(7);
|
||||||
|
document.getElementById(t+'-lng').value = lng.toFixed(7);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=id`);
|
||||||
|
const j = await r.json();
|
||||||
|
const aEl = document.getElementById(t+'-alamat');
|
||||||
|
if (aEl) aEl.value = j.display_name||'';
|
||||||
|
} catch(err){}
|
||||||
|
cancelPick();
|
||||||
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('modal-'+t)).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
function startDraw(shapeType, target) {
|
||||||
|
drawTarget = target;
|
||||||
|
document.querySelectorAll('.modal').forEach(m => bootstrap.Modal.getInstance(m)?.hide());
|
||||||
|
if (drawTempLayer) { map.removeLayer(drawTempLayer); drawTempLayer = null; }
|
||||||
|
if (shapeType === 'polyline') {
|
||||||
|
new L.Draw.Polyline(map, {shapeOptions:{color:'#f59e0b',weight:4}}).enable();
|
||||||
|
} else {
|
||||||
|
new L.Draw.Polygon(map, {shapeOptions:{color:'#8b5cf6',weight:2,fillOpacity:0.3}}).enable();
|
||||||
|
}
|
||||||
|
showToast('Klik peta untuk menggambar titik. Klik ganda untuk selesai.','info');
|
||||||
|
}
|
||||||
|
map.on('draw:created', function(e) {
|
||||||
|
if (drawTempLayer) map.removeLayer(drawTempLayer);
|
||||||
|
drawTempLayer = e.layer;
|
||||||
|
map.addLayer(drawTempLayer);
|
||||||
|
const gj = JSON.stringify(e.layer.toGeoJSON().geometry);
|
||||||
|
if (drawTarget === 'jalan') {
|
||||||
|
document.getElementById('jalan-geojson').value = gj;
|
||||||
|
document.getElementById('jalan-geojson-view').value = gj;
|
||||||
|
document.getElementById('jalan-geojson-status').innerHTML = '<small class="text-success mt-1 d-block"><i class="fas fa-check-circle me-1"></i>Jalur baru sudah digambar</small>';
|
||||||
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('modal-jalan')).show();
|
||||||
|
} else if (drawTarget === 'persil') {
|
||||||
|
document.getElementById('persil-geojson').value = gj;
|
||||||
|
document.getElementById('persil-geojson-view').value = gj;
|
||||||
|
document.getElementById('persil-geojson-status').innerHTML = '<small class="text-success mt-1 d-block"><i class="fas fa-check-circle me-1"></i>Area persil baru sudah digambar</small>';
|
||||||
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('modal-persil')).show();
|
||||||
|
} else if (drawTarget === 'wilayah') {
|
||||||
|
document.getElementById('wil-geojson').value = gj;
|
||||||
|
document.getElementById('wil-geojson-view').value = gj;
|
||||||
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('modal-wilayah')).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('modal-jalan').addEventListener('hidden.bs.modal',()=>{
|
||||||
|
if(drawTempLayer){map.removeLayer(drawTempLayer);drawTempLayer=null;}
|
||||||
|
});
|
||||||
|
document.getElementById('modal-persil').addEventListener('hidden.bs.modal',()=>{
|
||||||
|
if(drawTempLayer){map.removeLayer(drawTempLayer);drawTempLayer=null;}
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAdd(type) {
|
||||||
|
resetForm(type);
|
||||||
|
document.getElementById(`title-${type}`).innerHTML = `<i class="fas fa-plus-circle me-2"></i>Tambah Data ${type.charAt(0).toUpperCase()+type.slice(1)}`;
|
||||||
|
new bootstrap.Modal(document.getElementById('modal-'+type)).show();
|
||||||
|
}
|
||||||
|
function openEdit(type, id) {
|
||||||
|
const d = D[type].find(x => x.id==id);
|
||||||
|
if (!d) return;
|
||||||
|
resetForm(type);
|
||||||
|
document.getElementById(`title-${type}`).innerHTML = `<i class="fas fa-edit me-2"></i>Edit Data ${type.charAt(0).toUpperCase()+type.slice(1)}`;
|
||||||
|
fillForm(type, d);
|
||||||
|
new bootstrap.Modal(document.getElementById('modal-'+type)).show();
|
||||||
|
}
|
||||||
|
function resetForm(type) {
|
||||||
|
const fields = {
|
||||||
|
spbu: ['spbu-id','spbu-nama','spbu-nomor','spbu-lat','spbu-lng'],
|
||||||
|
jalan: ['jalan-id','jalan-nama','jalan-panjang','jalan-geojson','jalan-geojson-view'],
|
||||||
|
persil: ['persil-id','persil-nomor','persil-luas','persil-geojson','persil-geojson-view'],
|
||||||
|
wilayah: ['wil-id','wil-nama','wil-geojson','wil-geojson-view'],
|
||||||
|
};
|
||||||
|
(fields[type]||[]).forEach(id => { const el=document.getElementById(id); if(el) el.value=''; });
|
||||||
|
if (type==='spbu') document.getElementById('spbu-status24').value='Tidak';
|
||||||
|
if (type==='jalan') { const s=document.getElementById('jalan-geojson-status'); if(s) s.innerHTML=''; }
|
||||||
|
if (type==='persil') { const s=document.getElementById('persil-geojson-status'); if(s) s.innerHTML=''; }
|
||||||
|
}
|
||||||
|
function fillForm(type, d) {
|
||||||
|
if (type==='spbu') {
|
||||||
|
document.getElementById('spbu-id').value=d.id;
|
||||||
|
document.getElementById('spbu-nama').value=d.nama||'';
|
||||||
|
document.getElementById('spbu-nomor').value=d.nomor||'';
|
||||||
|
document.getElementById('spbu-status24').value=d.status_24jam||'Tidak';
|
||||||
|
document.getElementById('spbu-lat').value=d.lat;
|
||||||
|
document.getElementById('spbu-lng').value=d.lng;
|
||||||
|
} else if (type==='jalan') {
|
||||||
|
document.getElementById('jalan-id').value=d.id;
|
||||||
|
document.getElementById('jalan-nama').value=d.nama_jalan||'';
|
||||||
|
document.getElementById('jalan-status').value=d.status_jalan||'Kabupaten';
|
||||||
|
document.getElementById('jalan-panjang').value=d.panjang_m||'';
|
||||||
|
document.getElementById('jalan-geojson').value=d.geojson||'';
|
||||||
|
document.getElementById('jalan-geojson-view').value=d.geojson||'';
|
||||||
|
if (d.geojson) {
|
||||||
|
try {
|
||||||
|
if (drawTempLayer) { map.removeLayer(drawTempLayer); drawTempLayer = null; }
|
||||||
|
drawTempLayer = L.geoJSON(JSON.parse(d.geojson), {
|
||||||
|
style: { color:'#f59e0b', weight:6, opacity:0.9, dashArray:'8 4' }
|
||||||
|
}).addTo(map);
|
||||||
|
map.fitBounds(drawTempLayer.getBounds(), {padding:[40,40]});
|
||||||
|
} catch(e) {}
|
||||||
|
document.getElementById('jalan-geojson-status').innerHTML =
|
||||||
|
'<small class="text-success mt-1 d-block"><i class="fas fa-check-circle me-1"></i>Geometri tersimpan — simpan langsung atau gambar ulang untuk mengubah jalur</small>';
|
||||||
|
}
|
||||||
|
} else if (type==='persil') {
|
||||||
|
document.getElementById('persil-id').value=d.id;
|
||||||
|
document.getElementById('persil-nomor').value=d.nomor_persil||'';
|
||||||
|
document.getElementById('persil-status').value=d.status_milik||'SHM';
|
||||||
|
document.getElementById('persil-luas').value=d.luas_m2||'';
|
||||||
|
document.getElementById('persil-geojson').value=d.geojson||'';
|
||||||
|
document.getElementById('persil-geojson-view').value=d.geojson||'';
|
||||||
|
if (d.geojson) {
|
||||||
|
try {
|
||||||
|
if (drawTempLayer) { map.removeLayer(drawTempLayer); drawTempLayer = null; }
|
||||||
|
drawTempLayer = L.geoJSON(JSON.parse(d.geojson), {
|
||||||
|
style: { color:'#8b5cf6', weight:3, fillColor:'#8b5cf6', fillOpacity:0.25, dashArray:'6 3' }
|
||||||
|
}).addTo(map);
|
||||||
|
map.fitBounds(drawTempLayer.getBounds(), {padding:[40,40]});
|
||||||
|
} catch(e) {}
|
||||||
|
document.getElementById('persil-geojson-status').innerHTML =
|
||||||
|
'<small class="text-success mt-1 d-block"><i class="fas fa-check-circle me-1"></i>Geometri tersimpan — simpan langsung atau gambar ulang untuk mengubah area</small>';
|
||||||
|
}
|
||||||
|
} else if (type==='wilayah') {
|
||||||
|
document.getElementById('wil-id').value=d.id;
|
||||||
|
document.getElementById('wil-nama').value=d.nama||'';
|
||||||
|
document.getElementById('wil-tipe').value=d.tipe||'RT';
|
||||||
|
document.getElementById('wil-geojson').value=d.geojson||'';
|
||||||
|
document.getElementById('wil-geojson-view').value=d.geojson||'';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(url, fd) {
|
||||||
|
const r = await fetch(url, {method:'POST', body:fd});
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
async function saveSpbu() {
|
||||||
|
const id = document.getElementById('spbu-id').value;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('nama', document.getElementById('spbu-nama').value);
|
||||||
|
fd.append('nomor', document.getElementById('spbu-nomor').value);
|
||||||
|
fd.append('status', document.getElementById('spbu-status24').value);
|
||||||
|
fd.append('lat', document.getElementById('spbu-lat').value);
|
||||||
|
fd.append('lng', document.getElementById('spbu-lng').value);
|
||||||
|
let url = 'simpan_spbu.php';
|
||||||
|
if (id) { fd.append('id',id); fd.append('type','spbu'); url='update_data.php'; }
|
||||||
|
const j = await post(url, fd);
|
||||||
|
if(j.status==='success'){closeModal('spbu');await loadData();showToast('SPBU berhasil disimpan','success');}
|
||||||
|
else showToast('Error: '+(j.message||'Gagal'),'danger');
|
||||||
|
}
|
||||||
|
async function saveJalan() {
|
||||||
|
const gj = document.getElementById('jalan-geojson').value;
|
||||||
|
if(!gj){showToast('Gambar jalur di peta terlebih dahulu','warning');return;}
|
||||||
|
const id = document.getElementById('jalan-id').value;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('nama_jalan', document.getElementById('jalan-nama').value);
|
||||||
|
fd.append('status_jalan', document.getElementById('jalan-status').value);
|
||||||
|
fd.append('panjang', document.getElementById('jalan-panjang').value||0);
|
||||||
|
fd.append('geojson', gj);
|
||||||
|
let url = 'simpan_jalan.php';
|
||||||
|
if (id) { fd.append('id',id); fd.append('type','jalan'); url='update_data.php'; }
|
||||||
|
const j = await post(url, fd);
|
||||||
|
if(j.status==='success'){closeModal('jalan');if(drawTempLayer){map.removeLayer(drawTempLayer);drawTempLayer=null;}await loadData();showToast('Jalan berhasil disimpan','success');}
|
||||||
|
else showToast('Error: '+(j.message||'Gagal'),'danger');
|
||||||
|
}
|
||||||
|
async function savePersil() {
|
||||||
|
const gj = document.getElementById('persil-geojson').value;
|
||||||
|
if(!gj){showToast('Gambar persil di peta terlebih dahulu','warning');return;}
|
||||||
|
const id = document.getElementById('persil-id').value;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('nomor_persil', document.getElementById('persil-nomor').value);
|
||||||
|
fd.append('status_milik', document.getElementById('persil-status').value);
|
||||||
|
fd.append('luas', document.getElementById('persil-luas').value||0);
|
||||||
|
fd.append('geojson', gj);
|
||||||
|
let url = 'simpan_persil.php';
|
||||||
|
if (id) { fd.append('id',id); fd.append('type','persil'); url='update_data.php'; }
|
||||||
|
const j = await post(url, fd);
|
||||||
|
if(j.status==='success'){closeModal('persil');if(drawTempLayer){map.removeLayer(drawTempLayer);drawTempLayer=null;}await loadData();showToast('Persil berhasil disimpan','success');}
|
||||||
|
else showToast('Error: '+(j.message||'Gagal'),'danger');
|
||||||
|
}
|
||||||
|
async function saveWilayah() {
|
||||||
|
const gj = document.getElementById('wil-geojson').value;
|
||||||
|
if(!gj){showToast('Gambar batas wilayah di peta terlebih dahulu','warning');return;}
|
||||||
|
const id = document.getElementById('wil-id').value;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('nama', document.getElementById('wil-nama').value);
|
||||||
|
fd.append('tipe', document.getElementById('wil-tipe').value);
|
||||||
|
fd.append('geojson', gj);
|
||||||
|
let url = 'simpan_wilayah.php';
|
||||||
|
if (id) { fd.append('id',id); fd.append('type','wilayah'); url='update_data.php'; }
|
||||||
|
const j = await post(url, fd);
|
||||||
|
if(j.status==='success'){closeModal('wilayah');if(drawTempLayer){map.removeLayer(drawTempLayer);drawTempLayer=null;}await loadData();showToast('Wilayah berhasil disimpan','success');}
|
||||||
|
else showToast('Error: '+(j.message||'Gagal'),'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function delItem(type, id) {
|
||||||
|
if(!confirm('Hapus data ini?')) return;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('type',type); fd.append('id',id);
|
||||||
|
const j = await post('hapus.php', fd);
|
||||||
|
if(j.status==='success'){await loadData();showToast('Data berhasil dihapus','success');}
|
||||||
|
else showToast('Gagal: '+(j.message||'Error'),'danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(type) {
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('modal-'+type))?.hide();
|
||||||
|
}
|
||||||
|
function geolocate() {
|
||||||
|
navigator.geolocation.getCurrentPosition(pos => {
|
||||||
|
const {latitude:lat, longitude:lng} = pos.coords;
|
||||||
|
if (userMarker) map.removeLayer(userMarker);
|
||||||
|
userMarker = L.marker([lat,lng],{icon:ICONS.me}).addTo(map).bindPopup('<b>📍 Lokasi Saya</b>').openPopup();
|
||||||
|
map.setView([lat,lng], 16);
|
||||||
|
showToast('Lokasi ditemukan!','success');
|
||||||
|
}, () => showToast('Gagal mendapatkan lokasi GPS','danger'));
|
||||||
|
}
|
||||||
|
function exportPDF() { window.open('export_pdf.php','_blank'); }
|
||||||
|
function showToast(msg, type='success') {
|
||||||
|
const colors = {success:'#22c55e', danger:'#ef4444', warning:'#f59e0b', info:'#3b82f6'};
|
||||||
|
const el = document.getElementById('toast-el');
|
||||||
|
el.style.background = colors[type]||colors.success;
|
||||||
|
document.getElementById('toast-msg').textContent = msg;
|
||||||
|
new bootstrap.Toast(el, {delay:3000}).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
+586
@@ -0,0 +1,586 @@
|
|||||||
|
# Spesifikasi Kebutuhan Perangkat Lunak
|
||||||
|
|
||||||
|
# WebGIS Partisipasi Rumah Ibadah (Masjid) untuk Pengentasan Kemiskinan
|
||||||
|
|
||||||
|
Dosen Pengampu:
|
||||||
|
|
||||||
|
Dr. Ir. Yus Sholva, S.T., M.T., C.Pro.
|
||||||
|
|
||||||
|
197410192003121002
|
||||||
|
|
||||||
|
Disusun oleh:
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Nama** | Athallah Ghathfan Aqila |
|
||||||
|
| **NIM** | D1041231060 |
|
||||||
|
| **Mata Kuliah** | Sistem Informasi Geografis B |
|
||||||
|
|
||||||
|
**PROGRAM STUDI INFORMATIKA**
|
||||||
|
|
||||||
|
**FAKULTAS TEKNIK**
|
||||||
|
|
||||||
|
**UNIVERSITAS TANJUNGPURA**
|
||||||
|
|
||||||
|
**2025/2026**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DAFTAR ISI
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **PENDAHULUAN** | 3 |
|
||||||
|
| 1.1 Tujuan Dokumen | 3 |
|
||||||
|
| 1.2 Ruang Lingkup | 3 |
|
||||||
|
| 1.3 Definisi, Akronim, dan Singkatan | 5 |
|
||||||
|
| 1.4 Referensi | 7 |
|
||||||
|
| **DESKRIPSI SISTEM** | 8 |
|
||||||
|
| 2.1 Perspektif Produk | 8 |
|
||||||
|
| 2.2 Fungsi Utama Sistem | 8 |
|
||||||
|
| 2.3 Kelas Pengguna dan Karakteristik | 10 |
|
||||||
|
| 2.4 Batasan dan Kendala Sistem | 11 |
|
||||||
|
| **KEBUTUHAN FUNGSIONAL** | 12 |
|
||||||
|
| 3.1 Daftar Use Case | 12 |
|
||||||
|
| 3.2 Detail Use Case | 13 |
|
||||||
|
| **KEBUTUHAN NON-FUNGSIONAL** | 28 |
|
||||||
|
| 4.1 Spesifikasi Performa | 28 |
|
||||||
|
| 4.2 Spesifikasi Keamanan | 29 |
|
||||||
|
| 4.3 Spesifikasi Kemudahan Penggunaan | 30 |
|
||||||
|
| 4.4 Spesifikasi Reliabilitas | 30 |
|
||||||
|
| 4.5 Spesifikasi Portabilitas | 31 |
|
||||||
|
| **ANTARMUKA EKSTERNAL** | 32 |
|
||||||
|
| 5.1 Antarmuka Pengguna | 32 |
|
||||||
|
| 5.2 Antarmuka Perangkat Keras | 33 |
|
||||||
|
| 5.3 Antarmuka Perangkat Lunak | 34 |
|
||||||
|
| 5.4 Antarmuka Komunikasi | 35 |
|
||||||
|
| **DEFINISI FUNGSI DAN KEBUTUHAN PENGGUNA** | 36 |
|
||||||
|
| 6.1 Kebutuhan Pengguna | 36 |
|
||||||
|
| 6.2 Detail Fungsionalitas Spesifik | 37 |
|
||||||
|
| 6.3 Analisis Alur Kerja Pengentasan Kemiskinan | 42 |
|
||||||
|
| **KESIMPULAN** | 43 |
|
||||||
|
| **LAMPIRAN** | 44 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PENDAHULUAN
|
||||||
|
|
||||||
|
### 1.1 Tujuan Dokumen
|
||||||
|
|
||||||
|
Dokumen Spesifikasi Kebutuhan Perangkat Lunak (SKPL) ini mendefinisikan secara menyeluruh kebutuhan fungsional dan non-fungsional dalam pengembangan sistem "WebGIS Partisipasi Rumah Ibadah (Masjid) untuk Pengentasan Kemiskinan". Dokumen ini disusun oleh Athallah Ghathfan Aqila (D1041231060) sebagai panduan bagi pihak-pihak berikut:
|
||||||
|
|
||||||
|
- **Tim Pengembang (Mahasiswa):** Sebagai acuan teknis implementasi fitur analisis spasial *multilayer* (Point, Line, Polygon) menggunakan PHP Native, Leaflet.js, dan database MySQL.
|
||||||
|
- **Pemerintah/Dinas Sosial:** Sebagai instrumen verifikasi akurasi sistem dalam mengidentifikasi titik kemiskinan yang berada di luar jangkauan bantuan komunitas (*blind spot*) melalui visualisasi data.
|
||||||
|
- **Pengelola Rumah Ibadah (Masjid):** Sebagai panduan operasional untuk memetakan warga binaan dan menentukan radius jangkauan layanan sosial secara mandiri per masjid.
|
||||||
|
- **Tim Penguji:** Sebagai landasan pembuatan skenario pengujian teknis, khususnya pada akurasi perhitungan jarak menggunakan rumus *geodesic* dan perhitungan luas area tanah.
|
||||||
|
|
||||||
|
### 1.2 Ruang Lingkup
|
||||||
|
|
||||||
|
Sistem yang dikembangkan adalah platform WebGIS dinamis yang mengintegrasikan data infrastruktur perkotaan dengan data kependudukan miskin di Kota Pontianak. Sistem ini berfungsi sebagai mesin analisis spasial untuk menentukan distribusi bantuan sosial berbasis jarak.
|
||||||
|
|
||||||
|
**Sistem ini mencakup:**
|
||||||
|
|
||||||
|
- **Visualisasi Multilayer Interaktif:** Menampilkan peta dasar (*Base Map*) yang memuat berbagai jenis geometri:
|
||||||
|
- *Point*: Lokasi SPBU, lokasi Masjid, dan lokasi rumah penduduk miskin.
|
||||||
|
- *Line*: Jaringan jalan dengan fitur penghitung panjang otomatis (m).
|
||||||
|
- *Polygon*: Parsil atau kavling tanah dengan fitur penghitung luas otomatis (m²), serta Batas Wilayah Administratif (RT/RW/Kelurahan/Kecamatan).
|
||||||
|
- **Manajemen Radius Dinamis per Masjid (Buffer):** Fitur khusus pada setiap titik Masjid yang memungkinkan pengguna mengatur variabel "Radius Deteksi" (100m hingga 5000m) menggunakan *slider* secara *real-time*, disertai tombol *toggle* visibilitas radius per masjid secara individual.
|
||||||
|
- **Analisis Spasial Real-Time (Intersect Analysis):** Algoritma internal yang mengevaluasi jarak koordinat rumah penduduk miskin terhadap radius layanan Masjid menggunakan formula Haversine.
|
||||||
|
- **Analisis Batas Wilayah:** Algoritma *point-in-polygon* yang mengidentifikasi KK yang berada dalam setiap wilayah administratif, disertai statistik *breakdown* per wilayah.
|
||||||
|
- **Indikator Visual Otomatis:** Perubahan warna marker penduduk miskin secara otomatis:
|
||||||
|
- *Warna Merah*: Menandakan penduduk berada di dalam jangkauan masjid (Tercakup).
|
||||||
|
- *Warna Abu-abu*: Menandakan penduduk berada di luar jangkauan masjid (*Blind Spot*).
|
||||||
|
- **Filter dan Pencarian Data KK:** Fitur pencarian nama KK secara *real-time* dan filter berdasarkan jenis bantuan yang diterima (PKH, BPNT, BLT) yang mempengaruhi tampilan panel dan peta secara bersamaan.
|
||||||
|
- **Panel Statistik Real-Time:** Menampilkan ringkasan cakupan kemiskinan (total KK, jiwa, terjangkau, *blind spot*) beserta *breakdown* per masjid dan per wilayah administratif.
|
||||||
|
- **Visualisasi Heatmap:** Lapisan kepadatan titik kemiskinan menggunakan gradien warna (kuning–oranye–merah) untuk mengidentifikasi konsentrasi penduduk miskin.
|
||||||
|
- **Ekspor Data:** Menghasilkan laporan dalam format CSV (*client-side* via Blob API) dan PDF (*server-side* menggunakan mPDF).
|
||||||
|
|
||||||
|
**Sistem ini TIDAK mencakup:**
|
||||||
|
|
||||||
|
- Sistem manajemen transaksi keuangan atau penyaluran dana (E-Wallet/Payment Gateway).
|
||||||
|
- Pelacakan lokasi penduduk miskin secara *real-time* melalui pergerakan GPS (data koordinat bersifat statis berdasarkan lokasi rumah).
|
||||||
|
- Verifikasi dokumen legalitas tanah atau SPBU (sistem hanya mengelola representasi grafis spasial).
|
||||||
|
- Sistem autentikasi multi-pengguna; aplikasi dirancang untuk penggunaan lokal/intranet.
|
||||||
|
|
||||||
|
### 1.3 Definisi, Akronim, dan Singkatan
|
||||||
|
|
||||||
|
| **Istilah** | **Definisi** |
|
||||||
|
|---|---|
|
||||||
|
| **SKPL / SRS** | Spesifikasi Kebutuhan Perangkat Lunak — dokumen panduan teknis pengembangan sistem. |
|
||||||
|
| **WebGIS** | Sistem informasi berbasis web yang mengolah data bereferensi koordinat geografis. |
|
||||||
|
| **Radius Dinamis** | Jarak jangkauan variabel yang dapat disesuaikan oleh pengguna untuk memetakan wilayah pelayanan sosial tiap masjid secara individual. |
|
||||||
|
| **Blind Spot** | Area kemiskinan yang tidak tersentuh oleh radius layanan masjid manapun, sehingga memerlukan intervensi langsung pemerintah. |
|
||||||
|
| **Marker** | Simbol visual (ikon) pada peta yang mewakili objek Point di lapangan. |
|
||||||
|
| **AJAX / Fetch API** | Metode pertukaran data antara JavaScript dan PHP secara asinkron untuk pembaruan data tanpa *refresh* halaman. |
|
||||||
|
| **GeometryUtil** | Plugin tambahan Leaflet untuk melakukan perhitungan matematis spasial seperti luas area dan jarak *geodesic*. |
|
||||||
|
| **Haversine** | Formula matematika untuk menghitung jarak terpendek antara dua titik koordinat di permukaan bumi (jarak *geodesic*). |
|
||||||
|
| **Point-in-Polygon** | Algoritma *ray casting* untuk menentukan apakah sebuah titik koordinat berada di dalam area polygon. |
|
||||||
|
| **GeoJSON** | Format data berbasis JSON standar internasional untuk merepresentasikan objek geografis (titik, garis, poligon). |
|
||||||
|
| **KK** | Kepala Keluarga — unit data terkecil dalam pendataan kemiskinan. |
|
||||||
|
| **PKH** | Program Keluarga Harapan — program bantuan sosial bersyarat dari pemerintah. |
|
||||||
|
| **BPNT** | Bantuan Pangan Non-Tunai — program subsidi pangan dari pemerintah. |
|
||||||
|
| **BLT** | Bantuan Langsung Tunai — program bantuan uang tunai dari pemerintah. |
|
||||||
|
| **RT / RW** | Rukun Tetangga / Rukun Warga — unit administratif terkecil di Indonesia. |
|
||||||
|
| **Toggle** | Fitur tombol untuk mengaktifkan atau menonaktifkan suatu kondisi secara bergantian tanpa me-*refresh* halaman. |
|
||||||
|
| **Heatmap** | Visualisasi peta kepadatan menggunakan gradien warna yang menunjukkan intensitas distribusi data spasial. |
|
||||||
|
| **Layer Control** | Komponen antarmuka Leaflet untuk mengaktifkan/menonaktifkan lapisan data tertentu pada peta. |
|
||||||
|
| **mPDF** | Library PHP untuk menghasilkan dokumen PDF dari konten HTML di sisi server. |
|
||||||
|
| **Material Design** | Panduan desain antarmuka dari Google yang menekankan konsistensi visual, tipografi Roboto, dan ikon Material Symbols. |
|
||||||
|
|
||||||
|
### 1.4 Referensi
|
||||||
|
|
||||||
|
- **Standar Dokumentasi (IEEE Std 830-1998):** Mengikuti panduan IEEE *Recommended Practice for Software Requirements Specifications* guna memastikan struktur dokumen memenuhi standar profesional internasional dalam mendefinisikan kebutuhan perangkat lunak.
|
||||||
|
- **Dokumentasi Resmi Leaflet.js (v1.9.4):** Digunakan sebagai referensi utama dalam implementasi mesin pemetaan, manipulasi layer spasial, penanganan marker, dan integrasi *tile layer* dari OpenStreetMap secara asinkron.
|
||||||
|
- **Dokumentasi Leaflet.GeometryUtil (v0.10.3):** Menjadi rujukan teknis khusus untuk melakukan kalkulasi matematis tingkat lanjut pada peta, seperti perhitungan jarak *geodesic* (Haversine) antara titik masjid dan penduduk, serta penghitungan luas area (m²) pada fitur parsil tanah secara presisi.
|
||||||
|
- **Dokumentasi Leaflet.heat (v0.2.0):** Referensi implementasi lapisan heatmap kepadatan kemiskinan.
|
||||||
|
- **Dokumentasi PHP Native & MySQL:** Referensi teknis mengenai arsitektur *server-side* untuk pengolahan data relasional, manajemen koneksi basis data, dan pertukaran data dalam format JSON melalui Fetch API.
|
||||||
|
- **Dokumentasi mPDF (v8.3):** Referensi implementasi ekspor laporan kemiskinan dalam format PDF (*server-side*) dengan konten tabel terstruktur format A4 *Landscape*.
|
||||||
|
- **Standar Geospasial Indonesia (BIG):** Pedoman teknis pemetaan skala mikro dan aturan simbolisasi peta untuk lingkungan permukiman yang berlaku di Indonesia.
|
||||||
|
- **Kebijakan Strategis Pengentasan Kemiskinan Kota Pontianak:** Landasan operasional mengenai mekanisme partisipasi masyarakat dan peran rumah ibadah dalam penyaluran bantuan sosial bagi masyarakat berpenghasilan rendah di wilayah Pontianak.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DESKRIPSI SISTEM
|
||||||
|
|
||||||
|
### 2.1 Perspektif Produk
|
||||||
|
|
||||||
|
Sistem WebGIS Partisipasi Rumah Ibadah ini dikembangkan sebagai aplikasi *standalone* (mandiri) yang mengintegrasikan data infrastruktur perkotaan dengan data kependudukan sosial. Produk ini bekerja dengan menggabungkan basis data relasional MySQL sebagai tempat penyimpanan atribut geografis dan library Leaflet.js sebagai mesin visualisasi interaktif pada sisi klien (*client-side*).
|
||||||
|
|
||||||
|
Berbeda dengan sistem pemetaan statis, produk ini memiliki perspektif dinamis di mana analisis spasial (perhitungan jarak dan jangkauan) dilakukan secara asinkron di sisi klien tanpa memerlukan *refresh* halaman. Sistem memproses data koordinat Masjid dan Penduduk Miskin untuk menghasilkan indikator visual otomatis. Integrasi fitur manajemen Jalan (Line), Tanah (Polygon), dan Batas Wilayah Administratif (Polygon) dalam satu *interface* memberikan perspektif wilayah yang lebih luas bagi pengambil keputusan. Panel statistik *real-time* yang disertai fitur filter, pencarian, dan ekspor data menjadikan sistem ini sebagai alat analisis kemiskinan yang komprehensif.
|
||||||
|
|
||||||
|
### 2.2 Fungsi Utama Sistem
|
||||||
|
|
||||||
|
Sistem ini menyediakan fungsionalitas utama yang mencakup manajemen data spasial dan analisis jangkauan sebagai berikut:
|
||||||
|
|
||||||
|
| **No** | **Fungsi Utama** | **Deskripsi Singkat** |
|
||||||
|
|---|---|---|
|
||||||
|
| **F01** | Manajemen Data Masjid | Mengelola titik lokasi masjid beserta informasi Nama Masjid, Penanggung Jawab (PIC), dan Alamat. |
|
||||||
|
| **F02** | Pengaturan Radius Dinamis per Masjid | Fitur *slider* interaktif untuk menentukan/mengubah jangkauan pelayanan sosial tiap masjid (100m–5000m) secara *real-time*, disertai *toggle* visibilitas radius per masjid secara individual. |
|
||||||
|
| **F03** | Manajemen Data Penduduk Miskin | Mengelola data penduduk miskin mencakup Nama KK, Jumlah Anggota Keluarga, Alamat, jenis bantuan yang diterima (PKH/BPNT/BLT), dan foto KK. |
|
||||||
|
| **F04** | Manajemen Data SPBU | Mengelola titik lokasi SPBU beserta informasi nama, nomor, dan status operasional 24 jam. |
|
||||||
|
| **F05** | Manajemen Infrastruktur Jalan & Tanah | Menambah dan mengelola jaringan jalan (Line) disertai hitung panjang otomatis (m), serta parsil tanah (Polygon) disertai hitung luas otomatis (m²). |
|
||||||
|
| **F06** | Manajemen Batas Wilayah Administratif | Menggambar dan mengelola polygon batas wilayah (RT/RW/Kelurahan/Kecamatan) untuk analisis distribusi kemiskinan berbasis wilayah. |
|
||||||
|
| **F07** | Visualisasi Multilayer | Menampilkan peta dasar (OpenStreetMap) dengan fitur *Layer Control* untuk memfilter tampilan berbagai kategori data, disertai warna ikon berbeda per layer. |
|
||||||
|
| **F08** | Analisis Spasial Real-Time | Sistem secara otomatis menghitung jarak *geodesic* antara titik penduduk terhadap radius masjid aktif setiap kali data atau parameter berubah. |
|
||||||
|
| **F09** | Indikator Visual Marker | Mengubah warna marker penduduk: Merah (jika tercakup radius masjid aktif) dan Abu-abu (jika berada di luar semua radius/*blind spot*). |
|
||||||
|
| **F10** | Filter dan Pencarian KK | Fitur pencarian nama KK secara *real-time* dan filter berdasarkan jenis bantuan (PKH, BPNT, BLT) dengan hasil yang mempengaruhi panel dan peta secara bersamaan. |
|
||||||
|
| **F11** | Panel Statistik Real-Time | Menampilkan ringkasan cakupan kemiskinan (total KK, jiwa, terjangkau, *blind spot*) beserta *breakdown* per masjid dan per wilayah administratif. |
|
||||||
|
| **F12** | Heatmap Kemiskinan | Menampilkan lapisan kepadatan titik kemiskinan menggunakan gradien warna (kuning–oranye–merah) untuk mengidentifikasi konsentrasi kemiskinan secara visual. |
|
||||||
|
| **F13** | Ekspor CSV | Menghasilkan file CSV data kemiskinan (mencerminkan filter aktif) secara *client-side* dengan encoding UTF-8 BOM untuk kompatibilitas Microsoft Excel. |
|
||||||
|
| **F14** | Ekspor PDF | Menghasilkan laporan PDF A4 *Landscape* berisi statistik dan tabel KK lengkap menggunakan mPDF di sisi server. |
|
||||||
|
| **F15** | Informasi Detail (Popup) | Menampilkan jendela informasi detail dan alat kontrol modifikasi data saat sebuah objek di peta diklik (Edit, Hapus, Update Radius, Toggle). |
|
||||||
|
|
||||||
|
### 2.3 Kelas Pengguna dan Karakteristik
|
||||||
|
|
||||||
|
| **Kelas Pengguna** | **Karakteristik** | **Hak Akses** |
|
||||||
|
|---|---|---|
|
||||||
|
| **Admin / Operator (Dinsos/PIC)** | Pengelola sistem yang memiliki otoritas terhadap validitas data sosial dan infrastruktur. | Melakukan operasi CRUD penuh pada semua layer, mengelola batas wilayah, menentukan/mengupdate radius masjid, mengekspor laporan. |
|
||||||
|
| **Pengurus Masjid (PIC)** | Pengurus masjid yang bertanggung jawab atas data warga binaan di sekitar masjid. | Mengelola data masjid dan radius per masjid, menambah data penduduk, melihat statistik jangkauan. |
|
||||||
|
| **Pengunjung / Masyarakat** | Pengguna umum atau *stakeholder* yang memerlukan informasi sebaran kemiskinan. | Melihat peta sebaran, memantau transparansi jangkauan bantuan, dan mengidentifikasi area *blind spot* secara visual. |
|
||||||
|
|
||||||
|
### 2.4 Batasan dan Kendala Sistem
|
||||||
|
|
||||||
|
Dalam pengembangan dan implementasinya, sistem ini memiliki batasan teknis dan operasional sebagai berikut:
|
||||||
|
|
||||||
|
- **Teknologi Pengembangan:** Wajib menggunakan *stack* teknologi PHP Native (tanpa framework) dan MySQL untuk *backend*, serta Vanilla JavaScript dengan library Leaflet.js untuk mesin pemetaan spasial.
|
||||||
|
- **Konektivitas Global:** Sistem memerlukan koneksi internet aktif untuk memuat ubin peta (*tiles*) dari server OpenStreetMap (OSM).
|
||||||
|
- **Akurasi Data Spasial:** Ketepatan analisis radius sepenuhnya bergantung pada akurasi input koordinat *Latitude* dan *Longitude*. Kesalahan input koordinat akan menyebabkan kesalahan klasifikasi warna marker.
|
||||||
|
- **Geocoding Manual:** Sistem tidak menyediakan fitur konversi otomatis dari alamat teks ke koordinat (Geocoding); koordinat harus ditentukan secara manual melalui *plotting* pada peta.
|
||||||
|
- **Integritas Data Kemiskinan:** Sistem hanya bertugas sebagai alat visualisasi dan analisis; validasi status ekonomi penduduk dilakukan oleh otoritas terkait sebelum diinput ke sistem.
|
||||||
|
- **Lingkungan Operasional:** Aplikasi dioptimasi untuk berjalan pada server lokal menggunakan XAMPP/Laragon atau *shared hosting* dengan dukungan PHP minimal versi 7.4 dan ekstensi MySQLi aktif.
|
||||||
|
- **Keamanan Spasial:** Perlindungan data koordinat terbatas pada level aplikasi; sistem dirancang untuk lingkungan intranet. Implementasi HTTPS dan autentikasi disarankan sebelum *deployment* ke jaringan publik.
|
||||||
|
- **Kapasitas Data:** Sistem diuji dan dioptimasi untuk mengelola hingga 500 KK dan 20 Masjid secara bersamaan tanpa penurunan performa yang signifikan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KEBUTUHAN FUNGSIONAL
|
||||||
|
|
||||||
|
### 3.1 Daftar Use Case
|
||||||
|
|
||||||
|
| **UC ID** | **Nama Use Case** | **Aktor** | **Prioritas** |
|
||||||
|
|---|---|---|---|
|
||||||
|
| UC-01 | Mengelola Data Infrastruktur Spasial | Admin | Must Have |
|
||||||
|
| UC-02 | Mengelola Data SPBU | Admin | Must Have |
|
||||||
|
| UC-03 | Mengelola Data Masjid dan Radius | Admin / PIC Masjid | Must Have |
|
||||||
|
| UC-04 | Mengelola Data Penduduk Miskin | Admin / PIC Masjid | Must Have |
|
||||||
|
| UC-05 | Mengelola Batas Wilayah Administratif | Admin | Must Have |
|
||||||
|
| UC-06 | Visualisasi Peta Multilayer | Semua Pengguna | Must Have |
|
||||||
|
| UC-07 | Analisis Intersect & Warna Otomatis | Sistem | Must Have |
|
||||||
|
| UC-08 | Modifikasi Radius Dinamis per Masjid | Admin / PIC Masjid | Must Have |
|
||||||
|
| UC-09 | Filter dan Pencarian Data Kemiskinan | Admin / PIC Masjid | Should Have |
|
||||||
|
| UC-10 | Ekspor Data Laporan | Admin | Should Have |
|
||||||
|
|
||||||
|
### 3.2 Detail Use Case
|
||||||
|
|
||||||
|
**UC-01 Mengelola Data Infrastruktur Spasial**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Admin menambahkan data Line (Jalan) dan Polygon (Parsil Tanah) untuk memberikan konteks tata ruang pada peta kemiskinan. |
|
||||||
|
| **Aktor** | Admin |
|
||||||
|
| **Precondition** | Admin telah masuk ke sistem dan memilih mode (Jalan atau Tanah) di toolbar. |
|
||||||
|
| **Main Flow** | 1. Admin memilih mode "Jalan" atau "Tanah" dari toolbar. 2. Admin melakukan klik berkali-kali pada peta untuk membentuk jalur (Line) atau bidang (Polygon). 3. Sistem secara otomatis menghitung panjang jalan (m) atau luas tanah (m²) secara *real-time*. 4. Admin mengisi atribut terkait: Nama Jalan dan Status Jalan (Nasional/Provinsi/Kabupaten), atau Nomor Persil dan Status Milik (SHM/HGB/HGU/HP). 5. Admin menekan tombol "SIMPAN". 6. Sistem mengonversi koordinat ke format GeoJSON dan menyimpannya ke database. |
|
||||||
|
| **Alternative Flow** | 3a. Jika Polygon belum membentuk minimal 3 titik (segitiga), tombol simpan tidak dapat diklik. |
|
||||||
|
| **Postcondition** | Infrastruktur baru tergambar di peta dan data tersimpan di database (tabel *jalan* atau *persil*). |
|
||||||
|
| **Prioritas** | Must Have |
|
||||||
|
|
||||||
|
**UC-02 Mengelola Data SPBU**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Admin menambahkan dan mengelola titik lokasi SPBU beserta informasi operasional. |
|
||||||
|
| **Aktor** | Admin |
|
||||||
|
| **Precondition** | Admin telah memilih mode "SPBU" di toolbar. |
|
||||||
|
| **Main Flow** | 1. Admin memilih mode "SPBU" dari toolbar. 2. Admin melakukan klik (*plotting*) pada koordinat lokasi SPBU di peta. 3. *Popup* formulir input muncul. 4. Admin mengisi Nama SPBU, Nomor SPBU, dan Status 24 Jam (Ya/Tidak). 5. Admin menekan tombol "SIMPAN". 6. Sistem menyimpan data SPBU ke database. |
|
||||||
|
| **Alternative Flow** | — |
|
||||||
|
| **Postcondition** | Marker SPBU muncul di peta dengan ikon berbeda berdasarkan status 24 jam: biru (SPBU 24 jam) dan oranye (SPBU biasa). |
|
||||||
|
| **Prioritas** | Must Have |
|
||||||
|
|
||||||
|
**UC-03 Mengelola Data Masjid dan Radius**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Admin atau PIC menambahkan titik lokasi Masjid pada peta beserta informasi identitas dan radius layanan *default*. |
|
||||||
|
| **Aktor** | Admin / PIC Masjid |
|
||||||
|
| **Precondition** | Admin telah memilih mode "Masjid & Radius" di toolbar. |
|
||||||
|
| **Main Flow** | 1. Admin memilih menu "Masjid & Radius" dari toolbar. 2. Admin melakukan *plotting* satu titik (*marker*) pada peta. 3. Sistem menggambar lingkaran (*circle*) sementara dengan radius *default* 500m. 4. Admin mengisi Nama Masjid, PIC, dan Alamat Masjid. 5. Admin menggeser *slider* radius (100m–5000m) untuk menyesuaikan besaran lingkaran secara *real-time*. 6. Admin menekan tombol "SIMPAN MASJID". 7. Sistem menyimpan data titik koordinat dan nilai radius ke MySQL. |
|
||||||
|
| **Alternative Flow** | — |
|
||||||
|
| **Postcondition** | Titik Masjid muncul di peta lengkap dengan area lingkaran (*buffer*) hijau yang transparan dan tombol *toggle* radius. |
|
||||||
|
| **Prioritas** | Must Have |
|
||||||
|
|
||||||
|
**UC-04 Mengelola Data Penduduk Miskin**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Admin menambahkan data warga yang tergolong miskin ke dalam sistem untuk dianalisis posisinya terhadap jangkauan masjid. |
|
||||||
|
| **Aktor** | Admin / PIC Masjid |
|
||||||
|
| **Precondition** | Admin telah memilih mode "Kemiskinan" di toolbar. |
|
||||||
|
| **Main Flow** | 1. Admin memilih menu "Kemiskinan" dari toolbar. 2. Admin melakukan klik (*plotting*) pada koordinat rumah warga di peta. 3. *Popup* formulir input muncul. 4. Admin mengisi Nama Kepala Keluarga, Jumlah Anggota (Jiwa), dan Alamat Lengkap. 5. Admin mencentang jenis bantuan yang diterima (PKH, BPNT, BLT). 6. Opsional: Admin mengunggah foto KK (format JPG/PNG/WebP, maks. 5 MB). 7. Admin menekan tombol "SIMPAN". 8. Sistem menyimpan koordinat dan data demografi ke MySQL. |
|
||||||
|
| **Alternative Flow** | 6a. Jika foto tidak diunggah, data tetap tersimpan tanpa foto. 6b. Jika format atau ukuran foto tidak valid, sistem menampilkan pesan error dan menolak file. |
|
||||||
|
| **Postcondition** | Titik Penduduk muncul di peta. Warnanya bergantung pada hasil analisis radius (UC-07). |
|
||||||
|
| **Prioritas** | Must Have |
|
||||||
|
|
||||||
|
**UC-05 Mengelola Batas Wilayah Administratif**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Admin menggambar polygon batas wilayah administratif (RT/RW/Kelurahan/Kecamatan) untuk analisis distribusi kemiskinan per wilayah. |
|
||||||
|
| **Aktor** | Admin |
|
||||||
|
| **Precondition** | Admin telah memilih mode "Batas Wilayah" di toolbar. |
|
||||||
|
| **Main Flow** | 1. Admin memilih mode "Batas Wilayah" dari toolbar. 2. Admin melakukan klik berkali-kali pada peta untuk membentuk polygon batas wilayah. 3. Sistem secara otomatis menampilkan *preview* polygon yang sedang digambar. 4. Setelah selesai, Admin mengisi Nama Wilayah dan Tipe Wilayah (RT/RW/Kelurahan/Kecamatan). 5. Admin menekan tombol "SIMPAN WILAYAH". 6. Sistem mengonversi koordinat ke format GeoJSON dan menyimpan ke tabel *wilayah* di database. |
|
||||||
|
| **Alternative Flow** | 3a. Jika Polygon belum membentuk minimal 3 titik, formulir simpan tidak ditampilkan. |
|
||||||
|
| **Postcondition** | Polygon batas wilayah tergambar di peta dengan warna berbeda per tipe wilayah. Klik pada polygon menampilkan statistik KK di dalam wilayah tersebut di panel. |
|
||||||
|
| **Prioritas** | Must Have |
|
||||||
|
|
||||||
|
**UC-06 Visualisasi Peta Multilayer**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Pengguna melihat representasi visual dari seluruh data geografis dengan kontrol visibilitas (*Layer Control*). |
|
||||||
|
| **Aktor** | Semua Pengguna |
|
||||||
|
| **Precondition** | Halaman WebGIS dimuat dan browser memiliki koneksi internet untuk menarik *tiles* dari server OSM. |
|
||||||
|
| **Main Flow** | 1. Pengguna membuka halaman WebGIS. 2. Sistem memuat library Leaflet.js dan ubin peta dasar dari OpenStreetMap. 3. Sistem menggunakan Fetch API untuk memuat semua data dari PHP (`get_all_data.php`). 4. Sistem mengelompokkan data ke dalam L.layerGroup (SPBU 24 Jam, SPBU Biasa, Jalan, Persil, Kemiskinan, Masjid, Wilayah). 5. Sistem menjalankan analisis *coverage* otomatis dan merender semua marker dengan warna yang sesuai. 6. Pengguna menggunakan *Layer Control* di pojok kanan atas untuk mengaktifkan/menonaktifkan layer tertentu. |
|
||||||
|
| **Alternative Flow** | 3a. Jika database kosong, peta hanya menampilkan basis jalan (OSM) tanpa marker atau polygon. |
|
||||||
|
| **Postcondition** | Pengguna dapat menganalisis data spesifik (misal: hanya melihat Masjid dan Penduduk) dengan mudah melalui kontrol layer. |
|
||||||
|
| **Prioritas** | Must Have |
|
||||||
|
|
||||||
|
**UC-07 Analisis Intersect & Warna Otomatis (Core Feature)**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Sistem melakukan kalkulasi jarak secara otomatis di latar belakang untuk menentukan status jangkauan bantuan setiap KK. |
|
||||||
|
| **Aktor** | Sistem |
|
||||||
|
| **Precondition** | Data Masjid dan Data Penduduk Miskin berhasil dimuat dari database. |
|
||||||
|
| **Main Flow** | 1. Sistem melakukan iterasi pada seluruh titik Penduduk Miskin. 2. Untuk setiap penduduk, sistem menghitung jarak (*distanceTo*) ke seluruh titik Masjid yang status radius-nya aktif (*toggle* ON). 3. Jika jarak penduduk ke salah satu masjid aktif ≤ radius masjid tersebut, sistem mengklasifikasikan sebagai "Tercakup". 4. Sistem menerapkan ikon Merah untuk penduduk Tercakup dan ikon Abu-abu untuk penduduk *Blind Spot*. 5. Sistem memperbarui panel statistik (total KK terjangkau, total jiwa terjangkau, *breakdown* per masjid). 6. Sistem merender marker ke peta. |
|
||||||
|
| **Alternative Flow** | 2a. Jika radius masjid dimatikan (*toggle off*), masjid tersebut tidak ikut dalam perhitungan analisis. |
|
||||||
|
| **Postcondition** | Peta menampilkan dikotomi warna yang jelas antara penduduk yang sudah dijangkau masjid dan yang menjadi target bantuan pemerintah. Panel statistik diperbarui secara *real-time*. |
|
||||||
|
| **Prioritas** | Must Have |
|
||||||
|
|
||||||
|
**UC-08 Modifikasi Radius Dinamis per Masjid**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | PIC Masjid mengubah radius jangkauan layanan atau menonaktifkan/mengaktifkan radius pada masjid tertentu secara individual. |
|
||||||
|
| **Aktor** | Admin / PIC Masjid |
|
||||||
|
| **Precondition** | Marker Masjid sudah dirender di peta. |
|
||||||
|
| **Main Flow** | **Alur A — Ubah Radius:** 1. Pengguna mengklik marker Masjid di peta. 2. *Popup* muncul menampilkan informasi masjid, *slider* radius, dan tombol *toggle*. 3. Pengguna menggeser *slider*; sistem secara instan mengubah ukuran lingkaran di peta. 4. Pengguna menekan tombol "UPDATE RADIUS". 5. Sistem mengirim ID Masjid dan nilai radius baru via Fetch API ke PHP. 6. Analisis *coverage* dijalankan ulang secara otomatis. **Alur B — Toggle Radius:** 1. Pengguna mengklik tombol "Aktif/Nonaktif" pada *popup* masjid. 2. Lingkaran radius masjid tersebut ditampilkan atau disembunyikan. 3. Analisis *coverage* dijalankan ulang tanpa menutup *popup*. |
|
||||||
|
| **Alternative Flow** | — |
|
||||||
|
| **Postcondition** | Radius masjid di database diperbarui (Alur A). Status cakupan warna penduduk di sekitarnya berubah mengikuti perubahan radius atau status *toggle* (Alur A & B). |
|
||||||
|
| **Prioritas** | Must Have |
|
||||||
|
|
||||||
|
**UC-09 Filter dan Pencarian Data Kemiskinan**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Pengguna melakukan pencarian nama KK atau memfilter data berdasarkan jenis bantuan yang diterima (PKH/BPNT/BLT) melalui panel statistik. |
|
||||||
|
| **Aktor** | Admin / PIC Masjid |
|
||||||
|
| **Precondition** | Data kemiskinan telah dimuat dan panel statistik ditampilkan. |
|
||||||
|
| **Main Flow** | 1. Pengguna mengetik nama KK pada kolom pencarian di panel. 2. Sistem secara *real-time* (per karakter yang diketik) memfilter daftar KK yang sesuai. 3. Marker KK yang tidak sesuai filter disembunyikan dari peta. 4. Panel menampilkan daftar hasil filter beserta status jangkauan masing-masing KK. 5. Opsional: Pengguna menekan *chip* filter bantuan (PKH/BPNT/BLT) untuk menyaring lebih lanjut. Filter bantuan dapat dikombinasikan dengan pencarian nama. 6. Pengguna menekan tombol "Hapus Filter" untuk kembali ke tampilan lengkap. |
|
||||||
|
| **Alternative Flow** | 2a. Jika tidak ada KK yang cocok dengan kata kunci, panel menampilkan pesan "Tidak ada data ditemukan". |
|
||||||
|
| **Postcondition** | Panel dan peta menampilkan hanya data KK yang sesuai filter yang diterapkan. |
|
||||||
|
| **Prioritas** | Should Have |
|
||||||
|
|
||||||
|
**UC-10 Ekspor Data Laporan**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Deskripsi** | Admin mengekspor data kemiskinan dan statistik *coverage* dalam format CSV atau PDF untuk keperluan pelaporan. |
|
||||||
|
| **Aktor** | Admin |
|
||||||
|
| **Precondition** | Data kemiskinan telah dimuat dan analisis *coverage* telah dijalankan. |
|
||||||
|
| **Main Flow** | **Ekspor CSV:** 1. Admin menekan tombol "Export CSV" di panel. 2. Sistem menghasilkan file CSV menggunakan Blob API di sisi klien. 3. File berisi kolom: No, Nama KK, Jumlah Jiwa, Alamat, PKH, BPNT, BLT, Status Jangkauan, Nama Masjid yang Menjangkau, Jarak Terdekat (m), Foto. 4. Browser mengunduh file secara otomatis dengan encoding UTF-8 BOM. **Ekspor PDF:** 1. Admin menekan tombol "Export PDF" di panel. 2. Sistem mengirim *request* ke server PHP (`cetak.php`). 3. Server menghasilkan PDF A4 *Landscape* menggunakan mPDF berisi: judul laporan, tanggal cetak, tabel ringkasan statistik, tabel *breakdown* per masjid, dan tabel daftar KK lengkap. 4. Browser mengunduh file PDF secara otomatis. |
|
||||||
|
| **Alternative Flow** | 1a. (CSV) Jika filter aktif, CSV hanya berisi data KK yang sesuai filter. |
|
||||||
|
| **Postcondition** | File laporan berhasil diunduh oleh pengguna. |
|
||||||
|
| **Prioritas** | Should Have |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KEBUTUHAN NON-FUNGSIONAL
|
||||||
|
|
||||||
|
### 4.1 Spesifikasi Performa (Performance)
|
||||||
|
|
||||||
|
Mengatur tingkat kecepatan dan efisiensi sistem dalam memproses data spasial serta interaksi pengguna secara dinamis.
|
||||||
|
|
||||||
|
| **ID** | **Deskripsi Kebutuhan** |
|
||||||
|
|---|---|
|
||||||
|
| **P-01** | **Kecepatan Muat:** Seluruh komponen peta dasar dan aset data (SPBU, Jalan, Tanah, Masjid, Penduduk, Wilayah) harus muncul sepenuhnya dalam waktu kurang dari 3 detik pada koneksi LAN. |
|
||||||
|
| **P-02** | **Efisiensi Analisis:** Kalkulasi perubahan warna marker penduduk (Merah/Abu-abu) saat *slider* radius digeser harus diproses secara *asynchronous* di sisi klien tanpa memerlukan *refresh* halaman, dengan respons < 500 ms. |
|
||||||
|
| **P-03** | **Optimalisasi Query:** Pengambilan data koordinat melalui `get_all_data.php` menggunakan format JSON agar tidak membebani memori browser, meskipun jumlah titik penduduk mencapai ratusan. |
|
||||||
|
| **P-04** | **Responsivitas Filter:** Fungsi pencarian dan filter KK harus memberikan respons visual < 200 ms per karakter yang diketik. |
|
||||||
|
| **P-05** | **Waktu Generate PDF:** Server dapat menghasilkan PDF untuk ≤ 200 KK dalam waktu < 5 detik. |
|
||||||
|
| **P-06** | **Kapasitas Data:** Sistem mampu mengelola ≥ 500 KK dan ≥ 20 Masjid secara bersamaan tanpa penurunan performa yang signifikan. |
|
||||||
|
|
||||||
|
### 4.2 Spesifikasi Keamanan (Security)
|
||||||
|
|
||||||
|
Melindungi integritas basis data sistem dan menjaga kerahasiaan informasi kependudukan warga.
|
||||||
|
|
||||||
|
| **ID** | **Deskripsi Kebutuhan** |
|
||||||
|
|---|---|
|
||||||
|
| **S-01** | **Pencegahan SQL Injection:** Seluruh proses input data pada file PHP menggunakan fungsi `mysqli_real_escape_string()` dan validasi tipe data sebelum masuk query SQL, guna mencegah manipulasi *database*. |
|
||||||
|
| **S-02** | **Privasi Data:** Sistem tidak menampilkan data sensitif seperti NIK pada peta publik; hanya Nama KK dan status cakupan bantuan yang ditampilkan di *popup* peta. |
|
||||||
|
| **S-03** | **Validasi Spasial:** Sistem menolak data input jika format *Latitude* dan *Longitude* tidak sesuai standar koordinat geografis (angka desimal) untuk menjaga validitas visual peta. |
|
||||||
|
| **S-04** | **Validasi Upload File:** Sistem memvalidasi tipe file foto di sisi server (hanya JPG, JPEG, PNG, WebP yang diterima) dan membatasi ukuran file maksimal 5 MB untuk mencegah *file upload exploit*. |
|
||||||
|
| **S-05** | **Whitelist Operasi Hapus:** Operasi DELETE data menggunakan *whitelist* nama tabel yang diizinkan pada `hapus.php` untuk mencegah penghapusan tabel sembarang (*arbitrary table deletion*). |
|
||||||
|
| **S-06** | **Validasi Enum:** Nilai yang memiliki pilihan terbatas (status jalan, tipe wilayah, status SPBU, status milik persil) divalidasi terhadap daftar nilai yang diizinkan sebelum masuk ke database. |
|
||||||
|
|
||||||
|
### 4.3 Spesifikasi Kemudahan Penggunaan (Usability)
|
||||||
|
|
||||||
|
Menjamin sistem dapat dioperasikan secara intuitif oleh pengurus masjid maupun admin instansi pemerintah.
|
||||||
|
|
||||||
|
| **ID** | **Deskripsi Kebutuhan** |
|
||||||
|
|---|---|
|
||||||
|
| **U-01** | **Legenda Peta:** Sistem menyediakan legenda visual yang jelas untuk membedakan arti warna marker: Merah (Penduduk Tercakup Masjid) dan Abu-abu (Penduduk *Blind Spot*). |
|
||||||
|
| **U-02** | **Interaksi Intuitif:** Pengguna dapat menentukan lokasi objek (Masjid/Rumah/SPBU) secara akurat cukup dengan melakukan klik langsung pada posisi di peta untuk mendapatkan koordinat otomatis. |
|
||||||
|
| **U-03** | **Kontrol Navigasi:** Tersedianya alat navigasi standar Leaflet (*Zoom In/Out*) serta fitur *Layer Control* untuk memfilter visibilitas data infrastruktur dan data sosial secara terpisah. |
|
||||||
|
| **U-04** | **Diferensiasi Warna Toolbar:** Setiap tombol layer aktif memiliki warna yang berbeda (SPBU: oranye, Jalan: biru, Persil: hijau teal, Wilayah: indigo, Kemiskinan: ungu, Masjid: hijau) untuk identifikasi cepat mode aktif. |
|
||||||
|
| **U-05** | **Panel Statistik Interaktif:** Panel menampilkan ringkasan statistik secara *real-time* dan dapat menampilkan detail per masjid atau per wilayah melalui navigasi internal panel tanpa *reload* halaman. |
|
||||||
|
| **U-06** | **Ikon Material Design:** Sistem menggunakan Google Material Symbols sebagai ikon antarmuka yang konsisten dan familiar, menggantikan seluruh penggunaan emoji. |
|
||||||
|
|
||||||
|
### 4.4 Spesifikasi Reliabilitas (Reliability)
|
||||||
|
|
||||||
|
Menjamin tingkat keakuratan hasil analisis jangkauan sosial dan ketersediaan data secara konsisten.
|
||||||
|
|
||||||
|
| **ID** | **Deskripsi Kebutuhan** |
|
||||||
|
|---|---|
|
||||||
|
| **AV-01** | **Akurasi Spasial:** Perhitungan jangkauan menggunakan algoritma *geodesic* (Leaflet `distanceTo`) untuk memastikan akurasi klasifikasi pada penduduk yang berada di perbatasan radius. |
|
||||||
|
| **AV-02** | **Ketersediaan Data:** Sistem menjamin data koordinat yang diinput tersimpan secara permanen pada database MySQL dan dapat dipanggil secara konsisten setiap kali halaman dimuat ulang. |
|
||||||
|
| **AV-03** | **Anti-Double Count:** Sistem menjamin KK yang dijangkau oleh lebih dari satu masjid tetap dihitung sebagai 1 KK terjangkau (tidak ada *double counting* dalam statistik total coverage). |
|
||||||
|
| **AV-04** | **Konsistensi State Toggle:** Status *toggle* radius per masjid dipertahankan selama sesi browser aktif menggunakan objek *state* `radiusVisibleState{}` tanpa perlu *refresh* halaman. |
|
||||||
|
|
||||||
|
### 4.5 Spesifikasi Portabilitas (Portability)
|
||||||
|
|
||||||
|
Memastikan sistem dapat berjalan di berbagai lingkungan operasional dan perangkat tanpa kendala teknis.
|
||||||
|
|
||||||
|
| **ID** | **Deskripsi Kebutuhan** |
|
||||||
|
|---|---|
|
||||||
|
| **C-01** | **Kompatibilitas Browser:** Antarmuka WebGIS harus berfungsi penuh pada browser modern utama: Google Chrome, Mozilla Firefox, dan Microsoft Edge versi stabil terbaru. |
|
||||||
|
| **C-02** | **Akses Lintas Perangkat:** Tata letak peta dan form input responsif sehingga tetap nyaman digunakan pada Desktop, Laptop, maupun Tablet. |
|
||||||
|
| **C-03** | **Kemudahan Deployment:** Kode program terstruktur sehingga aplikasi mudah dipindahkan dari server lokal (XAMPP/Laragon) ke server *hosting* daring tanpa perubahan kode yang masif. |
|
||||||
|
| **C-04** | **Versi PHP:** Aplikasi kompatibel dengan PHP versi 7.4 ke atas dengan ekstensi MySQLi dan GD Library (untuk pemrosesan upload foto) aktif. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ANTARMUKA EKSTERNAL
|
||||||
|
|
||||||
|
### 5.1 Antarmuka Pengguna
|
||||||
|
|
||||||
|
Antarmuka pengguna sistem dirancang dengan prinsip Material Design untuk mempermudah visualisasi data spasial dengan kriteria desain responsif dan interaktif agar dapat dioperasikan pada berbagai resolusi layar.
|
||||||
|
|
||||||
|
| **Halaman** | **Deskripsi** | **Referensi** |
|
||||||
|
|---|---|---|
|
||||||
|
| **Halaman Peta Utama** | Area kerja utama yang menampilkan peta interaktif (Leaflet), toolbar navigasi (SPBU, Jalan, Tanah, Wilayah, Kemiskinan, Masjid), dan *Layer Control* untuk filter visibilitas. | Desain WebGIS Utama |
|
||||||
|
| **Form Input SPBU** | *Popup* formulir untuk menginput Nama SPBU, Nomor, dan Status 24 Jam. | Form Data SPBU |
|
||||||
|
| **Form Input Infrastruktur** | *Popup* formulir untuk menginput data Jalan (Line) dan Tanah (Polygon) yang muncul setelah geometri selesai digambar. | Form Data Spasial |
|
||||||
|
| **Form Input Masjid** | Panel input untuk Nama Masjid, PIC, Alamat, dan *slider* radius deteksi (100m–5000m) yang mengubah ukuran lingkaran secara *real-time*. | Form Data Masjid |
|
||||||
|
| **Form Input Penduduk Miskin** | Panel input untuk Nama KK, Alamat, Jumlah Jiwa, *checkbox* bantuan (PKH/BPNT/BLT), dan *upload* foto KK. | Form Data Miskin |
|
||||||
|
| **Form Input Batas Wilayah** | Panel input untuk Nama Wilayah dan Tipe (RT/RW/Kelurahan/Kecamatan) yang muncul setelah polygon selesai digambar. | Form Data Wilayah |
|
||||||
|
| **Panel Statistik Real-Time** | Panel di sisi peta yang menampilkan ringkasan total KK/jiwa/terjangkau/*blind spot*, *breakdown* per masjid, *breakdown* per wilayah, area filter/pencarian, dan tombol Export CSV/PDF. | Panel Analisis |
|
||||||
|
| **Popup Informasi & Update** | Jendela kecil yang muncul saat marker diklik, menampilkan detail data, *slider* radius dan tombol *toggle* (untuk masjid), tombol Edit, dan tombol Hapus. | Detail Popup & Update |
|
||||||
|
|
||||||
|
### 5.2 Antarmuka Perangkat Keras (Hardware Interface)
|
||||||
|
|
||||||
|
Sistem membutuhkan spesifikasi perangkat keras minimal agar analisis spasial dan pemrosesan data koordinat berjalan optimal.
|
||||||
|
|
||||||
|
| **Komponen** | **Spesifikasi Minimal** |
|
||||||
|
|---|---|
|
||||||
|
| **Perangkat Input** | Mendukung mouse, keyboard, dan layar sentuh untuk navigasi peta di perangkat tablet/mobile. |
|
||||||
|
| **Perangkat Output** | Monitor/Layar dengan resolusi minimal 1024×768 untuk tampilan analisis spasial yang jernih. |
|
||||||
|
| **Koneksi Internet** | Bandwidth minimal 2 Mbps untuk proses *streaming* map tiles secara asinkron dari server OpenStreetMap. |
|
||||||
|
| **Penyimpanan Server** | Server web dengan kapasitas minimal 500 MB untuk menyimpan skrip PHP, basis data MySQL, dan file foto KK yang diunggah. |
|
||||||
|
|
||||||
|
### 5.3 Antarmuka Perangkat Lunak (Software Interface)
|
||||||
|
|
||||||
|
Sistem berinteraksi dengan beberapa komponen perangkat lunak eksternal dan library untuk menjalankan fungsi GIS.
|
||||||
|
|
||||||
|
| **Layanan / Sistem** | **Tujuan Integrasi** | **Versi / Protokol** |
|
||||||
|
|---|---|---|
|
||||||
|
| **OpenStreetMap (OSM)** | Penyedia *Base Map* (Peta Dasar) yang dipanggil secara dinamis melalui *Tile Layer*. | HTTP/HTTPS |
|
||||||
|
| **Leaflet.js** | Library JavaScript utama untuk pengolahan layer, marker, dan perhitungan jarak *geodesic* di sisi klien. | v1.9.4 |
|
||||||
|
| **Leaflet.GeometryUtil** | Plugin untuk kalkulasi luas area (m²) parsil tanah dan panjang jalan secara akurat. | v0.10.3 |
|
||||||
|
| **Leaflet.heat** | Plugin untuk visualisasi heatmap kepadatan titik kemiskinan. | v0.2.0 |
|
||||||
|
| **Leaflet Color Markers** | Plugin CDN untuk ikon marker berwarna yang membedakan status jangkauan (merah/abu-abu) dan tipe SPBU. | via CDN |
|
||||||
|
| **Google Material Symbols** | *Icon font* (variabel) untuk ikon antarmuka pada toolbar dan tombol-tombol UI. | via Google Fonts CDN |
|
||||||
|
| **Google Fonts (Roboto)** | *Font* teks antarmuka dengan gaya Material Design. | via Google Fonts CDN |
|
||||||
|
| **MySQL Database** | Tempat penyimpanan data koordinat seluruh objek (Point, Line, Polygon) dan data demografi penduduk. | v5.7+ / SQL Query |
|
||||||
|
| **PHP Native** | Bahasa pemrograman *server-side* untuk logika bisnis, API *endpoint*, dan pemrosesan file upload. | v7.4+ |
|
||||||
|
| **mPDF** | Library PHP untuk menghasilkan dokumen PDF laporan kemiskinan dari konten HTML. | v8.3 |
|
||||||
|
| **Web Browser** | *Runtime environment* untuk eksekusi JavaScript/HTML5 dan rendering peta Leaflet. | Chrome / Firefox / Edge |
|
||||||
|
| **XAMPP / Laragon** | Lingkungan server lokal (Apache + MySQL + PHP) untuk pengembangan dan demonstrasi. | XAMPP 8.x |
|
||||||
|
|
||||||
|
### 5.4 Antarmuka Komunikasi (Communication Interface)
|
||||||
|
|
||||||
|
Protokol komunikasi yang digunakan untuk menjamin pertukaran data antara klien dan server berjalan lancar.
|
||||||
|
|
||||||
|
| **Jalur Komunikasi** | **Deskripsi Mekanisme** |
|
||||||
|
|---|---|
|
||||||
|
| **Client–Server** | Komunikasi antara browser pengguna dan server web menggunakan protokol HTTP (lokal) atau HTTPS (produksi). |
|
||||||
|
| **Data Exchange (JSON)** | Pengiriman data dari MySQL ke peta dilakukan melalui skrip PHP yang menghasilkan format JSON untuk diproses oleh JavaScript (`get_all_data.php`). |
|
||||||
|
| **Database Connectivity** | Skrip PHP berkomunikasi dengan database MySQL secara internal menggunakan ekstensi MySQLi (`mysqli_query`, `mysqli_fetch_all`). |
|
||||||
|
| **Asynchronous Fetch API** | Operasi simpan, update, dan hapus data menggunakan JavaScript Fetch API (metode POST) untuk komunikasi asinkron tanpa *reload* halaman. |
|
||||||
|
| **Asynchronous Map Tiles** | Permintaan ubin peta (*map tiles*) dilakukan secara asinkron oleh Leaflet sehingga peta tetap lancar saat digeser (*panning*). |
|
||||||
|
| **File Upload (Multipart)** | Upload foto KK menggunakan `multipart/form-data` melalui PHP `$_FILES`, disimpan ke folder `uploads/kemiskinan/` di server. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DEFINISI FUNGSI DAN KEBUTUHAN PENGGUNA
|
||||||
|
|
||||||
|
### 6.1 Kebutuhan Pengguna
|
||||||
|
|
||||||
|
| **ID** | **Peran Pengguna** | **Kebutuhan / Goal** | **Fungsi Sistem Terkait** |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **UR-01** | Pengurus Masjid | Menentukan wilayah tanggung jawab sosial di sekitar masjid secara presisi. | Pengaturan Radius Dinamis per Masjid |
|
||||||
|
| **UR-02** | Pengurus Masjid | Memetakan posisi jamaah/warga miskin agar bantuan tepat sasaran. | Manajemen Data & *Plotting* Penduduk |
|
||||||
|
| **UR-03** | Pengurus Masjid | Menonaktifkan radius masjid tertentu sementara agar tidak masuk analisis. | *Toggle* Radius per Masjid |
|
||||||
|
| **UR-04** | Pemerintah/Dinsos | Mengetahui wilayah mana yang penduduk miskinnya belum tersentuh bantuan masjid (*blind spot*). | Klasifikasi Warna Marker Otomatis |
|
||||||
|
| **UR-05** | Pemerintah/Dinsos | Mencari data KK berdasarkan nama atau jenis bantuan untuk keperluan verifikasi lapangan. | Filter dan Pencarian KK |
|
||||||
|
| **UR-06** | Pemerintah/Dinsos | Mengunduh laporan kemiskinan untuk keperluan rapat atau pengajuan anggaran. | Ekspor CSV dan PDF |
|
||||||
|
| **UR-07** | Pemerintah/Dinsos | Mengetahui sebaran kemiskinan per wilayah administratif (RT/RW) untuk alokasi bantuan yang adil. | Analisis Batas Wilayah & Panel Statistik |
|
||||||
|
| **UR-08** | Masyarakat Umum | Melihat transparansi sebaran rumah ibadah dan kontribusinya terhadap pengentasan kemiskinan di lingkungan. | Visualisasi Peta Multilayer Interaktif |
|
||||||
|
|
||||||
|
### 6.2 Detail Fungsionalitas Spesifik
|
||||||
|
|
||||||
|
#### 6.2.1 Fungsi Analisis Radius Jangkauan
|
||||||
|
|
||||||
|
- **Input:** Koordinat Masjid (*Lat_m*, *Long_m*), nilai Radius (*R*) dalam meter, dan status *toggle* radius per masjid.
|
||||||
|
- **Proses:** Sistem menggunakan library Leaflet untuk menggambar lingkaran transparan di sekeliling titik masjid. Secara *back-end* (di sisi klien), sistem membandingkan jarak setiap Penduduk (P) terhadap pusat Masjid (M) menggunakan formula Haversine (`marker.distanceTo()`). Hanya masjid yang status *toggle* radius-nya aktif (ON) yang diikutsertakan dalam perhitungan.
|
||||||
|
- **Output:** Penentuan status penduduk berdasarkan kondisi:
|
||||||
|
- Jika Jarak (P, M) ≤ R untuk setidaknya satu masjid aktif, maka penduduk masuk kategori "Terjangkau".
|
||||||
|
- Jika Jarak (P, M) > R untuk semua masjid aktif, maka penduduk masuk kategori "*Blind Spot*".
|
||||||
|
- **Anti-Double Count:** KK yang dijangkau lebih dari satu masjid tetap dihitung 1 KK dalam statistik total menggunakan struktur `kkCoverage[kkId]` yang berbasis ID unik KK.
|
||||||
|
|
||||||
|
#### 6.2.2 Fungsi Analisis Point-in-Polygon (Batas Wilayah)
|
||||||
|
|
||||||
|
- **Input:** Koordinat KK (titik) dan koordinat *vertex* polygon Batas Wilayah dalam format GeoJSON.
|
||||||
|
- **Proses:** Sistem menggunakan algoritma *ray casting* (`pointInPolygon()`) untuk menentukan apakah sebuah titik KK berada di dalam polygon wilayah. Algoritma memancarkan sebuah *ray* (sinar) horizontal dari titik ke arah tak terhingga dan menghitung jumlah perpotongan dengan sisi-sisi polygon. Jumlah ganjil berarti titik berada di dalam polygon.
|
||||||
|
- **Output:** Daftar KK per wilayah beserta statistik: jumlah total KK, jumlah jiwa, KK terjangkau masjid, dan KK *blind spot* dalam wilayah tersebut. Ditampilkan di panel saat wilayah diklik.
|
||||||
|
|
||||||
|
#### 6.2.3 Fungsi Klasifikasi Warna Otomatis (Marker Coloring)
|
||||||
|
|
||||||
|
Sistem mengubah atribut visual secara dinamis berdasarkan hasil analisis radius tanpa intervensi manual tambahan:
|
||||||
|
|
||||||
|
- **Marker Merah:** Menunjukkan penduduk miskin yang berada dalam jangkauan minimal satu masjid aktif. Fungsi ini membantu pihak masjid memvalidasi daftar penerima bantuan internal mereka.
|
||||||
|
- **Marker Abu-abu:** Menunjukkan penduduk miskin yang berada di luar jangkauan semua masjid aktif (*Blind Spot*). Fungsi ini memberikan sinyal darurat bagi pemerintah untuk segera memberikan intervensi bantuan di titik tersebut.
|
||||||
|
|
||||||
|
#### 6.2.4 Fungsi Informasi Interaktif (Tooltip & Popup)
|
||||||
|
|
||||||
|
Sistem menampilkan informasi detail saat pengguna mengklik objek di peta:
|
||||||
|
|
||||||
|
- **Informasi Masjid:** Nama Masjid, Nama PIC/Pengurus, Alamat, *slider* radius, tombol *toggle* radius (Aktif/Nonaktif), tombol Edit, tombol Hapus, dan jumlah KK yang dijangkau masjid ini.
|
||||||
|
- **Informasi Penduduk:** Profil KK (nama, jumlah jiwa, alamat), jenis bantuan (PKH/BPNT/BLT), foto KK (jika ada), status jangkauan (Terjangkau/*Blind Spot*), dan nama masjid yang menjangkau beserta jaraknya dalam meter.
|
||||||
|
- **Informasi SPBU:** Nama SPBU, Nomor, Status 24 Jam, tombol Edit, tombol Hapus.
|
||||||
|
- **Informasi Infrastruktur:** Panjang jalan otomatis (m), luas parsil tanah (m²), status kepemilikan/jalan, tombol Edit dan Hapus.
|
||||||
|
- **Informasi Batas Wilayah:** Nama wilayah, tipe (RT/RW/Kelurahan/Kecamatan), dan tombol untuk melihat detail KK per wilayah di panel statistik.
|
||||||
|
|
||||||
|
#### 6.2.5 Fungsi Filter dan Pencarian
|
||||||
|
|
||||||
|
- **Pencarian Nama KK:** Input teks *real-time* yang memfilter daftar KK berdasarkan kecocokan parsial nama (case-insensitive). Hasil filter ditampilkan di panel dan marker yang tidak cocok disembunyikan dari peta.
|
||||||
|
- **Filter Bantuan (PKH/BPNT/BLT):** Tombol *chip* yang dapat diklik untuk memfilter KK berdasarkan jenis bantuan yang diterima. Filter dapat dikombinasikan (multi-filter) dan digabungkan dengan pencarian nama.
|
||||||
|
- **Reset Filter:** Tombol "Hapus Filter" mengembalikan tampilan ke kondisi semula (semua KK ditampilkan di panel dan peta).
|
||||||
|
|
||||||
|
#### 6.2.6 Fungsi Ekspor Data
|
||||||
|
|
||||||
|
**Ekspor CSV:**
|
||||||
|
|
||||||
|
- Menggunakan JavaScript Blob API (*client-side*) tanpa memerlukan *request* tambahan ke server.
|
||||||
|
- Kolom: No, Nama KK, Jumlah Jiwa, Alamat, PKH, BPNT, BLT, Status Jangkauan, Dijangkau Masjid (nama-nama masjid), Jarak Terdekat (m), Nama File Foto.
|
||||||
|
- Encoding UTF-8 BOM untuk kompatibilitas Microsoft Excel.
|
||||||
|
- Mencerminkan filter yang sedang aktif saat tombol diklik.
|
||||||
|
|
||||||
|
**Ekspor PDF:**
|
||||||
|
|
||||||
|
- Menggunakan library mPDF v8.3 di sisi server (`cetak.php`).
|
||||||
|
- Format kertas A4 *Landscape*.
|
||||||
|
- Konten laporan: judul dan tanggal cetak, tabel ringkasan statistik, tabel *breakdown* per masjid (nama, radius, KK terjangkau, jiwa terjangkau), dan tabel daftar KK lengkap.
|
||||||
|
- Data *coverage* (terjangkau/*blind spot*) dihitung ulang di server menggunakan formula Haversine dalam PHP untuk konsistensi dengan tampilan di peta.
|
||||||
|
|
||||||
|
### 6.3 Analisis Alur Kerja Pengentasan Kemiskinan
|
||||||
|
|
||||||
|
Secara operasional, sistem ini mendukung kolaborasi strategis dalam urutan berikut:
|
||||||
|
|
||||||
|
| **Tahap** | **Aktivitas** | **Pelaksana** |
|
||||||
|
|---|---|---|
|
||||||
|
| **Inisialisasi Data** | Admin memasukkan data infrastruktur (SPBU, Jalan, Persil), menggambar polygon batas wilayah administratif, memasukkan koordinat masjid, serta menginput data penduduk miskin ke dalam database. | Admin Sistem |
|
||||||
|
| **Penentuan Komitmen** | Pengurus masjid menyesuaikan *slider* radius untuk menentukan berapa meter wilayah yang sanggup mereka "kawal" secara finansial (misal: 500m). Masjid yang belum siap dapat dinonaktifkan radiusnya sementara menggunakan *toggle*. | Pengurus Masjid |
|
||||||
|
| **Identifikasi Celah** | Pemerintah membuka peta, mengaktifkan layer kemiskinan, dan mencari posisi Marker Abu-abu (*blind spot*). Fitur filter dapat digunakan untuk memprioritaskan KK yang tidak menerima bantuan apapun (bukan PKH, BPNT, maupun BLT). | Pemerintah / Dinsos |
|
||||||
|
| **Analisis Per Wilayah** | Admin mengklik polygon batas wilayah untuk melihat distribusi KK terjangkau vs. *blind spot* per RT/RW, guna mengidentifikasi wilayah yang paling membutuhkan intervensi. | Pemerintah / Dinsos |
|
||||||
|
| **Pelaporan** | Admin mengekspor data ke CSV atau PDF untuk keperluan rapat koordinasi, pengajuan anggaran, atau dokumentasi program pengentasan kemiskinan. | Admin Sistem |
|
||||||
|
| **Intervensi Target** | Pemerintah mengirimkan bantuan khusus ke area Marker Abu-abu, sementara Marker Merah dikelola secara mandiri oleh masjid lokal berdasarkan radius yang telah ditetapkan. | Pemerintah / Dinsos |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KESIMPULAN
|
||||||
|
|
||||||
|
Sebagai penutup, dokumen SKPL ini disusun untuk memberikan panduan komprehensif dalam pengembangan sistem **WebGIS Partisipasi Masjid untuk Pengentasan Kemiskinan**. Melalui integrasi antara basis data MySQL, logika PHP Native, dan visualisasi Leaflet.js, sistem ini mampu mengubah data kemiskinan yang kompleks menjadi informasi spasial yang mudah dipahami.
|
||||||
|
|
||||||
|
Fitur-fitur inti yang diimplementasikan — meliputi analisis radius Haversine per masjid, *toggle* radius individual, batas wilayah administratif dengan analisis *point-in-polygon*, filter dan pencarian data KK, panel statistik *real-time*, visualisasi *heatmap*, serta ekspor CSV dan PDF — memungkinkan kolaborasi antara pemerintah dan rumah ibadah dalam menyalurkan bantuan menjadi lebih transparan, terukur, dan tepat sasaran. Antarmuka berbasis Material Design dengan Google Material Symbols menjamin kemudahan penggunaan bagi berbagai kalangan pengguna, mulai dari petugas pemerintah hingga pengurus masjid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LAMPIRAN
|
||||||
|
|
||||||
|
**Gambar 1** WebGIS Partisipasi Masjid untuk Pengentasan Kemiskinan
|
||||||
|
|
||||||
|
*(Screenshot tampilan utama peta dengan semua layer aktif)*
|
||||||
|
|
||||||
|
**Gambar 2** Input Masjid dan Radius
|
||||||
|
|
||||||
|
*(Screenshot form input masjid dengan slider radius real-time)*
|
||||||
|
|
||||||
|
**Gambar 3** Input Data Penduduk Miskin
|
||||||
|
|
||||||
|
*(Screenshot form input KK dengan checkbox bantuan PKH/BPNT/BLT dan upload foto)*
|
||||||
|
|
||||||
|
**Gambar 4** Fitur Batas Wilayah dan Panel Statistik Per Wilayah
|
||||||
|
|
||||||
|
*(Screenshot polygon batas wilayah dan panel breakdown statistik per wilayah)*
|
||||||
|
|
||||||
|
**Gambar 5** Fitur Filter dan Pencarian Data KK
|
||||||
|
|
||||||
|
*(Screenshot panel filter dengan chip bantuan dan hasil pencarian real-time)*
|
||||||
|
|
||||||
|
**Gambar 6** Use Case Diagram
|
||||||
|
|
||||||
|
*(Diagram UML Use Case sistem WebGIS)*
|
||||||
|
|
||||||
|
**Gambar 7** ERD Diagram
|
||||||
|
|
||||||
|
*(Entity Relationship Diagram database: tabel spbu, masjid, kemiskinan, jalan, persil, wilayah)*
|
||||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM spbu";
|
||||||
|
$query = mysqli_query($conn, $sql);
|
||||||
|
|
||||||
|
$data_spbu = [];
|
||||||
|
while ($row = mysqli_fetch_assoc($query)) {
|
||||||
|
$data_spbu[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($data_spbu);
|
||||||
|
?>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$user_id = (int)($_SESSION['user']['id'] ?? 0);
|
||||||
|
$old = $_POST['old_password'] ?? '';
|
||||||
|
$new = $_POST['new_password'] ?? '';
|
||||||
|
|
||||||
|
if (strlen($new) < 6) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Password baru minimal 6 karakter']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$st = mysqli_prepare($conn, "SELECT password FROM users WHERE id = ?");
|
||||||
|
mysqli_stmt_bind_param($st, 'i', $user_id);
|
||||||
|
mysqli_stmt_execute($st);
|
||||||
|
$row = mysqli_fetch_assoc(mysqli_stmt_get_result($st));
|
||||||
|
|
||||||
|
if (!$row || !password_verify($old, $row['password'])) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Password lama tidak sesuai']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = password_hash($new, PASSWORD_DEFAULT);
|
||||||
|
$st2 = mysqli_prepare($conn, "UPDATE users SET password = ? WHERE id = ?");
|
||||||
|
mysqli_stmt_bind_param($st2, 'si', $hash, $user_id);
|
||||||
|
mysqli_stmt_execute($st2);
|
||||||
|
|
||||||
|
$_SESSION['user']['password'] = $hash;
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireSuperAdmin();
|
||||||
|
|
||||||
|
$type = $_POST['type'] ?? '';
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
$whitelist = ['ibadah' => 'rumah_ibadah', 'penduduk' => 'penduduk_miskin'];
|
||||||
|
if (!isset($whitelist[$type]) || $id <= 0) {
|
||||||
|
echo json_encode(['status'=>'error','message'=>'Request tidak valid.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $whitelist[$type];
|
||||||
|
if (mysqli_query($conn, "DELETE FROM `{$table}` WHERE id = $id")) {
|
||||||
|
if (mysqli_affected_rows($conn) === 0) {
|
||||||
|
echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan.']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status'=>'success']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status'=>'error','message'=>mysqli_error($conn)]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$rows = mysqli_fetch_all(mysqli_query($conn,
|
||||||
|
"SELECT p.*, i.nama AS ibadah_nama,
|
||||||
|
DATE_FORMAT(p.tanggal_bantuan,'%d/%m/%Y') AS tgl_fmt
|
||||||
|
FROM penduduk_miskin p
|
||||||
|
LEFT JOIN rumah_ibadah i ON i.id = p.rumah_ibadah_id
|
||||||
|
ORDER BY p.kk_nama"), MYSQLI_ASSOC);
|
||||||
|
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
header('Content-Type: text/csv; charset=UTF-8');
|
||||||
|
header("Content-Disposition: attachment; filename=penduduk_miskin_{$date}.csv");
|
||||||
|
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
// UTF-8 BOM for Excel
|
||||||
|
fwrite($out, "\xEF\xBB\xBF");
|
||||||
|
|
||||||
|
fputcsv($out, ['No','Nama KK','NIK','Alamat','Jumlah Anggota','Rumah Ibadah Terdekat','Jarak (m)','Status Bantuan','Jenis Bantuan','Tanggal Bantuan','Latitude','Longitude']);
|
||||||
|
|
||||||
|
$i = 1;
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
fputcsv($out, [
|
||||||
|
$i++,
|
||||||
|
$r['kk_nama'],
|
||||||
|
$r['nik'],
|
||||||
|
$r['alamat'],
|
||||||
|
$r['jumlah_anggota'],
|
||||||
|
$r['ibadah_nama'] ?: '-',
|
||||||
|
$r['jarak_meter'] ? round($r['jarak_meter']) : '-',
|
||||||
|
$r['status_bantuan'],
|
||||||
|
$r['jenis_bantuan'],
|
||||||
|
$r['tgl_fmt'] ?: '-',
|
||||||
|
$r['lat'],
|
||||||
|
$r['lng'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($out);
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
// ── Data ─────────────────────────────────────────────────────────────────────
|
||||||
|
$allIbadah = mysqli_fetch_all(
|
||||||
|
mysqli_query($conn, "SELECT * FROM rumah_ibadah ORDER BY nama"),
|
||||||
|
MYSQLI_ASSOC
|
||||||
|
);
|
||||||
|
|
||||||
|
$allPenduduk = mysqli_fetch_all(
|
||||||
|
mysqli_query($conn,
|
||||||
|
"SELECT p.*, i.nama AS ibadah_nama
|
||||||
|
FROM penduduk_miskin p
|
||||||
|
LEFT JOIN rumah_ibadah i ON i.id = p.rumah_ibadah_id
|
||||||
|
ORDER BY p.kk_nama"),
|
||||||
|
MYSQLI_ASSOC
|
||||||
|
);
|
||||||
|
|
||||||
|
// Anggota count per penduduk
|
||||||
|
$anggotaCounts = [];
|
||||||
|
$res = mysqli_query($conn, "SELECT penduduk_id, COUNT(*) as cnt FROM anggota_keluarga GROUP BY penduduk_id");
|
||||||
|
while ($r = mysqli_fetch_assoc($res)) {
|
||||||
|
$anggotaCounts[$r['penduduk_id']] = (int)$r['cnt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Statistik ─────────────────────────────────────────────────────────────────
|
||||||
|
$total = count($allPenduduk);
|
||||||
|
$sudah = count(array_filter($allPenduduk, fn($p) => $p['status_bantuan'] === 'sudah_dibantu'));
|
||||||
|
$belum = count(array_filter($allPenduduk, fn($p) => $p['status_bantuan'] === 'belum_dibantu'));
|
||||||
|
$menunggu = count(array_filter($allPenduduk, fn($p) => $p['status_bantuan'] === 'menunggu_verifikasi'));
|
||||||
|
$pctSudah = $total > 0 ? round($sudah / $total * 100, 1) : 0;
|
||||||
|
|
||||||
|
$bulan = ['','Januari','Februari','Maret','April','Mei','Juni',
|
||||||
|
'Juli','Agustus','September','Oktober','November','Desember'];
|
||||||
|
$tgl = date('j') . ' ' . $bulan[(int)date('n')] . ' ' . date('Y');
|
||||||
|
$cetakOleh = htmlspecialchars(currentUser()['nama'] ?? 'Admin');
|
||||||
|
|
||||||
|
// ── Baris tabel ibadah ────────────────────────────────────────────────────────
|
||||||
|
$rowsIbadah = '';
|
||||||
|
foreach ($allIbadah as $i => $ib) {
|
||||||
|
$binaan = count(array_filter($allPenduduk, fn($p) => $p['rumah_ibadah_id'] == $ib['id']));
|
||||||
|
$bg = ($i % 2 === 0) ? '#ffffff' : '#f9fafb';
|
||||||
|
$rowsIbadah .= "
|
||||||
|
<tr style='background:{$bg}'>
|
||||||
|
<td style='text-align:center'>" . ($i + 1) . "</td>
|
||||||
|
<td><b>" . htmlspecialchars($ib['nama']) . "</b></td>
|
||||||
|
<td style='text-align:center'>" . htmlspecialchars($ib['jenis']) . "</td>
|
||||||
|
<td style='text-align:center'>{$ib['radius']} m</td>
|
||||||
|
<td style='text-align:center'><b style='color:#1a7a3e'>{$binaan}</b></td>
|
||||||
|
</tr>";
|
||||||
|
}
|
||||||
|
if (!$rowsIbadah) {
|
||||||
|
$rowsIbadah = "<tr><td colspan='5' style='text-align:center;color:#999'>Belum ada data rumah ibadah.</td></tr>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Baris tabel penduduk ─────────────────────────────────────────────────────
|
||||||
|
$statusLabel = [
|
||||||
|
'belum_dibantu' => 'Belum Dibantu',
|
||||||
|
'sudah_dibantu' => 'Sudah Dibantu',
|
||||||
|
'menunggu_verifikasi'=> 'Menunggu Verifikasi',
|
||||||
|
];
|
||||||
|
$statusColor = [
|
||||||
|
'belum_dibantu' => '#c0392b',
|
||||||
|
'sudah_dibantu' => '#1a7a3e',
|
||||||
|
'menunggu_verifikasi'=> '#856404',
|
||||||
|
];
|
||||||
|
|
||||||
|
$rowsPenduduk = '';
|
||||||
|
foreach ($allPenduduk as $i => $p) {
|
||||||
|
$bg = ($i % 2 === 0) ? '#ffffff' : '#f9fafb';
|
||||||
|
$status = $p['status_bantuan'];
|
||||||
|
$slbl = $statusLabel[$status] ?? $status;
|
||||||
|
$scol = $statusColor[$status] ?? '#333';
|
||||||
|
$jml = $p['jumlah_anggota'] ?? ($anggotaCounts[$p['id']] ?? 1);
|
||||||
|
$jarak = $p['jarak_meter'] ? round($p['jarak_meter']) . ' m' : '-';
|
||||||
|
$tglB = $p['tanggal_bantuan']
|
||||||
|
? date('d/m/Y', strtotime($p['tanggal_bantuan'])) : '-';
|
||||||
|
|
||||||
|
$rowsPenduduk .= "
|
||||||
|
<tr style='background:{$bg}'>
|
||||||
|
<td style='text-align:center'>" . ($i + 1) . "</td>
|
||||||
|
<td><b>" . htmlspecialchars($p['kk_nama']) . "</b>" .
|
||||||
|
($p['nik'] ? "<br><span style='font-size:8px;color:#888'>{$p['nik']}</span>" : '') .
|
||||||
|
"</td>
|
||||||
|
<td style='font-size:9px'>" . htmlspecialchars($p['alamat'] ?? '-') . "</td>
|
||||||
|
<td style='text-align:center'>{$jml}</td>
|
||||||
|
<td style='font-size:9px'>" . htmlspecialchars($p['ibadah_nama'] ?? '-') . "</td>
|
||||||
|
<td style='text-align:center'>{$jarak}</td>
|
||||||
|
<td style='text-align:center;color:{$scol};font-weight:bold'>{$slbl}</td>
|
||||||
|
<td style='font-size:9px'>" . htmlspecialchars($p['jenis_bantuan'] ?? '-') . "</td>
|
||||||
|
<td style='text-align:center'>{$tglB}</td>
|
||||||
|
</tr>";
|
||||||
|
}
|
||||||
|
if (!$rowsPenduduk) {
|
||||||
|
$rowsPenduduk = "<tr><td colspan='9' style='text-align:center;color:#999'>Belum ada data penduduk miskin.</td></tr>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HTML Template ─────────────────────────────────────────────────────────────
|
||||||
|
$html = <<<HTML
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; font-size: 10.5px; color: #1a1a1a; }
|
||||||
|
h1 { font-size: 15px; text-align: center; color: #1e3a5f; margin-bottom: 2px; }
|
||||||
|
.sub { text-align: center; font-size: 9.5px; color: #555; margin-bottom: 14px; }
|
||||||
|
.section { font-size: 11px; font-weight: bold; color: #1e3a5f;
|
||||||
|
border-bottom: 2px solid #1e3a5f; padding-bottom: 3px; margin: 12px 0 7px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 9.5px; }
|
||||||
|
th { background: #1e3a5f; color: white; padding: 5px 4px; text-align: center; }
|
||||||
|
td { padding: 4px 5px; border-bottom: 1px solid #e5e7eb; vertical-align: middle; }
|
||||||
|
.sg { width: 100%; border-collapse: collapse; margin-bottom: 6px; }
|
||||||
|
.sc { width: 25%; padding: 8px; text-align: center; border: 1px solid #d1d5db; border-radius: 4px; }
|
||||||
|
.sn { font-size: 20px; font-weight: bold; margin-bottom: 2px; }
|
||||||
|
.sl { font-size: 8.5px; color: #6b7280; }
|
||||||
|
.g { color: #1a7a3e; }
|
||||||
|
.r { color: #c0392b; }
|
||||||
|
.y { color: #856404; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h1>LAPORAN SEBARAN PENDUDUK MISKIN</h1>
|
||||||
|
<div class="sub">
|
||||||
|
Sistem WebGIS GeoMiskin — Universitas Tanjungpura |
|
||||||
|
Dicetak: {$tgl} |
|
||||||
|
Oleh: {$cetakOleh}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">Ringkasan Statistik</div>
|
||||||
|
<table class="sg">
|
||||||
|
<tr>
|
||||||
|
<td class="sc">
|
||||||
|
<div class="sn">{$total}</div>
|
||||||
|
<div class="sl">Total KK Miskin</div>
|
||||||
|
</td>
|
||||||
|
<td class="sc">
|
||||||
|
<div class="sn g">{$sudah}</div>
|
||||||
|
<div class="sl">Sudah Dibantu ({$pctSudah}%)</div>
|
||||||
|
</td>
|
||||||
|
<td class="sc">
|
||||||
|
<div class="sn r">{$belum}</div>
|
||||||
|
<div class="sl">Belum Dibantu</div>
|
||||||
|
</td>
|
||||||
|
<td class="sc">
|
||||||
|
<div class="sn y">{$menunggu}</div>
|
||||||
|
<div class="sl">Menunggu Verifikasi</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="section">Data Rumah Ibadah</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="5%">No</th>
|
||||||
|
<th width="35%">Nama Rumah Ibadah</th>
|
||||||
|
<th width="20%">Jenis</th>
|
||||||
|
<th width="15%">Radius</th>
|
||||||
|
<th width="25%">KK Binaan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{$rowsIbadah}</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="section">Daftar Penduduk Miskin</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="4%">No</th>
|
||||||
|
<th width="14%">Nama KK / NIK</th>
|
||||||
|
<th width="16%">Alamat</th>
|
||||||
|
<th width="5%">Jiwa</th>
|
||||||
|
<th width="14%">Rumah Ibadah</th>
|
||||||
|
<th width="8%">Jarak</th>
|
||||||
|
<th width="13%">Status</th>
|
||||||
|
<th width="14%">Jenis Bantuan</th>
|
||||||
|
<th width="12%">Tgl Bantuan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{$rowsPenduduk}</tbody>
|
||||||
|
</table>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
// ── Generate PDF ───────────────────────────────────────────────────────────────
|
||||||
|
try {
|
||||||
|
$mpdf = new \Mpdf\Mpdf([
|
||||||
|
'mode' => 'utf-8',
|
||||||
|
'format' => 'A4-L',
|
||||||
|
'margin_top' => 12,
|
||||||
|
'margin_bottom' => 12,
|
||||||
|
'margin_left' => 10,
|
||||||
|
'margin_right' => 10,
|
||||||
|
]);
|
||||||
|
$mpdf->SetTitle('Laporan Sebaran Penduduk Miskin');
|
||||||
|
$mpdf->SetAuthor('GeoMiskin WebGIS');
|
||||||
|
$mpdf->WriteHTML($html);
|
||||||
|
$fname = 'laporan_penduduk_miskin_' . date('Y-m-d') . '.pdf';
|
||||||
|
$mpdf->Output($fname, 'D');
|
||||||
|
} catch (\Mpdf\MpdfException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo '<pre style="font-family:monospace;padding:20px">
|
||||||
|
<b>Gagal membuat PDF:</b> ' . htmlspecialchars($e->getMessage()) . '
|
||||||
|
|
||||||
|
Pastikan extension GD aktif di XAMPP:
|
||||||
|
1. Buka C:\\xampp\\php\\php.ini
|
||||||
|
2. Cari ;extension=gd → hapus tanda titik koma
|
||||||
|
3. Restart Apache di XAMPP Control Panel
|
||||||
|
</pre>';
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
// Rumah Ibadah
|
||||||
|
$ibadah = mysqli_fetch_all(
|
||||||
|
mysqli_query($conn, "SELECT * FROM rumah_ibadah ORDER BY nama"),
|
||||||
|
MYSQLI_ASSOC
|
||||||
|
);
|
||||||
|
|
||||||
|
// Penduduk miskin with anggota count
|
||||||
|
$penduduk_raw = mysqli_fetch_all(
|
||||||
|
mysqli_query($conn,
|
||||||
|
"SELECT p.*, COUNT(a.id) as jumlah_anggota_db
|
||||||
|
FROM penduduk_miskin p
|
||||||
|
LEFT JOIN anggota_keluarga a ON a.penduduk_id = p.id
|
||||||
|
GROUP BY p.id
|
||||||
|
ORDER BY p.kk_nama"),
|
||||||
|
MYSQLI_ASSOC
|
||||||
|
);
|
||||||
|
|
||||||
|
// Attach anggota keluarga to each penduduk
|
||||||
|
$penduduk = [];
|
||||||
|
foreach ($penduduk_raw as $p) {
|
||||||
|
$pid = (int)$p['id'];
|
||||||
|
$anggota = mysqli_fetch_all(
|
||||||
|
mysqli_query($conn, "SELECT * FROM anggota_keluarga WHERE penduduk_id = $pid ORDER BY id"),
|
||||||
|
MYSQLI_ASSOC
|
||||||
|
);
|
||||||
|
// Use jumlah_anggota from table if set, else count from anggota table
|
||||||
|
if (empty($p['jumlah_anggota'])) {
|
||||||
|
$p['jumlah_anggota'] = count($anggota) ?: 1;
|
||||||
|
}
|
||||||
|
$p['anggota'] = $anggota;
|
||||||
|
$penduduk[] = $p;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['ibadah' => $ibadah, 'penduduk' => $penduduk]);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$nama = mysqli_real_escape_string($conn, trim($_POST['nama'] ?? ''));
|
||||||
|
$jenis = $_POST['jenis'] ?? 'Masjid';
|
||||||
|
$kontak = mysqli_real_escape_string($conn, trim($_POST['kontak'] ?? ''));
|
||||||
|
$alamat = mysqli_real_escape_string($conn, trim($_POST['alamat'] ?? ''));
|
||||||
|
$lat = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lng = (float)($_POST['lng'] ?? 0);
|
||||||
|
$radius = max(50, min(5000, (int)($_POST['radius'] ?? 300)));
|
||||||
|
|
||||||
|
$valid_jenis = ['Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya'];
|
||||||
|
if (!$nama || !$alamat || !$lat || !$lng || !in_array($jenis, $valid_jenis)) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap atau tidak valid.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jenis = mysqli_real_escape_string($conn, $jenis);
|
||||||
|
$q = "INSERT INTO rumah_ibadah (nama, jenis, kontak, alamat, lat, lng, radius)
|
||||||
|
VALUES ('$nama','$jenis','$kontak','$alamat',$lat,$lng,$radius)";
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $q)) {
|
||||||
|
echo json_encode(['status' => 'success', 'id' => mysqli_insert_id($conn)]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$kk_nama = esc($conn, $_POST['kk_nama'] ?? '');
|
||||||
|
$nik = esc($conn, $_POST['nik'] ?? '');
|
||||||
|
$alamat = esc($conn, $_POST['alamat'] ?? '');
|
||||||
|
$kecamatan = esc($conn, $_POST['kecamatan'] ?? '');
|
||||||
|
$kelurahan = esc($conn, $_POST['kelurahan'] ?? '');
|
||||||
|
$lat = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lng = (float)($_POST['lng'] ?? 0);
|
||||||
|
$jumlah = max(1, (int)($_POST['jumlah_anggota'] ?? 1));
|
||||||
|
$status_allowed = ['belum_dibantu','sudah_dibantu','menunggu_verifikasi'];
|
||||||
|
$status = in_array($_POST['status_bantuan'] ?? '', $status_allowed) ? $_POST['status_bantuan'] : 'belum_dibantu';
|
||||||
|
$jenis_bantuan = esc($conn, $_POST['jenis_bantuan'] ?? '');
|
||||||
|
$tanggal = $_POST['tanggal_bantuan'] ?? '';
|
||||||
|
$tanggal_sql = ($tanggal && preg_match('/^\d{4}-\d{2}-\d{2}$/', $tanggal)) ? "'$tanggal'" : 'NULL';
|
||||||
|
|
||||||
|
if (!$kk_nama || !$alamat || !$lat || !$lng) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Find nearest rumah ibadah ---
|
||||||
|
$ibadah_res = mysqli_query($conn, "SELECT id, lat, lng, radius FROM rumah_ibadah");
|
||||||
|
$nearest_id = null;
|
||||||
|
$nearest_dist = PHP_FLOAT_MAX;
|
||||||
|
|
||||||
|
while ($row = mysqli_fetch_assoc($ibadah_res)) {
|
||||||
|
$d = haversine($lat, $lng, (float)$row['lat'], (float)$row['lng']);
|
||||||
|
if ($d <= (float)$row['radius'] && $d < $nearest_dist) {
|
||||||
|
$nearest_dist = $d;
|
||||||
|
$nearest_id = $row['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no ibadah covers, find nearest anyway
|
||||||
|
if (!$nearest_id) {
|
||||||
|
mysqli_data_seek($ibadah_res, 0);
|
||||||
|
while ($row = mysqli_fetch_assoc($ibadah_res)) {
|
||||||
|
$d = haversine($lat, $lng, (float)$row['lat'], (float)$row['lng']);
|
||||||
|
if ($d < $nearest_dist) { $nearest_dist = $d; $nearest_id = $row['id']; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$jarak = $nearest_id ? round($nearest_dist, 2) : 'NULL';
|
||||||
|
$ibadah_id_sql = $nearest_id ? $nearest_id : 'NULL';
|
||||||
|
$jarak_sql = is_numeric($jarak) ? $jarak : 'NULL';
|
||||||
|
|
||||||
|
// --- Handle file upload ---
|
||||||
|
$bukti_file = '';
|
||||||
|
if (!empty($_FILES['bukti_file']['name']) && $_FILES['bukti_file']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['bukti_file']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg','jpeg','png','webp','pdf','doc','docx'];
|
||||||
|
if (in_array($ext, $allowed) && $_FILES['bukti_file']['size'] <= 5 * 1024 * 1024) {
|
||||||
|
$dir = __DIR__ . '/../uploads/bukti/';
|
||||||
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||||
|
$fname = 'bukti_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['bukti_file']['tmp_name'], $dir . $fname)) {
|
||||||
|
$bukti_file = $fname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$bukti_sql = $bukti_file ? "'".esc($conn,$bukti_file)."'" : 'NULL';
|
||||||
|
|
||||||
|
// --- Insert penduduk ---
|
||||||
|
$q = "INSERT INTO penduduk_miskin (kk_nama,nik,alamat,kecamatan,kelurahan,lat,lng,jumlah_anggota,rumah_ibadah_id,jarak_meter,status_bantuan,jenis_bantuan,tanggal_bantuan,bukti_file)
|
||||||
|
VALUES ('$kk_nama','$nik','$alamat','$kecamatan','$kelurahan',$lat,$lng,$jumlah,$ibadah_id_sql,$jarak_sql,'$status','$jenis_bantuan',$tanggal_sql,$bukti_sql)";
|
||||||
|
|
||||||
|
if (!mysqli_query($conn, $q)) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$pid = mysqli_insert_id($conn);
|
||||||
|
|
||||||
|
// --- Insert anggota keluarga ---
|
||||||
|
$anggota = json_decode($_POST['anggota'] ?? '[]', true) ?: [];
|
||||||
|
foreach ($anggota as $a) {
|
||||||
|
if (empty($a['nama'])) continue;
|
||||||
|
$an = esc($conn,$a['nama']); $ah = esc($conn,$a['hubungan'] ?? '');
|
||||||
|
$au = (int)($a['umur'] ?? 0); $ap = esc($conn,$a['pekerjaan'] ?? ''); $ak = esc($conn,$a['keterangan'] ?? '');
|
||||||
|
mysqli_query($conn, "INSERT INTO anggota_keluarga (penduduk_id,nama,hubungan,umur,pekerjaan,keterangan) VALUES ($pid,'$an','$ah',$au,'$ap','$ak')");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'success', 'id' => $pid, 'rumah_ibadah_id' => $nearest_id, 'jarak_meter' => $jarak]);
|
||||||
|
|
||||||
|
function haversine($lat1, $lon1, $lat2, $lon2) {
|
||||||
|
$R = 6371000;
|
||||||
|
$d1 = deg2rad($lat1 - $lat2); $d2 = deg2rad($lon1 - $lon2);
|
||||||
|
$a = sin($d1/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($d2/2)**2;
|
||||||
|
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) { echo json_encode(['status'=>'error','message'=>'ID tidak valid']); exit; }
|
||||||
|
|
||||||
|
$nama = mysqli_real_escape_string($conn, trim($_POST['nama'] ?? ''));
|
||||||
|
$jenis = $_POST['jenis'] ?? 'Masjid';
|
||||||
|
$kontak = mysqli_real_escape_string($conn, trim($_POST['kontak'] ?? ''));
|
||||||
|
$alamat = mysqli_real_escape_string($conn, trim($_POST['alamat'] ?? ''));
|
||||||
|
$lat = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lng = (float)($_POST['lng'] ?? 0);
|
||||||
|
$radius = max(50, min(5000, (int)($_POST['radius'] ?? 300)));
|
||||||
|
|
||||||
|
$valid_jenis = ['Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya'];
|
||||||
|
if (!$nama || !$alamat || !$lat || !$lng || !in_array($jenis, $valid_jenis)) {
|
||||||
|
echo json_encode(['status'=>'error','message'=>'Data tidak valid']); exit;
|
||||||
|
}
|
||||||
|
$jenis = mysqli_real_escape_string($conn, $jenis);
|
||||||
|
$q = "UPDATE rumah_ibadah SET nama='$nama',jenis='$jenis',kontak='$kontak',alamat='$alamat',lat=$lat,lng=$lng,radius=$radius WHERE id=$id";
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $q)) {
|
||||||
|
// Recalculate nearest ibadah for affected penduduk
|
||||||
|
echo json_encode(['status'=>'success']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status'=>'error','message'=>mysqli_error($conn)]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
function haversine($lat1,$lon1,$lat2,$lon2){$R=6371000;$d1=deg2rad($lat1-$lat2);$d2=deg2rad($lon1-$lon2);$a=sin($d1/2)**2+cos(deg2rad($lat1))*cos(deg2rad($lat2))*sin($d2/2)**2;return $R*2*atan2(sqrt($a),sqrt(1-$a));}
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) { echo json_encode(['status'=>'error','message'=>'ID tidak valid']); exit; }
|
||||||
|
|
||||||
|
$kk_nama = esc($conn,$_POST['kk_nama'] ?? '');
|
||||||
|
$nik = esc($conn,$_POST['nik'] ?? '');
|
||||||
|
$alamat = esc($conn,$_POST['alamat'] ?? '');
|
||||||
|
$kecamatan = esc($conn,$_POST['kecamatan'] ?? '');
|
||||||
|
$kelurahan = esc($conn,$_POST['kelurahan'] ?? '');
|
||||||
|
$lat = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lng = (float)($_POST['lng'] ?? 0);
|
||||||
|
$jumlah = max(1,(int)($_POST['jumlah_anggota'] ?? 1));
|
||||||
|
$valid_st = ['belum_dibantu','sudah_dibantu','menunggu_verifikasi'];
|
||||||
|
$status = in_array($_POST['status_bantuan']??'',$valid_st) ? $_POST['status_bantuan'] : 'belum_dibantu';
|
||||||
|
$jenis_bantuan = esc($conn,$_POST['jenis_bantuan'] ?? '');
|
||||||
|
$tanggal = $_POST['tanggal_bantuan'] ?? '';
|
||||||
|
$tanggal_sql = ($tanggal && preg_match('/^\d{4}-\d{2}-\d{2}$/',$tanggal)) ? "'$tanggal'" : 'NULL';
|
||||||
|
|
||||||
|
if (!$kk_nama||!$alamat||!$lat||!$lng){echo json_encode(['status'=>'error','message'=>'Data tidak lengkap']);exit;}
|
||||||
|
|
||||||
|
// Find nearest ibadah
|
||||||
|
$ibadah_res = mysqli_query($conn,"SELECT id,lat,lng,radius FROM rumah_ibadah");
|
||||||
|
$nid=null;$ndist=PHP_FLOAT_MAX;
|
||||||
|
while($row=mysqli_fetch_assoc($ibadah_res)){
|
||||||
|
$d=haversine($lat,$lng,(float)$row['lat'],(float)$row['lng']);
|
||||||
|
if($d<=(float)$row['radius']&&$d<$ndist){$ndist=$d;$nid=$row['id'];}
|
||||||
|
}
|
||||||
|
if(!$nid){mysqli_data_seek($ibadah_res,0);while($row=mysqli_fetch_assoc($ibadah_res)){$d=haversine($lat,$lng,(float)$row['lat'],(float)$row['lng']);if($d<$ndist){$ndist=$d;$nid=$row['id'];}}}
|
||||||
|
$jarak=$nid?round($ndist,2):'NULL';$ibadah_sql=$nid?$nid:'NULL';$jarak_sql=is_numeric($jarak)?$jarak:'NULL';
|
||||||
|
|
||||||
|
// Handle file upload
|
||||||
|
$bukti_sql='bukti_file=bukti_file'; // keep existing
|
||||||
|
if(!empty($_FILES['bukti_file']['name'])&&$_FILES['bukti_file']['error']===UPLOAD_ERR_OK){
|
||||||
|
$ext=strtolower(pathinfo($_FILES['bukti_file']['name'],PATHINFO_EXTENSION));
|
||||||
|
$allowed=['jpg','jpeg','png','webp','pdf','doc','docx'];
|
||||||
|
if(in_array($ext,$allowed)&&$_FILES['bukti_file']['size']<=5*1024*1024){
|
||||||
|
$dir=__DIR__.'/../uploads/bukti/';
|
||||||
|
if(!is_dir($dir))mkdir($dir,0755,true);
|
||||||
|
$fname='bukti_'.time().'_'.bin2hex(random_bytes(4)).'.'.$ext;
|
||||||
|
if(move_uploaded_file($_FILES['bukti_file']['tmp_name'],$dir.$fname)){
|
||||||
|
$bukti_sql="bukti_file='".esc($conn,$fname)."'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$q="UPDATE penduduk_miskin SET kk_nama='$kk_nama',nik='$nik',alamat='$alamat',kecamatan='$kecamatan',kelurahan='$kelurahan',lat=$lat,lng=$lng,jumlah_anggota=$jumlah,rumah_ibadah_id=$ibadah_sql,jarak_meter=$jarak_sql,status_bantuan='$status',jenis_bantuan='$jenis_bantuan',tanggal_bantuan=$tanggal_sql,$bukti_sql WHERE id=$id";
|
||||||
|
if(!mysqli_query($conn,$q)){echo json_encode(['status'=>'error','message'=>mysqli_error($conn)]);exit;}
|
||||||
|
|
||||||
|
// Update anggota
|
||||||
|
mysqli_query($conn,"DELETE FROM anggota_keluarga WHERE penduduk_id=$id");
|
||||||
|
$anggota=json_decode($_POST['anggota']??'[]',true)?:[];
|
||||||
|
foreach($anggota as $a){
|
||||||
|
if(empty($a['nama']))continue;
|
||||||
|
$an=esc($conn,$a['nama']);$ah=esc($conn,$a['hubungan']??'');$au=(int)($a['umur']??0);$ap=esc($conn,$a['pekerjaan']??'');$ak=esc($conn,$a['keterangan']??'');
|
||||||
|
mysqli_query($conn,"INSERT INTO anggota_keluarga (penduduk_id,nama,hubungan,umur,pekerjaan,keterangan) VALUES ($id,'$an','$ah',$au,'$ap','$ak')");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status'=>'success']);
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '../koneksi.php';
|
||||||
|
include '../auth.php';
|
||||||
|
requireLogin();
|
||||||
|
|
||||||
|
$action = $_POST['action'] ?? $_GET['action'] ?? 'list';
|
||||||
|
|
||||||
|
// ── LIST ────────────────────────────────────────────────────────
|
||||||
|
if ($action === 'list') {
|
||||||
|
$rows = mysqli_fetch_all(
|
||||||
|
mysqli_query($conn, "SELECT id, username, nama, created_at FROM users ORDER BY nama"),
|
||||||
|
MYSQLI_ASSOC
|
||||||
|
);
|
||||||
|
echo json_encode(['status' => 'success', 'data' => $rows]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ADD ─────────────────────────────────────────────────────────
|
||||||
|
if ($action === 'add') {
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$nama = trim($_POST['nama'] ?? '');
|
||||||
|
|
||||||
|
if (!$username || !$nama || strlen($password) < 6) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap (password minimal 6 karakter)']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$role = in_array($_POST['role'] ?? '', ['admin','superadmin']) ? $_POST['role'] : 'admin';
|
||||||
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
$st = mysqli_prepare($conn, "INSERT INTO users (username, password, nama, role) VALUES (?,?,?,?)");
|
||||||
|
mysqli_stmt_bind_param($st, 'ssss', $username, $hash, $nama, $role);
|
||||||
|
if (mysqli_stmt_execute($st)) {
|
||||||
|
echo json_encode(['status' => 'success', 'id' => mysqli_insert_id($conn)]);
|
||||||
|
} else {
|
||||||
|
$err = mysqli_error($conn);
|
||||||
|
$msg = strpos($err, 'Duplicate') !== false ? "Username '{$username}' sudah digunakan" : $err;
|
||||||
|
echo json_encode(['status' => 'error', 'message' => $msg]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UPDATE ──────────────────────────────────────────────────────
|
||||||
|
if ($action === 'update') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$nama = trim($_POST['nama'] ?? '');
|
||||||
|
$pass = $_POST['password'] ?? '';
|
||||||
|
$role = in_array($_POST['role'] ?? '', ['admin','superadmin']) ? $_POST['role'] : 'admin';
|
||||||
|
|
||||||
|
if ($id <= 0 || !$nama) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Data tidak valid']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($pass) >= 6) {
|
||||||
|
$hash = password_hash($pass, PASSWORD_DEFAULT);
|
||||||
|
$st = mysqli_prepare($conn, "UPDATE users SET nama=?, role=?, password=? WHERE id=?");
|
||||||
|
mysqli_stmt_bind_param($st, 'sssi', $nama, $role, $hash, $id);
|
||||||
|
} else {
|
||||||
|
$st = mysqli_prepare($conn, "UPDATE users SET nama=?, role=? WHERE id=?");
|
||||||
|
mysqli_stmt_bind_param($st, 'ssi', $nama, $role, $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mysqli_stmt_execute($st)) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DELETE ──────────────────────────────────────────────────────
|
||||||
|
if ($action === 'delete') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$my_id = (int)($_SESSION['user']['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($id === $my_id) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Tidak dapat menghapus akun sendiri']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$st = mysqli_prepare($conn, "DELETE FROM users WHERE id = ?");
|
||||||
|
mysqli_stmt_bind_param($st, 'i', $id);
|
||||||
|
if (mysqli_stmt_execute($st)) {
|
||||||
|
if (mysqli_stmt_affected_rows($st) > 0) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'User tidak ditemukan']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Action tidak dikenal']);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
|
||||||
|
function requireLogin() {
|
||||||
|
if (empty($_SESSION['user'])) {
|
||||||
|
if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Sesi habis, silakan login kembali.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
header('Location: /webgus_lama/uas/login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireSuperAdmin() {
|
||||||
|
requireLogin();
|
||||||
|
if ($_SESSION['user']['role'] !== 'superadmin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Akses ditolak. Hanya superadmin.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSuperAdmin() {
|
||||||
|
return ($_SESSION['user']['role'] ?? '') === 'superadmin';
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentUser() {
|
||||||
|
return $_SESSION['user'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc($conn, $v) {
|
||||||
|
return mysqli_real_escape_string($conn, trim((string)$v));
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"mpdf/mpdf": "^8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+478
@@ -0,0 +1,478 @@
|
|||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "6acbc9f2692b6bca49a9658332298068",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "mpdf/mpdf",
|
||||||
|
"version": "v8.3.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/mpdf.git",
|
||||||
|
"reference": "2a454ec334109911fdb323a284c19dbf3f049810"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/2a454ec334109911fdb323a284c19dbf3f049810",
|
||||||
|
"reference": "2a454ec334109911fdb323a284c19dbf3f049810",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"mpdf/psr-http-message-shim": "^1.0 || ^2.0",
|
||||||
|
"mpdf/psr-log-aware-trait": "^2.0 || ^3.0",
|
||||||
|
"myclabs/deep-copy": "^1.7",
|
||||||
|
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
|
||||||
|
"php": "^5.6 || ^7.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||||
|
"psr/http-message": "^1.0 || ^2.0",
|
||||||
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"setasign/fpdi": "^2.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.3.0",
|
||||||
|
"mpdf/qrcode": "^1.1.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5.0",
|
||||||
|
"tracy/tracy": "~2.5",
|
||||||
|
"yoast/phpunit-polyfills": "^1.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "Needed for generation of some types of barcodes",
|
||||||
|
"ext-imagick": "Needed if developing the Mpdf library",
|
||||||
|
"ext-xml": "Needed mainly for SVG manipulation",
|
||||||
|
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-2.0-only"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matěj Humpál",
|
||||||
|
"role": "Developer, maintainer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ian Back",
|
||||||
|
"role": "Developer (retired)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
|
||||||
|
"homepage": "https://mpdf.github.io",
|
||||||
|
"keywords": [
|
||||||
|
"pdf",
|
||||||
|
"php",
|
||||||
|
"utf-8"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"docs": "https://mpdf.github.io",
|
||||||
|
"issues": "https://github.com/mpdf/mpdf/issues",
|
||||||
|
"source": "https://github.com/mpdf/mpdf"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.paypal.me/mpdf",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-11T10:58:44+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/psr-http-message-shim",
|
||||||
|
"version": "v2.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/psr-http-message-shim.git",
|
||||||
|
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/psr-http-message-shim/zipball/f25a0153d645e234f9db42e5433b16d9b113920f",
|
||||||
|
"reference": "f25a0153d645e234f9db42e5433b16d9b113920f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\PsrHttpMessageShim\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Dorison",
|
||||||
|
"email": "mark@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kristofer Widholm",
|
||||||
|
"email": "kristofer@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nigel Cunningham",
|
||||||
|
"email": "nigel.cunningham@technocrat.com.au"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Shim to allow support of different psr/message versions.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mpdf/psr-http-message-shim/issues",
|
||||||
|
"source": "https://github.com/mpdf/psr-http-message-shim/tree/v2.0.1"
|
||||||
|
},
|
||||||
|
"time": "2023-10-02T14:34:03+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/psr-log-aware-trait",
|
||||||
|
"version": "v3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/psr-log-aware-trait.git",
|
||||||
|
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/psr-log-aware-trait/zipball/a633da6065e946cc491e1c962850344bb0bf3e78",
|
||||||
|
"reference": "a633da6065e946cc491e1c962850344bb0bf3e78",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"psr/log": "^3.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\PsrLogAwareTrait\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Dorison",
|
||||||
|
"email": "mark@chromatichq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kristofer Widholm",
|
||||||
|
"email": "kristofer@chromatichq.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Trait to allow support of different psr/log versions.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/mpdf/psr-log-aware-trait/issues",
|
||||||
|
"source": "https://github.com/mpdf/psr-log-aware-trait/tree/v3.0.0"
|
||||||
|
},
|
||||||
|
"time": "2023-05-03T06:19:36+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "myclabs/deep-copy",
|
||||||
|
"version": "1.13.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||||
|
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||||
|
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/collections": "<1.6.8",
|
||||||
|
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/collections": "^1.6.8",
|
||||||
|
"doctrine/common": "^2.13.3 || ^3.2.2",
|
||||||
|
"phpspec/prophecy": "^1.10",
|
||||||
|
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/DeepCopy/deep_copy.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"DeepCopy\\": "src/DeepCopy/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Create deep copies (clones) of your objects",
|
||||||
|
"keywords": [
|
||||||
|
"clone",
|
||||||
|
"copy",
|
||||||
|
"duplicate",
|
||||||
|
"object",
|
||||||
|
"object graph"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||||
|
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-08-01T08:46:24+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paragonie/random_compat",
|
||||||
|
"version": "v9.99.100",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/paragonie/random_compat.git",
|
||||||
|
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||||
|
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">= 7"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "4.*|5.*",
|
||||||
|
"vimeo/psalm": "^1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Paragon Initiative Enterprises",
|
||||||
|
"email": "security@paragonie.com",
|
||||||
|
"homepage": "https://paragonie.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
|
||||||
|
"keywords": [
|
||||||
|
"csprng",
|
||||||
|
"polyfill",
|
||||||
|
"pseudorandom",
|
||||||
|
"random"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"email": "info@paragonie.com",
|
||||||
|
"issues": "https://github.com/paragonie/random_compat/issues",
|
||||||
|
"source": "https://github.com/paragonie/random_compat"
|
||||||
|
},
|
||||||
|
"time": "2020-10-15T08:29:30+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-message",
|
||||||
|
"version": "2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-message.git",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Message\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP messages",
|
||||||
|
"homepage": "https://github.com/php-fig/http-message",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-message",
|
||||||
|
"psr",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||||
|
},
|
||||||
|
"time": "2023-04-04T09:54:51+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/log",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/log.git",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Log\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for logging libraries",
|
||||||
|
"homepage": "https://github.com/php-fig/log",
|
||||||
|
"keywords": [
|
||||||
|
"log",
|
||||||
|
"psr",
|
||||||
|
"psr-3"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||||
|
},
|
||||||
|
"time": "2024-09-11T13:17:53+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "setasign/fpdi",
|
||||||
|
"version": "v2.6.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Setasign/FPDI.git",
|
||||||
|
"reference": "388c51e69982a3fc16698710b763e8107a49f510"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/388c51e69982a3fc16698710b763e8107a49f510",
|
||||||
|
"reference": "388c51e69982a3fc16698710b763e8107a49f510",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php": ">=7.2 <=8.5.99999"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"setasign/tfpdf": "<1.31"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^8.5.52",
|
||||||
|
"setasign/fpdf": "~1.8.6",
|
||||||
|
"setasign/tfpdf": "~1.33",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5",
|
||||||
|
"tecnickcom/tcpdf": "^6.8"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"setasign\\Fpdi\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jan Slabon",
|
||||||
|
"email": "jan.slabon@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Maximilian Kresse",
|
||||||
|
"email": "maximilian.kresse@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||||
|
"homepage": "https://www.setasign.com/fpdi",
|
||||||
|
"keywords": [
|
||||||
|
"fpdf",
|
||||||
|
"fpdi",
|
||||||
|
"pdf"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||||
|
"source": "https://github.com/Setasign/FPDI/tree/v2.6.7"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-13T10:16:22+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": {},
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": {},
|
||||||
|
"platform-dev": {},
|
||||||
|
"plugin-api-version": "2.9.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,723 @@
|
|||||||
|
<?php
|
||||||
|
include 'auth.php';
|
||||||
|
requireLogin();
|
||||||
|
$user = currentUser();
|
||||||
|
$isAdmin = isSuperAdmin();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard — GeoMiskin</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f172a;--bg2:#1e293b;--bg3:#334155;--bg4:#475569;--txt:#e2e8f0;--muted:#64748b;--blue:#3b82f6;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--indigo:#6366f1;--cyan:#06b6d4;--pink:#ec4899;--radius:14px;--trans:.2s ease}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html,body{height:100%;font-family:'Inter',sans-serif;background:var(--bg);color:var(--txt);overflow-x:hidden}
|
||||||
|
body{display:flex;flex-direction:column}
|
||||||
|
|
||||||
|
/* TOPBAR */
|
||||||
|
.topbar{background:var(--bg2);border-bottom:1px solid rgba(255,255,255,.06);padding:0 28px;height:60px;display:flex;align-items:center;gap:16px;flex-shrink:0;position:sticky;top:0;z-index:100}
|
||||||
|
.topbar-brand{display:flex;align-items:center;gap:10px;margin-right:auto}
|
||||||
|
.topbar-logo{width:34px;height:34px;background:linear-gradient(135deg,var(--blue),var(--indigo));border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
|
.topbar-logo i{color:#fff;font-size:15px}
|
||||||
|
.topbar-title{font-size:15px;font-weight:700}
|
||||||
|
.topbar-sub{font-size:11px;color:var(--muted);font-weight:400}
|
||||||
|
.topbar-user{display:flex;align-items:center;gap:8px;font-size:13px}
|
||||||
|
.topbar-avatar{width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,var(--blue),var(--indigo));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:12px;flex-shrink:0}
|
||||||
|
.topbar-name{font-weight:600;color:var(--txt)}
|
||||||
|
.topbar-role{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
|
||||||
|
.topbar-btn{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:9px;font-size:12px;font-weight:600;cursor:pointer;text-decoration:none;transition:var(--trans);border:none;font-family:inherit}
|
||||||
|
.btn-ghost{background:rgba(255,255,255,.06);color:var(--txt);border:1px solid rgba(255,255,255,.08)}
|
||||||
|
.btn-ghost:hover{background:rgba(255,255,255,.1)}
|
||||||
|
.btn-blue{background:var(--blue);color:#fff}
|
||||||
|
.btn-blue:hover{background:#2563eb}
|
||||||
|
.btn-red{background:rgba(239,68,68,.15);color:var(--red);border:1px solid rgba(239,68,68,.2)}
|
||||||
|
.btn-red:hover{background:rgba(239,68,68,.25)}
|
||||||
|
.divider-v{width:1px;height:24px;background:rgba(255,255,255,.08)}
|
||||||
|
|
||||||
|
/* CONTENT */
|
||||||
|
.content{flex:1;padding:28px;overflow-y:auto}
|
||||||
|
.page-header{margin-bottom:24px}
|
||||||
|
.page-header h1{font-size:22px;font-weight:700;display:flex;align-items:center;gap:10px}
|
||||||
|
.page-header h1 i{color:var(--blue)}
|
||||||
|
.page-header p{font-size:13px;color:var(--muted);margin-top:4px}
|
||||||
|
|
||||||
|
/* SUMMARY CARDS */
|
||||||
|
.summary-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:14px;margin-bottom:24px}
|
||||||
|
@media(max-width:1200px){.summary-grid{grid-template-columns:repeat(3,1fr)}}
|
||||||
|
@media(max-width:768px){.summary-grid{grid-template-columns:repeat(2,1fr)}}
|
||||||
|
.s-card{background:var(--bg2);border:1px solid rgba(255,255,255,.06);border-radius:var(--radius);padding:18px;display:flex;flex-direction:column;gap:10px;position:relative;overflow:hidden;transition:var(--trans)}
|
||||||
|
.s-card:hover{border-color:rgba(255,255,255,.12);transform:translateY(-2px)}
|
||||||
|
.s-card::before{content:'';position:absolute;top:-20px;right:-20px;width:80px;height:80px;border-radius:50%;opacity:.06}
|
||||||
|
.s-card.blue::before{background:var(--blue)}
|
||||||
|
.s-card.green::before{background:var(--green)}
|
||||||
|
.s-card.red::before{background:var(--red)}
|
||||||
|
.s-card.yellow::before{background:var(--yellow)}
|
||||||
|
.s-card.indigo::before{background:var(--indigo)}
|
||||||
|
.s-card.cyan::before{background:var(--cyan)}
|
||||||
|
.s-icon{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center}
|
||||||
|
.s-icon.blue{background:rgba(59,130,246,.2)} .s-icon.blue i{color:var(--blue)}
|
||||||
|
.s-icon.green{background:rgba(34,197,94,.2)} .s-icon.green i{color:var(--green)}
|
||||||
|
.s-icon.red{background:rgba(239,68,68,.2)} .s-icon.red i{color:var(--red)}
|
||||||
|
.s-icon.yellow{background:rgba(245,158,11,.2)} .s-icon.yellow i{color:var(--yellow)}
|
||||||
|
.s-icon.indigo{background:rgba(99,102,241,.2)} .s-icon.indigo i{color:var(--indigo)}
|
||||||
|
.s-icon.cyan{background:rgba(6,182,212,.2)} .s-icon.cyan i{color:var(--cyan)}
|
||||||
|
.s-icon i{font-size:15px}
|
||||||
|
.s-num{font-size:28px;font-weight:700;line-height:1;letter-spacing:-1px}
|
||||||
|
.s-lbl{font-size:11px;color:var(--muted);font-weight:500}
|
||||||
|
.s-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:2px 8px;border-radius:20px;margin-top:4px}
|
||||||
|
.s-badge.up{background:rgba(34,197,94,.15);color:var(--green)}
|
||||||
|
.s-badge.down{background:rgba(239,68,68,.15);color:var(--red)}
|
||||||
|
.s-badge.warn{background:rgba(245,158,11,.15);color:var(--yellow)}
|
||||||
|
|
||||||
|
/* PROGRESS */
|
||||||
|
.progress-section{background:var(--bg2);border:1px solid rgba(255,255,255,.06);border-radius:var(--radius);padding:20px;margin-bottom:24px}
|
||||||
|
.progress-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
|
||||||
|
.progress-header h3{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
|
||||||
|
.progress-header h3 i{color:var(--green)}
|
||||||
|
.progress-pct{font-size:24px;font-weight:700;color:var(--green)}
|
||||||
|
.progress-bar-wrap{background:var(--bg3);border-radius:8px;height:10px;overflow:hidden;margin-bottom:12px}
|
||||||
|
.progress-bar-fill{height:100%;background:linear-gradient(90deg,var(--blue),var(--green));border-radius:8px;transition:width .8s cubic-bezier(.4,0,.2,1)}
|
||||||
|
.progress-breakdown{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
|
||||||
|
.pb-item{text-align:center;padding:10px;background:var(--bg3);border-radius:10px}
|
||||||
|
.pb-num{font-size:20px;font-weight:700}
|
||||||
|
.pb-lbl{font-size:10px;color:var(--muted);margin-top:2px}
|
||||||
|
.pb-item.belum .pb-num{color:var(--red)}
|
||||||
|
.pb-item.sudah .pb-num{color:var(--green)}
|
||||||
|
.pb-item.tunggu .pb-num{color:var(--yellow)}
|
||||||
|
|
||||||
|
/* CHARTS GRID */
|
||||||
|
.charts-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:24px}
|
||||||
|
@media(max-width:1100px){.charts-grid{grid-template-columns:1fr 1fr}}
|
||||||
|
@media(max-width:700px){.charts-grid{grid-template-columns:1fr}}
|
||||||
|
.chart-card{background:var(--bg2);border:1px solid rgba(255,255,255,.06);border-radius:var(--radius);padding:20px}
|
||||||
|
.chart-card.wide{grid-column:span 2}
|
||||||
|
@media(max-width:1100px){.chart-card.wide{grid-column:span 1}}
|
||||||
|
.chart-title{font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px;margin-bottom:16px}
|
||||||
|
.chart-title i{font-size:14px;color:var(--blue)}
|
||||||
|
.chart-wrap{position:relative}
|
||||||
|
.chart-wrap canvas{max-width:100%}
|
||||||
|
|
||||||
|
/* TABLE */
|
||||||
|
.table-section{background:var(--bg2);border:1px solid rgba(255,255,255,.06);border-radius:var(--radius);overflow:hidden;margin-bottom:24px}
|
||||||
|
.table-header{padding:18px 20px;border-bottom:1px solid rgba(255,255,255,.06);display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||||||
|
.table-header h3{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
|
||||||
|
.table-header h3 i{color:var(--blue)}
|
||||||
|
.table-search{position:relative}
|
||||||
|
.table-search i{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:12px}
|
||||||
|
.table-search input{background:var(--bg);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:7px 10px 7px 30px;font-size:12px;font-family:inherit;color:var(--txt);outline:none;width:180px;transition:var(--trans)}
|
||||||
|
.table-search input:focus{border-color:var(--blue)}
|
||||||
|
.table-search input::placeholder{color:var(--muted)}
|
||||||
|
table{width:100%;border-collapse:collapse}
|
||||||
|
thead tr{background:rgba(255,255,255,.03)}
|
||||||
|
th{padding:10px 14px;text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);border-bottom:1px solid rgba(255,255,255,.06);white-space:nowrap}
|
||||||
|
td{padding:12px 14px;font-size:13px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle}
|
||||||
|
tbody tr:last-child td{border-bottom:none}
|
||||||
|
tbody tr:hover td{background:rgba(255,255,255,.02)}
|
||||||
|
.status-pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:20px;font-size:10px;font-weight:700}
|
||||||
|
.sp-belum{background:rgba(239,68,68,.15);color:var(--red)}
|
||||||
|
.sp-sudah{background:rgba(34,197,94,.15);color:var(--green)}
|
||||||
|
.sp-tunggu{background:rgba(245,158,11,.15);color:var(--yellow)}
|
||||||
|
.table-empty{padding:60px 20px;text-align:center;color:var(--muted)}
|
||||||
|
.table-empty i{font-size:32px;display:block;margin-bottom:8px;opacity:.4}
|
||||||
|
.pagination{padding:12px 20px;border-top:1px solid rgba(255,255,255,.06);display:flex;align-items:center;justify-content:space-between;font-size:12px;color:var(--muted)}
|
||||||
|
.pg-info{font-size:12px}
|
||||||
|
.pg-btns{display:flex;gap:6px}
|
||||||
|
.pg-btn{background:var(--bg3);border:1px solid rgba(255,255,255,.06);color:var(--txt);border-radius:6px;padding:5px 10px;font-size:11px;cursor:pointer;font-family:inherit;transition:var(--trans)}
|
||||||
|
.pg-btn:hover{background:var(--bg4)}
|
||||||
|
.pg-btn.active{background:var(--blue);border-color:var(--blue);color:#fff}
|
||||||
|
.pg-btn:disabled{opacity:.35;cursor:not-allowed}
|
||||||
|
|
||||||
|
/* IBADAH TABLE */
|
||||||
|
.ibadah-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px}
|
||||||
|
@media(max-width:900px){.ibadah-grid{grid-template-columns:1fr}}
|
||||||
|
|
||||||
|
/* LOADING */
|
||||||
|
.loading-overlay{position:fixed;inset:0;background:var(--bg);display:flex;align-items:center;justify-content:center;z-index:9999;transition:opacity .4s}
|
||||||
|
.loading-overlay.hide{opacity:0;pointer-events:none}
|
||||||
|
.spinner{width:40px;height:40px;border:3px solid var(--bg3);border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
|
||||||
|
/* TOAST */
|
||||||
|
#toast-wrap{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px}
|
||||||
|
.toast{background:var(--bg2);border:1px solid rgba(255,255,255,.08);color:var(--txt);padding:12px 16px;border-radius:12px;font-size:13px;font-weight:500;display:flex;align-items:center;gap:8px;animation:slideUp .25s ease;border-left:4px solid var(--blue);min-width:200px}
|
||||||
|
.toast.success{border-left-color:var(--green)}.toast.error{border-left-color:var(--red)}
|
||||||
|
@keyframes slideUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||||||
|
|
||||||
|
/* MISC */
|
||||||
|
.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||||
|
.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="loading-overlay" id="loader"><div class="spinner"></div></div>
|
||||||
|
|
||||||
|
<!-- ===== TOPBAR ===== -->
|
||||||
|
<div class="topbar">
|
||||||
|
<div class="topbar-brand">
|
||||||
|
<div class="topbar-logo"><i class="fas fa-chart-bar"></i></div>
|
||||||
|
<div>
|
||||||
|
<div class="topbar-title">GeoMiskin Dashboard</div>
|
||||||
|
<div class="topbar-sub">Ringkasan data sebaran penduduk miskin</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="index.php" class="topbar-btn btn-ghost"><i class="fas fa-map-marked-alt"></i> Buka Peta</a>
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<a href="users.php" class="topbar-btn btn-ghost"><i class="fas fa-users-cog"></i> Pengguna</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="divider-v"></div>
|
||||||
|
<div class="topbar-user">
|
||||||
|
<div class="topbar-avatar"><?= strtoupper(substr($user['nama'],0,1)) ?></div>
|
||||||
|
<div><div class="topbar-name"><?= htmlspecialchars($user['nama']) ?></div><div class="topbar-role"><?= $user['role'] ?></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="divider-v"></div>
|
||||||
|
<a href="logout.php" class="topbar-btn btn-red"><i class="fas fa-sign-out-alt"></i> Keluar</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== CONTENT ===== -->
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1><i class="fas fa-chart-pie"></i> Dashboard Analitik</h1>
|
||||||
|
<p id="last-updated">Memuat data...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SUMMARY CARDS -->
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="s-card blue">
|
||||||
|
<div class="s-icon blue"><i class="fas fa-users"></i></div>
|
||||||
|
<div class="s-num" id="s-total">—</div>
|
||||||
|
<div class="s-lbl">Total KK Miskin</div>
|
||||||
|
</div>
|
||||||
|
<div class="s-card green">
|
||||||
|
<div class="s-icon green"><i class="fas fa-check-circle"></i></div>
|
||||||
|
<div class="s-num" id="s-sudah">—</div>
|
||||||
|
<div class="s-lbl">Sudah Dibantu</div>
|
||||||
|
</div>
|
||||||
|
<div class="s-card red">
|
||||||
|
<div class="s-icon red"><i class="fas fa-times-circle"></i></div>
|
||||||
|
<div class="s-num" id="s-belum">—</div>
|
||||||
|
<div class="s-lbl">Belum Dibantu</div>
|
||||||
|
</div>
|
||||||
|
<div class="s-card yellow">
|
||||||
|
<div class="s-icon yellow"><i class="fas fa-hourglass-half"></i></div>
|
||||||
|
<div class="s-num" id="s-tunggu">—</div>
|
||||||
|
<div class="s-lbl">Menunggu Verifikasi</div>
|
||||||
|
</div>
|
||||||
|
<div class="s-card indigo">
|
||||||
|
<div class="s-icon indigo"><i class="fas fa-place-of-worship"></i></div>
|
||||||
|
<div class="s-num" id="s-ibadah">—</div>
|
||||||
|
<div class="s-lbl">Rumah Ibadah</div>
|
||||||
|
</div>
|
||||||
|
<div class="s-card cyan">
|
||||||
|
<div class="s-icon cyan"><i class="fas fa-person"></i></div>
|
||||||
|
<div class="s-num" id="s-jiwa">—</div>
|
||||||
|
<div class="s-lbl">Total Jiwa</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PROGRESS -->
|
||||||
|
<div class="progress-section">
|
||||||
|
<div class="progress-header">
|
||||||
|
<h3><i class="fas fa-tasks"></i> Progress Bantuan</h3>
|
||||||
|
<div class="progress-pct" id="prog-pct">0%</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-wrap"><div class="progress-bar-fill" id="prog-bar" style="width:0"></div></div>
|
||||||
|
<div class="progress-breakdown">
|
||||||
|
<div class="pb-item belum"><div class="pb-num" id="pb-belum">0</div><div class="pb-lbl">Belum Dibantu</div></div>
|
||||||
|
<div class="pb-item sudah"><div class="pb-num" id="pb-sudah">0</div><div class="pb-lbl">Sudah Dibantu</div></div>
|
||||||
|
<div class="pb-item tunggu"><div class="pb-num" id="pb-tunggu">0</div><div class="pb-lbl">Menunggu Verifikasi</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CHARTS -->
|
||||||
|
<div class="charts-grid">
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-title"><i class="fas fa-pie-chart"></i> Distribusi Status Bantuan</div>
|
||||||
|
<div class="chart-wrap" style="height:220px"><canvas id="chart-status"></canvas></div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card wide">
|
||||||
|
<div class="chart-title"><i class="fas fa-bar-chart"></i> KK Binaan per Rumah Ibadah</div>
|
||||||
|
<div class="chart-wrap" style="height:220px"><canvas id="chart-ibadah"></canvas></div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-title"><i class="fas fa-building"></i> Distribusi Jenis Rumah Ibadah</div>
|
||||||
|
<div class="chart-wrap" style="height:220px"><canvas id="chart-jenis"></canvas></div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card wide">
|
||||||
|
<div class="chart-title"><i class="fas fa-people-group"></i> Jumlah Anggota per KK</div>
|
||||||
|
<div class="chart-wrap" style="height:220px"><canvas id="chart-anggota"></canvas></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KECAMATAN CHART -->
|
||||||
|
<div class="chart-card" id="kecamatan-chart-section" style="margin-bottom:24px;display:none">
|
||||||
|
<div class="chart-title"><i class="fas fa-map" style="color:var(--indigo)"></i> Sebaran KK Miskin per Kecamatan</div>
|
||||||
|
<div class="chart-wrap" style="height:240px"><canvas id="chart-kecamatan"></canvas></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABLES ROW -->
|
||||||
|
<div class="ibadah-grid">
|
||||||
|
<!-- Rumah Ibadah Ranking -->
|
||||||
|
<div class="table-section">
|
||||||
|
<div class="table-header">
|
||||||
|
<h3><i class="fas fa-trophy"></i> Rumah Ibadah & KK Binaan</h3>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>Nama</th><th>Jenis</th><th style="text-align:right">KK Binaan</th></tr></thead>
|
||||||
|
<tbody id="tbl-ibadah-body"><tr><td colspan="4" class="table-empty"><i class="fas fa-spinner fa-spin"></i></td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bantuan Stats per Jenis -->
|
||||||
|
<div class="table-section">
|
||||||
|
<div class="table-header">
|
||||||
|
<h3><i class="fas fa-hand-holding-heart"></i> Rekap Bantuan per Jenis Ibadah</h3>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Jenis</th><th style="text-align:right">Total KK</th><th style="text-align:right">Dibantu</th><th style="text-align:right">%</th></tr></thead>
|
||||||
|
<tbody id="tbl-rekap-body"><tr><td colspan="4" class="table-empty"><i class="fas fa-spinner fa-spin"></i></td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PENDUDUK TABLE -->
|
||||||
|
<div class="table-section">
|
||||||
|
<div class="table-header">
|
||||||
|
<h3><i class="fas fa-list"></i> Daftar Penduduk Miskin</h3>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
|
<select id="tbl-filter-kec" onchange="filterTable()" style="background:var(--bg);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:7px 10px;font-size:12px;font-family:inherit;color:var(--txt);outline:none;cursor:pointer">
|
||||||
|
<option value="">Semua Kecamatan</option>
|
||||||
|
</select>
|
||||||
|
<div class="table-search"><i class="fas fa-search"></i><input type="text" id="tbl-search" placeholder="Cari nama / alamat..." oninput="filterTable()"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Nama KK</th>
|
||||||
|
<th>NIK</th>
|
||||||
|
<th>Kecamatan</th>
|
||||||
|
<th>Alamat</th>
|
||||||
|
<th style="text-align:center">Anggota</th>
|
||||||
|
<th>Rumah Ibadah</th>
|
||||||
|
<th style="text-align:right">Jarak</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tbl-penduduk-body">
|
||||||
|
<tr><td colspan="9" class="table-empty"><i class="fas fa-spinner fa-spin"></i><br>Memuat data...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="pagination">
|
||||||
|
<div class="pg-info" id="pg-info">—</div>
|
||||||
|
<div class="pg-btns" id="pg-btns"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /content -->
|
||||||
|
|
||||||
|
<div id="toast-wrap"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── state
|
||||||
|
let allIbadah = [], allPenduduk = [], filteredPenduduk = [];
|
||||||
|
let page = 1;
|
||||||
|
const PER_PAGE = 15;
|
||||||
|
let chartStatus, chartIbadah, chartJenis, chartAnggota, chartKecamatan;
|
||||||
|
|
||||||
|
const JENIS_COLOR = {
|
||||||
|
'Masjid':'#10b981','Musholla':'#34d399','Gereja Protestan':'#3b82f6',
|
||||||
|
'Gereja Katolik':'#6366f1','Pura':'#f59e0b','Vihara':'#ef4444',
|
||||||
|
'Klenteng':'#ec4899','Lainnya':'#94a3b8'
|
||||||
|
};
|
||||||
|
|
||||||
|
Chart.defaults.color = '#64748b';
|
||||||
|
Chart.defaults.font.family = 'Inter, sans-serif';
|
||||||
|
Chart.defaults.font.size = 11;
|
||||||
|
|
||||||
|
// ── load
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('api/get_data.php');
|
||||||
|
const text = await r.text();
|
||||||
|
let d;
|
||||||
|
try { d = JSON.parse(text); }
|
||||||
|
catch(e) {
|
||||||
|
toast('Server error: ' + text.substring(0, 120), 'error');
|
||||||
|
document.getElementById('loader').classList.add('hide');
|
||||||
|
document.getElementById('last-updated').textContent = 'Gagal memuat data';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.status === 'error') {
|
||||||
|
toast('DB Error: ' + d.message, 'error');
|
||||||
|
document.getElementById('loader').classList.add('hide');
|
||||||
|
document.getElementById('last-updated').textContent = 'Koneksi database gagal';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
allIbadah = d.ibadah || [];
|
||||||
|
allPenduduk = d.penduduk || [];
|
||||||
|
filteredPenduduk = [...allPenduduk];
|
||||||
|
updateAll();
|
||||||
|
document.getElementById('last-updated').textContent =
|
||||||
|
'Data diperbarui: ' + new Date().toLocaleString('id-ID');
|
||||||
|
document.getElementById('loader').classList.add('hide');
|
||||||
|
} catch(e) {
|
||||||
|
toast('Gagal memuat data: ' + e.message, 'error');
|
||||||
|
document.getElementById('loader').classList.add('hide');
|
||||||
|
document.getElementById('last-updated').textContent = 'Gagal memuat data';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAll() {
|
||||||
|
updateCards();
|
||||||
|
updateProgress();
|
||||||
|
updateCharts();
|
||||||
|
updateIbadahTable();
|
||||||
|
updateRekapTable();
|
||||||
|
// Populate kecamatan dropdown
|
||||||
|
const kecList = [...new Set(allPenduduk.map(p=>p.kecamatan).filter(Boolean))].sort();
|
||||||
|
const selK = document.getElementById('tbl-filter-kec');
|
||||||
|
selK.innerHTML = '<option value="">Semua Kecamatan</option>' +
|
||||||
|
kecList.map(k=>`<option value="${k}">${k}</option>`).join('');
|
||||||
|
filteredPenduduk = [...allPenduduk];
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── summary cards
|
||||||
|
function updateCards() {
|
||||||
|
const total = allPenduduk.length;
|
||||||
|
const sudah = allPenduduk.filter(p => p.status_bantuan === 'sudah_dibantu').length;
|
||||||
|
const belum = allPenduduk.filter(p => p.status_bantuan === 'belum_dibantu').length;
|
||||||
|
const tunggu = allPenduduk.filter(p => p.status_bantuan === 'menunggu_verifikasi').length;
|
||||||
|
const jiwa = allPenduduk.reduce((s, p) => s + (parseInt(p.jumlah_anggota) || 1), 0);
|
||||||
|
animNum('s-total', total);
|
||||||
|
animNum('s-sudah', sudah);
|
||||||
|
animNum('s-belum', belum);
|
||||||
|
animNum('s-tunggu', tunggu);
|
||||||
|
animNum('s-ibadah', allIbadah.length);
|
||||||
|
animNum('s-jiwa', jiwa);
|
||||||
|
}
|
||||||
|
|
||||||
|
function animNum(id, target) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
const start = parseInt(el.textContent) || 0;
|
||||||
|
const dur = 600, step = 16;
|
||||||
|
let cur = start, t = 0;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
t += step;
|
||||||
|
cur = Math.round(start + (target - start) * Math.min(t / dur, 1));
|
||||||
|
el.textContent = cur.toLocaleString('id-ID');
|
||||||
|
if (t >= dur) { el.textContent = target.toLocaleString('id-ID'); clearInterval(timer); }
|
||||||
|
}, step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── progress
|
||||||
|
function updateProgress() {
|
||||||
|
const total = allPenduduk.length;
|
||||||
|
const sudah = allPenduduk.filter(p => p.status_bantuan === 'sudah_dibantu').length;
|
||||||
|
const belum = allPenduduk.filter(p => p.status_bantuan === 'belum_dibantu').length;
|
||||||
|
const tunggu = allPenduduk.filter(p => p.status_bantuan === 'menunggu_verifikasi').length;
|
||||||
|
const pct = total > 0 ? Math.round(sudah / total * 100) : 0;
|
||||||
|
document.getElementById('prog-pct').textContent = pct + '%';
|
||||||
|
document.getElementById('prog-bar').style.width = pct + '%';
|
||||||
|
document.getElementById('pb-sudah').textContent = sudah;
|
||||||
|
document.getElementById('pb-belum').textContent = belum;
|
||||||
|
document.getElementById('pb-tunggu').textContent = tunggu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── charts
|
||||||
|
function updateCharts() {
|
||||||
|
const total = allPenduduk.length;
|
||||||
|
const sudah = allPenduduk.filter(p => p.status_bantuan === 'sudah_dibantu').length;
|
||||||
|
const belum = allPenduduk.filter(p => p.status_bantuan === 'belum_dibantu').length;
|
||||||
|
const tunggu = allPenduduk.filter(p => p.status_bantuan === 'menunggu_verifikasi').length;
|
||||||
|
|
||||||
|
// Donut: status
|
||||||
|
if (chartStatus) chartStatus.destroy();
|
||||||
|
chartStatus = new Chart(document.getElementById('chart-status'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Belum Dibantu','Sudah Dibantu','Menunggu Verifikasi'],
|
||||||
|
datasets: [{
|
||||||
|
data: [belum, sudah, tunggu],
|
||||||
|
backgroundColor: ['#ef4444','#22c55e','#f59e0b'],
|
||||||
|
borderWidth: 0, hoverOffset: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
cutout: '62%', responsive: true, maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'bottom', labels: { padding: 14, usePointStyle: true, pointStyleWidth: 8 } },
|
||||||
|
tooltip: { callbacks: { label: ctx => ` ${ctx.label}: ${ctx.raw} KK (${total?Math.round(ctx.raw/total*100):0}%)` } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bar: KK per ibadah
|
||||||
|
const ibadahKK = allIbadah.map(i => ({
|
||||||
|
nama: i.nama.length > 22 ? i.nama.substring(0,22)+'…' : i.nama,
|
||||||
|
jenis: i.jenis,
|
||||||
|
count: allPenduduk.filter(p => p.rumah_ibadah_id == i.id).length
|
||||||
|
})).sort((a,b) => b.count - a.count);
|
||||||
|
|
||||||
|
if (chartIbadah) chartIbadah.destroy();
|
||||||
|
chartIbadah = new Chart(document.getElementById('chart-ibadah'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ibadahKK.map(x => x.nama),
|
||||||
|
datasets: [{
|
||||||
|
label: 'KK Binaan',
|
||||||
|
data: ibadahKK.map(x => x.count),
|
||||||
|
backgroundColor: ibadahKK.map(x => (JENIS_COLOR[x.jenis]||'#94a3b8') + 'cc'),
|
||||||
|
borderColor: ibadahKK.map(x => JENIS_COLOR[x.jenis]||'#94a3b8'),
|
||||||
|
borderWidth: 1.5, borderRadius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true, maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { grid: { color: 'rgba(255,255,255,.04)' }, ticks: { maxRotation: 35, minRotation: 0 } },
|
||||||
|
y: { grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true, ticks: { precision: 0 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bar: Jenis Ibadah
|
||||||
|
const jenisCounts = {};
|
||||||
|
allIbadah.forEach(i => { jenisCounts[i.jenis] = (jenisCounts[i.jenis]||0) + 1; });
|
||||||
|
const jenisKeys = Object.keys(jenisCounts);
|
||||||
|
|
||||||
|
if (chartJenis) chartJenis.destroy();
|
||||||
|
chartJenis = new Chart(document.getElementById('chart-jenis'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: jenisKeys,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah',
|
||||||
|
data: jenisKeys.map(k => jenisCounts[k]),
|
||||||
|
backgroundColor: jenisKeys.map(k => (JENIS_COLOR[k]||'#94a3b8') + 'cc'),
|
||||||
|
borderColor: jenisKeys.map(k => JENIS_COLOR[k]||'#94a3b8'),
|
||||||
|
borderWidth: 1.5, borderRadius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { grid: { color: 'rgba(255,255,255,.04)' }, ticks: { precision: 0 } },
|
||||||
|
y: { grid: { color: 'rgba(255,255,255,.04)' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bar: Distribusi jumlah anggota per KK
|
||||||
|
const anggotaDist = {};
|
||||||
|
allPenduduk.forEach(p => {
|
||||||
|
const j = parseInt(p.jumlah_anggota) || 1;
|
||||||
|
const key = j <= 6 ? String(j) : '7+';
|
||||||
|
anggotaDist[key] = (anggotaDist[key]||0) + 1;
|
||||||
|
});
|
||||||
|
const anggotaKeys = ['1','2','3','4','5','6','7+'];
|
||||||
|
|
||||||
|
if (chartAnggota) chartAnggota.destroy();
|
||||||
|
chartAnggota = new Chart(document.getElementById('chart-anggota'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: anggotaKeys.map(k => k + ' jiwa'),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah KK',
|
||||||
|
data: anggotaKeys.map(k => anggotaDist[k]||0),
|
||||||
|
backgroundColor: '#3b82f6cc', borderColor: '#3b82f6',
|
||||||
|
borderWidth: 1.5, borderRadius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true, maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { grid: { color: 'rgba(255,255,255,.04)' } },
|
||||||
|
y: { grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true, ticks: { precision: 0 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bar: KK per Kecamatan
|
||||||
|
const kecCounts = {};
|
||||||
|
allPenduduk.forEach(p => {
|
||||||
|
if (!p.kecamatan) return;
|
||||||
|
kecCounts[p.kecamatan] = (kecCounts[p.kecamatan]||0) + 1;
|
||||||
|
});
|
||||||
|
const kecKeys = Object.keys(kecCounts).sort((a,b) => kecCounts[b] - kecCounts[a]);
|
||||||
|
const kecSection = document.getElementById('kecamatan-chart-section');
|
||||||
|
kecSection.style.display = kecKeys.length ? '' : 'none';
|
||||||
|
if (kecKeys.length) {
|
||||||
|
if (chartKecamatan) chartKecamatan.destroy();
|
||||||
|
chartKecamatan = new Chart(document.getElementById('chart-kecamatan'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: kecKeys,
|
||||||
|
datasets: [{
|
||||||
|
label: 'KK Miskin',
|
||||||
|
data: kecKeys.map(k => kecCounts[k]),
|
||||||
|
backgroundColor: '#6366f1cc', borderColor: '#6366f1',
|
||||||
|
borderWidth: 1.5, borderRadius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true, maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { grid: { color: 'rgba(255,255,255,.04)' } },
|
||||||
|
y: { grid: { color: 'rgba(255,255,255,.04)' }, beginAtZero: true, ticks: { precision: 0 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ibadah ranking table
|
||||||
|
function updateIbadahTable() {
|
||||||
|
const ranked = allIbadah.map(i => ({
|
||||||
|
...i,
|
||||||
|
kk: allPenduduk.filter(p => p.rumah_ibadah_id == i.id).length
|
||||||
|
})).sort((a,b) => b.kk - a.kk);
|
||||||
|
|
||||||
|
const tbody = document.getElementById('tbl-ibadah-body');
|
||||||
|
if (!ranked.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="table-empty"><i class="fas fa-mosque"></i><br>Belum ada rumah ibadah</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = ranked.map((r, i) => `
|
||||||
|
<tr>
|
||||||
|
<td style="color:var(--muted);font-size:12px">${i+1}</td>
|
||||||
|
<td>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<div class="dot" style="background:${JENIS_COLOR[r.jenis]||'#94a3b8'}"></div>
|
||||||
|
<span class="truncate" style="font-weight:500">${esc(r.nama)}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="font-size:11px;color:var(--muted)">${r.jenis}</td>
|
||||||
|
<td style="text-align:right">
|
||||||
|
<span style="font-weight:700;font-size:15px;color:${r.kk>0?'var(--blue)':'var(--muted)'}">${r.kk}</span>
|
||||||
|
<span style="color:var(--muted);font-size:11px"> KK</span>
|
||||||
|
</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── rekap table
|
||||||
|
function updateRekapTable() {
|
||||||
|
const jenisList = [...new Set(allIbadah.map(i => i.jenis))].sort();
|
||||||
|
const tbody = document.getElementById('tbl-rekap-body');
|
||||||
|
if (!jenisList.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" class="table-empty">Tidak ada data</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tbody.innerHTML = jenisList.map(jenis => {
|
||||||
|
const ibadahIds = allIbadah.filter(i => i.jenis === jenis).map(i => i.id);
|
||||||
|
const kks = allPenduduk.filter(p => ibadahIds.includes(parseInt(p.rumah_ibadah_id)));
|
||||||
|
const total = kks.length;
|
||||||
|
const sudah = kks.filter(p => p.status_bantuan === 'sudah_dibantu').length;
|
||||||
|
const pct = total > 0 ? Math.round(sudah / total * 100) : 0;
|
||||||
|
return `<tr>
|
||||||
|
<td><div style="display:flex;align-items:center;gap:7px"><div class="dot" style="background:${JENIS_COLOR[jenis]||'#94a3b8'}"></div><span style="font-size:12px">${jenis}</span></div></td>
|
||||||
|
<td style="text-align:right;font-weight:600">${total}</td>
|
||||||
|
<td style="text-align:right;color:var(--green);font-weight:600">${sudah}</td>
|
||||||
|
<td style="text-align:right">
|
||||||
|
<span style="font-weight:700;color:${pct>=50?'var(--green)':pct>=25?'var(--yellow)':'var(--red)'}">${pct}%</span>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── penduduk table
|
||||||
|
function filterTable() {
|
||||||
|
const q = document.getElementById('tbl-search').value.toLowerCase();
|
||||||
|
const fK = document.getElementById('tbl-filter-kec').value;
|
||||||
|
filteredPenduduk = allPenduduk.filter(p =>
|
||||||
|
(!fK || p.kecamatan === fK) &&
|
||||||
|
((p.kk_nama||'').toLowerCase().includes(q) ||
|
||||||
|
(p.nik||'').includes(q) ||
|
||||||
|
(p.alamat||'').toLowerCase().includes(q) ||
|
||||||
|
(p.kecamatan||'').toLowerCase().includes(q) ||
|
||||||
|
(p.kelurahan||'').toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
page = 1;
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable() {
|
||||||
|
const total = filteredPenduduk.length;
|
||||||
|
const pages = Math.ceil(total / PER_PAGE) || 1;
|
||||||
|
page = Math.max(1, Math.min(page, pages));
|
||||||
|
const slice = filteredPenduduk.slice((page-1)*PER_PAGE, page*PER_PAGE);
|
||||||
|
|
||||||
|
const SL = {belum_dibantu:'Belum',sudah_dibantu:'Sudah',menunggu_verifikasi:'Menunggu'};
|
||||||
|
const SK = {belum_dibantu:'sp-belum',sudah_dibantu:'sp-sudah',menunggu_verifikasi:'sp-tunggu'};
|
||||||
|
|
||||||
|
const tbody = document.getElementById('tbl-penduduk-body');
|
||||||
|
if (!slice.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="9" class="table-empty"><i class="fas fa-search"></i><br>Tidak ada data</td></tr>';
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = slice.map((p, i) => {
|
||||||
|
const ib = allIbadah.find(x => x.id == p.rumah_ibadah_id);
|
||||||
|
const no = (page-1)*PER_PAGE + i + 1;
|
||||||
|
return `<tr>
|
||||||
|
<td style="color:var(--muted);font-size:11px">${no}</td>
|
||||||
|
<td><span class="truncate" style="font-weight:600;display:block">${esc(p.kk_nama||'-')}</span></td>
|
||||||
|
<td style="font-size:11px;color:var(--muted);font-family:monospace">${p.nik||'-'}</td>
|
||||||
|
<td style="font-size:12px;color:var(--indigo)">${p.kecamatan?esc(p.kecamatan):'<span style="color:var(--muted)">—</span>'}</td>
|
||||||
|
<td><span class="truncate" style="color:var(--muted);font-size:12px;display:block">${esc(p.alamat||'-')}</span></td>
|
||||||
|
<td style="text-align:center;font-weight:600">${p.jumlah_anggota||1}</td>
|
||||||
|
<td style="font-size:12px;color:var(--muted)">${ib?esc(ib.nama).substring(0,20)+(ib.nama.length>20?'…':''):'—'}</td>
|
||||||
|
<td style="text-align:right;font-size:12px;font-family:monospace;color:var(--muted)">${p.jarak_meter?parseFloat(p.jarak_meter).toFixed(0)+' m':'—'}</td>
|
||||||
|
<td><span class="status-pill ${SK[p.status_bantuan]||'sp-belum'}">${SL[p.status_bantuan]||p.status_bantuan}</span></td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// pagination info
|
||||||
|
document.getElementById('pg-info').textContent =
|
||||||
|
total > 0 ? `Menampilkan ${(page-1)*PER_PAGE+1}–${Math.min(page*PER_PAGE,total)} dari ${total} data` : 'Tidak ada data';
|
||||||
|
|
||||||
|
// pagination buttons
|
||||||
|
const pgDiv = document.getElementById('pg-btns');
|
||||||
|
pgDiv.innerHTML = '';
|
||||||
|
const addBtn = (label, pg, disabled=false, active=false) => {
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.className = 'pg-btn' + (active?' active':'');
|
||||||
|
b.textContent = label;
|
||||||
|
b.disabled = disabled;
|
||||||
|
b.onclick = () => { page = pg; renderTable(); };
|
||||||
|
pgDiv.appendChild(b);
|
||||||
|
};
|
||||||
|
addBtn('‹', page-1, page===1);
|
||||||
|
const start = Math.max(1, page-2), end = Math.min(pages, page+2);
|
||||||
|
for (let p = start; p <= end; p++) addBtn(p, p, false, p===page);
|
||||||
|
addBtn('›', page+1, page===pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── utils
|
||||||
|
function esc(s) {
|
||||||
|
return String(s||'').replace(/[&<>"']/g, m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
||||||
|
}
|
||||||
|
function toast(msg, type='info') {
|
||||||
|
const t = document.createElement('div');
|
||||||
|
t.className = `toast ${type}`;
|
||||||
|
t.innerHTML = `<i class="fas fa-${type==='success'?'check-circle':'exclamation-circle'}"></i><span>${msg}</span>`;
|
||||||
|
document.getElementById('toast-wrap').appendChild(t);
|
||||||
|
setTimeout(() => { t.style.cssText='opacity:0;transition:.3s'; setTimeout(()=>t.remove(),300); }, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
-- ================================================================
|
||||||
|
-- WebGIS Sebaran Penduduk Miskin
|
||||||
|
-- Database: db_webgis
|
||||||
|
-- ================================================================
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS db_webgis CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE db_webgis;
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- Users
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
nama VARCHAR(100),
|
||||||
|
role ENUM('admin','operator') DEFAULT 'operator',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- Rumah Ibadah
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS rumah_ibadah (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
jenis ENUM('Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya') DEFAULT 'Masjid',
|
||||||
|
kontak VARCHAR(100),
|
||||||
|
alamat TEXT,
|
||||||
|
lat DECIMAL(10,7) NOT NULL,
|
||||||
|
lng DECIMAL(10,7) NOT NULL,
|
||||||
|
radius INT DEFAULT 300,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- Penduduk Miskin
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS penduduk_miskin (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
kk_nama VARCHAR(255) NOT NULL,
|
||||||
|
nik VARCHAR(20),
|
||||||
|
alamat TEXT,
|
||||||
|
lat DECIMAL(10,7) NOT NULL,
|
||||||
|
lng DECIMAL(10,7) NOT NULL,
|
||||||
|
jumlah_anggota INT DEFAULT 1,
|
||||||
|
rumah_ibadah_id INT,
|
||||||
|
jarak_meter DECIMAL(10,2),
|
||||||
|
status_bantuan ENUM('belum_dibantu','sudah_dibantu','menunggu_verifikasi') DEFAULT 'belum_dibantu',
|
||||||
|
jenis_bantuan VARCHAR(255),
|
||||||
|
tanggal_bantuan DATE,
|
||||||
|
bukti_file VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- Anggota Keluarga
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS anggota_keluarga (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
penduduk_id INT NOT NULL,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
hubungan VARCHAR(50),
|
||||||
|
umur INT,
|
||||||
|
pekerjaan VARCHAR(100),
|
||||||
|
keterangan TEXT,
|
||||||
|
FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- Default admin (password: admin123)
|
||||||
|
-- Ganti hash ini dengan php -r "echo password_hash('admin123', PASSWORD_DEFAULT);"
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
INSERT IGNORE INTO users (username, password, nama, role) VALUES
|
||||||
|
('admin', '$2y$10$TKh8H1.PfGLmAdaQXDLZQO3L4LYKE/VNRN3hJ3oZpYb7U0u0EWxLe', 'Administrator', 'admin'),
|
||||||
|
('operator', '$2y$10$TKh8H1.PfGLmAdaQXDLZQO3L4LYKE/VNRN3hJ3oZpYb7U0u0EWxLe', 'Operator Lapangan', 'operator');
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- Sample data (Pontianak area)
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
INSERT IGNORE INTO rumah_ibadah (id, nama, jenis, kontak, alamat, lat, lng, radius) VALUES
|
||||||
|
(1, 'Masjid Raya Mujahidin', 'Masjid', '0561-123456', 'Jl. Ahmad Yani, Pontianak', -0.0262, 109.3429, 500),
|
||||||
|
(2, 'Gereja Katedral Santo Yosef', 'Gereja Katolik', '0561-234567', 'Jl. Pattimura, Pontianak', -0.0312, 109.3301, 400),
|
||||||
|
(3, 'Vihara Bodhisattva Karaniya Metta', 'Vihara', '0561-345678', 'Jl. Tanjungpura, Pontianak', -0.0355, 109.3344, 350),
|
||||||
|
(4, 'Pura Agung Nusantara', 'Pura', '0561-456789', 'Jl. Gajah Mada, Pontianak', -0.0409, 109.3221, 400),
|
||||||
|
(5, 'Musholla Al-Ikhlas', 'Musholla', NULL, 'Jl. Kom Yos Sudarso, Pontianak', -0.0195, 109.3553, 250);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO penduduk_miskin (id, kk_nama, nik, alamat, lat, lng, rumah_ibadah_id, jarak_meter, status_bantuan, jenis_bantuan, tanggal_bantuan) VALUES
|
||||||
|
(1, 'Ahmad Subandi', '6171011234560001', 'Gg. Melati No.3, Pontianak Selatan', -0.0290, 109.3410, 1, 312.5, 'sudah_dibantu', 'PKH', '2024-03-01'),
|
||||||
|
(2, 'Siti Rahayu', '6171021234560002', 'Jl. Budi Utomo Gg.5, Pontianak', -0.0278, 109.3450, 1, 245.0, 'belum_dibantu', NULL, NULL),
|
||||||
|
(3, 'Budi Santoso', '6171031234560003', 'Jl. Gusti Hamzah, Pontianak Utara', -0.0320, 109.3290, 2, 198.3, 'menunggu_verifikasi', NULL, NULL),
|
||||||
|
(4, 'Maria Yuliana', '6171041234560004', 'Jl. Purnama Gg.2, Pontianak', -0.0350, 109.3320, 3, 267.1, 'sudah_dibantu', 'BPNT + PKH', '2024-01-15'),
|
||||||
|
(5, 'Wati Sari', '6171051234560005', 'Jl. Kom Yos Sudarso, Pontianak Timur', -0.0188, 109.3561, 5, 89.4, 'belum_dibantu', NULL, NULL);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan) VALUES
|
||||||
|
(1, 'Ahmad Subandi', 'Kepala Keluarga', 45, 'Buruh Harian'),
|
||||||
|
(1, 'Dewi Subandi', 'Istri', 40, 'Ibu Rumah Tangga'),
|
||||||
|
(1, 'Rizki Subandi', 'Anak', 18, 'Pelajar'),
|
||||||
|
(2, 'Siti Rahayu', 'Kepala Keluarga', 38, 'Pedagang Kecil'),
|
||||||
|
(2, 'Putri Rahayu', 'Anak', 12, 'Pelajar'),
|
||||||
|
(3, 'Budi Santoso', 'Kepala Keluarga', 52, 'Tidak Bekerja'),
|
||||||
|
(3, 'Sari Santoso', 'Istri', 48, 'Buruh Cuci'),
|
||||||
|
(4, 'Maria Yuliana', 'Kepala Keluarga', 41, 'Pedagang'),
|
||||||
|
(5, 'Wati Sari', 'Kepala Keluarga', 33, 'Buruh');
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
// ── Data dari DB ──────────────────────────────────────────────────────────
|
||||||
|
$allMasjid = mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM masjid"), MYSQLI_ASSOC);
|
||||||
|
$allKemiskinan = mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM kemiskinan"), MYSQLI_ASSOC);
|
||||||
|
|
||||||
|
// ── Haversine (meter) ─────────────────────────────────────────────────────
|
||||||
|
function haversine($lat1, $lng1, $lat2, $lng2) {
|
||||||
|
$R = 6371000;
|
||||||
|
$phi1 = deg2rad($lat1); $phi2 = deg2rad($lat2);
|
||||||
|
$dp = deg2rad($lat2 - $lat1);
|
||||||
|
$dl = deg2rad($lng2 - $lng1);
|
||||||
|
$a = sin($dp/2)**2 + cos($phi1)*cos($phi2)*sin($dl/2)**2;
|
||||||
|
return (int) round(2 * $R * asin(sqrt($a)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Analisis coverage (sama dengan logika JS) ─────────────────────────────
|
||||||
|
$kkCoverage = []; // kkId -> [masjid, ...]
|
||||||
|
$masjidCoverage = []; // masjidId -> ['kk'=>[], 'jiwa'=>0]
|
||||||
|
foreach ($allMasjid as $m) {
|
||||||
|
$masjidCoverage[$m['id']] = ['kk' => [], 'jiwa' => 0];
|
||||||
|
}
|
||||||
|
foreach ($allKemiskinan as $kk) {
|
||||||
|
$kkCoverage[$kk['id']] = [];
|
||||||
|
foreach ($allMasjid as $m) {
|
||||||
|
$dist = haversine($kk['lat'], $kk['lng'], $m['lat'], $m['lng']);
|
||||||
|
if ($dist <= (int)$m['radius_m']) {
|
||||||
|
$kkCoverage[$kk['id']][] = $m;
|
||||||
|
$masjidCoverage[$m['id']]['kk'][] = $kk;
|
||||||
|
$masjidCoverage[$m['id']]['jiwa'] += (int)$kk['jumlah_kk'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Statistik ringkasan ───────────────────────────────────────────────────
|
||||||
|
$totalKK = count($allKemiskinan);
|
||||||
|
$totalJiwa = array_sum(array_column($allKemiskinan, 'jumlah_kk'));
|
||||||
|
$countT = 0; $jiwaT = 0;
|
||||||
|
foreach ($allKemiskinan as $kk) {
|
||||||
|
if (count($kkCoverage[$kk['id']]) > 0) { $countT++; $jiwaT += (int)$kk['jumlah_kk']; }
|
||||||
|
}
|
||||||
|
$countTT = $totalKK - $countT;
|
||||||
|
$jiwaTT = $totalJiwa - $jiwaT;
|
||||||
|
$pct = $totalKK > 0 ? number_format(($countT / $totalKK) * 100, 1) : '0.0';
|
||||||
|
$pctTT = $totalKK > 0 ? number_format((($countTT) / $totalKK) * 100, 1) : '0.0';
|
||||||
|
|
||||||
|
// ── Tanggal dalam format Indonesia ───────────────────────────────────────
|
||||||
|
$bulan = ['','Januari','Februari','Maret','April','Mei','Juni',
|
||||||
|
'Juli','Agustus','September','Oktober','November','Desember'];
|
||||||
|
$tgl = date('j') . ' ' . $bulan[(int)date('n')] . ' ' . date('Y');
|
||||||
|
|
||||||
|
// ── Baris tabel masjid ────────────────────────────────────────────────────
|
||||||
|
$rowsMasjid = '';
|
||||||
|
foreach ($allMasjid as $i => $m) {
|
||||||
|
$cov = $masjidCoverage[$m['id']];
|
||||||
|
$bg = ($i % 2 === 0) ? '#ffffff' : '#f9fafb';
|
||||||
|
$rowsMasjid .= "
|
||||||
|
<tr style='background:{$bg};'>
|
||||||
|
<td style='text-align:center;'>" . ($i+1) . "</td>
|
||||||
|
<td>{$m['nama_masjid']}</td>
|
||||||
|
<td style='text-align:center;'>{$m['radius_m']}</td>
|
||||||
|
<td style='text-align:center;'><b>" . count($cov['kk']) . "</b></td>
|
||||||
|
<td style='text-align:center;'><b>{$cov['jiwa']}</b></td>
|
||||||
|
</tr>";
|
||||||
|
}
|
||||||
|
if (!$rowsMasjid) $rowsMasjid = "<tr><td colspan='5' style='text-align:center;color:#999;'>Belum ada data masjid.</td></tr>";
|
||||||
|
|
||||||
|
// ── Baris tabel KK miskin ─────────────────────────────────────────────────
|
||||||
|
$rowsKK = '';
|
||||||
|
foreach ($allKemiskinan as $i => $kk) {
|
||||||
|
$covers = $kkCoverage[$kk['id']];
|
||||||
|
$covered = count($covers) > 0;
|
||||||
|
|
||||||
|
// Jarak ke masjid terdekat
|
||||||
|
$minDist = '-';
|
||||||
|
if (count($allMasjid) > 0) {
|
||||||
|
$dists = array_map(fn($m) => haversine($kk['lat'], $kk['lng'], $m['lat'], $m['lng']), $allMasjid);
|
||||||
|
$minDist = min($dists);
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusTxt = $covered
|
||||||
|
? '<span style="color:#1a7a3e;font-weight:bold;">Terjangkau</span>'
|
||||||
|
: '<span style="color:#c0392b;font-weight:bold;">Tidak Terjangkau</span>';
|
||||||
|
$masjidNames = $covered ? implode(', ', array_column($covers, 'nama_masjid')) : '-';
|
||||||
|
$bg = ($i % 2 === 0) ? '#ffffff' : '#f9fafb';
|
||||||
|
|
||||||
|
$pkhTxt = $kk['pkh'] ? '<span style="color:#155724;font-weight:bold;">✓</span>' : '-';
|
||||||
|
$bpntTxt = $kk['bpnt'] ? '<span style="color:#004085;font-weight:bold;">✓</span>' : '-';
|
||||||
|
$bltTxt = $kk['blt'] ? '<span style="color:#856404;font-weight:bold;">✓</span>' : '-';
|
||||||
|
|
||||||
|
$rowsKK .= "
|
||||||
|
<tr style='background:{$bg};'>
|
||||||
|
<td style='text-align:center;'>" . ($i+1) . "</td>
|
||||||
|
<td>{$kk['nama_kk']}</td>
|
||||||
|
<td style='text-align:center;'>{$kk['jumlah_kk']}</td>
|
||||||
|
<td style='font-size:9px;'>{$kk['alamat']}</td>
|
||||||
|
<td style='text-align:center;'>{$pkhTxt}</td>
|
||||||
|
<td style='text-align:center;'>{$bpntTxt}</td>
|
||||||
|
<td style='text-align:center;'>{$bltTxt}</td>
|
||||||
|
<td style='text-align:center;'>{$statusTxt}</td>
|
||||||
|
<td style='font-size:9px;'>{$masjidNames}</td>
|
||||||
|
<td style='text-align:center;'>{$minDist}</td>
|
||||||
|
</tr>";
|
||||||
|
}
|
||||||
|
if (!$rowsKK) $rowsKK = "<tr><td colspan='10' style='text-align:center;color:#999;'>Belum ada data kemiskinan.</td></tr>";
|
||||||
|
|
||||||
|
// ── Template HTML untuk mPDF ──────────────────────────────────────────────
|
||||||
|
$html = <<<HTML
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; font-size: 11px; color: #222; }
|
||||||
|
h1 { font-size: 16px; text-align: center; margin-bottom: 2px; color: #1a3a5c; }
|
||||||
|
.subtitle { text-align: center; font-size: 10px; color: #555; margin-bottom: 16px; }
|
||||||
|
.section { font-size: 12px; font-weight: bold; color: #1a3a5c;
|
||||||
|
border-bottom: 2px solid #1a3a5c; padding-bottom: 3px; margin: 14px 0 8px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 10px; }
|
||||||
|
th { background: #1a3a5c; color: white; padding: 6px 5px; text-align: center; }
|
||||||
|
td { padding: 5px; border-bottom: 1px solid #e0e0e0; vertical-align: top; }
|
||||||
|
.stat-grid { width: 100%; border-collapse: collapse; margin-bottom: 4px; }
|
||||||
|
.stat-cell { width: 25%; padding: 8px; text-align: center; border: 1px solid #ddd; }
|
||||||
|
.stat-big { font-size: 22px; font-weight: bold; }
|
||||||
|
.stat-lbl { font-size: 9px; color: #666; margin-top: 2px; }
|
||||||
|
.green { color: #1a7a3e; }
|
||||||
|
.red { color: #c0392b; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h1>Laporan Analisis Kemiskinan & Jangkauan Masjid</h1>
|
||||||
|
<div class="subtitle">Tanggal Cetak: {$tgl} | Sumber Data: Sistem WebGIS</div>
|
||||||
|
|
||||||
|
<div class="section">Ringkasan Statistik</div>
|
||||||
|
<table class="stat-grid">
|
||||||
|
<tr>
|
||||||
|
<td class="stat-cell">
|
||||||
|
<div class="stat-big">{$totalKK}</div>
|
||||||
|
<div class="stat-lbl">Total KK Miskin</div>
|
||||||
|
</td>
|
||||||
|
<td class="stat-cell">
|
||||||
|
<div class="stat-big">{$totalJiwa}</div>
|
||||||
|
<div class="stat-lbl">Total Jiwa</div>
|
||||||
|
</td>
|
||||||
|
<td class="stat-cell">
|
||||||
|
<div class="stat-big green">{$countT} ({$pct}%)</div>
|
||||||
|
<div class="stat-lbl">KK Terjangkau • {$jiwaT} jiwa</div>
|
||||||
|
</td>
|
||||||
|
<td class="stat-cell">
|
||||||
|
<div class="stat-big red">{$countTT} ({$pctTT}%)</div>
|
||||||
|
<div class="stat-lbl">KK Tidak Terjangkau • {$jiwaTT} jiwa</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="section">Statistik per Masjid</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="5%">No</th>
|
||||||
|
<th width="35%">Nama Masjid</th>
|
||||||
|
<th width="15%">Radius (m)</th>
|
||||||
|
<th width="20%">KK Terjangkau</th>
|
||||||
|
<th width="25%">Jiwa Terjangkau</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{$rowsMasjid}</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="section">Daftar KK Miskin & Status Jangkauan</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="4%">No</th>
|
||||||
|
<th width="14%">Nama KK</th>
|
||||||
|
<th width="5%">Jiwa</th>
|
||||||
|
<th width="15%">Alamat</th>
|
||||||
|
<th width="5%">PKH</th>
|
||||||
|
<th width="5%">BPNT</th>
|
||||||
|
<th width="5%">BLT</th>
|
||||||
|
<th width="10%">Status</th>
|
||||||
|
<th width="22%">Dijangkau Masjid</th>
|
||||||
|
<th width="15%">Jarak Terdekat (m)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{$rowsKK}</tbody>
|
||||||
|
</table>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
// ── Generate PDF ──────────────────────────────────────────────────────────
|
||||||
|
try {
|
||||||
|
$mpdf = new \Mpdf\Mpdf([
|
||||||
|
'mode' => 'utf-8',
|
||||||
|
'format' => 'A4-L', // landscape untuk tabel lebar
|
||||||
|
'margin_top' => 14,
|
||||||
|
'margin_bottom' => 14,
|
||||||
|
'margin_left' => 12,
|
||||||
|
'margin_right' => 12,
|
||||||
|
]);
|
||||||
|
$mpdf->SetTitle('Laporan Analisis Kemiskinan & Jangkauan Masjid');
|
||||||
|
$mpdf->WriteHTML($html);
|
||||||
|
$fname = 'laporan_kemiskinan_' . date('Y-m-d') . '.pdf';
|
||||||
|
$mpdf->Output($fname, 'D'); // 'D' = force download
|
||||||
|
} catch (\Mpdf\MpdfException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo '<pre>Gagal membuat PDF: ' . htmlspecialchars($e->getMessage()) . "\n\n"
|
||||||
|
. "Pastikan extension PHP GD aktif di XAMPP:\n"
|
||||||
|
. " 1. Buka C:\\xampp\\php\\php.ini\n"
|
||||||
|
. " 2. Cari ;extension=gd dan hapus tanda titik koma\n"
|
||||||
|
. " 3. Restart Apache di XAMPP Control Panel</pre>";
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'spbu' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM spbu"), MYSQLI_ASSOC),
|
||||||
|
'jalan' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM jalan"), MYSQLI_ASSOC),
|
||||||
|
'persil' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM persil"), MYSQLI_ASSOC),
|
||||||
|
'kemiskinan'=> mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM kemiskinan"),MYSQLI_ASSOC),
|
||||||
|
'masjid' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM masjid"), MYSQLI_ASSOC),
|
||||||
|
'wilayah' => mysqli_fetch_all(mysqli_query($conn, "SELECT * FROM wilayah"), MYSQLI_ASSOC),
|
||||||
|
];
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($data);
|
||||||
|
?>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$type = $_POST['type'] ?? '';
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
$whitelist = ['spbu', 'kemiskinan', 'masjid', 'jalan', 'persil', 'wilayah'];
|
||||||
|
if (!in_array($type, $whitelist, true) || $id <= 0) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Request tidak valid.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$q = "DELETE FROM `{$type}` WHERE id = {$id}";
|
||||||
|
if (mysqli_query($conn, $q)) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
if (isset($_GET['id'])) {
|
||||||
|
$id = $_GET['id'];
|
||||||
|
$sql = "DELETE FROM spbu WHERE id = '$id'";
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $sql)) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
+877
@@ -0,0 +1,877 @@
|
|||||||
|
<?php
|
||||||
|
include 'auth.php';
|
||||||
|
requireLogin();
|
||||||
|
$user = currentUser();
|
||||||
|
$isAdmin = isSuperAdmin();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>GeoMiskin — WebGIS Sebaran Penduduk Miskin</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
|
||||||
|
<link href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{--sb:320px;--bg:#0f172a;--bg2:#1e293b;--bg3:#334155;--txt:#e2e8f0;--muted:#64748b;--blue:#3b82f6;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--indigo:#6366f1;--radius:12px;--trans:.2s ease}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Inter',sans-serif;height:100vh;overflow:hidden;display:flex}
|
||||||
|
|
||||||
|
/* SIDEBAR */
|
||||||
|
#sidebar{width:var(--sb);min-width:var(--sb);height:100vh;background:var(--bg);color:var(--txt);display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden;transition:transform var(--trans);z-index:1000;scrollbar-width:thin;scrollbar-color:var(--bg3) transparent}
|
||||||
|
#sidebar::-webkit-scrollbar{width:4px}
|
||||||
|
#sidebar::-webkit-scrollbar-thumb{background:var(--bg3);border-radius:4px}
|
||||||
|
.sb-brand{padding:20px 20px 16px;display:flex;align-items:center;gap:12px;border-bottom:1px solid rgba(255,255,255,.06);flex-shrink:0}
|
||||||
|
.sb-logo{width:40px;height:40px;background:linear-gradient(135deg,var(--blue),var(--indigo));border-radius:10px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(59,130,246,.4);flex-shrink:0}
|
||||||
|
.sb-logo i{color:#fff;font-size:18px}
|
||||||
|
.sb-title{font-weight:700;font-size:16px;line-height:1.2}
|
||||||
|
.sb-sub{font-size:10px;color:var(--muted);font-weight:400;letter-spacing:.5px;text-transform:uppercase;margin-top:2px}
|
||||||
|
.sb-user{padding:14px 20px;display:flex;align-items:center;gap:10px;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.06);flex-shrink:0}
|
||||||
|
.sb-avatar{width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,var(--blue),var(--indigo));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:13px;flex-shrink:0}
|
||||||
|
.sb-uname{font-size:13px;font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.sb-role{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
|
||||||
|
.btn-logout{background:none;border:none;color:var(--muted);cursor:pointer;padding:6px;border-radius:8px;transition:var(--trans);flex-shrink:0}
|
||||||
|
.btn-logout:hover{color:var(--red);background:rgba(239,68,68,.1)}
|
||||||
|
.sb-section{padding:14px 20px 8px;flex-shrink:0}
|
||||||
|
.sb-section-title{font-size:10px;color:var(--muted);letter-spacing:1px;text-transform:uppercase;font-weight:600;margin-bottom:10px}
|
||||||
|
|
||||||
|
/* STATS */
|
||||||
|
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:0 20px 14px}
|
||||||
|
.stat-card{background:var(--bg2);border-radius:var(--radius);padding:12px;border:1px solid rgba(255,255,255,.06);transition:var(--trans)}
|
||||||
|
.stat-card:hover{border-color:rgba(255,255,255,.12);transform:translateY(-1px)}
|
||||||
|
.stat-icon{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;margin-bottom:8px}
|
||||||
|
.stat-icon i{font-size:14px}
|
||||||
|
.stat-num{font-size:22px;font-weight:700;line-height:1;margin-bottom:3px}
|
||||||
|
.stat-lbl{font-size:10px;color:var(--muted);font-weight:500;line-height:1.3}
|
||||||
|
.ic-blue{background:rgba(59,130,246,.2)}.ic-blue i{color:var(--blue)}
|
||||||
|
.ic-red{background:rgba(239,68,68,.2)}.ic-red i{color:var(--red)}
|
||||||
|
.ic-green{background:rgba(34,197,94,.2)}.ic-green i{color:var(--green)}
|
||||||
|
.ic-yellow{background:rgba(245,158,11,.2)}.ic-yellow i{color:var(--yellow)}
|
||||||
|
|
||||||
|
/* SEARCH */
|
||||||
|
.sb-search{padding:0 20px 12px;flex-shrink:0}
|
||||||
|
.search-wrap{position:relative}
|
||||||
|
.search-wrap i{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:13px}
|
||||||
|
.search-wrap input{width:100%;background:var(--bg2);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:10px 10px 10px 36px;color:var(--txt);font-size:13px;font-family:inherit;outline:none;transition:var(--trans)}
|
||||||
|
.search-wrap input:focus{border-color:var(--blue);background:rgba(59,130,246,.08)}
|
||||||
|
.search-wrap input::placeholder{color:var(--muted)}
|
||||||
|
|
||||||
|
/* FILTER */
|
||||||
|
.filter-row{display:flex;flex-direction:column;gap:8px;padding:0 20px 12px}
|
||||||
|
.filter-row select{background:var(--bg2);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:9px 12px;color:var(--txt);font-size:12px;font-family:inherit;outline:none;cursor:pointer;transition:var(--trans)}
|
||||||
|
.filter-row select:focus{border-color:var(--blue)}
|
||||||
|
.filter-row select option{background:var(--bg2);color:var(--txt)}
|
||||||
|
|
||||||
|
/* LAYERS */
|
||||||
|
.layer-list{padding:0 20px 12px;display:flex;flex-direction:column;gap:6px}
|
||||||
|
.layer-item{display:flex;align-items:center;justify-content:space-between;background:var(--bg2);border-radius:10px;padding:10px 12px;border:1px solid rgba(255,255,255,.06)}
|
||||||
|
.layer-left{display:flex;align-items:center;gap:8px}
|
||||||
|
.layer-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||||||
|
.layer-name{font-size:12px;font-weight:500}
|
||||||
|
.toggle-switch{position:relative;width:36px;height:20px;flex-shrink:0}
|
||||||
|
.toggle-switch input{opacity:0;width:0;height:0}
|
||||||
|
.slider{position:absolute;inset:0;border-radius:20px;background:var(--bg3);cursor:pointer;transition:var(--trans)}
|
||||||
|
.slider:before{content:'';position:absolute;width:14px;height:14px;border-radius:50%;left:3px;top:3px;background:#fff;transition:var(--trans)}
|
||||||
|
.toggle-switch input:checked+.slider{background:var(--blue)}
|
||||||
|
.toggle-switch input:checked+.slider:before{transform:translateX(16px)}
|
||||||
|
|
||||||
|
/* ACTIONS */
|
||||||
|
.action-list{padding:0 20px 12px;display:flex;flex-direction:column;gap:6px}
|
||||||
|
.action-btn{display:flex;align-items:center;gap:10px;background:var(--bg2);border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:10px 14px;color:var(--txt);font-size:12px;font-weight:500;cursor:pointer;transition:var(--trans);font-family:inherit;width:100%;text-align:left}
|
||||||
|
.action-btn:hover{border-color:var(--blue);background:rgba(59,130,246,.1);color:var(--blue)}
|
||||||
|
.action-btn i{width:16px;text-align:center;font-size:13px}
|
||||||
|
.action-btn.green:hover{border-color:var(--green);background:rgba(34,197,94,.1);color:var(--green)}
|
||||||
|
.action-btn.yellow:hover{border-color:var(--yellow);background:rgba(245,158,11,.1);color:var(--yellow)}
|
||||||
|
|
||||||
|
/* LEGEND */
|
||||||
|
.legend{padding:0 20px 20px}
|
||||||
|
.legend-title{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;font-weight:600;margin-bottom:10px}
|
||||||
|
.legend-items{display:flex;flex-direction:column;gap:6px}
|
||||||
|
.legend-item{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--muted)}
|
||||||
|
.legend-dot{width:10px;height:10px;border-radius:50%;border:2px solid rgba(255,255,255,.3);flex-shrink:0}
|
||||||
|
|
||||||
|
/* MAP */
|
||||||
|
#map-container{flex:1;position:relative;min-width:0}
|
||||||
|
#map{width:100%;height:100%}
|
||||||
|
#sidebar-toggle{position:absolute;top:16px;left:16px;z-index:500;background:#fff;border:none;border-radius:10px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.15);transition:var(--trans)}
|
||||||
|
#sidebar-toggle:hover{background:#f8fafc}
|
||||||
|
#sidebar-toggle i{font-size:16px;color:#334155}
|
||||||
|
#pick-hint{position:absolute;top:70px;left:50%;transform:translateX(-50%);background:rgba(15,23,42,.9);color:#fff;padding:10px 20px;border-radius:10px;font-size:13px;font-weight:500;z-index:1000;display:none;pointer-events:none;border:1px solid rgba(255,255,255,.15);backdrop-filter:blur(8px)}
|
||||||
|
#pick-hint i{color:var(--yellow);margin-right:6px}
|
||||||
|
.user-dot{width:18px;height:18px;border-radius:50%;background:#3b82f6;border:3px solid #fff;box-shadow:0 0 0 4px rgba(59,130,246,.3);animation:pulse 2s infinite}
|
||||||
|
@keyframes pulse{0%,100%{box-shadow:0 0 0 4px rgba(59,130,246,.3)}50%{box-shadow:0 0 0 8px rgba(59,130,246,.08)}}
|
||||||
|
|
||||||
|
/* MODAL */
|
||||||
|
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:2000;display:none;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(4px)}
|
||||||
|
.modal-overlay.open{display:flex}
|
||||||
|
.modal-box{background:#fff;border-radius:20px;width:100%;max-width:560px;max-height:90vh;overflow-y:auto;box-shadow:0 25px 60px rgba(0,0,0,.4);animation:modalIn .2s ease}
|
||||||
|
@keyframes modalIn{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}
|
||||||
|
.modal-box.picking{opacity:.15;pointer-events:none}
|
||||||
|
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid #f1f5f9;position:sticky;top:0;background:#fff;z-index:1}
|
||||||
|
.modal-head h3{font-size:16px;font-weight:700;color:#0f172a;display:flex;align-items:center;gap:8px}
|
||||||
|
.modal-head h3 i{color:var(--blue)}
|
||||||
|
.btn-close-modal{background:none;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#94a3b8;transition:var(--trans)}
|
||||||
|
.btn-close-modal:hover{background:#f1f5f9;color:#334155}
|
||||||
|
.modal-body{padding:20px 24px}
|
||||||
|
.modal-foot{padding:16px 24px;border-top:1px solid #f1f5f9;display:flex;gap:10px;justify-content:flex-end;position:sticky;bottom:0;background:#fff}
|
||||||
|
|
||||||
|
/* FORM */
|
||||||
|
.fg{margin-bottom:16px}
|
||||||
|
.fg label{display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:6px}
|
||||||
|
.fg label .req{color:var(--red);margin-left:2px}
|
||||||
|
.fg input,.fg select,.fg textarea{width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;color:#0f172a;outline:none;transition:var(--trans);background:#fff}
|
||||||
|
.fg input:focus,.fg select:focus,.fg textarea:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(59,130,246,.1)}
|
||||||
|
.fg .hint{font-size:11px;color:#94a3b8;margin-top:4px}
|
||||||
|
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||||
|
.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
|
||||||
|
|
||||||
|
/* COORD */
|
||||||
|
.coord-group{background:#f8fafc;border-radius:10px;padding:14px;border:1px solid #e2e8f0;margin-bottom:16px}
|
||||||
|
.coord-group .coord-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
|
||||||
|
.coord-group label{font-size:11px;font-weight:600;color:#374151;display:block;margin-bottom:5px}
|
||||||
|
.coord-group input{border:1.5px solid #e2e8f0;border-radius:8px;padding:8px 12px;font-size:13px;font-family:inherit;color:#0f172a;outline:none;transition:var(--trans);width:100%;background:#fff}
|
||||||
|
.coord-group input:focus{border-color:var(--blue)}
|
||||||
|
.btn-pick{display:inline-flex;align-items:center;gap:6px;background:var(--blue);color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
|
||||||
|
.btn-pick:hover{background:#2563eb;transform:translateY(-1px)}
|
||||||
|
|
||||||
|
/* ANGGOTA */
|
||||||
|
.anggota-wrap{background:#f8fafc;border-radius:10px;padding:14px;border:1px solid #e2e8f0;margin-bottom:16px}
|
||||||
|
.anggota-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
|
||||||
|
.anggota-head h4{font-size:13px;font-weight:700;color:#0f172a}
|
||||||
|
.anggota-row{background:#fff;border-radius:8px;border:1px solid #e2e8f0;padding:12px;margin-bottom:8px;position:relative}
|
||||||
|
.btn-remove-anggota{position:absolute;top:10px;right:10px;background:none;border:none;color:#dc2626;cursor:pointer;padding:4px;border-radius:6px;transition:var(--trans)}
|
||||||
|
.btn-remove-anggota:hover{background:#fef2f2}
|
||||||
|
.btn-add-anggota{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:9px;border:1.5px dashed #cbd5e1;border-radius:8px;background:none;color:#64748b;font-size:12px;font-weight:500;cursor:pointer;transition:var(--trans);font-family:inherit;margin-top:8px}
|
||||||
|
.btn-add-anggota:hover{border-color:var(--blue);color:var(--blue);background:rgba(59,130,246,.05)}
|
||||||
|
|
||||||
|
/* STATUS BADGE */
|
||||||
|
.status-badge{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600}
|
||||||
|
.sb-belum{background:#fef2f2;color:#dc2626}
|
||||||
|
.sb-sudah{background:#f0fdf4;color:#16a34a}
|
||||||
|
.sb-menunggu{background:#fffbeb;color:#d97706}
|
||||||
|
|
||||||
|
/* UPLOAD */
|
||||||
|
.upload-area{border:2px dashed #cbd5e1;border-radius:10px;padding:20px;text-align:center;cursor:pointer;transition:var(--trans);background:#f8fafc}
|
||||||
|
.upload-area:hover{border-color:var(--blue);background:#eff6ff}
|
||||||
|
.upload-area i{font-size:24px;color:#94a3b8;margin-bottom:6px;display:block}
|
||||||
|
.upload-area p{font-size:12px;color:#64748b}
|
||||||
|
.upload-area input{display:none}
|
||||||
|
.upload-preview{font-size:12px;color:#374151;margin-top:8px;padding:6px 10px;background:#f1f5f9;border-radius:6px}
|
||||||
|
|
||||||
|
/* DIVIDER */
|
||||||
|
.divider{display:flex;align-items:center;gap:10px;margin:16px 0;font-size:11px;color:#94a3b8;font-weight:500}
|
||||||
|
.divider::before,.divider::after{content:'';flex:1;height:1px;background:#e2e8f0}
|
||||||
|
|
||||||
|
/* BUTTONS */
|
||||||
|
.btn-primary{background:var(--blue);color:#fff;border:none;border-radius:10px;padding:10px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
|
||||||
|
.btn-primary:hover{background:#2563eb;transform:translateY(-1px)}
|
||||||
|
.btn-secondary{background:#f1f5f9;color:#374151;border:none;border-radius:10px;padding:10px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
|
||||||
|
.btn-secondary:hover{background:#e2e8f0}
|
||||||
|
.btn-danger{background:#fef2f2;color:#dc2626;border:none;border-radius:10px;padding:10px 16px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
|
||||||
|
.btn-danger:hover{background:#fee2e2}
|
||||||
|
#bantuan-detail{border-top:1px solid #f1f5f9;padding-top:14px;margin-top:4px;display:none}
|
||||||
|
|
||||||
|
/* TOAST */
|
||||||
|
#toast-wrap{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px}
|
||||||
|
.toast{background:#0f172a;color:#fff;padding:12px 18px;border-radius:12px;font-size:13px;font-weight:500;box-shadow:0 8px 24px rgba(0,0,0,.3);display:flex;align-items:center;gap:10px;min-width:240px;max-width:320px;animation:slideUp .25s ease;border-left:4px solid var(--blue)}
|
||||||
|
.toast.success{border-left-color:var(--green)}
|
||||||
|
.toast.error{border-left-color:var(--red)}
|
||||||
|
.toast.warning{border-left-color:var(--yellow)}
|
||||||
|
@keyframes slideUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
|
||||||
|
.toast i{font-size:15px;flex-shrink:0}
|
||||||
|
.toast.success i{color:var(--green)}.toast.error i{color:var(--red)}.toast.warning i{color:var(--yellow)}
|
||||||
|
|
||||||
|
@media(max-width:768px){
|
||||||
|
#sidebar{position:fixed;top:0;left:0;z-index:1000;transform:translateX(-100%)}
|
||||||
|
#sidebar.open{transform:translateX(0)}
|
||||||
|
#map-container{width:100%}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ===== SIDEBAR ===== -->
|
||||||
|
<div id="sidebar">
|
||||||
|
<div class="sb-brand">
|
||||||
|
<div class="sb-logo"><i class="fas fa-map-marked-alt"></i></div>
|
||||||
|
<div><div class="sb-title">GeoMiskin</div><div class="sb-sub">Sebaran Penduduk Miskin</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="sb-user">
|
||||||
|
<div class="sb-avatar"><?= strtoupper(substr($user['nama'],0,1)) ?></div>
|
||||||
|
<div style="flex:1;min-width:0"><div class="sb-uname"><?= htmlspecialchars($user['nama']) ?></div><div class="sb-role"><?= $user['role'] ?></div></div>
|
||||||
|
<a href="logout.php" class="btn-logout" title="Logout"><i class="fas fa-sign-out-alt"></i></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sb-section"><div class="sb-section-title">Ringkasan</div></div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card"><div class="stat-icon ic-blue"><i class="fas fa-place-of-worship"></i></div><div class="stat-num" id="stat-ibadah">0</div><div class="stat-lbl">Rumah Ibadah</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-icon ic-red"><i class="fas fa-users"></i></div><div class="stat-num" id="stat-total">0</div><div class="stat-lbl">Total KK Miskin</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-icon ic-green"><i class="fas fa-check-circle"></i></div><div class="stat-num" id="stat-sudah">0</div><div class="stat-lbl">Sudah Dibantu</div></div>
|
||||||
|
<div class="stat-card"><div class="stat-icon ic-yellow"><i class="fas fa-clock"></i></div><div class="stat-num" id="stat-belum">0</div><div class="stat-lbl">Belum Dibantu</div></div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:0 20px 14px">
|
||||||
|
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:6px"><span>Sudah Dibantu</span><span id="progress-pct">0%</span></div>
|
||||||
|
<div style="background:var(--bg3);border-radius:6px;height:6px;overflow:hidden"><div id="progress-bar" style="height:100%;background:var(--green);border-radius:6px;width:0;transition:width .5s ease"></div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sb-search">
|
||||||
|
<div class="search-wrap"><i class="fas fa-search"></i><input type="text" id="search-box" placeholder="Cari warga atau rumah ibadah..."></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sb-section"><div class="sb-section-title">Filter</div></div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<select id="filter-jenis">
|
||||||
|
<option value="">Semua Jenis Rumah Ibadah</option>
|
||||||
|
<option>Masjid</option><option>Musholla</option><option>Gereja Protestan</option>
|
||||||
|
<option>Gereja Katolik</option><option>Pura</option><option>Vihara</option>
|
||||||
|
<option>Klenteng</option><option>Lainnya</option>
|
||||||
|
</select>
|
||||||
|
<select id="filter-status">
|
||||||
|
<option value="">Semua Status Bantuan</option>
|
||||||
|
<option value="belum_dibantu">Belum Dibantu</option>
|
||||||
|
<option value="sudah_dibantu">Sudah Dibantu</option>
|
||||||
|
<option value="menunggu_verifikasi">Menunggu Verifikasi</option>
|
||||||
|
</select>
|
||||||
|
<select id="filter-kecamatan">
|
||||||
|
<option value="">Semua Kecamatan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sb-section"><div class="sb-section-title">Layer Peta</div></div>
|
||||||
|
<div class="layer-list">
|
||||||
|
<div class="layer-item"><div class="layer-left"><div class="layer-dot" style="background:#3b82f6"></div><span class="layer-name">Rumah Ibadah + Radius</span></div><label class="toggle-switch"><input type="checkbox" id="layer-ibadah" checked><span class="slider"></span></label></div>
|
||||||
|
<div class="layer-item"><div class="layer-left"><div class="layer-dot" style="background:#ef4444"></div><span class="layer-name">Penduduk Miskin</span></div><label class="toggle-switch"><input type="checkbox" id="layer-penduduk" checked><span class="slider"></span></label></div>
|
||||||
|
<div class="layer-item"><div class="layer-left"><div class="layer-dot" style="background:linear-gradient(to right,#fbbf24,#ef4444)"></div><span class="layer-name">Heatmap Kemiskinan</span></div><label class="toggle-switch"><input type="checkbox" id="layer-heat"><span class="slider"></span></label></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sb-section"><div class="sb-section-title">Aksi</div></div>
|
||||||
|
<div class="action-list">
|
||||||
|
<button class="action-btn" onclick="openModalIbadah()"><i class="fas fa-plus-circle"></i> Tambah Rumah Ibadah</button>
|
||||||
|
<button class="action-btn green" onclick="openModalPenduduk()"><i class="fas fa-user-plus"></i> Tambah Penduduk Miskin</button>
|
||||||
|
<button class="action-btn yellow" onclick="getUserLocation()"><i class="fas fa-location-crosshairs"></i> Lokasi Saya Saat Ini</button>
|
||||||
|
<button class="action-btn" onclick="exportCSV()"><i class="fas fa-file-csv"></i> Export Data CSV</button>
|
||||||
|
<button class="action-btn" onclick="window.open('api/export_pdf.php','_blank')"><i class="fas fa-file-pdf"></i> Export PDF</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sb-section"><div class="sb-section-title">Akun</div></div>
|
||||||
|
<div class="action-list">
|
||||||
|
<button class="action-btn" onclick="window.location='dashboard.php'"><i class="fas fa-chart-bar"></i> Dashboard</button>
|
||||||
|
<button class="action-btn" onclick="openModal('password')"><i class="fas fa-key"></i> Ubah Password</button>
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<button class="action-btn" onclick="window.location='users.php'"><i class="fas fa-users-cog"></i> Manajemen Pengguna</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<button class="action-btn" onclick="window.location='../index.php'"><i class="fas fa-home"></i> Kembali ke Portal</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-title">Legenda Penduduk</div>
|
||||||
|
<div class="legend-items">
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div> Belum Dibantu</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#22c55e"></div> Sudah Dibantu</div>
|
||||||
|
<div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div> Menunggu Verifikasi</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== MAP ===== -->
|
||||||
|
<div id="map-container">
|
||||||
|
<button id="sidebar-toggle" title="Toggle sidebar"><i class="fas fa-bars"></i></button>
|
||||||
|
<div id="pick-hint"><i class="fas fa-mouse-pointer"></i>Klik di peta untuk memilih lokasi — tekan Esc untuk batal</div>
|
||||||
|
<div id="map"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast-wrap"></div>
|
||||||
|
|
||||||
|
<!-- ================================================================ MODAL: Rumah Ibadah ================================================================ -->
|
||||||
|
<div class="modal-overlay" id="modal-ibadah">
|
||||||
|
<div class="modal-box" id="modal-ibadah-box">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3><i class="fas fa-place-of-worship"></i> <span id="ibadah-modal-title">Tambah Rumah Ibadah</span></h3>
|
||||||
|
<button class="btn-close-modal" onclick="closeModal('ibadah')"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="ibadah-id">
|
||||||
|
<div class="fg"><label>Nama Rumah Ibadah <span class="req">*</span></label><input type="text" id="ibadah-nama" placeholder="cth. Masjid Al-Falah"></div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="fg"><label>Jenis <span class="req">*</span></label><select id="ibadah-jenis"><option>Masjid</option><option>Musholla</option><option>Gereja Protestan</option><option>Gereja Katolik</option><option>Pura</option><option>Vihara</option><option>Klenteng</option><option>Lainnya</option></select></div>
|
||||||
|
<div class="fg"><label>Kontak / Telepon</label><input type="text" id="ibadah-kontak" placeholder="0561-XXXXXX"></div>
|
||||||
|
</div>
|
||||||
|
<div class="coord-group">
|
||||||
|
<div class="coord-row">
|
||||||
|
<div><label>Latitude <span class="req">*</span></label><input type="number" step="any" id="ibadah-lat" placeholder="-0.0262"></div>
|
||||||
|
<div><label>Longitude <span class="req">*</span></label><input type="number" step="any" id="ibadah-lng" placeholder="109.3429"></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-pick" onclick="startPick('ibadah')"><i class="fas fa-map-pin"></i> Pilih Lokasi dari Peta</button>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label>Alamat <span class="req">*</span></label><input type="text" id="ibadah-alamat" placeholder="Akan terisi otomatis dari koordinat..."><div class="hint">Otomatis terisi saat memilih lokasi dari peta (Nominatim)</div></div>
|
||||||
|
<div class="fg"><label>Radius Layanan (meter)</label><input type="number" id="ibadah-radius" value="300" min="50" max="5000" step="50"><div class="hint">Default: 300 m. Perubahan langsung terlihat di peta.</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn-secondary" onclick="closeModal('ibadah')">Batal</button>
|
||||||
|
<button class="btn-primary" onclick="saveIbadah()"><i class="fas fa-save"></i> Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================================================================ MODAL: Penduduk Miskin ================================================================ -->
|
||||||
|
<div class="modal-overlay" id="modal-penduduk">
|
||||||
|
<div class="modal-box" id="modal-penduduk-box">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3><i class="fas fa-user-plus"></i> <span id="penduduk-modal-title">Tambah Penduduk Miskin</span></h3>
|
||||||
|
<button class="btn-close-modal" onclick="closeModal('penduduk')"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="penduduk-id">
|
||||||
|
<div class="divider">Lokasi</div>
|
||||||
|
<div class="coord-group">
|
||||||
|
<div class="coord-row">
|
||||||
|
<div><label>Latitude <span class="req">*</span></label><input type="number" step="any" id="penduduk-lat" placeholder="-0.0262"></div>
|
||||||
|
<div><label>Longitude <span class="req">*</span></label><input type="number" step="any" id="penduduk-lng" placeholder="109.3429"></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-pick" onclick="startPick('penduduk')"><i class="fas fa-map-pin"></i> Pilih Lokasi dari Peta</button>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label>Alamat Lengkap <span class="req">*</span></label><input type="text" id="penduduk-alamat" placeholder="Akan terisi otomatis dari koordinat..."></div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="fg"><label>Kecamatan</label><input type="text" id="penduduk-kecamatan" placeholder="Terisi otomatis..."></div>
|
||||||
|
<div class="fg"><label>Kelurahan / Desa</label><input type="text" id="penduduk-kelurahan" placeholder="Terisi otomatis..."></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">Data Kepala Keluarga</div>
|
||||||
|
<div class="fg"><label>Nama Kepala Keluarga <span class="req">*</span></label><input type="text" id="penduduk-kk-nama" placeholder="cth. Ahmad Subandi"></div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="fg"><label>NIK (Opsional)</label><input type="text" id="penduduk-nik" placeholder="16 digit NIK" maxlength="16"></div>
|
||||||
|
<div class="fg"><label>Jumlah Anggota <span class="req">*</span></label><input type="number" id="penduduk-jumlah" min="1" value="1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">Anggota Keluarga</div>
|
||||||
|
<div class="anggota-wrap">
|
||||||
|
<div class="anggota-head"><h4><i class="fas fa-users" style="color:#64748b;margin-right:6px"></i>Daftar Anggota</h4></div>
|
||||||
|
<div id="anggota-list"></div>
|
||||||
|
<button type="button" class="btn-add-anggota" onclick="addAnggotaRow()"><i class="fas fa-plus"></i> Tambah Anggota</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">Status Bantuan</div>
|
||||||
|
<div class="fg"><label>Status Bantuan <span class="req">*</span></label>
|
||||||
|
<select id="penduduk-status" onchange="toggleBantuanDetail(this.value)">
|
||||||
|
<option value="belum_dibantu">Belum Dibantu</option>
|
||||||
|
<option value="menunggu_verifikasi">Menunggu Verifikasi</option>
|
||||||
|
<option value="sudah_dibantu">Sudah Dibantu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="bantuan-detail">
|
||||||
|
<div class="row2">
|
||||||
|
<div class="fg"><label>Jenis Bantuan</label><input type="text" id="penduduk-jenis-bantuan" placeholder="PKH, BPNT, BLT, dll"></div>
|
||||||
|
<div class="fg"><label>Tanggal Bantuan</label><input type="date" id="penduduk-tanggal-bantuan"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider">Bukti Pendukung (Opsional)</div>
|
||||||
|
<div class="upload-area" onclick="document.getElementById('penduduk-bukti').click()">
|
||||||
|
<i class="fas fa-cloud-upload-alt"></i>
|
||||||
|
<p>Klik untuk upload foto / PDF / dokumen</p>
|
||||||
|
<p style="color:#94a3b8;font-size:10px;margin-top:4px">Maks. 5 MB</p>
|
||||||
|
<input type="file" id="penduduk-bukti" accept="image/*,application/pdf,.doc,.docx" onchange="previewFile(this)">
|
||||||
|
</div>
|
||||||
|
<div id="upload-preview" class="upload-preview" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn-secondary" onclick="closeModal('penduduk')">Batal</button>
|
||||||
|
<button class="btn-primary" onclick="savePenduduk()"><i class="fas fa-save"></i> Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================================================================ MODAL: Detail Ibadah ================================================================ -->
|
||||||
|
<div class="modal-overlay" id="modal-detail-ibadah">
|
||||||
|
<div class="modal-box" style="max-width:480px">
|
||||||
|
<div class="modal-head"><h3><i class="fas fa-info-circle"></i> Detail Rumah Ibadah</h3><button class="btn-close-modal" onclick="closeModal('detail-ibadah')"><i class="fas fa-times"></i></button></div>
|
||||||
|
<div class="modal-body" id="detail-ibadah-body"></div>
|
||||||
|
<div class="modal-foot" id="detail-ibadah-foot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================================================================ MODAL: Detail Penduduk ================================================================ -->
|
||||||
|
<div class="modal-overlay" id="modal-detail-penduduk">
|
||||||
|
<div class="modal-box" style="max-width:520px">
|
||||||
|
<div class="modal-head"><h3><i class="fas fa-user"></i> Detail Penduduk Miskin</h3><button class="btn-close-modal" onclick="closeModal('detail-penduduk')"><i class="fas fa-times"></i></button></div>
|
||||||
|
<div class="modal-body" id="detail-penduduk-body"></div>
|
||||||
|
<div class="modal-foot" id="detail-penduduk-foot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL: Ubah Password -->
|
||||||
|
<div class="modal-overlay" id="modal-password">
|
||||||
|
<div class="modal-box" style="max-width:420px">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3><i class="fas fa-key"></i> Ubah Password</h3>
|
||||||
|
<button class="btn-close-modal" onclick="closeModal('password')"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="fg"><label>Password Lama <span class="req">*</span></label><input type="password" id="pw-old" placeholder="Masukkan password saat ini"></div>
|
||||||
|
<div class="fg"><label>Password Baru <span class="req">*</span></label><input type="password" id="pw-new" placeholder="Minimal 6 karakter"></div>
|
||||||
|
<div class="fg"><label>Konfirmasi Password Baru <span class="req">*</span></label><input type="password" id="pw-conf" placeholder="Ulangi password baru"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn-secondary" onclick="closeModal('password')">Batal</button>
|
||||||
|
<button class="btn-primary" onclick="savePassword()"><i class="fas fa-save"></i> Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js"></script>
|
||||||
|
<script>
|
||||||
|
const IS_ADMIN = <?= $isAdmin ? 'true' : 'false' ?>;
|
||||||
|
// ================================================================ STATE
|
||||||
|
let map, ibadahLayer, pendudukLayer, heatLayer, userMarker;
|
||||||
|
let allIbadah = [], allPenduduk = [];
|
||||||
|
let anggotaCount = 0, pickingFor = null;
|
||||||
|
let editingIbadahId = null, editingPendudukId = null;
|
||||||
|
let tempIM = null, tempIC = null, tempPM = null;
|
||||||
|
|
||||||
|
const SC = {belum_dibantu:'#ef4444',sudah_dibantu:'#22c55e',menunggu_verifikasi:'#f59e0b'};
|
||||||
|
const IC = {Masjid:'#10b981',Musholla:'#34d399','Gereja Protestan':'#3b82f6','Gereja Katolik':'#6366f1',Pura:'#f59e0b',Vihara:'#ef4444',Klenteng:'#ec4899',Lainnya:'#94a3b8'};
|
||||||
|
|
||||||
|
// ================================================================ MAP
|
||||||
|
function initMap() {
|
||||||
|
map = L.map('map', {center:[-0.0263,109.3425], zoom:13, zoomControl:false});
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution:'© <a href="https://osm.org">OpenStreetMap</a>', maxZoom:19}).addTo(map);
|
||||||
|
L.control.zoom({position:'topright'}).addTo(map);
|
||||||
|
ibadahLayer = L.layerGroup().addTo(map);
|
||||||
|
pendudukLayer = L.layerGroup().addTo(map);
|
||||||
|
map.on('click', onMapClick);
|
||||||
|
document.addEventListener('keydown', e => { if (e.key==='Escape' && pickingFor) stopPick(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ DATA
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('api/get_data.php');
|
||||||
|
const d = await r.json();
|
||||||
|
allIbadah = d.ibadah || [];
|
||||||
|
allPenduduk = d.penduduk || [];
|
||||||
|
// Populate kecamatan filter from data
|
||||||
|
const kecList = [...new Set(allPenduduk.map(p=>p.kecamatan).filter(Boolean))].sort();
|
||||||
|
const selK = document.getElementById('filter-kecamatan');
|
||||||
|
const prevK = selK.value;
|
||||||
|
selK.innerHTML = '<option value="">Semua Kecamatan</option>' +
|
||||||
|
kecList.map(k=>`<option value="${k}"${k===prevK?' selected':''}>${k}</option>`).join('');
|
||||||
|
renderAll(); updateStats();
|
||||||
|
} catch(e) { toast('Gagal memuat data: '+e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ RENDER
|
||||||
|
function renderAll() {
|
||||||
|
ibadahLayer.clearLayers(); pendudukLayer.clearLayers();
|
||||||
|
const q = document.getElementById('search-box').value.toLowerCase();
|
||||||
|
const fJ = document.getElementById('filter-jenis').value;
|
||||||
|
const fS = document.getElementById('filter-status').value;
|
||||||
|
const fK = document.getElementById('filter-kecamatan').value;
|
||||||
|
allIbadah.filter(i => (!fJ||i.jenis===fJ)&&(!q||i.nama.toLowerCase().includes(q))).forEach(renderIbadah);
|
||||||
|
allPenduduk.filter(p =>
|
||||||
|
(!fS||p.status_bantuan===fS) &&
|
||||||
|
(!fK||p.kecamatan===fK) &&
|
||||||
|
(!q||p.kk_nama.toLowerCase().includes(q)||
|
||||||
|
(p.kecamatan||'').toLowerCase().includes(q)||
|
||||||
|
(p.kelurahan||'').toLowerCase().includes(q))
|
||||||
|
).forEach(renderPenduduk);
|
||||||
|
updateHeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ibadahIcon(jenis) {
|
||||||
|
const c = IC[jenis]||'#94a3b8';
|
||||||
|
return L.divIcon({className:'',html:`<div style="background:${c};width:30px;height:30px;border-radius:50%;border:3px solid #fff;display:flex;align-items:center;justify-content:center;box-shadow:0 3px 10px rgba(0,0,0,.3);font-size:13px;color:#fff">🛕</div>`,iconSize:[30,30],iconAnchor:[15,15]});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIbadah(d) {
|
||||||
|
const lat=parseFloat(d.lat), lng=parseFloat(d.lng), c=IC[d.jenis]||'#94a3b8';
|
||||||
|
ibadahLayer.addLayer(L.circle([lat,lng],{radius:parseInt(d.radius)||300,color:c,fillColor:c,fillOpacity:.08,weight:1.5,dashArray:'6,4'}));
|
||||||
|
const m = L.marker([lat,lng],{icon:ibadahIcon(d.jenis)});
|
||||||
|
m.on('click',()=>showDetailIbadah(d));
|
||||||
|
ibadahLayer.addLayer(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pendudukIcon(status) {
|
||||||
|
const c = SC[status]||'#ef4444';
|
||||||
|
return L.divIcon({className:'',html:`<div style="background:${c};width:14px;height:14px;border-radius:50%;border:2.5px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.35)"></div>`,iconSize:[14,14],iconAnchor:[7,7]});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPenduduk(d) {
|
||||||
|
const m = L.marker([parseFloat(d.lat),parseFloat(d.lng)],{icon:pendudukIcon(d.status_bantuan)});
|
||||||
|
m.on('click',()=>showDetailPenduduk(d));
|
||||||
|
pendudukLayer.addLayer(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ STATS
|
||||||
|
function updateStats() {
|
||||||
|
document.getElementById('stat-ibadah').textContent = allIbadah.length;
|
||||||
|
document.getElementById('stat-total').textContent = allPenduduk.length;
|
||||||
|
const sudah = allPenduduk.filter(p=>p.status_bantuan==='sudah_dibantu').length;
|
||||||
|
document.getElementById('stat-sudah').textContent = sudah;
|
||||||
|
document.getElementById('stat-belum').textContent = allPenduduk.filter(p=>p.status_bantuan==='belum_dibantu').length;
|
||||||
|
const pct = allPenduduk.length > 0 ? Math.round(sudah / allPenduduk.length * 100) : 0;
|
||||||
|
document.getElementById('progress-bar').style.width = pct + '%';
|
||||||
|
document.getElementById('progress-pct').textContent = pct + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ HEATMAP
|
||||||
|
function updateHeat() {
|
||||||
|
if (heatLayer) { try{map.removeLayer(heatLayer);}catch(e){} }
|
||||||
|
if (!document.getElementById('layer-heat').checked) return;
|
||||||
|
const pts = allPenduduk.map(p=>[parseFloat(p.lat),parseFloat(p.lng),0.6]);
|
||||||
|
if (!pts.length) return;
|
||||||
|
heatLayer = L.heatLayer(pts,{radius:30,blur:20,maxZoom:17,gradient:{0.4:'#fbbf24',0.7:'#f97316',1:'#ef4444'}});
|
||||||
|
map.addLayer(heatLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ LAYER TOGGLES
|
||||||
|
document.getElementById('layer-ibadah').addEventListener('change', function(){if(this.checked)map.addLayer(ibadahLayer);else map.removeLayer(ibadahLayer)});
|
||||||
|
document.getElementById('layer-penduduk').addEventListener('change', function(){if(this.checked)map.addLayer(pendudukLayer);else map.removeLayer(pendudukLayer)});
|
||||||
|
document.getElementById('layer-heat').addEventListener('change', updateHeat);
|
||||||
|
|
||||||
|
// ================================================================ SEARCH / FILTER
|
||||||
|
document.getElementById('search-box').addEventListener('input', renderAll);
|
||||||
|
document.getElementById('filter-jenis').addEventListener('change', renderAll);
|
||||||
|
document.getElementById('filter-status').addEventListener('change', renderAll);
|
||||||
|
document.getElementById('filter-kecamatan').addEventListener('change', renderAll);
|
||||||
|
|
||||||
|
// ================================================================ PICK LOCATION
|
||||||
|
function startPick(target) {
|
||||||
|
pickingFor = target;
|
||||||
|
document.getElementById(`modal-${target}`).classList.remove('open');
|
||||||
|
document.getElementById('pick-hint').style.display = 'block';
|
||||||
|
map.getContainer().style.cursor = 'crosshair';
|
||||||
|
}
|
||||||
|
function stopPick(reopen = true) {
|
||||||
|
if (!pickingFor) return;
|
||||||
|
const target = pickingFor;
|
||||||
|
pickingFor = null;
|
||||||
|
document.getElementById('pick-hint').style.display = 'none';
|
||||||
|
map.getContainer().style.cursor = '';
|
||||||
|
if (reopen) document.getElementById(`modal-${target}`).classList.add('open');
|
||||||
|
}
|
||||||
|
async function onMapClick(e) {
|
||||||
|
if (!pickingFor) return;
|
||||||
|
const {lat,lng} = e.latlng;
|
||||||
|
const fL=lat.toFixed(7), fG=lng.toFixed(7);
|
||||||
|
if (pickingFor==='ibadah') {
|
||||||
|
document.getElementById('ibadah-lat').value=fL;
|
||||||
|
document.getElementById('ibadah-lng').value=fG;
|
||||||
|
previewIbadahOnMap(lat,lng);
|
||||||
|
reverseGeocode(lat,lng).then(a=>document.getElementById('ibadah-alamat').value=a.display_name);
|
||||||
|
} else if (pickingFor==='penduduk') {
|
||||||
|
document.getElementById('penduduk-lat').value=fL;
|
||||||
|
document.getElementById('penduduk-lng').value=fG;
|
||||||
|
if(tempPM)pendudukLayer.removeLayer(tempPM);
|
||||||
|
tempPM=L.marker([lat,lng],{icon:pendudukIcon(document.getElementById('penduduk-status').value)});
|
||||||
|
pendudukLayer.addLayer(tempPM);
|
||||||
|
map.flyTo([lat,lng],16,{duration:.5});
|
||||||
|
reverseGeocode(lat,lng).then(a=>{
|
||||||
|
document.getElementById('penduduk-alamat').value=a.display_name;
|
||||||
|
document.getElementById('penduduk-kecamatan').value=a.kecamatan;
|
||||||
|
document.getElementById('penduduk-kelurahan').value=a.kelurahan;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
stopPick();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewIbadahOnMap(lat,lng) {
|
||||||
|
if(tempIM){ibadahLayer.removeLayer(tempIM);ibadahLayer.removeLayer(tempIC);}
|
||||||
|
const r=parseInt(document.getElementById('ibadah-radius').value)||300;
|
||||||
|
const c=IC[document.getElementById('ibadah-jenis').value]||'#3b82f6';
|
||||||
|
tempIC=L.circle([lat,lng],{radius:r,color:c,fillColor:c,fillOpacity:.08,weight:1.5,dashArray:'6,4'});
|
||||||
|
tempIM=L.marker([lat,lng],{icon:ibadahIcon(document.getElementById('ibadah-jenis').value)});
|
||||||
|
ibadahLayer.addLayer(tempIC); ibadahLayer.addLayer(tempIM);
|
||||||
|
map.flyTo([lat,lng],15,{duration:.5});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('input', e=>{
|
||||||
|
if(e.target.id==='ibadah-radius'&&tempIC) tempIC.setRadius(parseInt(e.target.value)||300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================ REVERSE GEOCODING
|
||||||
|
async function reverseGeocode(lat,lng) {
|
||||||
|
try {
|
||||||
|
const r=await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&accept-language=id`);
|
||||||
|
const d=await r.json();
|
||||||
|
const a=d.address||{};
|
||||||
|
return {
|
||||||
|
display_name: d.display_name||'',
|
||||||
|
kecamatan: a.city_district||a.county||a.district||'',
|
||||||
|
kelurahan: a.suburb||a.village||a.neighbourhood||a.quarter||''
|
||||||
|
};
|
||||||
|
} catch { return {display_name:'',kecamatan:'',kelurahan:''}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ GEOLOCATION
|
||||||
|
function getUserLocation() {
|
||||||
|
if(!navigator.geolocation){toast('Browser tidak mendukung Geolocation','error');return;}
|
||||||
|
toast('Mendapatkan lokasi...','warning');
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
pos=>{
|
||||||
|
const {latitude:lat,longitude:lng}=pos.coords;
|
||||||
|
if(userMarker)map.removeLayer(userMarker);
|
||||||
|
userMarker=L.marker([lat,lng],{icon:L.divIcon({className:'',html:'<div class="user-dot"></div>',iconSize:[18,18],iconAnchor:[9,9]})}).addTo(map).bindPopup('<b>📍 Lokasi Anda</b>').openPopup();
|
||||||
|
map.flyTo([lat,lng],16,{duration:1});
|
||||||
|
toast('Lokasi ditemukan!','success');
|
||||||
|
},
|
||||||
|
err=>toast('Gagal: '+err.message,'error')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ MODAL
|
||||||
|
function openModal(id) { document.getElementById(`modal-${id}`).classList.add('open'); }
|
||||||
|
function closeModal(id) {
|
||||||
|
document.getElementById(`modal-${id}`).classList.remove('open');
|
||||||
|
if (pickingFor === id) stopPick(false);
|
||||||
|
else stopPick();
|
||||||
|
if(id==='ibadah'){if(tempIM){ibadahLayer.removeLayer(tempIM);ibadahLayer.removeLayer(tempIC);tempIM=null;tempIC=null;}editingIbadahId=null;}
|
||||||
|
if(id==='penduduk'){if(tempPM){pendudukLayer.removeLayer(tempPM);tempPM=null;}editingPendudukId=null;}
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.modal-overlay').forEach(o=>o.addEventListener('click',e=>{if(e.target===o)closeModal(o.id.replace('modal-',''))}));
|
||||||
|
|
||||||
|
function openModalIbadah(d=null) {
|
||||||
|
editingIbadahId=d?d.id:null;
|
||||||
|
document.getElementById('ibadah-modal-title').textContent=d?'Edit Rumah Ibadah':'Tambah Rumah Ibadah';
|
||||||
|
['id','nama','kontak','lat','lng','alamat'].forEach(f=>document.getElementById(`ibadah-${f}`).value=d?.[f]||'');
|
||||||
|
document.getElementById('ibadah-jenis').value=d?.jenis||'Masjid';
|
||||||
|
document.getElementById('ibadah-radius').value=d?.radius||300;
|
||||||
|
openModal('ibadah');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModalPenduduk(d=null) {
|
||||||
|
editingPendudukId=d?d.id:null;
|
||||||
|
document.getElementById('penduduk-modal-title').textContent=d?'Edit Penduduk Miskin':'Tambah Penduduk Miskin';
|
||||||
|
document.getElementById('penduduk-id').value=d?.id||'';
|
||||||
|
document.getElementById('penduduk-lat').value=d?.lat||'';
|
||||||
|
document.getElementById('penduduk-lng').value=d?.lng||'';
|
||||||
|
document.getElementById('penduduk-alamat').value=d?.alamat||'';
|
||||||
|
document.getElementById('penduduk-kecamatan').value=d?.kecamatan||'';
|
||||||
|
document.getElementById('penduduk-kelurahan').value=d?.kelurahan||'';
|
||||||
|
document.getElementById('penduduk-kk-nama').value=d?.kk_nama||'';
|
||||||
|
document.getElementById('penduduk-nik').value=d?.nik||'';
|
||||||
|
document.getElementById('penduduk-jumlah').value=d?.jumlah_anggota||1;
|
||||||
|
document.getElementById('penduduk-status').value=d?.status_bantuan||'belum_dibantu';
|
||||||
|
document.getElementById('penduduk-jenis-bantuan').value=d?.jenis_bantuan||'';
|
||||||
|
document.getElementById('penduduk-tanggal-bantuan').value=d?.tanggal_bantuan||'';
|
||||||
|
toggleBantuanDetail(document.getElementById('penduduk-status').value);
|
||||||
|
document.getElementById('anggota-list').innerHTML=''; anggotaCount=0;
|
||||||
|
if(d?.anggota?.length) d.anggota.forEach(a=>addAnggotaRow(a)); else addAnggotaRow();
|
||||||
|
document.getElementById('upload-preview').style.display='none';
|
||||||
|
openModal('penduduk');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ ANGGOTA
|
||||||
|
function addAnggotaRow(d=null) {
|
||||||
|
const idx=anggotaCount++, row=document.createElement('div');
|
||||||
|
row.className='anggota-row'; row.id=`ar-${idx}`;
|
||||||
|
row.innerHTML=`<button type="button" class="btn-remove-anggota" onclick="removeAnggota(${idx})"><i class="fas fa-times"></i></button>
|
||||||
|
<div class="row2" style="margin-bottom:8px">
|
||||||
|
<div class="fg" style="margin:0"><label>Nama</label><input type="text" name="an[]" placeholder="Nama lengkap" value="${d?.nama||''}"></div>
|
||||||
|
<div class="fg" style="margin:0"><label>Hubungan</label><select name="ah[]">${['Kepala Keluarga','Istri','Anak','Orang Tua','Saudara','Lainnya'].map(h=>`<option ${d?.hubungan===h?'selected':''}>${h}</option>`).join('')}</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="row3">
|
||||||
|
<div class="fg" style="margin:0"><label>Umur</label><input type="number" name="au[]" placeholder="Thn" min="0" max="120" value="${d?.umur||''}"></div>
|
||||||
|
<div class="fg" style="margin:0"><label>Pekerjaan</label><input type="text" name="ap[]" placeholder="Pekerjaan" value="${d?.pekerjaan||''}"></div>
|
||||||
|
<div class="fg" style="margin:0"><label>Keterangan</label><input type="text" name="ak[]" placeholder="Opsional" value="${d?.keterangan||''}"></div>
|
||||||
|
</div>`;
|
||||||
|
document.getElementById('anggota-list').appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAnggota(idx) { const el=document.getElementById(`ar-${idx}`); if(el)el.remove(); }
|
||||||
|
|
||||||
|
function collectAnggota() {
|
||||||
|
return Array.from(document.querySelectorAll('#anggota-list .anggota-row')).map(row=>({
|
||||||
|
nama: row.querySelector('[name="an[]"]')?.value||'',
|
||||||
|
hubungan: row.querySelector('[name="ah[]"]')?.value||'',
|
||||||
|
umur: row.querySelector('[name="au[]"]')?.value||'',
|
||||||
|
pekerjaan:row.querySelector('[name="ap[]"]')?.value||'',
|
||||||
|
keterangan:row.querySelector('[name="ak[]"]')?.value||''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBantuanDetail(status) {
|
||||||
|
document.getElementById('bantuan-detail').style.display=status==='sudah_dibantu'?'block':'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ SAVE
|
||||||
|
async function saveIbadah() {
|
||||||
|
const nama=document.getElementById('ibadah-nama').value.trim();
|
||||||
|
const lat=document.getElementById('ibadah-lat').value;
|
||||||
|
const lng=document.getElementById('ibadah-lng').value;
|
||||||
|
const alamat=document.getElementById('ibadah-alamat').value.trim();
|
||||||
|
if(!nama||!lat||!lng||!alamat){toast('Lengkapi semua field wajib','error');return;}
|
||||||
|
const fd=new FormData();
|
||||||
|
fd.append('id',editingIbadahId||''); fd.append('nama',nama);
|
||||||
|
fd.append('jenis',document.getElementById('ibadah-jenis').value);
|
||||||
|
fd.append('kontak',document.getElementById('ibadah-kontak').value);
|
||||||
|
fd.append('alamat',alamat); fd.append('lat',lat); fd.append('lng',lng);
|
||||||
|
fd.append('radius',document.getElementById('ibadah-radius').value||300);
|
||||||
|
const url=editingIbadahId?'api/update_ibadah.php':'api/save_ibadah.php';
|
||||||
|
const r=await fetch(url,{method:'POST',body:fd});
|
||||||
|
const result=await r.json();
|
||||||
|
if(result.status==='success'){closeModal('ibadah');toast(editingIbadahId?'Diperbarui!':'Berhasil ditambahkan!','success');loadData();}
|
||||||
|
else toast('Error: '+result.message,'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePenduduk() {
|
||||||
|
const lat=document.getElementById('penduduk-lat').value;
|
||||||
|
const lng=document.getElementById('penduduk-lng').value;
|
||||||
|
const kk=document.getElementById('penduduk-kk-nama').value.trim();
|
||||||
|
const alamat=document.getElementById('penduduk-alamat').value.trim();
|
||||||
|
if(!lat||!lng||!kk||!alamat){toast('Lengkapi semua field wajib','error');return;}
|
||||||
|
const fd=new FormData();
|
||||||
|
fd.append('id',editingPendudukId||''); fd.append('lat',lat); fd.append('lng',lng);
|
||||||
|
fd.append('alamat',alamat);
|
||||||
|
fd.append('kecamatan',document.getElementById('penduduk-kecamatan').value);
|
||||||
|
fd.append('kelurahan',document.getElementById('penduduk-kelurahan').value);
|
||||||
|
fd.append('kk_nama',kk);
|
||||||
|
fd.append('nik',document.getElementById('penduduk-nik').value);
|
||||||
|
fd.append('jumlah_anggota',document.getElementById('penduduk-jumlah').value||1);
|
||||||
|
fd.append('status_bantuan',document.getElementById('penduduk-status').value);
|
||||||
|
fd.append('jenis_bantuan',document.getElementById('penduduk-jenis-bantuan').value);
|
||||||
|
fd.append('tanggal_bantuan',document.getElementById('penduduk-tanggal-bantuan').value);
|
||||||
|
fd.append('anggota',JSON.stringify(collectAnggota()));
|
||||||
|
const fi=document.getElementById('penduduk-bukti');
|
||||||
|
if(fi.files[0]) fd.append('bukti_file',fi.files[0]);
|
||||||
|
const url=editingPendudukId?'api/update_penduduk.php':'api/save_penduduk.php';
|
||||||
|
const r=await fetch(url,{method:'POST',body:fd});
|
||||||
|
const result=await r.json();
|
||||||
|
if(result.status==='success'){closeModal('penduduk');toast(editingPendudukId?'Diperbarui!':'Data penduduk ditambahkan!','success');loadData();}
|
||||||
|
else toast('Error: '+result.message,'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ DELETE
|
||||||
|
async function deleteRecord(type,id,name) {
|
||||||
|
if(!confirm(`Hapus "${name}"? Tindakan ini tidak dapat dibatalkan.`)) return;
|
||||||
|
const fd=new FormData(); fd.append('type',type); fd.append('id',id);
|
||||||
|
const r=await fetch('api/delete.php',{method:'POST',body:fd});
|
||||||
|
const result=await r.json();
|
||||||
|
if(result.status==='success'){
|
||||||
|
closeModal('detail-ibadah'); closeModal('detail-penduduk');
|
||||||
|
toast('Data berhasil dihapus.','success'); loadData();
|
||||||
|
} else toast('Gagal hapus: '+result.message,'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ DETAIL
|
||||||
|
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))}
|
||||||
|
const SL={belum_dibantu:'Belum Dibantu',sudah_dibantu:'Sudah Dibantu',menunggu_verifikasi:'Menunggu Verifikasi'};
|
||||||
|
const SK={belum_dibantu:'sb-belum',sudah_dibantu:'sb-sudah',menunggu_verifikasi:'sb-menunggu'};
|
||||||
|
|
||||||
|
function showDetailIbadah(d) {
|
||||||
|
const binaan=allPenduduk.filter(p=>p.rumah_ibadah_id==d.id).length;
|
||||||
|
const dJSON=JSON.stringify(d).replace(/"/g,'"');
|
||||||
|
document.getElementById('detail-ibadah-body').innerHTML=`
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
|
||||||
|
<div style="width:48px;height:48px;border-radius:14px;background:${IC[d.jenis]||'#94a3b8'};display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0">🛕</div>
|
||||||
|
<div><div style="font-weight:700;font-size:17px;color:#0f172a">${esc(d.nama)}</div><span style="background:#eff6ff;color:#2563eb;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600">${d.jenis}</span></div>
|
||||||
|
</div>
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||||||
|
<tr><td style="padding:7px 0;color:#64748b;font-weight:500;width:120px"><i class="fas fa-map-marker-alt" style="color:#3b82f6;margin-right:6px"></i>Alamat</td><td style="padding:7px 0;color:#374151">${esc(d.alamat)||'-'}</td></tr>
|
||||||
|
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-phone" style="color:#3b82f6;margin-right:6px"></i>Kontak</td><td style="padding:7px 0;color:#374151">${esc(d.kontak)||'-'}</td></tr>
|
||||||
|
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-circle" style="color:#3b82f6;margin-right:6px"></i>Radius</td><td style="padding:7px 0;color:#374151">${d.radius} meter</td></tr>
|
||||||
|
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-users" style="color:#22c55e;margin-right:6px"></i>KK Binaan</td><td style="padding:7px 0;font-weight:700;font-size:17px;color:#0f172a">${binaan} KK</td></tr>
|
||||||
|
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-map-pin" style="color:#3b82f6;margin-right:6px"></i>Koordinat</td><td style="padding:7px 0;color:#374151;font-family:monospace;font-size:11px">${parseFloat(d.lat).toFixed(6)}, ${parseFloat(d.lng).toFixed(6)}</td></tr>
|
||||||
|
</table>`;
|
||||||
|
document.getElementById('detail-ibadah-foot').innerHTML=`
|
||||||
|
<button class="btn-secondary" onclick="closeModal('detail-ibadah')">Tutup</button>
|
||||||
|
<button class="btn-primary" onclick="closeModal('detail-ibadah');openModalIbadah(${dJSON})"><i class="fas fa-edit"></i> Edit</button>
|
||||||
|
${IS_ADMIN ? `<button class="btn-danger" onclick="deleteRecord('ibadah',${d.id},'${esc(d.nama)}')"><i class="fas fa-trash"></i> Hapus</button>` : ''}`;
|
||||||
|
openModal('detail-ibadah');
|
||||||
|
map.flyTo([parseFloat(d.lat),parseFloat(d.lng)],15,{duration:.5});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDetailPenduduk(d) {
|
||||||
|
const ibadah=allIbadah.find(i=>i.id==d.rumah_ibadah_id);
|
||||||
|
const dJSON=JSON.stringify(d).replace(/"/g,'"');
|
||||||
|
let anggotaHtml='';
|
||||||
|
if(d.anggota?.length){
|
||||||
|
anggotaHtml=`<div style="margin-top:14px;border-top:1px solid #f1f5f9;padding-top:14px">
|
||||||
|
<div style="font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Anggota Keluarga</div>
|
||||||
|
<table style="width:100%;font-size:12px;border-collapse:collapse">
|
||||||
|
<tr style="background:#f8fafc"><th style="padding:6px 8px;text-align:left;color:#374151">Nama</th><th style="padding:6px 8px;text-align:left;color:#374151">Hubungan</th><th style="padding:6px 8px;text-align:left;color:#374151">Umur</th><th style="padding:6px 8px;text-align:left;color:#374151">Pekerjaan</th></tr>
|
||||||
|
${d.anggota.map(a=>`<tr style="border-top:1px solid #f1f5f9"><td style="padding:6px 8px;color:#374151">${esc(a.nama)}</td><td style="padding:6px 8px;color:#64748b">${esc(a.hubungan)||'-'}</td><td style="padding:6px 8px;color:#64748b">${esc(String(a.umur||''))||'-'}</td><td style="padding:6px 8px;color:#64748b">${esc(a.pekerjaan)||'-'}</td></tr>`).join('')}
|
||||||
|
</table></div>`;
|
||||||
|
}
|
||||||
|
document.getElementById('detail-penduduk-body').innerHTML=`
|
||||||
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:14px">
|
||||||
|
<div><div style="font-weight:700;font-size:17px;color:#0f172a">${esc(d.kk_nama)}</div>${d.nik?`<div style="font-size:11px;color:#94a3b8;font-family:monospace">NIK: ${d.nik}</div>`:''}</div>
|
||||||
|
<span class="status-badge ${SK[d.status_bantuan]||'sb-belum'}">${SL[d.status_bantuan]||d.status_bantuan}</span>
|
||||||
|
</div>
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||||||
|
<tr><td style="padding:7px 0;color:#64748b;font-weight:500;width:140px"><i class="fas fa-map-marker-alt" style="color:#ef4444;margin-right:6px"></i>Alamat</td><td style="padding:7px 0;color:#374151">${esc(d.alamat)||'-'}</td></tr>
|
||||||
|
${(d.kecamatan||d.kelurahan)?`<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-map" style="color:#8b5cf6;margin-right:6px"></i>Wilayah</td><td style="padding:7px 0;color:#374151">${[d.kelurahan,d.kecamatan].filter(Boolean).map(esc).join(', ')}</td></tr>`:''}
|
||||||
|
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-users" style="color:#3b82f6;margin-right:6px"></i>Jumlah Anggota</td><td style="padding:7px 0;color:#374151">${d.jumlah_anggota||'-'} jiwa</td></tr>
|
||||||
|
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-place-of-worship" style="color:#10b981;margin-right:6px"></i>Rumah Ibadah</td><td style="padding:7px 0;color:#374151">${ibadah?esc(ibadah.nama):'Belum teridentifikasi'} ${d.jarak_meter?`<span style="color:#94a3b8;font-size:11px">(${parseFloat(d.jarak_meter).toFixed(0)} m)</span>`:''}</td></tr>
|
||||||
|
${d.jenis_bantuan?`<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-hand-holding-heart" style="color:#22c55e;margin-right:6px"></i>Bantuan</td><td style="padding:7px 0;color:#374151">${esc(d.jenis_bantuan)} ${d.tanggal_bantuan?`<span style="color:#94a3b8;font-size:11px">(${d.tanggal_bantuan})</span>`:''}</td></tr>`:''}
|
||||||
|
${d.bukti_file?`<tr><td colspan="2" style="padding:7px 0"><a href="uploads/bukti/${d.bukti_file}" target="_blank" style="color:#3b82f6;font-size:12px"><i class="fas fa-paperclip"></i> Lihat Bukti</a></td></tr>`:''}
|
||||||
|
</table>${anggotaHtml}`;
|
||||||
|
document.getElementById('detail-penduduk-foot').innerHTML=`
|
||||||
|
<button class="btn-secondary" onclick="closeModal('detail-penduduk')">Tutup</button>
|
||||||
|
<button class="btn-primary" onclick="closeModal('detail-penduduk');openModalPenduduk(${dJSON})"><i class="fas fa-edit"></i> Edit</button>
|
||||||
|
${IS_ADMIN ? `<button class="btn-danger" onclick="deleteRecord('penduduk',${d.id},'${esc(d.kk_nama)}')"><i class="fas fa-trash"></i> Hapus</button>` : ''}`;
|
||||||
|
openModal('detail-penduduk');
|
||||||
|
map.flyTo([parseFloat(d.lat),parseFloat(d.lng)],16,{duration:.5});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ EXPORT
|
||||||
|
function exportCSV() {
|
||||||
|
let csv='No,Nama KK,NIK,Alamat,Kecamatan,Kelurahan,Jumlah Anggota,Rumah Ibadah Terdekat,Jarak (m),Status Bantuan,Jenis Bantuan,Tanggal Bantuan,Lat,Lng\n';
|
||||||
|
allPenduduk.forEach((p,i)=>{
|
||||||
|
const ib=allIbadah.find(ib=>ib.id==p.rumah_ibadah_id);
|
||||||
|
csv+=`${i+1},"${p.kk_nama||''}",${p.nik||''},"${p.alamat||''}","${p.kecamatan||''}","${p.kelurahan||''}",${p.jumlah_anggota||''},"${ib?.nama||''}",${p.jarak_meter||''},${p.status_bantuan||''},"${p.jenis_bantuan||''}",${p.tanggal_bantuan||''},${p.lat},${p.lng}\n`;
|
||||||
|
});
|
||||||
|
const a=document.createElement('a');
|
||||||
|
a.href=URL.createObjectURL(new Blob([''+csv],{type:'text/csv;charset=utf-8'}));
|
||||||
|
a.download=`penduduk_miskin_${new Date().toISOString().slice(0,10)}.csv`;
|
||||||
|
a.click(); toast('CSV berhasil diunduh','success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewFile(input) {
|
||||||
|
if(!input.files[0]) return;
|
||||||
|
const f=input.files[0];
|
||||||
|
document.getElementById('upload-preview').textContent=`📎 ${f.name} (${(f.size/1024).toFixed(1)} KB)`;
|
||||||
|
document.getElementById('upload-preview').style.display='block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ SIDEBAR TOGGLE
|
||||||
|
document.getElementById('sidebar-toggle').addEventListener('click',()=>document.getElementById('sidebar').classList.toggle('open'));
|
||||||
|
|
||||||
|
// ================================================================ TOAST
|
||||||
|
function toast(msg,type='info') {
|
||||||
|
const icons={success:'fa-check-circle',error:'fa-exclamation-circle',warning:'fa-exclamation-triangle',info:'fa-info-circle'};
|
||||||
|
const t=document.createElement('div');
|
||||||
|
t.className=`toast ${type}`;
|
||||||
|
t.innerHTML=`<i class="fas ${icons[type]}"></i><span>${msg}</span>`;
|
||||||
|
document.getElementById('toast-wrap').appendChild(t);
|
||||||
|
setTimeout(()=>{t.style.cssText='opacity:0;transform:translateY(8px);transition:.3s';setTimeout(()=>t.remove(),300)},3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ CHANGE PASSWORD
|
||||||
|
async function savePassword() {
|
||||||
|
const old = document.getElementById('pw-old').value;
|
||||||
|
const nw = document.getElementById('pw-new').value;
|
||||||
|
const cf = document.getElementById('pw-conf').value;
|
||||||
|
if (!old || !nw || !cf) { toast('Semua field wajib diisi','error'); return; }
|
||||||
|
if (nw.length < 6) { toast('Password baru minimal 6 karakter','error'); return; }
|
||||||
|
if (nw !== cf) { toast('Konfirmasi password tidak cocok','error'); return; }
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('old_password', old);
|
||||||
|
fd.append('new_password', nw);
|
||||||
|
const r = await fetch('api/change_password.php', {method:'POST', body:fd});
|
||||||
|
const result = await r.json();
|
||||||
|
if (result.status === 'success') {
|
||||||
|
closeModal('password');
|
||||||
|
['pw-old','pw-new','pw-conf'].forEach(id=>document.getElementById(id).value='');
|
||||||
|
toast('Password berhasil diubah!', 'success');
|
||||||
|
} else {
|
||||||
|
toast('Error: ' + result.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================ INIT
|
||||||
|
initMap(); loadData();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
if (isset($conn)) return;
|
||||||
|
$conn = mysqli_init();
|
||||||
|
mysqli_options($conn, MYSQLI_OPT_CONNECT_TIMEOUT, 5);
|
||||||
|
$ok = @mysqli_real_connect($conn, '127.0.0.1', 'root', '', 'db_webgis', 3306);
|
||||||
|
if (!$ok) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
die(json_encode(['status' => 'error', 'message' => 'Koneksi DB gagal: ' . mysqli_connect_error()]));
|
||||||
|
}
|
||||||
|
mysqli_set_charset($conn, 'utf8mb4');
|
||||||
+254
@@ -0,0 +1,254 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (!empty($_SESSION['user'])) { header('Location: index.php'); exit; }
|
||||||
|
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
// ── AJAX: cek username ──────────────────────────────────────────
|
||||||
|
if (($_POST['action'] ?? '') === 'check_user') {
|
||||||
|
$u = trim($_POST['username'] ?? '');
|
||||||
|
$st = mysqli_prepare($conn, "SELECT id, nama FROM users WHERE username = ?");
|
||||||
|
mysqli_stmt_bind_param($st, 's', $u);
|
||||||
|
mysqli_stmt_execute($st);
|
||||||
|
$row = mysqli_fetch_assoc(mysqli_stmt_get_result($st));
|
||||||
|
echo json_encode($row ? ['ok'=>true,'nama'=>$row['nama']] : ['ok'=>false]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── AJAX: reset password ────────────────────────────────────────
|
||||||
|
if (($_POST['action'] ?? '') === 'reset_password') {
|
||||||
|
$u = trim($_POST['username'] ?? '');
|
||||||
|
$pass = $_POST['new_password'] ?? '';
|
||||||
|
if (strlen($pass) < 6) { echo json_encode(['ok'=>false,'msg'=>'Password minimal 6 karakter']); exit; }
|
||||||
|
$hash = password_hash($pass, PASSWORD_DEFAULT);
|
||||||
|
$st = mysqli_prepare($conn, "UPDATE users SET password=? WHERE username=?");
|
||||||
|
mysqli_stmt_bind_param($st, 'ss', $hash, $u);
|
||||||
|
mysqli_stmt_execute($st);
|
||||||
|
echo json_encode(mysqli_stmt_affected_rows($st) > 0
|
||||||
|
? ['ok'=>true]
|
||||||
|
: ['ok'=>false,'msg'=>'Username tidak ditemukan']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Login biasa ─────────────────────────────────────────────────
|
||||||
|
$error = '';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$stmt = mysqli_prepare($conn, "SELECT id, username, password, nama, role FROM users WHERE username = ?");
|
||||||
|
mysqli_stmt_bind_param($stmt, 's', $username);
|
||||||
|
mysqli_stmt_execute($stmt);
|
||||||
|
$user = mysqli_fetch_assoc(mysqli_stmt_get_result($stmt));
|
||||||
|
if ($user && password_verify($password, $user['password'])) {
|
||||||
|
$_SESSION['user'] = $user;
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$error = 'Username atau password salah.';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login — GeoMiskin</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Inter',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0f172a}
|
||||||
|
.bg-pattern{position:fixed;inset:0;background-image:radial-gradient(circle at 25% 40%,rgba(59,130,246,.12) 0%,transparent 55%),radial-gradient(circle at 75% 70%,rgba(99,102,241,.1) 0%,transparent 55%);pointer-events:none}
|
||||||
|
.card{background:#1e293b;border:1px solid #334155;border-radius:20px;padding:40px;width:100%;max-width:400px;box-shadow:0 24px 48px rgba(0,0,0,.6)}
|
||||||
|
.logo{text-align:center;margin-bottom:32px}
|
||||||
|
.logo-icon{width:64px;height:64px;background:linear-gradient(135deg,#3b82f6,#6366f1);border-radius:16px;display:inline-flex;align-items:center;justify-content:center;margin-bottom:16px;box-shadow:0 8px 24px rgba(59,130,246,.35)}
|
||||||
|
.logo-icon svg{width:32px;height:32px;fill:white}
|
||||||
|
h1{color:#f1f5f9;font-size:22px;font-weight:700;margin-bottom:4px}
|
||||||
|
p.subtitle{color:#64748b;font-size:13.5px}
|
||||||
|
.form-group{margin-bottom:18px}
|
||||||
|
label{display:block;color:#94a3b8;font-size:12.5px;font-weight:600;margin-bottom:7px;letter-spacing:.3px}
|
||||||
|
input[type=text],input[type=password]{width:100%;background:#0f172a;border:1px solid #334155;border-radius:10px;padding:12px 14px;color:#f1f5f9;font-size:14.5px;font-family:inherit;outline:none;transition:.2s}
|
||||||
|
input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.2)}
|
||||||
|
input::placeholder{color:#94a3b8}
|
||||||
|
.btn{width:100%;padding:13px;background:linear-gradient(135deg,#3b82f6,#6366f1);border:none;border-radius:10px;color:#fff;font-size:15px;font-weight:600;font-family:inherit;cursor:pointer;transition:.2s;letter-spacing:.3px;margin-top:4px}
|
||||||
|
.btn:hover{opacity:.9;box-shadow:0 6px 20px rgba(59,130,246,.4)}
|
||||||
|
.btn:active{transform:translateY(1px)}
|
||||||
|
.btn-ghost{width:100%;padding:0;background:none;border:none;color:#94a3b8;font-size:13px;font-family:inherit;cursor:pointer;margin-top:14px;transition:.15s;text-decoration:underline;text-underline-offset:3px}
|
||||||
|
.btn-ghost:hover{color:#cbd5e1}
|
||||||
|
.error{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.35);border-radius:10px;padding:11px 14px;color:#fca5a5;font-size:13px;margin-bottom:18px;display:flex;align-items:center;gap:8px}
|
||||||
|
.hint{text-align:center;color:#475569;font-size:12px;margin-top:22px}
|
||||||
|
.hint span{color:#64748b;font-weight:500}
|
||||||
|
|
||||||
|
/* ── Modal ── */
|
||||||
|
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:999;opacity:0;pointer-events:none;transition:opacity .2s}
|
||||||
|
.overlay.show{opacity:1;pointer-events:all}
|
||||||
|
.modal{background:#1e293b;border:1px solid #334155;border-radius:18px;padding:32px;width:100%;max-width:380px;transform:translateY(16px);transition:transform .2s}
|
||||||
|
.overlay.show .modal{transform:translateY(0)}
|
||||||
|
.modal-title{color:#f1f5f9;font-size:17px;font-weight:700;margin-bottom:6px}
|
||||||
|
.modal-sub{color:#64748b;font-size:13px;margin-bottom:24px}
|
||||||
|
.alert-ok{background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.3);border-radius:10px;padding:11px 14px;color:#86efac;font-size:13px;margin-bottom:16px;display:none}
|
||||||
|
.alert-err{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);border-radius:10px;padding:11px 14px;color:#fca5a5;font-size:13px;margin-bottom:16px;display:none}
|
||||||
|
.step{display:none}.step.active{display:block}
|
||||||
|
.back-link{display:inline-flex;align-items:center;gap:4px;color:#64748b;font-size:12.5px;cursor:pointer;border:none;background:none;font-family:inherit;margin-bottom:16px;padding:0}
|
||||||
|
.back-link:hover{color:#94a3b8}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="bg-pattern"></div>
|
||||||
|
|
||||||
|
<!-- ── Form Login ── -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h1>GeoMiskin</h1>
|
||||||
|
<p class="subtitle">WebGIS Sebaran Penduduk Miskin</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="error">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
|
||||||
|
<?= htmlspecialchars($error) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" name="username" placeholder="Masukkan username" autocomplete="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password</label>
|
||||||
|
<input type="password" name="password" placeholder="Masukkan password" autocomplete="current-password" required>
|
||||||
|
</div>
|
||||||
|
<button class="btn" type="submit">Masuk ke Sistem</button>
|
||||||
|
<button class="btn-ghost" type="button" onclick="openReset()">Lupa password?</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="hint">Default: <span>admin</span> / <span>admin123</span></p>
|
||||||
|
<div style="text-align:center;margin-top:20px;padding-top:20px;border-top:1px solid #1e293b">
|
||||||
|
<a href="../index.php" style="color:#475569;font-size:12px;text-decoration:none;display:inline-flex;align-items:center;gap:6px;transition:.15s" onmouseover="this.style.color='#94a3b8'" onmouseout="this.style.color='#475569'">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
||||||
|
Kembali ke Portal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Modal Lupa Password ── -->
|
||||||
|
<div class="overlay" id="overlay" onclick="closeReset(event)">
|
||||||
|
<div class="modal" onclick="event.stopPropagation()">
|
||||||
|
|
||||||
|
<!-- Step 1: cek username -->
|
||||||
|
<div class="step active" id="step1">
|
||||||
|
<div class="modal-title">🔑 Lupa Password</div>
|
||||||
|
<div class="modal-sub">Masukkan username Anda untuk melanjutkan.</div>
|
||||||
|
<div class="alert-err" id="err1"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" id="fp-username" placeholder="Masukkan username" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<button class="btn" onclick="checkUser()">Lanjutkan</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: set password baru -->
|
||||||
|
<div class="step" id="step2">
|
||||||
|
<button class="back-link" onclick="goStep(1)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
||||||
|
Kembali
|
||||||
|
</button>
|
||||||
|
<div class="modal-title">Password Baru</div>
|
||||||
|
<div class="modal-sub" id="step2-sub"></div>
|
||||||
|
<div class="alert-err" id="err2"></div>
|
||||||
|
<div class="alert-ok" id="ok2"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Password Baru</label>
|
||||||
|
<input type="password" id="fp-pass1" placeholder="Minimal 6 karakter">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Konfirmasi Password</label>
|
||||||
|
<input type="password" id="fp-pass2" placeholder="Ulangi password baru">
|
||||||
|
</div>
|
||||||
|
<button class="btn" onclick="doReset()">Simpan Password</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openReset() {
|
||||||
|
document.getElementById('fp-username').value = '';
|
||||||
|
document.getElementById('fp-pass1').value = '';
|
||||||
|
document.getElementById('fp-pass2').value = '';
|
||||||
|
hideAlerts();
|
||||||
|
goStep(1);
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
}
|
||||||
|
function closeReset(e) {
|
||||||
|
if (!e || e.target === document.getElementById('overlay'))
|
||||||
|
document.getElementById('overlay').classList.remove('show');
|
||||||
|
}
|
||||||
|
function goStep(n) {
|
||||||
|
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
|
||||||
|
document.getElementById('step'+n).classList.add('active');
|
||||||
|
}
|
||||||
|
function hideAlerts() {
|
||||||
|
['err1','err2','ok2'].forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
el.style.display = 'none';
|
||||||
|
el.textContent = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkUser() {
|
||||||
|
const u = document.getElementById('fp-username').value.trim();
|
||||||
|
if (!u) { showErr('err1', 'Username tidak boleh kosong'); return; }
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'check_user');
|
||||||
|
fd.append('username', u);
|
||||||
|
const res = await fetch('login.php', {method:'POST', body:fd});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.ok) {
|
||||||
|
document.getElementById('step2-sub').textContent = 'Halo, ' + data.nama + '. Silakan buat password baru.';
|
||||||
|
hideAlerts();
|
||||||
|
goStep(2);
|
||||||
|
} else {
|
||||||
|
showErr('err1', 'Username tidak ditemukan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doReset() {
|
||||||
|
const u = document.getElementById('fp-username').value.trim();
|
||||||
|
const p1 = document.getElementById('fp-pass1').value;
|
||||||
|
const p2 = document.getElementById('fp-pass2').value;
|
||||||
|
if (p1.length < 6) { showErr('err2', 'Password minimal 6 karakter'); return; }
|
||||||
|
if (p1 !== p2) { showErr('err2', 'Password tidak cocok'); return; }
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'reset_password');
|
||||||
|
fd.append('username', u);
|
||||||
|
fd.append('new_password', p1);
|
||||||
|
const res = await fetch('login.php', {method:'POST', body:fd});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.ok) {
|
||||||
|
document.getElementById('err2').style.display = 'none';
|
||||||
|
showOk('ok2', 'Password berhasil diubah! Silakan login kembali.');
|
||||||
|
document.getElementById('fp-pass1').value = '';
|
||||||
|
document.getElementById('fp-pass2').value = '';
|
||||||
|
setTimeout(() => document.getElementById('overlay').classList.remove('show'), 2000);
|
||||||
|
} else {
|
||||||
|
showErr('err2', data.msg || 'Gagal mengubah password');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErr(id, msg) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
el.textContent = msg;
|
||||||
|
el.style.display = 'block';
|
||||||
|
}
|
||||||
|
function showOk(id, msg) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
el.textContent = msg;
|
||||||
|
el.style.display = 'block';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
session_destroy();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Jalankan script ini sekali di phpMyAdmin untuk menambahkan kolom baru ke tabel kemiskinan
|
||||||
|
-- Menu: phpMyAdmin → db_gis → SQL → paste → Go
|
||||||
|
|
||||||
|
ALTER TABLE kemiskinan
|
||||||
|
ADD COLUMN pkh TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Program Keluarga Harapan' AFTER alamat,
|
||||||
|
ADD COLUMN bpnt TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Bantuan Pangan Non-Tunai' AFTER pkh,
|
||||||
|
ADD COLUMN blt TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Bantuan Langsung Tunai' AFTER bpnt,
|
||||||
|
ADD COLUMN foto VARCHAR(255) DEFAULT NULL COMMENT 'Nama file foto rumah' AFTER blt;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Jalankan sekali di phpMyAdmin → db_gis → SQL → paste → Go
|
||||||
|
CREATE TABLE IF NOT EXISTS wilayah (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
tipe ENUM('RT','RW','Kelurahan','Kecamatan') DEFAULT 'RT',
|
||||||
|
geojson TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$n = $_POST['nama_jalan']; $s = $_POST['status_jalan']; $p = $_POST['panjang']; $g = $_POST['geojson'];
|
||||||
|
mysqli_query($conn, "INSERT INTO jalan (nama_jalan, status_jalan, panjang_m, geojson) VALUES ('$n', '$s', '$p', '$g')");
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
?>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$n = mysqli_real_escape_string($conn, $_POST['nama_kk'] ?? '');
|
||||||
|
$j = max(1, (int)($_POST['jumlah_kk'] ?? 1));
|
||||||
|
$a = mysqli_real_escape_string($conn, $_POST['alamat'] ?? '');
|
||||||
|
$lt = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lg = (float)($_POST['lng'] ?? 0);
|
||||||
|
$pkh = isset($_POST['pkh']) && $_POST['pkh'] == '1' ? 1 : 0;
|
||||||
|
$bpnt = isset($_POST['bpnt']) && $_POST['bpnt'] == '1' ? 1 : 0;
|
||||||
|
$blt = isset($_POST['blt']) && $_POST['blt'] == '1' ? 1 : 0;
|
||||||
|
|
||||||
|
$fotoSql = 'NULL';
|
||||||
|
if (!empty($_FILES['foto']['name']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg', 'jpeg', 'png', 'webp'];
|
||||||
|
if (in_array($ext, $allowed) && $_FILES['foto']['size'] <= 5 * 1024 * 1024) {
|
||||||
|
$dir = __DIR__ . '/uploads/kemiskinan/';
|
||||||
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||||
|
$fname = 'kk_' . time() . '_' . rand(1000, 9999) . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $dir . $fname)) {
|
||||||
|
$fotoSql = "'" . mysqli_real_escape_string($conn, $fname) . "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$q = "INSERT INTO kemiskinan (nama_kk, jumlah_kk, alamat, lat, lng, pkh, bpnt, blt, foto)
|
||||||
|
VALUES ('$n', $j, '$a', $lt, $lg, $pkh, $bpnt, $blt, $fotoSql)";
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $q)) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$n = $_POST['nama_masjid']; $p = $_POST['pic']; $a = $_POST['alamat']; $r = $_POST['radius_m']; $lt = $_POST['lat']; $lg = $_POST['lng'];
|
||||||
|
mysqli_query($conn, "INSERT INTO masjid (nama_masjid, pic, alamat, radius_m, lat, lng) VALUES ('$n', '$p', '$a', '$r', '$lt', '$lg')");
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
?>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
// Pastikan kunci $_POST ini sesuai dengan fd.append di index.php
|
||||||
|
$nomor = $_POST['nomor_persil'];
|
||||||
|
$status = $_POST['status_milik'];
|
||||||
|
$luas = $_POST['luas'];
|
||||||
|
$geojson = $_POST['geojson'];
|
||||||
|
|
||||||
|
// Query SQL (Pastikan nama kolom di database db_gis sudah benar)
|
||||||
|
$sql = "INSERT INTO persil (nomor_persil, status_milik, luas_m2, geojson)
|
||||||
|
VALUES ('$nomor', '$status', '$luas', '$geojson')";
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $sql)) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
// Jika gagal, tampilkan error MySQL-nya biar ketahuan apa yang salah
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
$n = $_POST['nama']; $no = $_POST['nomor']; $s = $_POST['status']; $lt = $_POST['lat']; $lg = $_POST['lng'];
|
||||||
|
mysqli_query($conn, "INSERT INTO spbu (nama, nomor, status_24jam, lat, lng) VALUES ('$n', '$no', '$s', '$lt', '$lg')");
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
// Tambahkan baris ini di file simpan_*.php kamu
|
||||||
|
$query = "INSERT INTO ... "; // query kamu
|
||||||
|
if (mysqli_query($conn, $query)) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
// Ini akan menampilkan pesan error asli dari MySQL
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$nama = mysqli_real_escape_string($conn, trim($_POST['nama'] ?? ''));
|
||||||
|
$tipe = in_array($_POST['tipe'] ?? '', ['RT','RW','Kelurahan','Kecamatan'])
|
||||||
|
? $_POST['tipe'] : 'RT';
|
||||||
|
$geojson = $_POST['geojson'] ?? '';
|
||||||
|
|
||||||
|
if (!$nama || !$geojson) {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$q = "INSERT INTO wilayah (nama, tipe, geojson)
|
||||||
|
VALUES ('$nama', '$tipe', '" . mysqli_real_escape_string($conn, $geojson) . "')";
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
$start = microtime(true);
|
||||||
|
$conn = @mysqli_connect('127.0.0.1', 'root', '', 'db_webgis');
|
||||||
|
$t1 = round((microtime(true)-$start)*1000);
|
||||||
|
echo "127.0.0.1: " . ($conn ? "OK ({$t1}ms)" : mysqli_connect_error()) . "\n";
|
||||||
|
|
||||||
|
$start = microtime(true);
|
||||||
|
$conn2 = @mysqli_connect('localhost', 'root', '', 'db_webgis');
|
||||||
|
$t2 = round((microtime(true)-$start)*1000);
|
||||||
|
echo "localhost: " . ($conn2 ? "OK ({$t2}ms)" : mysqli_connect_error()) . "\n";
|
||||||
|
|
||||||
|
echo "PHP: " . phpversion() . "\n";
|
||||||
|
echo "Time: " . date('H:i:s') . "\n";
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$type = $_POST['type'] ?? '';
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) { echo json_encode(['status' => 'error']); exit; }
|
||||||
|
|
||||||
|
function esc($conn, $v) { return mysqli_real_escape_string($conn, trim($v)); }
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'spbu':
|
||||||
|
$nama = esc($conn, $_POST['nama'] ?? '');
|
||||||
|
$nomor = esc($conn, $_POST['nomor'] ?? '');
|
||||||
|
$status = in_array($_POST['status'] ?? '', ['Ya', 'Tidak']) ? $_POST['status'] : 'Tidak';
|
||||||
|
$q = "UPDATE spbu SET nama='$nama', nomor='$nomor', status_24jam='$status' WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'kemiskinan':
|
||||||
|
$nama_kk = esc($conn, $_POST['nama_kk'] ?? '');
|
||||||
|
$jumlah_kk = max(1, (int)($_POST['jumlah_kk'] ?? 1));
|
||||||
|
$alamat = esc($conn, $_POST['alamat'] ?? '');
|
||||||
|
$pkh = isset($_POST['pkh']) && $_POST['pkh'] == '1' ? 1 : 0;
|
||||||
|
$bpnt = isset($_POST['bpnt']) && $_POST['bpnt'] == '1' ? 1 : 0;
|
||||||
|
$blt = isset($_POST['blt']) && $_POST['blt'] == '1' ? 1 : 0;
|
||||||
|
|
||||||
|
$fotoSet = '';
|
||||||
|
if (!empty($_FILES['foto']['name']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg', 'jpeg', 'png', 'webp'];
|
||||||
|
if (in_array($ext, $allowed) && $_FILES['foto']['size'] <= 5 * 1024 * 1024) {
|
||||||
|
$dir = __DIR__ . '/uploads/kemiskinan/';
|
||||||
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||||
|
$fname = 'kk_' . $id . '_' . time() . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $dir . $fname)) {
|
||||||
|
$fotoSet = ", foto='" . mysqli_real_escape_string($conn, $fname) . "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$q = "UPDATE kemiskinan SET nama_kk='$nama_kk', jumlah_kk=$jumlah_kk, alamat='$alamat',
|
||||||
|
pkh=$pkh, bpnt=$bpnt, blt=$blt{$fotoSet} WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'masjid':
|
||||||
|
$nama = esc($conn, $_POST['nama_masjid'] ?? '');
|
||||||
|
$pic = esc($conn, $_POST['pic'] ?? '');
|
||||||
|
$alamat = esc($conn, $_POST['alamat'] ?? '');
|
||||||
|
$q = "UPDATE masjid SET nama_masjid='$nama', pic='$pic', alamat='$alamat' WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jalan':
|
||||||
|
$nama = esc($conn, $_POST['nama_jalan'] ?? '');
|
||||||
|
$status = in_array($_POST['status_jalan'] ?? '', ['Nasional', 'Provinsi', 'Kabupaten'])
|
||||||
|
? $_POST['status_jalan'] : 'Kabupaten';
|
||||||
|
$q = "UPDATE jalan SET nama_jalan='$nama', status_jalan='$status' WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'persil':
|
||||||
|
$nomor = esc($conn, $_POST['nomor_persil'] ?? '');
|
||||||
|
$status = in_array($_POST['status_milik'] ?? '', ['SHM', 'HGB', 'HGU', 'HP'])
|
||||||
|
? $_POST['status_milik'] : 'SHM';
|
||||||
|
$q = "UPDATE persil SET nomor_persil='$nomor', status_milik='$status' WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'wilayah':
|
||||||
|
$nama = esc($conn, $_POST['nama'] ?? '');
|
||||||
|
$tipe = in_array($_POST['tipe'] ?? '', ['RT','RW','Kelurahan','Kecamatan'])
|
||||||
|
? $_POST['tipe'] : 'RT';
|
||||||
|
$q = "UPDATE wilayah SET nama='$nama', tipe='$tipe' WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Tipe tidak dikenal.']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $q)) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
// Menangkap ID masjid dan Radius baru
|
||||||
|
$id = $_POST['id'];
|
||||||
|
$radius_baru = $_POST['radius_m'];
|
||||||
|
|
||||||
|
// Update data radius di tabel masjid berdasarkan ID-nya
|
||||||
|
$query = "UPDATE masjid SET radius_m = '$radius_baru' WHERE id = '$id'";
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $query)) {
|
||||||
|
echo json_encode(['status' => 'success']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
+378
@@ -0,0 +1,378 @@
|
|||||||
|
<?php
|
||||||
|
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||||
|
include 'auth.php';
|
||||||
|
requireLogin();
|
||||||
|
$user = currentUser();
|
||||||
|
$isSuperAdmin = isSuperAdmin();
|
||||||
|
$myId = (int)($user['id'] ?? 0);
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Manajemen Pengguna — GeoMiskin</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f172a;--bg2:#1e293b;--bg3:#334155;--txt:#e2e8f0;--muted:#64748b;--blue:#3b82f6;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--indigo:#6366f1;--radius:12px;--trans:.2s ease}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Inter',sans-serif;min-height:100vh;background:var(--bg);color:var(--txt)}
|
||||||
|
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(59,130,246,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(59,130,246,.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none}
|
||||||
|
.page{max-width:960px;margin:0 auto;padding:40px 24px}
|
||||||
|
.topbar{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:36px;gap:16px;flex-wrap:wrap}
|
||||||
|
.topbar-left{display:flex;align-items:center;gap:16px}
|
||||||
|
.back-btn{display:inline-flex;align-items:center;gap:8px;background:var(--bg2);border:1px solid rgba(255,255,255,.08);color:var(--muted);padding:9px 16px;border-radius:10px;text-decoration:none;font-size:13px;font-weight:500;transition:var(--trans);white-space:nowrap}
|
||||||
|
.back-btn:hover{color:var(--txt);border-color:rgba(255,255,255,.18)}
|
||||||
|
.page-title{font-size:20px;font-weight:700;display:flex;align-items:center;gap:10px}
|
||||||
|
.page-title i{color:var(--blue)}
|
||||||
|
.page-sub{font-size:13px;color:var(--muted);margin-top:3px}
|
||||||
|
.btn-add{display:inline-flex;align-items:center;gap:8px;background:var(--blue);color:#fff;border:none;border-radius:10px;padding:10px 18px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
|
||||||
|
.btn-add:hover{background:#2563eb;transform:translateY(-1px)}
|
||||||
|
.card{background:var(--bg2);border:1px solid rgba(255,255,255,.06);border-radius:var(--radius);overflow:hidden}
|
||||||
|
table{width:100%;border-collapse:collapse}
|
||||||
|
thead tr{background:rgba(255,255,255,.03)}
|
||||||
|
th{padding:12px 16px;text-align:left;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);border-bottom:1px solid rgba(255,255,255,.06)}
|
||||||
|
td{padding:14px 16px;font-size:13px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle}
|
||||||
|
tbody tr:last-child td{border-bottom:none}
|
||||||
|
tbody tr:hover td{background:rgba(255,255,255,.02)}
|
||||||
|
.avatar-sm{width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;flex-shrink:0}
|
||||||
|
.user-row{display:flex;align-items:center;gap:12px}
|
||||||
|
.u-name{font-weight:600;font-size:13px}
|
||||||
|
.u-un{font-size:11px;color:var(--muted);margin-top:2px;font-family:monospace}
|
||||||
|
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600}
|
||||||
|
.badge-superadmin{background:rgba(99,102,241,.2);color:#a5b4fc}
|
||||||
|
.badge-admin{background:rgba(100,116,139,.2);color:#94a3b8}
|
||||||
|
.me-tag{display:inline-flex;align-items:center;gap:4px;font-size:10px;background:rgba(245,158,11,.15);color:#fbbf24;padding:2px 8px;border-radius:8px;margin-left:8px;font-weight:600}
|
||||||
|
.btn-sm{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;transition:var(--trans);border:none}
|
||||||
|
.btn-edit{background:rgba(59,130,246,.1);color:var(--blue);border:1px solid rgba(59,130,246,.2)}
|
||||||
|
.btn-edit:hover{background:rgba(59,130,246,.2)}
|
||||||
|
.btn-del{background:rgba(239,68,68,.1);color:var(--red);border:1px solid rgba(239,68,68,.2);margin-left:6px}
|
||||||
|
.btn-del:hover{background:rgba(239,68,68,.2)}
|
||||||
|
.state-row td{padding:48px;text-align:center;color:var(--muted)}
|
||||||
|
.state-row i{font-size:32px;display:block;margin-bottom:10px;opacity:.4}
|
||||||
|
.no-access{background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:10px;padding:20px;text-align:center;color:#fca5a5;font-size:14px;margin-bottom:24px}
|
||||||
|
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;padding:16px;z-index:999;opacity:0;pointer-events:none;transition:opacity .2s;backdrop-filter:blur(4px)}
|
||||||
|
.overlay.show{opacity:1;pointer-events:all}
|
||||||
|
.modal{background:var(--bg2);border:1px solid rgba(255,255,255,.08);border-radius:20px;width:100%;max-width:460px;transform:translateY(14px);transition:transform .2s;box-shadow:0 25px 60px rgba(0,0,0,.5)}
|
||||||
|
.overlay.show .modal{transform:translateY(0)}
|
||||||
|
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.06)}
|
||||||
|
.modal-head h3{font-size:16px;font-weight:700;display:flex;align-items:center;gap:8px}
|
||||||
|
.modal-head h3 i{color:var(--blue)}
|
||||||
|
.close-btn{background:none;border:none;color:var(--muted);cursor:pointer;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;transition:var(--trans)}
|
||||||
|
.close-btn:hover{background:rgba(255,255,255,.08);color:var(--txt)}
|
||||||
|
.modal-body{padding:20px 24px}
|
||||||
|
.modal-foot{padding:16px 24px;border-top:1px solid rgba(255,255,255,.06);display:flex;gap:10px;justify-content:flex-end}
|
||||||
|
.fg{margin-bottom:16px}
|
||||||
|
.fg label{display:block;font-size:12px;font-weight:600;color:var(--muted);margin-bottom:6px}
|
||||||
|
.fg label .req{color:var(--red);margin-left:2px}
|
||||||
|
.fg input,.fg select{width:100%;background:var(--bg);border:1.5px solid rgba(255,255,255,.08);border-radius:10px;padding:10px 14px;font-size:13px;font-family:inherit;color:var(--txt);outline:none;transition:var(--trans)}
|
||||||
|
.fg input:focus,.fg select:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(59,130,246,.15)}
|
||||||
|
.fg input::placeholder{color:var(--muted)}
|
||||||
|
.fg input:disabled{opacity:.45;cursor:not-allowed}
|
||||||
|
.fg select option{background:var(--bg2)}
|
||||||
|
.fg .hint{font-size:11px;color:var(--muted);margin-top:4px}
|
||||||
|
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||||
|
.btn-primary{background:var(--blue);color:#fff;border:none;border-radius:10px;padding:10px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
|
||||||
|
.btn-primary:hover{background:#2563eb}
|
||||||
|
.btn-secondary{background:rgba(255,255,255,.06);color:var(--txt);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:10px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
|
||||||
|
.btn-secondary:hover{background:rgba(255,255,255,.1)}
|
||||||
|
.toast-wrap{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px}
|
||||||
|
.toast{background:var(--bg2);border:1px solid rgba(255,255,255,.08);color:var(--txt);padding:12px 18px;border-radius:12px;font-size:13px;font-weight:500;box-shadow:0 8px 24px rgba(0,0,0,.4);display:flex;align-items:center;gap:10px;min-width:220px;animation:slideUp .25s ease;border-left:4px solid var(--blue)}
|
||||||
|
.toast.success{border-left-color:var(--green)}.toast.error{border-left-color:var(--red)}
|
||||||
|
@keyframes slideUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div class="topbar">
|
||||||
|
<div class="topbar-left">
|
||||||
|
<a href="index.php" class="back-btn"><i class="fas fa-arrow-left"></i> Kembali</a>
|
||||||
|
<div>
|
||||||
|
<div class="page-title"><i class="fas fa-users-cog"></i>Manajemen Pengguna</div>
|
||||||
|
<div class="page-sub">Login sebagai: <strong><?= htmlspecialchars($user['nama']) ?></strong> <span style="color:var(--muted)">(<?= $user['role'] ?>)</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if ($isSuperAdmin): ?>
|
||||||
|
<button class="btn-add" onclick="openAdd()"><i class="fas fa-user-plus"></i> Tambah Pengguna</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$isSuperAdmin): ?>
|
||||||
|
<div class="no-access"><i class="fas fa-lock" style="margin-right:8px"></i>Hanya superadmin yang dapat mengelola pengguna. Role kamu: <strong><?= $user['role'] ?></strong></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Pengguna</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Bergabung</th>
|
||||||
|
<?php if ($isSuperAdmin): ?><th style="text-align:right">Aksi</th><?php endif; ?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="user-tbody">
|
||||||
|
<tr class="state-row"><td colspan="<?= $isSuperAdmin ? 4 : 3 ?>">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>Memuat data...
|
||||||
|
</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast-wrap" class="toast-wrap"></div>
|
||||||
|
|
||||||
|
<!-- Modal Add / Edit (superadmin only) -->
|
||||||
|
<?php if ($isSuperAdmin): ?>
|
||||||
|
<div class="overlay" id="overlay" onclick="closeModal(event)">
|
||||||
|
<div class="modal" onclick="event.stopPropagation()">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3><i class="fas fa-user-edit"></i> <span id="modal-title">Tambah Pengguna</span></h3>
|
||||||
|
<button class="close-btn" onclick="closeModal()"><i class="fas fa-times"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="fg">
|
||||||
|
<label>Nama Lengkap <span class="req">*</span></label>
|
||||||
|
<input type="text" id="f-nama" placeholder="cth. Ahmad Fauzi">
|
||||||
|
</div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="fg">
|
||||||
|
<label>Username <span class="req">*</span></label>
|
||||||
|
<input type="text" id="f-username" placeholder="username" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Role <span class="req">*</span></label>
|
||||||
|
<select id="f-role">
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="superadmin">Superadmin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Password <span class="req" id="pass-req">*</span></label>
|
||||||
|
<input type="password" id="f-pass" placeholder="Minimal 6 karakter" autocomplete="new-password">
|
||||||
|
<div class="hint" id="pass-hint">Minimal 6 karakter.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn-secondary" onclick="closeModal()">Batal</button>
|
||||||
|
<button class="btn-primary" id="btn-save" onclick="saveUser()"><i class="fas fa-save"></i> Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const IS_SUPER = <?= $isSuperAdmin ? 'true' : 'false' ?>;
|
||||||
|
const MY_ID = <?= $myId ?>;
|
||||||
|
const COLORS = ['#3b82f6','#10b981','#f59e0b','#ef4444','#6366f1','#ec4899','#14b8a6','#8b5cf6'];
|
||||||
|
let editingId = null;
|
||||||
|
|
||||||
|
// ── avatar color (same hash as PHP version)
|
||||||
|
function avatarColor(name) {
|
||||||
|
let h = 0;
|
||||||
|
for (const c of (name || '?')) h = (h * 31 + c.charCodeAt(0)) % COLORS.length;
|
||||||
|
return COLORS[h];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── date format
|
||||||
|
function fmtDate(dt) {
|
||||||
|
if (!dt) return '—';
|
||||||
|
const d = new Date(dt);
|
||||||
|
const m = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'];
|
||||||
|
return d.getDate() + ' ' + m[d.getMonth()] + ' ' + d.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s||'').replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LOAD users via AJAX
|
||||||
|
async function loadUsers() {
|
||||||
|
document.getElementById('user-tbody').innerHTML =
|
||||||
|
`<tr class="state-row"><td colspan="${IS_SUPER?4:3}"><i class="fas fa-spinner fa-spin"></i>Memuat data...</td></tr>`;
|
||||||
|
try {
|
||||||
|
const r = await fetch('api/users.php?action=list');
|
||||||
|
const text = await r.text();
|
||||||
|
let d;
|
||||||
|
try { d = JSON.parse(text); }
|
||||||
|
catch(e) {
|
||||||
|
showTableError('Respons server tidak valid: ' + text.substring(0,80));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.status !== 'success') { showTableError('Error: ' + d.message); return; }
|
||||||
|
renderUsers(d.data || []);
|
||||||
|
} catch(e) {
|
||||||
|
showTableError('Gagal memuat: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTableError(msg) {
|
||||||
|
document.getElementById('user-tbody').innerHTML =
|
||||||
|
`<tr class="state-row"><td colspan="${IS_SUPER?4:3}">
|
||||||
|
<i class="fas fa-exclamation-circle" style="color:var(--red)"></i>
|
||||||
|
<div style="color:var(--red);font-size:13px;margin-top:4px">${esc(msg)}</div>
|
||||||
|
<button onclick="loadUsers()" style="margin-top:12px;background:var(--blue);color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:12px;cursor:pointer;font-family:inherit">Coba Lagi</button>
|
||||||
|
</td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUsers(rows) {
|
||||||
|
const cols = IS_SUPER ? 4 : 3;
|
||||||
|
if (!rows.length) {
|
||||||
|
document.getElementById('user-tbody').innerHTML =
|
||||||
|
`<tr class="state-row"><td colspan="${cols}"><i class="fas fa-users"></i>Belum ada pengguna</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('user-tbody').innerHTML = rows.map(u => {
|
||||||
|
const isMe = parseInt(u.id) === MY_ID;
|
||||||
|
const color = avatarColor(u.nama || u.username);
|
||||||
|
const init = (u.nama || u.username || '?')[0].toUpperCase();
|
||||||
|
const roleC = u.role === 'superadmin' ? 'badge-superadmin' : 'badge-admin';
|
||||||
|
const roleI = u.role === 'superadmin' ? 'shield-alt' : 'user';
|
||||||
|
const acols = IS_SUPER ? `
|
||||||
|
<td style="text-align:right;white-space:nowrap">
|
||||||
|
<button class="btn-sm btn-edit" onclick='openEdit(${JSON.stringify(u)})'>
|
||||||
|
<i class="fas fa-pen"></i> Edit
|
||||||
|
</button>
|
||||||
|
${!isMe ? `<button class="btn-sm btn-del" onclick="deleteUser(${u.id},'${esc(u.nama||u.username).replace(/'/g,"\\'")}')">
|
||||||
|
<i class="fas fa-trash"></i> Hapus
|
||||||
|
</button>` : ''}
|
||||||
|
</td>` : '';
|
||||||
|
return `<tr>
|
||||||
|
<td>
|
||||||
|
<div class="user-row">
|
||||||
|
<div class="avatar-sm" style="background:${color}">${init}</div>
|
||||||
|
<div>
|
||||||
|
<div class="u-name">${esc(u.nama||'—')}${isMe ? ' <span class="me-tag"><i class="fas fa-user"></i> Anda</span>' : ''}</div>
|
||||||
|
<div class="u-un">@${esc(u.username)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge ${roleC}"><i class="fas fa-${roleI}" style="font-size:9px"></i> ${esc(u.role)}</span></td>
|
||||||
|
<td style="color:var(--muted);font-size:12px">${fmtDate(u.created_at)}</td>
|
||||||
|
${acols}
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MODAL
|
||||||
|
function openAdd() {
|
||||||
|
editingId = null;
|
||||||
|
document.getElementById('modal-title').textContent = 'Tambah Pengguna';
|
||||||
|
document.getElementById('f-nama').value = '';
|
||||||
|
document.getElementById('f-username').value = '';
|
||||||
|
document.getElementById('f-pass').value = '';
|
||||||
|
document.getElementById('f-role').value = 'admin';
|
||||||
|
document.getElementById('f-username').disabled = false;
|
||||||
|
document.getElementById('pass-hint').textContent = 'Minimal 6 karakter.';
|
||||||
|
document.getElementById('pass-req').style.display = 'inline';
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
setTimeout(() => document.getElementById('f-nama').focus(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(u) {
|
||||||
|
if (typeof u === 'string') u = JSON.parse(u);
|
||||||
|
editingId = u.id;
|
||||||
|
document.getElementById('modal-title').textContent = 'Edit Pengguna';
|
||||||
|
document.getElementById('f-nama').value = u.nama || '';
|
||||||
|
document.getElementById('f-username').value = u.username || '';
|
||||||
|
document.getElementById('f-pass').value = '';
|
||||||
|
document.getElementById('f-role').value = u.role || 'admin';
|
||||||
|
document.getElementById('f-username').disabled = true;
|
||||||
|
document.getElementById('pass-hint').textContent = 'Kosongkan jika tidak ingin mengubah password.';
|
||||||
|
document.getElementById('pass-req').style.display = 'none';
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
setTimeout(() => document.getElementById('f-nama').focus(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(e) {
|
||||||
|
if (!e || e.target === document.getElementById('overlay'))
|
||||||
|
document.getElementById('overlay').classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SAVE
|
||||||
|
async function saveUser() {
|
||||||
|
const nama = document.getElementById('f-nama').value.trim();
|
||||||
|
const un = document.getElementById('f-username').value.trim();
|
||||||
|
const pass = document.getElementById('f-pass').value;
|
||||||
|
const role = document.getElementById('f-role').value;
|
||||||
|
if (!nama || !un) { toast('Nama dan username wajib diisi', 'error'); return; }
|
||||||
|
if (!editingId && pass.length < 6) { toast('Password minimal 6 karakter', 'error'); return; }
|
||||||
|
if (editingId && pass && pass.length < 6) { toast('Password minimal 6 karakter', 'error'); return; }
|
||||||
|
|
||||||
|
const btn = document.getElementById('btn-save');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Menyimpan...';
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', editingId ? 'update' : 'add');
|
||||||
|
fd.append('nama', nama);
|
||||||
|
fd.append('role', role);
|
||||||
|
if (editingId) { fd.append('id', editingId); if (pass) fd.append('password', pass); }
|
||||||
|
else { fd.append('username', un); fd.append('password', pass); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch('api/users.php', {method:'POST', body:fd});
|
||||||
|
const text = await r.text();
|
||||||
|
let d;
|
||||||
|
try { d = JSON.parse(text); }
|
||||||
|
catch(e) { toast('Server error: ' + text.substring(0,120), 'error'); return; }
|
||||||
|
if (d.status === 'success') {
|
||||||
|
closeModal();
|
||||||
|
toast(editingId ? 'Pengguna diperbarui!' : 'Pengguna ditambahkan!', 'success');
|
||||||
|
loadUsers();
|
||||||
|
} else {
|
||||||
|
toast('Error: ' + d.message, 'error');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
toast('Gagal: ' + e.message, 'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fas fa-save"></i> Simpan';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DELETE
|
||||||
|
async function deleteUser(id, name) {
|
||||||
|
if (!confirm(`Hapus pengguna "${name}"?\n\nTindakan ini tidak dapat dibatalkan.`)) return;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'delete');
|
||||||
|
fd.append('id', id);
|
||||||
|
try {
|
||||||
|
const r = await fetch('api/users.php', {method:'POST', body:fd});
|
||||||
|
const text = await r.text();
|
||||||
|
let d;
|
||||||
|
try { d = JSON.parse(text); }
|
||||||
|
catch(e) { toast('Server error: ' + text.substring(0,100), 'error'); return; }
|
||||||
|
if (d.status === 'success') {
|
||||||
|
toast('Pengguna berhasil dihapus', 'success');
|
||||||
|
loadUsers();
|
||||||
|
} else {
|
||||||
|
toast('Gagal: ' + d.message, 'error');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
toast('Gagal: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TOAST
|
||||||
|
function toast(msg, type='info') {
|
||||||
|
const icons = {success:'fa-check-circle', error:'fa-exclamation-circle', info:'fa-info-circle'};
|
||||||
|
const t = document.createElement('div');
|
||||||
|
t.className = `toast ${type}`;
|
||||||
|
t.innerHTML = `<i class="fas ${icons[type]||icons.info}"></i><span>${msg}</span>`;
|
||||||
|
document.getElementById('toast-wrap').appendChild(t);
|
||||||
|
setTimeout(() => { t.style.cssText='opacity:0;transform:translateY(8px);transition:.3s'; setTimeout(()=>t.remove(),300); }, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape') document.getElementById('overlay')?.classList.remove('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── init
|
||||||
|
loadUsers();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$type = $_POST['type'] ?? '';
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) { echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']); exit; }
|
||||||
|
|
||||||
|
function esc($conn, $v) { return mysqli_real_escape_string($conn, trim((string)$v)); }
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'spbu':
|
||||||
|
$nama = esc($conn, $_POST['nama'] ?? '');
|
||||||
|
$nomor = esc($conn, $_POST['nomor'] ?? '');
|
||||||
|
$status = in_array($_POST['status'] ?? '', ['Ya','Tidak']) ? $_POST['status'] : 'Tidak';
|
||||||
|
$lat = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lng = (float)($_POST['lng'] ?? 0);
|
||||||
|
$q = "UPDATE spbu SET nama='$nama', nomor='$nomor', status_24jam='$status', lat=$lat, lng=$lng WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'kemiskinan':
|
||||||
|
$nama_kk = esc($conn, $_POST['nama_kk'] ?? '');
|
||||||
|
$jumlah_kk = max(1, (int)($_POST['jumlah_kk'] ?? 1));
|
||||||
|
$alamat = esc($conn, $_POST['alamat'] ?? '');
|
||||||
|
$lat = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lng = (float)($_POST['lng'] ?? 0);
|
||||||
|
$pkh = (isset($_POST['pkh']) && $_POST['pkh'] == '1') ? 1 : 0;
|
||||||
|
$bpnt = (isset($_POST['bpnt']) && $_POST['bpnt'] == '1') ? 1 : 0;
|
||||||
|
$blt = (isset($_POST['blt']) && $_POST['blt'] == '1') ? 1 : 0;
|
||||||
|
|
||||||
|
$fotoSet = '';
|
||||||
|
if (!empty($_FILES['foto']['name']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg','jpeg','png','webp'];
|
||||||
|
if (in_array($ext, $allowed) && $_FILES['foto']['size'] <= 5*1024*1024) {
|
||||||
|
$dir = __DIR__ . '/uploads/kemiskinan/';
|
||||||
|
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||||
|
$fname = 'kk_' . $id . '_' . time() . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $dir.$fname)) {
|
||||||
|
$fotoSet = ", foto='" . esc($conn, $fname) . "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$q = "UPDATE kemiskinan SET nama_kk='$nama_kk', jumlah_kk=$jumlah_kk, alamat='$alamat',
|
||||||
|
lat=$lat, lng=$lng, pkh=$pkh, bpnt=$bpnt, blt=$blt{$fotoSet} WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'masjid':
|
||||||
|
$nama = esc($conn, $_POST['nama_masjid'] ?? '');
|
||||||
|
$pic = esc($conn, $_POST['pic'] ?? '');
|
||||||
|
$alamat = esc($conn, $_POST['alamat'] ?? '');
|
||||||
|
$radius = max(50, min(5000, (int)($_POST['radius_m'] ?? 300)));
|
||||||
|
$lat = (float)($_POST['lat'] ?? 0);
|
||||||
|
$lng = (float)($_POST['lng'] ?? 0);
|
||||||
|
$q = "UPDATE masjid SET nama_masjid='$nama', pic='$pic', alamat='$alamat',
|
||||||
|
radius_m=$radius, lat=$lat, lng=$lng WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'jalan':
|
||||||
|
$nama = esc($conn, $_POST['nama_jalan'] ?? '');
|
||||||
|
$status = in_array($_POST['status_jalan']??'', ['Nasional','Provinsi','Kabupaten']) ? $_POST['status_jalan'] : 'Kabupaten';
|
||||||
|
$panjang = (float)($_POST['panjang'] ?? 0);
|
||||||
|
$geojson = esc($conn, $_POST['geojson'] ?? '');
|
||||||
|
$q = "UPDATE jalan SET nama_jalan='$nama', status_jalan='$status', panjang_m=$panjang" .
|
||||||
|
($geojson ? ", geojson='$geojson'" : '') . " WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'persil':
|
||||||
|
$nomor = esc($conn, $_POST['nomor_persil'] ?? '');
|
||||||
|
$status = in_array($_POST['status_milik']??'', ['SHM','HGB','HGU','HP']) ? $_POST['status_milik'] : 'SHM';
|
||||||
|
$luas = (float)($_POST['luas'] ?? 0);
|
||||||
|
$geojson = esc($conn, $_POST['geojson'] ?? '');
|
||||||
|
$q = "UPDATE persil SET nomor_persil='$nomor', status_milik='$status', luas_m2=$luas" .
|
||||||
|
($geojson ? ", geojson='$geojson'" : '') . " WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'wilayah':
|
||||||
|
$nama = esc($conn, $_POST['nama'] ?? '');
|
||||||
|
$tipe = in_array($_POST['tipe']??'', ['RT','RW','Kelurahan','Kecamatan']) ? $_POST['tipe'] : 'RT';
|
||||||
|
$geojson = esc($conn, $_POST['geojson'] ?? '');
|
||||||
|
$q = "UPDATE wilayah SET nama='$nama', tipe='$tipe'" .
|
||||||
|
($geojson ? ", geojson='$geojson'" : '') . " WHERE id=$id";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Tipe tidak dikenal.']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mysqli_query($conn, $q)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include 'koneksi.php';
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$radius_baru = max(50, min(5000, (int)($_POST['radius_m'] ?? 300)));
|
||||||
|
|
||||||
|
if ($id <= 0) { echo json_encode(['status'=>'error','message'=>'ID tidak valid']); exit; }
|
||||||
|
|
||||||
|
$query = "UPDATE masjid SET radius_m = $radius_baru WHERE id = $id";
|
||||||
|
if (mysqli_query($conn, $query)) echo json_encode(['status' => 'success']);
|
||||||
|
else echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
|
||||||
Reference in New Issue
Block a user