feat: implement full CRUD functionality for jalan, parsil, and spbu modules including database integration and map interaction features
This commit is contained in:
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// 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();
|
||||
})();
|
||||
@@ -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();
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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:
|
||||
'© <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 = "";
|
||||
};
|
||||
|
||||
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ---- Expose ----
|
||||
window.openRightPanel = openPanel;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user