diff --git a/README.md b/README.md index e12869f..5ba2575 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,26 @@ # WebGIS Pemetaan Kemiskinan Kota Pontianak -Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, sebaran penduduk miskin, log bantuan, rumah ibadah, SPBU, serta data jalan dan parsil tanah di Kota Pontianak. +Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, sebaran penduduk miskin, log bantuan, dan rumah ibadah di Kota Pontianak. + +> [!WARNING] +> **DISCLAIMER — DATA DUMMY / BUKAN DATA NYATA** +> +> Seluruh data yang ditampilkan dalam aplikasi ini, termasuk namun tidak terbatas pada: +> - Data nama, lokasi, dan kategori **penduduk miskin** +> - **Foto rumah** dan **Foto Kartu Keluarga (KK)** yang diunggah +> - Data **rumah ibadah** beserta radius jangkauannya +> - **Log bantuan** yang tercatat di sistem +> +> adalah **data fiktif / dummy** yang dibuat semata-mata untuk keperluan **demonstrasi, pengembangan, dan pengujian sistem** ini. +> Data tersebut **tidak mewakili kondisi, identitas, atau situasi nyata** dari individu maupun lokasi mana pun di Kota Pontianak. ## Fitur Utama - **Pemetaan Interaktif**: Visualisasi peta dengan Leaflet.js yang berpusat di Kota Pontianak. - **Manajemen Data Spasial**: - - **SPBU**: Lokasi titik SPBU beserta informasi layanan 24 jam. - - **Jalan**: Fitur line/polyline dengan kategori status jalan (Nasional, Provinsi, Kabupaten). - - **Parsil**: Fitur area/polygon tanah dengan kategori sertifikasi (SHM, HGB, HGU, HP). - **Rumah Ibadah**: Lokasi titik rumah ibadah (Masjid, Gereja, Vihara, dll) beserta jangkauan radius jemaah/bantuan. - **Penduduk Miskin**: Pemetaan warga kurang mampu dan integrasi log/riwayat bantuan sosial yang disalurkan melalui rumah ibadah terdekat. -- **Pencarian Cepat**: Cari data jalan, parsil, SPBU, atau rumah ibadah langsung dari bilah pencarian. +- **Pencarian Cepat**: Cari data rumah ibadah langsung dari bilah pencarian. - **Impor GeoJSON Eksternal**: Pengguna dapat menambahkan file GeoJSON dari perangkat lokal ke dalam peta. ## Persyaratan Sistem @@ -48,13 +57,28 @@ Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, s - Buka browser dan navigasikan ke alamat: [http://localhost/webgis/webgis-poverty-pontianak/index.php](http://localhost/webgis/webgis-poverty-pontianak/index.php) +## Akun Akses Default (Login) + +Anda dapat masuk ke dalam sistem menggunakan akun simulasi berikut: + +- **Akun Administrator (Admin)**: + - **Username**: `admin` + - **Password**: `password` + - **Hak Akses**: Mengelola data spasial rumah ibadah, penduduk miskin, mengelola akun pengelola rumah ibadah, dan manajemen pengguna secara penuh. + +- **Akun Pengelola Rumah Ibadah**: + - **Username**: `pengelola_mujahidin` + - **Password**: `password` + - **Hak Akses**: Mengelola (tambah, edit, hapus, impor, geser lokasi) data penduduk miskin, serta menginput log bantuan sosial yang disalurkan melalui masjid kelolaannya (Masjid Raya Mujahidin). + ## Struktur Folder - `api/` — Kumpulan REST API endpoint untuk operasi CRUD masing-masing fitur. - - `jalan/`, `parsil/`, `spbu/`, `rumah_ibadah/`, `penduduk_miskin/`, `log_bantuan/` + - `rumah_ibadah/`, `penduduk_miskin/`, `log_bantuan/` - `assets/` — File aset frontend. - `css/` — Gaya UI (custom style.css). - `js/` — Logika frontend peta dan pengelolaan interaksi panel data. - - `features/` — Modul fitur Leaflet (menggambar peta, manajemen layer, geolokasi). + - `features/` — Modul fitur Leaflet (manajemen layer, geolokasi, dll). - `index.php` — Halaman utama interface WebGIS. -- `database.sql` — Skema database beserta data dummy Pontianak untuk pengetesan awal. +- `database.sql` — Skema database beserta **data dummy** Pontianak untuk pengetesan awal. Seluruh data di dalamnya bersifat fiktif dan tidak merepresentasikan data nyata. +- `uploads/` — Folder penyimpanan foto yang diunggah. File yang sudah ada merupakan contoh dummy semata. diff --git a/api/auth_helper.php b/api/auth_helper.php new file mode 100644 index 0000000..fef32e9 --- /dev/null +++ b/api/auth_helper.php @@ -0,0 +1,56 @@ + $_SESSION['user_id'], + 'username' => $_SESSION['username'], + 'role' => $_SESSION['role'], + 'ibadah_id' => $_SESSION['ibadah_id'] ? intval($_SESSION['ibadah_id']) : null + ]; +} + +function requireLogin() { + if (!isLoggedIn()) { + header('HTTP/1.1 401 Unauthorized'); + echo json_encode(['status' => 'error', 'message' => 'Unauthorized: Silakan login terlebih dahulu.']); + exit; + } +} + +function requireAdmin() { + requireLogin(); + if ($_SESSION['role'] !== 'admin') { + header('HTTP/1.1 403 Forbidden'); + echo json_encode(['status' => 'error', 'message' => 'Forbidden: Akses ditolak. Hanya Admin yang diizinkan.']); + exit; + } +} + +function requireAdminOrPengelola() { + requireLogin(); + if ($_SESSION['role'] !== 'admin' && $_SESSION['role'] !== 'pengelola') { + header('HTTP/1.1 403 Forbidden'); + echo json_encode(['status' => 'error', 'message' => 'Forbidden: Akses ditolak.']); + exit; + } +} + +function canManageIbadah($ibadah_id) { + if (!isLoggedIn()) return false; + if ($_SESSION['role'] === 'admin') return true; + if ($_SESSION['role'] === 'pengelola') { + return intval($_SESSION['ibadah_id']) === intval($ibadah_id); + } + return false; +} +?> diff --git a/api/check_session.php b/api/check_session.php new file mode 100644 index 0000000..b9d2f69 --- /dev/null +++ b/api/check_session.php @@ -0,0 +1,27 @@ + 'success', + 'isLoggedIn' => true, + 'data' => [ + 'id' => intval($_SESSION['user_id']), + 'username' => $_SESSION['username'], + 'role' => $_SESSION['role'], + 'ibadah_id' => $_SESSION['ibadah_id'] ? intval($_SESSION['ibadah_id']) : null, + 'nama_ibadah' => $_SESSION['nama_ibadah'] ?? null + ] + ]); +} else { + echo json_encode([ + 'status' => 'success', + 'isLoggedIn' => false, + 'data' => null + ]); +} +?> diff --git a/api/jalan/create.php b/api/jalan/create.php deleted file mode 100644 index a2bcd5b..0000000 --- a/api/jalan/create.php +++ /dev/null @@ -1,23 +0,0 @@ -geom)) { - $nama = $conn->real_escape_string($data->nama ?? 'Jalan Baru'); - $status = $conn->real_escape_string($data->status ?? 'Kabupaten'); - $panjang = (float)($data->panjang ?? 0); - $geom = $conn->real_escape_string(json_encode($data->geom)); // GeoJSON string - - $query = "INSERT INTO jalan (nama, status, panjang, geom) VALUES ('$nama', '$status', $panjang, ST_GeomFromGeoJSON('$geom'))"; - - if($conn->query($query)) { - echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Jalan berhasil ditambahkan."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal menambahkan jalan: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "Data geometri tidak lengkap."]); -} -?> diff --git a/api/jalan/delete.php b/api/jalan/delete.php deleted file mode 100644 index 5ad8416..0000000 --- a/api/jalan/delete.php +++ /dev/null @@ -1,20 +0,0 @@ -id)) { - $id = (int)$data->id; - - $query = "DELETE FROM jalan WHERE id=$id"; - - if($conn->query($query)) { - echo json_encode(["status" => "success", "message" => "Jalan berhasil dihapus."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal hapus jalan: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); -} -?> diff --git a/api/jalan/read.php b/api/jalan/read.php deleted file mode 100644 index ade48aa..0000000 --- a/api/jalan/read.php +++ /dev/null @@ -1,25 +0,0 @@ -query($query); - -$jalan_arr = array(); - -if($result->num_rows > 0) { - while($row = $result->fetch_assoc()) { - $jalan_item = array( - "id" => $row['id'], - "nama" => $row['nama'], - "status" => $row['status'], - "panjang" => (float)$row['panjang'], - "geom" => json_decode($row['geom']) - ); - array_push($jalan_arr, $jalan_item); - } - echo json_encode(["status" => "success", "data" => $jalan_arr]); -} else { - echo json_encode(["status" => "success", "data" => []]); -} -?> diff --git a/api/jalan/update.php b/api/jalan/update.php deleted file mode 100644 index 08702d7..0000000 --- a/api/jalan/update.php +++ /dev/null @@ -1,29 +0,0 @@ -id)) { - $id = (int)$data->id; - $nama = $conn->real_escape_string($data->nama ?? ''); - $status = $conn->real_escape_string($data->status ?? 'Kabupaten'); - - // Jika ada update geometri - if(!empty($data->geom)) { - $panjang = (float)($data->panjang ?? 0); - $geom = $conn->real_escape_string(json_encode($data->geom)); - $query = "UPDATE jalan SET nama='$nama', status='$status', panjang=$panjang, geom=ST_GeomFromGeoJSON('$geom') WHERE id=$id"; - } else { - $query = "UPDATE jalan SET nama='$nama', status='$status' WHERE id=$id"; - } - - if($conn->query($query)) { - echo json_encode(["status" => "success", "message" => "Jalan berhasil diupdate."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal update jalan: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); -} -?> diff --git a/api/log_bantuan/create.php b/api/log_bantuan/create.php index 2abdc03..df855e8 100644 --- a/api/log_bantuan/create.php +++ b/api/log_bantuan/create.php @@ -1,5 +1,7 @@ 'error', 'message' => 'Forbidden: Anda tidak diizinkan membuat log untuk rumah ibadah lain.']); + exit; + } +} + if (!$miskin_id || !$ibadah_id || !$tipe_bantuan || !$tanggal) { echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']); exit; diff --git a/api/login.php b/api/login.php new file mode 100644 index 0000000..8db0e64 --- /dev/null +++ b/api/login.php @@ -0,0 +1,55 @@ + 'error', 'message' => 'Username dan password wajib diisi']); + exit; +} + +// Cari user di database +$sql = "SELECT u.*, ri.nama as nama_ibadah + FROM users u + LEFT JOIN rumah_ibadah ri ON u.ibadah_id = ri.id + WHERE u.username = ?"; +$stmt = $conn->prepare($sql); +$stmt->bind_param('s', $username); +$stmt->execute(); +$result = $stmt->get_result(); + +if ($result->num_rows === 1) { + $user = $result->fetch_assoc(); + // Verifikasi password + if (password_verify($password, $user['password'])) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + $_SESSION['ibadah_id'] = $user['ibadah_id']; + $_SESSION['nama_ibadah'] = $user['nama_ibadah']; + + echo json_encode([ + 'status' => 'success', + 'message' => 'Login berhasil', + 'data' => [ + 'id' => intval($user['id']), + 'username' => $user['username'], + 'role' => $user['role'], + 'ibadah_id' => $user['ibadah_id'] ? intval($user['ibadah_id']) : null, + 'nama_ibadah' => $user['nama_ibadah'] + ] + ]); + exit; + } +} + +echo json_encode(['status' => 'error', 'message' => 'Username atau password salah']); +$conn->close(); +?> diff --git a/api/logout.php b/api/logout.php new file mode 100644 index 0000000..3e6c6de --- /dev/null +++ b/api/logout.php @@ -0,0 +1,21 @@ + 'success', 'message' => 'Logout berhasil']); +?> diff --git a/api/parsil/create.php b/api/parsil/create.php deleted file mode 100644 index 2f0d584..0000000 --- a/api/parsil/create.php +++ /dev/null @@ -1,23 +0,0 @@ -geom)) { - $nama = $conn->real_escape_string($data->nama ?? ''); - $status = $conn->real_escape_string($data->status ?? 'SHM'); - $luas = (float)($data->luas ?? 0); - $geom = $conn->real_escape_string(json_encode($data->geom)); // GeoJSON string - - $query = "INSERT INTO parsil (nama, status, luas, geom) VALUES ('$nama', '$status', $luas, ST_GeomFromGeoJSON('$geom'))"; - - if($conn->query($query)) { - echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Parsil berhasil ditambahkan."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal menambahkan parsil: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "Data geometri tidak lengkap."]); -} -?> diff --git a/api/parsil/delete.php b/api/parsil/delete.php deleted file mode 100644 index 67f8229..0000000 --- a/api/parsil/delete.php +++ /dev/null @@ -1,20 +0,0 @@ -id)) { - $id = (int)$data->id; - - $query = "DELETE FROM parsil WHERE id=$id"; - - if($conn->query($query)) { - echo json_encode(["status" => "success", "message" => "Parsil berhasil dihapus."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal hapus parsil: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); -} -?> diff --git a/api/parsil/read.php b/api/parsil/read.php deleted file mode 100644 index f24bacf..0000000 --- a/api/parsil/read.php +++ /dev/null @@ -1,25 +0,0 @@ -query($query); - -$parsil_arr = array(); - -if($result->num_rows > 0) { - while($row = $result->fetch_assoc()) { - $parsil_item = array( - "id" => $row['id'], - "nama" => $row['nama'], - "status" => $row['status'], - "luas" => (float)$row['luas'], - "geom" => json_decode($row['geom']) - ); - array_push($parsil_arr, $parsil_item); - } - echo json_encode(["status" => "success", "data" => $parsil_arr]); -} else { - echo json_encode(["status" => "success", "data" => []]); -} -?> diff --git a/api/parsil/update.php b/api/parsil/update.php deleted file mode 100644 index edb5621..0000000 --- a/api/parsil/update.php +++ /dev/null @@ -1,29 +0,0 @@ -id)) { - $id = (int)$data->id; - $nama = $conn->real_escape_string($data->nama ?? ''); - $status = $conn->real_escape_string($data->status ?? 'SHM'); - - // Jika ada update geometri - if(!empty($data->geom)) { - $luas = (float)($data->luas ?? 0); - $geom = $conn->real_escape_string(json_encode($data->geom)); - $query = "UPDATE parsil SET nama='$nama', status='$status', luas=$luas, geom=ST_GeomFromGeoJSON('$geom') WHERE id=$id"; - } else { - $query = "UPDATE parsil SET nama='$nama', status='$status' WHERE id=$id"; - } - - if($conn->query($query)) { - echo json_encode(["status" => "success", "message" => "Parsil berhasil diupdate."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal update parsil: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); -} -?> diff --git a/api/penduduk_miskin/bulk_create.php b/api/penduduk_miskin/bulk_create.php index e8f60d3..2e2a65d 100644 --- a/api/penduduk_miskin/bulk_create.php +++ b/api/penduduk_miskin/bulk_create.php @@ -1,5 +1,7 @@ lat) && !empty($data->lng)) { - $nama = $conn->real_escape_string($data->nama ?? ''); - $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); - $jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1); - $lat = (float)$data->lat; - $lng = (float)$data->lng; +if (!empty($_POST)) { + $nama = $conn->real_escape_string($_POST['nama'] ?? ''); + $kategori = $conn->real_escape_string($_POST['kategori_bantuan'] ?? 'Makan'); + $jumlah_jiwa = (int)($_POST['jumlah_jiwa'] ?? 1); + $lat = (float)($_POST['lat'] ?? 0); + $lng = (float)($_POST['lng'] ?? 0); +} else { + $data = json_decode(file_get_contents("php://input")); + if ($data) { + $nama = $conn->real_escape_string($data->nama ?? ''); + $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); + $jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1); + $lat = (float)($data->lat ?? 0); + $lng = (float)($data->lng ?? 0); + } +} - $query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng) - VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng)"; +if ($lat != 0 && $lng != 0) { + $upload_dir = "../../uploads/"; + if (!file_exists($upload_dir)) { + mkdir($upload_dir, 0777, true); + } + + $foto_rumah = null; + $foto_kk = null; + + function uploadFile($fileKey, $upload_dir) { + if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) { + $fileTmpPath = $_FILES[$fileKey]['tmp_name']; + $fileName = $_FILES[$fileKey]['name']; + + $fileNameCmps = explode(".", $fileName); + $fileExtension = strtolower(end($fileNameCmps)); + + $allowedfileExtensions = array('jpg', 'jpeg', 'png', 'gif'); + if (in_array($fileExtension, $allowedfileExtensions)) { + $newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\._-]/', '_', $fileName); + $dest_path = $upload_dir . $newFileName; + + if (move_uploaded_file($fileTmpPath, $dest_path)) { + return $newFileName; + } + } + } + return null; + } + + $foto_rumah = uploadFile('foto_rumah', $upload_dir); + $foto_kk = uploadFile('foto_kk', $upload_dir); + + $val_foto_rumah = $foto_rumah ? "'$foto_rumah'" : "NULL"; + $val_foto_kk = $foto_kk ? "'$foto_kk'" : "NULL"; + + $query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng, foto_rumah, foto_kk) + VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng, $val_foto_rumah, $val_foto_kk)"; if ($conn->query($query)) { echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Berhasil ditambahkan."]); diff --git a/api/penduduk_miskin/delete.php b/api/penduduk_miskin/delete.php index 50f5f9d..839ef2f 100644 --- a/api/penduduk_miskin/delete.php +++ b/api/penduduk_miskin/delete.php @@ -1,5 +1,7 @@ num_rows > 0) { "kategori_bantuan" => $row['kategori_bantuan'], "jumlah_jiwa" => isset($row['jumlah_jiwa']) ? (int)$row['jumlah_jiwa'] : 1, "lat" => (float)$row['lat'], - "lng" => (float)$row['lng'] + "lng" => (float)$row['lng'], + "foto_rumah" => $row['foto_rumah'], + "foto_kk" => $row['foto_kk'] ); array_push($arr, $item); } diff --git a/api/penduduk_miskin/update.php b/api/penduduk_miskin/update.php index 2d05a2d..532718e 100644 --- a/api/penduduk_miskin/update.php +++ b/api/penduduk_miskin/update.php @@ -1,20 +1,100 @@ id)) { - $id = (int)$data->id; - $nama = $conn->real_escape_string($data->nama ?? ''); - $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); - $jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1); - $lat = (float)$data->lat; - $lng = (float)$data->lng; +if (!empty($_POST)) { + $id = (int)($_POST['id'] ?? 0); + $nama = $conn->real_escape_string($_POST['nama'] ?? ''); + $kategori = $conn->real_escape_string($_POST['kategori_bantuan'] ?? 'Makan'); + $jumlah_jiwa = (int)($_POST['jumlah_jiwa'] ?? 1); + $lat = (float)($_POST['lat'] ?? 0); + $lng = (float)($_POST['lng'] ?? 0); +} else { + $data = json_decode(file_get_contents("php://input")); + if ($data) { + $id = (int)($data->id ?? 0); + $nama = $conn->real_escape_string($data->nama ?? ''); + $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); + $jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1); + $lat = (float)($data->lat ?? 0); + $lng = (float)($data->lng ?? 0); + } +} - $query = "UPDATE penduduk_miskin - SET nama='$nama', kategori_bantuan='$kategori', jumlah_jiwa=$jumlah_jiwa, lat=$lat, lng=$lng - WHERE id=$id"; +if ($id > 0) { + // Get current files to delete them if updated + $old_foto_rumah = null; + $old_foto_kk = null; + $res = $conn->query("SELECT foto_rumah, foto_kk FROM penduduk_miskin WHERE id=$id"); + if ($res && $row = $res->fetch_assoc()) { + $old_foto_rumah = $row['foto_rumah']; + $old_foto_kk = $row['foto_kk']; + } + + $upload_dir = "../../uploads/"; + if (!file_exists($upload_dir)) { + mkdir($upload_dir, 0777, true); + } + + $foto_rumah = null; + $foto_kk = null; + + function uploadFile($fileKey, $upload_dir) { + if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) { + $fileTmpPath = $_FILES[$fileKey]['tmp_name']; + $fileName = $_FILES[$fileKey]['name']; + + $fileNameCmps = explode(".", $fileName); + $fileExtension = strtolower(end($fileNameCmps)); + + $allowedfileExtensions = array('jpg', 'jpeg', 'png', 'gif'); + if (in_array($fileExtension, $allowedfileExtensions)) { + $newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\._-]/', '_', $fileName); + $dest_path = $upload_dir . $newFileName; + + if (move_uploaded_file($fileTmpPath, $dest_path)) { + return $newFileName; + } + } + } + return null; + } + + $foto_rumah = uploadFile('foto_rumah', $upload_dir); + $foto_kk = uploadFile('foto_kk', $upload_dir); + + // Build update fields + $update_fields = [ + "nama='$nama'", + "kategori_bantuan='$kategori'", + "jumlah_jiwa=$jumlah_jiwa", + "lat=$lat", + "lng=$lng" + ]; + + if ($foto_rumah !== null) { + $update_fields[] = "foto_rumah='$foto_rumah'"; + if ($old_foto_rumah && file_exists($upload_dir . $old_foto_rumah)) { + unlink($upload_dir . $old_foto_rumah); + } + } + if ($foto_kk !== null) { + $update_fields[] = "foto_kk='$foto_kk'"; + if ($old_foto_kk && file_exists($upload_dir . $old_foto_kk)) { + unlink($upload_dir . $old_foto_kk); + } + } + + $query = "UPDATE penduduk_miskin SET " . implode(", ", $update_fields) . " WHERE id=$id"; if ($conn->query($query)) { echo json_encode(["status" => "success", "message" => "Update berhasil."]); diff --git a/api/rumah_ibadah/create.php b/api/rumah_ibadah/create.php index 4f77d6a..2aa14bd 100644 --- a/api/rumah_ibadah/create.php +++ b/api/rumah_ibadah/create.php @@ -1,5 +1,7 @@ lat) && !empty($data->lng)) { - $nama = $conn->real_escape_string($data->nama ?? ''); - $no_wa = $conn->real_escape_string($data->no_wa ?? ''); - $is_24_jam = isset($data->is_24_jam) ? (int)$data->is_24_jam : 0; - $lat = (float)$data->lat; - $lng = (float)$data->lng; - - $query = "INSERT INTO spbu (nama, no_wa, is_24_jam, lat, lng) VALUES ('$nama', '$no_wa', $is_24_jam, $lat, $lng)"; - - if($conn->query($query)) { - echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "SPBU berhasil ditambahkan."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal menambahkan SPBU: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]); -} -?> diff --git a/api/spbu/delete.php b/api/spbu/delete.php deleted file mode 100644 index 106a386..0000000 --- a/api/spbu/delete.php +++ /dev/null @@ -1,20 +0,0 @@ -id)) { - $id = (int)$data->id; - - $query = "DELETE FROM spbu WHERE id=$id"; - - if($conn->query($query)) { - echo json_encode(["status" => "success", "message" => "SPBU berhasil dihapus."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal hapus SPBU: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); -} -?> diff --git a/api/spbu/read.php b/api/spbu/read.php deleted file mode 100644 index 942d823..0000000 --- a/api/spbu/read.php +++ /dev/null @@ -1,26 +0,0 @@ -query($query); - -$spbu_arr = array(); - -if($result->num_rows > 0) { - while($row = $result->fetch_assoc()) { - $spbu_item = array( - "id" => $row['id'], - "nama" => $row['nama'], - "no_wa" => $row['no_wa'], - "is_24_jam" => (bool)$row['is_24_jam'], - "lat" => (float)$row['lat'], - "lng" => (float)$row['lng'] - ); - array_push($spbu_arr, $spbu_item); - } - echo json_encode(["status" => "success", "data" => $spbu_arr]); -} else { - echo json_encode(["status" => "success", "data" => []]); -} -?> diff --git a/api/spbu/update.php b/api/spbu/update.php deleted file mode 100644 index 5fabf84..0000000 --- a/api/spbu/update.php +++ /dev/null @@ -1,25 +0,0 @@ -id) && !empty($data->lat) && !empty($data->lng)) { - $id = (int)$data->id; - $nama = $conn->real_escape_string($data->nama ?? ''); - $no_wa = $conn->real_escape_string($data->no_wa ?? ''); - $is_24_jam = isset($data->is_24_jam) ? (int)$data->is_24_jam : 0; - $lat = (float)$data->lat; - $lng = (float)$data->lng; - - $query = "UPDATE spbu SET nama='$nama', no_wa='$no_wa', is_24_jam=$is_24_jam, lat=$lat, lng=$lng WHERE id=$id"; - - if($conn->query($query)) { - echo json_encode(["status" => "success", "message" => "SPBU berhasil diupdate."]); - } else { - echo json_encode(["status" => "error", "message" => "Gagal update SPBU: " . $conn->error]); - } -} else { - echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]); -} -?> diff --git a/api/users/create.php b/api/users/create.php new file mode 100644 index 0000000..3de6ddc --- /dev/null +++ b/api/users/create.php @@ -0,0 +1,47 @@ + 'error', 'message' => 'Username dan password wajib diisi']); + exit; +} + +if ($role !== 'admin' && $role !== 'pengelola') { + echo json_encode(['status' => 'error', 'message' => 'Role tidak valid']); + exit; +} + +// Cek apakah username sudah ada +$stmt = $conn->prepare("SELECT id FROM users WHERE username = ?"); +$stmt->bind_param('s', $username); +$stmt->execute(); +if ($stmt->get_result()->num_rows > 0) { + echo json_encode(['status' => 'error', 'message' => 'Username sudah digunakan']); + exit; +} + +// Hash password +$hashed_password = password_hash($password, PASSWORD_BCRYPT); + +// Insert user baru +$stmt = $conn->prepare("INSERT INTO users (username, password, role, ibadah_id) VALUES (?, ?, ?, ?)"); +$stmt->bind_param('sssi', $username, $hashed_password, $role, $ibadah_id); + +if ($stmt->execute()) { + echo json_encode(['status' => 'success', 'message' => 'User berhasil ditambahkan', 'id' => $stmt->insert_id]); +} else { + echo json_encode(['status' => 'error', 'message' => 'Gagal menambahkan user: ' . $conn->error]); +} + +$conn->close(); +?> diff --git a/api/users/delete.php b/api/users/delete.php new file mode 100644 index 0000000..08e507f --- /dev/null +++ b/api/users/delete.php @@ -0,0 +1,30 @@ + 'error', 'message' => 'ID tidak valid']); + exit; +} + +if ($id === intval($_SESSION['user_id'])) { + echo json_encode(['status' => 'error', 'message' => 'Anda tidak bisa menghapus akun Anda sendiri']); + exit; +} + +$stmt = $conn->prepare("DELETE FROM users WHERE id = ?"); +$stmt->bind_param('i', $id); + +if ($stmt->execute()) { + echo json_encode(['status' => 'success', 'message' => 'User berhasil dihapus']); +} else { + echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus user: ' . $conn->error]); +} + +$conn->close(); +?> diff --git a/api/users/read.php b/api/users/read.php new file mode 100644 index 0000000..4478784 --- /dev/null +++ b/api/users/read.php @@ -0,0 +1,31 @@ +query($sql); +$arr = array(); + +if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $item = array( + "id" => intval($row['id']), + "username" => $row['username'], + "role" => $row['role'], + "ibadah_id" => $row['ibadah_id'] ? intval($row['ibadah_id']) : null, + "nama_ibadah" => $row['nama_ibadah'] ?? '-' + ); + array_push($arr, $item); + } + echo json_encode(["status" => "success", "data" => $arr]); +} else { + echo json_encode(["status" => "success", "data" => []]); +} +$conn->close(); +?> diff --git a/api/users/update.php b/api/users/update.php new file mode 100644 index 0000000..117e7a1 --- /dev/null +++ b/api/users/update.php @@ -0,0 +1,52 @@ + 'error', 'message' => 'Data tidak lengkap']); + exit; +} + +if ($role !== 'admin' && $role !== 'pengelola') { + echo json_encode(['status' => 'error', 'message' => 'Role tidak valid']); + exit; +} + +// Cek apakah username sudah ada di user lain +$stmt = $conn->prepare("SELECT id FROM users WHERE username = ? AND id != ?"); +$stmt->bind_param('si', $username, $id); +$stmt->execute(); +if ($stmt->get_result()->num_rows > 0) { + echo json_encode(['status' => 'error', 'message' => 'Username sudah digunakan oleh user lain']); + exit; +} + +if (!empty($password)) { + // Hash new password + $hashed_password = password_hash($password, PASSWORD_BCRYPT); + $stmt = $conn->prepare("UPDATE users SET username = ?, password = ?, role = ?, ibadah_id = ? WHERE id = ?"); + $stmt->bind_param('ssssi', $username, $hashed_password, $role, $ibadah_id, $id); +} else { + // Keep existing password + $stmt = $conn->prepare("UPDATE users SET username = ?, role = ?, ibadah_id = ? WHERE id = ?"); + $stmt->bind_param('sssi', $username, $role, $ibadah_id, $id); +} + +if ($stmt->execute()) { + echo json_encode(['status' => 'success', 'message' => 'User berhasil diperbarui']); +} else { + echo json_encode(['status' => 'error', 'message' => 'Gagal memperbarui user: ' . $conn->error]); +} + +$conn->close(); +?> diff --git a/assets/css/style.css b/assets/css/style.css index 0ef9b57..7dd697c 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -139,6 +139,18 @@ body.right-panel-open .custom-layer-panel { padding-bottom: 5px; } +.layer-section-title { + font-size: 11px; + font-weight: 700; + color: #6366f1; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 8px; + margin-top: 12px; + padding-bottom: 2px; + border-bottom: 1px dashed #e2e8f0; +} + .layer-option { display: flex; align-items: center; @@ -190,6 +202,25 @@ body.right-panel-open .custom-layer-panel { gap: 8px; } +.sidebar-section-label { + font-size: 9px; + font-weight: 700; + color: #94a3b8; + text-transform: uppercase; + text-align: center; + letter-spacing: 0.8px; + margin-top: 6px; + margin-bottom: 2px; + pointer-events: none; + user-select: none; +} + +.sidebar-divider { + height: 1px; + background: #e2e8f0; + margin: 4px 6px; +} + .sidebar-btn { width: 60px; height: 60px; @@ -400,9 +431,6 @@ body.right-panel-open .custom-layer-panel { flex-shrink: 0; } -.card-icon-spbu { background: #f0fdf4; } -.card-icon-jalan { background: #eff6ff; } -.card-icon-parsil { background: #fefce8; } .card-icon-ibadah { background: #fff7ed; } .card-icon-miskin-out { background: #f0fdf4; } .card-icon-miskin-in { background: #fef2f2; } @@ -690,6 +718,7 @@ body.right-panel-open .custom-layer-panel { .modal-body input[type="text"], .modal-body input[type="number"], .modal-body input[type="date"], +.modal-body input[type="password"], .modal-body select, .modal-body textarea { width: 100%; @@ -836,8 +865,6 @@ body.right-panel-open .custom-layer-panel { } /* Warna bubble per tipe */ -.emoji-marker .bubble.spbu-24 { background: linear-gradient(135deg, #22c55e, #16a34a); } -.emoji-marker .bubble.spbu-not24 { background: linear-gradient(135deg, #ef4444, #dc2626); } .emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); } .emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); } .emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); } @@ -893,3 +920,103 @@ body.right-panel-open .custom-layer-panel { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(10px) scale(0.95); } } + +/* Auth Widget Styling */ +.auth-widget { + position: absolute; + top: 20px; + right: 80px; + z-index: 1000; + transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +body.right-panel-open .auth-widget { + right: 400px; +} + +.btn-login { + background: linear-gradient(135deg, #6366f1, #4f46e5); + color: white; + border: none; + border-radius: 24px; + height: 48px; + padding: 0 24px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; +} + +.btn-login:hover { + box-shadow: 0 6px 16px rgba(99, 102, 241, 0.45); + transform: translateY(-1px); +} + +.user-profile-pill { + background-color: white; + border-radius: 24px; + height: 48px; + padding: 0 8px 0 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + align-items: center; + gap: 12px; + border: 1px solid rgba(226, 232, 240, 0.8); + backdrop-filter: blur(10px); + background: rgba(255, 255, 255, 0.9); +} + +.user-profile-info { + display: flex; + flex-direction: column; +} + +.user-profile-name { + font-size: 12px; + font-weight: 700; + color: #1e1e2e; +} + +.user-profile-role { + font-size: 9px; + font-weight: 600; + color: #6366f1; + text-transform: uppercase; +} + +.btn-profile-logout { + width: 32px; + height: 32px; + border-radius: 50%; + border: none; + background: #f1f5f9; + color: #64748b; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.btn-profile-logout:hover { + background: #fee2e2; + color: #ef4444; +} + +/* User management table styling */ +#userManagementModal table th { + padding: 10px 12px; + font-weight: 600; + background-color: #f1f5f9; + border-bottom: 1px solid #cbd5e1; +} + +#userManagementModal table td { + padding: 10px 12px; + border-bottom: 1px solid #e2e8f0; +} + diff --git a/assets/js/features/auth.js b/assets/js/features/auth.js new file mode 100644 index 0000000..77fe7a4 --- /dev/null +++ b/assets/js/features/auth.js @@ -0,0 +1,369 @@ +// --- Fitur Autentikasi dan Manajemen User (RBAC) --- + +(function() { + window.currentUser = null; + + // Elements + const authWidget = document.getElementById('authWidget'); + const loginModal = document.getElementById('loginModal'); + const loginUsernameInput = document.getElementById('loginUsername'); + const loginPasswordInput = document.getElementById('loginPassword'); + const loginSubmitBtn = document.getElementById('loginSubmitBtn'); + const loginErrorMsg = document.getElementById('loginErrorMsg'); + const closeLoginModal = document.getElementById('closeLoginModal'); + + // User Management Modal Elements + const userManagementModal = document.getElementById('userManagementModal'); + const closeUserManagementModal = document.getElementById('closeUserManagementModal'); + const menuUsersBtn = document.getElementById('menuUsers'); + const userTableBody = document.getElementById('userTableBody'); + const btnSaveUser = document.getElementById('btnSaveUser'); + const btnCancelUserEdit = document.getElementById('btnCancelUserEdit'); + const userFormTitle = document.getElementById('userFormTitle'); + + const manageUserId = document.getElementById('manageUserId'); + const manageUsername = document.getElementById('manageUsername'); + const managePassword = document.getElementById('managePassword'); + const manageRole = document.getElementById('manageRole'); + const manageIbadahId = document.getElementById('manageIbadahId'); + const manageIbadahGroup = document.getElementById('manageIbadahGroup'); + + // Startup Session Check + function checkSession() { + fetch('api/check_session.php') + .then(res => res.json()) + .then(data => { + if (data.status === 'success' && data.isLoggedIn) { + window.currentUser = data.data; + } else { + window.currentUser = null; + } + updateAuthUI(); + refreshAllLayers(); + }) + .catch(err => { + console.error("Session check failed:", err); + window.currentUser = null; + updateAuthUI(); + }); + } + + // Update UI based on logged-in state + function updateAuthUI() { + if (!authWidget) return; + + if (window.currentUser) { + // Logged In state + const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola'; + const extraLabel = window.currentUser.nama_ibadah ? ` - ${window.currentUser.nama_ibadah}` : ''; + + authWidget.innerHTML = ` +
+
+ + +
+ +
+ `; + + document.getElementById('authLogoutBtn').addEventListener('click', logout); + + // Show user management button in sidebar for Admin + const adminDivider = document.getElementById('sidebarAdminDivider'); + if (window.currentUser.role === 'admin') { + if (menuUsersBtn) menuUsersBtn.style.display = 'flex'; + if (adminDivider) adminDivider.style.display = 'block'; + } else { + if (menuUsersBtn) menuUsersBtn.style.display = 'none'; + if (adminDivider) adminDivider.style.display = 'none'; + } + } else { + // Logged Out state + authWidget.innerHTML = ` + + `; + document.getElementById('authLoginBtn').addEventListener('click', showLoginModal); + if (menuUsersBtn) menuUsersBtn.style.display = 'none'; + const adminDivider = document.getElementById('sidebarAdminDivider'); + if (adminDivider) adminDivider.style.display = 'none'; + } + } + + function refreshAllLayers() { + if (typeof loadSpbu === 'function') loadSpbu(); + if (typeof loadJalan === 'function') loadJalan(); + if (typeof loadParsil === 'function') loadParsil(); + if (typeof loadRumahIbadah === 'function') loadRumahIbadah(); + if (typeof loadPendudukMiskin === 'function') loadPendudukMiskin(); + if (window.refreshActivePanel) window.refreshActivePanel(); + } + + // Login Modals logic + function showLoginModal() { + loginUsernameInput.value = ''; + loginPasswordInput.value = ''; + loginErrorMsg.style.display = 'none'; + loginModal.classList.add('show'); + } + + function hideLoginModal() { + loginModal.classList.remove('show'); + } + + if (closeLoginModal) { + closeLoginModal.addEventListener('click', hideLoginModal); + } + + loginSubmitBtn.addEventListener('click', function() { + const username = loginUsernameInput.value.trim(); + const password = loginPasswordInput.value; + + if (!username || !password) { + loginErrorMsg.textContent = 'Username dan password wajib diisi'; + loginErrorMsg.style.display = 'block'; + return; + } + + fetch('api/login.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }) + .then(res => res.json()) + .then(data => { + if (data.status === 'success') { + window.currentUser = data.data; + hideLoginModal(); + updateAuthUI(); + refreshAllLayers(); + showToast('Login berhasil', 'success'); + } else { + loginErrorMsg.textContent = data.message || 'Login gagal'; + loginErrorMsg.style.display = 'block'; + } + }) + .catch(err => { + console.error(err); + loginErrorMsg.textContent = 'Koneksi ke server terputus'; + loginErrorMsg.style.display = 'block'; + }); + }); + + function logout() { + fetch('api/logout.php') + .then(res => res.json()) + .then(data => { + window.currentUser = null; + updateAuthUI(); + refreshAllLayers(); + showToast('Logout berhasil', 'success'); + }) + .catch(err => { + console.error(err); + showToast('Gagal logout', 'error'); + }); + } + + // User Management Modal Logic + if (menuUsersBtn) { + menuUsersBtn.addEventListener('click', function() { + userManagementModal.classList.add('show'); + loadUsers(); + populateIbadahDropdown(); + resetUserForm(); + }); + } + + if (closeUserManagementModal) { + closeUserManagementModal.addEventListener('click', function() { + userManagementModal.classList.remove('show'); + }); + } + + // Show/hide ibadah dropdown based on selected role in form + if (manageRole) { + manageRole.addEventListener('change', function() { + if (this.value === 'admin') { + manageIbadahGroup.style.display = 'none'; + manageIbadahId.value = ''; + } else { + manageIbadahGroup.style.display = 'flex'; + } + }); + } + + function populateIbadahDropdown() { + if (!manageIbadahId) return; + manageIbadahId.innerHTML = ''; + if (typeof ibadahDataList !== 'undefined') { + ibadahDataList.forEach(ib => { + const opt = document.createElement('option'); + opt.value = ib.id; + opt.textContent = `${ib.jenis || '🕌'} ${ib.nama}`; + manageIbadahId.appendChild(opt); + }); + } + } + + function loadUsers() { + if (!userTableBody) return; + userTableBody.innerHTML = 'Memuat user...'; + + fetch('api/users/read.php') + .then(res => res.json()) + .then(data => { + if (data.status === 'success') { + userTableBody.innerHTML = ''; + if (data.data.length === 0) { + userTableBody.innerHTML = 'Belum ada user'; + return; + } + data.data.forEach(user => { + const tr = document.createElement('tr'); + tr.style.borderBottom = '1px solid #e2e8f0'; + + const roleBadge = user.role === 'admin' + ? 'Admin' + : 'Pengelola'; + + tr.innerHTML = ` + ${escHtml(user.username)} + ${roleBadge} + ${escHtml(user.nama_ibadah)} + + + + + `; + userTableBody.appendChild(tr); + }); + } + }) + .catch(err => { + userTableBody.innerHTML = 'Gagal memuat user'; + }); + } + + window.editUser = function(user) { + userFormTitle.textContent = 'Edit Pengguna'; + manageUserId.value = user.id; + manageUsername.value = user.username; + managePassword.value = ''; // Password cleared in edit + manageRole.value = user.role; + + if (user.role === 'admin') { + manageIbadahGroup.style.display = 'none'; + manageIbadahId.value = ''; + } else { + manageIbadahGroup.style.display = 'flex'; + manageIbadahId.value = user.ibadah_id || ''; + } + + btnCancelUserEdit.style.display = 'inline-block'; + }; + + window.deleteUser = function(id) { + if (confirm('Apakah Anda yakin ingin menghapus user ini?')) { + fetch('api/users/delete.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }) + .then(res => res.json()) + .then(data => { + if (data.status === 'success') { + showToast(data.message, 'success'); + loadUsers(); + resetUserForm(); + } else { + showToast(data.message || 'Gagal menghapus user', 'error'); + } + }) + .catch(err => { + showToast('Koneksi terputus', 'error'); + }); + } + }; + + btnSaveUser.addEventListener('click', function() { + const id = manageUserId.value; + const username = manageUsername.value.trim(); + const password = managePassword.value; + const role = manageRole.value; + const ibadah_id = manageIbadahId.value; + + if (!username) { + alert('Username wajib diisi'); + return; + } + + if (!id && !password) { + alert('Password wajib diisi untuk user baru'); + return; + } + + const payload = { username, role, ibadah_id }; + if (id) payload.id = id; + if (password) payload.password = password; + + const endpoint = id ? 'api/users/update.php' : 'api/users/create.php'; + + fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }) + .then(res => res.json()) + .then(data => { + if (data.status === 'success') { + showToast(data.message, 'success'); + loadUsers(); + resetUserForm(); + } else { + alert(data.message || 'Gagal menyimpan user'); + } + }) + .catch(err => { + alert('Koneksi terputus'); + }); + }); + + btnCancelUserEdit.addEventListener('click', resetUserForm); + + function resetUserForm() { + userFormTitle.textContent = 'Tambah User Baru'; + manageUserId.value = ''; + manageUsername.value = ''; + managePassword.value = ''; + manageRole.value = 'pengelola'; + manageIbadahGroup.style.display = 'flex'; + manageIbadahId.value = ''; + btnCancelUserEdit.style.display = 'none'; + } + + // Helper functions + function escHtml(str) { + if (!str) return ''; + return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); + } + + // Toast Notification helper + function showToast(message, type = 'success') { + if (typeof window.showToast === 'function') { + window.showToast(message, type); + return; + } + // Fallback alert + alert(message); + } + + // Run session check on initialization + checkSession(); +})(); diff --git a/assets/js/features/draw_control.js b/assets/js/features/draw_control.js deleted file mode 100644 index 1ef9797..0000000 --- a/assets/js/features/draw_control.js +++ /dev/null @@ -1,138 +0,0 @@ -// --- Setup Leaflet Draw --- - -// Layer terpisah untuk hasil gambar sementara sebelum disimpan -const drawnItems = new L.FeatureGroup(); -map.addLayer(drawnItems); - -const drawControl = new L.Control.Draw({ - draw: { - polygon: { - allowIntersection: false, - showArea: true - }, - polyline: { - metric: true - }, - circle: false, - circlemarker: false, - marker: false, // Marker default dimatikan, kita pakai klik peta untuk SPBU - rectangle: false - }, - edit: { - featureGroup: drawnItems, // Kita tidak mengedit di drawnItems, karena data aslinya di jalanLayer/parsilLayer - edit: false, - remove: false - } -}); -// Hapus map.addControl(drawControl); agar tidak tampil UI bawaan LeafletJS - - -// Flag untuk menghindari munculnya form SPBU saat sedang menggambar -map.on(L.Draw.Event.DRAWSTART, function (e) { - isDrawingMode = true; -}); - -map.on(L.Draw.Event.DRAWSTOP, function (e) { - setTimeout(() => { - isDrawingMode = false; - window.currentDrawMode = null; - window.activeDrawHandler = null; - window.deactivateAddMode(); - }, 200); -}); - -window.activateDraw = function(type) { - console.log("-> activateDraw dipanggil untuk tipe:", type); - - // Toggle: jika mode yang sama ditekan lagi, batalkan - if (window.currentDrawMode === type) { - console.log("Mode", type, "sudah aktif, membatalkan..."); - window.deactivateAddMode(); - return; - } - - window.deactivateAddMode(); - window.currentDrawMode = type; - console.log("Mengaktifkan mode menggambar:", type); - - if (type === 'polyline') { - const handler = new L.Draw.Polyline(map, drawControl.options.draw.polyline); - handler.enable(); - window.activeDrawHandler = handler; - if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Jalan'; - } else if (type === 'polygon') { - const handler = new L.Draw.Polygon(map, drawControl.options.draw.polygon); - handler.enable(); - window.activeDrawHandler = handler; - if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Parsil'; - } -}; - -map.on(L.Draw.Event.CREATED, function (e) { - const type = e.layerType; - const layer = e.layer; - - // Convert layer to GeoJSON geometry - const geoJson = layer.toGeoJSON().geometry; - const geoJsonStr = JSON.stringify(geoJson); - - if (type === 'polyline') { - let length = 0; - const latlngs = layer.getLatLngs(); - for (let i = 0; i < latlngs.length - 1; i++) { - length += latlngs[i].distanceTo(latlngs[i + 1]); - } - - // Gunakan Modal alih-alih prompt - const bodyHTML = ` -
- - -
-
- - -
-
- -
- `; - openModal("Tambah Jalan Baru", bodyHTML, function() { - window.saveNewJalan(geoJsonStr, length, 'modalJalanNama', 'modalJalanStatus'); - }); - - } - else if (type === 'polygon') { - const latlngs = layer.getLatLngs()[0]; - let area = 0; - if (L.GeometryUtil && L.GeometryUtil.geodesicArea) { - area = L.GeometryUtil.geodesicArea(latlngs); - } - - const bodyHTML = ` -
- - -
-
- - -
-
- -
- `; - openModal("Tambah Parsil Tanah", bodyHTML, function() { - window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus'); - }); - } -}); diff --git a/assets/js/features/jalan.js b/assets/js/features/jalan.js deleted file mode 100644 index 1baf41e..0000000 --- a/assets/js/features/jalan.js +++ /dev/null @@ -1,164 +0,0 @@ -// --- Fitur Jalan --- - -const jalanColors = { - 'Nasional': '#ff0000', // Merah - 'Provinsi': '#0000ff', // Biru - 'Kabupaten': '#00ff00' // Hijau -}; - -function loadJalan() { - jalanLayer.clearLayers(); - fetch('api/jalan/read.php') - .then(res => res.json()) - .then(data => { - if (data.status === 'success' && data.data) { - data.data.forEach(item => { - addJalanToMap(item); - }); - } - if (window.refreshActivePanel) window.refreshActivePanel(); - }); -} - -function addJalanToMap(item) { - // geom adalah GeoJSON {type: "LineString", coordinates: [[lng, lat], ...]} - const latlngs = item.geom.coordinates.map(coord => [coord[1], coord[0]]); - - const polyline = L.polyline(latlngs, { - color: jalanColors[item.status] || '#3388ff', - weight: 5 - }); - - polyline.jalanData = item; - - // Tampilkan label nama jalan sejajar dengan garis (diagonal) - polyline.setText(item.nama, { - center: true, - offset: 0, - attributes: { - fill: '#000000', - 'font-weight': 'bold', - 'font-size': '14px', - 'dy': '7' - } - }); - - // Hitung popupContent sekali saat jalan dibuat - const d = item; - const popupContent = ` -
-

