/** * WebGIS BantSOSial โ€” script.js v4 * ===================================================== * Tambahan dari v3: * - Role system: Admin / Surveyer / Viewer * - Ikon rumah ibadah otomatis per jenis (masjid, gereja, vihara, dll) * - Edit kas langsung dari popup rumah ibadah * - Custom date picker (grid 3ร—3, navigasi bulan/tahun) * ===================================================== */ // ============================================================ // KONFIGURASI // ============================================================ const MAP_CENTER = [-0.0532, 109.3458]; const MAP_ZOOM = 15; const DEFAULT_RADIUS = 300; // ============================================================ // ROLE SYSTEM // ============================================================ // Roles: 'admin' | 'surveyer' | 'viewer' let currentRole = 'admin'; const ROLE_CONFIG = { admin: { label: 'Admin', icon: '๐Ÿ‘‘', canAdd: true, canEdit: true, canDelete: true, canReset: true, canReport: true, canViewReport: true, canViewDetail: true, canEditKas: true, canDragRadius: true }, surveyer: { label: 'Surveyer', icon: '๐Ÿ“‹', canAdd: true, canEdit: true, canDelete: false, canReset: false, canReport: true, canViewReport: true, canViewDetail: true, canEditKas: false, canDragRadius: false }, viewer: { label: 'Masyarakat', icon: '๐Ÿ‘', canAdd: false, canEdit: false, canDelete: false, canReset: false, canReport: true, canViewReport: false, canViewDetail: true, canEditKas: false, canDragRadius: false } }; // User session data let loggedInUser = null; // Auth check โ€” redirect to login if not logged in async function checkAuth() { try { const res = await fetch('auth_check.php'); const data = await res.json(); if (data.logged_in) { loggedInUser = data.user; currentRole = data.user.role; updateUserUI(); return true; } else { window.location.href = 'login.html'; return false; } } catch (e) { // Server not available โ€” allow offline usage with default role console.log('Auth server tidak tersedia, mode lokal.'); return true; } } function updateUserUI() { if (!loggedInUser) return; const cfg = ROLE_CONFIG[loggedInUser.role] || ROLE_CONFIG.viewer; // Update role icon (label already set by PHP) const roleIconEl = document.getElementById('roleIcon'); if (roleIconEl) roleIconEl.textContent = cfg.icon; // Show history nav for viewer (masyarakat) const navHistory = document.getElementById('navHistory'); if (navHistory) { navHistory.style.display = loggedInUser.role === 'viewer' ? 'flex' : 'none'; } // Show verify nav for admin const navVerify = document.getElementById('navVerify'); if (navVerify) { navVerify.style.display = loggedInUser.role === 'admin' ? 'flex' : 'none'; } // Hide add buttons for viewer applyRoleUI(); // Auto-fill report name if logged in const reportNameEl = document.getElementById('reportName'); if (reportNameEl && loggedInUser.nama) { reportNameEl.value = loggedInUser.nama; reportNameEl.readOnly = true; reportNameEl.style.background = '#f0f4f8'; } } function openRoleModal() { // Role modal removed โ€” role is from database/session // No-op for backward compatibility } function closeRoleModal() { // No-op } function setRole(role) { currentRole = role; const cfg = ROLE_CONFIG[role]; // Update role bar document.getElementById('roleIcon').textContent = cfg.icon; document.getElementById('roleLabel').textContent = cfg.label; // Apply UI restrictions applyRoleUI(); } function applyRoleUI() { const cfg = ROLE_CONFIG[currentRole]; // Add buttons const btnGroup = document.getElementById('actionBtnGroup'); if (btnGroup) btnGroup.style.display = cfg.canAdd ? 'flex' : 'none'; // Reset button const btnReset = document.getElementById('btnReset'); if (btnReset) btnReset.style.display = cfg.canReset ? 'block' : 'none'; // Viewer notice let notice = document.getElementById('viewerNotice'); if (!notice) { notice = document.createElement('div'); notice.id = 'viewerNotice'; notice.innerHTML = '๐Ÿ‘ Mode Viewer โ€” Anda hanya dapat melihat data'; const sidebar = document.getElementById('sidebar'); const firstSection = sidebar.querySelector('.section'); sidebar.insertBefore(notice, firstSection); } notice.style.display = currentRole === 'viewer' ? 'block' : 'none'; // Report page โ€” toggle list vs viewer panel const reportColList = document.getElementById('reportColList'); const reportColViewer = document.getElementById('reportColViewer'); if (cfg.canViewReport) { if (reportColList) reportColList.style.display = 'flex'; if (reportColViewer) reportColViewer.style.display = 'none'; } else { if (reportColList) reportColList.style.display = 'none'; if (reportColViewer) reportColViewer.style.display = 'flex'; } // Update report badge count updateReportBadge(); // Rebuild popups to reflect role (delete buttons, edit kas, etc.) centers.forEach(c => { if (c.marker) { c.marker.setPopupContent(buildCenterPopup(c)); } }); houses.forEach(h => { if (h.marker) { h.marker.setPopupContent(buildHouseMapPopup(h)); } }); // Update sidebar lists (delete buttons visibility) updateSidebar(); // Handle drag on radius handles centers.forEach(c => { if (c.handle) { if (cfg.canDragRadius) { c.handle.dragging?.enable(); } else { c.handle.dragging?.disable(); } } // Marker draggable if (c.marker) { if (cfg.canEdit) { c.marker.dragging?.enable(); } else { c.marker.dragging?.disable(); } } }); } // ============================================================ // PAGE NAVIGATION // ============================================================ function navigateTo(page) { const pagePeta = document.getElementById('pagePeta'); const pagePelaporan = document.getElementById('pagePelaporan'); const pageHistory = document.getElementById('pageHistory'); const navHome = document.getElementById('navHome'); const navReport = document.getElementById('navReport'); const navHistory = document.getElementById('navHistory'); // Hide all pages [pagePeta, pagePelaporan, pageHistory].forEach(p => { if(p) p.classList.remove('active-page'); }); [navHome, navReport, navHistory].forEach(n => { if(n) n.classList.remove('active'); }); if (page === 'map') { pagePeta.classList.add('active-page'); navHome.classList.add('active'); setTimeout(() => map.invalidateSize(), 100); } else if (page === 'report') { pagePelaporan.classList.add('active-page'); navReport.classList.add('active'); applyRoleUI(); renderReportList(); } else if (page === 'history') { pageHistory.classList.add('active-page'); if (navHistory) navHistory.classList.add('active'); loadHistory(); } } function can(action) { return ROLE_CONFIG[currentRole]?.[action] === true; } // ============================================================ // INIT MAP // ============================================================ const map = L.map('map', { zoomControl: false, preferCanvas: false }) .setView(MAP_CENTER, MAP_ZOOM); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'ยฉ OpenStreetMap', maxZoom: 19 }).addTo(map); L.control.zoom({ position: 'bottomright' }).addTo(map); // ============================================================ // STATE // ============================================================ let isAddingCenter = false; let isAddingHouse = false; let centers = []; let houses = []; let reports = []; let nextCenterId = 1; let nextHouseId = 1; // ============================================================ // ICON TYPES โ€” Rumah Ibadah // ============================================================ const CENTER_TYPE_MAP = { 'masjid': { emoji: '๐Ÿ•Œ', grad: 'linear-gradient(135deg,#0f766e,#0d9488)' }, 'musholla': { emoji: '๐Ÿ•Œ', grad: 'linear-gradient(135deg,#0f766e,#0d9488)' }, 'surau': { emoji: '๐Ÿ•Œ', grad: 'linear-gradient(135deg,#0f766e,#0d9488)' }, 'gereja katedral': { emoji: 'โ›ช', grad: 'linear-gradient(135deg,#1d4ed8,#3b82f6)' }, 'gereja katolik': { emoji: 'โ›ช', grad: 'linear-gradient(135deg,#1d4ed8,#3b82f6)' }, 'katedral': { emoji: 'โ›ช', grad: 'linear-gradient(135deg,#1d4ed8,#3b82f6)' }, 'gereja protestan': { emoji: 'โœ๏ธ', grad: 'linear-gradient(135deg,#6d28d9,#8b5cf6)' }, 'gereja': { emoji: 'โ›ช', grad: 'linear-gradient(135deg,#1e40af,#2563eb)' }, 'kapel': { emoji: 'โœ๏ธ', grad: 'linear-gradient(135deg,#6d28d9,#8b5cf6)' }, 'vihara': { emoji: '๐Ÿ›•', grad: 'linear-gradient(135deg,#b45309,#d97706)' }, 'klenteng': { emoji: '๐Ÿ›•', grad: 'linear-gradient(135deg,#b45309,#d97706)' }, 'pura': { emoji: '๐Ÿ›•', grad: 'linear-gradient(135deg,#b45309,#d97706)' }, 'kuil': { emoji: '๐Ÿ›•', grad: 'linear-gradient(135deg,#b45309,#d97706)' }, 'sinagog': { emoji: 'โœก๏ธ', grad: 'linear-gradient(135deg,#1e40af,#2563eb)' }, 'default': { emoji: '๐Ÿ›๏ธ', grad: 'linear-gradient(135deg,#475569,#64748b)' } }; function getCenterType(name) { if (!name) return 'default'; const lower = name.toLowerCase(); // longest match first const keys = Object.keys(CENTER_TYPE_MAP).filter(k => k !== 'default').sort((a,b) => b.length - a.length); for (const key of keys) { if (lower.includes(key)) return key; } return 'default'; } function createCenterIcon(name) { const type = getCenterType(name); const info = CENTER_TYPE_MAP[type] || CENTER_TYPE_MAP['default']; return L.divIcon({ className: '', html: `
${info.emoji}
`, iconSize: [38, 38], iconAnchor: [10, 38] }); } function createHouseIcon(aidStatus, hasData) { const palette = { helped: { bg: '#d97706', border: '#78350f', emoji: '๐Ÿ ' }, not_helped: { bg: '#dc2626', border: '#7f1d1d', emoji: '๐Ÿš' }, outside: { bg: '#16a34a', border: '#14532d', emoji: '๐Ÿ ' }, nodata: { bg: '#94a3b8', border: '#475569', emoji: '๐Ÿ“' } }; const key = !hasData ? 'nodata' : (aidStatus || 'outside'); const c = palette[key] || palette.nodata; return L.divIcon({ className: '', html: `
${c.emoji}
`, iconSize: [24, 24], iconAnchor: [6, 24] }); } function createHandleIcon() { return L.divIcon({ className: 'radius-handle', html: `
`, iconSize: [14, 14], iconAnchor: [7, 7] }); } // ============================================================ // UTILS // ============================================================ function haversineDistance(lat1, lng1, lat2, lng2) { const R = 6371000; const ฯ†1 = lat1*Math.PI/180, ฯ†2 = lat2*Math.PI/180; const ฮ”ฯ† = (lat2-lat1)*Math.PI/180, ฮ”ฮป = (lng2-lng1)*Math.PI/180; const a = Math.sin(ฮ”ฯ†/2)**2 + Math.cos(ฯ†1)*Math.cos(ฯ†2)*Math.sin(ฮ”ฮป/2)**2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); } async function reverseGeocode(lat, lng) { try { const res = await fetch( `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`, { headers: { 'Accept-Language': 'id' } } ); const data = await res.json(); const addr = data.address || {}; return { full: data.display_name || '', road: addr.road || addr.pedestrian || '', village: addr.village || addr.suburb || addr.neighbourhood || '', subdistrict: addr.city_district || addr.town || '', city: addr.city || addr.county || 'Pontianak', postcode: addr.postcode || '' }; } catch { return { full:`${lat.toFixed(5)}, ${lng.toFixed(5)}`, road:'', village:'', subdistrict:'', city:'', postcode:'' }; } } function formatRupiah(num) { if (!num && num !== 0) return 'โ€”'; return 'Rp ' + Number(num).toLocaleString('id-ID'); } function formatDate(d) { if (!d) return 'โ€”'; const dt = new Date(d); return isNaN(dt) ? d : dt.toLocaleDateString('id-ID', { day:'2-digit', month:'long', year:'numeric' }); } function calcAge(tglLahir) { if (!tglLahir) return null; const today = new Date(), birth = new Date(tglLahir); let age = today.getFullYear() - birth.getFullYear(); if (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate())) age--; return age; } function radiusPoint(lat, lng, dist, bearing = 90) { const R = 6371000, ฯ†1 = lat*Math.PI/180, ฮป1 = lng*Math.PI/180, brng = bearing*Math.PI/180; const ฯ†2 = Math.asin(Math.sin(ฯ†1)*Math.cos(dist/R) + Math.cos(ฯ†1)*Math.sin(dist/R)*Math.cos(brng)); const ฮป2 = ฮป1 + Math.atan2(Math.sin(brng)*Math.sin(dist/R)*Math.cos(ฯ†1), Math.cos(dist/R)-Math.sin(ฯ†1)*Math.sin(ฯ†2)); return L.latLng(ฯ†2*180/Math.PI, ฮป2*180/Math.PI); } // ============================================================ // RADIUS DRAG HANDLE // ============================================================ function addRadiusHandle(centerObj) { if (centerObj.handle) map.removeLayer(centerObj.handle); const tooltip = document.getElementById('radiusTooltip'); const handle = L.marker(radiusPoint(centerObj.lat, centerObj.lng, centerObj.radius), { icon: createHandleIcon(), draggable: true, zIndexOffset: 500 }).addTo(map); handle.on('drag', function(e) { if (!can('canDragRadius')) return; centerObj.radius = Math.max(50, Math.round(haversineDistance(centerObj.lat, centerObj.lng, e.target.getLatLng().lat, e.target.getLatLng().lng))); centerObj.circle.setRadius(centerObj.radius); tooltip.textContent = `โญ• Radius: ${centerObj.radius} m`; tooltip.classList.remove('hidden'); updateAllHouseIcons(); updateSidebar(); }); handle.on('dragend', function() { if (!can('canDragRadius')) { handle.setLatLng(radiusPoint(centerObj.lat, centerObj.lng, centerObj.radius)); return; } handle.setLatLng(radiusPoint(centerObj.lat, centerObj.lng, centerObj.radius)); tooltip.classList.add('hidden'); saveRadiusToBackend(centerObj); if (centerObj.marker.isPopupOpen()) centerObj.marker.setPopupContent(buildCenterPopup(centerObj)); }); centerObj.handle = handle; } // ============================================================ // HOUSE AID STATUS // ============================================================ function getAidStatus(house) { if (!house.hasData) return 'outside'; if (house.aidStatus === 'helped') return 'helped'; for (const c of centers) { if (haversineDistance(house.lat, house.lng, c.lat, c.lng) <= c.radius) return 'not_helped'; } return 'outside'; } function getCoveringCenters(house) { return centers.filter(c => haversineDistance(house.lat, house.lng, c.lat, c.lng) <= c.radius); } function findNearestCenter(house) { let nearest = null, minDist = Infinity; centers.forEach(c => { const d = haversineDistance(house.lat, house.lng, c.lat, c.lng); if (d < minDist) { minDist = d; nearest = c; } }); return { center: nearest, distance: Math.round(minDist) }; } function updateAllHouseIcons() { houses.forEach(h => { const status = getAidStatus(h); if (h.aidStatus !== 'helped') h.aidStatus = (status === 'not_helped') ? 'not_helped' : 'outside'; h.marker.setIcon(createHouseIcon(h.aidStatus, h.hasData)); }); updateStats(); } // ============================================================ // POPUPS // ============================================================ function buildHouseMapPopup(house) { const aidStatus = getAidStatus(house); const badgeCls = !house.hasData ? 'nodata' : (aidStatus==='helped'?'yellow':aidStatus==='not_helped'?'red':'green'); const badgeTxt = !house.hasData ? 'Belum ada data' : (aidStatus==='helped'?'Sudah Dibantu':aidStatus==='not_helped'?'Dalam Radius':'Luar Radius'); let actionBtns = ''; if (house.hasData) { actionBtns = ``; if (can('canEdit')) actionBtns += ``; } else { if (can('canEdit')) actionBtns = ``; else actionBtns = `Belum ada data`; } const deleteBtn = can('canDelete') ? `` : ''; return `
โ— ${badgeTxt}
๐Ÿ  Titik Rumah #${house.id}
${ house.address && house.address !== 'Mengambil alamat...' ? house.address : 'โณ Mengambil alamat...' }
${actionBtns}${deleteBtn}
`; } function buildCenterPopup(c) { const covered = houses.filter(h => haversineDistance(h.lat, h.lng, c.lat, c.lng) <= c.radius); const helped = covered.filter(h => h.aidStatus === 'helped').length; const type = getCenterType(c.name); const info = CENTER_TYPE_MAP[type] || CENTER_TYPE_MAP['default']; const kasEditHtml = can('canEditKas') ? ` ` : ''; const kasFormHtml = can('canEditKas') ? ` ` : ''; const editBtn = can('canEdit') ? `
` : ''; const deleteBtn = can('canDelete') ? `
` : ''; return `
${info.emoji}
${c.name}
๐Ÿ“ ${c.address}
๐Ÿ’ฐ Kas Tersedia ${formatRupiah(c.kas)} ${kasEditHtml}
${kasFormHtml}
โญ• Radius Jangkauan ${c.radius} m
๐Ÿ  Dalam radius ${covered.length} (${helped} dibantu)
${can('canDragRadius') ? '๐Ÿ’ก Drag titik biru untuk ubah radius' : '๐Ÿ‘ Radius hanya bisa dilihat'}
${editBtn}${deleteBtn}`; } // ============================================================ // CENTER EDIT MODAL (full edit: name, address, kas, radius) // ============================================================ function openCenterEditModal(centerId) { if (!can('canEdit')) { alert('Role Anda tidak dapat mengedit data.'); return; } const c = centers.find(c => c.id === centerId); if (!c) return; map.closePopup(); // Detect type from name const detectedType = getCenterType(c.name); document.getElementById('ce_id').value = c.id; document.getElementById('ce_name').value = c.name; document.getElementById('ce_address').value = c.address; document.getElementById('ce_kas').value = c.kas || 0; document.getElementById('ce_radius').value = c.radius; // Select the matching type option const sel = document.getElementById('ce_type'); const opts = Array.from(sel.options); const match = opts.find(o => o.value === detectedType); sel.value = match ? detectedType : 'default'; document.getElementById('centerEditModal').classList.remove('hidden'); setTimeout(() => document.getElementById('ce_name')?.focus(), 100); } function closeCenterEditModal() { document.getElementById('centerEditModal').classList.add('hidden'); } function updateCenterEditHeader() { // Optionally update something in modal when type changes (no-op for now) } function saveCenterEdit() { const id = parseInt(document.getElementById('ce_id')?.value); const name = document.getElementById('ce_name')?.value?.trim(); const address = document.getElementById('ce_address')?.value?.trim(); const kas = parseFloat(document.getElementById('ce_kas')?.value) || 0; const radius = parseInt(document.getElementById('ce_radius')?.value) || DEFAULT_RADIUS; if (!name) { document.getElementById('ce_name').style.borderColor='#dc2626'; document.getElementById('ce_name').focus(); return; } if (radius < 50) { alert('Radius minimal 50 meter!'); return; } const center = centers.find(c => c.id === id); if (!center) return; // Update in-memory center.name = name; center.address = address; center.kas = kas; center.radius = radius; // Update Leaflet layers center.marker.setIcon(createCenterIcon(name)); center.circle.setRadius(radius); if (center.handle) center.handle.setLatLng(radiusPoint(center.lat, center.lng, radius)); // Rebuild popup center.marker.setPopupContent(buildCenterPopup(center)); closeCenterEditModal(); updateAllHouseIcons(); updateSidebar(); // Sync to backend updateCenterPositionBackend(center); } // Edit kas functions function toggleEditKas(centerId) { const form = document.getElementById(`kasEditForm_${centerId}`); const badge = document.getElementById(`kasBadge_${centerId}`); if (!form) return; const visible = form.style.display !== 'none'; form.style.display = visible ? 'none' : 'block'; badge.style.display = visible ? 'flex' : 'none'; if (!visible) setTimeout(() => document.getElementById(`kasInput_${centerId}`)?.focus(), 50); } function saveKas(centerId) { const center = centers.find(c => c.id === centerId); if (!center) return; center.kas = parseFloat(document.getElementById(`kasInput_${centerId}`)?.value) || 0; const kasVal = document.getElementById(`kasVal_${centerId}`); if (kasVal) kasVal.textContent = formatRupiah(center.kas); toggleEditKas(centerId); updateCenterList(); updateCenterPositionBackend(center); } // ============================================================ // ADD MODES (role-gated) // ============================================================ function startAddingCenter() { if (!can('canAdd')) { alert('Role Anda tidak memiliki akses untuk menambah data.'); return; } if (isAddingCenter) { cancelModes(); return; } cancelModes(); isAddingCenter = true; setModeUI('center'); } function startAddingHouse() { if (!can('canAdd')) { alert('Role Anda tidak memiliki akses untuk menambah data.'); return; } if (isAddingHouse) { cancelModes(); return; } cancelModes(); isAddingHouse = true; setModeUI('house'); } function setModeUI(mode) { const badge = document.getElementById('modeIndicator'); badge.classList.remove('hidden','house-mode'); map.getContainer().classList.add('adding-mode'); if (mode === 'center') { document.getElementById('modeIndicatorText').textContent = 'Klik peta untuk menempatkan rumah ibadah'; document.getElementById('btnAddCenter').classList.add('active'); document.getElementById('btnAddCenter').textContent = 'โœ• Batalkan'; } else { badge.classList.add('house-mode'); document.getElementById('modeIndicatorText').textContent = 'Klik peta untuk menempatkan titik rumah miskin'; document.getElementById('btnAddHouse').classList.add('active'); document.getElementById('btnAddHouse').textContent = 'โœ• Batalkan'; } } function cancelModes() { isAddingCenter = isAddingHouse = false; document.getElementById('modeIndicator').classList.add('hidden'); document.getElementById('modeIndicator').classList.remove('house-mode'); document.getElementById('btnAddCenter').classList.remove('active'); document.getElementById('btnAddCenter').innerHTML = '๐Ÿ•Œ Tambah Rumah Ibadah'; document.getElementById('btnAddHouse').classList.remove('active'); document.getElementById('btnAddHouse').innerHTML = '๐Ÿ  Tambah Rumah Miskin'; map.getContainer().classList.remove('adding-mode'); } map.on('click', async function(e) { if (!isAddingCenter && !isAddingHouse) return; const { lat, lng } = e.latlng; if (isAddingCenter) { cancelModes(); openCenterForm(lat, lng, e.latlng); } else { cancelModes(); placeHousePin(lat, lng); } }); // ============================================================ // CENTER FORM // ============================================================ async function openCenterForm(lat, lng, latlng) { L.popup({ maxWidth: 290, closeOnClick: false }) .setLatLng(latlng) .setContent(`

