feat: implement full CRUD functionality for jalan, parsil, and spbu modules including database integration and map interaction features

This commit is contained in:
Syariffullah
2026-06-11 16:40:58 +07:00
parent d10253a001
commit 0ae903c0d1
38 changed files with 3098 additions and 2578 deletions
+155
View File
@@ -0,0 +1,155 @@
// --- Fitur Autentikasi dan Manajemen User (RBAC) ---
(function() {
window.currentUser = null;
// Elements
const authWidget = document.getElementById('authWidget');
const loginModal = document.getElementById('loginModal');
const loginUsernameInput = document.getElementById('loginUsername');
const loginPasswordInput = document.getElementById('loginPassword');
const loginSubmitBtn = document.getElementById('loginSubmitBtn');
const loginErrorMsg = document.getElementById('loginErrorMsg');
const closeLoginModal = document.getElementById('closeLoginModal');
// Startup Session Check
function checkSession() {
fetch('../poverty/api/check_session.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.isLoggedIn) {
window.currentUser = data.data;
} else {
window.currentUser = null;
}
updateAuthUI();
refreshAllLayers();
})
.catch(err => {
console.error("Session check failed:", err);
window.currentUser = null;
updateAuthUI();
});
}
// Update UI based on logged-in state
function updateAuthUI() {
if (!authWidget) return;
if (window.currentUser) {
// Logged In state
const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola';
authWidget.innerHTML = `
<div class="user-profile-pill">
<div class="user-profile-info">
<div class="user-profile-name">👤 ${escHtml(window.currentUser.username)}</div>
<div class="user-profile-role">${roleLabel}</div>
</div>
<button class="btn-profile-logout" id="authLogoutBtn" title="Logout">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
`;
document.getElementById('authLogoutBtn').addEventListener('click', logout);
} else {
// Logged Out state
authWidget.innerHTML = `
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
`;
document.getElementById('authLoginBtn').addEventListener('click', showLoginModal);
}
}
function refreshAllLayers() {
if (typeof loadJalan === 'function') loadJalan();
if (window.refreshActivePanel) window.refreshActivePanel();
}
// Login Modal logic
function showLoginModal() {
loginUsernameInput.value = '';
loginPasswordInput.value = '';
loginErrorMsg.style.display = 'none';
loginModal.classList.add('show');
}
function hideLoginModal() {
loginModal.classList.remove('show');
}
if (closeLoginModal) {
closeLoginModal.addEventListener('click', hideLoginModal);
}
if (loginSubmitBtn) {
loginSubmitBtn.addEventListener('click', function() {
const username = loginUsernameInput.value.trim();
const password = loginPasswordInput.value;
if (!username || !password) {
loginErrorMsg.textContent = 'Username dan password wajib diisi';
loginErrorMsg.style.display = 'block';
return;
}
fetch('../poverty/api/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
window.currentUser = data.data;
hideLoginModal();
updateAuthUI();
refreshAllLayers();
showToast('Login berhasil', 'success');
} else {
loginErrorMsg.textContent = data.message || 'Login gagal';
loginErrorMsg.style.display = 'block';
}
})
.catch(err => {
console.error(err);
loginErrorMsg.textContent = 'Koneksi ke server terputus';
loginErrorMsg.style.display = 'block';
});
});
}
function logout() {
fetch('../poverty/api/logout.php')
.then(res => res.json())
.then(data => {
window.currentUser = null;
updateAuthUI();
refreshAllLayers();
showToast('Logout berhasil', 'success');
})
.catch(err => {
console.error(err);
showToast('Gagal logout', 'error');
});
}
// Helper functions
function escHtml(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Toast Notification helper
function showToast(message, type = 'success') {
if (typeof window.showToast === 'function') {
window.showToast(message, type);
return;
}
alert(message);
}
// Run session check on initialization
checkSession();
})();
+1 -2
View File
@@ -29,12 +29,11 @@ const drawControl = new L.Control.Draw({
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
map.on(L.Draw.Event.DRAWSTART, function (e) {
isDrawingMode = true;
window.currentDrawMode = window.currentDrawMode || 'drawing';
});
map.on(L.Draw.Event.DRAWSTOP, function (e) {
setTimeout(() => {
isDrawingMode = false;
window.currentDrawMode = null;
window.activeDrawHandler = null;
window.deactivateAddMode();
+2 -3
View File
@@ -15,8 +15,6 @@ function makeSpbuIcon(is24) {
const spbuGreenIcon = makeSpbuIcon(true);
const spbuRedIcon = makeSpbuIcon(false);
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
// Fetch SPBU
function loadSpbu() {
@@ -75,7 +73,8 @@ function addSpbuMarker(item) {
// Map Click -> Form Add
map.on('click', function(e) {
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
// Jangan muncul form jika sedang dalam draw mode (jalan/parsil)
if (window.currentDrawMode) return;
// Hanya proses jika mode tambah SPBU aktif
if (window.currentAddMode === 'spbu') {
+179
View File
@@ -0,0 +1,179 @@
// Inisialisasi Peta Jalan
// Koordinat awal: [-0.0263, 109.3425] (Pontianak)
const map = L.map("map", { zoomControl: false }).setView(
[-0.0263, 109.3425],
13,
);
// ===== Toast Notification =====
window.showToast = function (message, type = "success", duration = 3500) {
const icons = { success: "✅", error: "❌", info: "️", warning: "⚠️" };
const container = document.getElementById("toastContainer");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.innerHTML = `<span class="toast-icon">${icons[type] || "️"}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add("hiding");
toast.addEventListener("animationend", () => toast.remove());
}, duration);
};
// Custom Zoom Control Logic
document.getElementById("zoomInBtn").addEventListener("click", function () {
map.zoomIn();
});
document.getElementById("zoomOutBtn").addEventListener("click", function () {
map.zoomOut();
});
// Base Map dari OpenStreetMap
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
// Inisialisasi FeatureGroups untuk masing-masing layer
const jalanLayer = L.featureGroup().addTo(map);
const searchInput = document.getElementById("searchInput");
const searchClear = document.getElementById("searchClear");
searchInput.addEventListener("focus", function () {
if (typeof window.deactivateAddMode === "function")
window.deactivateAddMode();
});
searchInput.addEventListener("input", function () {
const val = this.value.toLowerCase();
if (val.length > 0) {
searchClear.style.visibility = "visible";
window.showSearchResults(val);
} else {
searchClear.style.visibility = "hidden";
document.getElementById("searchResults").style.display = "none";
}
});
searchClear.addEventListener("click", function () {
searchInput.value = "";
searchClear.style.visibility = "hidden";
document.getElementById("searchResults").style.display = "none";
searchInput.focus();
});
// UI Logic: Custom Layer Control Toggle
const layerBtn = document.getElementById("layerBtn");
const layerPanel = document.getElementById("layerPanel");
if (layerBtn && layerPanel) {
layerBtn.addEventListener("click", function () {
if (typeof window.deactivateAddMode === "function")
window.deactivateAddMode();
layerPanel.style.display =
layerPanel.style.display === "block" ? "none" : "block";
});
}
// Sembunyikan layer panel saat klik di peta
map.on("click", function () {
if (layerPanel) layerPanel.style.display = "none";
});
// Logic untuk Toggle Layer Visibility
const setupLayerToggle = (id, layer) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener("change", function (e) {
e.target.checked ? map.addLayer(layer) : map.removeLayer(layer);
});
}
};
setupLayerToggle("layerJalan", jalanLayer);
// --- Sub-layer Toggle (expand/collapse) ---
window.toggleSubLayer = function (subId, iconEl) {
const sub = document.getElementById(subId);
if (!sub) return;
const isHidden = sub.style.display === "none";
sub.style.display = isHidden ? "" : "none";
iconEl.classList.toggle("collapsed", !isHidden);
};
// --- Sub-layer Filter ---
window.applySubFilter = function (type) {
if (type === "jalan") {
const checked = [...document.querySelectorAll(".sub-jalan:checked")].map(
(el) => el.value,
);
jalanLayer.eachLayer((layer) => {
if (!layer.jalanData) return;
const visible = checked.includes(layer.jalanData.status);
layer.setStyle({ opacity: visible ? 1 : 0, fillOpacity: visible ? 0.5 : 0 });
});
}
};
// --- Modal Logic ---
const unifiedModal = document.getElementById("unifiedModal");
const modalTitle = document.getElementById("modalTitle");
const modalBody = document.getElementById("modalBody");
window.openModal = function (title, bodyHTML, saveCallback) {
if (!unifiedModal) return;
modalTitle.textContent = title;
modalBody.innerHTML = bodyHTML;
unifiedModal.classList.add("show");
const currentSaveBtn = document.getElementById("modalSaveBtn");
if (currentSaveBtn) {
const newSaveBtn = currentSaveBtn.cloneNode(true);
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
newSaveBtn.addEventListener("click", saveCallback);
}
};
window.closeModal = function () {
if (unifiedModal) unifiedModal.classList.remove("show");
};
const confirmModal = document.getElementById("confirmModal");
const confirmMessage = document.getElementById("confirmMessage");
window.openConfirmModal = function (msg, confirmCallback) {
if (!confirmModal) return;
confirmMessage.textContent = msg;
confirmModal.classList.add("show");
const currentYesBtn = document.getElementById("confirmYesBtn");
if (currentYesBtn) {
const newYesBtn = currentYesBtn.cloneNode(true);
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
newYesBtn.addEventListener("click", function () {
confirmModal.classList.remove("show");
confirmCallback();
});
}
};
window.closeConfirmModal = function () {
if (confirmModal) confirmModal.classList.remove("show");
};
// --- Add Mode (untuk draw control) ---
window.currentDrawMode = null;
window.activeDrawHandler = null;
window.deactivateAddMode = function () {
if (window.activeDrawHandler) {
try { window.activeDrawHandler.disable(); } catch(e) {}
window.activeDrawHandler = null;
}
window.currentDrawMode = null;
document.getElementById("map").style.cursor = "";
};
+159
View File
@@ -0,0 +1,159 @@
// ===== Panel Management =====
// Mengelola Left Sidebar Toggle + Right Panel + Layer Control
(function () {
// ---- 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 ----
if (sidebarToggleBtn) {
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);
}
});
});
if (rightPanelClose) {
rightPanelClose.addEventListener('click', closePanel);
}
// ---- Open / Close Panel ----
function openPanel(panel) {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
activePanel = panel;
rightPanel.classList.add('open');
document.body.classList.add('right-panel-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() {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
activePanel = null;
rightPanel.classList.remove('open');
document.body.classList.remove('right-panel-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 'jalan': renderJalanPanel(); break;
}
}
// ==========================
// ===== Jalan Panel ========
// ==========================
function renderJalanPanel() {
rightPanelTitle.textContent = '🛣️ Daftar Jalan';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
if (isAdmin) {
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');
});
} else {
rightPanelActions.innerHTML = '';
}
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;
}
rightPanelBody.innerHTML = '';
items.forEach(d => {
const card = document.createElement('div');
card.className = 'data-card';
let actionButtons = '';
if (isAdmin) {
actionButtons = `
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
`;
}
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon" style="background:#e0e7ff; color:#4f46e5; padding:8px; border-radius:8px; font-size:18px;">🛣️</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">📏 ${d.panjang.toFixed(2)} m • 🏷️ ${escHtml(d.status)}</div>
</div>
${actionButtons}
</div>`;
if (isAdmin) {
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); window.openEditJalanModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); window.deleteJalan(d.id); });
}
card.addEventListener('click', () => {
jalanLayer.eachLayer(l => {
if (l.jalanData && l.jalanData.id == d.id) {
map.fitBounds(l.getBounds()); l.openPopup();
}
});
});
rightPanelBody.appendChild(card);
});
}
// ---- 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;
})();
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+16
View File
@@ -0,0 +1,16 @@
-- =============================================
-- DATABASE: webgis_jalan
-- Tabel: jalan (Pemetaan Jaringan Jalan)
-- =============================================
CREATE DATABASE IF NOT EXISTS webgis_jalan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE webgis_jalan;
CREATE TABLE IF NOT EXISTS jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
status ENUM('Nasional', 'Provinsi', 'Kabupaten') NOT NULL DEFAULT 'Kabupaten',
panjang DOUBLE NOT NULL DEFAULT 0,
geom LINESTRING NOT NULL,
SPATIAL INDEX (geom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+1 -1
View File
@@ -2,7 +2,7 @@
$host = "localhost";
$user = "root";
$pass = "";
$db = "webgis";
$db = "webgis_jalan";
// Coba koneksi ke server dan pilih database
$conn = new mysqli($host, $user, $pass, $db);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+43 -188
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pemetaan Kemiskinan</title>
<title>WebGIS Pemetaan Jaringan Jalan</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -30,7 +30,7 @@
<div class="search-icon">
<i class="fas fa-search"></i>
</div>
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah...">
<input type="text" class="search-input" id="searchInput" placeholder="Cari jalan...">
<button class="search-clear" id="searchClear">
<i class="fas fa-times"></i>
</button>
@@ -51,39 +51,50 @@
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button>
<!-- Tombol Kembali ke Beranda -->
<a href="/webgis/index.html" class="btn-back-home" title="Kembali ke Beranda">
<i class="fas fa-arrow-left"></i>
<span>Beranda</span>
</a>
<style>
.btn-back-home {
position: fixed;
top: 16px;
right: 70px;
z-index: 1100;
display: flex;
align-items: center;
gap: 7px;
background: #ffffff;
color: #4f46e5;
border: 1.5px solid rgba(79, 70, 229, 0.25);
border-radius: 10px;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
font-family: 'Inter', sans-serif;
text-decoration: none;
box-shadow: 0 2px 12px rgba(79, 70, 229, 0.12);
transition: all 0.2s ease;
}
.btn-back-home:hover {
background: #4f46e5;
color: #ffffff;
border-color: #4f46e5;
box-shadow: 0 4px 16px rgba(79, 70, 229, 0.3);
transform: translateY(-1px);
}
.btn-back-home i { font-size: 12px; }
</style>
<!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
<div class="sidebar-section-label">Umum</div>
<button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
<span class="sidebar-emoji"></span>
<span class="sidebar-label">SPBU</span>
</button>
<div class="sidebar-section-label">Menu</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>
<div class="sidebar-divider"></div>
<div class="sidebar-section-label">Poverty</div>
<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>
<div class="sidebar-divider" id="sidebarAdminDivider" style="display:none;"></div>
<button class="sidebar-btn" id="menuUsers" style="display:none;" title="Manajemen Pengguna">
<span class="sidebar-emoji">👥</span>
<span class="sidebar-label">User</span>
</button>
</nav>
@@ -100,24 +111,6 @@
<div class="layer-section-title">Infrastruktur & Lahan</div>
<!-- SPBU -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerSpbu" checked> SPBU
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subSpbu', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subSpbu" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="1" checked onchange="applySubFilter('spbu')"> 24 Jam
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="0" checked onchange="applySubFilter('spbu')"> Tidak 24 Jam
</label>
</div>
</div>
<!-- Jalan -->
<div class="layer-group-item">
<div class="layer-group-header">
@@ -139,30 +132,6 @@
</div>
</div>
<!-- Parsil -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerParsil" checked> Parsil Tanah
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subParsil', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subParsil" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="SHM" checked onchange="applySubFilter('parsil')"> SHM
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGB" checked onchange="applySubFilter('parsil')"> HGB
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
</label>
</div>
</div>
<div class="layer-section-title">Pemetaan Kemiskinan</div>
<!-- Rumah Ibadah -->
@@ -255,55 +224,7 @@
</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>
<!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget">
@@ -334,68 +255,7 @@
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="unified-modal">
<div class="modal-content" style="width: 700px; max-width: 95%;">
<div class="modal-header">
<h3>Manajemen Pengguna (Admin)</h3>
<span class="modal-close" id="closeUserManagementModal">&times;</span>
</div>
<div class="modal-body" style="gap: 15px;">
<!-- Form Tambah/Edit User -->
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
<input type="hidden" id="manageUserId" value="">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Username</label>
<input type="text" id="manageUsername" placeholder="Username">
</div>
<div class="form-group">
<label>Password (Kosongkan jika tidak diubah)</label>
<input type="password" id="managePassword" placeholder="Password">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Role</label>
<select id="manageRole">
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group" id="manageIbadahGroup">
<label>Rumah Ibadah yang Dikelola</label>
<select id="manageIbadahId">
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
</select>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
</div>
</div>
<!-- Tabel Daftar User -->
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
<th style="padding: 10px 12px;">Username</th>
<th style="padding: 10px 12px;">Role</th>
<th style="padding: 10px 12px;">Rumah Ibadah</th>
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- Diisi dinamis -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer"></div>
@@ -412,17 +272,12 @@
<!-- Main Map Initialization -->
<script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen -->
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script>
<script src="assets/js/features/jalan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/parsil.js?v=<?= time() ?>"></script>
<script src="assets/js/features/draw_control.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
<script src="assets/js/features/kemiskinan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
<!-- Core Auth Feature -->
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
</body>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
include_once 'db_connect.php';
$query = "SELECT id, nama, status, panjang, ST_AsGeoJSON(geom) as geom FROM jalan";
$result = $conn->query($query);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));