first commit UasSIGWebgis

This commit is contained in:
raditarahman5-cloud
2026-06-09 18:19:10 +07:00
commit 6d0b24d2fe
25 changed files with 4616 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
CREATE DATABASE webgis_spbu;
USE webgis_spbu;
CREATE TABLE spbu (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(100),
no_hp VARCHAR(15),
kategori ENUM('24jam','Tidak 24jam'),
latitude DOUBLE,
longitude DOUBLE
);
INSERT INTO spbu (nama, no_hp, kategori, latitude, longitude) VALUES
('SPBU Jl. Imam Bonjol', '08110000001', '24jam', -0.0400, 109.3490),
('SPBU Kota Baru (Prof. M. Yamin)', '08110000002', '24jam', -0.0460, 109.3240),
('SPBU Jl. Tanjungpura', '08110000003', '24jam', -0.0305, 109.3444),
('SPBU Jl. Merdeka', '08110000004', '24jam', -0.0267, 109.3361),
('SPBU 28 Oktober', '08110000008', '24jam', -0.0031, 109.3667),
('SPBU Samping Makodam', '08110000009', '24jam', -0.0890, 109.3800),
('SPBU Jl. A. Yani (Samping DPRD)', '08110000005', 'Tidak 24jam', -0.0520, 109.3410),
('SPBU RE. Martadinata', '08110000006', 'Tidak 24jam', -0.0160, 109.3105),
('SPBU Gusti Hamzah', '08110000007', 'Tidak 24jam', -0.0350, 109.3225);
+94
View File
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>WebGIS Kemiskinan</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
<style>
body { margin:0; font-family:sans-serif; }
#map { height:100vh; }
#formBox {
position:absolute;
top:80px;
right:20px;
width:300px;
background:#1e1e1e;
color:white;
padding:15px;
border-radius:12px;
display:none;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="formBox">
<h3>Input Warga</h3>
<input type="text" id="nama" placeholder="Nama">
<textarea id="alamat"></textarea>
<input type="number" id="penghasilan">
<input type="hidden" id="lat">
<input type="hidden" id="lng">
<button onclick="simpanData()">Simpan</button>
</div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script>
var map = L.map('map').setView([-0.0263,109.3425],13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// LOAD DATA
fetch('api.php?action=get_data')
.then(r=>r.json())
.then(res=>{
res.warga.forEach(d=>{
L.marker([d.latitude,d.longitude]).addTo(map)
.bindPopup(d.nama);
});
res.ibadah.forEach(r=>{
L.marker([r.latitude,r.longitude]).addTo(map)
.bindPopup(r.nama);
});
});
// TAMBAH DATA
map.on('contextmenu',function(e){
formBox.style.display="block";
lat.value = e.latlng.lat;
lng.value = e.latlng.lng;
});
// SIMPAN
function simpanData(){
var fd = new FormData();
fd.append("action","simpan");
fd.append("nama",nama.value);
fd.append("alamat",alamat.value);
fd.append("penghasilan",penghasilan.value);
fd.append("lat",lat.value);
fd.append("lng",lng.value);
fetch('api.php',{method:'POST',body:fd})
.then(r=>r.json())
.then(res=>{
if(res.status=="success"){
alert("Berhasil!");
location.reload();
}
});
}
</script>
</body>
</html>
+197
View File
@@ -0,0 +1,197 @@
<?php
session_start();
$is_public_action = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'simpan_pelaporan');
$is_public_get = ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['data']));
require 'db.php';
// Helper function to execute query and handle JSON error
function executeQuery($conn, $sql) {
if (!$conn->query($sql)) {
echo json_encode(["status" => "error", "message" => "Terjadi kesalahan database: " . $conn->error]);
exit;
}
}
$role = $_SESSION['role'] ?? 'warga';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$act = $_POST['action'] ?? '';
$edit_id = (int)($_POST['edit_id'] ?? 0);
// Role validation
$allowed_infra_actions = ['simpan_spbu', 'simpan_jalan', 'simpan_parsil'];
$allowed_infra_tables = ['spbu', 'jalan', 'parsil'];
$allowed_sosial_actions = ['simpan_warga', 'simpan_masjid', 'simpan_pelaporan'];
$allowed_sosial_tables = ['warga', 'rumah_ibadah', 'pelaporan'];
$is_authorized = false;
if (in_array($role, ['admin', 'admin_infra', 'admin_sosial'])) {
$is_authorized = true;
} elseif ($role === 'warga' && $act === 'simpan_pelaporan') {
$is_authorized = true;
}
if (!$is_authorized) {
header('Content-Type: application/json');
echo json_encode(["status" => "error", "message" => "Akses ditolak. Anda tidak memiliki izin untuk tindakan atau data ini."]);
exit;
}
if ($act == "simpan_warga") {
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
$jkk = !empty($_POST['jumlah_kk']) ? (int)$_POST['jumlah_kk'] : 0;
$alamat = $conn->real_escape_string($_POST['alamat'] ?? '');
$lat = $conn->real_escape_string($_POST['lat'] ?? '');
$lng = $conn->real_escape_string($_POST['lng'] ?? '');
$tgl_lahir = !empty($_POST['tanggal_lahir']) ? "'" . $conn->real_escape_string($_POST['tanggal_lahir']) . "'" : "NULL";
$pend = $conn->real_escape_string($_POST['pendidikan'] ?? '');
$penyakit = $conn->real_escape_string($_POST['riwayat_penyakit'] ?? '');
$bantuan = $conn->real_escape_string($_POST['histori_bantuan'] ?? '');
$status_bantuan = $conn->real_escape_string($_POST['status_bantuan'] ?? 'Belum');
if ($edit_id > 0) {
$sql = "UPDATE warga SET nama='$nama', jumlah_kk=$jkk, alamat='$alamat', latitude='$lat', longitude='$lng', tanggal_lahir=$tgl_lahir, pendidikan='$pend', riwayat_penyakit='$penyakit', status_bantuan='$status_bantuan', histori_bantuan='$bantuan' WHERE id=$edit_id";
} else {
$sql = "INSERT INTO warga (nama, jumlah_kk, alamat, latitude, longitude, tanggal_lahir, pendidikan, riwayat_penyakit, status_bantuan, histori_bantuan)
VALUES ('$nama', $jkk, '$alamat', '$lat', '$lng', $tgl_lahir, '$pend', '$penyakit', '$status_bantuan', '$bantuan')";
}
executeQuery($conn, $sql);
}
elseif ($act == "simpan_pelaporan") {
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
$laporan = $conn->real_escape_string($_POST['laporan'] ?? '');
$lat = $conn->real_escape_string($_POST['lat'] ?? '');
$lng = $conn->real_escape_string($_POST['lng'] ?? '');
if ($edit_id > 0) {
$sql = "UPDATE pelaporan SET nama_pelapor='$nama', isi_laporan='$laporan', latitude='$lat', longitude='$lng' WHERE id=$edit_id";
} else {
$sql = "INSERT INTO pelaporan (nama_pelapor, isi_laporan, latitude, longitude) VALUES ('$nama', '$laporan', '$lat', '$lng')";
}
executeQuery($conn, $sql);
}
elseif ($act == "simpan_spbu") {
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
$no_hp = $conn->real_escape_string($_POST['no_hp'] ?? '');
$kategori = $conn->real_escape_string($_POST['kategori'] ?? '24jam');
$lat = $conn->real_escape_string($_POST['lat'] ?? '');
$lng = $conn->real_escape_string($_POST['lng'] ?? '');
if ($edit_id > 0) {
$sql = "UPDATE spbu SET nama='$nama', no_hp='$no_hp', kategori='$kategori', latitude='$lat', longitude='$lng' WHERE id=$edit_id";
} else {
$sql = "INSERT INTO spbu (nama, no_hp, kategori, latitude, longitude) VALUES ('$nama', '$no_hp', '$kategori', '$lat', '$lng')";
}
executeQuery($conn, $sql);
}
elseif ($act == "simpan_masjid") {
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
$jenis = $conn->real_escape_string($_POST['jenis'] ?? 'Masjid');
$pic = $conn->real_escape_string($_POST['pic'] ?? '');
$alamat = $conn->real_escape_string($_POST['alamat'] ?? '');
$lat = $conn->real_escape_string($_POST['lat'] ?? '');
$lng = $conn->real_escape_string($_POST['lng'] ?? '');
if ($edit_id > 0) {
$sql = "UPDATE rumah_ibadah SET nama='$nama', jenis='$jenis', pic='$pic', alamat='$alamat', latitude='$lat', longitude='$lng' WHERE id=$edit_id";
} else {
$sql = "INSERT INTO rumah_ibadah (nama, jenis, pic, alamat, latitude, longitude) VALUES ('$nama', '$jenis', '$pic', '$alamat', '$lat', '$lng')";
}
executeQuery($conn, $sql);
}
elseif ($act == "simpan_jalan") {
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
$status = $conn->real_escape_string($_POST['status_jalan'] ?? '');
$panjang = (float)($_POST['panjang'] ?? 0);
$koordinat = $conn->real_escape_string($_POST['koordinat'] ?? '');
if ($edit_id > 0) {
$sql = "UPDATE jalan SET nama_jalan='$nama', status_jalan='$status', panjang_m=$panjang, koordinat='$koordinat' WHERE id=$edit_id";
} else {
$sql = "INSERT INTO jalan (nama_jalan, status_jalan, panjang_m, koordinat) VALUES ('$nama', '$status', $panjang, '$koordinat')";
}
executeQuery($conn, $sql);
}
elseif ($act == "simpan_parsil") {
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
$status = $conn->real_escape_string($_POST['status_hak'] ?? '');
$koordinat = $conn->real_escape_string($_POST['koordinat'] ?? '');
if ($edit_id > 0) {
$sql = "UPDATE parsil SET nama_pemilik='$nama', status_hak='$status', koordinat='$koordinat' WHERE id=$edit_id";
} else {
$sql = "INSERT INTO parsil (nama_pemilik, status_hak, koordinat) VALUES ('$nama', '$status', '$koordinat')";
}
executeQuery($conn, $sql);
}
elseif ($act == "update_koordinat") {
$id = (int)($_POST['id'] ?? 0);
$tipe = $_POST['tipe'] ?? '';
$koordinat = $conn->real_escape_string($_POST['koordinat'] ?? '');
if ($id > 0 && $koordinat != '') {
if ($tipe == 'jalan') {
$panjang = (float)($_POST['panjang_m'] ?? 0);
$sql = "UPDATE jalan SET koordinat='$koordinat', panjang_m=$panjang WHERE id=$id";
executeQuery($conn, $sql);
} elseif ($tipe == 'parsil') {
$sql = "UPDATE parsil SET koordinat='$koordinat' WHERE id=$id";
executeQuery($conn, $sql);
}
}
}
elseif ($act == "hapus_data") {
$id = (int)($_POST['id'] ?? 0);
$tipe = $_POST['tipe'] ?? '';
$tabelMap = [
'warga' => 'warga',
'masjid' => 'rumah_ibadah',
'pelaporan' => 'pelaporan',
'spbu' => 'spbu',
'jalan' => 'jalan',
'parsil' => 'parsil'
];
$tabel = $tabelMap[$tipe] ?? '';
if($tabel && $id > 0) {
$sql = "DELETE FROM $tabel WHERE id='$id'";
executeQuery($conn, $sql);
}
}
echo json_encode(["status" => "ok"]);
exit;
}
// Handle GET for JSON data
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['data'])) {
header('Content-Type: application/json');
// Public data
$masjid = $conn->query("SELECT * FROM rumah_ibadah")->fetch_all(MYSQLI_ASSOC);
$pelaporan = $conn->query("SELECT * FROM pelaporan ORDER BY waktu DESC")->fetch_all(MYSQLI_ASSOC);
$spbu = $conn->query("SELECT * FROM spbu")->fetch_all(MYSQLI_ASSOC);
$response = [
'masjid' => $masjid,
'pelaporan' => $pelaporan,
'spbu' => $spbu
];
$warga = $conn->query("SELECT * FROM warga")->fetch_all(MYSQLI_ASSOC);
$jalan = $conn->query("SELECT * FROM jalan")->fetch_all(MYSQLI_ASSOC);
$parsil = $conn->query("SELECT * FROM parsil")->fetch_all(MYSQLI_ASSOC);
$response['warga'] = $warga;
$response['jalan'] = $jalan;
$response['parsil'] = $parsil;
echo json_encode($response);
exit;
}
?>
+244
View File
@@ -0,0 +1,244 @@
<?php
// ==========================================
// KONFIGURASI DATABASE (Ubah saat Deployment)
// ==========================================
$db_host = "localhost";
$db_user = "root";
$db_pass = "radit123";
$db_name = "sinergi_spasial_db"; // Database Pusat
// Matikan error expose ke frontend (Security)
mysqli_report(MYSQLI_REPORT_OFF);
$conn = new mysqli($db_host, $db_user, $db_pass);
if ($conn->connect_error) {
die("Error: Koneksi database gagal. Silakan periksa kredensial di db.php.");
}
// Buat database jika belum ada
$conn->query("CREATE DATABASE IF NOT EXISTS `$db_name`");
$conn->select_db($db_name);
// AUTO MIGRATIONS (Mencegah Error saat Deploy di Server Baru)
$queries = [
"CREATE TABLE IF NOT EXISTS warga (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150),
jumlah_kk INT,
alamat TEXT,
latitude VARCHAR(50),
longitude VARCHAR(50),
tanggal_lahir DATE NULL,
pendidikan VARCHAR(100) NULL,
riwayat_penyakit TEXT NULL,
status_bantuan ENUM('Sudah', 'Belum') DEFAULT 'Belum',
histori_bantuan TEXT NULL
)",
"CREATE TABLE IF NOT EXISTS rumah_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150),
jenis VARCHAR(50) DEFAULT 'Masjid',
pic VARCHAR(100),
alamat TEXT,
latitude VARCHAR(50),
longitude VARCHAR(50)
)",
"CREATE TABLE IF NOT EXISTS pelaporan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_pelapor VARCHAR(100),
isi_laporan TEXT,
latitude VARCHAR(50),
longitude VARCHAR(50),
waktu TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
"CREATE TABLE IF NOT EXISTS spbu (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150),
no_hp VARCHAR(20),
kategori ENUM('24jam', 'Tidak 24jam') DEFAULT '24jam',
latitude VARCHAR(50),
longitude VARCHAR(50)
)",
"CREATE TABLE IF NOT EXISTS jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(150),
status_jalan ENUM('Nasional', 'Provinsi', 'Kabupaten'),
panjang_m FLOAT,
koordinat TEXT
)",
"CREATE TABLE IF NOT EXISTS parsil (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_pemilik VARCHAR(150),
status_hak ENUM('SHM', 'HGB', 'HGU', 'HP'),
koordinat TEXT
)",
"CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('admin', 'warga') DEFAULT 'warga',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"
];
foreach ($queries as $q) {
$conn->query($q);
}
// Alter warga jika belum ada field tambahan
$cols = $conn->query("SHOW COLUMNS FROM warga LIKE 'tanggal_lahir'");
if($cols && $cols->num_rows == 0) {
$conn->query("ALTER TABLE warga ADD COLUMN tanggal_lahir DATE NULL");
$conn->query("ALTER TABLE warga ADD COLUMN pendidikan VARCHAR(100) NULL");
$conn->query("ALTER TABLE warga ADD COLUMN riwayat_penyakit TEXT NULL");
$conn->query("ALTER TABLE warga ADD COLUMN histori_bantuan TEXT NULL");
}
$cols_status = $conn->query("SHOW COLUMNS FROM warga LIKE 'status_bantuan'");
if($cols_status && $cols_status->num_rows == 0) {
$conn->query("ALTER TABLE warga ADD COLUMN status_bantuan ENUM('Sudah', 'Belum') DEFAULT 'Belum' AFTER riwayat_penyakit");
}
// Alter spbu jika menggunakan schema lama
$cols_spbu = $conn->query("SHOW COLUMNS FROM spbu LIKE 'kategori'");
if($cols_spbu && $cols_spbu->num_rows == 0) {
$conn->query("ALTER TABLE spbu CHANGE nama_spbu nama VARCHAR(150)");
$conn->query("ALTER TABLE spbu ADD COLUMN kategori ENUM('24jam', 'Tidak 24jam') DEFAULT '24jam'");
$conn->query("ALTER TABLE spbu DROP COLUMN is_24jam");
} else {
// Ubah definisi ENUM dan update data yang lama jika terlanjur menggunakan Aman/Tidak Aman
$conn->query("ALTER TABLE spbu MODIFY COLUMN kategori ENUM('Aman', 'Tidak Aman', '24jam', 'Tidak 24jam')");
$conn->query("UPDATE spbu SET kategori = '24jam' WHERE kategori = 'Aman'");
$conn->query("UPDATE spbu SET kategori = 'Tidak 24jam' WHERE kategori = 'Tidak Aman'");
$conn->query("ALTER TABLE spbu MODIFY COLUMN kategori ENUM('24jam', 'Tidak 24jam') DEFAULT '24jam'");
}
// Tambahkan kolom jenis di rumah_ibadah jika belum ada (Migration)
$check_jenis = $conn->query("SHOW COLUMNS FROM rumah_ibadah LIKE 'jenis'");
if ($check_jenis && $check_jenis->num_rows == 0) {
$conn->query("ALTER TABLE rumah_ibadah ADD COLUMN jenis VARCHAR(50) DEFAULT 'Masjid' AFTER nama");
}
// Alter users jika belum ada role
$cols_users = $conn->query("SHOW COLUMNS FROM users LIKE 'role'");
if($cols_users && $cols_users->num_rows == 0) {
$conn->query("ALTER TABLE users ADD COLUMN role ENUM('admin_infra', 'admin_sosial', 'admin', 'warga') DEFAULT 'warga' AFTER password");
} else {
// Modify ENUM if it already exists to include new roles
$conn->query("ALTER TABLE users MODIFY COLUMN role ENUM('admin_infra', 'admin_sosial', 'admin', 'warga') DEFAULT 'warga'");
}
// Seed SPBU data if empty
$check_spbu = $conn->query("SELECT COUNT(*) as count FROM spbu");
if ($check_spbu) {
$row = $check_spbu->fetch_assoc();
if ($row['count'] == 0) {
$conn->query("INSERT INTO spbu (nama, no_hp, kategori, latitude, longitude) VALUES
('SPBU Jl. Imam Bonjol', '08110000001', '24jam', '-0.0400', '109.3490'),
('SPBU Kota Baru (Prof. M. Yamin)', '08110000002', '24jam', '-0.0460', '109.3240'),
('SPBU Jl. Tanjungpura', '08110000003', '24jam', '-0.0305', '109.3444'),
('SPBU Jl. Merdeka', '08110000004', '24jam', '-0.0267', '109.3361'),
('SPBU 28 Oktober', '08110000008', '24jam', '-0.0031', '109.3667'),
('SPBU Samping Makodam', '08110000009', '24jam', '-0.0890', '109.3800'),
('SPBU Jl. A. Yani (Samping DPRD)', '08110000005', 'Tidak 24jam', '-0.0520', '109.3410'),
('SPBU RE. Martadinata', '08110000006', 'Tidak 24jam', '-0.0160', '109.3105'),
('SPBU Gusti Hamzah', '08110000007', 'Tidak 24jam', '-0.0350', '109.3225')");
}
}
// Seed Jalan data if empty
$check_jalan = $conn->query("SELECT COUNT(*) as count FROM jalan");
if ($check_jalan) {
$row = $check_jalan->fetch_assoc();
if ($row['count'] == 0) {
$jalan1 = '[{"lat":-0.0528,"lng":109.3409},{"lat":-0.0556,"lng":109.3432},{"lat":-0.0601,"lng":109.3463}]';
$jalan2 = '[{"lat":-0.0343,"lng":109.3431},{"lat":-0.0308,"lng":109.3411}]';
$conn->query("INSERT INTO jalan (nama_jalan, status_jalan, panjang_m, koordinat) VALUES
('Jl. Jenderal Ahmad Yani', 'Nasional', 1200, '$jalan1'),
('Jl. Gajah Mada', 'Provinsi', 800, '$jalan2')");
}
}
// Seed Parsil data if empty
$check_parsil = $conn->query("SELECT COUNT(*) as count FROM parsil");
if ($check_parsil) {
$row = $check_parsil->fetch_assoc();
if ($row['count'] == 0) {
$parsil1 = '[{"lat":-0.0520,"lng":109.3400},{"lat":-0.0520,"lng":109.3415},{"lat":-0.0535,"lng":109.3415},{"lat":-0.0535,"lng":109.3400}]';
$parsil2 = '[{"lat":-0.0468,"lng":109.3361},{"lat":-0.0468,"lng":109.3385},{"lat":-0.0487,"lng":109.3385},{"lat":-0.0487,"lng":109.3361}]';
$conn->query("INSERT INTO parsil (nama_pemilik, status_hak, koordinat) VALUES
('Kompleks Kantor Gubernur Kalbar', 'HP', '$parsil1'),
('Area Masjid Raya Mujahidin', 'SHM', '$parsil2')");
}
}
// Seed Masjid data if empty
$check_masjid = $conn->query("SELECT COUNT(*) as count FROM rumah_ibadah");
if ($check_masjid) {
$row = $check_masjid->fetch_assoc();
if ($row['count'] == 0) {
$conn->query("INSERT INTO rumah_ibadah (nama, jenis, pic, alamat, latitude, longitude) VALUES
('Masjid Raya Mujahidin', 'Masjid', 'Bapak Ahmad', 'Jl. Jenderal Ahmad Yani, Pontianak Selatan', '-0.0468', '109.3361'),
('Masjid Jami Keraton Kadriyah', 'Masjid', 'Bapak Syarif', 'Beting, Pontianak Timur', '-0.0215', '109.3528'),
('Masjid Darul Falah', 'Masjid', 'Bapak Budi', 'Jl. H. Rais A. Rahman', '-0.0354', '109.3242'),
('Gereja Katedral St. Yosef', 'Gereja', 'Romo Yohanes', 'Jl. Pattimura, Pontianak Kota', '-0.0385', '109.3400'),
('Vihara Bodhisatva Karaniya Metta', 'Vihara', 'Bapak Ahong', 'Jl. Kapten Marsan, Pontianak', '-0.0250', '109.3440')");
}
}
// Seed Warga data if empty
$check_warga = $conn->query("SELECT COUNT(*) as count FROM warga");
if ($check_warga) {
$row = $check_warga->fetch_assoc();
if ($row['count'] == 0) {
$conn->query("INSERT INTO warga (nama, jumlah_kk, alamat, latitude, longitude, tanggal_lahir, pendidikan, riwayat_penyakit, histori_bantuan) VALUES
('Bapak Rudi (Dekat Masjid)', 4, 'Jl. Sutoyo', '-0.0450', '109.3350', '1980-05-12', 'SMA', 'Hipertensi', 'Sembako 2025'),
('Ibu Siti (Jauh dari Fasilitas)', 3, 'Jl. Perdamaian', '-0.0650', '109.3000', '1975-08-22', 'SD', 'Asma', 'BLT 2024'),
('Bapak Ahmad (Dekat Gereja)', 5, 'Jl. Pattimura', '-0.0380', '109.3405', '1982-11-03', 'SMA', 'TBC', 'BLT 2025'),
('Ibu Aminah (Dekat Vihara)', 2, 'Jl. Kapten Marsan', '-0.0245', '109.3445', '1990-01-15', 'SMP', '', 'PKH 2026'),
('Kakek Joni (Pesisir Kapuas)', 1, 'Kawasan Pesisir', '-0.0150', '109.3300', '1955-06-30', 'Tidak Sekolah', 'Rematik', 'Raskin')");
}
}
// Seed Pelaporan data if empty
$check_pelaporan = $conn->query("SELECT COUNT(*) as count FROM pelaporan");
if ($check_pelaporan) {
$row = $check_pelaporan->fetch_assoc();
if ($row['count'] == 0) {
$conn->query("INSERT INTO pelaporan (nama_pelapor, isi_laporan, latitude, longitude) VALUES
('Pak Udin', 'Banjir parah air masuk ke rumah warga hingga lutut', '-0.0500', '109.3200'),
('Ibu Rina', 'Warga lansia sakit parah butuh ambulans darurat', '-0.0400', '109.3350'),
('Anonim', 'Jalanan ambles membahayakan pengendara', '-0.0250', '109.3600')");
}
}
// Pastikan akun admin_infra ada
$check_infra = $conn->query("SELECT * FROM users WHERE username='admin_infra'");
if ($check_infra && $check_infra->num_rows == 0) {
$pw = password_hash('infra123', PASSWORD_DEFAULT);
$conn->query("INSERT INTO users (username, password, role) VALUES ('admin_infra', '$pw', 'admin_infra')");
}
// Pastikan akun admin_sosial ada
$check_sosial = $conn->query("SELECT * FROM users WHERE username='admin_sosial'");
if ($check_sosial && $check_sosial->num_rows == 0) {
$pw = password_hash('sosial123', PASSWORD_DEFAULT);
$conn->query("INSERT INTO users (username, password, role) VALUES ('admin_sosial', '$pw', 'admin_sosial')");
}
// Pastikan akun admin (Superadmin lama) ada
$check_admin = $conn->query("SELECT * FROM users WHERE username='admin'");
if ($check_admin && $check_admin->num_rows == 0) {
$default_password = password_hash('admin123', PASSWORD_DEFAULT);
$conn->query("INSERT INTO users (username, password, role) VALUES ('admin', '$default_password', 'admin')");
} else {
$conn->query("UPDATE users SET role='admin' WHERE username='admin' AND role='warga'");
}
// Pastikan akun warga ada
$check_warga = $conn->query("SELECT * FROM users WHERE username='warga'");
if ($check_warga && $check_warga->num_rows == 0) {
$warga_password = password_hash('warga123', PASSWORD_DEFAULT);
$conn->query("INSERT INTO users (username, password, role) VALUES ('warga', '$warga_password', 'warga')");
}
?>
+9
View File
@@ -0,0 +1,9 @@
<?php
session_start();
if (!isset($_SESSION['login'])) { exit('Unauthorized'); }
require 'db.php';
$res = $conn->query('SELECT * FROM rumah_ibadah');
$rows = $res->fetch_all(MYSQLI_ASSOC);
echo "Total data: " . count($rows) . "\n";
print_r($rows);
?>
+45
View File
@@ -0,0 +1,45 @@
# Analisis dan Proses Bisnis WebGIS - SinergiSpasial Pro (Kota Pontianak)
## 1. Analisis Kode Saat Ini
Sistem sebelumnya merupakan WebGIS dasar yang memetakan penduduk miskin, rumah ibadah, dan infrastruktur menggunakan PHP Native dan Leaflet.js. Namun, data yang direkam terlalu minim (hanya nama, jumlah KK, alamat) sehingga belum mampu menjawab tantangan riil di lapangan. Sistem membutuhkan peningkatan kedalaman data serta jalur komunikasi yang efektif untuk merespons kasus mendesak secara *real-time*.
## 2. Proses Bisnis (Berdasarkan Kasus Terkini)
Berdasarkan isu krusial di Kota Pontianak seperti percepatan penanganan *stunting*, pengentasan kemiskinan ekstrem, dan monitoring kesehatan masyarakat (misal: kasus gizi buruk atau penyakit menular), proses bisnis dirancang ulang sebagai berikut:
**Aktor Terlibat:**
1. **Masyarakat:** Bertindak sebagai informan pertama. Jika melihat tetangga yang mengalami sakit keras tak tertangani atau gizi buruk, mereka bisa langsung melakukan pelaporan.
2. **Admin/Petugas Lapangan:** Bertugas mengelola data utama, memverifikasi laporan masyarakat, dan memperbarui profil warga secara mendetail.
3. **Walikota Pontianak (Eksekutif):** Bertindak sebagai pengambil kebijakan (*Decision Maker*). Walikota menggunakan WebGIS ini untuk memantau titik-titik krusial di Kota Pontianak dan menginstruksikan pendistribusian bantuan secara tepat sasaran.
**Alur Proses:**
- **Pelaporan:** Masyarakat menggunakan **"Pelaporan Cepat"** untuk menandai lokasi (koordinat) dan menceritakan kasus yang terjadi tanpa harus login.
- **Tindak Lanjut & Verifikasi:** Petugas melihat tanda *marker orange* (Laporan Darurat) di peta, mendatangi lokasi, dan mengonversi/memvalidasi laporan tersebut menjadi data **Warga Miskin/Rentan**.
- **Pengayaan Data:** Petugas menginput informasi lanjutan berupa:
- **Tanggal Lahir:** Untuk mendeteksi rentan usia (lansia) atau usia balita (rentan *stunting*).
- **Pendidikan Terakhir:** Indikator kapabilitas ekonomi.
- **Riwayat Penyakit:** Untuk prioritas penanganan medis (contoh: Tuberkulosis, Gizi Buruk).
- **Histori Bantuan:** Untuk mencegah *overlapping* bantuan, dicatat kapan terakhir bantuan seperti PKH, Sembako, atau KIS diberikan.
- **Distribusi dan Ketersediaan Fasilitas Umum (SPBU):** WebGIS juga memetakan lokasi-lokasi Stasiun Pengisian Bahan Bakar (SPBU) di Kota Pontianak untuk mengamankan logistik dan mobilitas. Sistem menggunakan fitur *Layer Groups and Layers Control* yang memungkinkan pengguna (End-User/Walikota) untuk melakukan _toggling_ (menampilkan/menyembunyikan) data SPBU Buka 24 Jam (Pin Hijau) dan SPBU Tidak Buka 24 Jam (Pin Merah) cukup melalui opsi *checkbox* di peta. Ini sangat mempercepat pengambilan keputusan di kondisi darurat tanpa harus memilah data secara manual.
- **Manajemen Infrastruktur (Polyline & Polygon):**
- **Data Jalan (Polyline):** Mengelola infrastruktur jalan dengan mengklasifikasikannya menjadi Jalan Nasional (Warna Merah), Jalan Provinsi (Warna Kuning), dan Jalan Kabupaten (Warna Hijau). Panjang jalan juga dikalkulasi *otomatis* oleh fungsi _LeafletJS distanceTo()_ tanpa perlu input manual yang beresiko *human error*.
- **Parsil Tanah / Kavling (Polygon):** Memetakan area pemukiman dengan status legalitas seperti Sertifikat Hak Milik (SHM), Hak Guna Bangunan (HGB), Hak Guna Usaha (HGU), dan Hak Pakai (HP).
- **Monitoring Walikota:** Walikota membuka dashboard, mengeklik titik-titik merah di peta, dan melihat *popup summary* yang menampilkan seluruh riwayat kehidupan dan bantuan warga tersebut, serta memantau sebaran fasilitas SPBU yang masih beroperasi (pin hijau) untuk kebutuhan darurat.
## 3. Implementasi Perbaikan pada Kode (`index.php`)
Fitur-fitur yang telah berhasil ditambahkan dan disesuaikan:
1. **Auto Migration Database:** Sistem kini otomatis membuat tabel `pelaporan` dan menambahkan kolom baru ke tabel `warga` (`tanggal_lahir`, `pendidikan`, `riwayat_penyakit`, `histori_bantuan`).
2. **Fitur Pelaporan Cepat (Masyarakat):** Tambahan form input khusus bagi masyarakat untuk melapor.
3. **Penyempurnaan Form Input Warga:** Penambahan input Tanggal Lahir, Pendidikan, Riwayat Penyakit, dan Histori Bantuan.
4. **Dashboard Eksekutif Walikota:** UI disesuaikan dengan menampilkan *badge* Pantauan Walikota, serta list Laporan Darurat di *sidebar*.
5. **Implementasi Leaflet Layers Control (Filter SPBU):** Layer peta telah dipecah secara spesifik (`layerSpbu24` dan `layerSpbuNon24`). Terdapat panel interaktif (Layers Control) di sudut layar untuk menampilkan/menyembunyikan SPBU Buka 24 jam (Hijau) dan Non-24 Jam (Merah) via *checkbox*. Fungsionalitas *Layer Control* ini juga diaplikasikan pada semua dataset lain secara komprehensif.
6. **Pemetaan Jalan & Parsil Terpadu:** Logika Polyline diperbarui dengan perhitungan panjang matriks otomatis. Warna *polyline* langsung menyesuaikan status jalan. Selain itu, manajemen Kavling Tanah (Polygon) memuat detail SHM/HGB/HGU/HP.
7. **Arsitektur Modular Terpisah (Clean Code):** Mengabulkan kebutuhan arsitektur yang terpisah, seluruh *source code* yang menumpuk di `index.php` kini telah dipecah menjadi 5 file demi performa tinggi:
- `db.php` (Fokus Koneksi & Auto Migration DB)
- `api.php` (Fokus Backend Endpoint / JSON Producer)
- `index.php` (Fokus Tampilan View UI)
- `script.js` (Fokus Front-end Leaflet Logic)
- `style.css` (Fokus Desain Antarmuka)
8. **Popup Dinamis:** *Marker* pada Leaflet kini menampilkan secara komprehensif riwayat penyakit, histori bantuan warga, serta jam operasional dan kontak SPBU. Center map telah diatur secara akurat pada area Kota Pontianak.
9. **Dashboard Statistik Real-Time:** Penambahan blok statistik numerik di bagian atas sidebar untuk memudahkan Walikota melihat total (Agregat) Laporan, Warga, SPBU, dan Infrastruktur secara sekilas.
10. **Sistem Notifikasi Modern (SweetAlert2):** Notifikasi bawaan *browser* (alert/confirm) yang kaku telah digantikan dengan *SweetAlert2* untuk animasi penyimpanan dan penghapusan data yang profesional.
11. **Pelacakan GPS (Find My Location):** Penambahan kontrol navigasi (ikon *crosshairs*) yang menggunakan API Geolocation HTML5 untuk mendeteksi dan mengunci koordinat perangkat pengguna secara *real-time* di peta.
+5
View File
@@ -0,0 +1,5 @@
<?php
// Root router: Redirects all traffic to the portal repository
header("Location: portal/index.php");
exit;
?>
+134
View File
@@ -0,0 +1,134 @@
<?php
session_start();
require_once '../api/db.php';
?>
<!DOCTYPE html>
<html>
<head>
<title>WebGIS Poverty Mapping - Dashboard Infrastruktur</title>
<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"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"/>
<link rel="stylesheet" href="style.css"/>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body>
<button class="menu-toggle" onclick="toggleSidebar()" title="Buka Dashboard Eksekutif"><i class="fas fa-bars"></i></button>
<button onclick="window.location.href='../portal/index.php'" style="position: absolute; top: 15px; left: 65px; z-index: 1200; background: white; color: var(--primary); border: 2px solid var(--primary); padding: 10px 20px; border-radius: 30px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.1); font-size: 0.9rem; transition: all 0.3s ease; display: flex; align-items: center; gap: 8px;" onmouseover="this.style.background='var(--primary)'; this.style.color='white';" onmouseout="this.style.background='white'; this.style.color='var(--primary)';">
<i class="fas fa-arrow-left"></i> Kembali ke Dashboard
</button>
<button class="menu-toggle" style="left: auto; right: 20px; background: var(--primary);" onclick="toggleRightPanel()" title="Buka Form Input">
<i class="fas fa-pen"></i>
</button>
<div class="search-container">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" placeholder="Cari data GIS..." onkeyup="filterData()">
</div>
</div>
<div id="sidebar">
<div class="sidebar-header">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<h3><i class="fas fa-road"></i> Peta Infrastruktur</h3>
<small><i class="fas fa-hard-hat"></i> Dashboard Admin Infra</small>
</div>
<div style="display: flex; gap: 8px;">
<a href="../portal/index.php" title="Kembali ke Portal Utama" style="color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onmouseover="this.style.background='rgba(59, 130, 246, 0.8)'" onmouseout="this.style.background='rgba(255,255,255,0.2)'">
<i class="fas fa-home"></i>
</a>
<a href="#" id="notifBell" title="Notifikasi Laporan Warga" style="position: relative; color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onclick="toggleSidebar()">
<i class="fas fa-bell"></i>
<span id="notifBadge" style="display: none; position: absolute; top: -5px; right: -5px; background: #ef4444; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.6rem; font-weight: bold; border: 2px solid #1e293b; animation: bounce 1s infinite;">0</span>
</a>
<a href="../portal/logout.php" title="Logout" style="color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onmouseover="this.style.background='rgba(239, 68, 68, 0.8)'" onmouseout="this.style.background='rgba(255,255,255,0.2)'">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
</div>
</div>
<div class="sidebar-content" id="sidebarContent">
<!-- Dirender secara dinamis oleh script.js -->
<small>Memuat data...</small>
</div>
</div>
<div id="map"></div>
<div class="panel-input" id="panelInput">
<div style="display:flex; justify-content:space-between; align-items:center; border-bottom: 2px solid var(--border); padding-bottom: 10px; margin-bottom: 15px;">
<h4 style="margin:0; border:none; padding:0; font-size:1.1rem; color:var(--dark);">Form Input & Laporan</h4>
<button onclick="toggleRightPanel()" style="background:none; border:none; cursor:pointer; font-size:1.2rem; color:#94a3b8; transition:0.2s;" onmouseover="this.style.color='#ef4444'" onmouseout="this.style.color='#94a3b8'"><i class="fas fa-times"></i></button>
</div>
<div class="radius-box" style="display:none;">
<label>📡 Radius Jangkauan / Bantuan: <span id="radiusVal">500</span>m</label>
<input type="range" id="radiusRange" min="100" max="5000" value="500" step="100" oninput="changeRadius(this.value)">
</div>
<select id="jenis" onchange="toggleForm()">
<option value="spbu">⛽ SPBU (Bahan Bakar)</option>
<option value="jalan">🛣️ Jalan (Polyline)</option>
<option value="parsil">🟩 Parsil Tanah (Polygon)</option>
</select>
<!-- FORM SPBU -->
<div id="form_spbu">
<input type="text" id="nama_spbu" placeholder="Nama SPBU (Misal: SPBU Ahmad Yani)">
<input type="text" id="no_hp_spbu" placeholder="Nomor Handphone SPBU">
<select id="kategori_spbu">
<option value="">- Pilih Kategori SPBU -</option>
<option value="24jam">Buka 24 Jam</option>
<option value="Tidak 24jam">Tidak 24 Jam</option>
</select>
</div>
<!-- FORM JALAN -->
<div id="form_jalan" class="hidden">
<p style="font-size:0.7rem; color:red">*Gunakan tool Gambar Polyline di peta terlebih dahulu</p>
<input type="text" id="nama_jalan" placeholder="Nama Jalan">
<select id="status_jalan">
<option value="">- Pilih Status Jalan -</option>
<option value="Nasional">Jalan Nasional (Merah)</option>
<option value="Provinsi">Jalan Provinsi (Kuning)</option>
<option value="Kabupaten">Jalan Kabupaten (Hijau)</option>
</select>
<label style="font-size:0.75rem; color:#666;">Panjang Jalan (Otomatis Leaflet):</label>
<input type="text" id="panjang_jalan" readonly style="background:#f0f0f0">
<input type="hidden" id="jalan_coords">
</div>
<!-- FORM PARSIL -->
<div id="form_parsil" class="hidden">
<p style="font-size:0.7rem; color:red">*Gunakan tool Gambar Polygon di peta terlebih dahulu</p>
<input type="text" id="nama_pemilik" placeholder="Nama Pemilik Tanah">
<select id="status_hak">
<option value="">- Pilih Status Kepemilikan -</option>
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Sertifikat Hak Pakai (HP)</option>
</select>
<input type="hidden" id="parsil_coords">
</div>
<input type="hidden" id="lat"><input type="hidden" id="lng"><input type="hidden" id="edit_id" value="0">
<div style="display:flex; gap:10px;">
<button class="btn-save" id="btnSimpanUtama" onclick="simpanData()" style="flex:1;">Simpan Data</button>
<button class="btn-del" id="btnBatalEdit" onclick="batalEdit()" style="display:none; padding:14px; border-radius:10px; font-weight:700;">Batal</button>
</div>
</div>
<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>
// Penanda untuk script.js agar tahu konteks dashboard
window.dashboardType = 'infra';
</script>
<script src="script.js"></script>
</body>
</html>
+749
View File
@@ -0,0 +1,749 @@
var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Layer Groups yang Spesifik
var layerMasjid = L.layerGroup().addTo(map);
var layerMasjidRadius = L.layerGroup().addTo(map);
var layerPelaporan = L.layerGroup().addTo(map);
var layerWarga = L.layerGroup().addTo(map);
var layerSpbu24 = L.layerGroup().addTo(map);
var layerSpbuNon24 = L.layerGroup().addTo(map);
var layerLines = L.layerGroup().addTo(map);
var layerPolygons = L.layerGroup().addTo(map);
// Layers Control
var overlays = {};
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
overlays["🕌 Tempat Ibadah"] = layerMasjid;
overlays["📡 Radius Ibadah/Bantuan"] = layerMasjidRadius;
overlays["🚨 Pelaporan Darurat"] = layerPelaporan;
overlays["📍 Warga Rentan"] = layerWarga;
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
overlays["⛽ SPBU 24 Jam (Hijau)"] = layerSpbu24;
overlays["⛽ SPBU Terbatas (Merah)"] = layerSpbuNon24;
overlays["🛣️ Jalan (Polyline)"] = layerLines;
overlays["🟩 Kavling (Polygon)"] = layerPolygons;
}
L.control.layers(null, overlays, { collapsed: false, position: 'topright' }).addTo(map);
var drawnItems = new L.FeatureGroup().addTo(map);
var currentRadius = 500;
var cachedData = null;
var drawControl = new L.Control.Draw({
draw: {
marker:false, circle:false, circlemarker:false,
polyline: { shapeOptions: { color: 'blue', weight: 4 } },
polygon: { shapeOptions: { color: 'red' } },
rectangle: false
},
edit: { featureGroup: drawnItems }
});
map.addControl(drawControl);
// Locate Control
var locateBtn = L.control({position: 'topleft'});
locateBtn.onAdd = function (map) {
var div = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
div.style.backgroundColor = 'white';
div.style.width = '34px';
div.style.height = '34px';
div.style.cursor = 'pointer';
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.justifyContent = 'center';
div.innerHTML = '<i class="fas fa-crosshairs" style="font-size:1.2rem; color:#2563eb;"></i>';
div.title = "Lokasi Anda Saat Ini";
div.onclick = function(){ map.locate({setView: true, maxZoom: 16}); }
return div;
};
locateBtn.addTo(map);
map.on('locationfound', function(e) {
L.circleMarker(e.latlng, {radius: 8, fillColor: '#3b82f6', color: '#fff', weight: 3, fillOpacity: 1}).addTo(map).bindPopup("Lokasi Anda").openPopup();
});
// Event Draw
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var type = e.layerType;
drawnItems.addLayer(layer);
var panel = document.getElementById('panelInput');
if (panel) panel.classList.remove('collapsed');
if (type === 'polyline') {
document.getElementById('jenis').value = 'jalan';
toggleForm();
var coords = layer.getLatLngs();
document.getElementById('jalan_coords').value = JSON.stringify(coords);
// Calculate Length automatically
var length = 0;
for (var i = 0; i < coords.length - 1; i++) {
length += coords[i].distanceTo(coords[i + 1]);
}
document.getElementById('panjang_jalan').value = length.toFixed(2);
} else if (type === 'polygon') {
document.getElementById('jenis').value = 'parsil';
toggleForm();
var coords = layer.getLatLngs()[0];
document.getElementById('parsil_coords').value = JSON.stringify(coords);
}
});
map.on(L.Draw.Event.EDITED, function (e) {
var layers = e.layers;
layers.eachLayer(function (layer) {
if(layer.db_id && layer.db_tipe) {
var coords = (layer.db_tipe === 'parsil') ? layer.getLatLngs()[0] : layer.getLatLngs();
var fd = new FormData();
fd.append('action', 'update_koordinat');
fd.append('id', layer.db_id);
fd.append('tipe', layer.db_tipe);
fd.append('koordinat', JSON.stringify(coords));
if(layer.db_tipe === 'jalan') {
var length = 0;
for (var i = 0; i < coords.length - 1; i++) {
length += coords[i].distanceTo(coords[i + 1]);
}
fd.append('panjang_m', length.toFixed(2));
}
fetch('../api/api.php', { method: 'POST', body: fd });
}
});
Swal.fire({icon: 'success', title: 'Tersimpan', text: 'Perubahan bentuk berhasil disimpan!', timer: 1500, showConfirmButton: false});
});
map.on(L.Draw.Event.DELETED, function (e) {
var layers = e.layers;
layers.eachLayer(function (layer) {
if(layer.db_id && layer.db_tipe) {
var fd = new FormData();
fd.append('action', 'hapus_data');
fd.append('id', layer.db_id);
fd.append('tipe', layer.db_tipe);
fetch('../api/api.php', { method: 'POST', body: fd });
}
});
Swal.fire({icon: 'success', title: 'Terhapus', text: 'Data berhasil dihapus dari peta!', timer: 1500, showConfirmButton: false});
});
function toggleSidebar() { document.getElementById('sidebar').classList.toggle('active'); }
function toggleRightPanel() {
var panel = document.getElementById('panelInput');
if(panel) panel.classList.toggle('collapsed');
}
function filterData() {
var input = document.getElementById('searchInput').value.toLowerCase();
var cards = document.getElementsByClassName('item-data');
if(input.length > 0) document.getElementById('sidebar').classList.add('active');
for (var i = 0; i < cards.length; i++) {
cards[i].style.display = cards[i].getAttribute('data-nama').includes(input) ? "" : "none";
}
}
function changeRadius(v) {
currentRadius = parseInt(v);
document.getElementById('radiusVal').innerText = v;
if(cachedData) renderMap(cachedData);
}
var prevReportCount = -1;
function loadData(isPolling = false) {
fetch('../api/api.php?data=all')
.then(r => r.json())
.then(data => {
cachedData = data;
// Logika Notifikasi Bubble
if (prevReportCount !== -1 && data.pelaporan.length > prevReportCount) {
var newCount = data.pelaporan.length - prevReportCount;
Swal.fire({
title: 'Laporan Baru!',
text: `Ada ${newCount} laporan warga baru yang masuk.`,
icon: 'info',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000
});
}
prevReportCount = data.pelaporan.length;
var badge = document.getElementById('notifBadge');
if(badge) {
if(data.pelaporan.length > 0) {
badge.style.display = 'block';
badge.innerText = data.pelaporan.length;
} else {
badge.style.display = 'none';
}
}
if (!isPolling) {
renderSidebar(data);
renderMap(data);
} else {
// If polling, maybe just update sidebar if it's open, or wait for manual refresh
// But for real-time map, we can re-render map as well (might cause flicker if doing heavy edits)
// We'll skip re-rendering map to avoid interrupting user edits, just update the bubble
renderSidebar(data);
renderMap(data);
}
});
}
// Auto-poll every 10 seconds
setInterval(() => {
// Only poll if not currently editing (edit_id is 0)
if(document.getElementById('edit_id') && document.getElementById('edit_id').value === "0") {
loadData(true);
}
}, 10000);
function renderSidebar(data) {
var html = `<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:20px">`;
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
html += `
<div style="background:#fee2e2; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#ef4444; font-size:1.5rem">${data.pelaporan.length}</b><br><small style="color:#991b1b; font-weight:600">Laporan</small>
</div>
<div style="background:#fef3c7; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#d97706; font-size:1.5rem">${data.warga.length}</b><br><small style="color:#92400e; font-weight:600">Warga Rentan</small>
</div>`;
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
html += `
<div style="background:#dcfce7; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#16a34a; font-size:1.5rem">${data.spbu.length}</b><br><small style="color:#166534; font-weight:600">SPBU Terdata</small>
</div>
<div style="background:#e0e7ff; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#4f46e5; font-size:1.5rem">${data.jalan.length}</b><br><small style="color:#3730a3; font-weight:600">Jalur Jalan</small>
</div>`;
}
html += `</div>`;
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Pelaporan
html += `<div class="section-title" style="color:#ef4444">🚨 Laporan Darurat</div>`;
if(data.pelaporan.length === 0) html += `<small style="color:#666">Belum ada laporan</small>`;
data.pelaporan.forEach(p => {
html += `<div class="card item-data" data-nama="${p.nama_pelapor.toLowerCase()} ${p.isi_laporan.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${p.latitude}, ${p.longitude})" style="cursor:pointer">
<b>${p.nama_pelapor}</b><small><i class="far fa-clock"></i> ${p.waktu}</small><br><span>${p.isi_laporan.substring(0,40)}...</span>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('pelaporan', ${p.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${p.id}, 'pelaporan')">Hapus</button>
</div>
</div>
</div>`;
});
// Warga
html += `<div class="section-title">📍 Penduduk Miskin / Rentan</div>`;
data.warga.forEach(w => {
html += `<div class="card item-data" data-nama="${w.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${w.latitude}, ${w.longitude})" style="cursor:pointer">
<b>${w.nama}</b>
${w.riwayat_penyakit ? '<span class="badge" style="background:#fef3c7; color:#d97706;"><i class="fas fa-heartbeat"></i> Medis</span>' : ''}
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('warga', ${w.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${w.id}, 'warga')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// SPBU
html += `<div class="section-title">⛽ Stasiun Pengisian (SPBU)</div>`;
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
html += `<div class="card item-data" data-nama="${s.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${s.latitude}, ${s.longitude})" style="cursor:pointer">
<b>${s.nama}</b>
<small><i class="fas fa-phone"></i> ${s.no_hp}</small><br>
${is24 ? '<span class="badge" style="background:#dcfce7; color:#16a34a;">Buka 24 Jam</span>' : '<span class="badge" style="background:#fee2e2; color:#ef4444;">Tidak 24 Jam</span>'}
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('spbu', ${s.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${s.id}, 'spbu')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Tempat Ibadah
html += `<div class="section-title">🕌 Tempat Ibadah & Bantuan</div>`;
data.masjid.forEach(m => {
html += `<div class="card item-data" data-nama="${m.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${m.latitude}, ${m.longitude})" style="cursor:pointer"><b>${m.nama}</b></span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('masjid', ${m.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${m.id}, 'masjid')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Jalan
html += `<div class="section-title">🛣️ Manajemen Jalan</div>`;
data.jalan.forEach(j => {
var color = '#ef4444'; // default red
if(j.status_jalan === 'Nasional') color = '#ef4444';
else if(j.status_jalan === 'Provinsi') color = '#eab308';
else if(j.status_jalan === 'Kabupaten') color = '#22c55e';
html += `<div class="card item-data" data-nama="${j.nama_jalan.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span>
<b>${j.nama_jalan}</b>
<small style="color:${color}; font-weight:bold;">${j.status_jalan}</small><br>
<small>${j.panjang_m} meter</small>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('jalan', ${j.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${j.id}, 'jalan')">Hapus</button>
</div>
</div>
</div>`;
});
// Parsil
html += `<div class="section-title">🟩 Manajemen Kavling / Parsil</div>`;
data.parsil.forEach(p => {
html += `<div class="card item-data" data-nama="${p.nama_pemilik.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span>
<b>${p.nama_pemilik}</b><br>
<small>Status: <b>${p.status_hak}</b></small>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('parsil', ${p.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${p.id}, 'parsil')">Hapus</button>
</div>
</div>
</div>`;
});
}
document.getElementById('sidebarContent').innerHTML = html;
}
function renderMap(data) {
layerMasjid.clearLayers();
layerMasjidRadius.clearLayers();
layerPelaporan.clearLayers();
layerWarga.clearLayers();
layerSpbu24.clearLayers();
layerSpbuNon24.clearLayers();
layerLines.clearLayers();
layerPolygons.clearLayers();
var lokMasjid = data.masjid.map(m => L.latLng(m.latitude, m.longitude));
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Render Masjid / Tempat Ibadah
data.masjid.forEach(m => {
var popup = `<b>${m.jenis || 'Tempat Ibadah'}:</b> ${m.nama}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('masjid', ${m.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${m.id}, 'masjid')">Hapus</button>
</div>`;
var faClass = 'fa-mosque';
if(m.jenis === 'Gereja') faClass = 'fa-church';
else if(m.jenis === 'Vihara') faClass = 'fa-vihara';
else if(m.jenis === 'Pura') faClass = 'fa-torii-gate';
// Buat custom marker bentuk map pin dengan icon sesuai jenis
var masjidIcon = L.divIcon({
html: `
<div style="
width: 34px;
height: 34px;
background-color: #2563eb;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas ${faClass}" style="
transform: rotate(45deg);
color: white;
font-size: 14px;
margin-top: 2px;
margin-left: -2px;
"></i>
</div>
`,
className: '', // Hilangkan background default Leaflet
iconSize: [34, 34],
iconAnchor: [17, 40], // Anchor ujung lancip di bawah
popupAnchor: [0, -40]
});
L.marker([m.latitude, m.longitude], { icon: masjidIcon }).addTo(layerMasjid).bindPopup(popup);
L.circle([m.latitude, m.longitude], {
radius: currentRadius, color: '#2563eb', fillOpacity: 0.1, weight: 1
}).addTo(layerMasjidRadius);
});
// Render Pelaporan
data.pelaporan.forEach(p => {
var popup = `<b>🚨 Laporan:</b> ${p.isi_laporan}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('pelaporan', ${p.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${p.id}, 'pelaporan')">Hapus</button>
</div>`;
var pelaporanIcon = L.divIcon({
html: `
<div style="
width: 32px;
height: 32px;
background-color: #f97316; /* Orange color */
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-exclamation" style="
transform: rotate(45deg);
color: white;
font-size: 16px;
font-weight: bold;
margin-top: 2px;
margin-left: -2px;
"></i>
</div>
`,
className: '',
iconSize: [32, 32],
iconAnchor: [16, 38],
popupAnchor: [0, -38]
});
L.marker([p.latitude, p.longitude], {
icon: pelaporanIcon
}).addTo(layerPelaporan).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Render SPBU
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
var iconUrl = is24 ? 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png' : 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png';
var targetLayer = is24 ? layerSpbu24 : layerSpbuNon24;
var popup = `<b>⛽ ${s.nama}</b><br>Kategori: ${s.kategori === '24jam' ? 'Buka 24 Jam' : 'Tidak 24 Jam'}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('spbu', ${s.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${s.id}, 'spbu')">Hapus</button>
</div>`;
L.marker([s.latitude, s.longitude], {
icon: L.icon({ iconUrl: iconUrl, iconSize: [25, 41], iconAnchor: [12, 41] })
}).addTo(targetLayer).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Render Warga
data.warga.forEach(w => {
var pos = L.latLng(w.latitude, w.longitude);
var isCovered = lokMasjid.some(m => pos.distanceTo(m) <= currentRadius);
// Buat elemen point berbentuk map pin (teardrop) dengan ikon rumah di dalamnya
var color = isCovered ? '#16a34a' : '#ef4444'; // Hijau aman, merah di luar
var houseIcon = L.divIcon({
html: `
<div style="
width: 30px;
height: 30px;
background-color: ${color};
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
">
<div style="
width: 18px;
height: 18px;
background-color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-home" style="
transform: rotate(45deg);
color: ${color};
font-size: 10px;
"></i>
</div>
</div>
`,
className: '', // Kosongkan agar background default leaflet tidak muncul
iconSize: [30, 30],
iconAnchor: [15, 36], // Titik jangkar di ujung bawah pin
popupAnchor: [0, -36]
});
var isPenerima = (w.histori_bantuan && w.histori_bantuan.trim().length > 0) ? 'Ya' : 'Belum';
var txtPendidikan = w.pendidikan ? w.pendidikan : 'Tidak diketahui';
var txtPenyakit = (w.riwayat_penyakit && w.riwayat_penyakit.trim().length > 0) ? w.riwayat_penyakit : 'Tidak ada';
var popup = `
<div style="min-width: 200px;">
<b style="font-size: 1.1em; color: #1e293b;"><i class="fas fa-user-circle"></i> Warga: ${w.nama}</b>
<hr style="margin: 5px 0; border: 0; border-top: 1px solid #cbd5e1;">
<table style="width: 100%; font-size: 0.85em; margin-bottom: 8px;">
<tr><td style="color: #64748b; padding-right: 10px;">Jangkauan</td><td>: <b>${isCovered ? '<span style="color:#16a34a">Aman (Dekat Fasilitas)</span>' : '<span style="color:#ef4444">Di Luar</span>'}</b></td></tr>
<tr><td style="color: #64748b;">Tanggungan</td><td>: <b>${w.jumlah_kk} Jiwa</b></td></tr>
<tr><td style="color: #64748b;">Pendidikan</td><td>: ${txtPendidikan}</td></tr>
<tr><td style="color: #64748b;">Riwayat Sakit</td><td>: <span style="color: ${txtPenyakit !== 'Tidak ada' ? '#ef4444' : '#64748b'}">${txtPenyakit}</span></td></tr>
<tr><td style="color: #64748b;">Terima Bantuan</td><td>: <b>${isPenerima === 'Ya' ? '<span style="color:#16a34a">Sudah</span>' : '<span style="color:#ef4444">Belum</span>'}</b></td></tr>
</table>
<div style="font-size: 0.8em; color: #64748b; margin-bottom: 10px; background: #f8fafc; padding: 5px; border-radius: 4px; border: 1px solid #e2e8f0;">
<b>Histori:</b> ${w.histori_bantuan || '<i>Tidak ada riwayat bantuan</i>'}
</div>
<div style="text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('warga', ${w.id})">Edit Data</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${w.id}, 'warga')">Hapus</button>
</div>
</div>`;
L.marker(pos, { icon: houseIcon })
.addTo(layerWarga).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Render Jalan (Polyline)
data.jalan.forEach(j => {
var coords = JSON.parse(j.koordinat);
var color = '#ef4444'; // default red
if(j.status_jalan === 'Nasional') color = '#ef4444'; // Merah
else if(j.status_jalan === 'Provinsi') color = '#eab308'; // Kuning
else if(j.status_jalan === 'Kabupaten') color = '#22c55e'; // Hijau
var popup = `<b>🛣️ ${j.nama_jalan}</b><br>Status: ${j.status_jalan}<br>Panjang: ${j.panjang_m} meter<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('jalan', ${j.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${j.id}, 'jalan')">Hapus</button>
</div>`;
var polyline = L.polyline(coords, {color: color, weight: 6}).addTo(layerLines).bindPopup(popup);
polyline.db_id = j.id;
polyline.db_tipe = 'jalan';
drawnItems.addLayer(polyline);
});
// Render Parsil (Polygon)
data.parsil.forEach(p => {
var coords = JSON.parse(p.koordinat);
var popup = `<b>🟩 Kavling</b><br>Pemilik: ${p.nama_pemilik}<br>Status Hak: ${p.status_hak}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('parsil', ${p.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${p.id}, 'parsil')">Hapus</button>
</div>`;
var polygon = L.polygon(coords, {color: '#2563eb', fillOpacity: 0.4}).addTo(layerPolygons).bindPopup(popup);
polygon.db_id = p.id;
polygon.db_tipe = 'parsil';
drawnItems.addLayer(polygon);
});
}
}
function toggleForm() {
var j = document.getElementById('jenis').value;
['pelaporan', 'warga', 'masjid', 'spbu', 'jalan', 'parsil'].forEach(f => {
var el = document.getElementById('form_' + f);
if (el) {
el.classList.toggle('hidden', j !== f);
}
});
}
map.on('click', e => {
document.getElementById('lat').value = e.latlng.lat;
document.getElementById('lng').value = e.latlng.lng;
L.popup().setLatLng(e.latlng).setContent("Dipilih: " + e.latlng.lat.toFixed(4) + ", " + e.latlng.lng.toFixed(4)).openOn(map);
});
function simpanData() {
var j = document.getElementById('jenis').value;
var fd = new FormData();
fd.append('action', 'simpan_' + j);
fd.append('edit_id', document.getElementById('edit_id').value);
var lat = document.getElementById('lat').value;
var lng = document.getElementById('lng').value;
if (j === 'jalan') {
fd.append('nama', document.getElementById('nama_jalan').value);
fd.append('status_jalan', document.getElementById('status_jalan').value);
fd.append('panjang', document.getElementById('panjang_jalan').value);
fd.append('koordinat', document.getElementById('jalan_coords').value);
} else if (j === 'parsil') {
fd.append('nama', document.getElementById('nama_pemilik').value);
fd.append('status_hak', document.getElementById('status_hak').value);
fd.append('koordinat', document.getElementById('parsil_coords').value);
} else {
if(j !== 'geometri' && (!lat || !lng) && document.getElementById('edit_id').value === "0") {
Swal.fire({icon: 'warning', title: 'Oops...', text: 'Silakan klik titik koordinat di peta terlebih dahulu!'});
return;
}
fd.append('lat', lat); fd.append('lng', lng);
if (j === 'pelaporan') {
fd.append('nama', document.getElementById('nama_pelapor').value);
fd.append('laporan', document.getElementById('isi_laporan').value);
} else if (j === 'warga') {
fd.append('nama', document.getElementById('nama_kk').value);
fd.append('jumlah_kk', document.getElementById('jumlah_kk').value);
fd.append('tanggal_lahir', document.getElementById('tanggal_lahir').value);
fd.append('pendidikan', document.getElementById('pendidikan').value);
fd.append('riwayat_penyakit', document.getElementById('riwayat_penyakit').value);
fd.append('histori_bantuan', document.getElementById('histori_bantuan').value);
fd.append('alamat', document.getElementById('alamat_warga').value);
} else if (j === 'masjid') {
fd.append('nama', document.getElementById('nama_masjid').value);
fd.append('jenis', document.getElementById('jenis_masjid').value);
fd.append('pic', document.getElementById('pic_masjid').value);
fd.append('alamat', document.getElementById('alamat_masjid').value);
} else if (j === 'spbu') {
fd.append('nama', document.getElementById('nama_spbu').value);
fd.append('no_hp', document.getElementById('no_hp_spbu').value);
fd.append('kategori', document.getElementById('kategori_spbu').value);
}
}
Swal.fire({ title: 'Menyimpan...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
Swal.fire({icon: 'success', title: 'Berhasil', text: 'Data berhasil disimpan!', timer: 1500, showConfirmButton: false})
.then(() => location.reload());
}
});
}
function hapusData(id, tipe) {
Swal.fire({
title: 'Hapus Data?', text: "Data yang dihapus tidak dapat dikembalikan!", icon: 'warning',
showCancelButton: true, confirmButtonColor: '#ef4444', cancelButtonColor: '#94a3b8', confirmButtonText: 'Ya, Hapus!'
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({ title: 'Menghapus...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
var fd = new FormData();
fd.append('action', 'hapus_data');
fd.append('id', id);
fd.append('tipe', tipe);
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
location.reload();
}
});
}
});
}
function fokus(lat, lng) { map.flyTo([lat, lng], 18); }
function batalEdit() {
document.getElementById('edit_id').value = "0";
document.getElementById('btnSimpanUtama').innerText = "Simpan Data";
document.getElementById('btnBatalEdit').style.display = "none";
document.getElementById('lat').value = "";
document.getElementById('lng').value = "";
}
function editData(tipe, id) {
var item = cachedData[tipe].find(x => x.id == id);
if(!item) return;
document.getElementById('edit_id').value = id;
document.getElementById('jenis').value = tipe;
toggleForm();
var panel = document.getElementById('panelInput');
if(panel) panel.classList.remove('collapsed');
if(tipe == 'warga') {
document.getElementById('nama_kk').value = item.nama;
document.getElementById('jumlah_kk').value = item.jumlah_kk;
document.getElementById('tanggal_lahir').value = item.tanggal_lahir || '';
document.getElementById('pendidikan').value = item.pendidikan || '';
document.getElementById('riwayat_penyakit').value = item.riwayat_penyakit || '';
document.getElementById('histori_bantuan').value = item.histori_bantuan || '';
document.getElementById('alamat_warga').value = item.alamat || '';
} else if (tipe == 'pelaporan') {
document.getElementById('nama_pelapor').value = item.nama_pelapor;
document.getElementById('isi_laporan').value = item.isi_laporan;
} else if (tipe == 'spbu') {
document.getElementById('nama_spbu').value = item.nama;
document.getElementById('no_hp_spbu').value = item.no_hp;
document.getElementById('kategori_spbu').value = item.kategori;
} else if (tipe == 'masjid') {
document.getElementById('nama_masjid').value = item.nama;
document.getElementById('pic_masjid').value = item.pic;
document.getElementById('alamat_masjid').value = item.alamat;
} else if (tipe == 'jalan') {
document.getElementById('nama_jalan').value = item.nama_jalan;
document.getElementById('status_jalan').value = item.status_jalan;
document.getElementById('panjang_jalan').value = item.panjang_m;
document.getElementById('jalan_coords').value = item.koordinat;
} else if (tipe == 'parsil') {
document.getElementById('nama_pemilik').value = item.nama_pemilik;
document.getElementById('status_hak').value = item.status_hak;
document.getElementById('parsil_coords').value = item.koordinat;
}
if(item.latitude && item.longitude) {
document.getElementById('lat').value = item.latitude;
document.getElementById('lng').value = item.longitude;
fokus(item.latitude, item.longitude);
}
document.getElementById('btnSimpanUtama').innerText = "Update Data";
document.getElementById('btnBatalEdit').style.display = "inline-block";
}
// INIT
loadData();
+149
View File
@@ -0,0 +1,149 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--dark: #0f172a;
--dark-grad: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
--success: #10b981;
--success-hover: #059669;
--danger: #ef4444;
--danger-hover: #dc2626;
--warning: #f59e0b;
--bg-light: #f8fafc;
--border: #e2e8f0;
}
body {
margin: 0; padding: 0;
font-family: 'Inter', sans-serif;
overflow: hidden; display: flex;
background: #e2e8f0;
}
#map { width: 100vw; height: 100vh; z-index: 1; }
/* Custom Sleek Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
/* SEARCH & MENU */
.search-container { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1100; width: 380px; }
.search-box {
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px);
border-radius: 50px; display: flex; align-items: center; padding: 5px 20px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.5);
transition: 0.3s ease;
}
.search-box:focus-within { box-shadow: 0 10px 30px rgba(37,99,235,0.2); border-color: var(--primary); }
.search-box i { color: #64748b; font-size: 1.1rem; }
.search-box input[type="text"] {
border: none !important; outline: none; padding: 12px 10px; margin: 0;
width: 100%; background: transparent !important; font-size: 0.95rem;
color: var(--dark); font-weight: 500; box-shadow: none !important;
}
.search-box input::placeholder { color: #94a3b8; }
.menu-toggle {
position: absolute; bottom: 30px; left: 20px; z-index: 1200;
background: var(--dark-grad); color: white; border: none;
width: 55px; height: 55px; border-radius: 50%; cursor: pointer;
box-shadow: 0 10px 25px rgba(15,23,42,0.4); font-size: 1.4rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.menu-toggle:hover { transform: scale(1.1) rotate(5deg); }
.menu-toggle:active { transform: scale(0.95); }
/* SIDEBAR */
#sidebar {
width: 360px; height: 100vh; background: white; border-right: 1px solid var(--border);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 1150; position: absolute; left: 0; transform: translateX(-100%);
display: flex; flex-direction: column; box-shadow: 5px 0 25px rgba(0,0,0,0.05);
}
#sidebar.active { transform: translateX(0); }
.sidebar-header { padding: 25px 20px; background: var(--dark-grad); color: white; border-bottom: 3px solid var(--primary); }
.sidebar-header h3 { margin: 0 0 5px 0; font-size: 1.4rem; font-weight: 700; letter-spacing: -0.5px; }
.sidebar-header small { color: #cbd5e1; font-weight: 500; font-size: 0.85rem;}
.sidebar-content { padding: 20px; overflow-y: auto; flex: 1; background: var(--bg-light); }
/* PANEL INPUT */
.panel-input {
position: absolute; top: 20px; right: 20px; width: 320px;
background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(10px);
border-radius: 16px; padding: 25px 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.12);
z-index: 1000; max-height: 85vh; overflow-y: auto; border: 1px solid rgba(255,255,255,0.6);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
transform-origin: top right;
}
.panel-input.collapsed {
transform: scale(0.9) translateX(150%);
opacity: 0;
pointer-events: none;
}
.panel-input h4 { color: var(--dark); font-size: 1.1rem; font-weight: 700; border-bottom: 2px solid var(--border); padding-bottom: 10px; margin-top: 0; }
/* RADIUS SLIDER */
.radius-box {
background: #eff6ff; padding: 15px; border-radius: 12px; margin-bottom: 20px;
border: 1px solid #bfdbfe; box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
}
.radius-box label { font-size: 0.85rem; font-weight: 700; color: var(--primary); display: block; margin-bottom: 8px;}
input[type=range] { width: 100%; cursor: pointer; accent-color: var(--primary); }
/* UI HELPERS */
input[type=text], input[type=number], input[type=date], select, textarea {
width: 100%; padding: 12px 15px; margin: 0 0 15px;
border: 1px solid #cbd5e1; border-radius: 8px; box-sizing: border-box;
transition: all 0.3s ease; font-family: 'Inter', sans-serif; font-size: 0.9rem;
background: #f8fafc; color: var(--dark);
}
input:focus, select:focus, textarea:focus {
border-color: var(--primary); outline: none; background: white;
box-shadow: 0 0 0 4px rgba(37,99,235,0.15); transform: translateY(-1px);
}
select { cursor: pointer; font-weight: 500; }
textarea { resize: vertical; min-height: 80px; }
/* CARDS IN SIDEBAR */
.card {
background: #ffffff; padding: 15px; border-radius: 12px; margin-bottom: 12px;
border: 1px solid var(--border); font-size: 0.85rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.02); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; overflow: hidden;
}
.card::before { content:''; position:absolute; left:0; top:0; width:4px; height:100%; background: var(--primary); opacity: 0; transition: 0.3s; }
.card:hover { transform: translateY(-3px); box-shadow: 0 10px 15px rgba(0,0,0,0.06); border-color: #cbd5e1; }
.card:hover::before { opacity: 1; }
.card b { color: var(--dark); display: block; font-size: 0.95rem; margin-bottom: 4px; font-weight: 700;}
.hidden { display: none !important; }
/* BUTTONS */
.btn-save {
background: var(--success); color: white; border: none; padding: 14px;
border-radius: 10px; font-weight: 700; cursor: pointer; width: 100%;
font-size: 1rem; transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.3); text-transform: uppercase; letter-spacing: 0.5px;
}
.btn-save:hover { background: var(--success-hover); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); }
.btn-save:active { transform: translateY(0); }
.btn-del {
background: #fee2e2; color: var(--danger); border: none; padding: 5px 10px;
border-radius: 6px; font-size: 0.75rem; font-weight: 600; cursor: pointer; transition: 0.2s;
}
.btn-del:hover { background: var(--danger); color: white; }
.badge {
display: inline-block; padding: 4px 8px; border-radius: 6px;
font-size: 0.7rem; font-weight: 700; margin-top: 5px; text-transform: uppercase; letter-spacing: 0.5px;
}
.section-title {
font-size: 1rem; font-weight: 700; color: var(--dark);
margin: 20px 0 12px 0; border-bottom: 2px solid var(--border);
padding-bottom: 8px; display: flex; align-items: center; gap: 8px;
}
+25
View File
@@ -0,0 +1,25 @@
<?php
session_start();
if (!isset($_SESSION['login'])) { exit('Unauthorized'); }
require 'db.php';
// Seed Jalan
$conn->query('TRUNCATE TABLE jalan');
$jalan1 = '[{"lat":-0.0528,"lng":109.3409},{"lat":-0.0556,"lng":109.3432},{"lat":-0.0601,"lng":109.3463}]';
$jalan2 = '[{"lat":-0.0343,"lng":109.3431},{"lat":-0.0308,"lng":109.3411}]';
$sqlJalan = "INSERT INTO jalan (nama_jalan, status_jalan, panjang_m, koordinat) VALUES
('Jl. Jenderal Ahmad Yani', 'Nasional', 1200, '$jalan1'),
('Jl. Gajah Mada', 'Provinsi', 800, '$jalan2')";
$conn->query($sqlJalan);
// Seed Parsil / Bangunan
$conn->query('TRUNCATE TABLE parsil');
$parsil1 = '[{"lat":-0.0520,"lng":109.3400},{"lat":-0.0520,"lng":109.3415},{"lat":-0.0535,"lng":109.3415},{"lat":-0.0535,"lng":109.3400}]';
$parsil2 = '[{"lat":-0.0468,"lng":109.3361},{"lat":-0.0468,"lng":109.3385},{"lat":-0.0487,"lng":109.3385},{"lat":-0.0487,"lng":109.3361}]';
$sqlParsil = "INSERT INTO parsil (nama_pemilik, status_hak, koordinat) VALUES
('Kompleks Kantor Gubernur Kalbar', 'HP', '$parsil1'),
('Area Masjid Raya Mujahidin', 'SHM', '$parsil2')";
$conn->query($sqlParsil);
echo "Berhasil insert data jalan dan parsil (bangunan).";
?>
BIN
View File
Binary file not shown.
+183
View File
@@ -0,0 +1,183 @@
<?php
session_start();
$role = $_SESSION['role'] ?? null;
$is_logged_in = isset($_SESSION['login']);
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Portal Utama - WebGIS Poverty Mapping</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
:root {
--primary: #2563eb;
--primary-dark: #1e40af;
--sosial: #db2777;
--infra: #059669;
--bg: #f8fafc;
--dark: #0f172a;
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
margin: 0;
padding: 0;
color: var(--dark);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
font-size: 3rem;
font-weight: 800;
margin: 0;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header p {
font-size: 1.2rem;
color: #64748b;
margin-top: 10px;
}
.portal-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
width: 100%;
max-width: 1000px;
padding: 0 20px;
box-sizing: border-box;
}
.portal-card {
background: white;
border-radius: 20px;
padding: 40px 30px;
text-align: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
transition: transform 0.3s, box-shadow 0.3s;
text-decoration: none;
color: inherit;
position: relative;
overflow: hidden;
}
.portal-card:hover {
transform: translateY(-10px);
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.card-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.card-infra .card-icon { color: var(--infra); }
.card-sosial .card-icon { color: var(--sosial); }
.card-warga .card-icon { color: var(--primary); }
.portal-card h2 {
font-size: 1.5rem;
margin: 0 0 15px 0;
font-weight: 600;
}
.portal-card p {
color: #64748b;
font-size: 0.95rem;
line-height: 1.5;
margin-bottom: 25px;
}
.btn-enter {
display: inline-block;
padding: 12px 25px;
border-radius: 10px;
color: white;
font-weight: 600;
text-decoration: none;
transition: 0.3s;
}
.card-infra .btn-enter { background: var(--infra); }
.card-sosial .btn-enter { background: var(--sosial); }
.card-warga .btn-enter { background: var(--primary); }
.btn-enter:hover {
opacity: 0.9;
transform: scale(1.05);
}
.auth-bar {
position: absolute;
top: 20px;
right: 30px;
}
.btn-auth {
background: white;
color: var(--primary);
padding: 10px 20px;
border-radius: 10px;
text-decoration: none;
font-weight: 600;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
transition: 0.3s;
}
.btn-auth.logout {
color: #ef4444;
}
.btn-auth:hover {
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
}
.locked-badge {
position: absolute;
top: 20px;
right: 20px;
background: #f1f5f9;
color: #94a3b8;
padding: 5px 10px;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 600;
}
</style>
</head>
<body>
<div class="header">
<h1>WebGIS Poverty Mapping</h1>
<p>Pusat Integrasi Data GIS Kota Pontianak</p>
</div>
<div class="portal-grid">
<!-- Project 1: Infrastruktur -->
<div class="portal-card card-infra">
<i class="fas fa-city card-icon"></i>
<h2>Fasilitas & Tata Kota</h2>
<p>Manajemen data spasial fasilitas umum, jaringan jalan raya, batas kepemilikan lahan, dan lokasi SPBU.</p>
<a href="../infra/index.php" class="btn-enter">Buka Dashboard <i class="fas fa-arrow-right"></i></a>
</div>
<!-- Project 2: Sosial & Warga -->
<div class="portal-card card-sosial">
<i class="fas fa-hands-helping card-icon"></i>
<h2>Sosial & Kemasyarakatan</h2>
<p>Pemetaan kawasan rentan, distribusi penduduk, pemantauan rumah ibadah, dan pelaporan darurat.</p>
<a href="../sosial/index.php" class="btn-enter">Buka Dashboard <i class="fas fa-arrow-right"></i></a>
</div>
<!-- Project 3: Publik / Warga -->
<div class="portal-card card-warga">
<i class="fas fa-bullhorn card-icon"></i>
<h2>Portal Publik (Warga)</h2>
<p>Akses terbuka untuk masyarakat. Lihat fasilitas umum terdekat dan lakukan pelaporan cepat kedaruratan.</p>
<a href="../warga/index.php" class="btn-enter">Akses Publik <i class="fas fa-arrow-right"></i></a>
</div>
</div>
</body>
</html>
+322
View File
@@ -0,0 +1,322 @@
<?php
session_start();
require_once '../api/db.php';
if (isset($_SESSION['login'])) {
header("Location: index.php");
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
if (password_verify($password, $row['password'])) {
$_SESSION['login'] = true;
$_SESSION['username'] = $username;
$_SESSION['role'] = $row['role'] ?? 'warga';
header("Location: index.php");
exit;
} else {
$error = 'Username atau Password salah!';
}
} else {
$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 - WebGIS Poverty Mapping</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"/>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #0f172a, #1e293b, #3b82f6);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
overflow: hidden;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Glassmorphism card */
.login-container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 40px;
width: 100%;
max-width: 360px;
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
transform: translateY(20px);
opacity: 0;
animation: fadeUp 0.8s forwards cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
}
@keyframes fadeUp {
to {
transform: translateY(0);
opacity: 1;
}
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header i {
font-size: 48px;
color: #3b82f6;
margin-bottom: 15px;
filter: drop-shadow(0 0 10px rgba(59, 130, 246, 0.5));
}
.login-header h1 {
font-size: 24px;
font-weight: 700;
margin: 0;
margin-bottom: 5px;
letter-spacing: -0.5px;
}
.login-header p {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
.form-group {
margin-bottom: 20px;
position: relative;
}
.form-group input {
width: 100%;
padding: 15px 20px;
padding-left: 45px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: #fff;
font-size: 15px;
outline: none;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-group input:focus {
background: rgba(0, 0, 0, 0.3);
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
.form-group input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.form-group > i {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: rgba(255, 255, 255, 0.5);
transition: all 0.3s ease;
pointer-events: none;
}
.form-group input:focus + i {
color: #3b82f6;
}
.toggle-password {
position: absolute;
right: 18px;
top: 50%;
transform: translateY(-50%);
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.3s ease;
z-index: 2;
}
.toggle-password i {
position: static;
transform: none;
pointer-events: auto;
}
.toggle-password:hover {
color: #fff;
}
.btn-login {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #3b82f6, #2563eb);
border: none;
border-radius: 12px;
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
margin-top: 10px;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.5);
}
.btn-login:active {
transform: translateY(0);
}
.error-message {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.5);
color: #fca5a5;
padding: 12px;
border-radius: 8px;
font-size: 14px;
margin-bottom: 20px;
text-align: center;
animation: shake 0.5s;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* Floating shapes */
.shape {
position: absolute;
filter: blur(60px);
z-index: 1;
opacity: 0.6;
border-radius: 50%;
}
.shape-1 {
width: 300px;
height: 300px;
background: #ec4899;
top: -50px;
left: -50px;
animation: float 8s infinite alternate ease-in-out;
}
.shape-2 {
width: 400px;
height: 400px;
background: #8b5cf6;
bottom: -100px;
right: -100px;
animation: float 12s infinite alternate-reverse ease-in-out;
}
.shape-3 {
width: 200px;
height: 200px;
background: #14b8a6;
top: 40%;
left: 60%;
animation: float 10s infinite alternate ease-in-out;
}
@keyframes float {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(30px, 30px) scale(1.1); }
}
.footer-text {
text-align: center;
margin-top: 25px;
font-size: 12px;
color: rgba(255,255,255,0.4);
}
</style>
</head>
<body>
<div class="shape shape-1"></div>
<div class="shape shape-2"></div>
<div class="shape shape-3"></div>
<div class="login-container">
<div class="login-header">
<i class="fas fa-map-marked-alt"></i>
<h1>WebGIS Poverty Mapping</h1>
<p>Dashboard Eksekutif Terpadu</p>
</div>
<?php if ($error): ?>
<div class="error-message">
<i class="fas fa-exclamation-circle"></i> <?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form method="POST" action="">
<div class="form-group">
<input type="text" name="username" placeholder="Username" required autocomplete="off">
<i class="fas fa-user"></i>
</div>
<div class="form-group">
<input type="password" name="password" id="password" placeholder="Password" required>
<i class="fas fa-lock"></i>
<span class="toggle-password" onclick="togglePassword()">
<i class="fas fa-eye" id="eyeIcon"></i>
</span>
</div>
<button type="submit" class="btn-login">Masuk Dashboard</button>
</form>
<div class="footer-text">
&copy; 2026 WebGIS Poverty Mapping. All rights reserved.
</div>
</div>
<script>
function togglePassword() {
var pwd = document.getElementById('password');
var icon = document.getElementById('eyeIcon');
if (pwd.type === 'password') {
pwd.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
pwd.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
</script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<?php
session_start();
session_unset();
session_destroy();
header("Location: login.php");
exit;
?>
+149
View File
@@ -0,0 +1,149 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--dark: #0f172a;
--dark-grad: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
--success: #10b981;
--success-hover: #059669;
--danger: #ef4444;
--danger-hover: #dc2626;
--warning: #f59e0b;
--bg-light: #f8fafc;
--border: #e2e8f0;
}
body {
margin: 0; padding: 0;
font-family: 'Inter', sans-serif;
overflow: hidden; display: flex;
background: #e2e8f0;
}
#map { width: 100vw; height: 100vh; z-index: 1; }
/* Custom Sleek Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
/* SEARCH & MENU */
.search-container { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1100; width: 380px; }
.search-box {
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px);
border-radius: 50px; display: flex; align-items: center; padding: 5px 20px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.5);
transition: 0.3s ease;
}
.search-box:focus-within { box-shadow: 0 10px 30px rgba(37,99,235,0.2); border-color: var(--primary); }
.search-box i { color: #64748b; font-size: 1.1rem; }
.search-box input[type="text"] {
border: none !important; outline: none; padding: 12px 10px; margin: 0;
width: 100%; background: transparent !important; font-size: 0.95rem;
color: var(--dark); font-weight: 500; box-shadow: none !important;
}
.search-box input::placeholder { color: #94a3b8; }
.menu-toggle {
position: absolute; bottom: 30px; left: 20px; z-index: 1200;
background: var(--dark-grad); color: white; border: none;
width: 55px; height: 55px; border-radius: 50%; cursor: pointer;
box-shadow: 0 10px 25px rgba(15,23,42,0.4); font-size: 1.4rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.menu-toggle:hover { transform: scale(1.1) rotate(5deg); }
.menu-toggle:active { transform: scale(0.95); }
/* SIDEBAR */
#sidebar {
width: 360px; height: 100vh; background: white; border-right: 1px solid var(--border);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 1150; position: absolute; left: 0; transform: translateX(-100%);
display: flex; flex-direction: column; box-shadow: 5px 0 25px rgba(0,0,0,0.05);
}
#sidebar.active { transform: translateX(0); }
.sidebar-header { padding: 25px 20px; background: var(--dark-grad); color: white; border-bottom: 3px solid var(--primary); }
.sidebar-header h3 { margin: 0 0 5px 0; font-size: 1.4rem; font-weight: 700; letter-spacing: -0.5px; }
.sidebar-header small { color: #cbd5e1; font-weight: 500; font-size: 0.85rem;}
.sidebar-content { padding: 20px; overflow-y: auto; flex: 1; background: var(--bg-light); }
/* PANEL INPUT */
.panel-input {
position: absolute; top: 20px; right: 20px; width: 320px;
background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(10px);
border-radius: 16px; padding: 25px 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.12);
z-index: 1000; max-height: 85vh; overflow-y: auto; border: 1px solid rgba(255,255,255,0.6);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
transform-origin: top right;
}
.panel-input.collapsed {
transform: scale(0.9) translateX(150%);
opacity: 0;
pointer-events: none;
}
.panel-input h4 { color: var(--dark); font-size: 1.1rem; font-weight: 700; border-bottom: 2px solid var(--border); padding-bottom: 10px; margin-top: 0; }
/* RADIUS SLIDER */
.radius-box {
background: #eff6ff; padding: 15px; border-radius: 12px; margin-bottom: 20px;
border: 1px solid #bfdbfe; box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
}
.radius-box label { font-size: 0.85rem; font-weight: 700; color: var(--primary); display: block; margin-bottom: 8px;}
input[type=range] { width: 100%; cursor: pointer; accent-color: var(--primary); }
/* UI HELPERS */
input[type=text], input[type=number], input[type=date], select, textarea {
width: 100%; padding: 12px 15px; margin: 0 0 15px;
border: 1px solid #cbd5e1; border-radius: 8px; box-sizing: border-box;
transition: all 0.3s ease; font-family: 'Inter', sans-serif; font-size: 0.9rem;
background: #f8fafc; color: var(--dark);
}
input:focus, select:focus, textarea:focus {
border-color: var(--primary); outline: none; background: white;
box-shadow: 0 0 0 4px rgba(37,99,235,0.15); transform: translateY(-1px);
}
select { cursor: pointer; font-weight: 500; }
textarea { resize: vertical; min-height: 80px; }
/* CARDS IN SIDEBAR */
.card {
background: #ffffff; padding: 15px; border-radius: 12px; margin-bottom: 12px;
border: 1px solid var(--border); font-size: 0.85rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.02); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; overflow: hidden;
}
.card::before { content:''; position:absolute; left:0; top:0; width:4px; height:100%; background: var(--primary); opacity: 0; transition: 0.3s; }
.card:hover { transform: translateY(-3px); box-shadow: 0 10px 15px rgba(0,0,0,0.06); border-color: #cbd5e1; }
.card:hover::before { opacity: 1; }
.card b { color: var(--dark); display: block; font-size: 0.95rem; margin-bottom: 4px; font-weight: 700;}
.hidden { display: none !important; }
/* BUTTONS */
.btn-save {
background: var(--success); color: white; border: none; padding: 14px;
border-radius: 10px; font-weight: 700; cursor: pointer; width: 100%;
font-size: 1rem; transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.3); text-transform: uppercase; letter-spacing: 0.5px;
}
.btn-save:hover { background: var(--success-hover); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); }
.btn-save:active { transform: translateY(0); }
.btn-del {
background: #fee2e2; color: var(--danger); border: none; padding: 5px 10px;
border-radius: 6px; font-size: 0.75rem; font-weight: 600; cursor: pointer; transition: 0.2s;
}
.btn-del:hover { background: var(--danger); color: white; }
.badge {
display: inline-block; padding: 4px 8px; border-radius: 6px;
font-size: 0.7rem; font-weight: 700; margin-top: 5px; text-transform: uppercase; letter-spacing: 0.5px;
}
.section-title {
font-size: 1rem; font-weight: 700; color: var(--dark);
margin: 20px 0 12px 0; border-bottom: 2px solid var(--border);
padding-bottom: 8px; display: flex; align-items: center; gap: 8px;
}
+139
View File
@@ -0,0 +1,139 @@
<?php
session_start();
require_once '../api/db.php';
?>
<!DOCTYPE html>
<html>
<head>
<title>WebGIS Poverty Mapping - Dashboard Sosial & Warga</title>
<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"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"/>
<link rel="stylesheet" href="style.css"/>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body>
<button class="menu-toggle" onclick="toggleSidebar()" title="Buka Dashboard Eksekutif"><i class="fas fa-bars"></i></button>
<button onclick="window.location.href='../portal/index.php'" style="position: absolute; top: 15px; left: 65px; z-index: 1200; background: white; color: var(--primary); border: 2px solid var(--primary); padding: 10px 20px; border-radius: 30px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.1); font-size: 0.9rem; transition: all 0.3s ease; display: flex; align-items: center; gap: 8px;" onmouseover="this.style.background='var(--primary)'; this.style.color='white';" onmouseout="this.style.background='white'; this.style.color='var(--primary)';">
<i class="fas fa-arrow-left"></i> Kembali ke Dashboard
</button>
<button class="menu-toggle" style="left: auto; right: 20px; background: var(--primary);" onclick="toggleRightPanel()" title="Buka Form Input">
<i class="fas fa-pen"></i>
</button>
<div class="search-container">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" placeholder="Cari data GIS..." onkeyup="filterData()">
</div>
</div>
<div id="sidebar">
<div class="sidebar-header">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<h3><i class="fas fa-users"></i> Peta Sosial Warga</h3>
<small><i class="fas fa-hands-helping"></i> Dashboard Admin Sosial</small>
</div>
<div style="display: flex; gap: 8px;">
<a href="../portal/index.php" title="Kembali ke Portal Utama" style="color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onmouseover="this.style.background='rgba(59, 130, 246, 0.8)'" onmouseout="this.style.background='rgba(255,255,255,0.2)'">
<i class="fas fa-home"></i>
</a>
<a href="#" id="notifBell" title="Notifikasi Laporan Warga" style="position: relative; color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onclick="toggleSidebar()">
<i class="fas fa-bell"></i>
<span id="notifBadge" style="display: none; position: absolute; top: -5px; right: -5px; background: #ef4444; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.6rem; font-weight: bold; border: 2px solid #1e293b; animation: bounce 1s infinite;">0</span>
</a>
<a href="../portal/logout.php" title="Logout" style="color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onmouseover="this.style.background='rgba(239, 68, 68, 0.8)'" onmouseout="this.style.background='rgba(255,255,255,0.2)'">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
</div>
</div>
<div class="sidebar-content" id="sidebarContent">
<!-- Dirender secara dinamis oleh script.js -->
<small>Memuat data...</small>
</div>
</div>
<div id="map"></div>
<div class="panel-input" id="panelInput">
<div style="display:flex; justify-content:space-between; align-items:center; border-bottom: 2px solid var(--border); padding-bottom: 10px; margin-bottom: 15px;">
<h4 style="margin:0; border:none; padding:0; font-size:1.1rem; color:var(--dark);">Form Input & Laporan</h4>
<button onclick="toggleRightPanel()" style="background:none; border:none; cursor:pointer; font-size:1.2rem; color:#94a3b8; transition:0.2s;" onmouseover="this.style.color='#ef4444'" onmouseout="this.style.color='#94a3b8'"><i class="fas fa-times"></i></button>
</div>
<div class="radius-box">
<label>📡 Radius Jangkauan / Bantuan: <span id="radiusVal">500</span>m</label>
<input type="range" id="radiusRange" min="100" max="5000" value="500" step="100" oninput="changeRadius(this.value)">
</div>
<select id="jenis" onchange="toggleForm()">
<option value="pelaporan">🚨 Pelaporan Cepat (Masyarakat)</option>
<option value="warga">📍 Warga (Update Admin)</option>
<option value="masjid">🕌 Tempat Ibadah / Pusat Bantuan</option>
</select>
<!-- FORM PELAPORAN -->
<div id="form_pelaporan">
<p style="font-size:0.75rem; color:var(--danger); margin-top:0;">*Gunakan ini untuk lapor kasus gizi buruk, sakit, dll.</p>
<input type="text" id="nama_pelapor" placeholder="Nama Pelapor (Anda)">
<textarea id="isi_laporan" placeholder="Jelaskan kondisi darurat..."></textarea>
</div>
<!-- FORM WARGA -->
<div id="form_warga" class="hidden">
<input type="text" id="nama_kk" placeholder="Nama Kepala Keluarga">
<input type="number" id="jumlah_kk" placeholder="Jumlah Anggota">
<label style="font-size:0.75rem; color:#666;">Tanggal Lahir Kepala Keluarga:</label>
<input type="date" id="tanggal_lahir" title="Tanggal Lahir">
<select id="pendidikan">
<option value="">- Pilih Pendidikan Terakhir -</option>
<option value="Tidak Sekolah">Tidak Sekolah</option>
<option value="SD">SD Sederajat</option>
<option value="SMP">SMP Sederajat</option>
<option value="SMA">SMA Sederajat</option>
<option value="Perguruan Tinggi">Perguruan Tinggi</option>
</select>
<input type="text" id="riwayat_penyakit" placeholder="Riwayat Penyakit (misal: Stunting, TBC)">
<select id="status_bantuan">
<option value="Belum">Belum Menerima Bantuan</option>
<option value="Sudah">Sudah Menerima Bantuan</option>
</select>
<textarea id="histori_bantuan" placeholder="Histori Bantuan (misal: Sembako Jan 2026, PKH)"></textarea>
<textarea id="alamat_warga" placeholder="Alamat lengkap..."></textarea>
</div>
<!-- FORM MASJID -->
<div id="form_masjid" class="hidden">
<input type="text" id="nama_masjid" placeholder="Nama Tempat Ibadah">
<select id="jenis_masjid">
<option value="Masjid">Masjid</option>
<option value="Gereja">Gereja</option>
<option value="Vihara">Vihara / Klenteng</option>
<option value="Pura">Pura</option>
</select>
<input type="text" id="pic_masjid" placeholder="Nama Pengurus / PIC">
<textarea id="alamat_masjid" placeholder="Alamat lengkap..."></textarea>
</div>
<!-- END FORMS -->
<input type="hidden" id="lat"><input type="hidden" id="lng"><input type="hidden" id="edit_id" value="0">
<div style="display:flex; gap:10px;">
<button class="btn-save" id="btnSimpanUtama" onclick="simpanData()" style="flex:1;">Simpan Data</button>
<button class="btn-del" id="btnBatalEdit" onclick="batalEdit()" style="display:none; padding:14px; border-radius:10px; font-weight:700;">Batal</button>
</div>
</div>
<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>
// Penanda untuk script.js agar tahu konteks dashboard
window.dashboardType = 'sosial';
</script>
<script src="script.js"></script>
</body>
</html>
+768
View File
@@ -0,0 +1,768 @@
var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Layer Groups yang Spesifik
var layerMasjid = L.layerGroup().addTo(map);
var layerMasjidRadius = L.layerGroup().addTo(map);
var layerPelaporan = L.layerGroup().addTo(map);
var layerWarga = L.layerGroup().addTo(map);
var layerSpbu24 = L.layerGroup().addTo(map);
var layerSpbuNon24 = L.layerGroup().addTo(map);
var layerLines = L.layerGroup().addTo(map);
var layerPolygons = L.layerGroup().addTo(map);
// Layers Control
var overlays = {};
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
overlays["🕌 Tempat Ibadah"] = layerMasjid;
overlays["📡 Radius Ibadah/Bantuan"] = layerMasjidRadius;
overlays["🚨 Pelaporan Darurat"] = layerPelaporan;
overlays["📍 Warga Rentan"] = layerWarga;
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
overlays["⛽ SPBU 24 Jam (Hijau)"] = layerSpbu24;
overlays["⛽ SPBU Terbatas (Merah)"] = layerSpbuNon24;
overlays["🛣️ Jalan (Polyline)"] = layerLines;
overlays["🟩 Kavling (Polygon)"] = layerPolygons;
}
L.control.layers(null, overlays, { collapsed: false, position: 'topright' }).addTo(map);
var drawnItems = new L.FeatureGroup().addTo(map);
var currentRadius = 500;
var cachedData = null;
var drawControl = new L.Control.Draw({
draw: {
marker:false, circle:false, circlemarker:false,
polyline: { shapeOptions: { color: 'blue', weight: 4 } },
polygon: { shapeOptions: { color: 'red' } },
rectangle: false
},
edit: { featureGroup: drawnItems }
});
map.addControl(drawControl);
// Locate Control
var locateBtn = L.control({position: 'topleft'});
locateBtn.onAdd = function (map) {
var div = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
div.style.backgroundColor = 'white';
div.style.width = '34px';
div.style.height = '34px';
div.style.cursor = 'pointer';
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.justifyContent = 'center';
div.innerHTML = '<i class="fas fa-crosshairs" style="font-size:1.2rem; color:#2563eb;"></i>';
div.title = "Lokasi Anda Saat Ini";
div.onclick = function(){ map.locate({setView: true, maxZoom: 16}); }
return div;
};
locateBtn.addTo(map);
map.on('locationfound', function(e) {
L.circleMarker(e.latlng, {radius: 8, fillColor: '#3b82f6', color: '#fff', weight: 3, fillOpacity: 1}).addTo(map).bindPopup("Lokasi Anda").openPopup();
});
// Event Draw
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var type = e.layerType;
drawnItems.addLayer(layer);
var panel = document.getElementById('panelInput');
if (panel) panel.classList.remove('collapsed');
if (type === 'polyline') {
document.getElementById('jenis').value = 'jalan';
toggleForm();
var coords = layer.getLatLngs();
document.getElementById('jalan_coords').value = JSON.stringify(coords);
// Calculate Length automatically
var length = 0;
for (var i = 0; i < coords.length - 1; i++) {
length += coords[i].distanceTo(coords[i + 1]);
}
document.getElementById('panjang_jalan').value = length.toFixed(2);
} else if (type === 'polygon') {
document.getElementById('jenis').value = 'parsil';
toggleForm();
var coords = layer.getLatLngs()[0];
document.getElementById('parsil_coords').value = JSON.stringify(coords);
}
});
map.on(L.Draw.Event.EDITED, function (e) {
var layers = e.layers;
layers.eachLayer(function (layer) {
if(layer.db_id && layer.db_tipe) {
var coords = (layer.db_tipe === 'parsil') ? layer.getLatLngs()[0] : layer.getLatLngs();
var fd = new FormData();
fd.append('action', 'update_koordinat');
fd.append('id', layer.db_id);
fd.append('tipe', layer.db_tipe);
fd.append('koordinat', JSON.stringify(coords));
if(layer.db_tipe === 'jalan') {
var length = 0;
for (var i = 0; i < coords.length - 1; i++) {
length += coords[i].distanceTo(coords[i + 1]);
}
fd.append('panjang_m', length.toFixed(2));
}
fetch('../api/api.php', { method: 'POST', body: fd });
}
});
Swal.fire({icon: 'success', title: 'Tersimpan', text: 'Perubahan bentuk berhasil disimpan!', timer: 1500, showConfirmButton: false});
});
map.on(L.Draw.Event.DELETED, function (e) {
var layers = e.layers;
layers.eachLayer(function (layer) {
if(layer.db_id && layer.db_tipe) {
var fd = new FormData();
fd.append('action', 'hapus_data');
fd.append('id', layer.db_id);
fd.append('tipe', layer.db_tipe);
fetch('../api/api.php', { method: 'POST', body: fd });
}
});
Swal.fire({icon: 'success', title: 'Terhapus', text: 'Data berhasil dihapus dari peta!', timer: 1500, showConfirmButton: false});
});
function toggleSidebar() { document.getElementById('sidebar').classList.toggle('active'); }
function toggleRightPanel() {
var panel = document.getElementById('panelInput');
if(panel) panel.classList.toggle('collapsed');
}
function filterData() {
var input = document.getElementById('searchInput').value.toLowerCase();
var cards = document.getElementsByClassName('item-data');
if(input.length > 0) document.getElementById('sidebar').classList.add('active');
for (var i = 0; i < cards.length; i++) {
cards[i].style.display = cards[i].getAttribute('data-nama').includes(input) ? "" : "none";
}
}
function changeRadius(v) {
currentRadius = parseInt(v);
document.getElementById('radiusVal').innerText = v;
if(cachedData) {
renderMap(cachedData);
renderSidebar(cachedData);
}
}
var prevReportCount = -1;
function loadData(isPolling = false) {
fetch('../api/api.php?data=all')
.then(r => r.json())
.then(data => {
cachedData = data;
// Logika Notifikasi Bubble
if (prevReportCount !== -1 && data.pelaporan.length > prevReportCount) {
var newCount = data.pelaporan.length - prevReportCount;
Swal.fire({
title: 'Laporan Baru!',
text: `Ada ${newCount} laporan warga baru yang masuk.`,
icon: 'info',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000
});
}
prevReportCount = data.pelaporan.length;
var badge = document.getElementById('notifBadge');
if(badge) {
if(data.pelaporan.length > 0) {
badge.style.display = 'block';
badge.innerText = data.pelaporan.length;
} else {
badge.style.display = 'none';
}
}
if (!isPolling) {
renderSidebar(data);
renderMap(data);
} else {
// If polling, maybe just update sidebar if it's open, or wait for manual refresh
// But for real-time map, we can re-render map as well (might cause flicker if doing heavy edits)
// We'll skip re-rendering map to avoid interrupting user edits, just update the bubble
renderSidebar(data);
renderMap(data);
}
});
}
// Auto-poll every 10 seconds
setInterval(() => {
// Only poll if not currently editing (edit_id is 0)
if(document.getElementById('edit_id') && document.getElementById('edit_id').value === "0") {
loadData(true);
}
}, 10000);
function renderSidebar(data) {
var html = `<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:20px">`;
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
html += `
<div style="background:#fee2e2; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#ef4444; font-size:1.5rem">${data.pelaporan.length}</b><br><small style="color:#991b1b; font-weight:600">Laporan</small>
</div>
<div style="background:#fef3c7; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#d97706; font-size:1.5rem">${data.warga.length}</b><br><small style="color:#92400e; font-weight:600">Warga Rentan</small>
</div>`;
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
html += `
<div style="background:#dcfce7; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#16a34a; font-size:1.5rem">${data.spbu.length}</b><br><small style="color:#166534; font-weight:600">SPBU Terdata</small>
</div>
<div style="background:#e0e7ff; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#4f46e5; font-size:1.5rem">${data.jalan.length}</b><br><small style="color:#3730a3; font-weight:600">Jalur Jalan</small>
</div>`;
}
html += `</div>`;
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Pelaporan
html += `<div class="section-title" style="color:#ef4444">🚨 Laporan Darurat</div>`;
if(data.pelaporan.length === 0) html += `<small style="color:#666">Belum ada laporan</small>`;
data.pelaporan.forEach(p => {
html += `<div class="card item-data" data-nama="${p.nama_pelapor.toLowerCase()} ${p.isi_laporan.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${p.latitude}, ${p.longitude})" style="cursor:pointer">
<b>${p.nama_pelapor}</b><small><i class="far fa-clock"></i> ${p.waktu}</small><br><span>${p.isi_laporan.substring(0,40)}...</span>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('pelaporan', ${p.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${p.id}, 'pelaporan')">Hapus</button>
</div>
</div>
</div>`;
});
// Warga
html += `<div class="section-title">📍 Penduduk Miskin / Rentan</div>`;
data.warga.forEach(w => {
var pos = L.latLng(w.latitude, w.longitude);
var nearbyFacilities = data.masjid.map(m => {
return { nama: m.nama, distance: pos.distanceTo(L.latLng(m.latitude, m.longitude)) };
}).filter(f => f.distance <= currentRadius).sort((a,b) => a.distance - b.distance);
var facilityInfo = nearbyFacilities.length > 0 ? `<br><small style="color:#16a34a"><i class="fas fa-check-circle"></i> Jangkauan: ${nearbyFacilities[0].nama} (${Math.round(nearbyFacilities[0].distance)}m)</small>` : `<br><small style="color:#ef4444"><i class="fas fa-exclamation-triangle"></i> Jauh dari fasilitas / bantuan</small>`;
html += `<div class="card item-data" data-nama="${w.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${w.latitude}, ${w.longitude})" style="cursor:pointer">
<b>${w.nama.replace(/\s*\(.*?\)\s*/g, '')}</b>
${w.riwayat_penyakit ? '<span class="badge" style="background:#fef3c7; color:#d97706;"><i class="fas fa-heartbeat"></i> Medis</span>' : ''}
${facilityInfo}
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('warga', ${w.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${w.id}, 'warga')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// SPBU
html += `<div class="section-title">⛽ Stasiun Pengisian (SPBU)</div>`;
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
html += `<div class="card item-data" data-nama="${s.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${s.latitude}, ${s.longitude})" style="cursor:pointer">
<b>${s.nama}</b>
<small><i class="fas fa-phone"></i> ${s.no_hp}</small><br>
${is24 ? '<span class="badge" style="background:#dcfce7; color:#16a34a;">Buka 24 Jam</span>' : '<span class="badge" style="background:#fee2e2; color:#ef4444;">Tidak 24 Jam</span>'}
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('spbu', ${s.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${s.id}, 'spbu')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Tempat Ibadah
html += `<div class="section-title">🕌 Tempat Ibadah & Bantuan</div>`;
data.masjid.forEach(m => {
html += `<div class="card item-data" data-nama="${m.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${m.latitude}, ${m.longitude})" style="cursor:pointer"><b>${m.nama}</b></span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('masjid', ${m.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${m.id}, 'masjid')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Jalan
html += `<div class="section-title">🛣️ Manajemen Jalan</div>`;
data.jalan.forEach(j => {
var color = '#ef4444'; // default red
if(j.status_jalan === 'Nasional') color = '#ef4444';
else if(j.status_jalan === 'Provinsi') color = '#eab308';
else if(j.status_jalan === 'Kabupaten') color = '#22c55e';
html += `<div class="card item-data" data-nama="${j.nama_jalan.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span>
<b>${j.nama_jalan}</b>
<small style="color:${color}; font-weight:bold;">${j.status_jalan}</small><br>
<small>${j.panjang_m} meter</small>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('jalan', ${j.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${j.id}, 'jalan')">Hapus</button>
</div>
</div>
</div>`;
});
// Parsil
html += `<div class="section-title">🟩 Manajemen Kavling / Parsil</div>`;
data.parsil.forEach(p => {
html += `<div class="card item-data" data-nama="${p.nama_pemilik.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span>
<b>${p.nama_pemilik}</b><br>
<small>Status: <b>${p.status_hak}</b></small>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('parsil', ${p.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${p.id}, 'parsil')">Hapus</button>
</div>
</div>
</div>`;
});
}
document.getElementById('sidebarContent').innerHTML = html;
}
function renderMap(data) {
layerMasjid.clearLayers();
layerMasjidRadius.clearLayers();
layerPelaporan.clearLayers();
layerWarga.clearLayers();
layerSpbu24.clearLayers();
layerSpbuNon24.clearLayers();
layerLines.clearLayers();
layerPolygons.clearLayers();
var lokMasjid = data.masjid.map(m => L.latLng(m.latitude, m.longitude));
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Render Masjid / Tempat Ibadah
data.masjid.forEach(m => {
var popup = `<b>${m.jenis || 'Tempat Ibadah'}:</b> ${m.nama}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('masjid', ${m.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${m.id}, 'masjid')">Hapus</button>
</div>`;
var faClass = 'fa-mosque';
if(m.jenis === 'Gereja') faClass = 'fa-church';
else if(m.jenis === 'Vihara') faClass = 'fa-vihara';
else if(m.jenis === 'Pura') faClass = 'fa-torii-gate';
// Buat custom marker bentuk map pin dengan icon sesuai jenis
var masjidIcon = L.divIcon({
html: `
<div style="
width: 34px;
height: 34px;
background-color: #2563eb;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas ${faClass}" style="
transform: rotate(45deg);
color: white;
font-size: 14px;
margin-top: 2px;
margin-left: -2px;
"></i>
</div>
`,
className: '', // Hilangkan background default Leaflet
iconSize: [34, 34],
iconAnchor: [17, 40], // Anchor ujung lancip di bawah
popupAnchor: [0, -40]
});
L.marker([m.latitude, m.longitude], { icon: masjidIcon }).addTo(layerMasjid).bindPopup(popup);
L.circle([m.latitude, m.longitude], {
radius: currentRadius, color: '#2563eb', fillOpacity: 0.1, weight: 1
}).addTo(layerMasjidRadius);
});
// Render Pelaporan
data.pelaporan.forEach(p => {
var popup = `<b>🚨 Laporan:</b> ${p.isi_laporan}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('pelaporan', ${p.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${p.id}, 'pelaporan')">Hapus</button>
</div>`;
var pelaporanIcon = L.divIcon({
html: `
<div style="
width: 32px;
height: 32px;
background-color: #f97316; /* Orange color */
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-exclamation" style="
transform: rotate(45deg);
color: white;
font-size: 16px;
font-weight: bold;
margin-top: 2px;
margin-left: -2px;
"></i>
</div>
`,
className: '',
iconSize: [32, 32],
iconAnchor: [16, 38],
popupAnchor: [0, -38]
});
L.marker([p.latitude, p.longitude], {
icon: pelaporanIcon
}).addTo(layerPelaporan).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Render SPBU
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
var iconUrl = is24 ? 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png' : 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png';
var targetLayer = is24 ? layerSpbu24 : layerSpbuNon24;
var popup = `<b>⛽ ${s.nama}</b><br>Kategori: ${s.kategori === '24jam' ? 'Buka 24 Jam' : 'Tidak 24 Jam'}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('spbu', ${s.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${s.id}, 'spbu')">Hapus</button>
</div>`;
L.marker([s.latitude, s.longitude], {
icon: L.icon({ iconUrl: iconUrl, iconSize: [25, 41], iconAnchor: [12, 41] })
}).addTo(targetLayer).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Render Warga
data.warga.forEach(w => {
var pos = L.latLng(w.latitude, w.longitude);
var nearbyFacilities = data.masjid.map(m => {
return { nama: m.nama, distance: pos.distanceTo(L.latLng(m.latitude, m.longitude)) };
}).filter(f => f.distance <= currentRadius).sort((a,b) => a.distance - b.distance);
var isCovered = nearbyFacilities.length > 0;
var nearestName = isCovered ? nearbyFacilities[0].nama : '-';
var nearestDist = isCovered ? Math.round(nearbyFacilities[0].distance) + 'm' : '-';
// Buat elemen point berbentuk map pin (teardrop) dengan ikon rumah di dalamnya
var color = isCovered ? '#16a34a' : '#ef4444'; // Hijau aman, merah di luar
var houseIcon = L.divIcon({
html: `
<div style="
width: 30px;
height: 30px;
background-color: ${color};
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
">
<div style="
width: 18px;
height: 18px;
background-color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-home" style="
transform: rotate(45deg);
color: ${color};
font-size: 10px;
"></i>
</div>
</div>
`,
className: '', // Kosongkan agar background default leaflet tidak muncul
iconSize: [30, 30],
iconAnchor: [15, 36], // Titik jangkar di ujung bawah pin
popupAnchor: [0, -36]
});
var isPenerima = (w.status_bantuan === 'Sudah') ? 'Ya' : 'Belum';
var txtPendidikan = w.pendidikan ? w.pendidikan : 'Tidak diketahui';
var txtPenyakit = (w.riwayat_penyakit && w.riwayat_penyakit.trim().length > 0) ? w.riwayat_penyakit : 'Tidak ada';
var popup = `
<div style="min-width: 200px;">
<b style="font-size: 1.1em; color: #1e293b;"><i class="fas fa-user-circle"></i> Warga: ${w.nama.replace(/\s*\(.*?\)\s*/g, '')}</b>
<hr style="margin: 5px 0; border: 0; border-top: 1px solid #cbd5e1;">
<table style="width: 100%; font-size: 0.85em; margin-bottom: 8px;">
<tr><td style="color: #64748b; padding-right: 10px;">Jangkauan</td><td>: <b>${isCovered ? '<span style="color:#16a34a">Aman (Dalam Radius)</span>' : '<span style="color:#ef4444">Di Luar</span>'}</b></td></tr>
${isCovered ? `<tr><td style="color: #64748b;">Fasilitas Trdkt</td><td>: <b>${nearestName}</b> (${nearestDist})</td></tr>` : ''}
<tr><td style="color: #64748b;">Tanggungan</td><td>: <b>${w.jumlah_kk} Jiwa</b></td></tr>
<tr><td style="color: #64748b;">Pendidikan</td><td>: ${txtPendidikan}</td></tr>
<tr><td style="color: #64748b;">Riwayat Sakit</td><td>: <span style="color: ${txtPenyakit !== 'Tidak ada' ? '#ef4444' : '#64748b'}">${txtPenyakit}</span></td></tr>
<tr><td style="color: #64748b;">Terima Bantuan</td><td>: <b>${isPenerima === 'Ya' ? '<span style="color:#16a34a">Sudah</span>' : '<span style="color:#ef4444">Belum</span>'}</b></td></tr>
</table>
<div style="font-size: 0.8em; color: #64748b; margin-bottom: 10px; background: #f8fafc; padding: 5px; border-radius: 4px; border: 1px solid #e2e8f0;">
<b>Histori:</b> ${w.histori_bantuan || '<i>Tidak ada riwayat bantuan</i>'}
</div>
<div style="text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('warga', ${w.id})">Edit Data</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${w.id}, 'warga')">Hapus</button>
</div>
</div>`;
L.marker(pos, { icon: houseIcon })
.addTo(layerWarga).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Render Jalan (Polyline)
data.jalan.forEach(j => {
var coords = JSON.parse(j.koordinat);
var color = '#ef4444'; // default red
if(j.status_jalan === 'Nasional') color = '#ef4444'; // Merah
else if(j.status_jalan === 'Provinsi') color = '#eab308'; // Kuning
else if(j.status_jalan === 'Kabupaten') color = '#22c55e'; // Hijau
var popup = `<b>🛣️ ${j.nama_jalan}</b><br>Status: ${j.status_jalan}<br>Panjang: ${j.panjang_m} meter<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('jalan', ${j.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${j.id}, 'jalan')">Hapus</button>
</div>`;
var polyline = L.polyline(coords, {color: color, weight: 6}).addTo(layerLines).bindPopup(popup);
polyline.db_id = j.id;
polyline.db_tipe = 'jalan';
drawnItems.addLayer(polyline);
});
// Render Parsil (Polygon)
data.parsil.forEach(p => {
var coords = JSON.parse(p.koordinat);
var popup = `<b>🟩 Kavling</b><br>Pemilik: ${p.nama_pemilik}<br>Status Hak: ${p.status_hak}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('parsil', ${p.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${p.id}, 'parsil')">Hapus</button>
</div>`;
var polygon = L.polygon(coords, {color: '#2563eb', fillOpacity: 0.4}).addTo(layerPolygons).bindPopup(popup);
polygon.db_id = p.id;
polygon.db_tipe = 'parsil';
drawnItems.addLayer(polygon);
});
}
}
function toggleForm() {
var j = document.getElementById('jenis').value;
['pelaporan', 'warga', 'masjid', 'spbu', 'jalan', 'parsil'].forEach(f => {
var el = document.getElementById('form_' + f);
if (el) {
el.classList.toggle('hidden', j !== f);
}
});
}
map.on('click', e => {
document.getElementById('lat').value = e.latlng.lat;
document.getElementById('lng').value = e.latlng.lng;
L.popup().setLatLng(e.latlng).setContent("Dipilih: " + e.latlng.lat.toFixed(4) + ", " + e.latlng.lng.toFixed(4)).openOn(map);
});
function simpanData() {
var j = document.getElementById('jenis').value;
var fd = new FormData();
fd.append('action', 'simpan_' + j);
fd.append('edit_id', document.getElementById('edit_id').value);
var lat = document.getElementById('lat').value;
var lng = document.getElementById('lng').value;
if (j === 'jalan') {
fd.append('nama', document.getElementById('nama_jalan').value);
fd.append('status_jalan', document.getElementById('status_jalan').value);
fd.append('panjang', document.getElementById('panjang_jalan').value);
fd.append('koordinat', document.getElementById('jalan_coords').value);
} else if (j === 'parsil') {
fd.append('nama', document.getElementById('nama_pemilik').value);
fd.append('status_hak', document.getElementById('status_hak').value);
fd.append('koordinat', document.getElementById('parsil_coords').value);
} else {
if(j !== 'geometri' && (!lat || !lng) && document.getElementById('edit_id').value === "0") {
Swal.fire({icon: 'warning', title: 'Oops...', text: 'Silakan klik titik koordinat di peta terlebih dahulu!'});
return;
}
fd.append('lat', lat); fd.append('lng', lng);
if (j === 'pelaporan') {
fd.append('nama', document.getElementById('nama_pelapor').value);
fd.append('laporan', document.getElementById('isi_laporan').value);
} else if (j === 'warga') {
fd.append('nama', document.getElementById('nama_kk').value);
fd.append('jumlah_kk', document.getElementById('jumlah_kk').value);
fd.append('tanggal_lahir', document.getElementById('tanggal_lahir').value);
fd.append('pendidikan', document.getElementById('pendidikan').value);
fd.append('riwayat_penyakit', document.getElementById('riwayat_penyakit').value);
fd.append('status_bantuan', document.getElementById('status_bantuan').value);
fd.append('histori_bantuan', document.getElementById('histori_bantuan').value);
fd.append('alamat', document.getElementById('alamat_warga').value);
} else if (j === 'masjid') {
fd.append('nama', document.getElementById('nama_masjid').value);
fd.append('jenis', document.getElementById('jenis_masjid').value);
fd.append('pic', document.getElementById('pic_masjid').value);
fd.append('alamat', document.getElementById('alamat_masjid').value);
} else if (j === 'spbu') {
fd.append('nama', document.getElementById('nama_spbu').value);
fd.append('no_hp', document.getElementById('no_hp_spbu').value);
fd.append('kategori', document.getElementById('kategori_spbu').value);
}
}
Swal.fire({ title: 'Menyimpan...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
Swal.fire({icon: 'success', title: 'Berhasil', text: 'Data berhasil disimpan!', timer: 1500, showConfirmButton: false})
.then(() => location.reload());
}
});
}
function hapusData(id, tipe) {
Swal.fire({
title: 'Hapus Data?', text: "Data yang dihapus tidak dapat dikembalikan!", icon: 'warning',
showCancelButton: true, confirmButtonColor: '#ef4444', cancelButtonColor: '#94a3b8', confirmButtonText: 'Ya, Hapus!'
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({ title: 'Menghapus...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
var fd = new FormData();
fd.append('action', 'hapus_data');
fd.append('id', id);
fd.append('tipe', tipe);
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
location.reload();
}
});
}
});
}
function fokus(lat, lng) { map.flyTo([lat, lng], 18); }
function batalEdit() {
document.getElementById('edit_id').value = "0";
document.getElementById('btnSimpanUtama').innerText = "Simpan Data";
document.getElementById('btnBatalEdit').style.display = "none";
document.getElementById('lat').value = "";
document.getElementById('lng').value = "";
}
function editData(tipe, id) {
var item = cachedData[tipe].find(x => x.id == id);
if(!item) return;
document.getElementById('edit_id').value = id;
document.getElementById('jenis').value = tipe;
toggleForm();
var panel = document.getElementById('panelInput');
if(panel) panel.classList.remove('collapsed');
if(tipe == 'warga') {
document.getElementById('nama_kk').value = item.nama;
document.getElementById('jumlah_kk').value = item.jumlah_kk;
document.getElementById('tanggal_lahir').value = item.tanggal_lahir || '';
document.getElementById('pendidikan').value = item.pendidikan || '';
document.getElementById('riwayat_penyakit').value = item.riwayat_penyakit || '';
document.getElementById('status_bantuan').value = item.status_bantuan || 'Belum';
document.getElementById('histori_bantuan').value = item.histori_bantuan || '';
document.getElementById('alamat_warga').value = item.alamat || '';
} else if (tipe == 'pelaporan') {
document.getElementById('nama_pelapor').value = item.nama_pelapor;
document.getElementById('isi_laporan').value = item.isi_laporan;
} else if (tipe == 'spbu') {
document.getElementById('nama_spbu').value = item.nama;
document.getElementById('no_hp_spbu').value = item.no_hp;
document.getElementById('kategori_spbu').value = item.kategori;
} else if (tipe == 'masjid') {
document.getElementById('nama_masjid').value = item.nama;
document.getElementById('pic_masjid').value = item.pic;
document.getElementById('alamat_masjid').value = item.alamat;
} else if (tipe == 'jalan') {
document.getElementById('nama_jalan').value = item.nama_jalan;
document.getElementById('status_jalan').value = item.status_jalan;
document.getElementById('panjang_jalan').value = item.panjang_m;
document.getElementById('jalan_coords').value = item.koordinat;
} else if (tipe == 'parsil') {
document.getElementById('nama_pemilik').value = item.nama_pemilik;
document.getElementById('status_hak').value = item.status_hak;
document.getElementById('parsil_coords').value = item.koordinat;
}
if(item.latitude && item.longitude) {
document.getElementById('lat').value = item.latitude;
document.getElementById('lng').value = item.longitude;
fokus(item.latitude, item.longitude);
}
document.getElementById('btnSimpanUtama').innerText = "Update Data";
document.getElementById('btnBatalEdit').style.display = "inline-block";
}
// INIT
loadData();
+149
View File
@@ -0,0 +1,149 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--dark: #0f172a;
--dark-grad: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
--success: #10b981;
--success-hover: #059669;
--danger: #ef4444;
--danger-hover: #dc2626;
--warning: #f59e0b;
--bg-light: #f8fafc;
--border: #e2e8f0;
}
body {
margin: 0; padding: 0;
font-family: 'Inter', sans-serif;
overflow: hidden; display: flex;
background: #e2e8f0;
}
#map { width: 100vw; height: 100vh; z-index: 1; }
/* Custom Sleek Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
/* SEARCH & MENU */
.search-container { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1100; width: 380px; }
.search-box {
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px);
border-radius: 50px; display: flex; align-items: center; padding: 5px 20px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.5);
transition: 0.3s ease;
}
.search-box:focus-within { box-shadow: 0 10px 30px rgba(37,99,235,0.2); border-color: var(--primary); }
.search-box i { color: #64748b; font-size: 1.1rem; }
.search-box input[type="text"] {
border: none !important; outline: none; padding: 12px 10px; margin: 0;
width: 100%; background: transparent !important; font-size: 0.95rem;
color: var(--dark); font-weight: 500; box-shadow: none !important;
}
.search-box input::placeholder { color: #94a3b8; }
.menu-toggle {
position: absolute; bottom: 30px; left: 20px; z-index: 1200;
background: var(--dark-grad); color: white; border: none;
width: 55px; height: 55px; border-radius: 50%; cursor: pointer;
box-shadow: 0 10px 25px rgba(15,23,42,0.4); font-size: 1.4rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.menu-toggle:hover { transform: scale(1.1) rotate(5deg); }
.menu-toggle:active { transform: scale(0.95); }
/* SIDEBAR */
#sidebar {
width: 360px; height: 100vh; background: white; border-right: 1px solid var(--border);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 1150; position: absolute; left: 0; transform: translateX(-100%);
display: flex; flex-direction: column; box-shadow: 5px 0 25px rgba(0,0,0,0.05);
}
#sidebar.active { transform: translateX(0); }
.sidebar-header { padding: 25px 20px; background: var(--dark-grad); color: white; border-bottom: 3px solid var(--primary); }
.sidebar-header h3 { margin: 0 0 5px 0; font-size: 1.4rem; font-weight: 700; letter-spacing: -0.5px; }
.sidebar-header small { color: #cbd5e1; font-weight: 500; font-size: 0.85rem;}
.sidebar-content { padding: 20px; overflow-y: auto; flex: 1; background: var(--bg-light); }
/* PANEL INPUT */
.panel-input {
position: absolute; top: 20px; right: 20px; width: 320px;
background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(10px);
border-radius: 16px; padding: 25px 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.12);
z-index: 1000; max-height: 85vh; overflow-y: auto; border: 1px solid rgba(255,255,255,0.6);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
transform-origin: top right;
}
.panel-input.collapsed {
transform: scale(0.9) translateX(150%);
opacity: 0;
pointer-events: none;
}
.panel-input h4 { color: var(--dark); font-size: 1.1rem; font-weight: 700; border-bottom: 2px solid var(--border); padding-bottom: 10px; margin-top: 0; }
/* RADIUS SLIDER */
.radius-box {
background: #eff6ff; padding: 15px; border-radius: 12px; margin-bottom: 20px;
border: 1px solid #bfdbfe; box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
}
.radius-box label { font-size: 0.85rem; font-weight: 700; color: var(--primary); display: block; margin-bottom: 8px;}
input[type=range] { width: 100%; cursor: pointer; accent-color: var(--primary); }
/* UI HELPERS */
input[type=text], input[type=number], input[type=date], select, textarea {
width: 100%; padding: 12px 15px; margin: 0 0 15px;
border: 1px solid #cbd5e1; border-radius: 8px; box-sizing: border-box;
transition: all 0.3s ease; font-family: 'Inter', sans-serif; font-size: 0.9rem;
background: #f8fafc; color: var(--dark);
}
input:focus, select:focus, textarea:focus {
border-color: var(--primary); outline: none; background: white;
box-shadow: 0 0 0 4px rgba(37,99,235,0.15); transform: translateY(-1px);
}
select { cursor: pointer; font-weight: 500; }
textarea { resize: vertical; min-height: 80px; }
/* CARDS IN SIDEBAR */
.card {
background: #ffffff; padding: 15px; border-radius: 12px; margin-bottom: 12px;
border: 1px solid var(--border); font-size: 0.85rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.02); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; overflow: hidden;
}
.card::before { content:''; position:absolute; left:0; top:0; width:4px; height:100%; background: var(--primary); opacity: 0; transition: 0.3s; }
.card:hover { transform: translateY(-3px); box-shadow: 0 10px 15px rgba(0,0,0,0.06); border-color: #cbd5e1; }
.card:hover::before { opacity: 1; }
.card b { color: var(--dark); display: block; font-size: 0.95rem; margin-bottom: 4px; font-weight: 700;}
.hidden { display: none !important; }
/* BUTTONS */
.btn-save {
background: var(--success); color: white; border: none; padding: 14px;
border-radius: 10px; font-weight: 700; cursor: pointer; width: 100%;
font-size: 1rem; transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.3); text-transform: uppercase; letter-spacing: 0.5px;
}
.btn-save:hover { background: var(--success-hover); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); }
.btn-save:active { transform: translateY(0); }
.btn-del {
background: #fee2e2; color: var(--danger); border: none; padding: 5px 10px;
border-radius: 6px; font-size: 0.75rem; font-weight: 600; cursor: pointer; transition: 0.2s;
}
.btn-del:hover { background: var(--danger); color: white; }
.badge {
display: inline-block; padding: 4px 8px; border-radius: 6px;
font-size: 0.7rem; font-weight: 700; margin-top: 5px; text-transform: uppercase; letter-spacing: 0.5px;
}
.section-title {
font-size: 1rem; font-weight: 700; color: var(--dark);
margin: 20px 0 12px 0; border-bottom: 2px solid var(--border);
padding-bottom: 8px; display: flex; align-items: center; gap: 8px;
}
+16
View File
@@ -0,0 +1,16 @@
<?php
session_start();
if (!isset($_SESSION['login'])) { exit('Unauthorized'); }
require 'db.php';
$conn->query('TRUNCATE TABLE rumah_ibadah');
if(!$conn->query("INSERT INTO rumah_ibadah (nama, jenis, pic, alamat, latitude, longitude) VALUES
('Masjid Raya Mujahidin', 'Masjid', 'Bapak Ahmad', 'Jl. Jenderal Ahmad Yani, Pontianak Selatan', '-0.0468', '109.3361'),
('Masjid Jami Keraton Kadriyah', 'Masjid', 'Bapak Syarif', 'Beting, Pontianak Timur', '-0.0215', '109.3528'),
('Masjid Darul Falah', 'Masjid', 'Bapak Budi', 'Jl. H. Rais A. Rahman', '-0.0354', '109.3242'),
('Gereja Katedral St. Yosef', 'Gereja', 'Romo Yohanes', 'Jl. Pattimura, Pontianak Kota', '-0.0385', '109.3400'),
('Vihara Bodhisatva Karaniya Metta', 'Vihara', 'Bapak Ahong', 'Jl. Kapten Marsan, Pontianak', '-0.0250', '109.3440')")) {
echo "Error: " . $conn->error;
} else {
echo "Success!";
}
?>
+7
View File
@@ -0,0 +1,7 @@
<?php
session_start();
if (!isset($_SESSION['login'])) { exit('Unauthorized'); }
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['data'] = 'all';
require 'api.php';
?>
+7
View File
@@ -0,0 +1,7 @@
<?php
session_start();
if (!isset($_SESSION['login'])) { exit('Unauthorized'); }
require 'db.php';
$r=$conn->query("SELECT * FROM rumah_ibadah");
echo count($r->fetch_all(MYSQLI_ASSOC));
?>
+298
View File
@@ -0,0 +1,298 @@
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: ../portal/login.php");
exit;
}
require_once '../api/db.php';
$username = $_SESSION['username'] ?? 'Warga';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Portal Warga - WebGIS Poverty Mapping</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"/>
<link rel="stylesheet" href="style.css"/>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
.header-warga {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
padding: 10px 25px;
border-radius: 30px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 15px;
font-family: 'Inter', sans-serif;
border: 1px solid rgba(0,0,0,0.05);
}
.header-warga h3 { margin: 0; color: #1e293b; font-size: 1.2rem; }
.header-warga span { font-size: 0.85rem; color: #64748b; }
.btn-lapor-darurat {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
border: none;
padding: 15px 30px;
border-radius: 30px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
box-shadow: 0 10px 25px rgba(239, 68, 68, 0.4);
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
animation: pulse 2s infinite;
}
.btn-lapor-darurat:hover {
transform: translateX(-50%) scale(1.05);
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
70% { box-shadow: 0 0 0 15px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
.lapor-modal {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(5px);
z-index: 2000;
justify-content: center;
align-items: center;
}
.lapor-modal.active { display: flex; }
.lapor-content {
background: white;
padding: 30px;
border-radius: 20px;
width: 90%;
max-width: 400px;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.lapor-content input, .lapor-content textarea {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 1px solid #cbd5e1;
border-radius: 10px;
box-sizing: border-box;
font-family: inherit;
}
.lapor-content textarea { resize: vertical; min-height: 100px; }
</style>
</head>
<body>
<div class="header-warga">
<i class="fas fa-map-marked-alt" style="color:#3b82f6; font-size: 1.5rem;"></i>
<div style="flex: 1;">
<h3>Portal Warga GIS</h3>
<span>Informasi Fasilitas & Pelaporan Darurat</span>
</div>
<div style="display: flex; gap: 8px;">
<a href="../portal/index.php" title="Kembali ke Portal Utama" style="color: #fff; background: rgba(59, 130, 246, 0.8); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onmouseover="this.style.background='rgba(37, 99, 235, 1)'" onmouseout="this.style.background='rgba(59, 130, 246, 0.8)'">
<i class="fas fa-home"></i>
</a>
<a href="../portal/logout.php" title="Logout" style="color: #fff; background: rgba(239, 68, 68, 0.8); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onmouseover="this.style.background='rgba(220, 38, 38, 1)'" onmouseout="this.style.background='rgba(239, 68, 68, 0.8)'">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
</div>
<div id="map" style="height: 100vh; width: 100vw; position: absolute; top: 0; left: 0; z-index: 1;"></div>
<button class="btn-lapor-darurat" onclick="bukaLapor()">
<i class="fas fa-exclamation-triangle"></i> LAPOR DARURAT SEKARANG
</button>
<div class="lapor-modal" id="modalLapor">
<div class="lapor-content">
<h3 style="margin-top:0; color:#ef4444;"><i class="fas fa-bullhorn"></i> Form Lapor Darurat</h3>
<p style="font-size:0.85rem; color:#64748b; margin-bottom: 20px;">Pastikan lokasi Anda sudah sesuai di peta sebelum mengirim laporan ini.</p>
<input type="text" id="w_nama" value="<?php echo htmlspecialchars($username); ?>" readonly style="background:#f1f5f9; cursor:not-allowed;">
<textarea id="w_laporan" placeholder="Jelaskan situasi darurat (Misal: Jalan amblas, butuh ambulans)..." required></textarea>
<div style="display: flex; gap: 10px;">
<button onclick="tutupLapor()" style="flex:1; padding:12px; border:none; border-radius:10px; background:#e2e8f0; color:#475569; font-weight:bold; cursor:pointer;">Batal</button>
<button onclick="kirimLaporan()" style="flex:1; padding:12px; border:none; border-radius:10px; background:#ef4444; color:white; font-weight:bold; cursor:pointer;">Kirim Laporan</button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
var layerMasjid = L.layerGroup().addTo(map);
var layerPelaporan = L.layerGroup().addTo(map);
var layerSpbu24 = L.layerGroup().addTo(map);
var layerSpbuNon24 = L.layerGroup().addTo(map);
var overlays = {
"🕌 Tempat Ibadah/Bantuan": layerMasjid,
"🚨 Titik Laporan": layerPelaporan,
"⛽ SPBU 24 Jam": layerSpbu24,
"⛽ SPBU Terbatas": layerSpbuNon24
};
L.control.layers(null, overlays, { collapsed: true, position: 'topright' }).addTo(map);
// Titik lokasi warga untuk lapor
var userMarker = null;
var userLat = -0.0263;
var userLng = 109.3425;
// Dapatkan lokasi pengguna otomatis
map.locate({setView: true, maxZoom: 15});
map.on('locationfound', function(e) {
userLat = e.latlng.lat;
userLng = e.latlng.lng;
if(userMarker) map.removeLayer(userMarker);
userMarker = L.marker(e.latlng, {draggable: true}).addTo(map)
.bindPopup("Geser pin ini ke lokasi persis kejadian!").openPopup();
userMarker.on('dragend', function(ev) {
var pos = ev.target.getLatLng();
userLat = pos.lat;
userLng = pos.lng;
});
});
// Jika map diklik, pindahkan marker
map.on('click', function(e) {
userLat = e.latlng.lat;
userLng = e.latlng.lng;
if(!userMarker) {
userMarker = L.marker(e.latlng, {draggable: true}).addTo(map)
.bindPopup("Lokasi laporan").openPopup();
userMarker.on('dragend', function(ev) {
var pos = ev.target.getLatLng();
userLat = pos.lat;
userLng = pos.lng;
});
} else {
userMarker.setLatLng(e.latlng);
}
});
function loadData() {
fetch('../api/api.php?data=all')
.then(r => r.json())
.then(data => {
renderMap(data);
});
}
function renderMap(data) {
layerMasjid.clearLayers();
layerPelaporan.clearLayers();
layerSpbu24.clearLayers();
layerSpbuNon24.clearLayers();
data.masjid.forEach(m => {
var popup = `<b>${m.jenis || 'Tempat Ibadah'}</b><br>${m.nama}<br><small>${m.alamat}</small>`;
var faClass = 'fa-mosque';
if(m.jenis === 'Gereja') faClass = 'fa-church';
else if(m.jenis === 'Vihara') faClass = 'fa-vihara';
else if(m.jenis === 'Pura') faClass = 'fa-torii-gate';
var mIcon = L.divIcon({
html: `<div style="width:30px; height:30px; background:#2563eb; border-radius:50% 50% 50% 0; transform:rotate(-45deg); display:flex; align-items:center; justify-content:center; border:2px solid white; box-shadow: 2px 2px 5px rgba(0,0,0,0.5);"><i class="fas ${faClass}" style="transform:rotate(45deg); color:white; font-size:12px;"></i></div>`,
className: '', iconSize: [30, 30], iconAnchor: [15, 30]
});
L.marker([m.latitude, m.longitude], { icon: mIcon }).addTo(layerMasjid).bindPopup(popup);
});
data.pelaporan.forEach(p => {
var popup = `<b>🚨 ${p.nama_pelapor} melapor:</b><br>${p.isi_laporan}<br><small><i class="far fa-clock"></i> ${p.waktu}</small>`;
var pIcon = L.divIcon({
html: `<div style="width:28px; height:28px; background:#f97316; border-radius:50% 50% 50% 0; transform:rotate(-45deg); display:flex; align-items:center; justify-content:center; border:2px solid white;"><i class="fas fa-exclamation" style="transform:rotate(45deg); color:white; font-size:12px; font-weight:bold;"></i></div>`,
className: '', iconSize: [28, 28], iconAnchor: [14, 28]
});
L.marker([p.latitude, p.longitude], { icon: pIcon }).addTo(layerPelaporan).bindPopup(popup);
});
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
var iconUrl = is24 ? 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png' : 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png';
var targetLayer = is24 ? layerSpbu24 : layerSpbuNon24;
var popup = `<b>⛽ ${s.nama}</b><br>Kategori: ${s.kategori === '24jam' ? 'Buka 24 Jam' : 'Tidak 24 Jam'}`;
L.marker([s.latitude, s.longitude], { icon: L.icon({ iconUrl: iconUrl, iconSize: [25, 41], iconAnchor: [12, 41] }) }).addTo(targetLayer).bindPopup(popup);
});
}
function bukaLapor() {
document.getElementById('modalLapor').classList.add('active');
if(!userMarker) {
Swal.fire('Perhatian', 'Pastikan Anda telah mengizinkan akses lokasi atau mengklik titik di peta untuk menentukan lokasi laporan.', 'info');
}
}
function tutupLapor() {
document.getElementById('modalLapor').classList.remove('active');
}
function kirimLaporan() {
var nama = document.getElementById('w_nama').value;
var laporan = document.getElementById('w_laporan').value;
if(!laporan) {
Swal.fire('Oops!', 'Mohon isi penjelasan daruratnya!', 'warning');
return;
}
var fd = new FormData();
fd.append('action', 'simpan_pelaporan');
fd.append('lat', userLat);
fd.append('lng', userLng);
fd.append('nama', nama || 'Anonim');
fd.append('laporan', laporan);
Swal.fire({ title: 'Mengirim...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
tutupLapor();
document.getElementById('w_laporan').value = '';
Swal.fire({icon: 'success', title: 'Terkirim', text: 'Laporan darurat berhasil dikirim! Petugas akan segera memproses.', timer: 3000, showConfirmButton: false});
loadData(); // Refresh map
}
})
.catch(err => {
Swal.fire('Error', 'Terjadi kesalahan jaringan.', 'error');
});
}
loadData();
setInterval(loadData, 15000); // Poll every 15s to see other reports
</script>
</body>
</html>
+749
View File
@@ -0,0 +1,749 @@
var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Layer Groups yang Spesifik
var layerMasjid = L.layerGroup().addTo(map);
var layerMasjidRadius = L.layerGroup().addTo(map);
var layerPelaporan = L.layerGroup().addTo(map);
var layerWarga = L.layerGroup().addTo(map);
var layerSpbu24 = L.layerGroup().addTo(map);
var layerSpbuNon24 = L.layerGroup().addTo(map);
var layerLines = L.layerGroup().addTo(map);
var layerPolygons = L.layerGroup().addTo(map);
// Layers Control
var overlays = {};
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
overlays["🕌 Tempat Ibadah"] = layerMasjid;
overlays["📡 Radius Ibadah/Bantuan"] = layerMasjidRadius;
overlays["🚨 Pelaporan Darurat"] = layerPelaporan;
overlays["📍 Warga Rentan"] = layerWarga;
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
overlays["⛽ SPBU 24 Jam (Hijau)"] = layerSpbu24;
overlays["⛽ SPBU Terbatas (Merah)"] = layerSpbuNon24;
overlays["🛣️ Jalan (Polyline)"] = layerLines;
overlays["🟩 Kavling (Polygon)"] = layerPolygons;
}
L.control.layers(null, overlays, { collapsed: false, position: 'topright' }).addTo(map);
var drawnItems = new L.FeatureGroup().addTo(map);
var currentRadius = 500;
var cachedData = null;
var drawControl = new L.Control.Draw({
draw: {
marker:false, circle:false, circlemarker:false,
polyline: { shapeOptions: { color: 'blue', weight: 4 } },
polygon: { shapeOptions: { color: 'red' } },
rectangle: false
},
edit: { featureGroup: drawnItems }
});
map.addControl(drawControl);
// Locate Control
var locateBtn = L.control({position: 'topleft'});
locateBtn.onAdd = function (map) {
var div = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
div.style.backgroundColor = 'white';
div.style.width = '34px';
div.style.height = '34px';
div.style.cursor = 'pointer';
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.justifyContent = 'center';
div.innerHTML = '<i class="fas fa-crosshairs" style="font-size:1.2rem; color:#2563eb;"></i>';
div.title = "Lokasi Anda Saat Ini";
div.onclick = function(){ map.locate({setView: true, maxZoom: 16}); }
return div;
};
locateBtn.addTo(map);
map.on('locationfound', function(e) {
L.circleMarker(e.latlng, {radius: 8, fillColor: '#3b82f6', color: '#fff', weight: 3, fillOpacity: 1}).addTo(map).bindPopup("Lokasi Anda").openPopup();
});
// Event Draw
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var type = e.layerType;
drawnItems.addLayer(layer);
var panel = document.getElementById('panelInput');
if (panel) panel.classList.remove('collapsed');
if (type === 'polyline') {
document.getElementById('jenis').value = 'jalan';
toggleForm();
var coords = layer.getLatLngs();
document.getElementById('jalan_coords').value = JSON.stringify(coords);
// Calculate Length automatically
var length = 0;
for (var i = 0; i < coords.length - 1; i++) {
length += coords[i].distanceTo(coords[i + 1]);
}
document.getElementById('panjang_jalan').value = length.toFixed(2);
} else if (type === 'polygon') {
document.getElementById('jenis').value = 'parsil';
toggleForm();
var coords = layer.getLatLngs()[0];
document.getElementById('parsil_coords').value = JSON.stringify(coords);
}
});
map.on(L.Draw.Event.EDITED, function (e) {
var layers = e.layers;
layers.eachLayer(function (layer) {
if(layer.db_id && layer.db_tipe) {
var coords = (layer.db_tipe === 'parsil') ? layer.getLatLngs()[0] : layer.getLatLngs();
var fd = new FormData();
fd.append('action', 'update_koordinat');
fd.append('id', layer.db_id);
fd.append('tipe', layer.db_tipe);
fd.append('koordinat', JSON.stringify(coords));
if(layer.db_tipe === 'jalan') {
var length = 0;
for (var i = 0; i < coords.length - 1; i++) {
length += coords[i].distanceTo(coords[i + 1]);
}
fd.append('panjang_m', length.toFixed(2));
}
fetch('../api/api.php', { method: 'POST', body: fd });
}
});
Swal.fire({icon: 'success', title: 'Tersimpan', text: 'Perubahan bentuk berhasil disimpan!', timer: 1500, showConfirmButton: false});
});
map.on(L.Draw.Event.DELETED, function (e) {
var layers = e.layers;
layers.eachLayer(function (layer) {
if(layer.db_id && layer.db_tipe) {
var fd = new FormData();
fd.append('action', 'hapus_data');
fd.append('id', layer.db_id);
fd.append('tipe', layer.db_tipe);
fetch('../api/api.php', { method: 'POST', body: fd });
}
});
Swal.fire({icon: 'success', title: 'Terhapus', text: 'Data berhasil dihapus dari peta!', timer: 1500, showConfirmButton: false});
});
function toggleSidebar() { document.getElementById('sidebar').classList.toggle('active'); }
function toggleRightPanel() {
var panel = document.getElementById('panelInput');
if(panel) panel.classList.toggle('collapsed');
}
function filterData() {
var input = document.getElementById('searchInput').value.toLowerCase();
var cards = document.getElementsByClassName('item-data');
if(input.length > 0) document.getElementById('sidebar').classList.add('active');
for (var i = 0; i < cards.length; i++) {
cards[i].style.display = cards[i].getAttribute('data-nama').includes(input) ? "" : "none";
}
}
function changeRadius(v) {
currentRadius = parseInt(v);
document.getElementById('radiusVal').innerText = v;
if(cachedData) renderMap(cachedData);
}
var prevReportCount = -1;
function loadData(isPolling = false) {
fetch('../api/api.php?data=all')
.then(r => r.json())
.then(data => {
cachedData = data;
// Logika Notifikasi Bubble
if (prevReportCount !== -1 && data.pelaporan.length > prevReportCount) {
var newCount = data.pelaporan.length - prevReportCount;
Swal.fire({
title: 'Laporan Baru!',
text: `Ada ${newCount} laporan warga baru yang masuk.`,
icon: 'info',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000
});
}
prevReportCount = data.pelaporan.length;
var badge = document.getElementById('notifBadge');
if(badge) {
if(data.pelaporan.length > 0) {
badge.style.display = 'block';
badge.innerText = data.pelaporan.length;
} else {
badge.style.display = 'none';
}
}
if (!isPolling) {
renderSidebar(data);
renderMap(data);
} else {
// If polling, maybe just update sidebar if it's open, or wait for manual refresh
// But for real-time map, we can re-render map as well (might cause flicker if doing heavy edits)
// We'll skip re-rendering map to avoid interrupting user edits, just update the bubble
renderSidebar(data);
renderMap(data);
}
});
}
// Auto-poll every 10 seconds
setInterval(() => {
// Only poll if not currently editing (edit_id is 0)
if(document.getElementById('edit_id') && document.getElementById('edit_id').value === "0") {
loadData(true);
}
}, 10000);
function renderSidebar(data) {
var html = `<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:20px">`;
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
html += `
<div style="background:#fee2e2; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#ef4444; font-size:1.5rem">${data.pelaporan.length}</b><br><small style="color:#991b1b; font-weight:600">Laporan</small>
</div>
<div style="background:#fef3c7; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#d97706; font-size:1.5rem">${data.warga.length}</b><br><small style="color:#92400e; font-weight:600">Warga Rentan</small>
</div>`;
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
html += `
<div style="background:#dcfce7; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#16a34a; font-size:1.5rem">${data.spbu.length}</b><br><small style="color:#166534; font-weight:600">SPBU Terdata</small>
</div>
<div style="background:#e0e7ff; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#4f46e5; font-size:1.5rem">${data.jalan.length}</b><br><small style="color:#3730a3; font-weight:600">Jalur Jalan</small>
</div>`;
}
html += `</div>`;
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Pelaporan
html += `<div class="section-title" style="color:#ef4444">🚨 Laporan Darurat</div>`;
if(data.pelaporan.length === 0) html += `<small style="color:#666">Belum ada laporan</small>`;
data.pelaporan.forEach(p => {
html += `<div class="card item-data" data-nama="${p.nama_pelapor.toLowerCase()} ${p.isi_laporan.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${p.latitude}, ${p.longitude})" style="cursor:pointer">
<b>${p.nama_pelapor}</b><small><i class="far fa-clock"></i> ${p.waktu}</small><br><span>${p.isi_laporan.substring(0,40)}...</span>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('pelaporan', ${p.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${p.id}, 'pelaporan')">Hapus</button>
</div>
</div>
</div>`;
});
// Warga
html += `<div class="section-title">📍 Penduduk Miskin / Rentan</div>`;
data.warga.forEach(w => {
html += `<div class="card item-data" data-nama="${w.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${w.latitude}, ${w.longitude})" style="cursor:pointer">
<b>${w.nama}</b>
${w.riwayat_penyakit ? '<span class="badge" style="background:#fef3c7; color:#d97706;"><i class="fas fa-heartbeat"></i> Medis</span>' : ''}
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('warga', ${w.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${w.id}, 'warga')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// SPBU
html += `<div class="section-title">⛽ Stasiun Pengisian (SPBU)</div>`;
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
html += `<div class="card item-data" data-nama="${s.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${s.latitude}, ${s.longitude})" style="cursor:pointer">
<b>${s.nama}</b>
<small><i class="fas fa-phone"></i> ${s.no_hp}</small><br>
${is24 ? '<span class="badge" style="background:#dcfce7; color:#16a34a;">Buka 24 Jam</span>' : '<span class="badge" style="background:#fee2e2; color:#ef4444;">Tidak 24 Jam</span>'}
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('spbu', ${s.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${s.id}, 'spbu')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Tempat Ibadah
html += `<div class="section-title">🕌 Tempat Ibadah & Bantuan</div>`;
data.masjid.forEach(m => {
html += `<div class="card item-data" data-nama="${m.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${m.latitude}, ${m.longitude})" style="cursor:pointer"><b>${m.nama}</b></span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('masjid', ${m.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${m.id}, 'masjid')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Jalan
html += `<div class="section-title">🛣️ Manajemen Jalan</div>`;
data.jalan.forEach(j => {
var color = '#ef4444'; // default red
if(j.status_jalan === 'Nasional') color = '#ef4444';
else if(j.status_jalan === 'Provinsi') color = '#eab308';
else if(j.status_jalan === 'Kabupaten') color = '#22c55e';
html += `<div class="card item-data" data-nama="${j.nama_jalan.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span>
<b>${j.nama_jalan}</b>
<small style="color:${color}; font-weight:bold;">${j.status_jalan}</small><br>
<small>${j.panjang_m} meter</small>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('jalan', ${j.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${j.id}, 'jalan')">Hapus</button>
</div>
</div>
</div>`;
});
// Parsil
html += `<div class="section-title">🟩 Manajemen Kavling / Parsil</div>`;
data.parsil.forEach(p => {
html += `<div class="card item-data" data-nama="${p.nama_pemilik.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span>
<b>${p.nama_pemilik}</b><br>
<small>Status: <b>${p.status_hak}</b></small>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('parsil', ${p.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${p.id}, 'parsil')">Hapus</button>
</div>
</div>
</div>`;
});
}
document.getElementById('sidebarContent').innerHTML = html;
}
function renderMap(data) {
layerMasjid.clearLayers();
layerMasjidRadius.clearLayers();
layerPelaporan.clearLayers();
layerWarga.clearLayers();
layerSpbu24.clearLayers();
layerSpbuNon24.clearLayers();
layerLines.clearLayers();
layerPolygons.clearLayers();
var lokMasjid = data.masjid.map(m => L.latLng(m.latitude, m.longitude));
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Render Masjid / Tempat Ibadah
data.masjid.forEach(m => {
var popup = `<b>${m.jenis || 'Tempat Ibadah'}:</b> ${m.nama}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('masjid', ${m.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${m.id}, 'masjid')">Hapus</button>
</div>`;
var faClass = 'fa-mosque';
if(m.jenis === 'Gereja') faClass = 'fa-church';
else if(m.jenis === 'Vihara') faClass = 'fa-vihara';
else if(m.jenis === 'Pura') faClass = 'fa-torii-gate';
// Buat custom marker bentuk map pin dengan icon sesuai jenis
var masjidIcon = L.divIcon({
html: `
<div style="
width: 34px;
height: 34px;
background-color: #2563eb;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas ${faClass}" style="
transform: rotate(45deg);
color: white;
font-size: 14px;
margin-top: 2px;
margin-left: -2px;
"></i>
</div>
`,
className: '', // Hilangkan background default Leaflet
iconSize: [34, 34],
iconAnchor: [17, 40], // Anchor ujung lancip di bawah
popupAnchor: [0, -40]
});
L.marker([m.latitude, m.longitude], { icon: masjidIcon }).addTo(layerMasjid).bindPopup(popup);
L.circle([m.latitude, m.longitude], {
radius: currentRadius, color: '#2563eb', fillOpacity: 0.1, weight: 1
}).addTo(layerMasjidRadius);
});
// Render Pelaporan
data.pelaporan.forEach(p => {
var popup = `<b>🚨 Laporan:</b> ${p.isi_laporan}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('pelaporan', ${p.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${p.id}, 'pelaporan')">Hapus</button>
</div>`;
var pelaporanIcon = L.divIcon({
html: `
<div style="
width: 32px;
height: 32px;
background-color: #f97316; /* Orange color */
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-exclamation" style="
transform: rotate(45deg);
color: white;
font-size: 16px;
font-weight: bold;
margin-top: 2px;
margin-left: -2px;
"></i>
</div>
`,
className: '',
iconSize: [32, 32],
iconAnchor: [16, 38],
popupAnchor: [0, -38]
});
L.marker([p.latitude, p.longitude], {
icon: pelaporanIcon
}).addTo(layerPelaporan).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Render SPBU
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
var iconUrl = is24 ? 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png' : 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png';
var targetLayer = is24 ? layerSpbu24 : layerSpbuNon24;
var popup = `<b>⛽ ${s.nama}</b><br>Kategori: ${s.kategori === '24jam' ? 'Buka 24 Jam' : 'Tidak 24 Jam'}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('spbu', ${s.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${s.id}, 'spbu')">Hapus</button>
</div>`;
L.marker([s.latitude, s.longitude], {
icon: L.icon({ iconUrl: iconUrl, iconSize: [25, 41], iconAnchor: [12, 41] })
}).addTo(targetLayer).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Render Warga
data.warga.forEach(w => {
var pos = L.latLng(w.latitude, w.longitude);
var isCovered = lokMasjid.some(m => pos.distanceTo(m) <= currentRadius);
// Buat elemen point berbentuk map pin (teardrop) dengan ikon rumah di dalamnya
var color = isCovered ? '#16a34a' : '#ef4444'; // Hijau aman, merah di luar
var houseIcon = L.divIcon({
html: `
<div style="
width: 30px;
height: 30px;
background-color: ${color};
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
">
<div style="
width: 18px;
height: 18px;
background-color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-home" style="
transform: rotate(45deg);
color: ${color};
font-size: 10px;
"></i>
</div>
</div>
`,
className: '', // Kosongkan agar background default leaflet tidak muncul
iconSize: [30, 30],
iconAnchor: [15, 36], // Titik jangkar di ujung bawah pin
popupAnchor: [0, -36]
});
var isPenerima = (w.status_bantuan === 'Sudah') ? 'Ya' : 'Belum';
var txtPendidikan = w.pendidikan ? w.pendidikan : 'Tidak diketahui';
var txtPenyakit = (w.riwayat_penyakit && w.riwayat_penyakit.trim().length > 0) ? w.riwayat_penyakit : 'Tidak ada';
var popup = `
<div style="min-width: 200px;">
<b style="font-size: 1.1em; color: #1e293b;"><i class="fas fa-user-circle"></i> Warga: ${w.nama}</b>
<hr style="margin: 5px 0; border: 0; border-top: 1px solid #cbd5e1;">
<table style="width: 100%; font-size: 0.85em; margin-bottom: 8px;">
<tr><td style="color: #64748b; padding-right: 10px;">Jangkauan</td><td>: <b>${isCovered ? '<span style="color:#16a34a">Aman (Dekat Fasilitas)</span>' : '<span style="color:#ef4444">Di Luar</span>'}</b></td></tr>
<tr><td style="color: #64748b;">Tanggungan</td><td>: <b>${w.jumlah_kk} Jiwa</b></td></tr>
<tr><td style="color: #64748b;">Pendidikan</td><td>: ${txtPendidikan}</td></tr>
<tr><td style="color: #64748b;">Riwayat Sakit</td><td>: <span style="color: ${txtPenyakit !== 'Tidak ada' ? '#ef4444' : '#64748b'}">${txtPenyakit}</span></td></tr>
<tr><td style="color: #64748b;">Terima Bantuan</td><td>: <b>${isPenerima === 'Ya' ? '<span style="color:#16a34a">Sudah</span>' : '<span style="color:#ef4444">Belum</span>'}</b></td></tr>
</table>
<div style="font-size: 0.8em; color: #64748b; margin-bottom: 10px; background: #f8fafc; padding: 5px; border-radius: 4px; border: 1px solid #e2e8f0;">
<b>Histori:</b> ${w.histori_bantuan || '<i>Tidak ada riwayat bantuan</i>'}
</div>
<div style="text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('warga', ${w.id})">Edit Data</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${w.id}, 'warga')">Hapus</button>
</div>
</div>`;
L.marker(pos, { icon: houseIcon })
.addTo(layerWarga).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Render Jalan (Polyline)
data.jalan.forEach(j => {
var coords = JSON.parse(j.koordinat);
var color = '#ef4444'; // default red
if(j.status_jalan === 'Nasional') color = '#ef4444'; // Merah
else if(j.status_jalan === 'Provinsi') color = '#eab308'; // Kuning
else if(j.status_jalan === 'Kabupaten') color = '#22c55e'; // Hijau
var popup = `<b>🛣️ ${j.nama_jalan}</b><br>Status: ${j.status_jalan}<br>Panjang: ${j.panjang_m} meter<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('jalan', ${j.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${j.id}, 'jalan')">Hapus</button>
</div>`;
var polyline = L.polyline(coords, {color: color, weight: 6}).addTo(layerLines).bindPopup(popup);
polyline.db_id = j.id;
polyline.db_tipe = 'jalan';
drawnItems.addLayer(polyline);
});
// Render Parsil (Polygon)
data.parsil.forEach(p => {
var coords = JSON.parse(p.koordinat);
var popup = `<b>🟩 Kavling</b><br>Pemilik: ${p.nama_pemilik}<br>Status Hak: ${p.status_hak}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('parsil', ${p.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${p.id}, 'parsil')">Hapus</button>
</div>`;
var polygon = L.polygon(coords, {color: '#2563eb', fillOpacity: 0.4}).addTo(layerPolygons).bindPopup(popup);
polygon.db_id = p.id;
polygon.db_tipe = 'parsil';
drawnItems.addLayer(polygon);
});
}
}
function toggleForm() {
var j = document.getElementById('jenis').value;
['pelaporan', 'warga', 'masjid', 'spbu', 'jalan', 'parsil'].forEach(f => {
var el = document.getElementById('form_' + f);
if (el) {
el.classList.toggle('hidden', j !== f);
}
});
}
map.on('click', e => {
document.getElementById('lat').value = e.latlng.lat;
document.getElementById('lng').value = e.latlng.lng;
L.popup().setLatLng(e.latlng).setContent("Dipilih: " + e.latlng.lat.toFixed(4) + ", " + e.latlng.lng.toFixed(4)).openOn(map);
});
function simpanData() {
var j = document.getElementById('jenis').value;
var fd = new FormData();
fd.append('action', 'simpan_' + j);
fd.append('edit_id', document.getElementById('edit_id').value);
var lat = document.getElementById('lat').value;
var lng = document.getElementById('lng').value;
if (j === 'jalan') {
fd.append('nama', document.getElementById('nama_jalan').value);
fd.append('status_jalan', document.getElementById('status_jalan').value);
fd.append('panjang', document.getElementById('panjang_jalan').value);
fd.append('koordinat', document.getElementById('jalan_coords').value);
} else if (j === 'parsil') {
fd.append('nama', document.getElementById('nama_pemilik').value);
fd.append('status_hak', document.getElementById('status_hak').value);
fd.append('koordinat', document.getElementById('parsil_coords').value);
} else {
if(j !== 'geometri' && (!lat || !lng) && document.getElementById('edit_id').value === "0") {
Swal.fire({icon: 'warning', title: 'Oops...', text: 'Silakan klik titik koordinat di peta terlebih dahulu!'});
return;
}
fd.append('lat', lat); fd.append('lng', lng);
if (j === 'pelaporan') {
fd.append('nama', document.getElementById('nama_pelapor').value);
fd.append('laporan', document.getElementById('isi_laporan').value);
} else if (j === 'warga') {
fd.append('nama', document.getElementById('nama_kk').value);
fd.append('jumlah_kk', document.getElementById('jumlah_kk').value);
fd.append('tanggal_lahir', document.getElementById('tanggal_lahir').value);
fd.append('pendidikan', document.getElementById('pendidikan').value);
fd.append('riwayat_penyakit', document.getElementById('riwayat_penyakit').value);
fd.append('histori_bantuan', document.getElementById('histori_bantuan').value);
fd.append('alamat', document.getElementById('alamat_warga').value);
} else if (j === 'masjid') {
fd.append('nama', document.getElementById('nama_masjid').value);
fd.append('jenis', document.getElementById('jenis_masjid').value);
fd.append('pic', document.getElementById('pic_masjid').value);
fd.append('alamat', document.getElementById('alamat_masjid').value);
} else if (j === 'spbu') {
fd.append('nama', document.getElementById('nama_spbu').value);
fd.append('no_hp', document.getElementById('no_hp_spbu').value);
fd.append('kategori', document.getElementById('kategori_spbu').value);
}
}
Swal.fire({ title: 'Menyimpan...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
Swal.fire({icon: 'success', title: 'Berhasil', text: 'Data berhasil disimpan!', timer: 1500, showConfirmButton: false})
.then(() => location.reload());
}
});
}
function hapusData(id, tipe) {
Swal.fire({
title: 'Hapus Data?', text: "Data yang dihapus tidak dapat dikembalikan!", icon: 'warning',
showCancelButton: true, confirmButtonColor: '#ef4444', cancelButtonColor: '#94a3b8', confirmButtonText: 'Ya, Hapus!'
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({ title: 'Menghapus...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
var fd = new FormData();
fd.append('action', 'hapus_data');
fd.append('id', id);
fd.append('tipe', tipe);
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
location.reload();
}
});
}
});
}
function fokus(lat, lng) { map.flyTo([lat, lng], 18); }
function batalEdit() {
document.getElementById('edit_id').value = "0";
document.getElementById('btnSimpanUtama').innerText = "Simpan Data";
document.getElementById('btnBatalEdit').style.display = "none";
document.getElementById('lat').value = "";
document.getElementById('lng').value = "";
}
function editData(tipe, id) {
var item = cachedData[tipe].find(x => x.id == id);
if(!item) return;
document.getElementById('edit_id').value = id;
document.getElementById('jenis').value = tipe;
toggleForm();
var panel = document.getElementById('panelInput');
if(panel) panel.classList.remove('collapsed');
if(tipe == 'warga') {
document.getElementById('nama_kk').value = item.nama;
document.getElementById('jumlah_kk').value = item.jumlah_kk;
document.getElementById('tanggal_lahir').value = item.tanggal_lahir || '';
document.getElementById('pendidikan').value = item.pendidikan || '';
document.getElementById('riwayat_penyakit').value = item.riwayat_penyakit || '';
document.getElementById('histori_bantuan').value = item.histori_bantuan || '';
document.getElementById('alamat_warga').value = item.alamat || '';
} else if (tipe == 'pelaporan') {
document.getElementById('nama_pelapor').value = item.nama_pelapor;
document.getElementById('isi_laporan').value = item.isi_laporan;
} else if (tipe == 'spbu') {
document.getElementById('nama_spbu').value = item.nama;
document.getElementById('no_hp_spbu').value = item.no_hp;
document.getElementById('kategori_spbu').value = item.kategori;
} else if (tipe == 'masjid') {
document.getElementById('nama_masjid').value = item.nama;
document.getElementById('pic_masjid').value = item.pic;
document.getElementById('alamat_masjid').value = item.alamat;
} else if (tipe == 'jalan') {
document.getElementById('nama_jalan').value = item.nama_jalan;
document.getElementById('status_jalan').value = item.status_jalan;
document.getElementById('panjang_jalan').value = item.panjang_m;
document.getElementById('jalan_coords').value = item.koordinat;
} else if (tipe == 'parsil') {
document.getElementById('nama_pemilik').value = item.nama_pemilik;
document.getElementById('status_hak').value = item.status_hak;
document.getElementById('parsil_coords').value = item.koordinat;
}
if(item.latitude && item.longitude) {
document.getElementById('lat').value = item.latitude;
document.getElementById('lng').value = item.longitude;
fokus(item.latitude, item.longitude);
}
document.getElementById('btnSimpanUtama').innerText = "Update Data";
document.getElementById('btnBatalEdit').style.display = "inline-block";
}
// INIT
loadData();
+149
View File
@@ -0,0 +1,149 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--dark: #0f172a;
--dark-grad: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
--success: #10b981;
--success-hover: #059669;
--danger: #ef4444;
--danger-hover: #dc2626;
--warning: #f59e0b;
--bg-light: #f8fafc;
--border: #e2e8f0;
}
body {
margin: 0; padding: 0;
font-family: 'Inter', sans-serif;
overflow: hidden; display: flex;
background: #e2e8f0;
}
#map { width: 100vw; height: 100vh; z-index: 1; }
/* Custom Sleek Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
/* SEARCH & MENU */
.search-container { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1100; width: 380px; }
.search-box {
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px);
border-radius: 50px; display: flex; align-items: center; padding: 5px 20px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.5);
transition: 0.3s ease;
}
.search-box:focus-within { box-shadow: 0 10px 30px rgba(37,99,235,0.2); border-color: var(--primary); }
.search-box i { color: #64748b; font-size: 1.1rem; }
.search-box input[type="text"] {
border: none !important; outline: none; padding: 12px 10px; margin: 0;
width: 100%; background: transparent !important; font-size: 0.95rem;
color: var(--dark); font-weight: 500; box-shadow: none !important;
}
.search-box input::placeholder { color: #94a3b8; }
.menu-toggle {
position: absolute; bottom: 30px; left: 20px; z-index: 1200;
background: var(--dark-grad); color: white; border: none;
width: 55px; height: 55px; border-radius: 50%; cursor: pointer;
box-shadow: 0 10px 25px rgba(15,23,42,0.4); font-size: 1.4rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.menu-toggle:hover { transform: scale(1.1) rotate(5deg); }
.menu-toggle:active { transform: scale(0.95); }
/* SIDEBAR */
#sidebar {
width: 360px; height: 100vh; background: white; border-right: 1px solid var(--border);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 1150; position: absolute; left: 0; transform: translateX(-100%);
display: flex; flex-direction: column; box-shadow: 5px 0 25px rgba(0,0,0,0.05);
}
#sidebar.active { transform: translateX(0); }
.sidebar-header { padding: 25px 20px; background: var(--dark-grad); color: white; border-bottom: 3px solid var(--primary); }
.sidebar-header h3 { margin: 0 0 5px 0; font-size: 1.4rem; font-weight: 700; letter-spacing: -0.5px; }
.sidebar-header small { color: #cbd5e1; font-weight: 500; font-size: 0.85rem;}
.sidebar-content { padding: 20px; overflow-y: auto; flex: 1; background: var(--bg-light); }
/* PANEL INPUT */
.panel-input {
position: absolute; top: 20px; right: 20px; width: 320px;
background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(10px);
border-radius: 16px; padding: 25px 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.12);
z-index: 1000; max-height: 85vh; overflow-y: auto; border: 1px solid rgba(255,255,255,0.6);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
transform-origin: top right;
}
.panel-input.collapsed {
transform: scale(0.9) translateX(150%);
opacity: 0;
pointer-events: none;
}
.panel-input h4 { color: var(--dark); font-size: 1.1rem; font-weight: 700; border-bottom: 2px solid var(--border); padding-bottom: 10px; margin-top: 0; }
/* RADIUS SLIDER */
.radius-box {
background: #eff6ff; padding: 15px; border-radius: 12px; margin-bottom: 20px;
border: 1px solid #bfdbfe; box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
}
.radius-box label { font-size: 0.85rem; font-weight: 700; color: var(--primary); display: block; margin-bottom: 8px;}
input[type=range] { width: 100%; cursor: pointer; accent-color: var(--primary); }
/* UI HELPERS */
input[type=text], input[type=number], input[type=date], select, textarea {
width: 100%; padding: 12px 15px; margin: 0 0 15px;
border: 1px solid #cbd5e1; border-radius: 8px; box-sizing: border-box;
transition: all 0.3s ease; font-family: 'Inter', sans-serif; font-size: 0.9rem;
background: #f8fafc; color: var(--dark);
}
input:focus, select:focus, textarea:focus {
border-color: var(--primary); outline: none; background: white;
box-shadow: 0 0 0 4px rgba(37,99,235,0.15); transform: translateY(-1px);
}
select { cursor: pointer; font-weight: 500; }
textarea { resize: vertical; min-height: 80px; }
/* CARDS IN SIDEBAR */
.card {
background: #ffffff; padding: 15px; border-radius: 12px; margin-bottom: 12px;
border: 1px solid var(--border); font-size: 0.85rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.02); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; overflow: hidden;
}
.card::before { content:''; position:absolute; left:0; top:0; width:4px; height:100%; background: var(--primary); opacity: 0; transition: 0.3s; }
.card:hover { transform: translateY(-3px); box-shadow: 0 10px 15px rgba(0,0,0,0.06); border-color: #cbd5e1; }
.card:hover::before { opacity: 1; }
.card b { color: var(--dark); display: block; font-size: 0.95rem; margin-bottom: 4px; font-weight: 700;}
.hidden { display: none !important; }
/* BUTTONS */
.btn-save {
background: var(--success); color: white; border: none; padding: 14px;
border-radius: 10px; font-weight: 700; cursor: pointer; width: 100%;
font-size: 1rem; transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.3); text-transform: uppercase; letter-spacing: 0.5px;
}
.btn-save:hover { background: var(--success-hover); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); }
.btn-save:active { transform: translateY(0); }
.btn-del {
background: #fee2e2; color: var(--danger); border: none; padding: 5px 10px;
border-radius: 6px; font-size: 0.75rem; font-weight: 600; cursor: pointer; transition: 0.2s;
}
.btn-del:hover { background: var(--danger); color: white; }
.badge {
display: inline-block; padding: 4px 8px; border-radius: 6px;
font-size: 0.7rem; font-weight: 700; margin-top: 5px; text-transform: uppercase; letter-spacing: 0.5px;
}
.section-title {
font-size: 1rem; font-weight: 700; color: var(--dark);
margin: 20px 0 12px 0; border-bottom: 2px solid var(--border);
padding-bottom: 8px; display: flex; align-items: center; gap: 8px;
}