/**
* 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 `
`;
}
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') ? `
โ๏ธ Edit
` : '';
const kasFormHtml = can('canEditKas') ? `
` : '';
const editBtn = can('canEdit')
? `
โ๏ธ Edit Rumah Ibadah
`
: '';
const deleteBtn = can('canDelete')
? `
๐ Hapus Rumah Ibadah
`
: '';
return `
${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(`
`)
.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 = `
๐ Informasi Lokasi (Otomatis)
Alamat Jalan ${geo.road || house.address.split(',')[0] || 'โ'}
Kelurahan / Desa ${geo.village || 'โ'}
Kecamatan ${geo.subdistrict || 'โ'}
Kota ${geo.city || 'Pontianak'}
๐ Rumah Ibadah yang Menangani
${centersHtml}
๐ Status Kemiskinan
๐ข Sangat Miskin
๐ Miskin
๐ Tidak Miskin
๐จโ๐ฉโ๐ง Anggota Keluarga
Jumlah Anggota Keluarga *
`;
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 = `
Batal
๐พ Simpan Data `;
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 => `${s} `).join('');
const pekerjaanOpts = ['Tidak Bekerja','Pelajar/Mahasiswa','Buruh/Karyawan','Wiraswasta','PNS/TNI/Polri','Petani/Nelayan','Lainnya']
.map(p => `${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 `
Status dalam Keluarga
${statusOptions}
Nama Lengkap *
Pekerjaan
${pekerjaanOpts}
`;
}
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 = `
${bodyHtml}
Batal
โ Pilih Tanggal
`;
}
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 = `
โนโน Sebelumnya
${_dpYearRangeStart} โ ${endYear}
Selanjutnya โบโบ
`;
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'
? `โฉ Batalkan Bantuan `
: aidStatus==='not_helped'
? `โ
Tandai Sudah Dibantu `
: '')
: '';
const editBtn = can('canEdit')
? `โ๏ธ Edit `
: '';
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]||'โ'}
๐จโ๐ฉโ๐ง Anggota Keluarga (${house.jumlahAnggota} jiwa)
${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') ? `
๐ Baru
๐ Ditangani
โ
Selesai
` : '';
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 ? ` ` : ''}
${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}
Tolak
Setujui
`;
});
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 ? `
` : ''}
`;
}).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();
})();