const map = L.map('map', { zoomControl: false }).setView([-0.0258, 109.3323], 13); L.control.zoom({ position: 'bottomright' }).addTo(map); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map); let currentMode = null; let tempLayer = null; let places = []; let houses = []; let houseFilter = 'all'; // 'all' | 'linked' | 'unlinked' // Add a simple layer control (top-right) to filter households by linked status const HouseFilterControl = L.Control.extend({ options: { position: 'bottomright' }, onAdd: function () { const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control house-filter-control'); container.innerHTML = `
Filter Households
`; L.DomEvent.disableClickPropagation(container); setTimeout(() => { container.querySelectorAll('input[name="houseFilter"]').forEach(inp => { inp.addEventListener('change', (e) => { houseFilter = e.target.value; applyHouseFilter(houseFilter); }); }); }, 50); return container; } }); map.addControl(new HouseFilterControl()); // Auth State let authToken = localStorage.getItem('authToken'); let userRole = localStorage.getItem('userRole'); let userName = localStorage.getItem('userName'); async function apiFetch(url, options = {}) { if (!options.headers) options.headers = {}; if (authToken) options.headers['Authorization'] = `Bearer ${authToken}`; const response = await fetch(url, options); if (response.status === 401 || response.status === 403) { // Only alert on modification routes if (options.method && options.method !== 'GET') { const data = await response.json(); alert(data.message || 'Authentication error or forbidden access.'); } throw new Error('Unauthorized'); } return response; } const getHouseIcon = (color) => L.divIcon({ className: 'custom-div-icon', html: `
`, iconSize: [20, 20], iconAnchor: [10, 10] }); const centerIcon = L.divIcon({ className: 'custom-div-icon', html: `
`, iconSize: [16, 16], iconAnchor: [8, 8] }); function getPlaceIcon(type) { // Use simple emoji markers inside divIcon for clarity let bg = '#007bff'; let emoji = '🕌'; switch (type) { case 'mosque': bg = '#0ea5e9'; emoji = '🕌'; break; case 'church_catholic': bg = '#6366f1'; emoji = '⛪️'; break; case 'church_protestant': bg = '#6ee7b7'; emoji = '⛪️'; break; case 'pura': bg = '#f97316'; emoji = '🛕'; break; case 'vihara': bg = '#f43f5e'; emoji = '🕍'; break; case 'kelenteng': bg = '#f59e0b'; emoji = '🀄️'; break; default: bg = '#007bff'; emoji = '🕌'; } return L.divIcon({ className: 'custom-div-icon', html: `
${emoji}
`, iconSize: [24, 24], iconAnchor: [12, 12] }); } function getPlaceEmoji(type) { switch (type) { case 'mosque': return '🕌'; case 'church_catholic': return '⛪️'; case 'church_protestant': return '⛪️'; case 'pura': return '🛕'; case 'vihara': return '🕍'; case 'kelenteng': return '🀄️'; default: return '🕌'; } } // ------------------------------------------------------------------ // AUTHENTICATION & UI TOGGLES // ------------------------------------------------------------------ function openLogin() { document.getElementById('login-overlay').classList.remove('hidden'); } function closeLogin() { document.getElementById('login-overlay').classList.add('hidden'); document.getElementById('login-error').innerText = ''; } function openAdminPanel() { document.getElementById('admin-overlay').classList.remove('hidden'); } function closeAdminPanel() { document.getElementById('admin-overlay').classList.add('hidden'); document.getElementById('reg-msg').innerText = ''; } async function login() { const email = document.getElementById('login-email').value; const password = document.getElementById('login-password').value; try { const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Login failed'); authToken = data.token; userRole = data.role; userName = data.username; localStorage.setItem('authToken', authToken); localStorage.setItem('userRole', userRole); localStorage.setItem('userName', userName); closeLogin(); updateUIPermissions(); await refreshData(); } catch (err) { document.getElementById('login-error').innerText = err.message; } } function logout() { authToken = null; userRole = null; userName = null; localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('userName'); clearMapData(); updateUIPermissions(); } async function registerSurveyor() { const username = document.getElementById('reg-username').value; const email = document.getElementById('reg-email').value; const password = document.getElementById('reg-password').value; const msgEl = document.getElementById('reg-msg'); try { const res = await apiFetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, password, role: 'surveyor' }) }); const data = await res.json(); if (!res.ok) { msgEl.style.color = '#dc3545'; msgEl.innerText = data.message || 'Error'; } else { msgEl.style.color = '#28a745'; msgEl.innerText = 'Surveyor registered successfully!'; } } catch (err) { msgEl.style.color = '#dc3545'; msgEl.innerText = 'Registration failed. Check permissions.'; } } function updateUIPermissions() { const toolbar = document.getElementById('toolbar'); const loginBtn = document.getElementById('btn-open-login'); const logoutBtn = document.getElementById('btn-logout'); const adminPanelBtn = document.getElementById('btn-admin-panel'); const logisticsBtn = document.getElementById('btn-logistics'); const userInfo = document.getElementById('user-info'); if (userRole === 'admin' || userRole === 'surveyor') { toolbar.classList.remove('hidden'); loginBtn.style.display = 'none'; logoutBtn.style.display = 'block'; logisticsBtn.style.display = 'block'; userInfo.style.display = 'inline'; userInfo.innerText = `Welcome, ${userName} (${userRole})`; if (userRole === 'admin') adminPanelBtn.style.display = 'block'; else adminPanelBtn.style.display = 'none'; } else { toolbar.classList.add('hidden'); loginBtn.style.display = 'block'; logoutBtn.style.display = 'none'; logisticsBtn.style.display = 'none'; userInfo.style.display = 'none'; adminPanelBtn.style.display = 'none'; // Hide logistics panel if open document.getElementById('logistics-panel').classList.add('hidden'); cancelDraw(); } } function clearMapData() { places.forEach(place => { if (place.marker && map.hasLayer(place.marker)) map.removeLayer(place.marker); if (place.circle && map.hasLayer(place.circle)) map.removeLayer(place.circle); }); houses.forEach(house => { if (house.layer && map.hasLayer(house.layer)) map.removeLayer(house.layer); }); places = []; houses = []; } async function refreshData() { clearMapData(); await loadData(); } function getPopupButtons(type, id) { if (!userRole) return ''; // Public sees nothing let buttons = ``; if (userRole === 'admin') { buttons += ``; } return buttons; } async function loadData() { clearMapData(); const placesRes = await apiFetch('/api/places').catch(() => null); if (placesRes) { places = await placesRes.json(); places.forEach(p => renderPlace(p)); } const housesRes = await apiFetch('/api/houses').catch(() => null); if (housesRes) { houses = await housesRes.json(); houses.forEach(h => renderHouse(h)); } // Apply current house filter after rendering applyHouseFilter(houseFilter); updateUIPermissions(); } function setMode(mode) { currentMode = mode; document.getElementById('status-text').innerText = mode === 'place' ? 'Click on map to place a Circle.' : 'Click on map to place a Point.'; document.getElementById('btn-cancel').style.display = 'block'; cleanupTemp(); } function cancelDraw() { currentMode = null; document.getElementById('status-text').innerText = 'Select a tool to begin.'; document.getElementById('btn-cancel').style.display = 'none'; cleanupTemp(); } function cleanupTemp() { if (tempLayer) { if (tempLayer.marker) { map.removeLayer(tempLayer.marker); map.removeLayer(tempLayer.circle); } else { map.removeLayer(tempLayer); } tempLayer = null; } } // Map Click Logic map.on('click', function (e) { if (!userRole) return; // Prevent public from drawing if (currentMode === 'place') { currentMode = 'filling_form'; tempLayer = { marker: L.marker(e.latlng, { icon: centerIcon, draggable: true }).addTo(map), circle: L.circle(e.latlng, { radius: 500, color: 'blue' }).addTo(map) }; tempLayer.marker.on('drag', (ev) => tempLayer.circle.setLatLng(ev.target.getLatLng())); const popupContent = ` `; tempLayer.marker.bindPopup(popupContent, { closeOnClick: false, autoClose: false }).openPopup(); } else if (currentMode === 'house') { currentMode = 'filling_form'; tempLayer = L.marker(e.latlng, { icon: getHouseIcon('gray'), draggable: true }).addTo(map); const popupContent = ` `; tempLayer.bindPopup(popupContent, { closeOnClick: false, autoClose: false }).openPopup(); } }); window.updateTempRadius = function (val) { if (tempLayer && tempLayer.circle) tempLayer.circle.setRadius(parseInt(val)); } window.nextPlaceStep = function () { const popupContent = ` `; tempLayer.marker.bindPopup(popupContent).openPopup(); } window.savePlace = async function () { const name = document.getElementById('pName').value; const place_type = document.getElementById('pType') ? document.getElementById('pType').value : 'mosque'; if (!name) return alert("Name is required!"); const latlng = tempLayer.marker.getLatLng(); const radius = tempLayer.circle.getRadius(); try { const response = await apiFetch('/api/places', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, lat: latlng.lat, lng: latlng.lng, radius, place_type }) }); const newPlace = await response.json(); places.push(newPlace); renderPlace(newPlace); cleanupTemp(); cancelDraw(); await refreshData(); } catch (e) { } } window.saveHouse = async function () { const name = document.getElementById('hName').value; const nik = document.getElementById('hNIK').value; const kk = document.getElementById('hKK').value; const resCount = document.getElementById('hRes').value; const electricity = document.getElementById('hElectricity').value; const floor = document.getElementById('hFloor').value; const wall = document.getElementById('hWall').value; const water = document.getElementById('hWater').value; if (!name || !nik || !kk || !resCount) return alert("All fields are required!"); const latlng = tempLayer.getLatLng(); let nearestPlace = findNearestPlace(latlng); const linked_place_id = nearestPlace ? nearestPlace.id : null; try { const response = await apiFetch('/api/houses', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ head_name: name, head_nik: nik, kk_number: kk, resident_count: parseInt(resCount, 10), lat: latlng.lat, lng: latlng.lng, linked_place_id, electricity_va: electricity, floor_material: floor, wall_material: wall, drinking_water_source: water }) }); if (!response.ok) { const errData = await response.json(); alert("Error saving household: " + (errData.message || response.statusText)); return; } const newHouse = await response.json(); houses.push(newHouse); renderHouse(newHouse); cleanupTemp(); cancelDraw(); await refreshData(); } catch (e) { console.error("Save household error:", e); alert("Failed to save household. Please check console logs."); } } // ------------------------------------------------------------------ // EDIT AND DELETE LOGIC // ------------------------------------------------------------------ window.updatePlace = async function (id) { const place = places.find(p => p.id === id); const newName = document.getElementById(`edit-pName-${id}`).value; const newRadius = parseInt(document.getElementById(`edit-pRadius-${id}`).value); const newType = document.getElementById(`edit-pType-${id}`) ? document.getElementById(`edit-pType-${id}`).value : 'mosque'; try { await apiFetch(`/api/places/attr/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName, radius: newRadius, place_type: newType }) }); place.name = newName; place.radius = newRadius; place.place_type = newType; renderPlace(place); map.closePopup(); await refreshData(); } catch (e) { } } window.deletePlace = async function (id) { if (!confirm("Are you sure you want to delete this place?")) return; try { await apiFetch(`/api/places/${id}`, { method: 'DELETE' }); const placeIndex = places.findIndex(p => p.id === id); if (placeIndex > -1) { const place = places[placeIndex]; if (place.marker && map.hasLayer(place.marker)) map.removeLayer(place.marker); if (place.circle && map.hasLayer(place.circle)) map.removeLayer(place.circle); places.splice(placeIndex, 1); } map.closePopup(); await refreshData(); } catch (e) { } } window.updateHouse = async function (id) { const house = houses.find(h => h.id === id); const newName = document.getElementById(`edit-hName-${id}`).value; const newNIK = document.getElementById(`edit-hNIK-${id}`).value; const newKK = document.getElementById(`edit-hKK-${id}`).value; const newRes = document.getElementById(`edit-hRes-${id}`).value; const newElectricity = document.getElementById(`edit-hElectricity-${id}`).value; const newFloor = document.getElementById(`edit-hFloor-${id}`).value; const newWall = document.getElementById(`edit-hWall-${id}`).value; const newWater = document.getElementById(`edit-hWater-${id}`).value; try { await apiFetch(`/api/houses/attr/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ head_name: newName, head_nik: newNIK, kk_number: newKK, resident_count: newRes, electricity_va: newElectricity, floor_material: newFloor, wall_material: newWall, drinking_water_source: newWater }) }); house.name = newName; house.head_name = newName; house.head_nik = newNIK; house.kk_number = newKK; house.resident_count = newRes; house.electricity_va = newElectricity; house.floor_material = newFloor; house.wall_material = newWall; house.drinking_water_source = newWater; renderHouse(house); map.closePopup(); await refreshData(); } catch (e) { console.error("Update household error:", e); alert("Failed to update household."); } } window.deleteHouse = async function (id) { if (!confirm("Are you sure you want to delete this house?")) return; try { await apiFetch(`/api/houses/${id}`, { method: 'DELETE' }); const houseIndex = houses.findIndex(h => h.id === id); if (houseIndex > -1) { const house = houses[houseIndex]; map.removeLayer(house.layer); houses.splice(houseIndex, 1); } map.closePopup(); await refreshData(); } catch (e) { } } // ------------------------------------------------------------------ // RENDERING // ------------------------------------------------------------------ function renderPlace(place) { if (place.marker) map.removeLayer(place.marker); if (place.circle) map.removeLayer(place.circle); // Draggable only if admin or owner. But for simplicity, enable draggable only if logged in. const draggable = userRole ? true : false; const marker = L.marker([place.lat, place.lng], { icon: getPlaceIcon(place.place_type || 'mosque'), draggable }).addTo(map); const circle = L.circle([place.lat, place.lng], { radius: place.radius, color: '#3388ff', fillOpacity: 0.2 }).addTo(map); place.marker = marker; place.circle = circle; if (userRole) { const popupContent = ` `; marker.bindPopup(popupContent); } else { marker.bindPopup(`${place.name}
Radius: ${place.radius}m`); } if (draggable) { marker.on('drag', (e) => circle.setLatLng(e.target.getLatLng())); marker.on('dragend', async function (e) { const newLatLng = e.target.getLatLng(); const oldLat = place.lat; const oldLng = place.lng; place.lat = newLatLng.lat; place.lng = newLatLng.lng; try { await apiFetch(`/api/places/${place.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat: place.lat, lng: place.lng }) }); recalculateAllHouses(); } catch (err) { // Revert on permission failure place.lat = oldLat; place.lng = oldLng; marker.setLatLng([oldLat, oldLng]); circle.setLatLng([oldLat, oldLng]); } }); } } function renderHouse(house) { if (house.layer) map.removeLayer(house.layer); const draggable = userRole ? true : false; const markerColor = house.linked_place_id ? '#28a745' : '#dc3545'; const marker = L.marker([house.lat, house.lng], { icon: getHouseIcon(markerColor), draggable }).addTo(map); house.layer = marker; const linkedPlace = places.find(p => p.id === house.linked_place_id); const linkedName = linkedPlace ? linkedPlace.name : 'Unlinked'; const linkedStatus = house.linked_place_id ? `Linked to ${linkedName}` : `Unlinked`; if (userRole) { const canDeliver = (userRole === 'admin' || userRole === 'surveyor') && house.verification_status === 'verified'; const deliveryFormHtml = canDeliver ? `
` : ''; const verifyHouseholdHtml = userRole === 'admin' && house.verification_status === 'pending' ? ` ` : ''; const pendingMessage = !canDeliver && house.verification_status !== 'verified' ? `

⚠️ Handover locked — household must be verified first.

` : ''; const popupContent = ` `; marker.bindPopup(popupContent, { maxWidth: 280 }); } else { marker.bindPopup(` Household: ${house.name}
Residents: ${house.resident_count}
NIK: ${house.head_nik || '-'}
KK: ${house.kk_number || '-'}

Linked Place: ${linkedName}
BPS Poverty Indicators:
⚡ Electricity: ${house.electricity_va} VA
🪵 Floor: ${house.floor_material}
🧱 Wall: ${house.wall_material}
🚰 Water: ${house.drinking_water_source}
Status: ${house.verification_status}
${linkedStatus} `); } if (draggable) { marker.on('dragend', async function (e) { const newLatLng = e.target.getLatLng(); const oldLat = house.lat; const oldLng = house.lng; const oldLink = house.linked_place_id; house.lat = newLatLng.lat; house.lng = newLatLng.lng; let nearestPlace = findNearestPlace(newLatLng); house.linked_place_id = nearestPlace ? nearestPlace.id : null; try { await apiFetch(`/api/houses/${house.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat: house.lat, lng: house.lng, linked_place_id: house.linked_place_id }) }); renderHouse(house); } catch (err) { // Revert house.lat = oldLat; house.lng = oldLng; house.linked_place_id = oldLink; renderHouse(house); } }); } } function findNearestPlace(latlng) { let nearest = null; let minDistance = Infinity; places.forEach(place => { const placeLatLng = L.latLng(place.lat, place.lng); const dist = map.distance(latlng, placeLatLng); if (dist <= place.radius && dist < minDistance) { minDistance = dist; nearest = place; } }); return nearest; } function applyHouseFilter(mode) { for (let house of houses) { if (!house.layer) continue; const linked = !!house.linked_place_id; if (mode === 'all') { if (!map.hasLayer(house.layer)) map.addLayer(house.layer); } else if (mode === 'linked') { if (linked) { if (!map.hasLayer(house.layer)) map.addLayer(house.layer); } else { if (map.hasLayer(house.layer)) map.removeLayer(house.layer); } } else if (mode === 'unlinked') { if (!linked) { if (!map.hasLayer(house.layer)) map.addLayer(house.layer); } else { if (map.hasLayer(house.layer)) map.removeLayer(house.layer); } } } } async function recalculateAllHouses() { for (let house of houses) { const houseLatLng = L.latLng(house.lat, house.lng); let nearestPlace = findNearestPlace(houseLatLng); const newLinkId = nearestPlace ? nearestPlace.id : null; if (house.linked_place_id !== newLinkId) { house.linked_place_id = newLinkId; try { // Not all houses might be owned by current surveyor, so this might silently fail on server. // We wrap it securely in apiFetch without throwing hard. await apiFetch(`/api/houses/${house.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lat: house.lat, lng: house.lng, linked_place_id: house.linked_place_id }) }); } catch (e) { } renderHouse(house); } } } // ------------------------------------------------------------------ // SEMBAKO DISTRIBUTION & LOGISTICS DASHBOARD // ------------------------------------------------------------------ /** * submitSembakoDelivery * Called from the popup button. Sends a POST to /api/distribution/log. * @param {number} householdId - The household's database ID * @param {number|null} placeId - The linked religious place ID */ window.submitSembakoDelivery = async function (householdId, placeId) { const notesEl = document.getElementById(`delivery-notes-${householdId}`); const notes = notesEl ? notesEl.value.trim() : ''; try { const res = await apiFetch('/api/distribution/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ household_id: householdId, place_id: placeId, amount_value: 1, // 1 sembako package per handover notes: notes || null }) }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Server error'); alert(`✅ Handover logged successfully! Distribution ID: ${data.distribution_id}`); map.closePopup(); // Refresh the logistics dashboard if it's open if (!document.getElementById('logistics-panel').classList.contains('hidden')) { loadSembakoDashboard(); } } catch (err) { console.error('Sembako delivery error:', err); alert(`❌ Failed to log handover: ${err.message}`); } }; window.verifyHousehold = async function (householdId) { try { const res = await apiFetch(`/api/houses/verify/${householdId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' } }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Verification failed'); alert('✅ Household verified successfully. Handover is now enabled.'); await refreshData(); } catch (err) { console.error('Household verification error:', err); alert(`❌ ${err.message}`); } }; async function loadPendingHandoverVerifications() { const res = await apiFetch('/api/distribution/pending'); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.message || 'Failed to fetch pending verifications'); } return res.json(); } async function verifyDistribution(distributionId) { if (!confirm('Verify this sembako handover?')) return; try { const res = await apiFetch(`/api/distribution/verify/${distributionId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' } }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Verification failed'); alert('✅ Handover verified successfully.'); await loadSembakoDashboard(); } catch (err) { console.error('Verification error:', err); alert(`❌ ${err.message}`); } } /** * loadSembakoDashboard * Fetches /api/analytics/sembako-demand and renders results * inside the #logistics-panel sidebar. */ async function loadSembakoDashboard() { const contentEl = document.getElementById('logistics-content'); contentEl.innerHTML = '

Loading...

'; try { const res = await apiFetch('/api/analytics/sembako-demand'); if (!res.ok) throw new Error('Failed to fetch analytics'); const data = await res.json(); const totalHouseholds = data.reduce((sum, r) => sum + Number(r.households_needing_aid || 0), 0); const totalResidents = data.reduce((sum, r) => sum + Number(r.total_residents_needing_aid || 0), 0); const rows = data.map(row => `
${getPlaceEmoji(row.place_type)}
${row.place_name}
${Number(row.households_needing_aid || 0)} KK perlu sembako
👥 ${Number(row.total_residents_needing_aid || 0)} jiwa terdampak
`).join(''); let pageContent = `
${totalHouseholds}Total KK
${totalResidents}Total Jiwa
${rows.length ? rows : `
🎉

All verified households have received sembako!

`} `; if (userRole === 'admin') { const pending = await loadPendingHandoverVerifications(); pageContent += renderPendingHandoverSection(pending); const pendingHouses = await loadPendingHouseholdVerifications(); pageContent += renderPendingHouseholdSection(pendingHouses); } contentEl.innerHTML = pageContent; } catch (err) { contentEl.innerHTML = `

❌ ${err.message}. Please log in first.

`; } } function renderPendingHandoverSection(records) { if (!records || records.length === 0) { return `

Pending Handover Verification

No pending handovers need verification right now.

`; } const rows = records.map(record => `
#${record.id}
Household: ${record.head_name || '-'} (${record.kk_number || '-'})
Place: ${record.place_name || 'Unlinked'}
Surveyor: ${record.distributed_by_name || 'Unknown'}
Notes: ${record.notes || '-'}
`).join(''); return `

Pending Handover Verification

${rows}
`; } async function loadPendingHouseholdVerifications() { const res = await apiFetch('/api/houses/pending'); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.message || 'Failed to fetch pending households'); } return res.json(); } function renderPendingHouseholdSection(records) { if (!records || records.length === 0) { return `

Pending Household Verifications

No pending household assessments.

`; } const rows = records.map(r => `
#${r.assessment_id} - ${r.head_name || '-'}
KK: ${r.kk_number || '-'}
Surveyor: ${r.surveyor_name || 'Unknown'}
Place: ${r.place_name || 'Unlinked'}
Indicators: ⚡ ${r.electricity_va || '-'} • Floor: ${r.floor_material || '-'} • Wall: ${r.wall_material || '-'}
`).join(''); return `

Pending Household Verifications

${rows}
`; } window.toggleLogisticsDashboard = function () { const panel = document.getElementById('logistics-panel'); const isHidden = panel.classList.toggle('hidden'); if (!isHidden && (userRole === 'admin' || userRole === 'surveyor')) { loadSembakoDashboard(); } }; // Boot up loadData();