commit 6d0b24d2fe1d6372011e584cbe0cbddd73a027c9 Author: raditarahman5-cloud Date: Tue Jun 9 18:19:10 2026 +0700 first commit UasSIGWebgis diff --git a/CREATE DATABASE webgis_spbu;.sql b/CREATE DATABASE webgis_spbu;.sql new file mode 100644 index 0000000..0209d3a --- /dev/null +++ b/CREATE DATABASE webgis_spbu;.sql @@ -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); diff --git a/Untitled-1.html b/Untitled-1.html new file mode 100644 index 0000000..c04302f --- /dev/null +++ b/Untitled-1.html @@ -0,0 +1,94 @@ + + + + +WebGIS Kemiskinan + + + + + + + + +
+ +
+

Input Warga

+ + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/api/api.php b/api/api.php new file mode 100644 index 0000000..517038b --- /dev/null +++ b/api/api.php @@ -0,0 +1,197 @@ +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; +} +?> diff --git a/api/db.php b/api/db.php new file mode 100644 index 0000000..60931b7 --- /dev/null +++ b/api/db.php @@ -0,0 +1,244 @@ +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')"); +} +?> diff --git a/check_db.php b/check_db.php new file mode 100644 index 0000000..8661bac --- /dev/null +++ b/check_db.php @@ -0,0 +1,9 @@ +query('SELECT * FROM rumah_ibadah'); +$rows = $res->fetch_all(MYSQLI_ASSOC); +echo "Total data: " . count($rows) . "\n"; +print_r($rows); +?> diff --git a/codeanalys.md b/codeanalys.md new file mode 100644 index 0000000..eb510e7 --- /dev/null +++ b/codeanalys.md @@ -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. diff --git a/index.php b/index.php new file mode 100644 index 0000000..5b8c789 --- /dev/null +++ b/index.php @@ -0,0 +1,5 @@ + diff --git a/infra/index.php b/infra/index.php new file mode 100644 index 0000000..c6d7b1b --- /dev/null +++ b/infra/index.php @@ -0,0 +1,134 @@ + + + + + WebGIS Poverty Mapping - Dashboard Infrastruktur + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+

Form Input & Laporan

+ +
+ + + + + +
+ + + +
+ + + + + + + + +
+ + +
+
+ + + + + + + + \ No newline at end of file diff --git a/infra/script.js b/infra/script.js new file mode 100644 index 0000000..f8a52dc --- /dev/null +++ b/infra/script.js @@ -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 = ''; + 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 = `
`; + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + html += ` +
+ ${data.pelaporan.length}
Laporan +
+
+ ${data.warga.length}
Warga Rentan +
`; + } + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + html += ` +
+ ${data.spbu.length}
SPBU Terdata +
+
+ ${data.jalan.length}
Jalur Jalan +
`; + } + html += `
`; + + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + // Pelaporan + html += `
🚨 Laporan Darurat
`; + if(data.pelaporan.length === 0) html += `Belum ada laporan`; + data.pelaporan.forEach(p => { + html += `
+
+ + ${p.nama_pelapor} ${p.waktu}
${p.isi_laporan.substring(0,40)}... +
+
+
+ +
+
+
`; + }); + + // Warga + html += `
📍 Penduduk Miskin / Rentan
`; + data.warga.forEach(w => { + html += `
+
+ + ${w.nama} + ${w.riwayat_penyakit ? ' Medis' : ''} + +
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + // SPBU + html += `
⛽ Stasiun Pengisian (SPBU)
`; + data.spbu.forEach(s => { + var is24 = s.kategori === '24jam'; + html += `
+
+ + ${s.nama} + ${s.no_hp}
+ ${is24 ? 'Buka 24 Jam' : 'Tidak 24 Jam'} +
+
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + // Tempat Ibadah + html += `
🕌 Tempat Ibadah & Bantuan
`; + data.masjid.forEach(m => { + html += `
+
+ ${m.nama} +
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + // Jalan + html += `
🛣️ Manajemen Jalan
`; + 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 += `
+
+ + ${j.nama_jalan} + ${j.status_jalan}
+ ${j.panjang_m} meter +
+
+
+ +
+
+
`; + }); + + // Parsil + html += `
🟩 Manajemen Kavling / Parsil
`; + data.parsil.forEach(p => { + html += `
+
+ + ${p.nama_pemilik}
+ Status: ${p.status_hak} +
+
+
+ +
+
+
`; + }); + } + + 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 = `${m.jenis || 'Tempat Ibadah'}: ${m.nama}
+
+ + +
`; + 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: ` +
+ +
+ `, + 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 = `🚨 Laporan: ${p.isi_laporan}
+
+ + +
`; + var pelaporanIcon = L.divIcon({ + html: ` +
+ +
+ `, + 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 = `⛽ ${s.nama}
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); + }); + } + + 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: ` +
+
+ +
+
+ `, + 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 = ` +
+ Warga: ${w.nama} +
+ + + + + + +
Jangkauan: ${isCovered ? 'Aman (Dekat Fasilitas)' : 'Di Luar'}
Tanggungan: ${w.jumlah_kk} Jiwa
Pendidikan: ${txtPendidikan}
Riwayat Sakit: ${txtPenyakit}
Terima Bantuan: ${isPenerima === 'Ya' ? 'Sudah' : 'Belum'}
+
+ Histori: ${w.histori_bantuan || 'Tidak ada riwayat bantuan'} +
+
+ + +
+
`; + 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 = `🛣️ ${j.nama_jalan}
Status: ${j.status_jalan}
Panjang: ${j.panjang_m} meter
+
+ + +
`; + 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 = `🟩 Kavling
Pemilik: ${p.nama_pemilik}
Status Hak: ${p.status_hak}
+
+ + +
`; + 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(); diff --git a/infra/style.css b/infra/style.css new file mode 100644 index 0000000..1876d9c --- /dev/null +++ b/infra/style.css @@ -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; +} \ No newline at end of file diff --git a/insert_jalan_parsil.php b/insert_jalan_parsil.php new file mode 100644 index 0000000..4e67539 --- /dev/null +++ b/insert_jalan_parsil.php @@ -0,0 +1,25 @@ +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)."; +?> diff --git a/out.json b/out.json new file mode 100644 index 0000000..ffacbf9 Binary files /dev/null and b/out.json differ diff --git a/portal/index.php b/portal/index.php new file mode 100644 index 0000000..b1a2512 --- /dev/null +++ b/portal/index.php @@ -0,0 +1,183 @@ + + + + + + + Portal Utama - WebGIS Poverty Mapping + + + + + + + + +
+