Jalan ${d.nama}

-

Status: ${d.status}

-

Panjang: ${d.panjang.toFixed(2)} m

-
- - -
-
- `; - - polyline.bindPopup(popupContent); - - jalanLayer.addLayer(polyline); -} - -window.openEditJalanModal = function(id) { - let d = null; - jalanLayer.eachLayer(function(layer) { - if (layer.jalanData && layer.jalanData.id == id) d = layer.jalanData; - }); - if (!d) return; - - const bodyHTML = ` -
- - -
-
- - -
-
- -
- `; - map.closePopup(); - openModal("Edit Jalan", bodyHTML, function() { - window.saveEditJalan(d.id, 'editJalanNama', 'editJalanStatus'); - }); -}; - -window.saveEditJalan = function(id, namaId, statusId) { - const nama = document.getElementById(namaId).value; - const status = document.getElementById(statusId).value; - - fetch('api/jalan/update.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id, nama, status }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - map.closePopup(); - closeModal(); - loadJalan(); - } else { - alert(data.message); - } - }); -}; - -window.deleteJalan = function(id) { - openConfirmModal("Yakin hapus jalan ini?", function() { - fetch('api/jalan/delete.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - map.closePopup(); - loadJalan(); - } else { - alert(data.message); - } - }); - }); -}; - -window.saveNewJalan = function(geoJsonStr, panjang, namaId, statusId) { - const nama = document.getElementById(namaId).value; - if (!nama) { - alert("Nama jalan harus diisi!"); - return false; - } - - const status = document.getElementById(statusId).value; - - const geom = JSON.parse(geoJsonStr); - - fetch('api/jalan/create.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nama, status, panjang, geom }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - closeModal(); - loadJalan(); - } else { - alert(data.message); - } - }); - return true; -}; - -// Initial Load -loadJalan(); diff --git a/assets/js/features/kemiskinan.js b/assets/js/features/kemiskinan.js index d592a02..76f1522 100644 --- a/assets/js/features/kemiskinan.js +++ b/assets/js/features/kemiskinan.js @@ -95,7 +95,9 @@ function addIbadahMarker(item) { if (isNaN(lat) || isNaN(lng)) return; - const marker = L.marker([lat, lng], { icon: makeIbadahIcon(item.jenis), draggable: true }); + const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin'); + + const marker = L.marker([lat, lng], { icon: makeIbadahIcon(item.jenis), draggable: isAdmin }); // Lingkaran radius const circle = L.circle([lat, lng], { @@ -107,7 +109,7 @@ function addIbadahMarker(item) { }); circle.on('mousemove', function(e) { - if (isResizing) return; + if (!isAdmin || isResizing) return; const center = circle.getLatLng(); const radius = circle.getRadius(); const dist = center.distanceTo(e.latlng); @@ -125,7 +127,7 @@ function addIbadahMarker(item) { }); circle.on('mousedown', function(e) { - if (circle.nearEdge) { + if (isAdmin && circle.nearEdge) { map.dragging.disable(); isResizing = true; resizingCircle = circle; @@ -139,16 +141,22 @@ function addIbadahMarker(item) { const d = item; const emot = IBADAH_EMOJI_MAP[d.jenis] || '🕌'; + let actionButtons = ''; + if (isAdmin) { + actionButtons = ` +
+ + +
+ `; + } const popupContent = `

