feat: implement core WebGIS mapping features, sidebar UI, and API endpoints for data management

This commit is contained in:
Syariffullah
2026-05-08 20:44:23 +07:00
parent 448083d2ea
commit 08d6d30e02
14 changed files with 1326 additions and 188 deletions
+27
View File
@@ -0,0 +1,27 @@
<?php
header('Content-Type: application/json');
require_once '../db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
$miskin_id = intval($data['miskin_id'] ?? 0);
$ibadah_id = intval($data['ibadah_id'] ?? 0);
$tipe_bantuan = trim($data['tipe_bantuan'] ?? '');
$tanggal = trim($data['tanggal'] ?? '');
$keterangan = trim($data['keterangan'] ?? '');
if (!$miskin_id || !$ibadah_id || !$tipe_bantuan || !$tanggal) {
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
exit;
}
$sql = "INSERT INTO log_bantuan (miskin_id, ibadah_id, tipe_bantuan, tanggal, keterangan) VALUES (?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param('iisss', $miskin_id, $ibadah_id, $tipe_bantuan, $tanggal, $keterangan);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'id' => $stmt->insert_id]);
} else {
echo json_encode(['status' => 'error', 'message' => $conn->error]);
}
$conn->close();
+37
View File
@@ -0,0 +1,37 @@
<?php
header('Content-Type: application/json');
require_once '../db_connect.php';
$miskin_id = isset($_GET['miskin_id']) ? intval($_GET['miskin_id']) : 0;
if (!$miskin_id) {
echo json_encode(['status' => 'error', 'message' => 'miskin_id diperlukan']);
exit;
}
$sql = "
SELECT
lb.id,
lb.tipe_bantuan,
lb.tanggal,
lb.keterangan,
ri.nama AS nama_ibadah,
ri.jenis AS jenis_ibadah
FROM log_bantuan lb
JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id
WHERE lb.miskin_id = ?
ORDER BY lb.tanggal DESC, lb.id DESC
";
$stmt = $conn->prepare($sql);
$stmt->bind_param('i', $miskin_id);
$stmt->execute();
$result = $stmt->get_result();
$data = [];
while ($row = $result->fetch_assoc()) {
$data[] = $row;
}
echo json_encode(['status' => 'success', 'data' => $data]);
$conn->close();
+9 -7
View File
@@ -4,15 +4,17 @@ include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
if(!empty($data->lat) && !empty($data->lng)) { if (!empty($data->lat) && !empty($data->lng)) {
$nama = $conn->real_escape_string($data->nama ?? ''); $nama = $conn->real_escape_string($data->nama ?? '');
$kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan');
$lat = (float)$data->lat; $jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1);
$lng = (float)$data->lng; $lat = (float)$data->lat;
$lng = (float)$data->lng;
$query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, lat, lng) VALUES ('$nama', '$kategori', $lat, $lng)"; $query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng)
VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng)";
if($conn->query($query)) { if ($conn->query($query)) {
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Berhasil ditambahkan."]); echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Berhasil ditambahkan."]);
} else { } else {
echo json_encode(["status" => "error", "message" => "Gagal: " . $conn->error]); echo json_encode(["status" => "error", "message" => "Gagal: " . $conn->error]);
+11 -8
View File
@@ -4,16 +4,19 @@ include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
if(!empty($data->id)) { if (!empty($data->id)) {
$id = (int)$data->id; $id = (int)$data->id;
$nama = $conn->real_escape_string($data->nama ?? ''); $nama = $conn->real_escape_string($data->nama ?? '');
$kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan');
$lat = (float)$data->lat; $jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1);
$lng = (float)$data->lng; $lat = (float)$data->lat;
$lng = (float)$data->lng;
$query = "UPDATE penduduk_miskin SET nama='$nama', kategori_bantuan='$kategori', lat=$lat, lng=$lng WHERE id=$id"; $query = "UPDATE penduduk_miskin
SET nama='$nama', kategori_bantuan='$kategori', jumlah_jiwa=$jumlah_jiwa, lat=$lat, lng=$lng
WHERE id=$id";
if($conn->query($query)) { if ($conn->query($query)) {
echo json_encode(["status" => "success", "message" => "Update berhasil."]); echo json_encode(["status" => "success", "message" => "Update berhasil."]);
} else { } else {
echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]); echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]);
+9 -7
View File
@@ -4,16 +4,18 @@ include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
if(!empty($data->lat) && !empty($data->lng)) { if (!empty($data->lat) && !empty($data->lng)) {
$nama = $conn->real_escape_string($data->nama ?? ''); $nama = $conn->real_escape_string($data->nama ?? '');
$jenis = $conn->real_escape_string($data->jenis ?? 'Masjid');
$alamat = $conn->real_escape_string($data->alamat ?? ''); $alamat = $conn->real_escape_string($data->alamat ?? '');
$radius = (float)($data->radius ?? 100); $radius = (float)($data->radius ?? 500);
$lat = (float)$data->lat; $lat = (float)$data->lat;
$lng = (float)$data->lng; $lng = (float)$data->lng;
$query = "INSERT INTO rumah_ibadah (nama, alamat, radius, lat, lng) VALUES ('$nama', '$alamat', $radius, $lat, $lng)"; $query = "INSERT INTO rumah_ibadah (nama, jenis, alamat, radius, lat, lng)
VALUES ('$nama', '$jenis', '$alamat', $radius, $lat, $lng)";
if($conn->query($query)) { if ($conn->query($query)) {
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Rumah Ibadah berhasil ditambahkan."]); echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Rumah Ibadah berhasil ditambahkan."]);
} else { } else {
echo json_encode(["status" => "error", "message" => "Gagal menambahkan: " . $conn->error]); echo json_encode(["status" => "error", "message" => "Gagal menambahkan: " . $conn->error]);
+11 -8
View File
@@ -4,17 +4,20 @@ include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
if(!empty($data->id)) { if (!empty($data->id)) {
$id = (int)$data->id; $id = (int)$data->id;
$nama = $conn->real_escape_string($data->nama ?? ''); $nama = $conn->real_escape_string($data->nama ?? '');
$jenis = $conn->real_escape_string($data->jenis ?? 'Masjid');
$alamat = $conn->real_escape_string($data->alamat ?? ''); $alamat = $conn->real_escape_string($data->alamat ?? '');
$radius = (float)($data->radius ?? 100); $radius = (float)($data->radius ?? 500);
$lat = (float)$data->lat; $lat = (float)$data->lat;
$lng = (float)$data->lng; $lng = (float)$data->lng;
$query = "UPDATE rumah_ibadah SET nama='$nama', alamat='$alamat', radius=$radius, lat=$lat, lng=$lng WHERE id=$id"; $query = "UPDATE rumah_ibadah
SET nama='$nama', jenis='$jenis', alamat='$alamat', radius=$radius, lat=$lat, lng=$lng
WHERE id=$id";
if($conn->query($query)) { if ($conn->query($query)) {
echo json_encode(["status" => "success", "message" => "Update berhasil."]); echo json_encode(["status" => "success", "message" => "Update berhasil."]);
} else { } else {
echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]); echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]);
+453 -62
View File
@@ -15,7 +15,7 @@ body, html {
#map { #map {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
z-index: 1; /* Di bawah UI element */ z-index: 1;
} }
/* Custom UI Container over Map */ /* Custom UI Container over Map */
@@ -75,7 +75,7 @@ body, html {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
visibility: hidden; /* Muncul jika ada input */ visibility: hidden;
} }
.search-clear:hover { .search-clear:hover {
@@ -115,9 +115,10 @@ body, html {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 15px; padding: 15px;
width: 250px; width: 250px;
display: none; /* Default tertutup */ display: none;
} }
.custom-layer-panel h3 { .custom-layer-panel h3 {
margin-bottom: 10px; margin-bottom: 10px;
font-size: 16px; font-size: 16px;
@@ -133,6 +134,436 @@ body, html {
font-size: 14px; font-size: 14px;
} }
/* ===== Sidebar Toggle Button ===== */
.sidebar-toggle-btn {
position: absolute;
left: 20px;
top: 90px;
z-index: 1001;
width: 36px;
height: 36px;
background: white;
border: none;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #6366f1;
font-size: 13px;
transition: all 0.2s;
}
.sidebar-toggle-btn:hover {
background: #6366f1;
color: white;
box-shadow: 0 4px 14px rgba(99,102,241,0.4);
}
.sidebar-toggle-btn.open {
background: #6366f1;
color: white;
}
/* ===== Left Sidebar ===== */
.left-sidebar {
position: absolute;
left: 20px;
top: 136px; /* di bawah toggle button */
z-index: 1000;
display: flex;
flex-direction: column;
gap: 8px;
}
.sidebar-btn {
width: 60px;
height: 60px;
background: white;
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
transition: all 0.2s ease;
padding: 6px 4px;
}
.sidebar-btn:hover {
transform: translateX(3px);
box-shadow: 0 6px 18px rgba(0,0,0,0.18);
background: #f8f9fa;
}
.sidebar-btn.active {
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: white;
box-shadow: 0 6px 18px rgba(99,102,241,0.4);
transform: translateX(4px);
}
.sidebar-emoji {
font-size: 22px;
line-height: 1;
}
.sidebar-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.3px;
color: #555;
text-transform: uppercase;
}
.sidebar-btn.active .sidebar-label {
color: rgba(255,255,255,0.85);
}
/* ===== Right Panel ===== */
.right-panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 320px;
background: #f8f9fb;
z-index: 999;
box-shadow: -4px 0 20px rgba(0,0,0,0.12);
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* shift layer control when panel open */
}
.right-panel.open {
transform: translateX(0);
}
.right-panel-header {
padding: 16px 16px 12px;
background: white;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding-top: 26px;
}
.right-panel-header h3 {
font-size: 15px;
font-weight: 700;
color: #1e1e2e;
}
.right-panel-close {
width: 30px;
height: 30px;
border: none;
background: #f3f4f6;
border-radius: 8px;
cursor: pointer;
color: #555;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
transition: background 0.2s;
}
.right-panel-close:hover {
background: #e5e7eb;
color: #333;
}
.right-panel-actions {
padding: 12px 14px;
background: white;
border-bottom: 1px solid #eee;
display: flex;
gap: 8px;
flex-shrink: 0;
}
.right-panel-actions button {
flex: 1;
padding: 9px 10px;
border: none;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.2s;
}
.btn-panel-add {
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: white;
box-shadow: 0 3px 10px rgba(99,102,241,0.3);
}
.btn-panel-add:hover {
box-shadow: 0 5px 14px rgba(99,102,241,0.45);
transform: translateY(-1px);
}
.btn-panel-import {
background: #f0fdf4;
color: #16a34a;
border: 1px solid #bbf7d0 !important;
}
.btn-panel-import:hover {
background: #dcfce7;
}
.right-panel-body {
flex: 1;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
/* Panel Section Heading */
.panel-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #9ca3af;
padding: 4px 2px 6px;
border-bottom: 1px solid #e5e7eb;
margin-top: 4px;
}
/* Panel Empty State */
.panel-empty {
text-align: center;
color: #aaa;
font-size: 13px;
padding: 40px 10px;
}
/* ===== Data Card ===== */
.data-card {
background: white;
border-radius: 12px;
padding: 12px 12px 10px;
box-shadow: 0 1px 4px rgba(0,0,0,0.07);
transition: box-shadow 0.2s;
cursor: pointer;
}
.data-card:hover {
box-shadow: 0 3px 12px rgba(0,0,0,0.12);
}
.data-card-main {
display: flex;
align-items: center;
gap: 10px;
}
.data-card-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.card-icon-spbu { background: #f0fdf4; }
.card-icon-jalan { background: #eff6ff; }
.card-icon-parsil { background: #fefce8; }
.card-icon-ibadah { background: #fff7ed; }
.card-icon-miskin-out { background: #f0fdf4; }
.card-icon-miskin-in { background: #fef2f2; }
.data-card-info {
flex: 1;
min-width: 0;
}
.data-card-name {
font-size: 13px;
font-weight: 600;
color: #1e1e2e;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.data-card-sub {
font-size: 11px;
color: #6b7280;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.data-card-badge {
font-size: 10px;
font-weight: 600;
padding: 2px 7px;
border-radius: 99px;
margin-top: 4px;
display: inline-block;
}
.badge-green { background: #dcfce7; color: #16a34a; }
.badge-red { background: #fee2e2; color: #dc2626; }
.badge-blue { background: #dbeafe; color: #2563eb; }
.badge-orange { background: #ffedd5; color: #ea580c; }
.badge-yellow { background: #fef9c3; color: #ca8a04; }
.data-card-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
align-items: center;
}
.btn-card-action {
width: 30px;
height: 30px;
border: none;
border-radius: 7px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transition: all 0.15s;
}
.btn-card-edit {
background: #eff6ff;
color: #3b82f6;
}
.btn-card-edit:hover {
background: #3b82f6;
color: white;
}
.btn-card-delete {
background: #fef2f2;
color: #ef4444;
}
.btn-card-delete:hover {
background: #ef4444;
color: white;
}
/* Expandable card (miskin) */
.data-card-expand {
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease, padding 0.2s;
border-top: 0px solid #f3f4f6;
}
.data-card.expanded .data-card-expand {
max-height: 400px;
border-top: 1px solid #f3f4f6;
margin-top: 10px;
padding-top: 10px;
}
.expand-row {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #6b7280;
padding: 3px 0;
}
.expand-row span:first-child {
font-weight: 500;
color: #374151;
}
.btn-log-bantuan {
margin-top: 10px;
width: 100%;
padding: 8px;
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: white;
border: none;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.2s;
}
.btn-log-bantuan:hover {
box-shadow: 0 4px 12px rgba(99,102,241,0.4);
}
/* Log Bantuan Item */
.log-item {
background: #f8f9fb;
border-radius: 8px;
padding: 10px 12px;
display: flex;
gap: 10px;
align-items: flex-start;
}
.log-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6366f1;
flex-shrink: 0;
margin-top: 5px;
}
.log-info {
flex: 1;
}
.log-type {
font-size: 12px;
font-weight: 600;
color: #1e1e2e;
}
.log-meta {
font-size: 11px;
color: #9ca3af;
margin-top: 2px;
}
.log-keterangan {
font-size: 11px;
color: #6b7280;
margin-top: 3px;
font-style: italic;
}
/* Popup Form */ /* Popup Form */
.popup-form { .popup-form {
display: flex; display: flex;
@@ -178,51 +609,6 @@ body, html {
opacity: 0.9; opacity: 0.9;
} }
/* Action Menu (Kiri) */
.action-menu {
position: absolute;
z-index: 1000;
background: transparent;
box-shadow: none;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
width: 200px;
}
.action-menu button {
padding: 10px 15px;
background-color: white;
border: none;
border-radius: 25px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
cursor: pointer;
text-align: left;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.action-menu button i {
width: 16px;
text-align: center;
}
.action-menu button:hover {
background-color: #f8f9fa;
transform: translateX(5px);
}
.action-menu button.active {
background-color: #007bff;
color: white;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
/* Unified Modal */ /* Unified Modal */
.unified-modal { .unified-modal {
display: none; display: none;
@@ -243,10 +629,10 @@ body, html {
.modal-content { .modal-content {
background-color: white; background-color: white;
border-radius: 8px; border-radius: 12px;
width: 400px; width: 400px;
max-width: 90%; max-width: 90%;
box-shadow: 0 4px 20px rgba(0,0,0,0.2); box-shadow: 0 8px 30px rgba(0,0,0,0.2);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -290,13 +676,15 @@ body, html {
.modal-body input[type="text"], .modal-body input[type="text"],
.modal-body input[type="number"], .modal-body input[type="number"],
.modal-body input[type="date"],
.modal-body select, .modal-body select,
.modal-body textarea { .modal-body textarea {
width: 100%; width: 100%;
padding: 8px; padding: 8px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-family: inherit;
} }
.modal-footer { .modal-footer {
@@ -308,20 +696,23 @@ body, html {
} }
.btn-save { .btn-save {
background-color: #007bff; background: linear-gradient(135deg, #6366f1, #4f46e5);
color: white; color: white;
border: none; border: none;
padding: 8px 16px; padding: 8px 18px;
border-radius: 4px; border-radius: 7px;
cursor: pointer; cursor: pointer;
font-weight: 600;
font-size: 13px;
} }
.btn-cancel { .btn-cancel {
background-color: #f8f9fa; background-color: #f8f9fa;
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 8px 16px; padding: 8px 18px;
border-radius: 4px; border-radius: 7px;
cursor: pointer; cursor: pointer;
font-size: 13px;
} }
.form-group { .form-group {
@@ -340,7 +731,7 @@ body, html {
border-radius: 4px; border-radius: 4px;
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
pointer-events: none; /* Supaya tidak menghalangi klik ke map */ pointer-events: none;
z-index: 9999; z-index: 9999;
white-space: nowrap; white-space: nowrap;
box-shadow: 0 2px 6px rgba(0,0,0,0.2); box-shadow: 0 2px 6px rgba(0,0,0,0.2);
@@ -432,8 +823,8 @@ body, html {
} }
/* Warna bubble per tipe */ /* Warna bubble per tipe */
.emoji-marker .bubble.spbu-24 { background: linear-gradient(135deg, #22c55e, #16a34a); } .emoji-marker .bubble.spbu-24 { background: linear-gradient(135deg, #22c55e, #16a34a); }
.emoji-marker .bubble.spbu-not24 { background: linear-gradient(135deg, #ef4444, #dc2626); } .emoji-marker .bubble.spbu-not24 { background: linear-gradient(135deg, #ef4444, #dc2626); }
.emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); } .emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); }
.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); } .emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); }
.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); } .emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); }
+1
View File
@@ -16,6 +16,7 @@ function loadJalan() {
addJalanToMap(item); addJalanToMap(item);
}); });
} }
if (window.refreshActivePanel) window.refreshActivePanel();
}); });
} }
+106 -50
View File
@@ -1,10 +1,20 @@
// --- Fitur Pemetaan Kemiskinan --- // --- Fitur Pemetaan Kemiskinan ---
// Emoji per jenis ibadah
const IBADAH_EMOJI_MAP = {
'Masjid': '🕌',
'Gereja': '⛪',
'Pura': '🛕',
'Vihara': '🪷',
'Kelenteng': '🏮',
};
// Emoji Bubble Icon builder // Emoji Bubble Icon builder
function makeIbadahIcon() { function makeIbadahIcon(jenis) {
const emot = IBADAH_EMOJI_MAP[jenis] || '🕌';
return L.divIcon({ return L.divIcon({
className: '', className: '',
html: `<div class="emoji-marker"><div class="bubble ibadah"><span>🕌</span></div></div>`, html: `<div class="emoji-marker"><div class="bubble ibadah"><span>${emot}</span></div></div>`,
iconSize: [38, 38], iconSize: [38, 38],
iconAnchor: [19, 38], iconAnchor: [19, 38],
popupAnchor: [0, -40] popupAnchor: [0, -40]
@@ -22,10 +32,6 @@ function makeMiskinIcon(inRadius) {
}); });
} }
const ibadahIcon = makeIbadahIcon();
const miskinMerahIcon = makeMiskinIcon(true);
const miskinHijauIcon = makeMiskinIcon(false);
let ibadahDataList = []; let ibadahDataList = [];
let miskinMarkerList = []; // Simpan referensi marker untuk update warna let miskinMarkerList = []; // Simpan referensi marker untuk update warna
@@ -76,13 +82,14 @@ function loadRumahIbadah() {
data.data.forEach(item => { data.data.forEach(item => {
addIbadahMarker(item); addIbadahMarker(item);
}); });
updateSemuaWarnaMiskin(); // Update warna setelah memuat rumah ibadah updateSemuaWarnaMiskin();
} }
if (window.refreshActivePanel) window.refreshActivePanel();
}); });
} }
function addIbadahMarker(item) { function addIbadahMarker(item) {
const marker = L.marker([item.lat, item.lng], { icon: ibadahIcon, draggable: true }); const marker = L.marker([item.lat, item.lng], { icon: makeIbadahIcon(item.jenis), draggable: true });
// Lingkaran radius // Lingkaran radius
const circle = L.circle([item.lat, item.lng], { const circle = L.circle([item.lat, item.lng], {
@@ -125,10 +132,12 @@ function addIbadahMarker(item) {
marker.circleLayer = circle; marker.circleLayer = circle;
const d = item; const d = item;
const emot = IBADAH_EMOJI_MAP[d.jenis] || '🕌';
const popupContent = ` const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 150px;"> <div style="font-family: Arial, sans-serif; min-width: 160px;">
<h4 style="margin:0 0 5px 0;">Rumah Ibadah ${d.nama}</h4> <h4 style="margin:0 0 5px 0;">${emot} ${d.nama}</h4>
<p style="margin: 0 0 5px 0;"><b>Alamat:</b> ${d.alamat}</p> <p style="margin: 0 0 3px 0;"><b>Jenis:</b> ${d.jenis || 'Masjid'}</p>
<p style="margin: 0 0 3px 0;"><b>Alamat:</b> ${d.alamat}</p>
<p style="margin: 0 0 10px 0;"><b>Radius:</b> ${d.radius} m</p> <p style="margin: 0 0 10px 0;"><b>Radius:</b> ${d.radius} m</p>
<div style="display:flex; gap:5px;"> <div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditIbadahModal(${d.id})">Edit</button> <button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditIbadahModal(${d.id})">Edit</button>
@@ -185,6 +194,16 @@ map.on('click', function(e) {
<label>Nama Rumah Ibadah</label> <label>Nama Rumah Ibadah</label>
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah"> <input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
</div> </div>
<div class="form-group">
<label>Jenis</label>
<select id="modalIbadahJenis">
<option value="Masjid">🕌 Masjid</option>
<option value="Gereja">⛪ Gereja</option>
<option value="Pura">🛕 Pura</option>
<option value="Vihara">🪷 Vihara</option>
<option value="Kelenteng">🏮 Kelenteng</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label>Alamat (Auto)</label> <label>Alamat (Auto)</label>
<textarea id="modalIbadahAlamat" rows="3">${alamat}</textarea> <textarea id="modalIbadahAlamat" rows="3">${alamat}</textarea>
@@ -196,7 +215,7 @@ map.on('click', function(e) {
`; `;
openModal("Tambah Rumah Ibadah", bodyHTML, function() { openModal("Tambah Rumah Ibadah", bodyHTML, function() {
window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahAlamat', 'modalIbadahRadius'); window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius');
}); });
window.deactivateAddMode(); window.deactivateAddMode();
@@ -212,6 +231,16 @@ map.on('click', function(e) {
<label>Nama Rumah Ibadah</label> <label>Nama Rumah Ibadah</label>
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah"> <input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
</div> </div>
<div class="form-group">
<label>Jenis</label>
<select id="modalIbadahJenis">
<option value="Masjid">🕌 Masjid</option>
<option value="Gereja">⛪ Gereja</option>
<option value="Pura">🛕 Pura</option>
<option value="Vihara">🪷 Vihara</option>
<option value="Kelenteng">🏮 Kelenteng</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label>Alamat (Manual)</label> <label>Alamat (Manual)</label>
<textarea id="modalIbadahAlamat" rows="3" placeholder="Masukkan alamat manual">${alamat}</textarea> <textarea id="modalIbadahAlamat" rows="3" placeholder="Masukkan alamat manual">${alamat}</textarea>
@@ -223,7 +252,7 @@ map.on('click', function(e) {
`; `;
openModal("Tambah Rumah Ibadah", bodyHTML, function() { openModal("Tambah Rumah Ibadah", bodyHTML, function() {
window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahAlamat', 'modalIbadahRadius'); window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius');
}); });
window.deactivateAddMode(); window.deactivateAddMode();
@@ -231,27 +260,23 @@ map.on('click', function(e) {
} }
}); });
window.saveNewIbadah = function(lat, lng, namaId, alamatId, radiusId) { window.saveNewIbadah = function(lat, lng, namaId, jenisId, alamatId, radiusId) {
const nama = document.getElementById(namaId).value; const nama = document.getElementById(namaId).value;
const jenis = document.getElementById(jenisId).value;
const alamat = document.getElementById(alamatId).value; const alamat = document.getElementById(alamatId).value;
const radius = document.getElementById(radiusId).value; const radius = document.getElementById(radiusId).value;
if (!nama) { if (!nama) { alert("Nama Rumah Ibadah harus diisi!"); return; }
alert("Nama Rumah Ibadah harus diisi!");
return;
}
fetch('api/rumah_ibadah/create.php', { fetch('api/rumah_ibadah/create.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, alamat, radius, lat, lng }) body: JSON.stringify({ nama, jenis, alamat, radius, lat, lng })
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if(data.status === 'success') { if(data.status === 'success') { closeModal(); loadRumahIbadah(); }
closeModal(); else { alert(data.message); }
loadRumahIbadah();
} else { alert(data.message); }
}); });
}; };
@@ -262,11 +287,19 @@ window.openEditIbadahModal = function(id) {
}); });
if (!d) return; if (!d) return;
const jenisOpts = ['Masjid','Gereja','Pura','Vihara','Kelenteng'].map(j =>
`<option value="${j}" ${d.jenis === j ? 'selected' : ''}>${IBADAH_EMOJI_MAP[j] || ''} ${j}</option>`
).join('');
const bodyHTML = ` const bodyHTML = `
<div class="form-group"> <div class="form-group">
<label>Nama Rumah Ibadah</label> <label>Nama Rumah Ibadah</label>
<input type="text" id="editIbadahNama" value="${d.nama}"> <input type="text" id="editIbadahNama" value="${d.nama}">
</div> </div>
<div class="form-group">
<label>Jenis</label>
<select id="editIbadahJenis">${jenisOpts}</select>
</div>
<div class="form-group"> <div class="form-group">
<label>Alamat</label> <label>Alamat</label>
<textarea id="editIbadahAlamat" rows="2">${d.alamat}</textarea> <textarea id="editIbadahAlamat" rows="2">${d.alamat}</textarea>
@@ -278,7 +311,7 @@ window.openEditIbadahModal = function(id) {
`; `;
map.closePopup(); map.closePopup();
openModal("Edit Rumah Ibadah", bodyHTML, function() { openModal("Edit Rumah Ibadah", bodyHTML, function() {
window.saveEditIbadah(d.id, d.lat, d.lng, 'editIbadahNama', 'editIbadahAlamat', 'editIbadahRadius'); window.saveEditIbadah(d.id, d.lat, d.lng, 'editIbadahNama', 'editIbadahJenis', 'editIbadahAlamat', 'editIbadahRadius');
}); });
}; };
@@ -293,18 +326,19 @@ window.previewRadius = function(id, newRadius) {
}); });
}; };
window.saveEditIbadah = function(id, lat, lng, namaId, alamatId, radiusId) { window.saveEditIbadah = function(id, lat, lng, namaId, jenisId, alamatId, radiusId) {
const nama = document.getElementById(namaId).value; const nama = document.getElementById(namaId).value;
const jenis = document.getElementById(jenisId).value;
const alamat = document.getElementById(alamatId).value; const alamat = document.getElementById(alamatId).value;
const radius = document.getElementById(radiusId).value; const radius = document.getElementById(radiusId).value;
updateIbadah(id, nama, alamat, radius, lat, lng); updateIbadah(id, nama, jenis, alamat, radius, lat, lng);
}; };
function updateIbadah(id, nama, alamat, radius, lat, lng) { function updateIbadah(id, nama, jenis, alamat, radius, lat, lng) {
fetch('api/rumah_ibadah/update.php', { fetch('api/rumah_ibadah/update.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, alamat, radius, lat, lng }) body: JSON.stringify({ id, nama, jenis, alamat, radius, lat, lng })
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
if(data.status === 'success') { map.closePopup(); loadRumahIbadah(); } if(data.status === 'success') { map.closePopup(); loadRumahIbadah(); }
}); });
@@ -336,19 +370,22 @@ function loadPendudukMiskin() {
}); });
updateSemuaWarnaMiskin(); updateSemuaWarnaMiskin();
} }
if (window.refreshActivePanel) window.refreshActivePanel();
}); });
} }
function addMiskinMarker(item) { function addMiskinMarker(item) {
const marker = L.marker([item.lat, item.lng], { icon: miskinHijauIcon, draggable: true }); const marker = L.marker([item.lat, item.lng], { icon: makeMiskinIcon(false), draggable: true });
marker.miskinData = item; marker.miskinData = item;
const d = item; const d = item;
const popupContent = ` const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 150px;"> <div style="font-family: Arial, sans-serif; min-width: 165px;">
<h4 style="margin:0 0 5px 0;">Penduduk Miskin ${d.nama}</h4> <h4 style="margin:0 0 5px 0;">🏠 ${d.nama}</h4>
<p style="margin: 0 0 10px 0;"><b>Bantuan:</b> ${d.kategori_bantuan}</p> <p style="margin: 0 0 3px 0;"><b>Bantuan:</b> ${d.kategori_bantuan}</p>
<div style="display:flex; gap:5px;"> <p style="margin: 0 0 10px 0;"><b>Jiwa:</b> ${d.jumlah_jiwa || '-'}</p>
<div style="display:flex; gap:5px; flex-wrap:wrap;">
<button style="padding:4px 8px; background:#6366f1; color:white; border:none; border-radius:3px; cursor:pointer; font-size:11px;" onclick="openLogBantuan(${d.id}, '${d.nama.replace(/'/g, "\\'")}');">📋 Log Bantuan</button>
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditMiskinModal(${d.id})">Edit</button> <button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditMiskinModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteMiskin(${d.id})">Hapus</button> <button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteMiskin(${d.id})">Hapus</button>
</div> </div>
@@ -374,34 +411,48 @@ function addMiskinMarker(item) {
} }
window.formAddMiskin = function(lat, lng) { window.formAddMiskin = function(lat, lng) {
const popupContent = ` map.closePopup();
<div class="popup-form"> const bodyHTML = `
<h4>Penduduk Miskin Baru</h4> <div class="form-group">
<input type="text" id="miskinNama" placeholder="Nama Penduduk"> <label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama Penduduk</label>
<input type="text" id="miskinNama" placeholder="Nama Lengkap">
</div>
<div class="form-group">
<label>Kategori Bantuan</label>
<select id="miskinKategori"> <select id="miskinKategori">
<option value="Makan">Bantuan Makan</option> <option value="Makan">Bantuan Makan</option>
<option value="Pemberdayaan">Pemberdayaan</option> <option value="Pemberdayaan">Pemberdayaan</option>
</select> </select>
<button onclick="saveNewMiskin(${lat}, ${lng})">Simpan</button> </div>
<div class="form-group">
<label>Jumlah Jiwa</label>
<input type="number" id="miskinJiwa" value="1" min="1">
</div> </div>
`; `;
map.closePopup(); openModal("Tambah Penduduk Miskin", bodyHTML, function() {
L.popup().setLatLng([lat, lng]).setContent(popupContent).openOn(map); window.saveNewMiskin(lat, lng);
});
}; };
window.saveNewMiskin = function(lat, lng) { window.saveNewMiskin = function(lat, lng) {
const nama = document.getElementById('miskinNama').value; const nama = document.getElementById('miskinNama').value;
const kategori_bantuan = document.getElementById('miskinKategori').value; const kategori_bantuan = document.getElementById('miskinKategori').value;
const jumlah_jiwa = document.getElementById('miskinJiwa').value;
if (!nama) { alert('Nama harus diisi!'); return; }
fetch('api/penduduk_miskin/create.php', { fetch('api/penduduk_miskin/create.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, kategori_bantuan, lat, lng }) body: JSON.stringify({ nama, kategori_bantuan, jumlah_jiwa, lat, lng })
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if(data.status === 'success') { if(data.status === 'success') {
map.closePopup(); closeModal();
loadPendudukMiskin(); loadPendudukMiskin();
} else { alert(data.message); } } else { alert(data.message); }
}); });
@@ -420,26 +471,31 @@ window.openEditMiskinModal = function(id) {
<input type="text" id="editMiskinNama" value="${d.nama}"> <input type="text" id="editMiskinNama" value="${d.nama}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Bantuan</label> <label>Kategori Bantuan</label>
<select id="editMiskinKategori"> <select id="editMiskinKategori">
<option value="Makan" ${d.kategori_bantuan === 'Makan' ? 'selected' : ''}>Bantuan Makan</option> <option value="Makan" ${d.kategori_bantuan === 'Makan' ? 'selected' : ''}>Bantuan Makan</option>
<option value="Pemberdayaan" ${d.kategori_bantuan === 'Pemberdayaan' ? 'selected' : ''}>Pemberdayaan</option> <option value="Pemberdayaan" ${d.kategori_bantuan === 'Pemberdayaan' ? 'selected' : ''}>Pemberdayaan</option>
</select> </select>
</div> </div>
<div class="form-group">
<label>Jumlah Jiwa</label>
<input type="number" id="editMiskinJiwa" value="${d.jumlah_jiwa || 1}" min="1">
</div>
`; `;
map.closePopup(); map.closePopup();
openModal("Edit Penduduk Miskin", bodyHTML, function() { openModal("Edit Penduduk Miskin", bodyHTML, function() {
window.saveEditMiskin(d.id, d.lat, d.lng, 'editMiskinNama', 'editMiskinKategori'); window.saveEditMiskin(d.id, d.lat, d.lng, 'editMiskinNama', 'editMiskinKategori', 'editMiskinJiwa');
}); });
}; };
window.saveEditMiskin = function(id, lat, lng, namaId, kategoriId) { window.saveEditMiskin = function(id, lat, lng, namaId, kategoriId, jiwaId) {
const nama = document.getElementById(namaId).value; const nama = document.getElementById(namaId).value;
const kategori_bantuan = document.getElementById(kategoriId).value; const kategori_bantuan = document.getElementById(kategoriId).value;
const jumlah_jiwa = jiwaId ? document.getElementById(jiwaId).value : 1;
fetch('api/penduduk_miskin/update.php', { fetch('api/penduduk_miskin/update.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, kategori_bantuan, lat, lng }) body: JSON.stringify({ id, nama, kategori_bantuan, jumlah_jiwa, lat, lng })
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
if(data.status === 'success') { closeModal(); loadPendudukMiskin(); } if(data.status === 'success') { closeModal(); loadPendudukMiskin(); }
}); });
+1
View File
@@ -17,6 +17,7 @@ function loadParsil() {
addParsilToMap(item); addParsilToMap(item);
}); });
} }
if (window.refreshActivePanel) window.refreshActivePanel();
}); });
} }
+1
View File
@@ -29,6 +29,7 @@ function loadSpbu() {
addSpbuMarker(item); addSpbuMarker(item);
}); });
} }
if (window.refreshActivePanel) window.refreshActivePanel();
}); });
} }
+13 -33
View File
@@ -107,22 +107,19 @@ window.showSearchResults = function(query) {
}; };
// UI Logic: Custom Layer Control Toggle // UI Logic: Custom Layer Control Toggle
const layerBtn = document.getElementById('layerBtn'); const layerBtn = document.getElementById('layerBtn');
const layerPanel = document.getElementById('layerPanel'); const layerPanel = document.getElementById('layerPanel');
layerBtn.addEventListener('click', function() { layerBtn.addEventListener('click', function() {
if (layerPanel.style.display === 'block') { layerPanel.style.display = layerPanel.style.display === 'block' ? 'none' : 'block';
layerPanel.style.display = 'none';
} else {
layerPanel.style.display = 'block';
}
}); });
// Sembunyikan layer panel saat klik di luar area (pada map) // Sembunyikan layer panel saat klik di peta
map.on('click', function() { map.on('click', function() {
layerPanel.style.display = 'none'; layerPanel.style.display = 'none';
}); });
// Logic untuk Toggle Layer Visibility // Logic untuk Toggle Layer Visibility
document.getElementById('layerSpbu').addEventListener('change', function(e) { document.getElementById('layerSpbu').addEventListener('change', function(e) {
e.target.checked ? map.addLayer(spbuLayer) : map.removeLayer(spbuLayer); e.target.checked ? map.addLayer(spbuLayer) : map.removeLayer(spbuLayer);
@@ -248,18 +245,7 @@ window.closeConfirmModal = function() {
confirmModal.classList.remove('show'); confirmModal.classList.remove('show');
}; };
// --- Action Menu Logic --- // --- Action Menu Logic (replaced by left sidebar) ---
const actionMenuBtn = document.getElementById('actionMenuBtn');
const actionMenu = document.getElementById('actionMenu');
actionMenuBtn.addEventListener('click', function() {
if (actionMenu.style.display === 'flex') {
actionMenu.style.display = 'none';
window.deactivateAddMode();
} else {
actionMenu.style.display = 'flex';
}
});
window.currentAddMode = null; // 'spbu' atau 'rumah_ibadah' window.currentAddMode = null; // 'spbu' atau 'rumah_ibadah'
window.currentDrawMode = null; // 'polyline' atau 'polygon' window.currentDrawMode = null; // 'polyline' atau 'polygon'
@@ -279,19 +265,14 @@ window.activateAddMode = function(mode) {
window.currentAddMode = mode; window.currentAddMode = mode;
window.currentDrawMode = null; window.currentDrawMode = null;
// Update button UI // Tooltip
document.querySelectorAll('.action-menu button').forEach(btn => btn.classList.remove('active')); const tooltips = {
'spbu': 'Klik untuk Tambah SPBU',
if (mode === 'spbu') { 'rumah_ibadah': 'Klik untuk Tambah Rumah Ibadah'
document.getElementById('btnMenuSpbu').classList.add('active'); };
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk Tambah SPBU'; if (window.cursorTooltip && tooltips[mode]) window.cursorTooltip.textContent = tooltips[mode];
}
if (mode === 'rumah_ibadah') {
document.getElementById('btnMenuIbadah').classList.add('active');
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk Tambah Rumah Ibadah';
}
// Ubah kursor map menjadi crosshair // Ubah kursor map menjadi crosshair
document.getElementById('map').style.cursor = 'crosshair'; document.getElementById('map').style.cursor = 'crosshair';
}; };
@@ -303,7 +284,6 @@ window.deactivateAddMode = function() {
window.activeDrawHandler.disable(); window.activeDrawHandler.disable();
window.activeDrawHandler = null; window.activeDrawHandler = null;
} }
document.querySelectorAll('.action-menu button').forEach(btn => btn.classList.remove('active'));
document.getElementById('map').style.cursor = ''; document.getElementById('map').style.cursor = '';
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none'; if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
}; };
+552
View File
@@ -0,0 +1,552 @@
// ===== Panel Management =====
// Mengelola Left Sidebar Toggle + Right Panel + Layer Control
(function () {
// ---- Konstanta Jenis Ibadah ----
const IBADAH_EMOJI = {
'Masjid': '🕌',
'Gereja': '⛪',
'Pura': '🛕',
'Vihara': '🪷',
'Kelenteng': '🏮',
};
window.IBADAH_EMOJI = IBADAH_EMOJI;
// ---- State ----
let activePanel = null;
// ---- Elements ----
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
const leftSidebar = document.getElementById('leftSidebar');
const rightPanel = document.getElementById('rightPanel');
const rightPanelTitle = document.getElementById('rightPanelTitle');
const rightPanelActions = document.getElementById('rightPanelActions');
const rightPanelBody = document.getElementById('rightPanelBody');
const rightPanelClose = document.getElementById('rightPanelClose');
// ---- Sidebar Toggle ----
sidebarToggleBtn.addEventListener('click', function () {
const isOpen = leftSidebar.style.display !== 'none';
if (isOpen) {
leftSidebar.style.display = 'none';
sidebarToggleBtn.classList.remove('open');
sidebarToggleIcon.className = 'fas fa-chevron-right';
} else {
leftSidebar.style.display = 'flex';
sidebarToggleBtn.classList.add('open');
sidebarToggleIcon.className = 'fas fa-chevron-left';
}
});
// ---- Sidebar Buttons ----
document.querySelectorAll('.sidebar-btn').forEach(btn => {
btn.addEventListener('click', function () {
const panel = this.dataset.panel;
if (activePanel === panel) {
closePanel();
} else {
openPanel(panel);
}
});
});
rightPanelClose.addEventListener('click', closePanel);
// ---- Open / Close Panel ----
function openPanel(panel) {
activePanel = panel;
rightPanel.classList.add('open');
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
const activeBtn = document.querySelector(`.sidebar-btn[data-panel="${panel}"]`);
if (activeBtn) activeBtn.classList.add('active');
renderPanel(panel);
}
function closePanel() {
activePanel = null;
rightPanel.classList.remove('open');
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
}
window.refreshActivePanel = function () {
if (activePanel && activePanel !== 'layer') renderPanel(activePanel);
};
// ---- Render Panel by Type ----
function renderPanel(type) {
rightPanelBody.innerHTML = '<div class="panel-empty">Memuat data...</div>';
rightPanelActions.innerHTML = '';
switch (type) {
case 'spbu': renderSpbuPanel(); break;
case 'jalan': renderJalanPanel(); break;
case 'parsil': renderParsilPanel(); break;
case 'ibadah': renderIbadahPanel(); break;
case 'miskin': renderMiskinPanel(); break;
}
}
// ==========================
// ===== SPBU Panel =========
// ==========================
function renderSpbuPanel() {
rightPanelTitle.textContent = '⛽ SPBU';
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddSpbu">
<i class="fas fa-plus"></i> Tambah SPBU
</button>`;
document.getElementById('btnPanelAddSpbu').addEventListener('click', () => {
window.activateAddMode('spbu');
});
const items = [];
spbuLayer.eachLayer(l => { if (l.spbuData) items.push(l.spbuData); });
if (!items.length) {
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data SPBU</div>';
return;
}
rightPanelBody.innerHTML = '';
items.forEach(d => {
const bdg = d.is_24_jam
? '<span class="data-card-badge badge-green">24 Jam</span>'
: '<span class="data-card-badge badge-red">Tidak 24 Jam</span>';
const card = document.createElement('div');
card.className = 'data-card';
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon card-icon-spbu">⛽</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">📞 ${d.no_wa || '-'}</div>
${bdg}
</div>
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>`;
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditSpbuModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteSpbu(d.id); });
card.addEventListener('click', () => flyToSpbu(d.id));
rightPanelBody.appendChild(card);
});
}
function flyToSpbu(id) {
spbuLayer.eachLayer(l => {
if (l.spbuData && l.spbuData.id == id) { map.setView(l.getLatLng(), 17); l.openPopup(); }
});
}
// ==========================
// ===== Jalan Panel ========
// ==========================
function renderJalanPanel() {
rightPanelTitle.textContent = '🛣️ Jalan';
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddJalan">
<i class="fas fa-plus"></i> Tambah Jalan
</button>`;
document.getElementById('btnPanelAddJalan').addEventListener('click', () => {
window.activateDraw('polyline');
});
const items = [];
jalanLayer.eachLayer(l => { if (l.jalanData) items.push(l.jalanData); });
if (!items.length) {
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data jalan</div>';
return;
}
const statusBadge = {
'Nasional' : '<span class="data-card-badge badge-red">Nasional</span>',
'Provinsi' : '<span class="data-card-badge badge-blue">Provinsi</span>',
'Kabupaten': '<span class="data-card-badge badge-green">Kabupaten</span>',
};
rightPanelBody.innerHTML = '';
items.forEach(d => {
const km = (d.panjang / 1000).toFixed(2);
const card = document.createElement('div');
card.className = 'data-card';
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon card-icon-jalan">🛣️</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">📏 ${km} km</div>
${statusBadge[d.status] || ''}
</div>
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>`;
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditJalanModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteJalan(d.id); });
card.addEventListener('click', () => flyToJalan(d.id));
rightPanelBody.appendChild(card);
});
}
function flyToJalan(id) {
jalanLayer.eachLayer(l => {
if (l.jalanData && l.jalanData.id == id && l.getBounds) {
map.fitBounds(l.getBounds(), { padding: [40, 40] }); l.openPopup();
}
});
}
// ==========================
// ===== Parsil Panel =======
// ==========================
function renderParsilPanel() {
rightPanelTitle.textContent = '🗺️ Parsil Tanah';
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddParsil">
<i class="fas fa-plus"></i> Tambah Parsil
</button>`;
document.getElementById('btnPanelAddParsil').addEventListener('click', () => {
window.activateDraw('polygon');
});
const items = [];
parsilLayer.eachLayer(l => { if (l.parsilData) items.push(l.parsilData); });
if (!items.length) {
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data parsil</div>';
return;
}
const statusBadge = {
'SHM': '<span class="data-card-badge badge-green">SHM</span>',
'HGB': '<span class="data-card-badge badge-blue">HGB</span>',
'HGU': '<span class="data-card-badge badge-yellow">HGU</span>',
'HP' : '<span class="data-card-badge badge-orange">HP</span>',
};
rightPanelBody.innerHTML = '';
items.forEach(d => {
const luas = d.luas >= 10000
? (d.luas / 10000).toFixed(2) + ' ha'
: d.luas.toFixed(0) + ' m²';
const card = document.createElement('div');
card.className = 'data-card';
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon card-icon-parsil">🗺️</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama || 'Tanpa Nama')}</div>
<div class="data-card-sub">📐 ${luas}</div>
${statusBadge[d.status] || ''}
</div>
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>`;
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditParsilModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteParsil(d.id); });
card.addEventListener('click', () => flyToParsil(d.id));
rightPanelBody.appendChild(card);
});
}
function flyToParsil(id) {
parsilLayer.eachLayer(l => {
if (l.parsilData && l.parsilData.id == id && l.getBounds) {
map.fitBounds(l.getBounds(), { padding: [40, 40] }); l.openPopup();
}
});
}
// ==========================
// ===== Ibadah Panel =======
// ==========================
function renderIbadahPanel() {
rightPanelTitle.textContent = '🕌 Rumah Ibadah';
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddIbadah">
<i class="fas fa-plus"></i> Tambah Ibadah
</button>`;
document.getElementById('btnPanelAddIbadah').addEventListener('click', () => {
window.activateAddMode('rumah_ibadah');
});
const items = [];
rumahIbadahLayer.eachLayer(l => {
if (l.ibadahData && l instanceof L.Marker) items.push(l.ibadahData);
});
if (!items.length) {
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data rumah ibadah</div>';
return;
}
rightPanelBody.innerHTML = '';
items.forEach(d => {
const emoji = IBADAH_EMOJI[d.jenis] || '🕌';
const card = document.createElement('div');
card.className = 'data-card';
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon card-icon-ibadah">${emoji}</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">${escHtml(d.jenis || 'Masjid')} • 📡 ${d.radius} m</div>
</div>
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>`;
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditIbadahModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteIbadah(d.id); });
card.addEventListener('click', () => flyToIbadah(d.id));
rightPanelBody.appendChild(card);
});
}
function flyToIbadah(id) {
rumahIbadahLayer.eachLayer(l => {
if (l.ibadahData && l.ibadahData.id == id && l instanceof L.Marker) {
map.setView(l.getLatLng(), 17); l.openPopup();
}
});
}
// ==========================
// ===== Miskin Panel =======
// ==========================
function renderMiskinPanel() {
rightPanelTitle.textContent = '🏠 Penduduk Miskin';
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelTambahMiskin">
<i class="fas fa-plus"></i> Tambah
</button>
<button class="btn-panel-import" id="btnPanelImportMiskin">
<i class="fas fa-file-import"></i> Impor
</button>`;
// Tombol Tambah: buka form via right-click popup di peta
document.getElementById('btnPanelTambahMiskin').addEventListener('click', () => {
// Aktifkan mode: klik kiri di peta = tambah miskin
window.currentAddMode = 'miskin_click';
document.getElementById('map').style.cursor = 'crosshair';
if (window.cursorTooltip) {
window.cursorTooltip.textContent = 'Klik di peta untuk tentukan lokasi penduduk miskin';
}
});
document.getElementById('btnPanelImportMiskin').addEventListener('click', () => {
alert('Fitur impor CSV akan segera tersedia.');
});
const all = [];
pendudukMiskinLayer.eachLayer(l => { if (l.miskinData) all.push(l.miskinData); });
if (!all.length) {
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data penduduk miskin</div>';
return;
}
const inRadius = (d) => {
if (typeof ibadahDataList === 'undefined') return false;
for (const ib of ibadahDataList) {
const dist = L.latLng(d.lat, d.lng).distanceTo(L.latLng(ib.lat, ib.lng));
if (dist <= ib.radius) return true;
}
return false;
};
rightPanelBody.innerHTML = '';
const terurusItems = all.filter(d => inRadius(d));
const tidakItems = all.filter(d => !inRadius(d));
if (terurusItems.length) {
const h1 = document.createElement('div');
h1.className = 'panel-section-title';
h1.innerHTML = `✅ Terurus <span style="font-weight:400;text-transform:none;font-size:10px;">(${terurusItems.length})</span>`;
rightPanelBody.appendChild(h1);
terurusItems.forEach(d => rightPanelBody.appendChild(buildMiskinCard(d, true)));
}
if (tidakItems.length) {
const h2 = document.createElement('div');
h2.className = 'panel-section-title';
h2.innerHTML = `⚠️ Tidak Terurus <span style="font-weight:400;text-transform:none;font-size:10px;">(${tidakItems.length})</span>`;
rightPanelBody.appendChild(h2);
tidakItems.forEach(d => rightPanelBody.appendChild(buildMiskinCard(d, false)));
}
}
function buildMiskinCard(d, terurus) {
const iconCls = terurus ? 'card-icon-miskin-in' : 'card-icon-miskin-out';
const badge = d.kategori_bantuan === 'Makan'
? '<span class="data-card-badge badge-orange">Bantuan Makan</span>'
: '<span class="data-card-badge badge-blue">Pemberdayaan</span>';
const card = document.createElement('div');
card.className = 'data-card';
card.dataset.miskinId = d.id;
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon ${iconCls}">🏠</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">👨‍👩‍👧 ${d.jumlah_jiwa || '-'} jiwa</div>
${badge}
</div>
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>
<div class="data-card-expand">
<div class="expand-row"><span>Kategori Bantuan</span><span>${escHtml(d.kategori_bantuan)}</span></div>
<div class="expand-row"><span>Jumlah Jiwa</span><span>${d.jumlah_jiwa || '-'}</span></div>
<div class="expand-row"><span>Status</span><span>${terurus ? 'Terurus ✅' : 'Tidak Terurus ⚠️'}</span></div>
<button class="btn-log-bantuan" data-id="${d.id}" data-nama="${escHtml(d.nama)}">
<i class="fas fa-history"></i> Lihat Log Bantuan
</button>
</div>`;
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditMiskinModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteMiskin(d.id); });
card.querySelector('.btn-log-bantuan').addEventListener('click', (e) => { e.stopPropagation(); openLogBantuan(d.id, d.nama); });
card.querySelector('.data-card-main').addEventListener('click', () => {
card.classList.toggle('expanded');
flyToMiskin(d.id);
});
return card;
}
function flyToMiskin(id) {
pendudukMiskinLayer.eachLayer(l => {
if (l.miskinData && l.miskinData.id == id) {
map.setView(l.getLatLng(), 17); l.openPopup();
}
});
}
// ---- Hook: klik peta saat mode miskin_click ----
// Ditangkap oleh kemiskinan.js via contextmenu — mode ini pakai klik kiri
map.on('click', function(e) {
if (window.currentAddMode === 'miskin_click') {
window.currentAddMode = null;
document.getElementById('map').style.cursor = '';
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
// Buka form tambah miskin
if (typeof window.formAddMiskin === 'function') {
window.formAddMiskin(e.latlng.lat, e.latlng.lng);
}
}
});
// ==========================
// ===== Log Bantuan Modal ==
// ==========================
const logModal = document.getElementById('logBantuanModal');
const logTitle = document.getElementById('logBantuanTitle');
const logList = document.getElementById('logBantuanList');
const logSaveBtn = document.getElementById('logSaveBtn');
const logIbadahSel = document.getElementById('logIbadahId');
let _logMiskinId = null;
window.openLogBantuan = function (miskinId, nama) {
_logMiskinId = miskinId;
logTitle.textContent = `Log Bantuan — ${nama}`;
logModal.classList.add('show');
// Isi dropdown ibadah
logIbadahSel.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>';
if (typeof ibadahDataList !== 'undefined') {
ibadahDataList.forEach(ib => {
const opt = document.createElement('option');
opt.value = ib.id;
opt.textContent = `${IBADAH_EMOJI[ib.jenis] || '🕌'} ${ib.nama}`;
logIbadahSel.appendChild(opt);
});
}
// Set tanggal default hari ini
document.getElementById('logTanggal').valueAsDate = new Date();
fetchLogBantuan(miskinId);
};
window.closeLogBantuanModal = function () {
logModal.classList.remove('show');
_logMiskinId = null;
};
function fetchLogBantuan(miskinId) {
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Memuat...</div>';
fetch(`api/log_bantuan/read.php?miskin_id=${miskinId}`)
.then(r => r.json())
.then(data => {
if (!data.data || !data.data.length) {
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Belum ada riwayat bantuan</div>';
return;
}
logList.innerHTML = '';
data.data.forEach(log => {
const tgl = new Date(log.tanggal).toLocaleDateString('id-ID', { day:'2-digit', month:'short', year:'numeric' });
const item = document.createElement('div');
item.className = 'log-item';
item.innerHTML = `
<div class="log-dot"></div>
<div class="log-info">
<div class="log-type">${escHtml(log.tipe_bantuan)}</div>
<div class="log-meta">${IBADAH_EMOJI[log.jenis_ibadah] || '🕌'} ${escHtml(log.nama_ibadah)}${tgl}</div>
${log.keterangan ? `<div class="log-keterangan">${escHtml(log.keterangan)}</div>` : ''}
</div>`;
logList.appendChild(item);
});
})
.catch(() => {
logList.innerHTML = '<div class="panel-empty" style="padding:15px; color:#ef4444;">Gagal memuat data</div>';
});
}
logSaveBtn.addEventListener('click', function () {
const ibadah_id = logIbadahSel.value;
const tipe_bantuan = document.getElementById('logTipeBantuan').value;
const tanggal = document.getElementById('logTanggal').value;
const keterangan = document.getElementById('logKeterangan').value;
if (!ibadah_id) { alert('Pilih rumah ibadah terlebih dahulu!'); return; }
if (!tanggal) { alert('Tanggal harus diisi!'); return; }
fetch('api/log_bantuan/create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ miskin_id: _logMiskinId, ibadah_id, tipe_bantuan, tanggal, keterangan })
})
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
document.getElementById('logKeterangan').value = '';
fetchLogBantuan(_logMiskinId);
} else {
alert(data.message || 'Gagal menyimpan');
}
});
});
// ---- Utility ----
function escHtml(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ---- Expose ----
window.openRightPanel = openPanel;
})();
+95 -13
View File
@@ -46,18 +46,49 @@
<button class="custom-layer-btn" id="zoomOutBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom Out"><i class="fas fa-minus"></i></button> <button class="custom-layer-btn" id="zoomOutBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom Out"><i class="fas fa-minus"></i></button>
</div> </div>
<!-- Action Menu Toggle Button --> <!-- Sidebar Toggle Button -->
<button class="custom-layer-btn" id="actionMenuBtn" style="top: 185px; left: 20px; right: auto;" title="Menu Tambah Data"> <button class="sidebar-toggle-btn" id="sidebarToggleBtn" title="Menu">
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button> </button>
<!-- Action Menu (Kiri) --> <!-- Left Sidebar Menu -->
<div class="action-menu" id="actionMenu" style="display: none; top: 240px; left: 20px;"> <nav class="left-sidebar" id="leftSidebar" style="display:none;">
<button id="btnMenuSpbu" onclick="window.activateAddMode('spbu')"><i class="fas fa-gas-pump"></i> Tambah SPBU</button> <button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
<button id="btnMenuIbadah" onclick="window.activateAddMode('rumah_ibadah')"><i class="fas fa-mosque"></i> Tambah Rumah Ibadah</button> <span class="sidebar-emoji"></span>
<button id="btnMenuJalan" onclick="window.activateDraw('polyline')"><i class="fas fa-road"></i> Tambah Jalan</button> <span class="sidebar-label">SPBU</span>
<button id="btnMenuParsil" onclick="window.activateDraw('polygon')"><i class="fas fa-draw-polygon"></i> Tambah Parsil Tanah</button> </button>
</div> <button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
<span class="sidebar-emoji">🛣️</span>
<span class="sidebar-label">Jalan</span>
</button>
<button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
<span class="sidebar-emoji">🗺️</span>
<span class="sidebar-label">Parsil</span>
</button>
<button class="sidebar-btn" id="menuIbadah" data-panel="ibadah" title="Rumah Ibadah">
<span class="sidebar-emoji">🕌</span>
<span class="sidebar-label">Ibadah</span>
</button>
<button class="sidebar-btn" id="menuMiskin" data-panel="miskin" title="Penduduk Miskin">
<span class="sidebar-emoji">🏠</span>
<span class="sidebar-label">Miskin</span>
</button>
</nav>
<!-- Right Data Panel (includes layer control) -->
<div class="right-panel" id="rightPanel">
<div class="right-panel-header">
<h3 id="rightPanelTitle">Data</h3>
<button class="right-panel-close" id="rightPanelClose"><i class="fas fa-times"></i></button>
</div>
<div class="right-panel-actions" id="rightPanelActions"></div>
<div class="right-panel-body" id="rightPanelBody">
<div class="panel-empty">Pilih menu di kiri untuk menampilkan data</div>
</div>
</div><!-- /right-panel -->
<!-- Custom Layer Control Button --> <!-- Custom Layer Control Button -->
<button class="custom-layer-btn" id="layerBtn" title="Atur Layer"> <button class="custom-layer-btn" id="layerBtn" title="Atur Layer">
@@ -126,7 +157,7 @@
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU <input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
</label> </label>
<label class="layer-option sub-option"> <label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP <input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
</label> </label>
</div> </div>
</div> </div>
@@ -168,7 +199,8 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div><!-- /custom-layer-panel -->
<!-- Map Container --> <!-- Map Container -->
<div id="map"></div> <div id="map"></div>
@@ -206,6 +238,56 @@
</div> </div>
</div> </div>
<!-- Log Bantuan Modal -->
<div id="logBantuanModal" class="unified-modal">
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
<div class="modal-header">
<h3 id="logBantuanTitle">Log Bantuan</h3>
<span class="modal-close" onclick="closeLogBantuanModal()">&times;</span>
</div>
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
<!-- Riwayat -->
<div style="padding:15px; border-bottom:1px solid #eee;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
</div>
</div>
<!-- Form Tambah -->
<div style="padding:15px;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
<div class="form-group">
<label>Rumah Ibadah</label>
<select id="logIbadahId"></select>
</div>
<div class="form-group">
<label>Tipe Bantuan</label>
<select id="logTipeBantuan">
<option value="Sembako">Sembako</option>
<option value="Uang Tunai">Uang Tunai</option>
<option value="Pakaian">Pakaian</option>
<option value="Pendidikan">Pendidikan</option>
<option value="Kesehatan">Kesehatan</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="form-group">
<label>Tanggal</label>
<input type="date" id="logTanggal">
</div>
<div class="form-group">
<label>Keterangan (opsional)</label>
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
</div>
</div>
</div>
<!-- Leaflet JS --> <!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
@@ -225,7 +307,7 @@
<script src="assets/js/features/geolocation.js"></script> <script src="assets/js/features/geolocation.js"></script>
<script src="assets/js/features/kemiskinan.js"></script> <script src="assets/js/features/kemiskinan.js"></script>
<script src="assets/js/features/geojson.js"></script> <script src="assets/js/features/geojson.js"></script>
<!-- (Akan ditambahkan sesuai tahapan) --> <script src="assets/js/panel.js"></script>
</body> </body>
</html> </html>