WebGIS Poverty Mapping

+

Pusat Integrasi Data GIS Kota Pontianak

+
+ +
+ +
+ +

Fasilitas & Tata Kota

+

Manajemen data spasial fasilitas umum, jaringan jalan raya, batas kepemilikan lahan, dan lokasi SPBU.

+ Buka Dashboard +
+ + +
+ +

Sosial & Kemasyarakatan

+

Pemetaan kawasan rentan, distribusi penduduk, pemantauan rumah ibadah, dan pelaporan darurat.

+ Buka Dashboard +
+ + +
+ +

Portal Publik (Warga)

+

Akses terbuka untuk masyarakat. Lihat fasilitas umum terdekat dan lakukan pelaporan cepat kedaruratan.

+ Akses Publik +
+
+ + + \ No newline at end of file diff --git a/portal/login.php b/portal/login.php new file mode 100644 index 0000000..2c934ef --- /dev/null +++ b/portal/login.php @@ -0,0 +1,322 @@ +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!'; + } +} +?> + + + + + + Login - WebGIS Poverty Mapping + + + + + +
+
+
+ +
+ + + +
+ +
+ + +
+
+ + +
+
+ + + + + +
+ +
+ + +
+ + + + diff --git a/portal/logout.php b/portal/logout.php new file mode 100644 index 0000000..ea82c68 --- /dev/null +++ b/portal/logout.php @@ -0,0 +1,7 @@ + diff --git a/portal/style.css b/portal/style.css new file mode 100644 index 0000000..1876d9c --- /dev/null +++ b/portal/style.css @@ -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; +} \ No newline at end of file diff --git a/sosial/index.php b/sosial/index.php new file mode 100644 index 0000000..b7e162a --- /dev/null +++ b/sosial/index.php @@ -0,0 +1,139 @@ + + + + + WebGIS Poverty Mapping - Dashboard Sosial & Warga + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+

Form Input & Laporan

+ +
+ +
+ + +
+ + + +
+

*Gunakan ini untuk lapor kasus gizi buruk, sakit, dll.