${emot} ${d.nama}

Jenis: ${d.jenis || 'Masjid'}

Alamat: ${d.alamat}

Radius: ${d.radius} m

-
- - -
+ ${actionButtons}
`; marker.bindPopup(popupContent); @@ -168,7 +176,9 @@ function addIbadahMarker(item) { // Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta) map.on('contextmenu', function(e) { - if (isDrawingMode) return; + + const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola')); + if (!canManage) return; const popupContent = ` +
+ + + ${d.foto_rumah ? ` +
+ + Foto Rumah + ❌ File gambar rusak atau tidak ditemukan + +
Upload baru untuk mengganti
+
` : ''} +
+
+ + + ${d.foto_kk ? ` +
+ + Foto KK + ❌ File gambar rusak atau tidak ditemukan + +
Upload baru untuk mengganti
+
` : ''} +
`; map.closePopup(); openModal("Edit Penduduk Miskin", bodyHTML, function() { @@ -506,10 +594,22 @@ window.saveEditMiskin = function(id, lat, lng, namaId, kategoriId, jiwaId) { const nama = document.getElementById(namaId).value; const kategori_bantuan = document.getElementById(kategoriId).value; const jumlah_jiwa = jiwaId ? document.getElementById(jiwaId).value : 1; + const fotoRumahInput = document.getElementById('editMiskinFotoRumah'); + const fotoKKInput = document.getElementById('editMiskinFotoKK'); + + const formData = new FormData(); + formData.append('id', id); + formData.append('nama', nama); + formData.append('kategori_bantuan', kategori_bantuan); + formData.append('jumlah_jiwa', jumlah_jiwa); + formData.append('lat', lat); + formData.append('lng', lng); + if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]); + if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]); + fetch('api/penduduk_miskin/update.php', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id, nama, kategori_bantuan, jumlah_jiwa, lat, lng }) + body: formData }).then(res => res.json()).then(data => { if(data.status === 'success') { closeModal(); loadPendudukMiskin(); } }); diff --git a/assets/js/features/parsil.js b/assets/js/features/parsil.js deleted file mode 100644 index a430b24..0000000 --- a/assets/js/features/parsil.js +++ /dev/null @@ -1,151 +0,0 @@ -// --- Fitur Parsil --- - -const parsilColors = { - 'SHM': '#28a745', // Hijau - 'HGB': '#17a2b8', // Biru Muda - 'HGU': '#ffc107', // Kuning - 'HP': '#fd7e14' // Oranye -}; - -function loadParsil() { - parsilLayer.clearLayers(); - fetch('api/parsil/read.php') - .then(res => res.json()) - .then(data => { - if (data.status === 'success' && data.data) { - data.data.forEach(item => { - addParsilToMap(item); - }); - } - if (window.refreshActivePanel) window.refreshActivePanel(); - }); -} - -function addParsilToMap(item) { - // geom adalah GeoJSON Polygon - // coordinates pada Polygon formatnya: [[[lng, lat], [lng, lat], ...]] - // Leaflet Polygon butuh array of [lat, lng] - const latlngs = item.geom.coordinates[0].map(coord => [coord[1], coord[0]]); - - const polygon = L.polygon(latlngs, { - color: parsilColors[item.status] || '#3388ff', - fillColor: parsilColors[item.status] || '#3388ff', - fillOpacity: 0.5, - weight: 2 - }); - - polygon.parsilData = item; - - const d = item; - const popupContent = ` -
-

