feat(landing-page):penambahan landing page utama pdemisah antar fitur
This commit is contained in:
+26
-1
@@ -67,6 +67,27 @@ $statements = [
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_aduan_coords (latitude, longitude),
|
||||
INDEX idx_aduan_status (status_tindak_lanjut)
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS locations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(255) NOT NULL,
|
||||
nomor VARCHAR(100) NOT NULL,
|
||||
buka_24jam VARCHAR(10) NOT NULL DEFAULT 'tidak',
|
||||
latitude DECIMAL(10, 8) NOT NULL,
|
||||
longitude DECIMAL(11, 8) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_locations_coords (latitude, longitude)
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS spatial_features (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_objek VARCHAR(255) NOT NULL,
|
||||
kategori VARCHAR(100) NOT NULL,
|
||||
status_objek VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
geometry_data LONGTEXT NOT NULL,
|
||||
nilai_ukur DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_features_kategori (kategori)
|
||||
)"
|
||||
];
|
||||
|
||||
@@ -85,7 +106,11 @@ $alterStatements = [
|
||||
];
|
||||
|
||||
foreach ($alterStatements as $alterSql) {
|
||||
$conn->query($alterSql);
|
||||
try {
|
||||
@$conn->query($alterSql);
|
||||
} catch (Exception $e) {
|
||||
// Abaikan jika kolom sudah ada
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'success', 'message' => 'Tabel berhasil dibuat atau sudah ada']);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM penduduk_miskin WHERE id = ?");
|
||||
$stmt->bind_param("i", $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data penduduk miskin dihapus']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
|
||||
$stmt->bind_param("i", $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Rumah ibadah berhasil dihapus']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID verifikasi tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare('DELETE FROM verifikasi_lapangan WHERE id = ?');
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $id);
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Verifikasi lapangan berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
$sql = "SELECT id, nama_ketua_kk, jumlah_anggota, latitude, longitude, rumah_ibadah_id, rumah_ibadah_nama, jarak_ke_rumah_ibadah_m, bantuan_status, bantuan_catatan, bantuan_updated_at, created_at
|
||||
FROM penduduk_miskin
|
||||
ORDER BY created_at DESC";
|
||||
|
||||
$result = $conn->query($sql);
|
||||
$data = [];
|
||||
|
||||
if (!$result) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$row['jumlah_anggota'] = (int)$row['jumlah_anggota'];
|
||||
$row['rumah_ibadah_id'] = $row['rumah_ibadah_id'] !== null ? (int)$row['rumah_ibadah_id'] : null;
|
||||
$row['jarak_ke_rumah_ibadah_m'] = $row['jarak_ke_rumah_ibadah_m'] !== null ? (float)$row['jarak_ke_rumah_ibadah_m'] : null;
|
||||
$row['bantuan_status'] = $row['bantuan_status'] ?: 'belum';
|
||||
$row['bantuan_catatan'] = $row['bantuan_catatan'] ?? '';
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
$sql = "SELECT id, nama, alamat, pic, latitude, longitude, radius_m, created_at
|
||||
FROM rumah_ibadah
|
||||
ORDER BY created_at DESC";
|
||||
|
||||
$result = $conn->query($sql);
|
||||
$data = [];
|
||||
|
||||
if (!$result) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$row['radius_m'] = (int)$row['radius_m'];
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
$sql = 'SELECT id, jenis_target, target_nama, petugas_nama, status_verifikasi, hasil_temuan, tindak_lanjut, foto, latitude, longitude, created_at FROM verifikasi_lapangan ORDER BY created_at DESC';
|
||||
$result = $conn->query($sql);
|
||||
|
||||
if (!$result) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
function distanceMeters($lat1, $lng1, $lat2, $lng2) {
|
||||
$earthRadius = 6371000;
|
||||
$toRadians = function ($value) {
|
||||
return $value * M_PI / 180;
|
||||
};
|
||||
|
||||
$deltaLat = $toRadians($lat2 - $lat1);
|
||||
$deltaLng = $toRadians($lng2 - $lng1);
|
||||
$a = sin($deltaLat / 2) * sin($deltaLat / 2) + cos($toRadians($lat1)) * cos($toRadians($lat2)) * sin($deltaLng / 2) * sin($deltaLng / 2);
|
||||
|
||||
return 2 * $earthRadius * atan2(sqrt($a), sqrt(1 - $a));
|
||||
}
|
||||
|
||||
function findNearestRumahIbadah($conn, $lat, $lng) {
|
||||
$result = $conn->query("SELECT id, nama, latitude, longitude FROM rumah_ibadah ORDER BY created_at DESC");
|
||||
if (!$result || $result->num_rows === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$nearest = null;
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$distance = distanceMeters($lat, $lng, (float)$row['latitude'], (float)$row['longitude']);
|
||||
if ($nearest === null || $distance < $nearest['distance']) {
|
||||
$nearest = [
|
||||
'id' => (int)$row['id'],
|
||||
'nama' => $row['nama'],
|
||||
'distance' => $distance
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $nearest;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$namaKetuaKk = trim($_POST['nama_ketua_kk'] ?? '');
|
||||
$jumlahAnggota = (int)($_POST['jumlah_anggota'] ?? 0);
|
||||
$lat = $_POST['latitude'] ?? null;
|
||||
$lng = $_POST['longitude'] ?? null;
|
||||
|
||||
if ($namaKetuaKk === '' || $jumlahAnggota <= 0 || $lat === null || $lng === null) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data penduduk miskin tidak lengkap']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$lat = (float)$lat;
|
||||
$lng = (float)$lng;
|
||||
$nearest = findNearestRumahIbadah($conn, $lat, $lng);
|
||||
|
||||
if (!$nearest) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Tambahkan rumah ibadah terlebih dahulu']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$jarak = $nearest['distance'];
|
||||
$rumahIbadahId = $nearest['id'];
|
||||
$rumahIbadahNama = $nearest['nama'];
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO penduduk_miskin (nama_ketua_kk, jumlah_anggota, latitude, longitude, rumah_ibadah_id, rumah_ibadah_nama, jarak_ke_rumah_ibadah_m) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param("siddisd", $namaKetuaKk, $jumlahAnggota, $lat, $lng, $rumahIbadahId, $rumahIbadahNama, $jarak);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Penduduk miskin berhasil disimpan']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$pic = trim($_POST['pic'] ?? '');
|
||||
$lat = $_POST['latitude'] ?? null;
|
||||
$lng = $_POST['longitude'] ?? null;
|
||||
$radius = $_POST['radius_m'] ?? 1000;
|
||||
|
||||
if ($nama === '' || $alamat === '' || $pic === '' || $lat === null || $lng === null) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data rumah ibadah tidak lengkap']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$lat = (float)$lat;
|
||||
$lng = (float)$lng;
|
||||
$radius = max(50, (int)$radius);
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO rumah_ibadah (nama, alamat, pic, latitude, longitude, radius_m) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param("sssddi", $nama, $alamat, $pic, $lat, $lng, $radius);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Rumah ibadah berhasil disimpan']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$jenisTarget = trim($_POST['jenis_target'] ?? 'penduduk_miskin');
|
||||
$targetNama = trim($_POST['target_nama'] ?? '');
|
||||
$petugasNama = trim($_POST['petugas_nama'] ?? '');
|
||||
$statusVerifikasi = trim($_POST['status_verifikasi'] ?? 'menunggu');
|
||||
$hasilTemuan = trim($_POST['hasil_temuan'] ?? '');
|
||||
$tindakLanjut = trim($_POST['tindak_lanjut'] ?? '');
|
||||
$latitude = $_POST['latitude'] ?? '';
|
||||
$longitude = $_POST['longitude'] ?? '';
|
||||
|
||||
if ($petugasNama === '' || empty($latitude) || empty($longitude)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Nama petugas dan koordinat wajib diisi']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$foto = null;
|
||||
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
|
||||
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
$filename = $_FILES['foto']['name'];
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($ext, $allowed, true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Format foto hanya JPG, PNG, GIF, WEBP']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Folder is in root
|
||||
if (!is_dir(__DIR__ . '/../../uploads')) {
|
||||
mkdir(__DIR__ . '/../../uploads', 0755, true);
|
||||
}
|
||||
|
||||
$foto = 'uploads/' . time() . '_' . basename($filename);
|
||||
if (!move_uploaded_file($_FILES['foto']['tmp_name'], __DIR__ . '/../../' . $foto)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Gagal upload foto']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare('INSERT INTO verifikasi_lapangan (jenis_target, target_nama, petugas_nama, status_verifikasi, hasil_temuan, tindak_lanjut, foto, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$latitude = (float)$latitude;
|
||||
$longitude = (float)$longitude;
|
||||
$stmt->bind_param('sssssssdd', $jenisTarget, $targetNama, $petugasNama, $statusVerifikasi, $hasilTemuan, $tindakLanjut, $foto, $latitude, $longitude);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Verifikasi lapangan berhasil disimpan', 'id' => $stmt->insert_id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Metode tidak diizinkan']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$status = strtolower(trim($_POST['bantuan_status'] ?? 'belum'));
|
||||
$catatan = trim($_POST['bantuan_catatan'] ?? '');
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!in_array($status, ['belum', 'proses', 'sudah'], true)) {
|
||||
$status = 'belum';
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE penduduk_miskin SET bantuan_status = ?, bantuan_catatan = ?, bantuan_updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||
$stmt->bind_param("ssi", $status, $catatan, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$updatedAtResult = $conn->query("SELECT bantuan_updated_at FROM penduduk_miskin WHERE id = " . (int)$id . " LIMIT 1");
|
||||
$updatedAt = null;
|
||||
if ($updatedAtResult && $updatedAtResult->num_rows > 0) {
|
||||
$row = $updatedAtResult->fetch_assoc();
|
||||
$updatedAt = $row['bantuan_updated_at'] ?? null;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Status bantuan berhasil diperbarui',
|
||||
'bantuan_updated_at' => $updatedAt
|
||||
]);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
function distanceMeters($lat1, $lng1, $lat2, $lng2) {
|
||||
$earthRadius = 6371000;
|
||||
$toRadians = function ($value) {
|
||||
return $value * M_PI / 180;
|
||||
};
|
||||
|
||||
$deltaLat = $toRadians($lat2 - $lat1);
|
||||
$deltaLng = $toRadians($lng2 - $lng1);
|
||||
$a = sin($deltaLat / 2) * sin($deltaLat / 2) + cos($toRadians($lat1)) * cos($toRadians($lat2)) * sin($deltaLng / 2) * sin($deltaLng / 2);
|
||||
|
||||
return 2 * $earthRadius * atan2(sqrt($a), sqrt(1 - $a));
|
||||
}
|
||||
|
||||
function findNearestRumahIbadah($conn, $lat, $lng) {
|
||||
$result = $conn->query("SELECT id, nama, latitude, longitude FROM rumah_ibadah ORDER BY created_at DESC");
|
||||
if (!$result || $result->num_rows === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$nearest = null;
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$distance = distanceMeters($lat, $lng, (float)$row['latitude'], (float)$row['longitude']);
|
||||
if ($nearest === null || $distance < $nearest['distance']) {
|
||||
$nearest = [
|
||||
'id' => (int)$row['id'],
|
||||
'nama' => $row['nama'],
|
||||
'distance' => $distance
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $nearest;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$namaKetuaKk = trim($_POST['nama_ketua_kk'] ?? '');
|
||||
$jumlahAnggota = (int)($_POST['jumlah_anggota'] ?? 0);
|
||||
$lat = $_POST['latitude'] ?? null;
|
||||
$lng = $_POST['longitude'] ?? null;
|
||||
|
||||
if ($id <= 0 || $namaKetuaKk === '' || $jumlahAnggota <= 0 || $lat === null || $lng === null) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data penduduk miskin tidak lengkap']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$lat = (float)$lat;
|
||||
$lng = (float)$lng;
|
||||
$nearest = findNearestRumahIbadah($conn, $lat, $lng);
|
||||
|
||||
if (!$nearest) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Tambahkan rumah ibadah terlebih dahulu']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$jarak = $nearest['distance'];
|
||||
$rumahIbadahId = $nearest['id'];
|
||||
$rumahIbadahNama = $nearest['nama'];
|
||||
|
||||
$stmt = $conn->prepare("UPDATE penduduk_miskin SET nama_ketua_kk = ?, jumlah_anggota = ?, latitude = ?, longitude = ?, rumah_ibadah_id = ?, rumah_ibadah_nama = ?, jarak_ke_rumah_ibadah_m = ? WHERE id = ?");
|
||||
$stmt->bind_param("siddisdi", $namaKetuaKk, $jumlahAnggota, $lat, $lng, $rumahIbadahId, $rumahIbadahNama, $jarak, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data penduduk miskin berhasil diperbarui']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$pic = trim($_POST['pic'] ?? '');
|
||||
$lat = $_POST['latitude'] ?? null;
|
||||
$lng = $_POST['longitude'] ?? null;
|
||||
$radius = $_POST['radius_m'] ?? 1000;
|
||||
|
||||
if ($id <= 0 || $nama === '' || $alamat === '' || $pic === '' || $lat === null || $lng === null) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data rumah ibadah tidak lengkap']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$lat = (float)$lat;
|
||||
$lng = (float)$lng;
|
||||
$radius = max(50, (int)$radius);
|
||||
|
||||
$stmt = $conn->prepare("UPDATE rumah_ibadah SET nama = ?, alamat = ?, pic = ?, latitude = ?, longitude = ?, radius_m = ? WHERE id = ?");
|
||||
$stmt->bind_param("sssddii", $nama, $alamat, $pic, $lat, $lng, $radius, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Rumah ibadah berhasil diperbarui']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$jenisTarget = trim($_POST['jenis_target'] ?? 'penduduk_miskin');
|
||||
$targetNama = trim($_POST['target_nama'] ?? '');
|
||||
$petugasNama = trim($_POST['petugas_nama'] ?? '');
|
||||
$statusVerifikasi = trim($_POST['status_verifikasi'] ?? 'menunggu');
|
||||
$hasilTemuan = trim($_POST['hasil_temuan'] ?? '');
|
||||
$tindakLanjut = trim($_POST['tindak_lanjut'] ?? '');
|
||||
$latitude = $_POST['latitude'] ?? '';
|
||||
$longitude = $_POST['longitude'] ?? '';
|
||||
|
||||
if ($id <= 0 || $petugasNama === '' || empty($latitude) || empty($longitude)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data verifikasi tidak lengkap']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$fotoSql = '';
|
||||
$foto = null;
|
||||
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
|
||||
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
$filename = $_FILES['foto']['name'];
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($ext, $allowed, true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Format foto hanya JPG, PNG, GIF, WEBP']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!is_dir(__DIR__ . '/../../uploads')) {
|
||||
mkdir(__DIR__ . '/../../uploads', 0755, true);
|
||||
}
|
||||
|
||||
$foto = 'uploads/' . time() . '_' . basename($filename);
|
||||
if (!move_uploaded_file($_FILES['foto']['tmp_name'], __DIR__ . '/../../' . $foto)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Gagal upload foto']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$fotoSql = ', foto = ?';
|
||||
}
|
||||
|
||||
$sql = 'UPDATE verifikasi_lapangan SET jenis_target = ?, target_nama = ?, petugas_nama = ?, status_verifikasi = ?, hasil_temuan = ?, tindak_lanjut = ?, latitude = ?, longitude = ?' . $fotoSql . ' WHERE id = ?';
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$latitude = (float)$latitude;
|
||||
$longitude = (float)$longitude;
|
||||
if ($foto !== null) {
|
||||
$stmt->bind_param('sssssssddi', $jenisTarget, $targetNama, $petugasNama, $statusVerifikasi, $hasilTemuan, $tindakLanjut, $foto, $latitude, $longitude, $id);
|
||||
} else {
|
||||
$stmt->bind_param('ssssssddi', $jenisTarget, $targetNama, $petugasNama, $statusVerifikasi, $hasilTemuan, $tindakLanjut, $latitude, $longitude, $id);
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Verifikasi lapangan berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID jalan tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM spatial_features WHERE id = ?");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("i", $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data jalan berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID laporan tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Optional: Get file path and delete physical image file
|
||||
$stmtSelect = $conn->prepare('SELECT gambar FROM jalan_rusak WHERE id = ?');
|
||||
if ($stmtSelect) {
|
||||
$stmtSelect->bind_param('i', $id);
|
||||
$stmtSelect->execute();
|
||||
$stmtSelect->bind_result($gambar);
|
||||
if ($stmtSelect->fetch()) {
|
||||
if ($gambar && file_exists(__DIR__ . '/../../' . $gambar)) {
|
||||
@unlink(__DIR__ . '/../../' . $gambar);
|
||||
}
|
||||
}
|
||||
$stmtSelect->close();
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare('DELETE FROM jalan_rusak WHERE id = ?');
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $id);
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Laporan jalan rusak berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
$sql = "SELECT id, nama_objek, kategori, status_objek, type, geometry_data, nilai_ukur, created_at
|
||||
FROM spatial_features
|
||||
WHERE kategori IN ('Jalan', 'Jalan Raya')
|
||||
ORDER BY created_at DESC";
|
||||
|
||||
$result = $conn->query($sql);
|
||||
$features = [];
|
||||
|
||||
if (!$result) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($result && $result->num_rows > 0) {
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$row['geometry_data'] = json_decode($row['geometry_data']);
|
||||
$row['nilai_ukur'] = (float)$row['nilai_ukur'];
|
||||
$features[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($features);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
$sql = "SELECT id, latitude, longitude, jenis_kerusakan, deskripsi, severity, gambar, created_at
|
||||
FROM jalan_rusak
|
||||
ORDER BY created_at DESC";
|
||||
|
||||
$result = $conn->query($sql);
|
||||
$data = [];
|
||||
|
||||
if (!$result) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($result && $result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$data[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$kategori = 'Jalan';
|
||||
$status = trim($_POST['status'] ?? 'Jalan Kabupaten');
|
||||
$type = trim($_POST['type'] ?? 'polyline');
|
||||
$geometry = $_POST['geometry'] ?? '';
|
||||
$nilai = (float)($_POST['nilai'] ?? 0);
|
||||
|
||||
if ($nama === '' || $geometry === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data jalan tidak lengkap']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO spatial_features (nama_objek, kategori, status_objek, type, geometry_data, nilai_ukur) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("sssssd", $nama, $kategori, $status, $type, $geometry, $nilai);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data jalan berhasil disimpan']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$latitude = $_POST['latitude'] ?? '';
|
||||
$longitude = $_POST['longitude'] ?? '';
|
||||
$jenis_kerusakan = $_POST['jenis_kerusakan'] ?? '';
|
||||
$deskripsi = $_POST['deskripsi'] ?? '';
|
||||
$severity = $_POST['severity'] ?? 'sedang';
|
||||
|
||||
if (empty($latitude) || empty($longitude) || empty($jenis_kerusakan)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Latitude, longitude, dan jenis kerusakan wajib diisi']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$gambar = null;
|
||||
|
||||
// Handle file upload
|
||||
if (isset($_FILES['gambar']) && $_FILES['gambar']['error'] === UPLOAD_ERR_OK) {
|
||||
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
|
||||
$filename = $_FILES['gambar']['name'];
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($ext, $allowed)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Format gambar hanya JPG, PNG, GIF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Buat folder uploads di root jika belum ada
|
||||
if (!is_dir(__DIR__ . '/../../uploads')) {
|
||||
mkdir(__DIR__ . '/../../uploads', 0755, true);
|
||||
}
|
||||
|
||||
// Generate nama file unik
|
||||
$gambar = 'uploads/' . time() . '_' . basename($filename);
|
||||
|
||||
if (!move_uploaded_file($_FILES['gambar']['tmp_name'], __DIR__ . '/../../' . $gambar)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Gagal upload gambar']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO jalan_rusak (latitude, longitude, jenis_kerusakan, deskripsi, severity, gambar) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("ddssss", $latitude, $longitude, $jenis_kerusakan, $deskripsi, $severity, $gambar);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Laporan jalan rusak berhasil disimpan', 'id' => $stmt->insert_id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID jalan tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$isGeometryUpdate = isset($_POST['geometry']);
|
||||
|
||||
if ($isGeometryUpdate) {
|
||||
$geometry = $_POST['geometry'] ?? '';
|
||||
$nilai = (float)($_POST['nilai'] ?? 0);
|
||||
|
||||
if ($geometry === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data geometri kosong']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE spatial_features SET geometry_data = ?, nilai_ukur = ? WHERE id = ?");
|
||||
$stmt->bind_param("sdi", $geometry, $nilai, $id);
|
||||
} else {
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
|
||||
if ($nama === '' || $status === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Nama atau status tidak boleh kosong']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE spatial_features SET nama_objek = ?, status_objek = ? WHERE id = ?");
|
||||
$stmt->bind_param("ssi", $nama, $status, $id);
|
||||
}
|
||||
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data jalan berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,951 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sistem GIS — Infrastruktur Jalan</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<!-- Leaflet CSS & Draw CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Outfit', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body { height: 100%; font-family: 'Outfit', sans-serif; overflow: hidden; }
|
||||
#map {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.custom-popup .leaflet-popup-content-wrapper {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
padding: 4px;
|
||||
}
|
||||
.custom-popup .leaflet-popup-content {
|
||||
margin: 12px;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800">
|
||||
|
||||
<!-- App Shell -->
|
||||
<div class="relative w-full h-screen flex overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="w-[380px] h-full bg-white border-r border-slate-200/80 flex flex-col z-10 shadow-lg relative">
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b border-slate-100 flex flex-col gap-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="../index.html" class="flex items-center justify-center w-8 h-8 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
</a>
|
||||
<div class="flex items-center gap-1.5 text-xs font-semibold text-rose-600 bg-rose-50 px-2.5 py-1 rounded-full uppercase tracking-wider">
|
||||
<span>Informatika UNTAN</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold text-slate-800 mt-2 font-black">Infrastruktur Jalan</h1>
|
||||
<p class="text-xs text-slate-500">Pemetaan jaringan jalan beserta pelaporan titik kerusakan jalan.</p>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="border-b border-slate-100 flex">
|
||||
<button onclick="switchTab('jalan')" id="tabJalan" class="flex-1 py-3 text-center text-sm font-bold border-b-2 border-indigo-600 text-indigo-600 transition">
|
||||
Jaringan Jalan
|
||||
</button>
|
||||
<button onclick="switchTab('rusak')" id="tabRusak" class="flex-1 py-3 text-center text-sm font-bold border-b-2 border-transparent text-slate-500 hover:text-slate-700 transition">
|
||||
Laporan Kerusakan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Layer toggles inside sidebar -->
|
||||
<div class="px-6 py-4 bg-slate-50 border-b border-slate-100 space-y-2">
|
||||
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Visibilitas Layer</div>
|
||||
<label class="flex items-center gap-2.5 text-xs text-slate-700 cursor-pointer">
|
||||
<input type="checkbox" id="toggleDrawnRoads" checked class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
<span>Jalan Digambar (Polyline)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2.5 text-xs text-slate-700 cursor-pointer">
|
||||
<input type="checkbox" id="toggleDamagedRoads" checked class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
<span>Laporan Jalan Rusak (Poin)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2.5 text-xs text-slate-700 cursor-pointer">
|
||||
<input type="checkbox" id="toggleExternalRoads" class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
<span>Jaringan Jalan Eksternal (GeoJSON)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- List & Search -->
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
<div class="p-4 border-b border-slate-100">
|
||||
<div class="relative">
|
||||
<input id="searchQuery" type="text" placeholder="Cari..." class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-10 pr-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<svg class="absolute left-3.5 top-3.5 text-slate-400" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Jalan Digambar -->
|
||||
<div id="listJalan" class="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
<!-- Loading Placeholder -->
|
||||
<div class="text-center py-8 text-slate-400 text-sm">Memuat data jalan...</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Jalan Rusak -->
|
||||
<div id="listRusak" class="flex-1 overflow-y-auto p-4 space-y-3 hidden">
|
||||
<!-- Loading Placeholder -->
|
||||
<div class="text-center py-8 text-slate-400 text-sm">Memuat laporan kerusakan...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tips Panel -->
|
||||
<div class="p-4 bg-rose-50/50 border-t border-rose-100/50 text-xs text-rose-800 flex gap-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0 mt-0.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||
<div id="tipsText">
|
||||
Gunakan tool <b>Draw Polyline</b> di sisi kanan peta untuk menambahkan ruas jalan baru dan menghitung panjangnya.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Container -->
|
||||
<div class="flex-1 h-full relative">
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Toast notification -->
|
||||
<div id="toast" class="absolute top-4 right-4 z-[2000] hidden items-center gap-3 px-4 py-3 bg-slate-900/90 text-white rounded-xl shadow-xl backdrop-blur-md transition transform scale-95 opacity-0">
|
||||
<span id="toastMessage" class="text-sm font-medium">Notifikasi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Input Jalan Polyline Baru -->
|
||||
<div id="newJalanModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Simpan Ruas Jalan Baru</h3>
|
||||
<button onclick="closeNewJalanModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Jalan</label>
|
||||
<input id="newJalanName" type="text" placeholder="Contoh: Jl. Ahmad Yani" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Kategori Status</label>
|
||||
<select id="newJalanStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<option value="Jalan Nasional">Jalan Nasional</option>
|
||||
<option value="Jalan Provinsi">Jalan Provinsi</option>
|
||||
<option value="Jalan Kabupaten">Jalan Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Panjang Ruas</label>
|
||||
<input id="newJalanLength" type="text" readonly class="w-full bg-slate-100 border border-slate-200 rounded-xl px-4 py-2.5 text-sm text-slate-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-3">
|
||||
<button onclick="saveNewJalan()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan Jalan</button>
|
||||
<button onclick="closeNewJalanModal()" class="flex-1 bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Input Laporan Jalan Rusak -->
|
||||
<div id="newDamageModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Lapor Jalan Rusak</h3>
|
||||
<button onclick="closeNewDamageModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Jenis Kerusakan</label>
|
||||
<select id="damageType" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-red-500 focus:bg-white focus:ring-2 focus:ring-red-100 transition shadow-sm">
|
||||
<option value="lubang">Lubang</option>
|
||||
<option value="retak">Retak</option>
|
||||
<option value="ambles">Ambles</option>
|
||||
<option value="bergelombang">Bergelombang</option>
|
||||
<option value="lainnya">Lainnya</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Tingkat Keparahan</label>
|
||||
<select id="damageSeverity" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-red-500 focus:bg-white focus:ring-2 focus:ring-red-100 transition shadow-sm">
|
||||
<option value="ringan">Ringan</option>
|
||||
<option value="sedang" selected>Sedang</option>
|
||||
<option value="berat">Berat</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Deskripsi Kondisi</label>
|
||||
<textarea id="damageDesc" placeholder="Jelaskan kondisi detail kerusakan..." rows="3" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-red-500 focus:bg-white focus:ring-2 focus:ring-red-100 transition shadow-sm"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Foto Kerusakan (Opsional)</label>
|
||||
<input id="damageImage" type="file" accept="image/*" class="w-full border border-slate-200 rounded-xl p-2 text-sm bg-slate-50">
|
||||
</div>
|
||||
|
||||
<div class="text-[10px] text-slate-400 bg-slate-50 p-2 rounded-lg">
|
||||
📍 Koordinat: <span id="damageCoords" class="font-mono">0, 0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-3">
|
||||
<button onclick="saveNewDamage()" class="flex-1 bg-red-600 hover:bg-red-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Kirim Laporan</button>
|
||||
<button onclick="closeNewDamageModal()" class="flex-1 bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Edit Jalan (Polyline) -->
|
||||
<div id="editJalanModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Edit Atribut Jalan</h3>
|
||||
<button onclick="closeEditJalanModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<input type="hidden" id="editJalanId">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Jalan</label>
|
||||
<input id="editJalanName" type="text" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Kategori Status</label>
|
||||
<select id="editJalanStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<option value="Jalan Nasional">Jalan Nasional</option>
|
||||
<option value="Jalan Provinsi">Jalan Provinsi</option>
|
||||
<option value="Jalan Kabupaten">Jalan Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Panjang Ruas</label>
|
||||
<input id="editJalanLength" type="text" readonly class="w-full bg-slate-100 border border-slate-200 rounded-xl px-4 py-2.5 text-sm text-slate-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-2">
|
||||
<button onclick="updateJalanAttr()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan</button>
|
||||
<button onclick="deleteJalanDirect()" class="bg-red-50 hover:bg-red-100 text-red-600 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Hapus</button>
|
||||
<button onclick="closeEditJalanModal()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet & Turf JS Libraries -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Init Map Pontianak
|
||||
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
||||
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||
|
||||
// Tile Layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Layers Group
|
||||
const drawnRoadsGroup = L.featureGroup().addTo(map);
|
||||
const damagedRoadsGroup = L.featureGroup().addTo(map);
|
||||
const externalRoadsGroup = L.featureGroup(); // toggled manually
|
||||
|
||||
// Draw control
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polyline: {
|
||||
shapeOptions: {
|
||||
color: '#6366f1',
|
||||
weight: 4
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
icon: L.icon({
|
||||
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
|
||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41]
|
||||
})
|
||||
},
|
||||
polygon: false,
|
||||
circle: false,
|
||||
circlemarker: false,
|
||||
rectangle: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnRoadsGroup,
|
||||
remove: true
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
let activeTab = 'jalan';
|
||||
let roadRecords = [];
|
||||
let damageRecords = [];
|
||||
let pendingLayer = null;
|
||||
let pendingDistance = 0;
|
||||
let mapRoadLayers = {};
|
||||
let mapDamageLayers = {};
|
||||
let pendingDamageLatLng = null;
|
||||
|
||||
// Styles color based on category
|
||||
const ROAD_COLORS = {
|
||||
'Jalan Nasional': '#16a34a', // green-600
|
||||
'Jalan Provinsi': '#f59e0b', // amber-500
|
||||
'Jalan Kabupaten': '#dc2626' // red-600
|
||||
};
|
||||
|
||||
const DAMAGE_SEVERITY_COLORS = {
|
||||
'ringan': '#eab308', // yellow
|
||||
'sedang': '#f97316', // orange
|
||||
'berat': '#ef4444' // red
|
||||
};
|
||||
|
||||
// Load data
|
||||
async function loadJalanData() {
|
||||
try {
|
||||
// Fetch drawn roads
|
||||
const resJalan = await fetch('api/get_features.php');
|
||||
roadRecords = await resJalan.json();
|
||||
renderRoadList(roadRecords);
|
||||
displayRoadsOnMap(roadRecords);
|
||||
|
||||
// Fetch damaged roads
|
||||
const resDamage = await fetch('api/get_jalan_rusak.php');
|
||||
damageRecords = await resDamage.json();
|
||||
renderDamageList(damageRecords);
|
||||
displayDamageOnMap(damageRecords);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Gagal memuat data jalan.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to parse geometry coordinates from GeoJSON or raw format
|
||||
function parseGeometryToCoords(geom, type) {
|
||||
if (!geom) return null;
|
||||
if (typeof geom === 'object' && geom.type && geom.coordinates) {
|
||||
if (geom.type === 'Polygon' || geom.type === 'MultiPolygon') {
|
||||
const rings = geom.coordinates;
|
||||
return rings.map(ring => ring.map(coord => [coord[1], coord[0]]));
|
||||
} else if (geom.type === 'LineString' || geom.type === 'MultiLineString') {
|
||||
const coords = geom.coordinates;
|
||||
if (geom.type === 'LineString') {
|
||||
return coords.map(coord => [coord[1], coord[0]]);
|
||||
} else {
|
||||
return coords.map(line => line.map(coord => [coord[1], coord[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return geom;
|
||||
}
|
||||
|
||||
// Display drawn roads on Map
|
||||
function displayRoadsOnMap(items) {
|
||||
drawnRoadsGroup.clearLayers();
|
||||
mapRoadLayers = {};
|
||||
|
||||
items.forEach(item => {
|
||||
if (!item.geometry_data) return;
|
||||
|
||||
const coords = parseGeometryToCoords(item.geometry_data, 'polyline');
|
||||
if (!coords) return;
|
||||
|
||||
const color = ROAD_COLORS[item.status_objek] || '#4f46e5';
|
||||
const layer = L.polyline(coords, {
|
||||
color: color,
|
||||
weight: 4,
|
||||
opacity: 0.85
|
||||
}).addTo(drawnRoadsGroup);
|
||||
|
||||
layer.on('click', (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
openEditJalanModal(item);
|
||||
});
|
||||
|
||||
layer.bindTooltip(`<b>${item.nama_objek}</b><br>${item.status_objek}<br>Panjang: ${Math.round(item.nilai_ukur).toLocaleString('id-ID')} m`, {
|
||||
sticky: true,
|
||||
className: 'text-xs rounded-lg'
|
||||
});
|
||||
|
||||
mapRoadLayers[item.id] = layer;
|
||||
});
|
||||
}
|
||||
|
||||
// Display damaged roads on Map
|
||||
function displayDamageOnMap(items) {
|
||||
damagedRoadsGroup.clearLayers();
|
||||
mapDamageLayers = {};
|
||||
|
||||
items.forEach(item => {
|
||||
const color = DAMAGE_SEVERITY_COLORS[item.severity] || '#ef4444';
|
||||
|
||||
// Create custom divicon for severity
|
||||
const iconHtml = `
|
||||
<div class="relative w-6 h-6 flex items-center justify-center rounded-full bg-white shadow-lg border border-slate-200 transition transform hover:scale-110">
|
||||
<div class="w-3.5 h-3.5 rounded-full flex items-center justify-center" style="background-color: ${color}">
|
||||
<div class="w-1.5 h-1.5 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const icon = L.divIcon({
|
||||
html: iconHtml,
|
||||
className: 'spbu-marker-icon',
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12]
|
||||
});
|
||||
|
||||
const marker = L.marker([item.latitude, item.longitude], { icon: icon }).addTo(damagedRoadsGroup);
|
||||
|
||||
// Popup Content
|
||||
const imgPath = item.gambar ? `../../${item.gambar}` : null;
|
||||
const imgTag = imgPath ? `<img src="${imgPath}" alt="Foto Kerusakan" class="w-full max-h-36 rounded-lg object-cover mt-2.5">` : '';
|
||||
|
||||
const popupContent = `
|
||||
<div class="custom-popup">
|
||||
<h4 class="font-bold text-slate-800 text-sm">Kerusakan: <span class="capitalize">${item.jenis_kerusakan}</span></h4>
|
||||
<div class="mt-1 flex items-center gap-1.5">
|
||||
<span class="text-[9px] font-extrabold uppercase tracking-wider px-2 py-0.5 rounded-full" style="background-color: ${color}20; color: ${color}">
|
||||
Keparahan: ${item.severity}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-600 mt-2">${escapeHtml(item.deskripsi || 'Tidak ada deskripsi')}</p>
|
||||
${imgTag}
|
||||
<div class="mt-3 pt-2.5 border-t border-slate-100 flex justify-end">
|
||||
<button onclick="deleteDamageDirect(${item.id})" class="text-xs text-red-600 font-bold hover:underline">Hapus Laporan</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
marker.bindPopup(popupContent, { className: 'custom-popup-wrapper' });
|
||||
mapDamageLayers[item.id] = marker;
|
||||
});
|
||||
}
|
||||
|
||||
// Render Road list
|
||||
function renderRoadList(items) {
|
||||
const list = document.getElementById('listJalan');
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = `<div class="text-center py-8 text-slate-400 text-sm">Belum ada jaringan jalan yang digambar.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => {
|
||||
const color = ROAD_COLORS[item.status_objek] || '#4f46e5';
|
||||
return `
|
||||
<div onclick="focusRoad(${item.id})" class="p-4 bg-white border border-slate-200 rounded-2xl hover:border-indigo-500 cursor-pointer transition flex flex-col gap-1.5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="font-bold text-slate-800 text-sm leading-tight">${escapeHtml(item.nama_objek)}</span>
|
||||
<span class="text-[9px] font-extrabold uppercase tracking-wider text-white px-2 py-0.5 rounded-full" style="background-color: ${color}">
|
||||
${item.status_objek.replace('Jalan ', '')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-slate-500">
|
||||
<span>Panjang: <strong>${Math.round(item.nilai_ukur)} m</strong></span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Render damage list
|
||||
function renderDamageList(items) {
|
||||
const list = document.getElementById('listRusak');
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = `<div class="text-center py-8 text-slate-400 text-sm">Belum ada laporan jalan rusak.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => {
|
||||
const color = DAMAGE_SEVERITY_COLORS[item.severity] || '#ef4444';
|
||||
return `
|
||||
<div onclick="focusDamage(${item.id})" class="p-4 bg-white border border-slate-200 rounded-2xl hover:border-red-500 cursor-pointer transition flex flex-col gap-1.5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="font-bold text-slate-800 text-sm leading-tight capitalize">${item.jenis_kerusakan}</span>
|
||||
<span class="text-[9px] font-extrabold uppercase tracking-wider px-2 py-0.5 rounded-full" style="background-color: ${color}20; color: ${color}">
|
||||
${item.severity}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 line-clamp-1">${escapeHtml(item.deskripsi || 'Tidak ada deskripsi')}</p>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Focus & Zoom to road polyline
|
||||
function focusRoad(id) {
|
||||
const layer = mapRoadLayers[id];
|
||||
if (layer) {
|
||||
map.fitBounds(layer.getBounds(), { padding: [40, 40], maxZoom: 17 });
|
||||
}
|
||||
}
|
||||
|
||||
// Focus & Zoom to damage marker
|
||||
function focusDamage(id) {
|
||||
const marker = mapDamageLayers[id];
|
||||
if (marker) {
|
||||
map.setView(marker.getLatLng(), 17);
|
||||
marker.openPopup();
|
||||
}
|
||||
}
|
||||
|
||||
// Leaflet Draw event handler
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
const layer = e.layer;
|
||||
pendingLayer = layer;
|
||||
|
||||
if (layer instanceof L.Polyline && !(layer instanceof L.Polygon)) {
|
||||
// It is a polyline (road)
|
||||
const geo = layer.toGeoJSON();
|
||||
pendingDistance = turf.length(geo, { units: 'meters' });
|
||||
|
||||
document.getElementById('newJalanName').value = '';
|
||||
document.getElementById('newJalanStatus').value = 'Jalan Kabupaten';
|
||||
document.getElementById('newJalanLength').value = `${Math.round(pendingDistance)} m`;
|
||||
|
||||
openNewJalanModal();
|
||||
} else if (layer instanceof L.Marker) {
|
||||
// It is a marker (road damage report)
|
||||
pendingDamageLatLng = layer.getLatLng();
|
||||
document.getElementById('damageDesc').value = '';
|
||||
document.getElementById('damageImage').value = '';
|
||||
document.getElementById('damageCoords').textContent = `${pendingDamageLatLng.lat.toFixed(6)}, ${pendingDamageLatLng.lng.toFixed(6)}`;
|
||||
|
||||
openNewDamageModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Edit polyline geometry
|
||||
map.on(L.Draw.Event.EDITED, function (e) {
|
||||
const layers = e.layers;
|
||||
layers.eachLayer(async function (layer) {
|
||||
let dbId = null;
|
||||
for (let id in mapRoadLayers) {
|
||||
if (mapRoadLayers[id] === layer) {
|
||||
dbId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dbId) {
|
||||
const geo = layer.toGeoJSON();
|
||||
const distance = turf.length(geo, { units: 'meters' });
|
||||
const geomString = JSON.stringify(geo.geometry);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', dbId);
|
||||
formData.append('geometry', geomString);
|
||||
formData.append('nilai', distance);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/update_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Geometri jalan berhasil diperbarui.', 'success');
|
||||
loadJalanData();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Gagal memperbarui geometri jalan.', 'error');
|
||||
loadJalanData();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete geometry via draw tools
|
||||
map.on(L.Draw.Event.DELETED, function (e) {
|
||||
const layers = e.layers;
|
||||
layers.eachLayer(async function (layer) {
|
||||
let dbId = null;
|
||||
for (let id in mapRoadLayers) {
|
||||
if (mapRoadLayers[id] === layer) {
|
||||
dbId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dbId) {
|
||||
const formData = new FormData();
|
||||
formData.append('id', dbId);
|
||||
try {
|
||||
await fetch('api/delete_feature.php', { method: 'POST', body: formData });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
setTimeout(loadJalanData, 500);
|
||||
});
|
||||
|
||||
// Save new road
|
||||
async function saveNewJalan() {
|
||||
const name = document.getElementById('newJalanName').value.trim();
|
||||
const status = document.getElementById('newJalanStatus').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('Nama jalan wajib diisi.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const geo = pendingLayer.toGeoJSON();
|
||||
const geomString = JSON.stringify(geo.geometry);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('nama', name);
|
||||
formData.append('status', status);
|
||||
formData.append('type', 'polyline');
|
||||
formData.append('geometry', geomString);
|
||||
formData.append('nilai', pendingDistance);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/save_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Jalan berhasil disimpan.', 'success');
|
||||
closeNewJalanModal();
|
||||
loadJalanData();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menyimpan jalan.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Save road damage report
|
||||
async function saveNewDamage() {
|
||||
const type = document.getElementById('damageType').value;
|
||||
const severity = document.getElementById('damageSeverity').value;
|
||||
const desc = document.getElementById('damageDesc').value.trim();
|
||||
const imgFile = document.getElementById('damageImage').files[0];
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('latitude', pendingDamageLatLng.lat);
|
||||
formData.append('longitude', pendingDamageLatLng.lng);
|
||||
formData.append('jenis_kerusakan', type);
|
||||
formData.append('deskripsi', desc);
|
||||
formData.append('severity', severity);
|
||||
if (imgFile) {
|
||||
formData.append('gambar', imgFile);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('api/save_jalan_rusak.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Laporan jalan rusak berhasil dikirim.', 'success');
|
||||
closeNewDamageModal();
|
||||
loadJalanData();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal mengirim laporan.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Open Edit road attributes modal
|
||||
function openEditJalanModal(record) {
|
||||
document.getElementById('editJalanId').value = record.id;
|
||||
document.getElementById('editJalanName').value = record.nama_objek;
|
||||
document.getElementById('editJalanStatus').value = record.status_objek;
|
||||
document.getElementById('editJalanLength').value = `${Math.round(record.nilai_ukur)} m`;
|
||||
|
||||
const modal = document.getElementById('editJalanModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Update road attributes
|
||||
async function updateJalanAttr() {
|
||||
const id = document.getElementById('editJalanId').value;
|
||||
const name = document.getElementById('editJalanName').value.trim();
|
||||
const status = document.getElementById('editJalanStatus').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('Nama jalan tidak boleh kosong.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('nama', name);
|
||||
formData.append('status', status);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/update_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Atribut jalan berhasil disimpan.', 'success');
|
||||
closeEditJalanModal();
|
||||
loadJalanData();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal memperbarui jalan.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete road direct from edit modal
|
||||
async function deleteJalanDirect() {
|
||||
const id = document.getElementById('editJalanId').value;
|
||||
if (!confirm('Hapus ruas jalan ini?')) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/delete_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Jalan berhasil dihapus.', 'success');
|
||||
closeEditJalanModal();
|
||||
loadJalanData();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menghapus jalan.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete road damage report
|
||||
window.deleteDamageDirect = async function(id) {
|
||||
if (!confirm('Apakah Anda ingin menghapus laporan kerusakan ini?')) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/delete_jalan_rusak.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Laporan kerusakan dihapus.', 'success');
|
||||
map.closePopup();
|
||||
loadJalanData();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menghapus laporan.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle visibility of layers based on checkbox inputs
|
||||
document.getElementById('toggleDrawnRoads').addEventListener('change', function(e) {
|
||||
if (e.target.checked) {
|
||||
map.addLayer(drawnRoadsGroup);
|
||||
} else {
|
||||
map.removeLayer(drawnRoadsGroup);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('toggleDamagedRoads').addEventListener('change', function(e) {
|
||||
if (e.target.checked) {
|
||||
map.addLayer(damagedRoadsGroup);
|
||||
} else {
|
||||
map.removeLayer(damagedRoadsGroup);
|
||||
}
|
||||
});
|
||||
|
||||
// Load external roads network from geojson file
|
||||
let externalRoadsLoaded = false;
|
||||
document.getElementById('toggleExternalRoads').addEventListener('change', async function(e) {
|
||||
if (e.target.checked) {
|
||||
if (!externalRoadsLoaded) {
|
||||
try {
|
||||
const res = await fetch('../../data/Export_Output_2.json');
|
||||
const data = await res.json();
|
||||
L.geoJSON(data, {
|
||||
style: {
|
||||
color: '#a855f7',
|
||||
weight: 2.5,
|
||||
opacity: 0.6
|
||||
}
|
||||
}).addTo(externalRoadsGroup);
|
||||
externalRoadsLoaded = true;
|
||||
} catch (err) {
|
||||
console.error('Gagal memuat jalan eksternal:', err);
|
||||
showToast('File data jalan eksternal tidak ditemukan.', 'error');
|
||||
e.target.checked = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
map.addLayer(externalRoadsGroup);
|
||||
} else {
|
||||
map.removeLayer(externalRoadsGroup);
|
||||
}
|
||||
});
|
||||
|
||||
// Sidebar search filter
|
||||
document.getElementById('searchQuery').addEventListener('input', function(e) {
|
||||
const query = e.target.value.toLowerCase();
|
||||
if (activeTab === 'jalan') {
|
||||
const filtered = roadRecords.filter(rec => rec.nama_objek.toLowerCase().includes(query) || rec.status_objek.toLowerCase().includes(query));
|
||||
renderRoadList(filtered);
|
||||
} else {
|
||||
const filtered = damageRecords.filter(rec => rec.jenis_kerusakan.toLowerCase().includes(query) || rec.deskripsi.toLowerCase().includes(query));
|
||||
renderDamageList(filtered);
|
||||
}
|
||||
});
|
||||
|
||||
// Tabs management
|
||||
function switchTab(tab) {
|
||||
activeTab = tab;
|
||||
const tabJalan = document.getElementById('tabJalan');
|
||||
const tabRusak = document.getElementById('tabRusak');
|
||||
const listJalan = document.getElementById('listJalan');
|
||||
const listRusak = document.getElementById('listRusak');
|
||||
const tipsText = document.getElementById('tipsText');
|
||||
|
||||
if (tab === 'jalan') {
|
||||
tabJalan.className = "flex-1 py-3 text-center text-sm font-bold border-b-2 border-indigo-600 text-indigo-600 transition";
|
||||
tabRusak.className = "flex-1 py-3 text-center text-sm font-bold border-b-2 border-transparent text-slate-500 hover:text-slate-700 transition";
|
||||
listJalan.classList.remove('hidden');
|
||||
listRusak.classList.add('hidden');
|
||||
tipsText.innerHTML = "Gunakan tool <b>Draw Polyline</b> di sisi kanan peta untuk menambahkan ruas jalan baru dan menghitung panjangnya.";
|
||||
} else {
|
||||
tabRusak.className = "flex-1 py-3 text-center text-sm font-bold border-b-2 border-indigo-600 text-indigo-600 transition";
|
||||
tabJalan.className = "flex-1 py-3 text-center text-sm font-bold border-b-2 border-transparent text-slate-500 hover:text-slate-700 transition";
|
||||
listJalan.classList.add('hidden');
|
||||
listRusak.classList.remove('hidden');
|
||||
tipsText.innerHTML = "Gunakan tool <b>Draw Marker</b> di sisi kanan peta, lalu letakkan di titik jalan rusak untuk mengisi detail laporan.";
|
||||
}
|
||||
}
|
||||
|
||||
// Modals management helpers
|
||||
function openNewJalanModal() {
|
||||
const modal = document.getElementById('newJalanModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeNewJalanModal() {
|
||||
const modal = document.getElementById('newJalanModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
if (pendingLayer) {
|
||||
map.removeLayer(pendingLayer);
|
||||
pendingLayer = null;
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function openNewDamageModal() {
|
||||
const modal = document.getElementById('newDamageModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeNewDamageModal() {
|
||||
const modal = document.getElementById('newDamageModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
if (pendingLayer) {
|
||||
map.removeLayer(pendingLayer);
|
||||
pendingLayer = null;
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function closeEditJalanModal() {
|
||||
const modal = document.getElementById('editJalanModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// Toast Helper
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast');
|
||||
const msg = document.getElementById('toastMessage');
|
||||
msg.textContent = message;
|
||||
|
||||
toast.classList.remove('hidden', 'bg-red-500', 'bg-emerald-500', 'bg-indigo-500');
|
||||
toast.classList.add('flex');
|
||||
|
||||
if (type === 'error') {
|
||||
toast.classList.add('bg-red-500');
|
||||
} else if (type === 'success') {
|
||||
toast.classList.add('bg-emerald-500');
|
||||
} else {
|
||||
toast.classList.add('bg-indigo-500');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('scale-95', 'opacity-0');
|
||||
toast.classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('scale-100', 'opacity-100');
|
||||
toast.classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hidden');
|
||||
}, 150);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// General helpers
|
||||
function escapeHtml(str) {
|
||||
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// Startup load
|
||||
loadJalanData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID parsel tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM spatial_features WHERE id = ?");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("i", $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Parsil tanah berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
$sql = "SELECT id, nama_objek, kategori, status_objek, type, geometry_data, nilai_ukur, created_at
|
||||
FROM spatial_features
|
||||
WHERE kategori IN ('Tanah', 'Parsil Tanah')
|
||||
ORDER BY created_at DESC";
|
||||
|
||||
$result = $conn->query($sql);
|
||||
$features = [];
|
||||
|
||||
if (!$result) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($result && $result->num_rows > 0) {
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$row['geometry_data'] = json_decode($row['geometry_data']);
|
||||
$row['nilai_ukur'] = (float)$row['nilai_ukur'];
|
||||
$features[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($features);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$kategori = 'Tanah'; // Force category to Tanah for Parsil feature
|
||||
$status = trim($_POST['status'] ?? 'SHM');
|
||||
$type = trim($_POST['type'] ?? 'polygon');
|
||||
$geometry = $_POST['geometry'] ?? '';
|
||||
$nilai = (float)($_POST['nilai'] ?? 0);
|
||||
|
||||
if ($nama === '' || $geometry === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data parsel tidak lengkap']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO spatial_features (nama_objek, kategori, status_objek, type, geometry_data, nilai_ukur) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("sssssd", $nama, $kategori, $status, $type, $geometry, $nilai);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Parsil tanah berhasil disimpan']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
ini_set('display_errors', '0');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID parsel tidak valid']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if it is a geometry update or attribute update
|
||||
$isGeometryUpdate = isset($_POST['geometry']);
|
||||
|
||||
if ($isGeometryUpdate) {
|
||||
$geometry = $_POST['geometry'] ?? '';
|
||||
$nilai = (float)($_POST['nilai'] ?? 0);
|
||||
|
||||
if ($geometry === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data geometri kosong']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE spatial_features SET geometry_data = ?, nilai_ukur = ? WHERE id = ?");
|
||||
$stmt->bind_param("sdi", $geometry, $nilai, $id);
|
||||
} else {
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
|
||||
if ($nama === '' || $status === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Nama atau status tidak boleh kosong']);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE spatial_features SET nama_objek = ?, status_objek = ? WHERE id = ?");
|
||||
$stmt->bind_param("ssi", $nama, $status, $id);
|
||||
}
|
||||
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Parsil tanah berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,651 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sistem GIS — Parsil Tanah</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<!-- Leaflet CSS & Draw CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Outfit', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body { height: 100%; font-family: 'Outfit', sans-serif; overflow: hidden; }
|
||||
#map {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.custom-popup .leaflet-popup-content-wrapper {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
padding: 4px;
|
||||
}
|
||||
.custom-popup .leaflet-popup-content {
|
||||
margin: 12px;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800">
|
||||
|
||||
<!-- App Shell -->
|
||||
<div class="relative w-full h-screen flex overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="w-[380px] h-full bg-white border-r border-slate-200/80 flex flex-col z-10 shadow-lg relative">
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b border-slate-100 flex flex-col gap-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="../index.html" class="flex items-center justify-center w-8 h-8 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
</a>
|
||||
<div class="flex items-center gap-1.5 text-xs font-semibold text-indigo-600 bg-indigo-50 px-2.5 py-1 rounded-full uppercase tracking-wider">
|
||||
<span>Informatika UNTAN</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold text-slate-800 mt-2">Parsil Tanah</h1>
|
||||
<p class="text-xs text-slate-500">Kelola dan ukur luas parsel tanah (polygon) secara realtime.</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="px-6 py-4 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
|
||||
<span class="text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Parsil</span>
|
||||
<span id="parselCount" class="text-base font-black text-indigo-600">0</span>
|
||||
</div>
|
||||
|
||||
<!-- List & Search -->
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
<div class="p-4 border-b border-slate-100">
|
||||
<div class="relative">
|
||||
<input id="searchQuery" type="text" placeholder="Cari nama parsil atau status..." class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-10 pr-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<svg class="absolute left-3.5 top-3.5 text-slate-400" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="parselList" class="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
<!-- Loading Placeholder -->
|
||||
<div class="text-center py-8 text-slate-400 text-sm">
|
||||
<svg class="animate-spin h-6 w-6 mx-auto mb-2 text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Memuat data parsel...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions Panel -->
|
||||
<div class="p-4 bg-indigo-50/50 border-t border-indigo-100/50 text-xs text-indigo-800 flex gap-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0 mt-0.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||
<div>
|
||||
<span class="font-bold">Tips:</span> Gunakan tombol gambar di peta (sisi kanan) untuk menggambar batas parsel tanah baru, lalu isi nama pemilik & status sertifikat.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Container -->
|
||||
<div class="flex-1 h-full relative">
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Toast notification -->
|
||||
<div id="toast" class="absolute top-4 right-4 z-[2000] hidden items-center gap-3 px-4 py-3 bg-slate-900/90 text-white rounded-xl shadow-xl backdrop-blur-md transition transform scale-95 opacity-0">
|
||||
<span id="toastMessage" class="text-sm font-medium">Notifikasi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Input Parsil Baru -->
|
||||
<div id="newParselModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Simpan Parsil Tanah</h3>
|
||||
<button onclick="closeNewParselModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Pemilik / Objek</label>
|
||||
<input id="newParselName" type="text" placeholder="Contoh: Tanah Kavling Pak Budi" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Status Sertifikat</label>
|
||||
<select id="newParselStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<option value="SHM">SHM (Hak Milik)</option>
|
||||
<option value="HGB">HGB (Guna Bangunan)</option>
|
||||
<option value="HGU">HGU (Guna Usaha)</option>
|
||||
<option value="HP">HP (Hak Pakai)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Luas Tanah</label>
|
||||
<input id="newParselArea" type="text" readonly class="w-full bg-slate-100 border border-slate-200 rounded-xl px-4 py-2.5 text-sm text-slate-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-3">
|
||||
<button onclick="saveNewParsel()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg shadow-indigo-600/10 hover:shadow-indigo-600/20 transition">Simpan Parsil</button>
|
||||
<button onclick="closeNewParselModal()" class="flex-1 bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Edit Parsil -->
|
||||
<div id="editParselModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Edit Atribut Parsil</h3>
|
||||
<button onclick="closeEditParselModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<input type="hidden" id="editParselId">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Pemilik / Objek</label>
|
||||
<input id="editParselName" type="text" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Status Sertifikat</label>
|
||||
<select id="editParselStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<option value="SHM">SHM (Hak Milik)</option>
|
||||
<option value="HGB">HGB (Guna Bangunan)</option>
|
||||
<option value="HGU">HGU (Guna Usaha)</option>
|
||||
<option value="HP">HP (Hak Pakai)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Luas Tanah</label>
|
||||
<input id="editParselArea" type="text" readonly class="w-full bg-slate-100 border border-slate-200 rounded-xl px-4 py-2.5 text-sm text-slate-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-2">
|
||||
<button onclick="updateParsilAttr()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan</button>
|
||||
<button onclick="deleteParsilDirect()" class="bg-red-50 hover:bg-red-100 text-red-600 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Hapus</button>
|
||||
<button onclick="closeEditParselModal()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet & Turf JS Libraries -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Init Map Pontianak
|
||||
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
||||
|
||||
// Custom Zoom Control at Top-Right
|
||||
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||
|
||||
// Tile Layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Draw Layer & Control
|
||||
const drawnItems = new L.FeatureGroup().addTo(map);
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polygon: {
|
||||
allowIntersection: false,
|
||||
showArea: true,
|
||||
shapeOptions: {
|
||||
color: '#6366f1',
|
||||
fillColor: '#818cf8',
|
||||
fillOpacity: 0.35,
|
||||
weight: 3
|
||||
}
|
||||
},
|
||||
polyline: false,
|
||||
circle: false,
|
||||
marker: false,
|
||||
circlemarker: false,
|
||||
rectangle: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnItems,
|
||||
remove: true
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
let records = [];
|
||||
let pendingLayer = null;
|
||||
let pendingArea = 0;
|
||||
|
||||
// Colors based on Status Sertifikat
|
||||
const STATUS_COLORS = {
|
||||
'SHM': '#10b981', // green
|
||||
'HGB': '#3b82f6', // blue
|
||||
'HGU': '#8b5cf6', // purple
|
||||
'HP': '#64748b' // slate
|
||||
};
|
||||
|
||||
// Load data from DB
|
||||
async function loadParsil() {
|
||||
try {
|
||||
const response = await fetch('api/get_features.php');
|
||||
if (!response.ok) throw new Error('HTTP Error');
|
||||
records = await response.json();
|
||||
renderParsilList(records);
|
||||
displayParsilOnMap(records);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Gagal memuat data parsel.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Render List in Sidebar
|
||||
function renderParsilList(items) {
|
||||
const list = document.getElementById('parselList');
|
||||
document.getElementById('parselCount').textContent = items.length;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = `
|
||||
<div class="text-center py-12 text-slate-400 text-sm border-2 border-dashed border-slate-200 rounded-2xl">
|
||||
<svg class="h-8 w-8 mx-auto mb-2 text-slate-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
|
||||
Belum ada data parsel tanah.
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => {
|
||||
const color = STATUS_COLORS[item.status_objek] || '#4f46e5';
|
||||
return `
|
||||
<div onclick="focusParsil(${item.id})" class="p-4 bg-white border border-slate-200 rounded-2xl hover:border-indigo-500 hover:shadow-md cursor-pointer transition flex flex-col gap-2.5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="font-bold text-slate-800 text-sm leading-tight line-clamp-1">${escapeHtml(item.nama_objek)}</span>
|
||||
<span class="text-[9px] font-extrabold uppercase tracking-wider text-white px-2 py-0.5 rounded-full" style="background-color: ${color}">
|
||||
${item.status_objek}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-slate-500">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M21 12H3"/><path d="M12 3v18"/></svg>
|
||||
Luas: <strong>${Math.round(item.nilai_ukur).toLocaleString('id-ID')} m²</strong>
|
||||
</span>
|
||||
<span class="text-[10px] text-slate-400">${formatDate(item.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Helper to parse geometry coordinates from GeoJSON or raw format
|
||||
function parseGeometryToCoords(geom, type) {
|
||||
if (!geom) return null;
|
||||
if (typeof geom === 'object' && geom.type && geom.coordinates) {
|
||||
if (geom.type === 'Polygon' || geom.type === 'MultiPolygon') {
|
||||
const rings = geom.coordinates;
|
||||
return rings.map(ring => ring.map(coord => [coord[1], coord[0]]));
|
||||
} else if (geom.type === 'LineString' || geom.type === 'MultiLineString') {
|
||||
const coords = geom.coordinates;
|
||||
if (geom.type === 'LineString') {
|
||||
return coords.map(coord => [coord[1], coord[0]]);
|
||||
} else {
|
||||
return coords.map(line => line.map(coord => [coord[1], coord[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return geom;
|
||||
}
|
||||
|
||||
// Display Markers/Polygons on Map
|
||||
let mapLayers = {};
|
||||
function displayParsilOnMap(items) {
|
||||
drawnItems.clearLayers();
|
||||
mapLayers = {};
|
||||
|
||||
items.forEach(item => {
|
||||
if (!item.geometry_data) return;
|
||||
|
||||
const coords = parseGeometryToCoords(item.geometry_data, 'polygon');
|
||||
if (!coords) return;
|
||||
|
||||
const color = STATUS_COLORS[item.status_objek] || '#4f46e5';
|
||||
const layer = L.polygon(coords, {
|
||||
color: color,
|
||||
fillColor: color,
|
||||
fillOpacity: 0.2,
|
||||
weight: 2.5
|
||||
}).addTo(drawnItems);
|
||||
|
||||
layer.on('click', (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
openEditModal(item);
|
||||
});
|
||||
|
||||
// bind simple tooltip
|
||||
layer.bindTooltip(`<b>${item.nama_objek}</b><br>Luas: ${Math.round(item.nilai_ukur).toLocaleString('id-ID')} m²`, {
|
||||
sticky: true,
|
||||
className: 'text-xs rounded-lg border-slate-100 font-sans'
|
||||
});
|
||||
|
||||
mapLayers[item.id] = layer;
|
||||
});
|
||||
}
|
||||
|
||||
// Focus & Zoom to parcel
|
||||
function focusParsil(id) {
|
||||
const layer = mapLayers[id];
|
||||
if (layer) {
|
||||
map.fitBounds(layer.getBounds(), { padding: [40, 40], maxZoom: 18 });
|
||||
}
|
||||
}
|
||||
|
||||
// Leaflet Draw Event Handler
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
const layer = e.layer;
|
||||
pendingLayer = layer;
|
||||
|
||||
const geo = layer.toGeoJSON();
|
||||
pendingArea = turf.area(geo); // calculate area using turf in m2
|
||||
|
||||
// open form modal
|
||||
document.getElementById('newParselName').value = '';
|
||||
document.getElementById('newParselStatus').value = 'SHM';
|
||||
document.getElementById('newParselArea').value = `${Math.round(pendingArea).toLocaleString('id-ID')} m²`;
|
||||
|
||||
openNewParselModal();
|
||||
});
|
||||
|
||||
// Edit polygon geometry directly inside map edit event
|
||||
map.on(L.Draw.Event.EDITED, function (e) {
|
||||
const layers = e.layers;
|
||||
layers.eachLayer(async function (layer) {
|
||||
// Find DB ID for this layer
|
||||
let dbId = null;
|
||||
for (let id in mapLayers) {
|
||||
if (mapLayers[id] === layer) {
|
||||
dbId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dbId) {
|
||||
const geo = layer.toGeoJSON();
|
||||
const area = turf.area(geo);
|
||||
const geomString = JSON.stringify(geo.geometry);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', dbId);
|
||||
formData.append('geometry', geomString);
|
||||
formData.append('nilai', area);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/update_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Geometri parsel berhasil diperbarui.', 'success');
|
||||
loadParsil();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Gagal menyimpan perubahan geometri.', 'error');
|
||||
loadParsil();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete geometry via Leaflet Draw Delete tool
|
||||
map.on(L.Draw.Event.DELETED, function (e) {
|
||||
const layers = e.layers;
|
||||
layers.eachLayer(async function (layer) {
|
||||
let dbId = null;
|
||||
for (let id in mapLayers) {
|
||||
if (mapLayers[id] === layer) {
|
||||
dbId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dbId) {
|
||||
const formData = new FormData();
|
||||
formData.append('id', dbId);
|
||||
try {
|
||||
const res = await fetch('api/delete_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Parsil berhasil dihapus.', 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
setTimeout(loadParsil, 500);
|
||||
});
|
||||
|
||||
// Save new parsel to DB
|
||||
async function saveNewParsel() {
|
||||
const name = document.getElementById('newParselName').value.trim();
|
||||
const status = document.getElementById('newParselStatus').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('Masukkan nama pemilik atau objek parsel.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const geo = pendingLayer.toGeoJSON();
|
||||
const geomString = JSON.stringify(geo.geometry);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('nama', name);
|
||||
formData.append('status', status);
|
||||
formData.append('type', 'polygon');
|
||||
formData.append('geometry', geomString);
|
||||
formData.append('nilai', pendingArea);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/save_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Parsil berhasil disimpan!', 'success');
|
||||
closeNewParselModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menyimpan parsel.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Open edit attribute modal
|
||||
function openEditModal(record) {
|
||||
document.getElementById('editParselId').value = record.id;
|
||||
document.getElementById('editParselName').value = record.nama_objek;
|
||||
document.getElementById('editParselStatus').value = record.status_objek;
|
||||
document.getElementById('editParselArea').value = `${Math.round(record.nilai_ukur).toLocaleString('id-ID')} m²`;
|
||||
|
||||
openEditParselModal();
|
||||
}
|
||||
|
||||
// Save edited attributes
|
||||
async function updateParsilAttr() {
|
||||
const id = document.getElementById('editParselId').value;
|
||||
const name = document.getElementById('editParselName').value.trim();
|
||||
const status = document.getElementById('editParselStatus').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('Nama tidak boleh kosong.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('nama', name);
|
||||
formData.append('status', status);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/update_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Atribut parsel berhasil disimpan.', 'success');
|
||||
closeEditParselModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal memperbarui atribut.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete parsel direct from modal
|
||||
async function deleteParsilDirect() {
|
||||
const id = document.getElementById('editParselId').value;
|
||||
if (!confirm('Hapus parsel tanah ini?')) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/delete_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Parsil berhasil dihapus.', 'success');
|
||||
closeEditParselModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menghapus parsel.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Search Filter
|
||||
document.getElementById('searchQuery').addEventListener('input', function (e) {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = records.filter(rec =>
|
||||
rec.nama_objek.toLowerCase().includes(query) ||
|
||||
rec.status_objek.toLowerCase().includes(query)
|
||||
);
|
||||
renderParsilList(filtered);
|
||||
});
|
||||
|
||||
// Modals management helpers
|
||||
function openNewParselModal() {
|
||||
const modal = document.getElementById('newParselModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeNewParselModal() {
|
||||
const modal = document.getElementById('newParselModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
if (pendingLayer) {
|
||||
map.removeLayer(pendingLayer);
|
||||
pendingLayer = null;
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function openEditParselModal() {
|
||||
const modal = document.getElementById('editParselModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeEditParselModal() {
|
||||
const modal = document.getElementById('editParselModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// Toast Helper
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast');
|
||||
const msg = document.getElementById('toastMessage');
|
||||
msg.textContent = message;
|
||||
|
||||
toast.classList.remove('hidden', 'bg-red-500', 'bg-emerald-500', 'bg-indigo-500');
|
||||
toast.classList.add('flex');
|
||||
|
||||
if (type === 'error') {
|
||||
toast.classList.add('bg-red-500');
|
||||
} else if (type === 'success') {
|
||||
toast.classList.add('bg-emerald-500');
|
||||
} else {
|
||||
toast.classList.add('bg-indigo-500');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('scale-95', 'opacity-0');
|
||||
toast.classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('scale-100', 'opacity-100');
|
||||
toast.classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hidden');
|
||||
}, 150);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// General helpers
|
||||
function escapeHtml(str) {
|
||||
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const d = new Date(dateString.replace(' ', 'T'));
|
||||
return d.toLocaleDateString('id-ID', { day: 'numeric', month: 'short' });
|
||||
}
|
||||
|
||||
// Startup load
|
||||
loadParsil();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM locations WHERE id = ?");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("i", $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
$sql = "SELECT * FROM locations ORDER BY created_at DESC";
|
||||
$result = $conn->query($sql);
|
||||
|
||||
$locations = [];
|
||||
|
||||
if (!$result) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($result->num_rows > 0) {
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$locations[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($locations);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nama = $_POST['nama'] ?? '';
|
||||
$nomor = $_POST['nomor'] ?? '';
|
||||
$status = $_POST['status'] ?? 'tidak';
|
||||
$lat = $_POST['lat'] ?? '';
|
||||
$lng = $_POST['lng'] ?? '';
|
||||
|
||||
// Validasi sederhana
|
||||
if(empty($nama) || empty($nomor) || empty($lat) || empty($lng)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO locations (nama, nomor, buka_24jam, latitude, longitude) VALUES (?, ?, ?, ?, ?)");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("sssdd", $nama, $nomor, $status, $lat, $lng);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data SPBU berhasil disimpan']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$lat = $_POST['lat'] ?? '';
|
||||
$lng = $_POST['lng'] ?? '';
|
||||
|
||||
if ($id <= 0 || empty($lat) || empty($lng)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE locations SET latitude = ?, longitude = ? WHERE id = ?");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("ddi", $lat, $lng, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Koordinat SPBU berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include __DIR__ . '/../../db_config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$nama = $_POST['nama'] ?? '';
|
||||
$nomor = $_POST['nomor'] ?? '';
|
||||
$status = $_POST['status'] ?? 'tidak';
|
||||
|
||||
if ($id <= 0 || empty($nama) || empty($nomor)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE locations SET nama = ?, nomor = ?, buka_24jam = ? WHERE id = ?");
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt->bind_param("sssi", $nama, $nomor, $status, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Detail SPBU berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,600 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sistem GIS — Lokasi SPBU</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Outfit', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body { height: 100%; font-family: 'Outfit', sans-serif; overflow: hidden; }
|
||||
#map {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.custom-popup .leaflet-popup-content-wrapper {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
padding: 4px;
|
||||
}
|
||||
.custom-popup .leaflet-popup-content {
|
||||
margin: 12px;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
.spbu-marker-icon {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800">
|
||||
|
||||
<!-- App Shell -->
|
||||
<div class="relative w-full h-screen flex overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="w-[380px] h-full bg-white border-r border-slate-200/80 flex flex-col z-10 shadow-lg relative">
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b border-slate-100 flex flex-col gap-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="../index.html" class="flex items-center justify-center w-8 h-8 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
</a>
|
||||
<div class="flex items-center gap-1.5 text-xs font-semibold text-emerald-600 bg-emerald-50 px-2.5 py-1 rounded-full uppercase tracking-wider">
|
||||
<span>Informatika UNTAN</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold text-slate-800 mt-2 font-black">Lokasi SPBU</h1>
|
||||
<p class="text-xs text-slate-500">Pemetaan Stasiun Pengisian Bahan Bakar Umum (SPBU) beserta jam operasional.</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="px-6 py-4 bg-slate-50 border-b border-slate-100 grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Total SPBU</span>
|
||||
<span id="spbuCount" class="text-lg font-black text-indigo-600">0</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Buka 24 Jam</span>
|
||||
<span id="spbu24Count" class="text-lg font-black text-emerald-600">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List & Search -->
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
<div class="p-4 border-b border-slate-100">
|
||||
<div class="relative">
|
||||
<input id="searchQuery" type="text" placeholder="Cari SPBU berdasarkan nama/nomor..." class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-10 pr-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<svg class="absolute left-3.5 top-3.5 text-slate-400" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="spbuList" class="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
<!-- Loading Placeholder -->
|
||||
<div class="text-center py-8 text-slate-400 text-sm">
|
||||
<svg class="animate-spin h-6 w-6 mx-auto mb-2 text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Memuat data SPBU...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions Panel -->
|
||||
<div class="p-4 bg-emerald-50/50 border-t border-emerald-100/50 text-xs text-emerald-800 flex gap-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0 mt-0.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||
<div>
|
||||
<span class="font-bold">Cara Menambah SPBU:</span> Klik tombol <span class="bg-indigo-600 text-white px-1.5 py-0.5 rounded font-semibold text-[10px]">Tambah Titik</span> di atas peta, lalu klik lokasi SPBU pada peta.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Container -->
|
||||
<div class="flex-1 h-full relative">
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Custom floating action toolbar -->
|
||||
<div class="absolute top-4 left-4 z-[2000] flex gap-2.5">
|
||||
<button id="addPointBtn" onclick="toggleAddMode()" class="flex items-center gap-2 px-4 py-2.5 bg-white border border-slate-200/80 rounded-xl shadow-lg hover:border-indigo-500 hover:shadow-indigo-500/10 font-semibold text-sm transition">
|
||||
<svg id="addIcon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="text-indigo-600"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
|
||||
<span id="addText" class="text-slate-700">Tambah Titik SPBU</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Toast notification -->
|
||||
<div id="toast" class="absolute top-4 right-4 z-[2000] hidden items-center gap-3 px-4 py-3 bg-slate-900/90 text-white rounded-xl shadow-xl backdrop-blur-md transition transform scale-95 opacity-0">
|
||||
<span id="toastMessage" class="text-sm font-medium">Notifikasi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Input SPBU Baru -->
|
||||
<div id="newSpbuModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Tambah SPBU Baru</h3>
|
||||
<button onclick="closeNewSpbuModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama SPBU</label>
|
||||
<input id="newSpbuName" type="text" placeholder="Contoh: SPBU Pertamina 61.781.01" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nomor SPBU / Kode</label>
|
||||
<input id="newSpbuNumber" type="text" placeholder="Contoh: 61.781.01" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Buka 24 Jam?</label>
|
||||
<select id="newSpbuStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<option value="ya">Ya (24 Jam)</option>
|
||||
<option value="tidak">Tidak</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-[10px] text-slate-400 bg-slate-50 border border-slate-100 p-2.5 rounded-lg">
|
||||
📍 Koordinat terpilih: <span id="newSpbuCoords" class="font-mono font-semibold">0, 0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-3">
|
||||
<button onclick="saveNewSpbu()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan</button>
|
||||
<button onclick="closeNewSpbuModal()" class="flex-1 bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Edit SPBU -->
|
||||
<div id="editSpbuModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Edit Data SPBU</h3>
|
||||
<button onclick="closeEditSpbuModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<input type="hidden" id="editSpbuId">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama SPBU</label>
|
||||
<input id="editSpbuName" type="text" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nomor SPBU</label>
|
||||
<input id="editSpbuNumber" type="text" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Buka 24 Jam?</label>
|
||||
<select id="editSpbuStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<option value="ya">Ya (24 Jam)</option>
|
||||
<option value="tidak">Tidak</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-2">
|
||||
<button onclick="updateSpbuDetails()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan Detail</button>
|
||||
<button onclick="deleteSpbuDirect()" class="bg-red-50 hover:bg-red-100 text-red-600 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Hapus</button>
|
||||
<button onclick="closeEditSpbuModal()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script>
|
||||
// Initialize Map
|
||||
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
||||
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||
|
||||
// Tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
let records = [];
|
||||
let markersGroup = L.featureGroup().addTo(map);
|
||||
let mapLayers = {};
|
||||
let isAddMode = false;
|
||||
let activePoint = null;
|
||||
|
||||
// Custom DivIcon for SPBU based on status
|
||||
function createSpbuIcon(isOpen24) {
|
||||
const color = isOpen24 ? '#10b981' : '#ef4444'; // Green if 24 Hours, Red if not
|
||||
const iconHtml = `
|
||||
<div class="relative w-7 h-7 flex items-center justify-center rounded-full bg-white shadow-lg border border-slate-200 transition transform hover:scale-110">
|
||||
<div class="w-4.5 h-4.5 rounded-full flex items-center justify-center" style="background-color: ${color}">
|
||||
<svg class="text-white w-2.5 h-2.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 10h12"/>
|
||||
<path d="M12 4v12"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>`;
|
||||
return L.divIcon({
|
||||
html: iconHtml,
|
||||
className: 'spbu-marker-icon',
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 14]
|
||||
});
|
||||
}
|
||||
|
||||
// Load SPBU data
|
||||
async function loadSPBU() {
|
||||
try {
|
||||
const response = await fetch('api/get_location.php');
|
||||
if (!response.ok) throw new Error('Query error');
|
||||
records = await response.json();
|
||||
renderSpbuList(records);
|
||||
displaySpbuOnMap(records);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Gagal memuat lokasi SPBU.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Render Sidebar List
|
||||
function renderSpbuList(items) {
|
||||
const list = document.getElementById('spbuList');
|
||||
document.getElementById('spbuCount').textContent = items.length;
|
||||
document.getElementById('spbu24Count').textContent = items.filter(i => (i.buka_24jam || '').toLowerCase() === 'ya').length;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = `
|
||||
<div class="text-center py-12 text-slate-400 text-sm border-2 border-dashed border-slate-200 rounded-2xl">
|
||||
<svg class="h-8 w-8 mx-auto mb-2 text-slate-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
Belum ada data lokasi SPBU.
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => {
|
||||
const isOpen24 = (item.buka_24jam || '').toLowerCase() === 'ya';
|
||||
return `
|
||||
<div onclick="focusSpbu(${item.id})" class="p-4 bg-white border border-slate-200 rounded-2xl hover:border-indigo-500 hover:shadow-md cursor-pointer transition flex flex-col gap-2.5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="font-bold text-slate-800 text-sm leading-tight line-clamp-1">${escapeHtml(item.nama)}</span>
|
||||
<span class="text-[9px] font-extrabold uppercase tracking-wider px-2 py-0.5 rounded-full ${isOpen24 ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'}">
|
||||
${isOpen24 ? '24 Jam' : 'Reguler'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-slate-500 border-t border-slate-50 pt-2">
|
||||
<span>No. SPBU: <strong>${escapeHtml(item.nomor)}</strong></span>
|
||||
<span class="font-mono text-[10px] text-slate-400">${Number(item.latitude || 0).toFixed(4)}, ${Number(item.longitude || 0).toFixed(4)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Display markers on Leaflet
|
||||
function displaySpbuOnMap(items) {
|
||||
markersGroup.clearLayers();
|
||||
mapLayers = {};
|
||||
|
||||
items.forEach(item => {
|
||||
const lat = parseFloat(item.latitude);
|
||||
const lng = parseFloat(item.longitude);
|
||||
if (isNaN(lat) || isNaN(lng)) return;
|
||||
|
||||
const isOpen24 = (item.buka_24jam || '').toLowerCase() === 'ya';
|
||||
const marker = L.marker([lat, lng], {
|
||||
icon: createSpbuIcon(isOpen24),
|
||||
draggable: true
|
||||
}).addTo(markersGroup);
|
||||
|
||||
// Open detail on click
|
||||
marker.on('click', (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
openEditModal(item);
|
||||
});
|
||||
|
||||
// Update coords on dragend
|
||||
marker.on('dragend', async (e) => {
|
||||
const newLatLng = marker.getLatLng();
|
||||
const formData = new FormData();
|
||||
formData.append('id', item.id);
|
||||
formData.append('lat', newLatLng.lat);
|
||||
formData.append('lng', newLatLng.lng);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/update_coords.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Koordinat SPBU berhasil dipindahkan.', 'success');
|
||||
loadSPBU();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal mengubah koordinat SPBU.', 'error');
|
||||
loadSPBU();
|
||||
}
|
||||
});
|
||||
|
||||
mapLayers[item.id] = marker;
|
||||
});
|
||||
}
|
||||
|
||||
// Focus and Zoom to SPBU marker
|
||||
function focusSpbu(id) {
|
||||
const marker = mapLayers[id];
|
||||
if (marker) {
|
||||
map.setView(marker.getLatLng(), 16);
|
||||
marker.bounce();
|
||||
}
|
||||
}
|
||||
|
||||
// Add Mode Toggle
|
||||
function toggleAddMode() {
|
||||
isAddMode = !isAddMode;
|
||||
const btn = document.getElementById('addPointBtn');
|
||||
const addIcon = document.getElementById('addIcon');
|
||||
const addText = document.getElementById('addText');
|
||||
|
||||
if (isAddMode) {
|
||||
btn.classList.add('bg-indigo-600', 'border-indigo-600', 'text-white');
|
||||
addIcon.classList.remove('text-indigo-600');
|
||||
addIcon.classList.add('text-white');
|
||||
addIcon.innerHTML = `<path d="M18 6 6 18"/><path d="m6 6 12 12"/>`;
|
||||
addText.textContent = 'Batal Tambah';
|
||||
addText.classList.remove('text-slate-700');
|
||||
addText.classList.add('text-white');
|
||||
map.getContainer().style.cursor = 'crosshair';
|
||||
showToast('Klik lokasi pada peta untuk meletakkan titik SPBU.', 'info');
|
||||
} else {
|
||||
btn.classList.remove('bg-indigo-600', 'border-indigo-600', 'text-white');
|
||||
addIcon.classList.remove('text-white');
|
||||
addIcon.classList.add('text-indigo-600');
|
||||
addIcon.innerHTML = `<path d="M12 5v14"/><path d="M5 12h14"/>`;
|
||||
addText.textContent = 'Tambah Titik SPBU';
|
||||
addText.classList.remove('text-white');
|
||||
addText.classList.add('text-slate-700');
|
||||
map.getContainer().style.cursor = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Map Click Handler for Adding Point
|
||||
map.on('click', function (e) {
|
||||
if (!isAddMode) return;
|
||||
|
||||
activePoint = e.latlng;
|
||||
document.getElementById('newSpbuName').value = '';
|
||||
document.getElementById('newSpbuNumber').value = '';
|
||||
document.getElementById('newSpbuStatus').value = 'ya';
|
||||
document.getElementById('newSpbuCoords').textContent = `${activePoint.lat.toFixed(6)}, ${activePoint.lng.toFixed(6)}`;
|
||||
|
||||
openNewSpbuModal();
|
||||
});
|
||||
|
||||
// Save new SPBU
|
||||
async function saveNewSpbu() {
|
||||
const name = document.getElementById('newSpbuName').value.trim();
|
||||
const number = document.getElementById('newSpbuNumber').value.trim();
|
||||
const status = document.getElementById('newSpbuStatus').value;
|
||||
|
||||
if (!name || !number) {
|
||||
showToast('Harap isi nama dan nomor SPBU.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('nama', name);
|
||||
formData.append('nomor', number);
|
||||
formData.append('status', status);
|
||||
formData.append('lat', activePoint.lat);
|
||||
formData.append('lng', activePoint.lng);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/save_location.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('SPBU berhasil ditambahkan!', 'success');
|
||||
closeNewSpbuModal();
|
||||
toggleAddMode(); // disable add mode
|
||||
loadSPBU();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menyimpan lokasi SPBU.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Open edit modal
|
||||
function openEditModal(record) {
|
||||
document.getElementById('editSpbuId').value = record.id;
|
||||
document.getElementById('editSpbuName').value = record.nama;
|
||||
document.getElementById('editSpbuNumber').value = record.nomor;
|
||||
document.getElementById('editSpbuStatus').value = (record.buka_24jam || '').toLowerCase();
|
||||
|
||||
openEditSpbuModal();
|
||||
}
|
||||
|
||||
// Update details
|
||||
async function updateSpbuDetails() {
|
||||
const id = document.getElementById('editSpbuId').value;
|
||||
const name = document.getElementById('editSpbuName').value.trim();
|
||||
const number = document.getElementById('editSpbuNumber').value.trim();
|
||||
const status = document.getElementById('editSpbuStatus').value;
|
||||
|
||||
if (!name || !number) {
|
||||
showToast('Nama dan Nomor SPBU tidak boleh kosong.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('nama', name);
|
||||
formData.append('nomor', number);
|
||||
formData.append('status', status);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/update_details.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Data SPBU berhasil disimpan.', 'success');
|
||||
closeEditSpbuModal();
|
||||
loadSPBU();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal memperbarui SPBU.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete SPBU
|
||||
async function deleteSpbuDirect() {
|
||||
const id = document.getElementById('editSpbuId').value;
|
||||
if (!confirm('Apakah Anda yakin ingin menghapus SPBU ini?')) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/delete_location.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('SPBU berhasil dihapus.', 'success');
|
||||
closeEditSpbuModal();
|
||||
loadSPBU();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menghapus SPBU.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Search Filter
|
||||
document.getElementById('searchQuery').addEventListener('input', function (e) {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = records.filter(rec =>
|
||||
rec.nama.toLowerCase().includes(query) ||
|
||||
rec.nomor.toLowerCase().includes(query)
|
||||
);
|
||||
renderSpbuList(filtered);
|
||||
});
|
||||
|
||||
// Modals management
|
||||
function openNewSpbuModal() {
|
||||
const modal = document.getElementById('newSpbuModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeNewSpbuModal() {
|
||||
const modal = document.getElementById('newSpbuModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function openEditSpbuModal() {
|
||||
const modal = document.getElementById('editSpbuModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeEditSpbuModal() {
|
||||
const modal = document.getElementById('editSpbuModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// Toast Helper
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast');
|
||||
const msg = document.getElementById('toastMessage');
|
||||
msg.textContent = message;
|
||||
|
||||
toast.classList.remove('hidden', 'bg-red-500', 'bg-emerald-500', 'bg-indigo-500');
|
||||
toast.classList.add('flex');
|
||||
|
||||
if (type === 'error') {
|
||||
toast.classList.add('bg-red-500');
|
||||
} else if (type === 'success') {
|
||||
toast.classList.add('bg-emerald-500');
|
||||
} else {
|
||||
toast.classList.add('bg-indigo-500');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('scale-95', 'opacity-0');
|
||||
toast.classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('scale-100', 'opacity-100');
|
||||
toast.classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hidden');
|
||||
}, 150);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Helper escape HTML
|
||||
function escapeHtml(str) {
|
||||
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// Simple bounce animation for markers
|
||||
L.Marker.prototype.bounce = function() {
|
||||
const marker = this;
|
||||
const element = marker._icon;
|
||||
if (!element) return;
|
||||
element.style.transition = 'transform 0.15s ease-out';
|
||||
element.style.transform = 'translate3d(0px, -14px, 0px) scale(1.15)';
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'translate3d(0px, 0px, 0px) scale(1)';
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// Startup Load
|
||||
loadSPBU();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+207
-4752
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user