+ + +
+ + + + + + + + + + +
+ + +
+
+ + + + + + + + \ No newline at end of file diff --git a/sosial/script.js b/sosial/script.js new file mode 100644 index 0000000..57f0b58 --- /dev/null +++ b/sosial/script.js @@ -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 = ''; + 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 = `
`; + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + html += ` +
+ ${data.pelaporan.length}
Laporan +
+
+ ${data.warga.length}
Warga Rentan +
`; + } + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + html += ` +
+ ${data.spbu.length}
SPBU Terdata +
+
+ ${data.jalan.length}
Jalur Jalan +
`; + } + html += `
`; + + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + // Pelaporan + html += `
🚨 Laporan Darurat
`; + if(data.pelaporan.length === 0) html += `Belum ada laporan`; + data.pelaporan.forEach(p => { + html += `
+
+ + ${p.nama_pelapor} ${p.waktu}
${p.isi_laporan.substring(0,40)}... +
+
+
+ +
+
+
`; + }); + + // Warga + html += `
📍 Penduduk Miskin / Rentan
`; + 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 ? `
Jangkauan: ${nearbyFacilities[0].nama} (${Math.round(nearbyFacilities[0].distance)}m)` : `
Jauh dari fasilitas / bantuan`; + + html += `
+
+ + ${w.nama.replace(/\s*\(.*?\)\s*/g, '')} + ${w.riwayat_penyakit ? ' Medis' : ''} + ${facilityInfo} + +
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + // SPBU + html += `
⛽ Stasiun Pengisian (SPBU)
`; + data.spbu.forEach(s => { + var is24 = s.kategori === '24jam'; + html += `
+
+ + ${s.nama} + ${s.no_hp}
+ ${is24 ? 'Buka 24 Jam' : 'Tidak 24 Jam'} +
+
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + // Tempat Ibadah + html += `
🕌 Tempat Ibadah & Bantuan
`; + data.masjid.forEach(m => { + html += `
+
+ ${m.nama} +
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + // Jalan + html += `
🛣️ Manajemen Jalan
`; + 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 += `
+
+ + ${j.nama_jalan} + ${j.status_jalan}
+ ${j.panjang_m} meter +
+
+
+ +
+
+
`; + }); + + // Parsil + html += `
🟩 Manajemen Kavling / Parsil
`; + data.parsil.forEach(p => { + html += `
+
+ + ${p.nama_pemilik}
+ Status: ${p.status_hak} +
+
+
+ +
+
+
`; + }); + } + + 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 = `${m.jenis || 'Tempat Ibadah'}: ${m.nama}
+
+ + +
`; + 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: ` +
+ +
+ `, + 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 = `🚨 Laporan: ${p.isi_laporan}
+
+ + +
`; + var pelaporanIcon = L.divIcon({ + html: ` +
+ +
+ `, + 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 = `⛽ ${s.nama}
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); + }); + } + + 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: ` +
+
+ +
+
+ `, + 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 = ` +
+ Warga: ${w.nama.replace(/\s*\(.*?\)\s*/g, '')} +
+ + + ${isCovered ? `` : ''} + + + + +
Jangkauan: ${isCovered ? 'Aman (Dalam Radius)' : 'Di Luar'}
Fasilitas Trdkt: ${nearestName} (${nearestDist})
Tanggungan: ${w.jumlah_kk} Jiwa
Pendidikan: ${txtPendidikan}
Riwayat Sakit: ${txtPenyakit}
Terima Bantuan: ${isPenerima === 'Ya' ? 'Sudah' : 'Belum'}
+
+ Histori: ${w.histori_bantuan || 'Tidak ada riwayat bantuan'} +
+
+ + +
+
`; + 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 = `🛣️ ${j.nama_jalan}
Status: ${j.status_jalan}
Panjang: ${j.panjang_m} meter
+
+ + +
`; + 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 = `🟩 Kavling
Pemilik: ${p.nama_pemilik}
Status Hak: ${p.status_hak}
+
+ + +
`; + 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(); diff --git a/sosial/style.css b/sosial/style.css new file mode 100644 index 0000000..1876d9c --- /dev/null +++ b/sosial/style.css @@ -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; +} \ No newline at end of file diff --git a/test.php b/test.php new file mode 100644 index 0000000..a31d5c0 --- /dev/null +++ b/test.php @@ -0,0 +1,16 @@ +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!"; +} +?> diff --git a/test2.php b/test2.php new file mode 100644 index 0000000..ea40477 --- /dev/null +++ b/test2.php @@ -0,0 +1,7 @@ + diff --git a/test3.php b/test3.php new file mode 100644 index 0000000..0237e1a --- /dev/null +++ b/test3.php @@ -0,0 +1,7 @@ +query("SELECT * FROM rumah_ibadah"); +echo count($r->fetch_all(MYSQLI_ASSOC)); +?> diff --git a/warga/index.php b/warga/index.php new file mode 100644 index 0000000..33ecdb4 --- /dev/null +++ b/warga/index.php @@ -0,0 +1,298 @@ + + + + + + Portal Warga - WebGIS Poverty Mapping + + + + + + + + +
+ +
+