Parsil ${d.nama || ''}

-

Status: ${d.status}

-

Luas: ${d.luas.toFixed(2)} m²

-
- - -
-
- `; - polygon.bindPopup(popupContent); - - parsilLayer.addLayer(polygon); -} - -window.openEditParsilModal = function(id) { - let d = null; - parsilLayer.eachLayer(function(layer) { - if (layer.parsilData && layer.parsilData.id == id) d = layer.parsilData; - }); - if (!d) return; - - const bodyHTML = ` -
- - -
-
- - -
-
- -
- `; - map.closePopup(); - openModal("Edit Parsil Tanah", bodyHTML, function() { - window.saveEditParsil(d.id, 'editParsilNama', 'editParsilStatus'); - }); -}; - -window.saveEditParsil = function(id, namaId, statusId) { - const nama = document.getElementById(namaId).value; - const status = document.getElementById(statusId).value; - - fetch('api/parsil/update.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id, nama, status }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - map.closePopup(); - closeModal(); - loadParsil(); - } else { - alert(data.message); - } - }); -}; - -window.deleteParsil = function(id) { - openConfirmModal("Yakin hapus parsil ini?", function() { - fetch('api/parsil/delete.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - map.closePopup(); - loadParsil(); - } else { - alert(data.message); - } - }); - }); -}; - -window.saveNewParsil = function(geoJsonStr, luas, namaId, statusId) { - const nama = document.getElementById(namaId).value; - const status = document.getElementById(statusId).value; - - const geom = JSON.parse(geoJsonStr); - - fetch('api/parsil/create.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nama, status, luas, geom }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - closeModal(); - loadParsil(); - } else { - alert(data.message); - } - }); - return true; -}; - -// Initial Load -loadParsil(); diff --git a/assets/js/features/spbu.js b/assets/js/features/spbu.js deleted file mode 100644 index 73778b9..0000000 --- a/assets/js/features/spbu.js +++ /dev/null @@ -1,206 +0,0 @@ -// --- Fitur SPBU --- - -// Emoji Bubble Icon builder -function makeSpbuIcon(is24) { - const cls = is24 ? 'spbu-24' : 'spbu-not24'; - return L.divIcon({ - className: '', - html: `
`, - iconSize: [38, 38], - iconAnchor: [19, 38], - popupAnchor: [0, -40] - }); -} - -const spbuGreenIcon = makeSpbuIcon(true); -const spbuRedIcon = makeSpbuIcon(false); - -let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil) - - -// Fetch SPBU -function loadSpbu() { - spbuLayer.clearLayers(); - fetch('api/spbu/read.php') - .then(res => res.json()) - .then(data => { - if (data.status === 'success' && data.data) { - data.data.forEach(item => { - addSpbuMarker(item); - }); - } - if (window.refreshActivePanel) window.refreshActivePanel(); - }); -} - -function addSpbuMarker(item) { - const icon = makeSpbuIcon(item.is_24_jam); - const marker = L.marker([item.lat, item.lng], { - icon: icon, - draggable: true - }); - - marker.spbuData = item; // Simpan data di objek marker - - // Hitung popupContent sekali saat marker dibuat - const d = item; - const popupContent = ` -
-