๐Ÿ•Œ Tambah Rumah Ibadah

โณ Mengambil alamat...
`) .openOn(map); const geoData = await reverseGeocode(lat, lng); const loadEl = document.getElementById('fpAddrLoading'); const addrEl = document.getElementById('fpAddr'); if (loadEl) loadEl.style.display = 'none'; if (addrEl) { addrEl.style.display = 'block'; addrEl.value = geoData.full; } } function updateCenterFormHeader() { const type = document.getElementById('fpType')?.value || 'masjid'; const info = CENTER_TYPE_MAP[type] || CENTER_TYPE_MAP['default']; const h3 = document.getElementById('fpHeader'); if (h3) h3.textContent = info.emoji + ' Tambah Rumah Ibadah'; } async function submitCenter(lat, lng) { const name = document.getElementById('fpName')?.value?.trim(); const address = document.getElementById('fpAddr')?.value?.trim() || `${lat.toFixed(5)}, ${lng.toFixed(5)}`; const kas = parseFloat(document.getElementById('fpKas')?.value) || 0; const radius = parseInt(document.getElementById('fpRadius')?.value) || DEFAULT_RADIUS; if (!name) { highlightError('fpName','โš  Nama wajib diisi!'); return; } map.closePopup(); addCenter({ name, address, kas, lat, lng, radius }); } // ============================================================ // HOUSE PIN // ============================================================ async function placeHousePin(lat, lng) { const id = nextHouseId++; const marker = L.marker([lat, lng], { icon: createHouseIcon(null, false), zIndexOffset: 100 }).addTo(map); const house = { id, lat, lng, address: 'Mengambil alamat...', rt:'', rw:'', kelurahan:'', statusMiskin:'', jumlahAnggota:0, anggota:[], aidStatus:'outside', hasData:false, marker, _geoData:null }; houses.push(house); marker.bindPopup(() => buildHouseMapPopup(house), { maxWidth: 270 }); marker.on('popupopen', () => marker.setPopupContent(buildHouseMapPopup(house))); marker.openPopup(); try { const geo = await reverseGeocode(lat, lng); house.address = geo.full || `${lat.toFixed(5)}, ${lng.toFixed(5)}`; house._geoData = geo; } catch { house.address = `${lat.toFixed(5)}, ${lng.toFixed(5)}`; } marker.setPopupContent(buildHouseMapPopup(house)); updateStats(); updateHouseList(); saveHouseToBackend(house); } // ============================================================ // HOUSE DATA MODAL // ============================================================ async function openHouseModal(houseId) { if (!can('canEdit')) { alert('Role Anda tidak dapat mengedit data.'); return; } const house = houses.find(h => h.id === houseId); if (!house) return; map.closePopup(); if (house._geo) { house._geoData = await house._geo; house._geo = null; } const geo = house._geoData || {}; const covering = getCoveringCenters(house); const nearestR = findNearestCenter(house); const jumlah = house.jumlahAnggota || 0; const centersHtml = covering.length > 0 ? covering.map(c => { const info = CENTER_TYPE_MAP[getCenterType(c.name)] || CENTER_TYPE_MAP['default']; return `${info.emoji} ${c.name}`; }).join(' ') : `Tidak ada (luar radius) โ€” Terdekat: ${nearestR.center ? nearestR.center.name + ' (' + nearestR.distance + ' m)' : 'โ€”'}`; document.getElementById('houseModalBody').innerHTML = ` `; if (jumlah > 0) renderMemberForms(houseId); if (house.statusMiskin) setTimeout(() => selectStatusMiskin(house.statusMiskin), 0); document.getElementById('houseModal').classList.remove('hidden'); const modal = document.getElementById('houseModal'); const oldBar = modal.querySelector('.modal-actions'); if (oldBar) oldBar.remove(); const actBar = document.createElement('div'); actBar.className = 'modal-actions'; actBar.innerHTML = ` `; modal.querySelector('.modal-box').appendChild(actBar); } function selectStatusMiskin(val) { document.querySelectorAll('.status-pill').forEach(el => el.classList.remove('selected','sangat_miskin','miskin','tidak_miskin')); const target = document.querySelector(`.status-pill[data-val="${val}"]`); if (target) target.classList.add('selected', val); const hidden = document.getElementById('hm_statusMiskin'); if (hidden) hidden.value = val; } function renderMemberForms(houseId) { const house = houses.find(h => h.id === houseId); const jumlah = parseInt(document.getElementById('hm_jumlah')?.value) || 0; const container = document.getElementById('memberFormsContainer'); if (!container) return; container.innerHTML = ''; for (let i = 0; i < jumlah; i++) { container.insertAdjacentHTML('beforeend', buildMemberForm(i, house?.anggota?.[i] || {})); } } function buildMemberForm(i, data = {}) { const statusOptions = ['Kepala Keluarga','Istri/Suami','Anak','Orang Tua','Saudara','Lainnya'] .map(s => ``).join(''); const pekerjaanOpts = ['Tidak Bekerja','Pelajar/Mahasiswa','Buruh/Karyawan','Wiraswasta','PNS/TNI/Polri','Petani/Nelayan','Lainnya'] .map(p => ``).join(''); const showSalary = data.pekerjaan && !['Tidak Bekerja','Pelajar/Mahasiswa'].includes(data.pekerjaan); // Parse existing date const existingDate = data.tglLahir || ''; const dateParts = existingDate ? existingDate.split('-') : []; const displayDate = existingDate ? formatDate(existingDate) : ''; return `
${i+1}
Anggota ke-${i+1}
`; } function toggleSalary(i) { const pek = document.getElementById(`am_pekerjaan_${i}`)?.value; const row = document.getElementById(`salaryRow_${i}`); if (!row) return; row.classList.toggle('visible', pek && !['Tidak Bekerja','Pelajar/Mahasiswa'].includes(pek)); } // ============================================================ // CUSTOM DATE PICKER // ============================================================ let _dpTarget = null; // index of current anggota let _dpYear = new Date().getFullYear(); let _dpMonth = new Date().getMonth(); // 0-based let _dpDay = null; let _dpActiveTab = 'day'; // 'day' | 'month' | 'year' let _dpYearRangeStart = Math.floor(new Date().getFullYear() / 12) * 12; const MONTHS_ID = ['Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember']; const DAYS_ID = ['Min','Sen','Sel','Rab','Kam','Jum','Sab']; function openDatepicker(idx) { _dpTarget = idx; // Read existing value const existing = document.getElementById(`am_lahir_${idx}`)?.value; if (existing) { const [y, m, d] = existing.split('-').map(Number); _dpYear = y; _dpMonth = m - 1; _dpDay = d; } else { _dpYear = new Date().getFullYear() - 25; _dpMonth = 0; _dpDay = null; } _dpActiveTab = 'day'; renderDatepicker(); document.getElementById('datepickerOverlay').classList.remove('hidden'); } function closeDatepicker() { document.getElementById('datepickerOverlay').classList.add('hidden'); _dpTarget = null; } function closeDatepickerIfOutside(e) { if (e.target === document.getElementById('datepickerOverlay')) closeDatepicker(); } function renderDatepicker() { const box = document.getElementById('datepickerBox'); const displayVal = _dpDay ? `${String(_dpDay).padStart(2,'0')} ${MONTHS_ID[_dpMonth]} ${_dpYear}` : 'Pilih Tanggal'; let bodyHtml = ''; if (_dpActiveTab === 'day') { bodyHtml = renderDayGrid(); } else if (_dpActiveTab === 'month') { bodyHtml = renderMonthGrid(); } else { bodyHtml = renderYearGrid(); } box.innerHTML = `
Tanggal Lahir
${displayVal}
Tanggal
Bulan
Tahun
${bodyHtml}
`; } function switchDpTab(tab) { _dpActiveTab = tab; if (tab === 'year') _dpYearRangeStart = Math.floor(_dpYear / 12) * 12; renderDatepicker(); } function renderDayGrid() { const today = new Date(); const firstDay = new Date(_dpYear, _dpMonth, 1).getDay(); const daysInMonth = new Date(_dpYear, _dpMonth + 1, 0).getDate(); let html = `
${MONTHS_ID[_dpMonth]} ${_dpYear}
`; DAYS_ID.forEach(d => { html += `
${d}
`; }); // Empty cells before first day for (let i = 0; i < firstDay; i++) html += `
`; for (let d = 1; d <= daysInMonth; d++) { const isSelected = d === _dpDay; const isToday = d === today.getDate() && _dpMonth === today.getMonth() && _dpYear === today.getFullYear(); html += `
${d}
`; } html += '
'; return html; } function renderMonthGrid() { let html = '
'; MONTHS_ID.forEach((m, i) => { const isSelected = i === _dpMonth; html += `
${m.slice(0,3)}
`; }); html += '
'; return html; } function renderYearGrid() { const endYear = _dpYearRangeStart + 11; let html = `
${_dpYearRangeStart} โ€“ ${endYear}
`; for (let y = _dpYearRangeStart; y <= endYear; y++) { const isSelected = y === _dpYear; html += `
${y}
`; } html += '
'; return html; } function dpPrevMonth() { _dpMonth--; if (_dpMonth < 0) { _dpMonth = 11; _dpYear--; } renderDatepicker(); } function dpNextMonth() { _dpMonth++; if (_dpMonth > 11) { _dpMonth = 0; _dpYear++; } renderDatepicker(); } function dpSelectDay(d) { _dpDay = d; renderDatepicker(); } function dpSelectMonth(m) { _dpMonth = m; _dpActiveTab = 'day'; renderDatepicker(); } function dpSelectYear(y) { _dpYear = y; _dpActiveTab = 'month'; renderDatepicker(); } function dpPrevYearRange() { _dpYearRangeStart -= 12; renderDatepicker(); } function dpNextYearRange() { _dpYearRangeStart += 12; renderDatepicker(); } function confirmDatepicker() { if (_dpTarget === null || !_dpDay) { alert('Pilih tanggal terlebih dahulu!'); return; } const dateStr = `${_dpYear}-${String(_dpMonth+1).padStart(2,'0')}-${String(_dpDay).padStart(2,'0')}`; const hidden = document.getElementById(`am_lahir_${_dpTarget}`); const display = document.getElementById(`am_lahir_display_${_dpTarget}`); if (hidden) hidden.value = dateStr; if (display) { display.textContent = formatDate(dateStr); display.classList.remove('date-trigger-placeholder'); } closeDatepicker(); } // ============================================================ // SAVE HOUSE DATA // ============================================================ function saveHouseData(houseId) { const house = houses.find(h => h.id === houseId); if (!house) return; const rt = document.getElementById('hm_rt')?.value?.trim(); const rw = document.getElementById('hm_rw')?.value?.trim(); const kelurahan = document.getElementById('hm_kel')?.value?.trim(); const statusMiskin = document.getElementById('hm_statusMiskin')?.value; const jumlah = parseInt(document.getElementById('hm_jumlah')?.value) || 0; if (!statusMiskin) { alert('Pilih status kemiskinan!'); return; } if (jumlah < 1) { alert('Jumlah anggota minimal 1!'); return; } const anggota = []; for (let i = 0; i < jumlah; i++) { const nama = document.getElementById(`am_nama_${i}`)?.value?.trim(); if (!nama) { alert(`Nama anggota ke-${i+1} wajib diisi!`); return; } anggota.push({ nama, nik: document.getElementById(`am_nik_${i}`)?.value?.trim(), tglLahir: document.getElementById(`am_lahir_${i}`)?.value, statusAnggota: document.getElementById(`am_status_${i}`)?.value, pekerjaan: document.getElementById(`am_pekerjaan_${i}`)?.value, gaji: parseFloat(document.getElementById(`am_gaji_${i}`)?.value) || 0 }); } house.rt = rt; house.rw = rw; house.kelurahan = kelurahan; house.statusMiskin = statusMiskin; house.jumlahAnggota = jumlah; house.anggota = anggota; house.hasData = true; const aidCalc = getAidStatus(house); if (house.aidStatus !== 'helped') house.aidStatus = aidCalc; house.marker.setIcon(createHouseIcon(house.aidStatus, true)); house.marker.setPopupContent(buildHouseMapPopup(house)); closeHouseModal(); updateSidebar(); saveHouseDataToBackend(house); } function closeHouseModal() { document.getElementById('houseModal').classList.add('hidden'); } // ============================================================ // DETAIL MODAL // ============================================================ function openDetailModal(houseId) { const house = houses.find(h => h.id === houseId); if (!house) return; map.closePopup(); const covering = getCoveringCenters(house); const aidStatus = getAidStatus(house); const miskinLabels = { sangat_miskin:'๐Ÿ˜ข Sangat Miskin', miskin:'๐Ÿ˜Ÿ Miskin', tidak_miskin:'๐Ÿ˜Š Tidak Miskin' }; const centersHtml = covering.length ? covering.map(c => { const info = CENTER_TYPE_MAP[getCenterType(c.name)]||CENTER_TYPE_MAP['default']; return `${info.emoji} ${c.name}`; }).join(' ยท ') : 'Tidak ada (luar radius)'; const membersHtml = (house.anggota || []).map((a, i) => { const age = calcAge(a.tglLahir); const showGaji = a.gaji && !['Tidak Bekerja','Pelajar/Mahasiswa'].includes(a.pekerjaan); return `
${i+1}. ${a.nama} (${a.statusAnggota})
NIK${a.nik||'โ€”'}
Tanggal Lahir${formatDate(a.tglLahir)}${age!==null?' ยท '+age+' thn':''}
Pekerjaan${a.pekerjaan||'โ€”'}
${showGaji?`
Penghasilan${formatRupiah(a.gaji)}/bln
`:''}
`; }).join(''); const aidBtnHtml = house.hasData && can('canEdit') ? (aidStatus==='helped' ? `` : aidStatus==='not_helped' ? `` : '') : ''; const editBtn = can('canEdit') ? `` : ''; document.getElementById('detailModalBody').innerHTML = `
๐Ÿ  Titik Rumah #${house.id}
๐Ÿ“ ${house.address}
${house.rt?`
RT ${house.rt} / RW ${house.rw} ยท ${house.kelurahan}
`:''}
๐Ÿ•Œ Menangani: ${centersHtml}
${miskinLabels[house.statusMiskin]||'โ€”'}
${membersHtml||'
Belum ada data anggota.
'}
Status Bantuan: ${aidStatus==='helped'?'๐ŸŸก Sudah Dibantu':aidStatus==='not_helped'?'๐Ÿ”ด Belum Dibantu':'๐ŸŸข Luar Radius'} ${aidBtnHtml}${editBtn}
`; document.getElementById('detailModal').classList.remove('hidden'); } function closeDetailModal() { document.getElementById('detailModal').classList.add('hidden'); } function toggleAid(houseId, action) { if (!can('canEdit')) return; const house = houses.find(h => h.id === houseId); if (!house) return; if (action === 'help') house.aidStatus = 'helped'; else house.aidStatus = getAidStatus({ ...house, aidStatus: 'outside' }); house.marker.setIcon(createHouseIcon(house.aidStatus, house.hasData)); house.marker.setPopupContent(buildHouseMapPopup(house)); updateStats(); updateHouseList(); const { center } = findNearestCenter(house); updateHouseStatusBackend(house, center); openDetailModal(houseId); } // ============================================================ // ADD CENTER // ============================================================ function addCenter(data) { const id = data.id || nextCenterId++; const circle = L.circle([data.lat, data.lng], { radius: data.radius, color:'#3b82f6', fillColor:'#3b82f6', fillOpacity:.07, weight:2, dashArray:'6 4' }).addTo(map); const marker = L.marker([data.lat, data.lng], { icon: createCenterIcon(data.name), draggable: can('canEdit'), zIndexOffset:1000 }).addTo(map); const centerObj = { id, name:data.name, address:data.address, kas:data.kas||0, lat:data.lat, lng:data.lng, radius:data.radius, marker, circle, handle:null }; centers.push(centerObj); marker.bindPopup(buildCenterPopup(centerObj), { maxWidth:300 }); marker.on('popupopen', () => marker.setPopupContent(buildCenterPopup(centerObj))); marker.on('drag', function(e) { if (!can('canEdit')) return; const pos = e.target.getLatLng(); centerObj.lat = pos.lat; centerObj.lng = pos.lng; circle.setLatLng(pos); if (centerObj.handle) centerObj.handle.setLatLng(radiusPoint(pos.lat, pos.lng, centerObj.radius)); updateAllHouseIcons(); updateSidebar(); }); marker.on('dragend', function() { if (!can('canEdit')) return; addRadiusHandle(centerObj); updateCenterPositionBackend(centerObj); }); addRadiusHandle(centerObj); updateAllHouseIcons(); updateSidebar(); // Only save to backend if it's a new center (no existing id) if (!data.id) { saveCenterToBackend(centerObj); } } // ============================================================ // DELETE // ============================================================ function deleteCenter(id) { if (!can('canDelete')) { alert('Role Anda tidak bisa menghapus data.'); return; } if (!confirm('Hapus rumah ibadah ini?')) return; const idx = centers.findIndex(c => c.id === id); if (idx === -1) return; const c = centers[idx]; map.removeLayer(c.marker); map.removeLayer(c.circle); if (c.handle) map.removeLayer(c.handle); centers.splice(idx, 1); map.closePopup(); updateAllHouseIcons(); updateSidebar(); deleteCenterBackend(id); } function deleteHouse(id) { if (!can('canDelete')) { alert('Role Anda tidak bisa menghapus data.'); return; } if (!confirm('Hapus titik rumah ini?')) return; const idx = houses.findIndex(h => h.id === id); if (idx === -1) return; map.removeLayer(houses[idx].marker); map.closePopup(); houses.splice(idx, 1); updateStats(); updateHouseList(); deleteHouseBackend(id); } // ============================================================ // SIDEBAR // ============================================================ function updateSidebar() { updateStats(); updateCenterList(); updateHouseList(); } function updateStats() { const helped = houses.filter(h => h.hasData && h.aidStatus === 'helped').length; const notHelped = houses.filter(h => h.hasData && h.aidStatus === 'not_helped').length; const outside = houses.filter(h => !h.hasData || h.aidStatus === 'outside').length; animateNum('statTotal', houses.length); animateNum('statInside', notHelped); animateNum('statHelped', helped); animateNum('statOutside',outside); } function animateNum(id, target) { const el = document.getElementById(id); if (!el) return; const cur = parseInt(el.textContent) || 0; if (cur === target) return; const step = target > cur ? 1 : -1, steps = Math.abs(target - cur); let cnt = 0; const iv = setInterval(() => { el.textContent = parseInt(el.textContent) + step; cnt++; if (cnt >= steps) clearInterval(iv); }, Math.max(15, 180/steps)); } function filterCenters() { const q = document.getElementById('searchCenter')?.value?.toLowerCase() || ''; const sort = document.getElementById('sortCenter')?.value || ''; let list = [...centers]; if (q) list = list.filter(c => c.name.toLowerCase().includes(q)); if (sort === 'kas_desc') list.sort((a,b) => b.kas - a.kas); else if (sort === 'kas_asc') list.sort((a,b) => a.kas - b.kas); else if (sort === 'tanggungan_desc') list.sort((a,b) => getCoveredJiwa(b) - getCoveredJiwa(a)); else if (sort === 'tanggungan_asc') list.sort((a,b) => getCoveredJiwa(a) - getCoveredJiwa(b)); renderCenterList(list); } function getCoveredJiwa(c) { return houses.filter(h => haversineDistance(h.lat,h.lng,c.lat,c.lng) <= c.radius).reduce((s,h) => s+h.jumlahAnggota, 0); } function updateCenterList() { filterCenters(); } function renderCenterList(list) { const el = document.getElementById('centerList'); document.getElementById('centerCount').textContent = centers.length; if (list.length === 0) { el.innerHTML = '
Tidak ada data.
'; return; } el.innerHTML = ''; list.forEach(c => { const type = getCenterType(c.name); const emoji = (CENTER_TYPE_MAP[type] || CENTER_TYPE_MAP['default']).emoji; const jiwa = getCoveredJiwa(c); const div = document.createElement('div'); div.className = 'center-item'; div.innerHTML = `
${emoji} ${c.name}
๐Ÿ“ ${c.address.slice(0,42)}${c.address.length>42?'โ€ฆ':''}
๐Ÿ’ฐ ${formatRupiah(c.kas)}
โญ• ${c.radius} m ยท ๐Ÿ‘ฅ ${jiwa} jiwa
${can('canDelete') ? `` : ''}`; div.addEventListener('click', () => { map.setView([c.lat,c.lng],16); c.marker.openPopup(); }); el.appendChild(div); }); } function filterHouses() { const q = document.getElementById('searchHouse')?.value?.toLowerCase() || ''; const statusFil = document.getElementById('filterStatus')?.value || ''; const miskinFil = document.getElementById('filterMiskin')?.value || ''; let list = [...houses]; if (q) list = list.filter(h => (h.anggota?.[0]?.nama || '').toLowerCase().includes(q) || (h.address||'').toLowerCase().includes(q)); if (statusFil) list = list.filter(h => { if (statusFil === 'yellow') return h.aidStatus === 'helped'; if (statusFil === 'red') return h.aidStatus === 'not_helped'; if (statusFil === 'green') return h.aidStatus === 'outside'; return true; }); if (miskinFil) list = list.filter(h => h.statusMiskin === miskinFil); renderHouseList(list); } function updateHouseList() { filterHouses(); } function renderHouseList(list) { const el = document.getElementById('houseList'); document.getElementById('houseCount').textContent = houses.length; if (list.length === 0) { el.innerHTML = '
Tidak ada data.
'; return; } const statusLabel = { helped:'๐ŸŸก Sudah dibantu', not_helped:'๐Ÿ”ด Belum dibantu', outside:'๐ŸŸข Luar radius' }; const miskinLabel = { sangat_miskin:'๐Ÿ˜ข Sangat Miskin', miskin:'๐Ÿ˜Ÿ Miskin', tidak_miskin:'๐Ÿ˜Š Tidak Miskin' }; el.innerHTML = ''; list.forEach(h => { const kk = h.anggota?.[0]?.nama || (h.hasData ? 'Data tidak lengkap' : 'Belum ada data'); const aidS = h.hasData ? (h.aidStatus||'outside') : 'outside'; const div = document.createElement('div'); div.className = `house-item status-${aidS==='helped'?'yellow':aidS==='not_helped'?'red':'green'}`; div.innerHTML = `
๐Ÿ  ${kk}
${h.rt?'RT '+h.rt+' RW '+h.rw+' ยท ':''}${h.kelurahan||''} ยท ${h.jumlahAnggota||0} jiwa
${statusLabel[aidS]||'๐ŸŸข Luar radius'}${h.statusMiskin?' ยท '+(miskinLabel[h.statusMiskin]||''):''}
${can('canDelete') ? `` : ''}`; div.addEventListener('click', () => { map.setView([h.lat,h.lng],17); if (h.hasData) openDetailModal(h.id); else h.marker.openPopup(); }); el.appendChild(div); }); } // ============================================================ // RESET // ============================================================ function confirmReset() { if (!can('canReset')) { alert('Role Anda tidak memiliki akses reset.'); return; } if (!confirm('Reset semua data?\nAksi ini tidak dapat dibatalkan.')) return; centers.forEach(c => { map.removeLayer(c.marker); map.removeLayer(c.circle); if(c.handle) map.removeLayer(c.handle); }); centers = []; houses.forEach(h => map.removeLayer(h.marker)); houses = []; reports = []; nextCenterId = nextHouseId = 1; renderReportList(); updateReportBadge(); updateSidebar(); fetch('reset.php').then(r=>r.json()).catch(()=>{}); } // ============================================================ // PELAPORAN // ============================================================ function previewImage(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { document.getElementById('previewImg').src = e.target.result; document.getElementById('imgPreview').style.display = 'block'; document.getElementById('uploadArea').style.display = 'none'; }; reader.readAsDataURL(file); } function removeImage() { document.getElementById('reportImg').value = ''; document.getElementById('imgPreview').style.display = 'none'; document.getElementById('uploadArea').style.display = 'block'; } function submitReport() { if (!can('canReport')) { alert('Role Anda tidak dapat mengirim laporan.'); return; } const text = document.getElementById('reportText')?.value?.trim(); const name = document.getElementById('reportName')?.value?.trim() || 'Anonim'; const location = document.getElementById('reportLocation')?.value?.trim() || ''; if (!text) { alert('Isi deskripsi laporan!'); return; } const imgEl = document.getElementById('previewImg'); const imgSrc = imgEl?.src && document.getElementById('imgPreview').style.display!=='none' ? imgEl.src : null; const report = { id: Date.now(), name, text, location, imgBase64: imgSrc, time: new Date().toLocaleString('id-ID'), status: 'baru' }; reports.unshift(report); document.getElementById('reportText').value = ''; document.getElementById('reportName').value = ''; document.getElementById('reportLocation').value = ''; removeImage(); renderReportList(); updateReportBadge(); saveReportToBackend(report); alert('โœ… Laporan berhasil dikirim! Terima kasih.'); } function updateReportBadge() { const badge = document.getElementById('navReportBadge'); const newCount = reports.filter(r => r.status === 'baru').length; if (badge) { badge.textContent = newCount; badge.classList.toggle('hidden', newCount === 0); } } function filterReports() { const q = (document.getElementById('searchReport')?.value || '').toLowerCase(); const status = document.getElementById('filterReportStatus')?.value || ''; let filtered = [...reports]; if (q) { filtered = filtered.filter(r => (r.name || '').toLowerCase().includes(q) || (r.text || '').toLowerCase().includes(q) || (r.location || '').toLowerCase().includes(q) ); } if (status) { filtered = filtered.filter(r => (r.status || 'baru') === status); } renderReportCards(filtered); } function renderReportList() { const count = document.getElementById('reportCount'); if (count) count.textContent = reports.length; filterReports(); } function renderReportCards(list) { const container = document.getElementById('reportList'); if (!container) return; if (!list || list.length === 0) { container.innerHTML = '
Belum ada laporan masuk.
'; return; } const statusLabels = { baru: '๐Ÿ†• Baru', ditangani: '๐Ÿ”„ Ditangani', selesai: 'โœ… Selesai' }; container.innerHTML = list.map(r => { const statusClass = r.status || 'baru'; const hasImg = r.imgBase64 ? ' report-card-has-img' : ''; return `
๐Ÿ‘ค ${r.name || 'Anonim'}
๐Ÿ• ${r.time || ''}
${r.text || ''}
${r.location ? `
๐Ÿ“ ${r.location}
` : ''}
`; }).join(''); } // ============================================================ // REPORT DETAIL MODAL // ============================================================ function openReportDetail(reportId) { const report = reports.find(r => r.id === reportId); if (!report) return; const statusLabels = { baru: '๐Ÿ†• Baru', ditangani: '๐Ÿ”„ Ditangani', selesai: 'โœ… Selesai' }; const statusClass = report.status || 'baru'; const statusBtns = can('canViewReport') ? `
` : ''; document.getElementById('reportDetailBody').innerHTML = `
Pelapor ${report.name || 'Anonim'}
Waktu ${report.time || 'โ€”'}
${report.location ? `
Lokasi ${report.location}
` : ''}
Status ${statusLabels[statusClass] || '๐Ÿ†• Baru'}
Deskripsi Laporan
${report.text || 'โ€”'}
${report.imgBase64 ? `Foto laporan` : ''} ${statusBtns}`; document.getElementById('reportDetailModal').classList.remove('hidden'); } function closeReportDetail() { document.getElementById('reportDetailModal').classList.add('hidden'); } function changeReportStatus(reportId, newStatus) { const report = reports.find(r => r.id === reportId); if (!report) return; report.status = newStatus; renderReportList(); updateReportBadge(); // Re-open detail to refresh status buttons openReportDetail(reportId); } // ============================================================ // LOAD FROM BACKEND // ============================================================ function loadFromBackend() { fetch('get_data.php') .then(r => r.json()) .then(data => { if (data.centers) { data.centers.forEach(c => { addCenter({ id:c.id, name:c.name, address:c.address, kas:parseFloat(c.kas||0), lat:parseFloat(c.latitude), lng:parseFloat(c.longitude), radius:parseInt(c.radius) }); if (c.id >= nextCenterId) nextCenterId = c.id + 1; }); } if (data.houses) { data.houses.forEach(h => { const id = h.id; const marker = L.marker([parseFloat(h.latitude), parseFloat(h.longitude)], { icon: createHouseIcon(h.aid_status||'outside', !!h.has_data), zIndexOffset:100 }).addTo(map); const house = { id, lat:parseFloat(h.latitude), lng:parseFloat(h.longitude), address:h.address||'', rt:h.rt||'', rw:h.rw||'', kelurahan:h.kelurahan||'', statusMiskin:h.status_miskin||'', jumlahAnggota:parseInt(h.jumlah_anggota)||0, anggota: Array.isArray(h.anggota) ? h.anggota : (typeof h.anggota==='string' ? JSON.parse(h.anggota||'[]') : []), aidStatus:h.aid_status||'outside', hasData:!!h.has_data, marker, _geoData:null }; houses.push(house); marker.bindPopup(() => buildHouseMapPopup(house), { maxWidth:270 }); marker.on('popupopen', () => marker.setPopupContent(buildHouseMapPopup(house))); if (id >= nextHouseId) nextHouseId = id + 1; }); } if (data.reports) { data.reports.forEach(r => reports.push(r)); renderReportList(); } updateAllHouseIcons(); updateSidebar(); }) .catch(() => console.log('Backend tidak tersedia โ€” mode lokal.')); } // ============================================================ // BACKEND CALLS // ============================================================ function saveCenterToBackend(c) { fetch('simpan_pusat.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`name=${encodeURIComponent(c.name)}&address=${encodeURIComponent(c.address)}&kas=${c.kas}&lat=${c.lat}&lng=${c.lng}&radius=${c.radius}` }) .then(r=>r.json()).then(d=>{ if(d.id){ c.id=d.id; if(d.id>=nextCenterId) nextCenterId=d.id+1; updateCenterList(); } }).catch(()=>{}); } function updateCenterPositionBackend(c) { fetch('simpan_pusat.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`id=${c.id}&name=${encodeURIComponent(c.name)}&address=${encodeURIComponent(c.address)}&kas=${c.kas}&lat=${c.lat}&lng=${c.lng}&radius=${c.radius}&update=1` }).catch(()=>{}); } function saveRadiusToBackend(c) { fetch('update_radius.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`id=${c.id}&radius=${c.radius}` }).catch(()=>{}); } function saveHouseToBackend(h) { fetch('simpan_rumah.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`lat=${h.lat}&lng=${h.lng}&address=${encodeURIComponent(h.address||'')}&has_data=0` }) .then(r=>r.json()).then(d=>{ if(d.id&&d.id!==h.id){ h.id=d.id; if(d.id>=nextHouseId) nextHouseId=d.id+1; updateHouseList(); } }).catch(()=>{}); } function saveHouseDataToBackend(h) { const body = [ `id=${h.id}`,`lat=${h.lat}`,`lng=${h.lng}`, `address=${encodeURIComponent(h.address||'')}`, `rt=${encodeURIComponent(h.rt||'')}`,`rw=${encodeURIComponent(h.rw||'')}`, `kelurahan=${encodeURIComponent(h.kelurahan||'')}`, `status_miskin=${encodeURIComponent(h.statusMiskin||'')}`, `jumlah_anggota=${h.jumlahAnggota||0}`, `anggota=${encodeURIComponent(JSON.stringify(h.anggota||[]))}`, `aid_status=${h.aidStatus||'outside'}`,`has_data=1` ].join('&'); fetch('simpan_rumah.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body }).catch(()=>{}); } function updateHouseStatusBackend(h, center) { fetch('update_status.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`house_id=${h.id}&status=${h.aidStatus}¢er_id=${center?center.id:0}` }).catch(()=>{}); } function deleteCenterBackend(id) { fetch('hapus_pusat.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`id=${id}` }).catch(()=>{}); } function deleteHouseBackend(id) { fetch('hapus_rumah.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`id=${id}` }).catch(()=>{}); } function saveReportToBackend(r) { fetch('simpan_laporan.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`name=${encodeURIComponent(r.name)}&text=${encodeURIComponent(r.text)}&img=${encodeURIComponent(r.imgBase64||'')}` }).catch(()=>{}); } function highlightError(id, msg) { const el = document.getElementById(id); if (!el) return; el.style.borderColor = '#dc2626'; el.placeholder = msg; el.focus(); } // ============================================================ // VERIFIKASI AKUN (Admin) // ============================================================ function openVerifyModal() { document.getElementById('verifyModal').classList.remove('hidden'); switchAdminTab('verify'); loadPendingUsers(); loadResetRequests(); } function closeVerifyModal() { document.getElementById('verifyModal').classList.add('hidden'); } function switchAdminTab(tab) { const panelVerify = document.getElementById('adminPanelVerify'); const panelReset = document.getElementById('adminPanelReset'); const tabVerify = document.getElementById('vTabVerify'); const tabReset = document.getElementById('vTabReset'); if (tab === 'verify') { panelVerify.style.display = 'block'; panelReset.style.display = 'none'; tabVerify.style.color = 'var(--primary)'; tabVerify.style.borderBottom = '3px solid var(--primary)'; tabVerify.style.fontWeight = '700'; tabReset.style.color = 'var(--text-muted)'; tabReset.style.borderBottom = '3px solid transparent'; tabReset.style.fontWeight = '600'; } else { panelVerify.style.display = 'none'; panelReset.style.display = 'block'; tabReset.style.color = 'var(--primary)'; tabReset.style.borderBottom = '3px solid var(--primary)'; tabReset.style.fontWeight = '700'; tabVerify.style.color = 'var(--text-muted)'; tabVerify.style.borderBottom = '3px solid transparent'; tabVerify.style.fontWeight = '600'; } } async function loadPendingUsers() { const container = document.getElementById('verifyListContainer'); container.innerHTML = '
Memuat data...
'; try { const res = await fetch('get_pending_users.php'); const data = await res.json(); if (data.success) { if (data.data.length === 0) { container.innerHTML = '
โœ… Tidak ada akun yang menunggu verifikasi.
'; } else { let html = '
'; data.data.forEach(u => { const roleLabel = ROLE_CONFIG[u.role] ? ROLE_CONFIG[u.role].label : u.role; html += `
${u.nama}
${u.email} • ${roleLabel}
๐Ÿ•’ Terdaftar: ${u.waktu}
`; }); html += '
'; container.innerHTML = html; } } else { container.innerHTML = `
Gagal memuat: ${data.message}
`; } } catch (e) { container.innerHTML = `
Error mengambil data.
`; } } async function verifyUserAction(userId, action) { const actionText = action === 'approve' ? 'menyetujui' : 'menolak'; if (!confirm(`Yakin ingin ${actionText} akun ini?`)) return; try { const res = await fetch('verify_user.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `user_id=${userId}&action=${action}` }); const data = await res.json(); if (data.success) { alert(data.message); loadPendingUsers(); } else { alert('Gagal: ' + data.message); } } catch (e) { alert('Terjadi kesalahan koneksi.'); } } async function loadResetRequests() { const container = document.getElementById('resetListContainer'); if (!container) return; container.innerHTML = '
Memuat data...
'; try { const res = await fetch('get_reset_requests.php'); const data = await res.json(); if (data.success) { // Update badge const badge = document.getElementById('resetBadge'); if (badge) { if (data.data.length > 0) { badge.style.display = 'inline'; badge.textContent = data.data.length; } else { badge.style.display = 'none'; } } if (data.data.length === 0) { container.innerHTML = '
โœ… Tidak ada permintaan reset password.
'; } else { let html = '
'; data.data.forEach(r => { const roleLabel = ROLE_CONFIG[r.role] ? ROLE_CONFIG[r.role].label : r.role; html += `
๐Ÿ”‘ ${r.nama}
${r.email} • ${roleLabel}
๐Ÿ•’ Diminta: ${r.waktu}
`; }); html += '
'; container.innerHTML = html; } } else { container.innerHTML = `
Gagal: ${data.message}
`; } } catch (e) { container.innerHTML = `
Error mengambil data.
`; } } function toggleAdminResetPassword(id) { const input = document.getElementById(`newPw_${id}`); const btn = document.getElementById(`togglePw_${id}`); if (input.type === 'password') { input.type = 'text'; btn.textContent = '๐Ÿ”’'; } else { input.type = 'password'; btn.textContent = '๐Ÿ‘'; } } async function adminDoReset(requestId, userId) { const newPw = document.getElementById(`newPw_${requestId}`)?.value || ''; if (newPw.length < 6) { alert('Password baru minimal 6 karakter!'); return; } if (!confirm(`Yakin ingin mereset password untuk user ini?`)) return; try { const res = await fetch('admin_reset_password.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `request_id=${requestId}&user_id=${userId}&new_password=${encodeURIComponent(newPw)}` }); const data = await res.json(); if (data.success) { alert('โœ… ' + data.message); loadResetRequests(); } else { alert('โŒ Gagal: ' + data.message); } } catch (e) { alert('Terjadi kesalahan koneksi.'); } } // ============================================================ // LOGOUT // ============================================================ function doLogout() { if (!confirm('Yakin ingin keluar?')) return; // Redirect langsung ke server โ€” session pasti ter-destroy window.location.href = 'auth_logout.php'; } // ============================================================ // HISTORY PELAPORAN (Masyarakat โ€” read-only) // ============================================================ let historyReports = []; async function loadHistory() { try { const res = await fetch('get_laporan_user.php'); const data = await res.json(); if (data.success && data.reports) { historyReports = data.reports; renderHistory(); } } catch(e) { // If backend not available, show reports from memory historyReports = [...reports]; renderHistory(); } } function renderHistory() { const container = document.getElementById('historyList'); if (!container) return; // Update stats const total = historyReports.length; const baru = historyReports.filter(r => (r.status || 'baru') === 'baru').length; const ditangani = historyReports.filter(r => r.status === 'ditangani').length; const selesai = historyReports.filter(r => r.status === 'selesai').length; const elTotal = document.getElementById('historyTotal'); const elBaru = document.getElementById('historyBaru'); const elDitangani = document.getElementById('historyDitangani'); const elSelesai = document.getElementById('historySelesai'); if (elTotal) elTotal.textContent = total; if (elBaru) elBaru.textContent = baru; if (elDitangani) elDitangani.textContent = ditangani; if (elSelesai) elSelesai.textContent = selesai; filterHistory(); } function filterHistory() { const q = (document.getElementById('searchHistory')?.value || '').toLowerCase(); let filtered = [...historyReports]; if (q) { filtered = filtered.filter(r => (r.name || '').toLowerCase().includes(q) || (r.text || '').toLowerCase().includes(q) ); } renderHistoryCards(filtered); } function renderHistoryCards(list) { const container = document.getElementById('historyList'); if (!container) return; if (!list || list.length === 0) { container.innerHTML = '
๐Ÿ“ญ Tidak ada laporan yang ditemukan.
'; return; } const statusLabels = { baru: '๐Ÿ†• Baru', ditangani: '๐Ÿ”„ Ditangani', selesai: 'โœ… Selesai' }; container.innerHTML = list.map(r => { const statusClass = r.status || 'baru'; return `
๐Ÿ‘ค ${r.name || 'Anonim'}
๐Ÿ• ${r.time || ''}
${r.text || ''}
${r.imgBase64 ? `Foto laporan` : ''}
`; }).join(''); } // ============================================================ // INIT // ============================================================ (async function init() { const isAuth = await checkAuth(); if (!isAuth) return; // Will redirect to login // Set role from session setRole(currentRole); loadFromBackend(); updateStats(); applyRoleUI(); updateReportBadge(); })();