Portal Warga GIS

+ Informasi Fasilitas & Pelaporan Darurat +
+
+ + + + + + +
+
+ +
+ + + +
+
+

Form Lapor Darurat

+

Pastikan lokasi Anda sudah sesuai di peta sebelum mengirim laporan ini.

+ + + + +
+ + +
+
+
+ + + + + diff --git a/warga/script.js b/warga/script.js new file mode 100644 index 0000000..3e5e71c --- /dev/null +++ b/warga/script.js @@ -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 = ''; + 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 = `
`; + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + html += ` +
+ ${data.pelaporan.length}
Laporan +
+
+ ${data.warga.length}
Warga Rentan +
`; + } + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + html += ` +
+ ${data.spbu.length}
SPBU Terdata +
+
+ ${data.jalan.length}
Jalur Jalan +
`; + } + html += `
`; + + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + // Pelaporan + html += `
🚨 Laporan Darurat
`; + if(data.pelaporan.length === 0) html += `Belum ada laporan`; + data.pelaporan.forEach(p => { + html += `
+
+ + ${p.nama_pelapor} ${p.waktu}
${p.isi_laporan.substring(0,40)}... +
+
+
+ +
+
+
`; + }); + + // Warga + html += `
📍 Penduduk Miskin / Rentan
`; + data.warga.forEach(w => { + html += `
+
+ + ${w.nama} + ${w.riwayat_penyakit ? ' Medis' : ''} + +
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + // SPBU + html += `
⛽ Stasiun Pengisian (SPBU)
`; + data.spbu.forEach(s => { + var is24 = s.kategori === '24jam'; + html += `
+
+ + ${s.nama} + ${s.no_hp}
+ ${is24 ? 'Buka 24 Jam' : 'Tidak 24 Jam'} +
+
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') { + // Tempat Ibadah + html += `
🕌 Tempat Ibadah & Bantuan
`; + data.masjid.forEach(m => { + html += `
+
+ ${m.nama} +
+
+ +
+
+
`; + }); + } + + if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') { + // Jalan + html += `
🛣️ Manajemen Jalan
`; + 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 += `
+
+ + ${j.nama_jalan} + ${j.status_jalan}
+ ${j.panjang_m} meter +
+
+
+ +
+
+
`; + }); + + // Parsil + html += `
🟩 Manajemen Kavling / Parsil
`; + data.parsil.forEach(p => { + html += `
+
+ + ${p.nama_pemilik}
+ Status: ${p.status_hak} +
+
+
+ +
+
+
`; + }); + } + + 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 = `${m.jenis || 'Tempat Ibadah'}: ${m.nama}
+
+ + +
`; + 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: ` +
+ +
+ `, + 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 = `🚨 Laporan: ${p.isi_laporan}
+
+ + +
`; + var pelaporanIcon = L.divIcon({ + html: ` +
+ +
+ `, + 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 = `⛽ ${s.nama}
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); + }); + } + + 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: ` +
+
+ +
+
+ `, + 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 = ` +
+ Warga: ${w.nama} +
+ + + + + + +
Jangkauan: ${isCovered ? 'Aman (Dekat Fasilitas)' : 'Di Luar'}
Tanggungan: ${w.jumlah_kk} Jiwa
Pendidikan: ${txtPendidikan}
Riwayat Sakit: ${txtPenyakit}
Terima Bantuan: ${isPenerima === 'Ya' ? 'Sudah' : 'Belum'}
+
+ Histori: ${w.histori_bantuan || 'Tidak ada riwayat bantuan'} +
+
+ + +
+
`; + 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 = `🛣️ ${j.nama_jalan}
Status: ${j.status_jalan}
Panjang: ${j.panjang_m} meter
+
+ + +
`; + 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 = `🟩 Kavling
Pemilik: ${p.nama_pemilik}
Status Hak: ${p.status_hak}
+
+ + +
`; + 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(); diff --git a/warga/style.css b/warga/style.css new file mode 100644 index 0000000..1876d9c --- /dev/null +++ b/warga/style.css @@ -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; +} \ No newline at end of file