SPBU ${d.nama}

-

No. WA: ${d.no_wa}

-

Buka 24 Jam: ${d.is_24_jam ? 'Ya' : 'Tidak'}

-
- - -
-
- `; - marker.bindPopup(popupContent); - - // Event drag untuk update koordinat - marker.on('dragend', function(e) { - const newPos = marker.getLatLng(); - updateSpbu(item.id, item.nama, item.no_wa, item.is_24_jam, newPos.lat, newPos.lng); - }); - - spbuLayer.addLayer(marker); -} - -// Map Click -> Form Add -map.on('click', function(e) { - if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon - - // Hanya proses jika mode tambah SPBU aktif - if (window.currentAddMode === 'spbu') { - const bodyHTML = ` -
- -
-
- - -
-
- - -
-
- -
- - -
-
- `; - - openModal("Tambah SPBU Baru", bodyHTML, function() { - window.saveNewSpbu(e.latlng.lat, e.latlng.lng, 'modalSpbuNama', 'modalSpbuWa', 'modalSpbu24'); - }); - - // Nonaktifkan mode tambah setelah modal muncul - window.deactivateAddMode(); - } -}); - -window.saveNewSpbu = function(lat, lng, namaId, waId, radioName) { - const nama = document.getElementById(namaId).value; - if (!nama) { - alert("Nama SPBU harus diisi!"); - return; - } - const no_wa = document.getElementById(waId).value; - const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value; - - fetch('api/spbu/create.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nama, no_wa, is_24_jam, lat, lng }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - closeModal(); - loadSpbu(); - } else { - alert(data.message); - } - }); -}; - -window.openEditSpbuModal = function(id) { - let d = null; - spbuLayer.eachLayer(function(layer) { - if (layer.spbuData && layer.spbuData.id == id) d = layer.spbuData; - }); - if (!d) return; - - const bodyHTML = ` -
- - -
-
- - -
-
- -
- - -
-
- `; - map.closePopup(); - openModal("Edit SPBU", bodyHTML, function() { - window.saveEditSpbu(d.id, d.lat, d.lng, 'editSpbuNama', 'editSpbuWa', 'editSpbu24'); - }); -}; - -window.saveEditSpbu = function(id, lat, lng, namaId, waId, radioName) { - const nama = document.getElementById(namaId).value; - const no_wa = document.getElementById(waId).value; - const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value; - - updateSpbu(id, nama, no_wa, is_24_jam, lat, lng); -}; - -function updateSpbu(id, nama, no_wa, is_24_jam, lat, lng) { - fetch('api/spbu/update.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id, nama, no_wa, is_24_jam, lat, lng }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - map.closePopup(); - closeModal(); - loadSpbu(); - } else { - alert(data.message); - } - }); -} - -window.deleteSpbu = function(id) { - openConfirmModal("Yakin hapus SPBU ini?", function() { - fetch('api/spbu/delete.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id }) - }) - .then(res => res.json()) - .then(data => { - if(data.status === 'success') { - map.closePopup(); - loadSpbu(); - } else { - alert(data.message); - } - }); - }); -}; - -// Initial Load -loadSpbu(); diff --git a/assets/js/map.js b/assets/js/map.js index 44e0c23..c660d8f 100644 --- a/assets/js/map.js +++ b/assets/js/map.js @@ -29,9 +29,6 @@ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { }).addTo(map); // Inisialisasi FeatureGroups untuk masing-masing layer -const spbuLayer = L.featureGroup().addTo(map); -const jalanLayer = L.featureGroup().addTo(map); -const parsilLayer = L.featureGroup().addTo(map); const rumahIbadahLayer = L.featureGroup().addTo(map); const pendudukMiskinLayer = L.featureGroup().addTo(map); const geoJsonLayer = L.featureGroup().addTo(map); @@ -66,29 +63,7 @@ window.showSearchResults = function(query) { resultsContainer.innerHTML = ''; let results = []; - if (typeof spbuLayer !== 'undefined') { - spbuLayer.eachLayer(layer => { - if (layer.spbuData && layer.spbuData.nama && layer.spbuData.nama.toLowerCase().includes(query)) { - results.push({ type: 'SPBU', nama: layer.spbuData.nama, layer: layer }); - } - }); - } - - if (typeof jalanLayer !== 'undefined') { - jalanLayer.eachLayer(layer => { - if (layer.jalanData && layer.jalanData.nama && layer.jalanData.nama.toLowerCase().includes(query)) { - results.push({ type: 'Jalan', nama: layer.jalanData.nama, layer: layer }); - } - }); - } - - if (typeof parsilLayer !== 'undefined') { - parsilLayer.eachLayer(layer => { - if (layer.parsilData && layer.parsilData.nama && layer.parsilData.nama.toLowerCase().includes(query)) { - results.push({ type: 'Parsil', nama: layer.parsilData.nama, layer: layer }); - } - }); - } + if (typeof rumahIbadahLayer !== 'undefined') { rumahIbadahLayer.eachLayer(layer => { @@ -143,15 +118,6 @@ map.on('click', function() { // Logic untuk Toggle Layer Visibility -document.getElementById('layerSpbu').addEventListener('change', function(e) { - e.target.checked ? map.addLayer(spbuLayer) : map.removeLayer(spbuLayer); -}); -document.getElementById('layerJalan').addEventListener('change', function(e) { - e.target.checked ? map.addLayer(jalanLayer) : map.removeLayer(jalanLayer); -}); -document.getElementById('layerParsil').addEventListener('change', function(e) { - e.target.checked ? map.addLayer(parsilLayer) : map.removeLayer(parsilLayer); -}); document.getElementById('layerRumahIbadah').addEventListener('change', function(e) { e.target.checked ? map.addLayer(rumahIbadahLayer) : map.removeLayer(rumahIbadahLayer); }); @@ -170,44 +136,6 @@ window.toggleSubLayer = function(subId, iconEl) { // --- Sub-layer Filter --- window.applySubFilter = function(type) { - if (type === 'spbu') { - const checked = [...document.querySelectorAll('.sub-spbu:checked')].map(el => el.value); - spbuLayer.eachLayer(layer => { - if (!layer.spbuData) return; - const val = layer.spbuData.is_24_jam ? '1' : '0'; - if (checked.includes(val)) { - if (!spbuLayer.hasLayer(layer)) spbuLayer.addLayer(layer); - layer.getElement && layer.getElement() && (layer.getElement().style.display = ''); - } else { - layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none'); - } - }); - } - - if (type === 'jalan') { - const checked = [...document.querySelectorAll('.sub-jalan:checked')].map(el => el.value); - jalanLayer.eachLayer(layer => { - if (!layer.jalanData) return; - const visible = checked.includes(layer.jalanData.status); - const display = visible ? '' : 'none'; - // Sembunyikan garis polyline - if (layer._path) layer._path.style.display = display; - // Sembunyikan label teks (leaflet-textpath simpan di _textNode) - if (layer._textNode) layer._textNode.style.display = display; - }); - } - - if (type === 'parsil') { - const checked = [...document.querySelectorAll('.sub-parsil:checked')].map(el => el.value); - parsilLayer.eachLayer(layer => { - if (!layer.parsilData) return; - if (checked.includes(layer.parsilData.status)) { - layer._path && (layer._path.style.display = ''); - } else { - layer._path && (layer._path.style.display = 'none'); - } - }); - } if (type === 'miskin') { const checked = [...document.querySelectorAll('.sub-miskin:checked')].map(el => el.value); @@ -269,8 +197,7 @@ window.closeConfirmModal = function() { // --- Action Menu Logic (replaced by left sidebar) --- -window.currentAddMode = null; // 'spbu' atau 'rumah_ibadah' -window.currentDrawMode = null; // 'polyline' atau 'polygon' +window.currentAddMode = null; // 'rumah_ibadah' atau 'miskin_click' window.activateAddMode = function(mode) { // Toggle: jika mode yang sama diklik lagi, batalkan @@ -279,18 +206,10 @@ window.activateAddMode = function(mode) { return; } - // Reset draw mode if active - if (window.activeDrawHandler) { - window.activeDrawHandler.disable(); - window.activeDrawHandler = null; - } - window.currentAddMode = mode; - window.currentDrawMode = null; // Tooltip const tooltips = { - 'spbu': 'Klik untuk Tambah SPBU', 'rumah_ibadah': 'Klik untuk Tambah Rumah Ibadah' }; if (window.cursorTooltip && tooltips[mode]) window.cursorTooltip.textContent = tooltips[mode]; @@ -301,11 +220,6 @@ window.activateAddMode = function(mode) { window.deactivateAddMode = function() { window.currentAddMode = null; - window.currentDrawMode = null; - if (window.activeDrawHandler) { - window.activeDrawHandler.disable(); - window.activeDrawHandler = null; - } document.getElementById('map').style.cursor = ''; if (window.cursorTooltip) window.cursorTooltip.style.display = 'none'; }; @@ -318,7 +232,7 @@ document.body.appendChild(window.cursorTooltip); // Gunakan document-level mousemove agar selalu terpicu // bahkan saat Leaflet Draw overlay aktif menangkap event map document.addEventListener('mousemove', function(e) { - if (window.currentAddMode || window.currentDrawMode) { + if (window.currentAddMode) { window.cursorTooltip.style.display = 'block'; window.cursorTooltip.style.left = e.pageX + 15 + 'px'; window.cursorTooltip.style.top = e.pageY + 15 + 'px'; diff --git a/assets/js/panel.js b/assets/js/panel.js index c380d8c..b9dc6bf 100644 --- a/assets/js/panel.js +++ b/assets/js/panel.js @@ -83,203 +83,30 @@ rightPanelActions.innerHTML = ''; switch (type) { - case 'spbu': renderSpbuPanel(); break; - case 'jalan': renderJalanPanel(); break; - case 'parsil': renderParsilPanel(); break; case 'ibadah': renderIbadahPanel(); break; case 'miskin': renderMiskinPanel(); break; } } - // ========================== - // ===== SPBU Panel ========= - // ========================== - function renderSpbuPanel() { - rightPanelTitle.textContent = '⛽ SPBU'; - rightPanelActions.innerHTML = ` - `; - document.getElementById('btnPanelAddSpbu').addEventListener('click', () => { - window.activateAddMode('spbu'); - }); - const items = []; - spbuLayer.eachLayer(l => { if (l.spbuData) items.push(l.spbuData); }); - - if (!items.length) { - rightPanelBody.innerHTML = '
Belum ada data SPBU
'; - return; - } - - rightPanelBody.innerHTML = ''; - items.forEach(d => { - const bdg = d.is_24_jam - ? '24 Jam' - : 'Tidak 24 Jam'; - const card = document.createElement('div'); - card.className = 'data-card'; - card.innerHTML = ` -
-
-
-
${escHtml(d.nama)}
-
📞 ${d.no_wa || '-'}
- ${bdg} -
-
- - -
-
`; - card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditSpbuModal(d.id); }); - card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteSpbu(d.id); }); - card.addEventListener('click', () => flyToSpbu(d.id)); - rightPanelBody.appendChild(card); - }); - } - - function flyToSpbu(id) { - spbuLayer.eachLayer(l => { - if (l.spbuData && l.spbuData.id == id) { map.setView(l.getLatLng(), 17); l.openPopup(); } - }); - } - - // ========================== - // ===== Jalan Panel ======== - // ========================== - function renderJalanPanel() { - rightPanelTitle.textContent = '🛣️ Jalan'; - rightPanelActions.innerHTML = ` - `; - document.getElementById('btnPanelAddJalan').addEventListener('click', () => { - window.activateDraw('polyline'); - }); - - const items = []; - jalanLayer.eachLayer(l => { if (l.jalanData) items.push(l.jalanData); }); - - if (!items.length) { - rightPanelBody.innerHTML = '
Belum ada data jalan
'; - return; - } - - const statusBadge = { - 'Nasional' : 'Nasional', - 'Provinsi' : 'Provinsi', - 'Kabupaten': 'Kabupaten', - }; - - rightPanelBody.innerHTML = ''; - items.forEach(d => { - const km = (d.panjang / 1000).toFixed(2); - const card = document.createElement('div'); - card.className = 'data-card'; - card.innerHTML = ` -
-
🛣️
-
-
${escHtml(d.nama)}
-
📏 ${km} km
- ${statusBadge[d.status] || ''} -
-
- - -
-
`; - card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditJalanModal(d.id); }); - card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteJalan(d.id); }); - card.addEventListener('click', () => flyToJalan(d.id)); - rightPanelBody.appendChild(card); - }); - } - - function flyToJalan(id) { - jalanLayer.eachLayer(l => { - if (l.jalanData && l.jalanData.id == id && l.getBounds) { - map.fitBounds(l.getBounds(), { padding: [40, 40] }); l.openPopup(); - } - }); - } - - // ========================== - // ===== Parsil Panel ======= - // ========================== - function renderParsilPanel() { - rightPanelTitle.textContent = '🗺️ Parsil Tanah'; - rightPanelActions.innerHTML = ` - `; - document.getElementById('btnPanelAddParsil').addEventListener('click', () => { - window.activateDraw('polygon'); - }); - - const items = []; - parsilLayer.eachLayer(l => { if (l.parsilData) items.push(l.parsilData); }); - - if (!items.length) { - rightPanelBody.innerHTML = '
Belum ada data parsil
'; - return; - } - - const statusBadge = { - 'SHM': 'SHM', - 'HGB': 'HGB', - 'HGU': 'HGU', - 'HP' : 'HP', - }; - - rightPanelBody.innerHTML = ''; - items.forEach(d => { - const luas = d.luas >= 10000 - ? (d.luas / 10000).toFixed(2) + ' ha' - : d.luas.toFixed(0) + ' m²'; - const card = document.createElement('div'); - card.className = 'data-card'; - card.innerHTML = ` -
-
🗺️
-
-
${escHtml(d.nama || 'Tanpa Nama')}
-
📐 ${luas}
- ${statusBadge[d.status] || ''} -
-
- - -
-
`; - card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditParsilModal(d.id); }); - card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteParsil(d.id); }); - card.addEventListener('click', () => flyToParsil(d.id)); - rightPanelBody.appendChild(card); - }); - } - - function flyToParsil(id) { - parsilLayer.eachLayer(l => { - if (l.parsilData && l.parsilData.id == id && l.getBounds) { - map.fitBounds(l.getBounds(), { padding: [40, 40] }); l.openPopup(); - } - }); - } // ========================== // ===== Ibadah Panel ======= // ========================== function renderIbadahPanel() { rightPanelTitle.textContent = '🕌 Rumah Ibadah'; - rightPanelActions.innerHTML = ` - `; - document.getElementById('btnPanelAddIbadah').addEventListener('click', () => { - window.activateAddMode('rumah_ibadah'); - }); + const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin'); + if (isAdmin) { + rightPanelActions.innerHTML = ` + `; + document.getElementById('btnPanelAddIbadah').addEventListener('click', () => { + window.activateAddMode('rumah_ibadah'); + }); + } else { + rightPanelActions.innerHTML = ''; + } const items = []; rumahIbadahLayer.eachLayer(l => { @@ -296,6 +123,15 @@ const emoji = IBADAH_EMOJI[d.jenis] || '🕌'; const card = document.createElement('div'); card.className = 'data-card'; + let actionButtons = ''; + if (isAdmin) { + actionButtons = ` +
+ + +
+ `; + } card.innerHTML = `
${emoji}
@@ -303,13 +139,12 @@
${escHtml(d.nama)}
${escHtml(d.jenis || 'Masjid')} • 📡 ${d.radius} m
-
- - -
+ ${actionButtons} `; - card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditIbadahModal(d.id); }); - card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteIbadah(d.id); }); + if (isAdmin) { + card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditIbadahModal(d.id); }); + card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteIbadah(d.id); }); + } card.addEventListener('click', () => flyToIbadah(d.id)); rightPanelBody.appendChild(card); }); @@ -328,31 +163,34 @@ // ========================== function renderMiskinPanel() { rightPanelTitle.textContent = '🏠 Penduduk Miskin'; - rightPanelActions.innerHTML = ` - - `; + const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola')); + if (canManage) { + rightPanelActions.innerHTML = ` + + `; - // Tombol Tambah: buka form via right-click popup di peta - document.getElementById('btnPanelTambahMiskin').addEventListener('click', () => { - // Aktifkan mode: klik kiri di peta = tambah miskin - window.currentAddMode = 'miskin_click'; - document.getElementById('map').style.cursor = 'crosshair'; - if (window.cursorTooltip) { - window.cursorTooltip.textContent = 'Klik di peta untuk tentukan lokasi penduduk miskin'; - } - }); + document.getElementById('btnPanelTambahMiskin').addEventListener('click', () => { + window.currentAddMode = 'miskin_click'; + document.getElementById('map').style.cursor = 'crosshair'; + if (window.cursorTooltip) { + window.cursorTooltip.textContent = 'Klik di peta untuk tentukan lokasi penduduk miskin'; + } + }); - document.getElementById('btnPanelImportMiskin').addEventListener('click', () => { - if (typeof window.openImportMiskinModal === 'function') { - window.openImportMiskinModal(); - } else { - alert('Fitur impor belum siap.'); - } - }); + document.getElementById('btnPanelImportMiskin').addEventListener('click', () => { + if (typeof window.openImportMiskinModal === 'function') { + window.openImportMiskinModal(); + } else { + alert('Fitur impor belum siap.'); + } + }); + } else { + rightPanelActions.innerHTML = ''; + } const all = []; pendudukMiskinLayer.eachLayer(l => { if (l.miskinData) all.push(l.miskinData); }); @@ -394,6 +232,7 @@ } function buildMiskinCard(d, terurus) { + const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola')); const iconCls = terurus ? 'card-icon-miskin-in' : 'card-icon-miskin-out'; const badge = d.kategori_bantuan === 'Makan' ? 'Bantuan Makan' @@ -402,6 +241,41 @@ const card = document.createElement('div'); card.className = 'data-card'; card.dataset.miskinId = d.id; + + let actionButtons = ''; + if (canManage) { + actionButtons = ` +
+ + +
+ `; + } + + let fotoHtml = ''; + if (d.foto_rumah || d.foto_kk) { + fotoHtml += `
`; + if (d.foto_rumah) { + fotoHtml += ` +
+ Foto Rumah + + + +
`; + } + if (d.foto_kk) { + fotoHtml += ` +
+ Foto KK + + + +
`; + } + fotoHtml += `
`; + } + card.innerHTML = `
🏠
@@ -410,22 +284,22 @@
👨‍👩‍👧 ${d.jumlah_jiwa || '-'} jiwa
${badge}
-
- - -
+ ${actionButtons}
Kategori Bantuan${escHtml(d.kategori_bantuan)}
Jumlah Jiwa${d.jumlah_jiwa || '-'}
Status${terurus ? 'Terurus ✅' : 'Tidak Terurus ⚠️'}
+ ${fotoHtml}
`; - card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditMiskinModal(d.id); }); - card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteMiskin(d.id); }); + if (canManage) { + card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditMiskinModal(d.id); }); + card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteMiskin(d.id); }); + } card.querySelector('.btn-log-bantuan').addEventListener('click', (e) => { e.stopPropagation(); openLogBantuan(d.id, d.nama); }); card.querySelector('.data-card-main').addEventListener('click', () => { @@ -473,6 +347,18 @@ logTitle.textContent = `Log Bantuan — ${nama}`; logModal.classList.add('show'); + const isAuth = !!window.currentUser; + const formContainer = document.querySelector('#logBantuanModal .modal-body > div:last-child'); + const modalFooter = document.querySelector('#logBantuanModal .modal-footer'); + + if (isAuth) { + if (formContainer) formContainer.style.display = 'block'; + if (modalFooter) modalFooter.style.display = 'flex'; + } else { + if (formContainer) formContainer.style.display = 'none'; + if (modalFooter) modalFooter.style.display = 'none'; + } + // Isi dropdown ibadah logIbadahSel.innerHTML = ''; if (typeof ibadahDataList !== 'undefined') { @@ -484,6 +370,13 @@ }); } + if (window.currentUser && window.currentUser.role === 'pengelola') { + logIbadahSel.value = window.currentUser.ibadah_id; + logIbadahSel.disabled = true; + } else { + logIbadahSel.disabled = false; + } + // Set tanggal default hari ini document.getElementById('logTanggal').valueAsDate = new Date(); fetchLogBantuan(miskinId); diff --git a/database.sql b/database.sql index fd63156..0590fb1 100644 --- a/database.sql +++ b/database.sql @@ -26,6 +26,8 @@ CREATE TABLE `penduduk_miskin` ( `jumlah_jiwa` int(11) NOT NULL DEFAULT 1, `lat` double NOT NULL, `lng` double NOT NULL, + `foto_rumah` varchar(255) DEFAULT NULL, + `foto_kk` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -44,39 +46,6 @@ CREATE TABLE `log_bantuan` ( CONSTRAINT `fk_log_miskin` FOREIGN KEY (`miskin_id`) REFERENCES `penduduk_miskin` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 4. Tabel jalan -DROP TABLE IF EXISTS `jalan`; -CREATE TABLE `jalan` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `nama` varchar(255) NOT NULL, - `status` varchar(100) NOT NULL DEFAULT 'Kabupaten', - `panjang` double NOT NULL DEFAULT 0, - `geom` geometry NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 5. Tabel parsil -DROP TABLE IF EXISTS `parsil`; -CREATE TABLE `parsil` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `nama` varchar(255) NOT NULL, - `status` varchar(100) NOT NULL DEFAULT 'SHM', - `luas` double NOT NULL DEFAULT 0, - `geom` geometry NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 6. Tabel spbu -DROP TABLE IF EXISTS `spbu`; -CREATE TABLE `spbu` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `nama` varchar(255) NOT NULL, - `no_wa` varchar(20) DEFAULT NULL, - `is_24_jam` tinyint(1) NOT NULL DEFAULT 0, - `lat` double NOT NULL, - `lng` double NOT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Data Contoh (Sample Data) untuk Pontianak @@ -98,20 +67,24 @@ INSERT INTO `log_bantuan` (`miskin_id`, `ibadah_id`, `tipe_bantuan`, `tanggal`, (2, 2, 'Pendidikan', '2026-05-20', 'Bantuan biaya sekolah anak'), (3, 1, 'Uang Tunai', '2026-06-01', 'Santunan tunai darurat'); --- Data Contoh SPBU -INSERT INTO `spbu` (`nama`, `no_wa`, `is_24_jam`, `lat`, `lng`) VALUES -('SPBU Ahmad Yani', '081234567890', 1, -0.0485, 109.3490), -('SPBU Oesman Sapta Odang', '081234567891', 0, -0.0345, 109.3512), -('SPBU Kota Baru', '081234567892', 1, -0.0512, 109.3198); --- Data Contoh Jalan (Polyline) --- Menggunakan ST_GeomFromText untuk membuat LineString -INSERT INTO `jalan` (`nama`, `status`, `panjang`, `geom`) VALUES -('Jl. Jenderal Ahmad Yani', 'Nasional', 2500, ST_GeomFromText('LINESTRING(109.3400 -0.0380, 109.3450 -0.0450, 109.3500 -0.0520)')), -('Jl. Gajah Mada', 'Provinsi', 1800, ST_GeomFromText('LINESTRING(109.3350 -0.0290, 109.3410 -0.0280, 109.3480 -0.0270)')); --- Data Contoh Parsil (Polygon) --- Menggunakan ST_GeomFromText untuk membuat Polygon (pastikan koordinat berputar kembali ke titik awal untuk menutup polygon) -INSERT INTO `parsil` (`nama`, `status`, `luas`, `geom`) VALUES -('Parsil Tanah A', 'SHM', 150, ST_GeomFromText('POLYGON((109.3360 -0.0250, 109.3375 -0.0250, 109.3375 -0.0260, 109.3360 -0.0260, 109.3360 -0.0250))')), -('Parsil Tanah B', 'HGB', 320, ST_GeomFromText('POLYGON((109.3405 -0.0310, 109.3420 -0.0310, 109.3420 -0.0330, 109.3405 -0.0330, 109.3405 -0.0310))')); +-- 7. Tabel users +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(100) NOT NULL UNIQUE, + `password` varchar(255) NOT NULL, + `role` enum('admin', 'pengelola') NOT NULL, + `ibadah_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_users_ibadah` FOREIGN KEY (`ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Data Contoh Pengguna (Default Users) +-- Password admin: password +-- Password pengelola: password (terhubung ke Masjid Raya Mujahidin, ibadah_id = 1) +INSERT INTO `users` (`username`, `password`, `role`, `ibadah_id`) VALUES +('admin', '$2y$10$qqKxivn4646czFu/ffwa/uZxJrkj3T9xmsrRbyxJbHx6jllpOOfDO', 'admin', NULL), +('pengelola_mujahidin', '$2y$10$qqKxivn4646czFu/ffwa/uZxJrkj3T9xmsrRbyxJbHx6jllpOOfDO', 'pengelola', 1); + diff --git a/dummy_house.png b/dummy_house.png new file mode 100644 index 0000000..fbdc40b Binary files /dev/null and b/dummy_house.png differ diff --git a/dummy_kk.png b/dummy_kk.png new file mode 100644 index 0000000..9a0df9b Binary files /dev/null and b/dummy_kk.png differ diff --git a/index.php b/index.php index ff36493..e2667a4 100644 --- a/index.php +++ b/index.php @@ -15,8 +15,7 @@ - - + @@ -30,7 +29,7 @@
- + @@ -53,18 +52,7 @@ @@ -87,68 +81,9 @@

