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 = `
+
+
+
👤 ${escHtml(window.currentUser.username)}
+
${roleLabel}${escHtml(extraLabel)}
+
+
+
+
+
+ `;
+
+ 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 = `
+ Login
+ `;
+ 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 = '-- Pilih Rumah Ibadah -- ';
+ 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 = `
-
- Nama Jalan
-
-
-
- Status
-
- Nasional
- Provinsi
- Kabupaten
-
-
-
- Panjang: ${length.toFixed(2)} m
-
- `;
- 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 = `
-
- Nama Pemilik/Area
-
-
-
- Status Tanah
-
- SHM
- HGB
- HGU
- HP
-
-
-
- Luas: ${area.toFixed(2)} m²
-
- `;
- 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
-
- Edit
- Hapus
-
-
- `;
-
- 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 = `
-
- Nama Jalan
-
-
-
- Status
-
- Nasional
- Provinsi
- Kabupaten
-
-
-
- Panjang: ${d.panjang.toFixed(2)} m
-
- `;
- 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 = `
+
+ Edit
+ Hapus
+
+ `;
+ }
const popupContent = `
${emot} ${d.nama}
Jenis: ${d.jenis || 'Masjid'}
Alamat: ${d.alamat}
Radius: ${d.radius} m
-
- Edit
- Hapus
-
+ ${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 = `
+
+
`;
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²
-
- Edit
- Hapus
-
-
- `;
- 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 = `
-
- Nama Pemilik/Area
-
-
-
- Status
-
- SHM
- HGB
- HGU
- HP
-
-
-
- Luas: ${d.luas.toFixed(2)} m²
-
- `;
- 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'}
-
- Edit
- Hapus
-
-
- `;
- 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 = `
-
- Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}
-
-
- Nama SPBU
-
-
-
- No. WA
-
-
-
- `;
-
- 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 = `
-
- Nama SPBU
-
-
-
- No. WA
-
-
-
- `;
- 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 = `
-
- Tambah SPBU
- `;
- 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 = `
-
- Tambah Jalan
- `;
- 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 = `
-
- Tambah Parsil
- `;
- 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 = `
-
- Tambah Ibadah
- `;
- document.getElementById('btnPanelAddIbadah').addEventListener('click', () => {
- window.activateAddMode('rumah_ibadah');
- });
+ const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
+ if (isAdmin) {
+ rightPanelActions.innerHTML = `
+
+ Tambah Ibadah
+ `;
+ 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 = `
-
- Tambah
-
-
- Impor
- `;
+ const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
+ if (canManage) {
+ rightPanelActions.innerHTML = `
+
+ Tambah
+
+
+ Impor
+ `;
- // 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}
Lihat Log Bantuan
`;
- 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 = '-- Pilih Rumah Ibadah -- ';
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
@@ -290,29 +225,166 @@
+
+
+ Login
+
+
+
+
+
+
+
+
+ Username
+
+
+
+ Password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Role
+
+ Pengelola Rumah Ibadah
+ Admin
+
+
+
+ Rumah Ibadah yang Dikelola
+
+
+
+
+
+
+ Batal
+ Simpan User
+
+
+
+
+
+
+
+
+ Username
+ Role
+ Rumah Ibadah
+ Aksi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
⚠️
+
+
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.
+
+
+
Tutup
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+