feat: implement full authentication system and CRUD APIs for users, rumah ibadah, and penduduk miskin with database updates.

This commit is contained in:
Syariffullah
2026-06-11 08:10:42 +07:00
parent 5585c34c5c
commit b77d4f6869
48 changed files with 1439 additions and 1442 deletions
+32 -8
View File
@@ -1,17 +1,26 @@
# WebGIS Pemetaan Kemiskinan Kota Pontianak # 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 ## Fitur Utama
- **Pemetaan Interaktif**: Visualisasi peta dengan Leaflet.js yang berpusat di Kota Pontianak. - **Pemetaan Interaktif**: Visualisasi peta dengan Leaflet.js yang berpusat di Kota Pontianak.
- **Manajemen Data Spasial**: - **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. - **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. - **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. - **Impor GeoJSON Eksternal**: Pengguna dapat menambahkan file GeoJSON dari perangkat lokal ke dalam peta.
## Persyaratan Sistem ## Persyaratan Sistem
@@ -48,13 +57,28 @@ Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, s
- Buka browser dan navigasikan ke alamat: - Buka browser dan navigasikan ke alamat:
[http://localhost/webgis/webgis-poverty-pontianak/index.php](http://localhost/webgis/webgis-poverty-pontianak/index.php) [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 ## Struktur Folder
- `api/` — Kumpulan REST API endpoint untuk operasi CRUD masing-masing fitur. - `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. - `assets/` — File aset frontend.
- `css/` — Gaya UI (custom style.css). - `css/` — Gaya UI (custom style.css).
- `js/` — Logika frontend peta dan pengelolaan interaksi panel data. - `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. - `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.
+56
View File
@@ -0,0 +1,56 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function isLoggedIn() {
return isset($_SESSION['user_id']);
}
function getCurrentUser() {
if (!isLoggedIn()) {
return null;
}
return [
'id' => $_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;
}
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (isset($_SESSION['user_id'])) {
echo json_encode([
'status' => '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
]);
}
?>
-23
View File
@@ -1,23 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
-20
View File
@@ -1,20 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
-25
View File
@@ -1,25 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT id, nama, status, panjang, ST_AsGeoJSON(geom) as geom FROM jalan";
$result = $conn->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" => []]);
}
?>
-29
View File
@@ -1,29 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+10
View File
@@ -1,5 +1,7 @@
<?php <?php
header('Content-Type: application/json'); header('Content-Type: application/json');
require_once '../auth_helper.php';
requireAdminOrPengelola();
require_once '../db_connect.php'; require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
@@ -10,6 +12,14 @@ $tipe_bantuan = trim($data['tipe_bantuan'] ?? '');
$tanggal = trim($data['tanggal'] ?? ''); $tanggal = trim($data['tanggal'] ?? '');
$keterangan = trim($data['keterangan'] ?? ''); $keterangan = trim($data['keterangan'] ?? '');
if ($_SESSION['role'] === 'pengelola') {
if ($ibadah_id !== intval($_SESSION['ibadah_id'])) {
header('HTTP/1.1 403 Forbidden');
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Anda tidak diizinkan membuat log untuk rumah ibadah lain.']);
exit;
}
}
if (!$miskin_id || !$ibadah_id || !$tipe_bantuan || !$tanggal) { if (!$miskin_id || !$ibadah_id || !$tipe_bantuan || !$tanggal) {
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']); echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
exit; exit;
+55
View File
@@ -0,0 +1,55 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once 'db_connect.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
if (empty($username) || empty($password)) {
echo json_encode(['status' => '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();
?>
+21
View File
@@ -0,0 +1,21 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION = array();
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
session_destroy();
echo json_encode(['status' => 'success', 'message' => 'Logout berhasil']);
?>
-23
View File
@@ -1,23 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
-20
View File
@@ -1,20 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
-25
View File
@@ -1,25 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT id, nama, status, luas, ST_AsGeoJSON(geom) as geom FROM parsil";
$result = $conn->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" => []]);
}
?>
-29
View File
@@ -1,29 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdminOrPengelola();
include_once '../db_connect.php'; include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
+58 -6
View File
@@ -1,18 +1,70 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdminOrPengelola();
include_once '../db_connect.php'; include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $nama = '';
$kategori = 'Makan';
$jumlah_jiwa = 1;
$lat = 0.0;
$lng = 0.0;
if (!empty($data->lat) && !empty($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 ?? ''); $nama = $conn->real_escape_string($data->nama ?? '');
$kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan');
$jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1); $jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1);
$lat = (float)$data->lat; $lat = (float)($data->lat ?? 0);
$lng = (float)$data->lng; $lng = (float)($data->lng ?? 0);
}
}
$query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng) if ($lat != 0 && $lng != 0) {
VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng)"; $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)) { if ($conn->query($query)) {
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Berhasil ditambahkan."]); echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Berhasil ditambahkan."]);
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdminOrPengelola();
include_once '../db_connect.php'; include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
+3 -1
View File
@@ -15,7 +15,9 @@ if($result->num_rows > 0) {
"kategori_bantuan" => $row['kategori_bantuan'], "kategori_bantuan" => $row['kategori_bantuan'],
"jumlah_jiwa" => isset($row['jumlah_jiwa']) ? (int)$row['jumlah_jiwa'] : 1, "jumlah_jiwa" => isset($row['jumlah_jiwa']) ? (int)$row['jumlah_jiwa'] : 1,
"lat" => (float)$row['lat'], "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); array_push($arr, $item);
} }
+88 -8
View File
@@ -1,20 +1,100 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdminOrPengelola();
include_once '../db_connect.php'; include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $id = 0;
$nama = '';
$kategori = 'Makan';
$jumlah_jiwa = 1;
$lat = 0.0;
$lng = 0.0;
if (!empty($data->id)) { if (!empty($_POST)) {
$id = (int)$data->id; $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 ?? ''); $nama = $conn->real_escape_string($data->nama ?? '');
$kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan');
$jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1); $jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1);
$lat = (float)$data->lat; $lat = (float)($data->lat ?? 0);
$lng = (float)$data->lng; $lng = (float)($data->lng ?? 0);
}
}
$query = "UPDATE penduduk_miskin if ($id > 0) {
SET nama='$nama', kategori_bantuan='$kategori', jumlah_jiwa=$jumlah_jiwa, lat=$lat, lng=$lng // Get current files to delete them if updated
WHERE id=$id"; $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)) { if ($conn->query($query)) {
echo json_encode(["status" => "success", "message" => "Update berhasil."]); echo json_encode(["status" => "success", "message" => "Update berhasil."]);
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php'; include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php'; include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
+2
View File
@@ -1,5 +1,7 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php'; include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
-24
View File
@@ -1,24 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
-20
View File
@@ -1,20 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
-26
View File
@@ -1,26 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT * FROM spbu";
$result = $conn->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" => []]);
}
?>
-25
View File
@@ -1,25 +0,0 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+47
View File
@@ -0,0 +1,47 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireAdmin();
require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
$role = trim($data['role'] ?? 'pengelola');
$ibadah_id = !empty($data['ibadah_id']) ? intval($data['ibadah_id']) : null;
if (empty($username) || empty($password)) {
echo json_encode(['status' => '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();
?>
+30
View File
@@ -0,0 +1,30 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireAdmin();
require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
$id = intval($data['id'] ?? 0);
if (!$id) {
echo json_encode(['status' => '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();
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireAdmin();
require_once '../db_connect.php';
$sql = "SELECT u.id, u.username, u.role, u.ibadah_id, ri.nama as nama_ibadah
FROM users u
LEFT JOIN rumah_ibadah ri ON u.ibadah_id = ri.id
ORDER BY u.id DESC";
$result = $conn->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();
?>
+52
View File
@@ -0,0 +1,52 @@
<?php
header('Content-Type: application/json; charset=UTF-8');
require_once '../auth_helper.php';
requireAdmin();
require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
$id = intval($data['id'] ?? 0);
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
$role = trim($data['role'] ?? 'pengelola');
$ibadah_id = !empty($data['ibadah_id']) ? intval($data['ibadah_id']) : null;
if (!$id || empty($username)) {
echo json_encode(['status' => '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();
?>
+132 -5
View File
@@ -139,6 +139,18 @@ body.right-panel-open .custom-layer-panel {
padding-bottom: 5px; 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 { .layer-option {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -190,6 +202,25 @@ body.right-panel-open .custom-layer-panel {
gap: 8px; 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 { .sidebar-btn {
width: 60px; width: 60px;
height: 60px; height: 60px;
@@ -400,9 +431,6 @@ body.right-panel-open .custom-layer-panel {
flex-shrink: 0; 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-ibadah { background: #fff7ed; }
.card-icon-miskin-out { background: #f0fdf4; } .card-icon-miskin-out { background: #f0fdf4; }
.card-icon-miskin-in { background: #fef2f2; } .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="text"],
.modal-body input[type="number"], .modal-body input[type="number"],
.modal-body input[type="date"], .modal-body input[type="date"],
.modal-body input[type="password"],
.modal-body select, .modal-body select,
.modal-body textarea { .modal-body textarea {
width: 100%; width: 100%;
@@ -836,8 +865,6 @@ body.right-panel-open .custom-layer-panel {
} }
/* Warna bubble per tipe */ /* 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.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); }
.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); } .emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); }
.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); } .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); } 0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(10px) scale(0.95); } 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;
}
+369
View File
@@ -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 = `
<div class="user-profile-pill">
<div class="user-profile-info">
<div class="user-profile-name">👤 ${escHtml(window.currentUser.username)}</div>
<div class="user-profile-role">${roleLabel}${escHtml(extraLabel)}</div>
</div>
<button class="btn-profile-logout" id="authLogoutBtn" title="Logout">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
`;
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 = `
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
`;
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 = '<option value="">-- Pilih Rumah Ibadah --</option>';
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 = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#aaa;">Memuat user...</td></tr>';
fetch('api/users/read.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
userTableBody.innerHTML = '';
if (data.data.length === 0) {
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#aaa;">Belum ada user</td></tr>';
return;
}
data.data.forEach(user => {
const tr = document.createElement('tr');
tr.style.borderBottom = '1px solid #e2e8f0';
const roleBadge = user.role === 'admin'
? '<span class="data-card-badge badge-red">Admin</span>'
: '<span class="data-card-badge badge-blue">Pengelola</span>';
tr.innerHTML = `
<td style="padding:10px 12px; font-weight:600;">${escHtml(user.username)}</td>
<td style="padding:10px 12px;">${roleBadge}</td>
<td style="padding:10px 12px; color:#64748b;">${escHtml(user.nama_ibadah)}</td>
<td style="padding:10px 12px; text-align:center; display:flex; gap:6px; justify-content:center;">
<button class="btn-card-action btn-card-edit" title="Edit User" onclick="editUser(${JSON.stringify(user).replace(/"/g, '&quot;')})">
<i class="fas fa-pen"></i>
</button>
<button class="btn-card-action btn-card-delete" title="Hapus User" onclick="deleteUser(${user.id})">
<i class="fas fa-trash"></i>
</button>
</td>
`;
userTableBody.appendChild(tr);
});
}
})
.catch(err => {
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#ef4444;">Gagal memuat user</td></tr>';
});
}
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// 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();
})();
-138
View File
@@ -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 = `
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="modalJalanNama" placeholder="Nama Jalan">
</div>
<div class="form-group">
<label>Status</label>
<select id="modalJalanStatus">
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten" selected>Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang: ${length.toFixed(2)} m</label>
</div>
`;
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 = `
<div class="form-group">
<label>Nama Pemilik/Area</label>
<input type="text" id="modalParsilNama" placeholder="Contoh: Budi atau Sawah A">
</div>
<div class="form-group">
<label>Status Tanah</label>
<select id="modalParsilStatus">
<option value="SHM" selected>SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
</div>
<div class="form-group">
<label>Luas: ${area.toFixed(2)} m²</label>
</div>
`;
openModal("Tambah Parsil Tanah", bodyHTML, function() {
window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus');
});
}
});
-164
View File
@@ -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 = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">Jalan ${d.nama}</h4>
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
<p style="margin: 0 0 10px 0;"><b>Panjang:</b> ${d.panjang.toFixed(2)} m</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditJalanModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteJalan(${d.id})">Hapus</button>
</div>
</div>
`;
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 = `
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="editJalanNama" value="${d.nama}">
</div>
<div class="form-group">
<label>Status</label>
<select id="editJalanStatus">
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Nasional</option>
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang: ${d.panjang.toFixed(2)} m</label>
</div>
`;
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();
+119 -19
View File
@@ -95,7 +95,9 @@ function addIbadahMarker(item) {
if (isNaN(lat) || isNaN(lng)) return; 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 // Lingkaran radius
const circle = L.circle([lat, lng], { const circle = L.circle([lat, lng], {
@@ -107,7 +109,7 @@ function addIbadahMarker(item) {
}); });
circle.on('mousemove', function(e) { circle.on('mousemove', function(e) {
if (isResizing) return; if (!isAdmin || isResizing) return;
const center = circle.getLatLng(); const center = circle.getLatLng();
const radius = circle.getRadius(); const radius = circle.getRadius();
const dist = center.distanceTo(e.latlng); const dist = center.distanceTo(e.latlng);
@@ -125,7 +127,7 @@ function addIbadahMarker(item) {
}); });
circle.on('mousedown', function(e) { circle.on('mousedown', function(e) {
if (circle.nearEdge) { if (isAdmin && circle.nearEdge) {
map.dragging.disable(); map.dragging.disable();
isResizing = true; isResizing = true;
resizingCircle = circle; resizingCircle = circle;
@@ -139,16 +141,22 @@ function addIbadahMarker(item) {
const d = item; const d = item;
const emot = IBADAH_EMOJI_MAP[d.jenis] || '🕌'; const emot = IBADAH_EMOJI_MAP[d.jenis] || '🕌';
let actionButtons = '';
if (isAdmin) {
actionButtons = `
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditIbadahModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteIbadah(${d.id})">Hapus</button>
</div>
`;
}
const popupContent = ` const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 160px;"> <div style="font-family: Arial, sans-serif; min-width: 160px;">
<h4 style="margin:0 0 5px 0;">${emot} ${d.nama}</h4> <h4 style="margin:0 0 5px 0;">${emot} ${d.nama}</h4>
<p style="margin: 0 0 3px 0;"><b>Jenis:</b> ${d.jenis || 'Masjid'}</p> <p style="margin: 0 0 3px 0;"><b>Jenis:</b> ${d.jenis || 'Masjid'}</p>
<p style="margin: 0 0 3px 0;"><b>Alamat:</b> ${d.alamat}</p> <p style="margin: 0 0 3px 0;"><b>Alamat:</b> ${d.alamat}</p>
<p style="margin: 0 0 10px 0;"><b>Radius:</b> ${d.radius} m</p> <p style="margin: 0 0 10px 0;"><b>Radius:</b> ${d.radius} m</p>
<div style="display:flex; gap:5px;"> ${actionButtons}
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditIbadahModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteIbadah(${d.id})">Hapus</button>
</div>
</div> </div>
`; `;
marker.bindPopup(popupContent); marker.bindPopup(popupContent);
@@ -168,7 +176,9 @@ function addIbadahMarker(item) {
// Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta) // Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta)
map.on('contextmenu', function(e) { 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 = ` const popupContent = `
<div class="popup-form"> <div class="popup-form">
@@ -181,8 +191,6 @@ map.on('contextmenu', function(e) {
// Map Click -> Form Add Rumah Ibadah // Map Click -> Form Add Rumah Ibadah
map.on('click', function(e) { map.on('click', function(e) {
if (isDrawingMode) return;
if (window.currentAddMode === 'rumah_ibadah') { if (window.currentAddMode === 'rumah_ibadah') {
const lat = e.latlng.lat; const lat = e.latlng.lat;
const lng = e.latlng.lng; const lng = e.latlng.lng;
@@ -389,19 +397,55 @@ function addMiskinMarker(item) {
const lng = parseFloat(item.lng); const lng = parseFloat(item.lng);
if (isNaN(lat) || isNaN(lng)) return; if (isNaN(lat) || isNaN(lng)) return;
const marker = L.marker([lat, lng], { icon: makeMiskinIcon(false), draggable: true }); const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
const marker = L.marker([lat, lng], { icon: makeMiskinIcon(false), draggable: canManage });
marker.miskinData = item; marker.miskinData = item;
const d = item; const d = item;
let actionButtons = '';
if (canManage) {
actionButtons = `
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditMiskinModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteMiskin(${d.id})">Hapus</button>
`;
}
let photoLinksHtml = '';
if (d.foto_rumah) {
photoLinksHtml += `
<div style="margin: 5px 0;">
<b style="font-size:11px;">Foto Rumah:</b><br>
<a href="/poverty/uploads/${d.foto_rumah}" target="_blank">
<img src="/poverty/uploads/${d.foto_rumah}" alt="Foto Rumah"
style="max-width:150px; max-height:100px; border-radius:4px; margin-top:3px; border:1px solid #ddd; object-fit:cover; cursor:pointer;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
<span style="display:none; font-size:11px; color:#ef4444;">❌ Foto tidak dapat dimuat</span>
</a>
</div>`;
}
if (d.foto_kk) {
photoLinksHtml += `
<div style="margin: 5px 0;">
<b style="font-size:11px;">Foto KK:</b><br>
<a href="/poverty/uploads/${d.foto_kk}" target="_blank">
<img src="/poverty/uploads/${d.foto_kk}" alt="Foto KK"
style="max-width:150px; max-height:100px; border-radius:4px; margin-top:3px; border:1px solid #ddd; object-fit:cover; cursor:pointer;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
<span style="display:none; font-size:11px; color:#ef4444;">❌ Foto tidak dapat dimuat</span>
</a>
</div>`;
}
const popupContent = ` const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 165px;"> <div style="font-family: Arial, sans-serif; min-width: 165px;">
<h4 style="margin:0 0 5px 0;">🏠 ${d.nama}</h4> <h4 style="margin:0 0 5px 0;">🏠 ${d.nama}</h4>
<p style="margin: 0 0 3px 0;"><b>Bantuan:</b> ${d.kategori_bantuan}</p> <p style="margin: 0 0 3px 0;"><b>Bantuan:</b> ${d.kategori_bantuan}</p>
<p style="margin: 0 0 10px 0;"><b>Jiwa:</b> ${d.jumlah_jiwa || '-'}</p> <p style="margin: 0 0 5px 0;"><b>Jiwa:</b> ${d.jumlah_jiwa || '-'}</p>
<div style="display:flex; gap:5px; flex-wrap:wrap;"> ${photoLinksHtml}
<div style="display:flex; gap:5px; flex-wrap:wrap; margin-top:8px;">
<button style="padding:4px 8px; background:#6366f1; color:white; border:none; border-radius:3px; cursor:pointer; font-size:11px;" onclick="openLogBantuan(${d.id}, '${d.nama.replace(/'/g, "\\'")}');">📋 Log Bantuan</button> <button style="padding:4px 8px; background:#6366f1; color:white; border:none; border-radius:3px; cursor:pointer; font-size:11px;" onclick="openLogBantuan(${d.id}, '${d.nama.replace(/'/g, "\\'")}');">📋 Log Bantuan</button>
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditMiskinModal(${d.id})">Edit</button> ${actionButtons}
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteMiskin(${d.id})">Hapus</button>
</div> </div>
</div> </div>
`; `;
@@ -445,6 +489,14 @@ window.formAddMiskin = function(lat, lng) {
<label>Jumlah Jiwa</label> <label>Jumlah Jiwa</label>
<input type="number" id="miskinJiwa" value="1" min="1"> <input type="number" id="miskinJiwa" value="1" min="1">
</div> </div>
<div class="form-group">
<label>Foto Rumah</label>
<input type="file" id="miskinFotoRumah" accept="image/*">
</div>
<div class="form-group">
<label>Foto Kartu Keluarga (KK)</label>
<input type="file" id="miskinFotoKK" accept="image/*">
</div>
`; `;
openModal("Tambah Penduduk Miskin", bodyHTML, function() { openModal("Tambah Penduduk Miskin", bodyHTML, function() {
window.saveNewMiskin(lat, lng); window.saveNewMiskin(lat, lng);
@@ -455,13 +507,23 @@ window.saveNewMiskin = function(lat, lng) {
const nama = document.getElementById('miskinNama').value; const nama = document.getElementById('miskinNama').value;
const kategori_bantuan = document.getElementById('miskinKategori').value; const kategori_bantuan = document.getElementById('miskinKategori').value;
const jumlah_jiwa = document.getElementById('miskinJiwa').value; const jumlah_jiwa = document.getElementById('miskinJiwa').value;
const fotoRumahInput = document.getElementById('miskinFotoRumah');
const fotoKKInput = document.getElementById('miskinFotoKK');
if (!nama) { alert('Nama harus diisi!'); return; } if (!nama) { alert('Nama harus diisi!'); return; }
const formData = new FormData();
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/create.php', { fetch('api/penduduk_miskin/create.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: formData
body: JSON.stringify({ nama, kategori_bantuan, jumlah_jiwa, lat, lng })
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
@@ -495,6 +557,32 @@ window.openEditMiskinModal = function(id) {
<label>Jumlah Jiwa</label> <label>Jumlah Jiwa</label>
<input type="number" id="editMiskinJiwa" value="${d.jumlah_jiwa || 1}" min="1"> <input type="number" id="editMiskinJiwa" value="${d.jumlah_jiwa || 1}" min="1">
</div> </div>
<div class="form-group">
<label>Foto Rumah</label>
<input type="file" id="editMiskinFotoRumah" accept="image/*">
${d.foto_rumah ? `
<div style="margin-top:5px;">
<a href="/poverty/uploads/${d.foto_rumah}" target="_blank">
<img src="/poverty/uploads/${d.foto_rumah}" alt="Foto Rumah" style="max-width:120px; max-height:80px; border-radius:4px; border:1px solid #ddd; object-fit:cover;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
<span style="display:none; font-size:11px; color:#ef4444;">❌ File gambar rusak atau tidak ditemukan</span>
</a>
<div style="font-size:11px;color:#888;margin-top:2px;">Upload baru untuk mengganti</div>
</div>` : ''}
</div>
<div class="form-group">
<label>Foto Kartu Keluarga (KK)</label>
<input type="file" id="editMiskinFotoKK" accept="image/*">
${d.foto_kk ? `
<div style="margin-top:5px;">
<a href="/poverty/uploads/${d.foto_kk}" target="_blank">
<img src="/poverty/uploads/${d.foto_kk}" alt="Foto KK" style="max-width:120px; max-height:80px; border-radius:4px; border:1px solid #ddd; object-fit:cover;"
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
<span style="display:none; font-size:11px; color:#ef4444;">❌ File gambar rusak atau tidak ditemukan</span>
</a>
<div style="font-size:11px;color:#888;margin-top:2px;">Upload baru untuk mengganti</div>
</div>` : ''}
</div>
`; `;
map.closePopup(); map.closePopup();
openModal("Edit Penduduk Miskin", bodyHTML, function() { 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 nama = document.getElementById(namaId).value;
const kategori_bantuan = document.getElementById(kategoriId).value; const kategori_bantuan = document.getElementById(kategoriId).value;
const jumlah_jiwa = jiwaId ? document.getElementById(jiwaId).value : 1; 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', { fetch('api/penduduk_miskin/update.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: formData
body: JSON.stringify({ id, nama, kategori_bantuan, jumlah_jiwa, lat, lng })
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
if(data.status === 'success') { closeModal(); loadPendudukMiskin(); } if(data.status === 'success') { closeModal(); loadPendudukMiskin(); }
}); });
-151
View File
@@ -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 = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">Parsil ${d.nama || ''}</h4>
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
<p style="margin: 0 0 10px 0;"><b>Luas:</b> ${d.luas.toFixed(2)} m²</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditParsilModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteParsil(${d.id})">Hapus</button>
</div>
</div>
`;
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 = `
<div class="form-group">
<label>Nama Pemilik/Area</label>
<input type="text" id="editParsilNama" value="${d.nama || ''}">
</div>
<div class="form-group">
<label>Status</label>
<select id="editParsilStatus">
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM</option>
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB</option>
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU</option>
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP</option>
</select>
</div>
<div class="form-group">
<label>Luas: ${d.luas.toFixed(2)} m²</label>
</div>
`;
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();
-206
View File
@@ -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: `<div class="emoji-marker"><div class="bubble ${cls}"><span>⛽</span></div></div>`,
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 = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">SPBU ${d.nama}</h4>
<p style="margin: 0 0 5px 0;"><b>No. WA:</b> ${d.no_wa}</p>
<p style="margin: 0 0 10px 0;"><b>Buka 24 Jam:</b> ${d.is_24_jam ? 'Ya' : 'Tidak'}</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditSpbuModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteSpbu(${d.id})">Hapus</button>
</div>
</div>
`;
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 = `
<div class="form-group">
<label>Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="modalSpbuNama" placeholder="Nama SPBU">
</div>
<div class="form-group">
<label>No. WA</label>
<input type="text" id="modalSpbuWa" placeholder="No. WA">
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<div class="radio-group" style="margin-top:5px;">
<label><input type="radio" name="modalSpbu24" value="1"> Ya</label>
<label><input type="radio" name="modalSpbu24" value="0" checked> Tidak</label>
</div>
</div>
`;
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 = `
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="editSpbuNama" value="${d.nama}">
</div>
<div class="form-group">
<label>No. WA</label>
<input type="text" id="editSpbuWa" value="${d.no_wa}">
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<div class="radio-group" style="margin-top:5px;">
<label><input type="radio" name="editSpbu24" value="1" ${d.is_24_jam ? 'checked' : ''}> Ya</label>
<label><input type="radio" name="editSpbu24" value="0" ${!d.is_24_jam ? 'checked' : ''}> Tidak</label>
</div>
</div>
`;
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();
+2 -88
View File
@@ -29,9 +29,6 @@ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
}).addTo(map); }).addTo(map);
// Inisialisasi FeatureGroups untuk masing-masing layer // 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 rumahIbadahLayer = L.featureGroup().addTo(map);
const pendudukMiskinLayer = L.featureGroup().addTo(map); const pendudukMiskinLayer = L.featureGroup().addTo(map);
const geoJsonLayer = L.featureGroup().addTo(map); const geoJsonLayer = L.featureGroup().addTo(map);
@@ -66,29 +63,7 @@ window.showSearchResults = function(query) {
resultsContainer.innerHTML = ''; resultsContainer.innerHTML = '';
let results = []; 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') { if (typeof rumahIbadahLayer !== 'undefined') {
rumahIbadahLayer.eachLayer(layer => { rumahIbadahLayer.eachLayer(layer => {
@@ -143,15 +118,6 @@ map.on('click', function() {
// Logic untuk Toggle Layer Visibility // 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) { document.getElementById('layerRumahIbadah').addEventListener('change', function(e) {
e.target.checked ? map.addLayer(rumahIbadahLayer) : map.removeLayer(rumahIbadahLayer); e.target.checked ? map.addLayer(rumahIbadahLayer) : map.removeLayer(rumahIbadahLayer);
}); });
@@ -170,44 +136,6 @@ window.toggleSubLayer = function(subId, iconEl) {
// --- Sub-layer Filter --- // --- Sub-layer Filter ---
window.applySubFilter = function(type) { 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') { if (type === 'miskin') {
const checked = [...document.querySelectorAll('.sub-miskin:checked')].map(el => el.value); 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) --- // --- Action Menu Logic (replaced by left sidebar) ---
window.currentAddMode = null; // 'spbu' atau 'rumah_ibadah' window.currentAddMode = null; // 'rumah_ibadah' atau 'miskin_click'
window.currentDrawMode = null; // 'polyline' atau 'polygon'
window.activateAddMode = function(mode) { window.activateAddMode = function(mode) {
// Toggle: jika mode yang sama diklik lagi, batalkan // Toggle: jika mode yang sama diklik lagi, batalkan
@@ -279,18 +206,10 @@ window.activateAddMode = function(mode) {
return; return;
} }
// Reset draw mode if active
if (window.activeDrawHandler) {
window.activeDrawHandler.disable();
window.activeDrawHandler = null;
}
window.currentAddMode = mode; window.currentAddMode = mode;
window.currentDrawMode = null;
// Tooltip // Tooltip
const tooltips = { const tooltips = {
'spbu': 'Klik untuk Tambah SPBU',
'rumah_ibadah': 'Klik untuk Tambah Rumah Ibadah' 'rumah_ibadah': 'Klik untuk Tambah Rumah Ibadah'
}; };
if (window.cursorTooltip && tooltips[mode]) window.cursorTooltip.textContent = tooltips[mode]; if (window.cursorTooltip && tooltips[mode]) window.cursorTooltip.textContent = tooltips[mode];
@@ -301,11 +220,6 @@ window.activateAddMode = function(mode) {
window.deactivateAddMode = function() { window.deactivateAddMode = function() {
window.currentAddMode = null; window.currentAddMode = null;
window.currentDrawMode = null;
if (window.activeDrawHandler) {
window.activeDrawHandler.disable();
window.activeDrawHandler = null;
}
document.getElementById('map').style.cursor = ''; document.getElementById('map').style.cursor = '';
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none'; if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
}; };
@@ -318,7 +232,7 @@ document.body.appendChild(window.cursorTooltip);
// Gunakan document-level mousemove agar selalu terpicu // Gunakan document-level mousemove agar selalu terpicu
// bahkan saat Leaflet Draw overlay aktif menangkap event map // bahkan saat Leaflet Draw overlay aktif menangkap event map
document.addEventListener('mousemove', function(e) { document.addEventListener('mousemove', function(e) {
if (window.currentAddMode || window.currentDrawMode) { if (window.currentAddMode) {
window.cursorTooltip.style.display = 'block'; window.cursorTooltip.style.display = 'block';
window.cursorTooltip.style.left = e.pageX + 15 + 'px'; window.cursorTooltip.style.left = e.pageX + 15 + 'px';
window.cursorTooltip.style.top = e.pageY + 15 + 'px'; window.cursorTooltip.style.top = e.pageY + 15 + 'px';
+81 -188
View File
@@ -83,196 +83,20 @@
rightPanelActions.innerHTML = ''; rightPanelActions.innerHTML = '';
switch (type) { switch (type) {
case 'spbu': renderSpbuPanel(); break;
case 'jalan': renderJalanPanel(); break;
case 'parsil': renderParsilPanel(); break;
case 'ibadah': renderIbadahPanel(); break; case 'ibadah': renderIbadahPanel(); break;
case 'miskin': renderMiskinPanel(); break; case 'miskin': renderMiskinPanel(); break;
} }
} }
// ==========================
// ===== SPBU Panel =========
// ==========================
function renderSpbuPanel() {
rightPanelTitle.textContent = '⛽ SPBU';
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddSpbu">
<i class="fas fa-plus"></i> Tambah SPBU
</button>`;
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 = '<div class="panel-empty">Belum ada data SPBU</div>';
return;
}
rightPanelBody.innerHTML = '';
items.forEach(d => {
const bdg = d.is_24_jam
? '<span class="data-card-badge badge-green">24 Jam</span>'
: '<span class="data-card-badge badge-red">Tidak 24 Jam</span>';
const card = document.createElement('div');
card.className = 'data-card';
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon card-icon-spbu">⛽</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">📞 ${d.no_wa || '-'}</div>
${bdg}
</div>
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>`;
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 = `
<button class="btn-panel-add" id="btnPanelAddJalan">
<i class="fas fa-plus"></i> Tambah Jalan
</button>`;
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 = '<div class="panel-empty">Belum ada data jalan</div>';
return;
}
const statusBadge = {
'Nasional' : '<span class="data-card-badge badge-red">Nasional</span>',
'Provinsi' : '<span class="data-card-badge badge-blue">Provinsi</span>',
'Kabupaten': '<span class="data-card-badge badge-green">Kabupaten</span>',
};
rightPanelBody.innerHTML = '';
items.forEach(d => {
const km = (d.panjang / 1000).toFixed(2);
const card = document.createElement('div');
card.className = 'data-card';
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon card-icon-jalan">🛣️</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">📏 ${km} km</div>
${statusBadge[d.status] || ''}
</div>
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>`;
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 = `
<button class="btn-panel-add" id="btnPanelAddParsil">
<i class="fas fa-plus"></i> Tambah Parsil
</button>`;
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 = '<div class="panel-empty">Belum ada data parsil</div>';
return;
}
const statusBadge = {
'SHM': '<span class="data-card-badge badge-green">SHM</span>',
'HGB': '<span class="data-card-badge badge-blue">HGB</span>',
'HGU': '<span class="data-card-badge badge-yellow">HGU</span>',
'HP' : '<span class="data-card-badge badge-orange">HP</span>',
};
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 = `
<div class="data-card-main">
<div class="data-card-icon card-icon-parsil">🗺️</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama || 'Tanpa Nama')}</div>
<div class="data-card-sub">📐 ${luas}</div>
${statusBadge[d.status] || ''}
</div>
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>`;
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 ======= // ===== Ibadah Panel =======
// ========================== // ==========================
function renderIbadahPanel() { function renderIbadahPanel() {
rightPanelTitle.textContent = '🕌 Rumah Ibadah'; rightPanelTitle.textContent = '🕌 Rumah Ibadah';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
if (isAdmin) {
rightPanelActions.innerHTML = ` rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddIbadah"> <button class="btn-panel-add" id="btnPanelAddIbadah">
<i class="fas fa-plus"></i> Tambah Ibadah <i class="fas fa-plus"></i> Tambah Ibadah
@@ -280,6 +104,9 @@
document.getElementById('btnPanelAddIbadah').addEventListener('click', () => { document.getElementById('btnPanelAddIbadah').addEventListener('click', () => {
window.activateAddMode('rumah_ibadah'); window.activateAddMode('rumah_ibadah');
}); });
} else {
rightPanelActions.innerHTML = '';
}
const items = []; const items = [];
rumahIbadahLayer.eachLayer(l => { rumahIbadahLayer.eachLayer(l => {
@@ -296,6 +123,15 @@
const emoji = IBADAH_EMOJI[d.jenis] || '🕌'; const emoji = IBADAH_EMOJI[d.jenis] || '🕌';
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'data-card'; card.className = 'data-card';
let actionButtons = '';
if (isAdmin) {
actionButtons = `
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
`;
}
card.innerHTML = ` card.innerHTML = `
<div class="data-card-main"> <div class="data-card-main">
<div class="data-card-icon card-icon-ibadah">${emoji}</div> <div class="data-card-icon card-icon-ibadah">${emoji}</div>
@@ -303,13 +139,12 @@
<div class="data-card-name">${escHtml(d.nama)}</div> <div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">${escHtml(d.jenis || 'Masjid')} • 📡 ${d.radius} m</div> <div class="data-card-sub">${escHtml(d.jenis || 'Masjid')} • 📡 ${d.radius} m</div>
</div> </div>
<div class="data-card-actions"> ${actionButtons}
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>`; </div>`;
if (isAdmin) {
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditIbadahModal(d.id); }); 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.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteIbadah(d.id); });
}
card.addEventListener('click', () => flyToIbadah(d.id)); card.addEventListener('click', () => flyToIbadah(d.id));
rightPanelBody.appendChild(card); rightPanelBody.appendChild(card);
}); });
@@ -328,6 +163,8 @@
// ========================== // ==========================
function renderMiskinPanel() { function renderMiskinPanel() {
rightPanelTitle.textContent = '🏠 Penduduk Miskin'; rightPanelTitle.textContent = '🏠 Penduduk Miskin';
const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
if (canManage) {
rightPanelActions.innerHTML = ` rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelTambahMiskin"> <button class="btn-panel-add" id="btnPanelTambahMiskin">
<i class="fas fa-plus"></i> Tambah <i class="fas fa-plus"></i> Tambah
@@ -336,9 +173,7 @@
<i class="fas fa-file-import"></i> Impor <i class="fas fa-file-import"></i> Impor
</button>`; </button>`;
// Tombol Tambah: buka form via right-click popup di peta
document.getElementById('btnPanelTambahMiskin').addEventListener('click', () => { document.getElementById('btnPanelTambahMiskin').addEventListener('click', () => {
// Aktifkan mode: klik kiri di peta = tambah miskin
window.currentAddMode = 'miskin_click'; window.currentAddMode = 'miskin_click';
document.getElementById('map').style.cursor = 'crosshair'; document.getElementById('map').style.cursor = 'crosshair';
if (window.cursorTooltip) { if (window.cursorTooltip) {
@@ -353,6 +188,9 @@
alert('Fitur impor belum siap.'); alert('Fitur impor belum siap.');
} }
}); });
} else {
rightPanelActions.innerHTML = '';
}
const all = []; const all = [];
pendudukMiskinLayer.eachLayer(l => { if (l.miskinData) all.push(l.miskinData); }); pendudukMiskinLayer.eachLayer(l => { if (l.miskinData) all.push(l.miskinData); });
@@ -394,6 +232,7 @@
} }
function buildMiskinCard(d, terurus) { 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 iconCls = terurus ? 'card-icon-miskin-in' : 'card-icon-miskin-out';
const badge = d.kategori_bantuan === 'Makan' const badge = d.kategori_bantuan === 'Makan'
? '<span class="data-card-badge badge-orange">Bantuan Makan</span>' ? '<span class="data-card-badge badge-orange">Bantuan Makan</span>'
@@ -402,6 +241,41 @@
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'data-card'; card.className = 'data-card';
card.dataset.miskinId = d.id; card.dataset.miskinId = d.id;
let actionButtons = '';
if (canManage) {
actionButtons = `
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
`;
}
let fotoHtml = '';
if (d.foto_rumah || d.foto_kk) {
fotoHtml += `<div class="expand-row-photos" style="display:flex; gap:10px; margin-top:8px; justify-content: space-around;">`;
if (d.foto_rumah) {
fotoHtml += `
<div style="text-align:center;">
<span style="font-size:10px; display:block; font-weight:500; color:#555;">Foto Rumah</span>
<a href="uploads/${d.foto_rumah}" target="_blank">
<img src="uploads/${d.foto_rumah}" style="width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid #ddd; margin-top:3px;" />
</a>
</div>`;
}
if (d.foto_kk) {
fotoHtml += `
<div style="text-align:center;">
<span style="font-size:10px; display:block; font-weight:500; color:#555;">Foto KK</span>
<a href="uploads/${d.foto_kk}" target="_blank">
<img src="uploads/${d.foto_kk}" style="width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid #ddd; margin-top:3px;" />
</a>
</div>`;
}
fotoHtml += `</div>`;
}
card.innerHTML = ` card.innerHTML = `
<div class="data-card-main"> <div class="data-card-main">
<div class="data-card-icon ${iconCls}">🏠</div> <div class="data-card-icon ${iconCls}">🏠</div>
@@ -410,22 +284,22 @@
<div class="data-card-sub">👨‍👩‍👧 ${d.jumlah_jiwa || '-'} jiwa</div> <div class="data-card-sub">👨‍👩‍👧 ${d.jumlah_jiwa || '-'} jiwa</div>
${badge} ${badge}
</div> </div>
<div class="data-card-actions"> ${actionButtons}
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div> </div>
<div class="data-card-expand"> <div class="data-card-expand">
<div class="expand-row"><span>Kategori Bantuan</span><span>${escHtml(d.kategori_bantuan)}</span></div> <div class="expand-row"><span>Kategori Bantuan</span><span>${escHtml(d.kategori_bantuan)}</span></div>
<div class="expand-row"><span>Jumlah Jiwa</span><span>${d.jumlah_jiwa || '-'}</span></div> <div class="expand-row"><span>Jumlah Jiwa</span><span>${d.jumlah_jiwa || '-'}</span></div>
<div class="expand-row"><span>Status</span><span>${terurus ? 'Terurus ✅' : 'Tidak Terurus ⚠️'}</span></div> <div class="expand-row"><span>Status</span><span>${terurus ? 'Terurus ✅' : 'Tidak Terurus ⚠️'}</span></div>
${fotoHtml}
<button class="btn-log-bantuan" data-id="${d.id}" data-nama="${escHtml(d.nama)}"> <button class="btn-log-bantuan" data-id="${d.id}" data-nama="${escHtml(d.nama)}">
<i class="fas fa-history"></i> Lihat Log Bantuan <i class="fas fa-history"></i> Lihat Log Bantuan
</button> </button>
</div>`; </div>`;
if (canManage) {
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditMiskinModal(d.id); }); 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-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('.btn-log-bantuan').addEventListener('click', (e) => { e.stopPropagation(); openLogBantuan(d.id, d.nama); });
card.querySelector('.data-card-main').addEventListener('click', () => { card.querySelector('.data-card-main').addEventListener('click', () => {
@@ -473,6 +347,18 @@
logTitle.textContent = `Log Bantuan — ${nama}`; logTitle.textContent = `Log Bantuan — ${nama}`;
logModal.classList.add('show'); 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 // Isi dropdown ibadah
logIbadahSel.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>'; logIbadahSel.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>';
if (typeof ibadahDataList !== 'undefined') { 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 // Set tanggal default hari ini
document.getElementById('logTanggal').valueAsDate = new Date(); document.getElementById('logTanggal').valueAsDate = new Date();
fetchLogBantuan(miskinId); fetchLogBantuan(miskinId);
+21 -48
View File
@@ -26,6 +26,8 @@ CREATE TABLE `penduduk_miskin` (
`jumlah_jiwa` int(11) NOT NULL DEFAULT 1, `jumlah_jiwa` int(11) NOT NULL DEFAULT 1,
`lat` double NOT NULL, `lat` double NOT NULL,
`lng` double NOT NULL, `lng` double NOT NULL,
`foto_rumah` varchar(255) DEFAULT NULL,
`foto_kk` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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 CONSTRAINT `fk_log_miskin` FOREIGN KEY (`miskin_id`) REFERENCES `penduduk_miskin` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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 -- 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'), (2, 2, 'Pendidikan', '2026-05-20', 'Bantuan biaya sekolah anak'),
(3, 1, 'Uang Tunai', '2026-06-01', 'Santunan tunai darurat'); (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) -- 7. Tabel users
-- Menggunakan ST_GeomFromText untuk membuat Polygon (pastikan koordinat berputar kembali ke titik awal untuk menutup polygon) DROP TABLE IF EXISTS `users`;
INSERT INTO `parsil` (`nama`, `status`, `luas`, `geom`) VALUES CREATE TABLE `users` (
('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))')), `id` int(11) NOT NULL AUTO_INCREMENT,
('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))')); `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);
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 MiB

+155 -83
View File
@@ -15,8 +15,7 @@
<!-- Leaflet CSS --> <!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Leaflet Draw CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<!-- Custom CSS --> <!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
@@ -30,7 +29,7 @@
<div class="search-icon"> <div class="search-icon">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</div> </div>
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah..."> <input type="text" class="search-input" id="searchInput" placeholder="Cari rumah ibadah...">
<button class="search-clear" id="searchClear"> <button class="search-clear" id="searchClear">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
@@ -53,18 +52,7 @@
<!-- Left Sidebar Menu --> <!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;"> <nav class="left-sidebar" id="leftSidebar" style="display:none;">
<button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
<span class="sidebar-emoji"></span>
<span class="sidebar-label">SPBU</span>
</button>
<button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
<span class="sidebar-emoji">🛣️</span>
<span class="sidebar-label">Jalan</span>
</button>
<button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
<span class="sidebar-emoji">🗺️</span>
<span class="sidebar-label">Parsil</span>
</button>
<button class="sidebar-btn" id="menuIbadah" data-panel="ibadah" title="Rumah Ibadah"> <button class="sidebar-btn" id="menuIbadah" data-panel="ibadah" title="Rumah Ibadah">
<span class="sidebar-emoji">🕌</span> <span class="sidebar-emoji">🕌</span>
<span class="sidebar-label">Ibadah</span> <span class="sidebar-label">Ibadah</span>
@@ -73,6 +61,12 @@
<span class="sidebar-emoji">🏠</span> <span class="sidebar-emoji">🏠</span>
<span class="sidebar-label">Miskin</span> <span class="sidebar-label">Miskin</span>
</button> </button>
<div class="sidebar-divider" id="sidebarAdminDivider" style="display:none;"></div>
<button class="sidebar-btn" id="menuUsers" style="display:none;" title="Manajemen Pengguna">
<span class="sidebar-emoji">👥</span>
<span class="sidebar-label">User</span>
</button>
</nav> </nav>
@@ -87,68 +81,9 @@
<div class="custom-layer-panel" id="layerPanel"> <div class="custom-layer-panel" id="layerPanel">
<h3>Daftar Layer</h3> <h3>Daftar Layer</h3>
<!-- SPBU -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerSpbu" checked> SPBU
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subSpbu', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subSpbu" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="1" checked onchange="applySubFilter('spbu')"> 24 Jam
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="0" checked onchange="applySubFilter('spbu')"> Tidak 24 Jam
</label>
</div>
</div>
<!-- Jalan -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerJalan" checked> Jalan
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subJalan', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subJalan" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Nasional" checked onchange="applySubFilter('jalan')"> Nasional
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Provinsi" checked onchange="applySubFilter('jalan')"> Provinsi
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Kabupaten" checked onchange="applySubFilter('jalan')"> Kabupaten
</label>
</div>
</div>
<!-- Parsil --> <div class="layer-section-title">Pemetaan Kemiskinan</div>
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerParsil" checked> Parsil Tanah
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subParsil', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subParsil" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="SHM" checked onchange="applySubFilter('parsil')"> SHM
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGB" checked onchange="applySubFilter('parsil')"> HGB
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
</label>
</div>
</div>
<!-- Rumah Ibadah --> <!-- Rumah Ibadah -->
<label class="layer-option"> <label class="layer-option">
@@ -290,29 +225,166 @@
</div> </div>
</div> </div>
<!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget">
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
</div>
<!-- Login Modal -->
<div id="loginModal" class="unified-modal">
<div class="modal-content" style="width: 360px;">
<div class="modal-header">
<h3>Login Ke Sistem</h3>
<span class="modal-close" id="closeLoginModal">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="loginUsername">Username</label>
<input type="text" id="loginUsername" placeholder="Masukkan username">
</div>
<div class="form-group">
<label for="loginPassword">Password</label>
<input type="password" id="loginPassword" placeholder="Masukkan password">
</div>
<div id="loginErrorMsg" style="display:none; color:#ef4444; font-size:12px; font-weight:500;"></div>
</div>
<div class="modal-footer">
<button id="loginSubmitBtn" class="btn-save" style="width: 100%;">Masuk</button>
</div>
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="unified-modal">
<div class="modal-content" style="width: 700px; max-width: 95%;">
<div class="modal-header">
<h3>Manajemen Pengguna (Admin)</h3>
<span class="modal-close" id="closeUserManagementModal">&times;</span>
</div>
<div class="modal-body" style="gap: 15px;">
<!-- Form Tambah/Edit User -->
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
<input type="hidden" id="manageUserId" value="">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Username</label>
<input type="text" id="manageUsername" placeholder="Username">
</div>
<div class="form-group">
<label>Password (Kosongkan jika tidak diubah)</label>
<input type="password" id="managePassword" placeholder="Password">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Role</label>
<select id="manageRole">
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group" id="manageIbadahGroup">
<label>Rumah Ibadah yang Dikelola</label>
<select id="manageIbadahId">
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
</select>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
</div>
</div>
<!-- Tabel Daftar User -->
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
<th style="padding: 10px 12px;">Username</th>
<th style="padding: 10px 12px;">Role</th>
<th style="padding: 10px 12px;">Rumah Ibadah</th>
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- Diisi dinamis -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast Notification Container --> <!-- Toast Notification Container -->
<div id="toastContainer"></div> <div id="toastContainer"></div>
<!-- Disclaimer Banner -->
<div id="disclaimerBanner" style="
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 2000;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
color: #f1f5f9;
border: 1px solid #f59e0b;
border-left: 4px solid #f59e0b;
border-radius: 10px;
padding: 12px 16px;
max-width: 520px;
width: calc(100% - 40px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
display: flex;
align-items: flex-start;
gap: 12px;
font-family: 'Inter', sans-serif;
animation: slideUpBanner 0.4s ease;
">
<span style="font-size: 20px; flex-shrink:0; margin-top:1px;">⚠️</span>
<div style="flex:1;">
<div style="font-size: 13px; font-weight: 700; color: #fbbf24; margin-bottom: 4px; letter-spacing: 0.3px;">DISCLAIMER DATA DUMMY</div>
<div style="font-size: 12px; line-height: 1.6; color: #cbd5e1;">
Seluruh data pada aplikasi ini termasuk <strong style="color:#fde68a;">nama penduduk, foto rumah, dan foto Kartu Keluarga (KK)</strong> merupakan <strong style="color:#fde68a;">data fiktif / dummy</strong> yang hanya digunakan untuk keperluan demonstrasi dan pengujian sistem. Bukan data nyata.
</div>
</div>
<button onclick="document.getElementById('disclaimerBanner').style.display='none'" style="
background: transparent;
border: 1px solid #475569;
color: #94a3b8;
border-radius: 6px;
padding: 3px 8px;
cursor: pointer;
font-size: 11px;
flex-shrink: 0;
margin-top: 1px;
transition: all 0.2s;
" onmouseover="this.style.borderColor='#f59e0b';this.style.color='#f59e0b'" onmouseout="this.style.borderColor='#475569';this.style.color='#94a3b8'">Tutup</button>
</div>
<style>
@keyframes slideUpBanner {
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
</style>
<!-- Leaflet JS --> <!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet Draw JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<!-- Leaflet TextPath JS (untuk teks diagonal) -->
<script src="https://cdn.jsdelivr.net/npm/leaflet-textpath@1.2.3/leaflet.textpath.min.js"></script>
<!-- Main Map Initialization --> <!-- Main Map Initialization -->
<script src="assets/js/map.js?v=<?= time() ?>"></script> <script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen --> <!-- Fitur & Komponen -->
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script>
<script src="assets/js/features/jalan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/parsil.js?v=<?= time() ?>"></script>
<script src="assets/js/features/draw_control.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script> <script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
<script src="assets/js/features/kemiskinan.js?v=<?= time() ?>"></script> <script src="assets/js/features/kemiskinan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script> <script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script> <script src="assets/js/panel.js?v=<?= time() ?>"></script>
<!-- Core Auth Feature -->
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
</body> </body>
</html> </html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 MiB