Daftar Layer

- -
-
- - -
- -
+ - -
-
- - -
- -
- - -
-
- - -
- -
+
Pemetaan Kemiskinan
+ +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+ ⚠️ +
+
DISCLAIMER — DATA DUMMY
+
+ Seluruh data pada aplikasi ini termasuk nama penduduk, foto rumah, dan foto Kartu Keluarga (KK) merupakan data fiktif / dummy yang hanya digunakan untuk keperluan demonstrasi dan pengujian sistem. Bukan data nyata. +
+
+ +
+ + + - - - - - - - - + + + diff --git a/uploads/6a28c0d13106b_dummy_house.png b/uploads/6a28c0d13106b_dummy_house.png new file mode 100644 index 0000000..fbdc40b Binary files /dev/null and b/uploads/6a28c0d13106b_dummy_house.png differ diff --git a/uploads/6a291097d5e1f_dummy_house.png b/uploads/6a291097d5e1f_dummy_house.png new file mode 100644 index 0000000..fbdc40b Binary files /dev/null and b/uploads/6a291097d5e1f_dummy_house.png differ diff --git a/uploads/6a291097d7930_dummy_kk.png b/uploads/6a291097d7930_dummy_kk.png new file mode 100644 index 0000000..9a0df9b Binary files /dev/null and b/uploads/6a291097d7930_dummy_kk.png differ diff --git a/uploads/6a29121a0a4ae_dummy_house.png b/uploads/6a29121a0a4ae_dummy_house.png new file mode 100644 index 0000000..fbdc40b Binary files /dev/null and b/uploads/6a29121a0a4ae_dummy_house.png differ diff --git a/uploads/6a29121a6921e_dummy_kk.png b/uploads/6a29121a6921e_dummy_kk.png new file mode 100644 index 0000000..9a0df9b Binary files /dev/null and b/uploads/6a29121a6921e_dummy_kk.png differ