Files
UAS_WEBGIS_DeranggaArayAbim…/sig-03/script.js
T
Araya's Project 6462f2e396 First Commit
2026-06-10 12:12:51 +07:00

1463 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* BantSOSial GIS — script.js
* WebGIS Distribusi Bantuan Sosial
*/
// ============================================================
// CONFIG
// ============================================================
const MAP_CENTER = [-0.0532, 109.3458];
const MAP_ZOOM = 15;
const DEFAULT_RADIUS = 300;
// ============================================================
// ROLE SYSTEM
// ============================================================
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: 'Viewer', icon: '👁',
canAdd: false, canEdit: false, canDelete: false, canReset: false,
canReport: true, canViewReport: false, canViewDetail: true,
canEditKas: false, canDragRadius: false
}
};
function can(action) { return ROLE_CONFIG[currentRole]?.[action] === true; }
function openRoleModal() {
document.getElementById('roleModal').classList.remove('hidden');
document.querySelectorAll('.role-card').forEach(c => c.classList.remove('active-role'));
document.getElementById(`roleCard_${currentRole}`)?.classList.add('active-role');
}
function closeRoleModal() { document.getElementById('roleModal').classList.add('hidden'); }
function setRole(role) {
currentRole = role;
const cfg = ROLE_CONFIG[role];
document.getElementById('roleIcon').textContent = cfg.icon;
document.getElementById('roleLabel').textContent = cfg.label;
document.querySelectorAll('.role-card').forEach(c => c.classList.remove('active-role'));
document.getElementById(`roleCard_${role}`)?.classList.add('active-role');
applyRoleUI();
closeRoleModal();
}
function applyRoleUI() {
const cfg = ROLE_CONFIG[currentRole];
const btnGroup = document.getElementById('actionBtnGroup');
if (btnGroup) btnGroup.style.display = cfg.canAdd ? 'flex' : 'none';
const btnReset = document.getElementById('btnReset');
if (btnReset) btnReset.style.display = cfg.canReset ? 'block' : 'none';
const notice = document.getElementById('viewerNotice');
if (notice) notice.style.display = currentRole === 'viewer' ? 'block' : 'none';
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';
}
updateReportBadge();
centers.forEach(c => { if (c.marker) c.marker.setPopupContent(buildCenterPopup(c)); });
houses.forEach(h => { if (h.marker) h.marker.setPopupContent(buildHouseMapPopup(h)); });
updateSidebar();
centers.forEach(c => {
if (c.handle) {
if (cfg.canDragRadius) c.handle.dragging?.enable();
else c.handle.dragging?.disable();
}
if (c.marker) {
if (cfg.canEdit) c.marker.dragging?.enable();
else c.marker.dragging?.disable();
}
});
}
// ============================================================
// NAVIGATION
// ============================================================
function navigateTo(page) {
const pagePeta = document.getElementById('pagePeta');
const pagePelaporan = document.getElementById('pagePelaporan');
const navHome = document.getElementById('navHome');
const navReport = document.getElementById('navReport');
if (page === 'map') {
pagePeta.classList.add('active-page');
pagePelaporan.classList.remove('active-page');
navHome.classList.add('active');
navReport.classList.remove('active');
setTimeout(() => map.invalidateSize(), 100);
} else {
pagePeta.classList.remove('active-page');
pagePelaporan.classList.add('active-page');
navHome.classList.remove('active');
navReport.classList.add('active');
applyRoleUI();
renderReportList();
}
}
// ============================================================
// MAP INIT
// ============================================================
const map = L.map('map', { zoomControl: false }).setView(MAP_CENTER, MAP_ZOOM);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
}).addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
// ============================================================
// STATE
// ============================================================
let isAddingCenter = false;
let isAddingHouse = false;
let centers = [];
let houses = [];
let reports = [];
// ============================================================
// ICON TYPES
// ============================================================
const CENTER_TYPE_MAP = {
'masjid': { emoji: '🕌', color: '#0d9488' },
'musholla': { emoji: '🕌', color: '#0d9488' },
'surau': { emoji: '🕌', color: '#0d9488' },
'gereja katedral': { emoji: '⛪', color: '#2563eb' },
'gereja katolik': { emoji: '⛪', color: '#2563eb' },
'katedral': { emoji: '⛪', color: '#2563eb' },
'gereja protestan': { emoji: '✝️', color: '#7c3aed' },
'gereja': { emoji: '⛪', color: '#1d4ed8' },
'kapel': { emoji: '✝️', color: '#7c3aed' },
'vihara': { emoji: '🛕', color: '#d97706' },
'klenteng': { emoji: '🛕', color: '#d97706' },
'pura': { emoji: '🛕', color: '#d97706' },
'kuil': { emoji: '🛕', color: '#d97706' },
'sinagog': { emoji: '✡️', color: '#1d4ed8' },
'default': { emoji: '🏛️', color: '#64748b' }
};
function getCenterType(name) {
if (!name) return 'default';
const lower = name.toLowerCase();
const keys = Object.keys(CENTER_TYPE_MAP)
.filter(k => k !== 'default')
.sort((a, b) => b.length - a.length);
for (const k of keys) { if (lower.includes(k)) return k; }
return 'default';
}
function createCenterIcon(name) {
const info = CENTER_TYPE_MAP[getCenterType(name)] || CENTER_TYPE_MAP['default'];
return L.divIcon({
className: '',
html: `<div style="width:38px;height:38px;background:${info.color};
border:3px solid #fff;border-radius:50% 50% 50% 0;transform:rotate(-45deg);
display:flex;align-items:center;justify-content:center;
box-shadow:0 3px 12px rgba(0,0,0,.3);">
<span style="transform:rotate(45deg);font-size:18px;line-height:1">${info.emoji}</span></div>`,
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: `<div style="width:24px;height:24px;background:${c.bg};
border:2.5px solid ${c.border};border-radius:4px 4px 4px 0;
transform:rotate(-45deg);display:flex;align-items:center;justify-content:center;
font-size:11px;line-height:1;box-shadow:0 2px 8px rgba(0,0,0,.25);">
<span style="transform:rotate(45deg)">${c.emoji}</span></div>`,
iconSize: [24, 24], iconAnchor: [6, 24]
});
}
function createHandleIcon() {
return L.divIcon({
className: 'radius-handle',
html: `<div style="width:14px;height:14px;background:white;border:2.5px solid #3b82f6;
border-radius:50%;box-shadow:0 2px 6px rgba(59,130,246,.5);cursor:ew-resize;"></div>`,
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;
const Δλ = (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));
}
function radiusPoint(lat, lng, dist, bearing = 90) {
const R = 6371000;
const φ1 = lat * Math.PI / 180, λ1 = lng * Math.PI / 180;
const 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);
}
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 || '',
postcode: addr.postcode || ''
};
} catch {
return { full: `${lat.toFixed(5)}, ${lng.toFixed(5)}`, road: '', village: '', subdistrict: '', city: '', postcode: '' };
}
}
function formatRupiah(num) {
if (num === null || num === undefined || num === '') return '—';
return 'Rp ' + Number(num).toLocaleString('id-ID');
}
function formatDate(d) {
if (!d) return '—';
const dt = new Date(d);
if (isNaN(dt)) return d;
return 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;
}
// ============================================================
// AID STATUS LOGIC
// ============================================================
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();
}
// ============================================================
// STATS
// ============================================================
function updateStats() {
const total = houses.filter(h => h.hasData).length;
const helped = houses.filter(h => h.aidStatus === 'helped').length;
const inside = houses.filter(h => getAidStatus(h) === 'not_helped').length;
const outside = houses.filter(h => hasData && getAidStatus(h) === 'outside').length;
document.getElementById('statTotal').textContent = houses.length;
document.getElementById('statHelped').textContent = helped;
document.getElementById('statInside').textContent = inside;
document.getElementById('statOutside').textContent = houses.filter(h => h.hasData && getAidStatus(h) === 'outside').length;
}
// ============================================================
// 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');
saveCenterBackend(centerObj, true);
if (centerObj.marker.isPopupOpen()) centerObj.marker.setPopupContent(buildCenterPopup(centerObj));
});
centerObj.handle = handle;
}
// ============================================================
// POPUPS
// ============================================================
function buildHouseMapPopup(house) {
const status = getAidStatus(house);
const badgeCls = !house.hasData ? 'nodata' : status;
const badgeTxt = !house.hasData ? 'Belum ada data'
: status === 'helped' ? 'Sudah Dibantu'
: status === 'not_helped' ? 'Dalam Radius'
: 'Luar Radius';
const kkName = house.anggota?.[0]?.nama || '';
let actionBtns = '';
if (house.hasData) {
actionBtns = `<button class="popup-btn primary" onclick="openDetailModal(${house.id})">📋 Detail</button>`;
if (can('canEdit')) actionBtns += `<button class="popup-btn orange" onclick="openHouseModal(${house.id})">✏️ Edit</button>`;
} else {
if (can('canEdit')) actionBtns = `<button class="popup-btn orange" style="flex:2" onclick="openHouseModal(${house.id})"> Isi Data</button>`;
else actionBtns = `<span style="font-size:11px;color:var(--text-muted);padding:4px">Belum ada data</span>`;
}
const deleteBtn = can('canDelete')
? `<button class="popup-btn" style="background:var(--danger-light);color:var(--danger);border:1px solid #fca5a5;" onclick="deleteHouse(${house.id})">🗑</button>`
: '';
return `<div class="map-popup">
<div class="map-popup-badge ${badgeCls}">● ${badgeTxt}</div>
<div class="map-popup-title">🏠 Rumah #${house.id}${kkName ? ' — ' + kkName : ''}</div>
<div class="map-popup-addr">${house.address || '<em style="color:var(--text-muted)">Mengambil alamat...</em>'}</div>
</div>
<div class="map-popup-actions">${actionBtns}${deleteBtn}</div>`;
}
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 info = CENTER_TYPE_MAP[getCenterType(c.name)] || CENTER_TYPE_MAP['default'];
const kasEditHtml = can('canEditKas') ? `
<button onclick="toggleEditKas(${c.id})" style="margin-left:6px;padding:2px 8px;background:white;border:1px solid #86efac;border-radius:6px;font-size:10px;cursor:pointer;color:#16a34a;font-weight:600;">✏️ Edit</button>` : '';
const kasFormHtml = can('canEditKas') ? `
<div id="kasEditForm_${c.id}" style="display:none;margin-bottom:8px;padding:8px 10px;background:#f0fdf4;border:1px solid #86efac;border-radius:8px;">
<div style="font-size:10px;font-weight:700;color:#16a34a;margin-bottom:6px">Edit Kas</div>
<div style="display:flex;gap:6px;align-items:center;">
<input type="number" id="kasInput_${c.id}" value="${c.kas}" min="0" step="1000"
style="flex:1;padding:6px 8px;border:1.5px solid #86efac;border-radius:6px;font-size:12px;font-weight:600;color:#16a34a;outline:none;"/>
<button onclick="saveKas(${c.id})" style="padding:6px 10px;background:#16a34a;color:white;border:none;border-radius:6px;font-size:11px;font-weight:700;cursor:pointer;">Simpan</button>
<button onclick="toggleEditKas(${c.id})" style="padding:6px 8px;background:white;color:#64748b;border:1px solid #e2e8f0;border-radius:6px;font-size:11px;cursor:pointer;">Batal</button>
</div>
</div>` : '';
const editBtn = can('canEdit')
? `<div style="padding:5px 13px 0;">
<button onclick="openCenterEditModal(${c.id})" style="width:100%;padding:7px;background:#fff7ed;color:var(--orange);border:1px solid #fdba74;border-radius:8px;font-size:11px;font-weight:700;cursor:pointer;">✏️ Edit Rumah Ibadah</button>
</div>` : '';
const deleteBtn = can('canDelete')
? `<div style="padding:6px 13px 10px;">
<button onclick="deleteCenter(${c.id})" style="width:100%;padding:7px;background:var(--danger-light);color:var(--danger);border:1px solid #fca5a5;border-radius:8px;font-size:11px;font-weight:700;cursor:pointer;">🗑 Hapus</button>
</div>` : '';
return `<div class="center-popup">
<div class="center-popup-header">
<div class="center-popup-icon">${info.emoji}</div>
<div class="center-popup-title">${c.name}</div>
</div>
<div class="center-popup-addr">📍 ${c.address || '—'}</div>
<div class="kas-badge" id="kasBadge_${c.id}">
<span class="kas-label">💰 Kas</span>
<span class="kas-value" id="kasVal_${c.id}">${formatRupiah(c.kas)}</span>
${kasEditHtml}
</div>
${kasFormHtml}
<div class="radius-info-box">
<span>⭕ Radius</span><span class="radius-val">${c.radius} m</span>
</div>
<div class="radius-info-box" style="background:#fef9c3;border-color:#fde047;color:#854d0e;">
<span>🏠 Dalam radius</span><span class="radius-val">${covered.length} (${helped} dibantu)</span>
</div>
<div class="radius-drag-hint">${can('canDragRadius') ? '💡 Drag titik biru untuk ubah radius' : '👁 Radius hanya bisa dilihat'}</div>
</div>${editBtn}${deleteBtn}`;
}
// ============================================================
// KAS EDIT (popup inline)
// ============================================================
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();
saveCenterBackend(center, true);
}
// ============================================================
// CENTER EDIT MODAL
// ============================================================
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();
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;
const sel = document.getElementById('ce_type');
const type = getCenterType(c.name);
const match = Array.from(sel.options).find(o => o.value === type);
sel.value = match ? type : 'default';
document.getElementById('centerEditModal').classList.remove('hidden');
setTimeout(() => document.getElementById('ce_name')?.focus(), 100);
}
function closeCenterEditModal() { document.getElementById('centerEditModal').classList.add('hidden'); }
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').focus(); return; }
if (radius < 50) { alert('Radius minimal 50 meter!'); return; }
const center = centers.find(c => c.id === id);
if (!center) return;
center.name = name;
center.address = address;
center.kas = kas;
center.radius = radius;
center.marker.setIcon(createCenterIcon(name));
center.circle.setRadius(radius);
if (center.handle) center.handle.setLatLng(radiusPoint(center.lat, center.lng, radius));
center.marker.setPopupContent(buildCenterPopup(center));
closeCenterEditModal();
updateAllHouseIcons();
updateSidebar();
saveCenterBackend(center, true);
}
// ============================================================
// ADD MODES
// ============================================================
function startAddingCenter() {
if (!can('canAdd')) { alert('Role Anda tidak dapat menambah data.'); return; }
if (isAddingCenter) { cancelModes(); return; }
cancelModes(); isAddingCenter = true; setModeUI('center');
}
function startAddingHouse() {
if (!can('canAdd')) { alert('Role Anda tidak dapat 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 (map popup)
// ============================================================
async function openCenterForm(lat, lng, latlng) {
const popup = L.popup({ maxWidth: 290, closeOnClick: false })
.setLatLng(latlng)
.setContent(`
<div class="form-popup">
<h3>🕌 Tambah Rumah Ibadah</h3>
<div class="form-group">
<label class="form-label">Jenis</label>
<select class="form-input" id="fpType">
<option value="masjid">🕌 Masjid / Musholla</option>
<option value="gereja katedral">⛪ Gereja Katolik</option>
<option value="gereja protestan">✝️ Gereja Protestan</option>
<option value="vihara">🛕 Vihara / Klenteng / Pura</option>
<option value="sinagog">✡️ Sinagog</option>
<option value="default">🏛️ Lainnya</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Nama *</label>
<input class="form-input" id="fpName" type="text" placeholder="Nama rumah ibadah..." autofocus/>
</div>
<div class="form-group">
<label class="form-label">Alamat</label>
<div id="fpAddrLoading" style="font-size:11px;color:var(--text-muted);padding:4px 0">⏳ Mengambil alamat...</div>
<input class="form-input" id="fpAddr" type="text" placeholder="Alamat..." style="display:none"/>
</div>
<div class="form-group">
<label class="form-label">Kas / Dana (Rp)</label>
<input class="form-input" id="fpKas" type="number" placeholder="0" min="0" step="1000"/>
</div>
<div class="form-group">
<label class="form-label">Radius Awal (meter)</label>
<input class="form-input" id="fpRadius" type="number" value="${DEFAULT_RADIUS}" min="50" step="10"/>
</div>
<button class="form-submit" onclick="submitCenter(${lat},${lng})">💾 Simpan</button>
<button class="form-cancel" onclick="map.closePopup()" style="width:100%;margin-top:6px;padding:8px;border:1px solid var(--border);border-radius:9px;background:var(--bg);cursor:pointer;font-size:12px;">Batal</button>
</div>`)
.openOn(map);
const geo = 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 = geo.full; }
}
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) { document.getElementById('fpName')?.focus(); return; }
map.closePopup();
addCenter({ name, address, kas, lat, lng, radius });
}
function addCenter(data) {
const marker = L.marker([data.lat, data.lng], {
icon: createCenterIcon(data.name),
draggable: can('canEdit'),
zIndexOffset: 200
}).addTo(map);
const circle = L.circle([data.lat, data.lng], {
radius: data.radius, color: '#3b82f6', weight: 1.5,
fillColor: '#3b82f6', fillOpacity: 0.07, dashArray: '6 4'
}).addTo(map);
const centerObj = {
id: data.id || null, name: data.name, address: data.address,
kas: data.kas || 0, lat: data.lat, lng: data.lng,
radius: data.radius, marker, circle, handle: null
};
marker.bindPopup(() => buildCenterPopup(centerObj), { maxWidth: 260 });
marker.on('popupopen', () => marker.setPopupContent(buildCenterPopup(centerObj)));
marker.on('dragend', function(e) {
centerObj.lat = e.target.getLatLng().lat;
centerObj.lng = e.target.getLatLng().lng;
circle.setLatLng([centerObj.lat, centerObj.lng]);
if (centerObj.handle) centerObj.handle.setLatLng(radiusPoint(centerObj.lat, centerObj.lng, centerObj.radius));
updateAllHouseIcons(); updateSidebar();
saveCenterBackend(centerObj, true);
});
addRadiusHandle(centerObj);
if (!can('canDragRadius')) centerObj.handle?.dragging?.disable();
if (!can('canEdit')) marker.dragging?.disable();
centers.push(centerObj);
if (!data.id) {
saveCenterBackend(centerObj, false);
} else {
marker.openPopup();
updateAllHouseIcons(); updateSidebar();
}
}
async function saveCenterBackend(centerObj, isUpdate) {
const body = new URLSearchParams({
id: centerObj.id || 0,
name: centerObj.name, address: centerObj.address || '',
kas: centerObj.kas, lat: centerObj.lat, lng: centerObj.lng,
radius: centerObj.radius, update: isUpdate ? '1' : '0'
});
try {
const res = await fetch('simpan_pusat.php', { method: 'POST', body });
const data = await res.json();
if (data.success && !centerObj.id) {
centerObj.id = data.id;
centerObj.marker.setPopupContent(buildCenterPopup(centerObj));
centerObj.marker.openPopup();
updateAllHouseIcons(); updateSidebar();
}
} catch (e) { console.error('saveCenterBackend', e); }
}
// ============================================================
// HOUSE PIN
// ============================================================
async function placeHousePin(lat, lng) {
const marker = L.marker([lat, lng], { icon: createHouseIcon(null, false), zIndexOffset: 100 }).addTo(map);
const house = {
id: null, lat, lng, address: '',
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();
await saveHouseBackend(house, false);
}
async function saveHouseBackend(house, isUpdate) {
const body = new URLSearchParams({
id: house.id || 0,
lat: house.lat,
lng: house.lng,
address: house.address || '',
rt: house.rt || '',
rw: house.rw || '',
kelurahan: house.kelurahan || '',
status_miskin: house.statusMiskin || '',
jumlah_anggota: house.jumlahAnggota || 0,
anggota: JSON.stringify(house.anggota || []),
aid_status: house.aidStatus || 'outside',
has_data: house.hasData ? 1 : 0,
update: isUpdate ? '1' : '0'
});
try {
const res = await fetch('simpan_rumah.php', { method: 'POST', body });
const data = await res.json();
if (data.success && !house.id) {
house.id = data.id;
house.marker.setPopupContent(buildHouseMapPopup(house));
updateHouseList();
}
} catch (e) { console.error('saveHouseBackend', e); }
}
// ============================================================
// HOUSE DATA MODAL
// ============================================================
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();
const covering = getCoveringCenters(house);
const nearestR = findNearestCenter(house);
const geo = house._geoData || {};
const centersHtml = covering.length > 0
? covering.map(c => {
const info = CENTER_TYPE_MAP[getCenterType(c.name)] || CENTER_TYPE_MAP['default'];
return `<span style="display:inline-flex;align-items:center;gap:4px;background:var(--primary-light);color:var(--primary);padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;margin:2px;">${info.emoji} ${c.name}</span>`;
}).join('')
: `<span style="font-size:11px;color:var(--text-muted)">Tidak ada — Terdekat: ${nearestR.center ? nearestR.center.name + ' (' + nearestR.distance + ' m)' : '—'}</span>`;
document.getElementById('houseModalBody').innerHTML = `
<div class="modal-section">
<div class="modal-section-title">📍 Informasi Lokasi</div>
<div class="info-box">
<div class="info-box-row"><span class="info-box-label">Alamat</span><span class="info-box-val">${geo.road || house.address?.split(',')[0] || '—'}</span></div>
<div class="info-box-row"><span class="info-box-label">Kelurahan</span><span class="info-box-val">${geo.village || '—'}</span></div>
<div class="info-box-row"><span class="info-box-label">Kecamatan</span><span class="info-box-val">${geo.subdistrict || '—'}</span></div>
<div class="info-box-row"><span class="info-box-label">Kota</span><span class="info-box-val">${geo.city || '—'}</span></div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">RT</label>
<input class="form-input" id="hm_rt" type="text" placeholder="001" value="${house.rt || ''}"/>
</div>
<div class="form-group">
<label class="form-label">RW</label>
<input class="form-input" id="hm_rw" type="text" placeholder="001" value="${house.rw || ''}"/>
</div>
<div class="form-group" style="flex:2">
<label class="form-label">Kelurahan</label>
<input class="form-input" id="hm_kel" type="text" value="${house.kelurahan || geo.village || ''}"/>
</div>
</div>
</div>
<div class="modal-section">
<div class="modal-section-title">🕌 Rumah Ibadah yang Menangani</div>
<div style="margin-bottom:8px">${centersHtml}</div>
</div>
<div class="modal-section">
<div class="modal-section-title">📊 Status Kemiskinan</div>
<div class="status-pills">
<div class="status-pill" data-val="sangat_miskin" onclick="selectStatusMiskin('sangat_miskin')">😢 Sangat Miskin</div>
<div class="status-pill" data-val="miskin" onclick="selectStatusMiskin('miskin')">😟 Miskin</div>
<div class="status-pill" data-val="tidak_miskin" onclick="selectStatusMiskin('tidak_miskin')">😊 Tidak Miskin</div>
</div>
<input type="hidden" id="hm_statusMiskin" value="${house.statusMiskin || ''}"/>
</div>
<div class="modal-section">
<div class="modal-section-title">👨‍👩‍👧 Anggota Keluarga</div>
<div class="form-group">
<label class="form-label">Jumlah Anggota Keluarga *</label>
<input class="form-input" id="hm_jumlah" type="number" min="1" max="20"
placeholder="Masukkan jumlah..." value="${house.jumlahAnggota || ''}"
onchange="renderMemberForms(${houseId})"/>
</div>
<div id="memberFormsContainer"></div>
</div>`;
if (house.jumlahAnggota > 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 bar = document.createElement('div');
bar.className = 'modal-actions';
bar.innerHTML = `
<button class="btn-modal-cancel" onclick="closeHouseModal()">Batal</button>
<button class="btn-modal-save" onclick="saveHouseData(${houseId})">💾 Simpan Data</button>`;
modal.querySelector('.modal-box').appendChild(bar);
}
function closeHouseModal() { document.getElementById('houseModal').classList.add('hidden'); }
function selectStatusMiskin(val) {
document.querySelectorAll('.status-pill').forEach(el => el.classList.remove('selected', 'sangat_miskin', 'miskin', 'tidak_miskin'));
document.querySelector(`.status-pill[data-val="${val}"]`)?.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 < Math.min(jumlah, 20); i++) {
container.insertAdjacentHTML('beforeend', buildMemberForm(i, house?.anggota?.[i] || {}));
}
}
function buildMemberForm(i, data = {}) {
const statusOpts = ['Kepala Keluarga', 'Istri/Suami', 'Anak', 'Orang Tua', 'Saudara', 'Lainnya']
.map(s => `<option ${data.statusAnggota === s ? 'selected' : ''}>${s}</option>`).join('');
const pekerjaanOpts = ['Tidak Bekerja', 'Pelajar/Mahasiswa', 'Buruh/Karyawan', 'Wiraswasta', 'PNS/TNI/Polri', 'Petani/Nelayan', 'Lainnya']
.map(p => `<option ${data.pekerjaan === p ? 'selected' : ''}>${p}</option>`).join('');
const showSalary = data.pekerjaan && !['Tidak Bekerja', 'Pelajar/Mahasiswa'].includes(data.pekerjaan);
const displayDate = data.tglLahir ? formatDate(data.tglLahir) : '';
return `<div class="member-card">
<div class="member-card-header"><div class="member-num">${i + 1}</div> Anggota ke-${i + 1}</div>
<div class="member-card-body">
<div class="form-row">
<div class="form-group" style="flex:2">
<label class="form-label">Nama Lengkap</label>
<input class="form-input" id="m_nama_${i}" type="text" value="${data.nama || ''}" placeholder="Nama..."/>
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select class="form-input" id="m_status_${i}">${statusOpts}</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Tanggal Lahir</label>
<div class="date-input-wrap">
<button type="button" class="date-display-btn" id="m_tgl_btn_${i}"
onclick="openDatepicker('m_tgl_${i}', 'm_tgl_btn_${i}')">${displayDate || '📅 Pilih tanggal'}</button>
<input type="hidden" id="m_tgl_${i}" value="${data.tglLahir || ''}"/>
</div>
</div>
<div class="form-group">
<label class="form-label">Pekerjaan</label>
<select class="form-input" id="m_pkrj_${i}" onchange="toggleSalary(${i})">${pekerjaanOpts}</select>
</div>
</div>
<div class="form-row salary-row${showSalary ? ' visible' : ''}" id="salary_row_${i}">
<div class="form-group" style="width:100%">
<label class="form-label">Penghasilan per Bulan (Rp)</label>
<input class="form-input" id="m_gaji_${i}" type="number" min="0" step="50000" value="${data.gaji || ''}" placeholder="0"/>
</div>
</div>
</div>
</div>`;
}
function toggleSalary(i) {
const pekerjaan = document.getElementById(`m_pkrj_${i}`)?.value;
const row = document.getElementById(`salary_row_${i}`);
if (!row) return;
const hide = ['Tidak Bekerja', 'Pelajar/Mahasiswa'].includes(pekerjaan);
row.classList.toggle('visible', !hide);
}
async function saveHouseData(houseId) {
const house = houses.find(h => h.id === houseId);
if (!house) return;
house.rt = document.getElementById('hm_rt')?.value?.trim() || '';
house.rw = document.getElementById('hm_rw')?.value?.trim() || '';
house.kelurahan = document.getElementById('hm_kel')?.value?.trim() || '';
house.statusMiskin = document.getElementById('hm_statusMiskin')?.value || '';
house.jumlahAnggota = parseInt(document.getElementById('hm_jumlah')?.value) || 0;
house.anggota = [];
for (let i = 0; i < house.jumlahAnggota; i++) {
house.anggota.push({
nama: document.getElementById(`m_nama_${i}`)?.value?.trim() || '',
statusAnggota: document.getElementById(`m_status_${i}`)?.value || '',
tglLahir: document.getElementById(`m_tgl_${i}`)?.value || '',
pekerjaan: document.getElementById(`m_pkrj_${i}`)?.value || '',
gaji: parseFloat(document.getElementById(`m_gaji_${i}`)?.value) || 0
});
}
house.hasData = house.jumlahAnggota > 0 || !!house.statusMiskin;
const status = getAidStatus(house);
if (house.aidStatus !== 'helped') house.aidStatus = status;
house.marker.setIcon(createHouseIcon(house.aidStatus, house.hasData));
house.marker.setPopupContent(buildHouseMapPopup(house));
closeHouseModal();
updateStats(); updateHouseList();
await saveHouseBackend(house, true);
}
// ============================================================
// DETAIL MODAL
// ============================================================
function openDetailModal(houseId) {
const house = houses.find(h => h.id === houseId);
if (!house) return;
map.closePopup();
const covering = getCoveringCenters(house);
const status = getAidStatus(house);
const nearestR = findNearestCenter(house);
const statusLabels = {
sangat_miskin: '😢 Sangat Miskin',
miskin: '😟 Miskin',
tidak_miskin: '😊 Tidak Miskin',
'': '—'
};
const aidLabels = {
helped: '✅ Sudah Dibantu',
not_helped: '⏳ Belum Dibantu',
outside: '🟢 Luar Radius'
};
const membersHtml = house.anggota.length > 0
? `<table class="member-table"><thead><tr>
<th>#</th><th>Nama</th><th>Status</th><th>Usia</th><th>Pekerjaan</th><th>Penghasilan</th>
</tr></thead><tbody>` +
house.anggota.map((m, i) => {
const age = calcAge(m.tglLahir);
return `<tr>
<td>${i + 1}</td>
<td>${m.nama || '—'}</td>
<td>${m.statusAnggota || '—'}</td>
<td>${age !== null ? age + ' th' : '—'}</td>
<td>${m.pekerjaan || '—'}</td>
<td>${m.gaji ? formatRupiah(m.gaji) : '—'}</td>
</tr>`;
}).join('') + `</tbody></table>`
: '<div class="empty-state">Belum ada data anggota keluarga.</div>';
const nearestInfo = nearestR.center
? `${nearestR.center.name} (${nearestR.distance} m)`
: '—';
const aidBtns = can('canEdit') ? `
<div class="aid-btn-row">
<button class="aid-btn${house.aidStatus === 'helped' ? ' active-helped' : ''}" onclick="setAidStatus(${house.id}, 'helped', '${covering[0]?.id || 0}')">✅ Sudah Dibantu</button>
<button class="aid-btn${house.aidStatus === 'not_helped' ? ' active-not_helped' : ''}" onclick="setAidStatus(${house.id}, 'not_helped', '${covering[0]?.id || 0}')">⏳ Belum Dibantu</button>
<button class="aid-btn${house.aidStatus === 'outside' ? ' active-outside' : ''}" onclick="setAidStatus(${house.id}, 'outside', '0')">🟢 Luar Radius</button>
</div>` : '';
document.getElementById('detailModalBody').innerHTML = `
<div class="detail-section">
<div class="detail-section-title">📍 Lokasi</div>
<div class="detail-grid">
<div class="detail-kv"><div class="detail-key">Alamat</div><div class="detail-val">${house.address || '—'}</div></div>
<div class="detail-kv"><div class="detail-key">RT / RW</div><div class="detail-val">${house.rt || '—'} / ${house.rw || '—'}</div></div>
<div class="detail-kv"><div class="detail-key">Kelurahan</div><div class="detail-val">${house.kelurahan || '—'}</div></div>
<div class="detail-kv"><div class="detail-key">Koordinat</div><div class="detail-val" style="font-family:var(--mono);font-size:11px">${house.lat.toFixed(5)}, ${house.lng.toFixed(5)}</div></div>
</div>
</div>
<div class="detail-section">
<div class="detail-section-title">📊 Status Bantuan</div>
<div class="detail-grid">
<div class="detail-kv"><div class="detail-key">Kemiskinan</div><div class="detail-val">${statusLabels[house.statusMiskin] || '—'}</div></div>
<div class="detail-kv"><div class="detail-key">Status Bantuan</div><div class="detail-val">${aidLabels[status] || '—'}</div></div>
<div class="detail-kv"><div class="detail-key">Rumah Ibadah</div><div class="detail-val">${covering.length > 0 ? covering.map(c => c.name).join(', ') : 'Luar radius'}</div></div>
<div class="detail-kv"><div class="detail-key">Terdekat</div><div class="detail-val">${nearestInfo}</div></div>
</div>
${aidBtns}
</div>
<div class="detail-section">
<div class="detail-section-title">👨‍👩‍👧 Anggota Keluarga (${house.jumlahAnggota} orang)</div>
${membersHtml}
</div>`;
document.getElementById('detailModal').classList.remove('hidden');
}
function closeDetailModal() { document.getElementById('detailModal').classList.add('hidden'); }
async function setAidStatus(houseId, newStatus, centerId) {
const house = houses.find(h => h.id === houseId);
if (!house) return;
house.aidStatus = newStatus;
house.marker.setIcon(createHouseIcon(house.aidStatus, house.hasData));
house.marker.setPopupContent(buildHouseMapPopup(house));
updateStats(); updateHouseList();
closeDetailModal();
try {
const body = new URLSearchParams({ house_id: houseId, aid_status: newStatus, center_id: centerId });
await fetch('update_status.php', { method: 'POST', body });
} catch (e) { console.error('setAidStatus', e); }
}
// ============================================================
// DELETE
// ============================================================
function deleteHouse(houseId) {
if (!can('canDelete')) return;
if (!confirm('Hapus data rumah ini?')) return;
const idx = houses.findIndex(h => h.id === houseId);
if (idx === -1) return;
map.removeLayer(houses[idx].marker);
houses.splice(idx, 1);
updateStats(); updateHouseList();
fetch('hapus_rumah.php', { method: 'POST', body: new URLSearchParams({ id: houseId }) });
}
function deleteCenter(centerId) {
if (!can('canDelete')) return;
if (!confirm('Hapus rumah ibadah ini beserta data terkait?')) return;
const idx = centers.findIndex(c => c.id === centerId);
if (idx === -1) return;
map.removeLayer(centers[idx].marker);
map.removeLayer(centers[idx].circle);
if (centers[idx].handle) map.removeLayer(centers[idx].handle);
centers.splice(idx, 1);
map.closePopup();
updateAllHouseIcons(); updateSidebar();
fetch('hapus_pusat.php', { method: 'POST', body: new URLSearchParams({ id: centerId }) });
}
// ============================================================
// RESET
// ============================================================
function confirmReset() {
if (!can('canReset')) return;
if (!confirm('RESET semua data? Tindakan ini tidak dapat dibatalkan!')) return;
centers.forEach(c => {
map.removeLayer(c.marker);
map.removeLayer(c.circle);
if (c.handle) map.removeLayer(c.handle);
});
houses.forEach(h => map.removeLayer(h.marker));
centers = []; houses = []; reports = [];
updateStats(); updateSidebar();
fetch('reset.php', { method: 'POST' });
}
// ============================================================
// SIDEBAR
// ============================================================
function updateSidebar() { updateCenterList(); updateHouseList(); }
function filterCenters() {
const query = document.getElementById('searchCenter')?.value?.toLowerCase() || '';
const sort = document.getElementById('sortCenter')?.value || '';
let list = centers.filter(c => c.name.toLowerCase().includes(query) || (c.address || '').toLowerCase().includes(query));
if (sort === 'kas_desc') list.sort((a, b) => (b.kas || 0) - (a.kas || 0));
else if (sort === 'kas_asc') list.sort((a, b) => (a.kas || 0) - (b.kas || 0));
else if (sort === 'tanggungan_desc') list.sort((a, b) => countCovered(b) - countCovered(a));
else if (sort === 'tanggungan_asc') list.sort((a, b) => countCovered(a) - countCovered(b));
renderCenterList(list);
}
function countCovered(c) {
return houses.filter(h => haversineDistance(h.lat, h.lng, c.lat, c.lng) <= c.radius).length;
}
function updateCenterList() {
document.getElementById('centerCount').textContent = centers.length;
filterCenters();
}
function renderCenterList(list) {
const el = document.getElementById('centerList');
if (list.length === 0) { el.innerHTML = '<div class="empty-state">Belum ada rumah ibadah.</div>'; return; }
el.innerHTML = list.map(c => {
const info = CENTER_TYPE_MAP[getCenterType(c.name)] || CENTER_TYPE_MAP['default'];
const covered = countCovered(c);
const helped = houses.filter(h => haversineDistance(h.lat, h.lng, c.lat, c.lng) <= c.radius && h.aidStatus === 'helped').length;
const delBtn = can('canDelete') ? `<button class="center-item-del" onclick="event.stopPropagation();deleteCenter(${c.id})">🗑</button>` : '';
return `<div class="center-item" onclick="focusCenter(${c.id})">
<div class="center-item-icon">${info.emoji}</div>
<div class="center-item-body">
<div class="center-item-name">${c.name}</div>
<div class="center-item-addr">${c.address || '—'}</div>
<div class="center-item-meta">
<span class="meta-tag">⭕ ${c.radius}m</span>
<span class="meta-tag">🏠 ${covered}</span>
<span class="meta-tag">💰 ${formatRupiah(c.kas)}</span>
${helped > 0 ? `<span class="meta-tag" style="background:#fef9c3;color:#854d0e">✅ ${helped}</span>` : ''}
</div>
</div>
${delBtn}
</div>`;
}).join('');
}
function focusCenter(centerId) {
const c = centers.find(c => c.id === centerId);
if (!c) return;
map.setView([c.lat, c.lng], 16);
c.marker.openPopup();
}
function filterHouses() {
const query = document.getElementById('searchHouse')?.value?.toLowerCase() || '';
const filterStatus = document.getElementById('filterStatus')?.value || '';
const filterMiskin = document.getElementById('filterMiskin')?.value || '';
let list = houses.filter(h => {
const kkName = h.anggota?.[0]?.nama?.toLowerCase() || '';
const addr = (h.address || '').toLowerCase();
const matchQ = !query || kkName.includes(query) || addr.includes(query);
const matchS = !filterStatus || getAidStatus(h) === filterStatus;
const matchM = !filterMiskin || h.statusMiskin === filterMiskin;
return matchQ && matchS && matchM;
});
renderHouseList(list);
}
function updateHouseList() {
document.getElementById('houseCount').textContent = houses.length;
filterHouses();
}
function renderHouseList(list) {
const el = document.getElementById('houseList');
if (list.length === 0) { el.innerHTML = '<div class="empty-state">Belum ada data.</div>'; return; }
el.innerHTML = list.map(h => {
const status = getAidStatus(h);
const kkName = h.anggota?.[0]?.nama || `Rumah #${h.id}`;
const addr = h.kelurahan || h.address?.split(',')[0] || '—';
const icon = h.hasData ? (status === 'helped' ? '🏠' : status === 'not_helped' ? '🏚' : '🏠') : '📍';
const delBtn = can('canDelete') ? `<button class="house-item-del" onclick="event.stopPropagation();deleteHouse(${h.id})">🗑</button>` : '';
const statusLabels = { helped: '✅ Dibantu', not_helped: '⏳ Belum', outside: '🟢 Luar', nodata: '📍 Pin' };
const miskinLabels = { sangat_miskin: '😢 Sangat Miskin', miskin: '😟 Miskin', tidak_miskin: '😊 Tidak Miskin' };
return `<div class="house-item status-${status}" onclick="focusHouse(${h.id})">
<div class="house-item-icon">${icon}</div>
<div class="house-item-body">
<div class="house-item-name">${kkName}</div>
<div class="house-item-addr">${addr}</div>
<div class="house-item-tags">
<span class="tag tag-${h.hasData ? status : 'nodata'}">${statusLabels[h.hasData ? status : 'nodata'] || status}</span>
${h.statusMiskin ? `<span class="tag tag-${h.statusMiskin}">${miskinLabels[h.statusMiskin]}</span>` : ''}
</div>
</div>
${delBtn}
</div>`;
}).join('');
}
function focusHouse(houseId) {
const h = houses.find(h => h.id === houseId);
if (!h) return;
map.setView([h.lat, h.lng], 17);
h.marker.openPopup();
}
// ============================================================
// REPORTS
// ============================================================
function updateReportBadge() {
const badge = document.getElementById('navReportBadge');
const count = can('canViewReport') ? reports.filter(r => r.status === 'baru').length : 0;
if (badge) {
badge.textContent = count;
badge.classList.toggle('hidden', count === 0);
}
}
function filterReports() {
const query = document.getElementById('searchReport')?.value?.toLowerCase() || '';
const status = document.getElementById('filterReportStatus')?.value || '';
const list = reports.filter(r => {
const matchQ = !query || r.text.toLowerCase().includes(query) || (r.name || '').toLowerCase().includes(query);
const matchS = !status || r.status === status;
return matchQ && matchS;
});
renderReportList(list);
}
function renderReportList(list) {
if (!list) list = reports;
document.getElementById('reportCount').textContent = reports.length;
const el = document.getElementById('reportList');
if (list.length === 0) { el.innerHTML = '<div class="empty-state" style="margin-top:20px">Belum ada laporan.</div>'; return; }
el.innerHTML = list.map(r => {
const statusBadge = `<span class="report-status ${r.status}">${r.status === 'baru' ? '🆕 Baru' : r.status === 'ditangani' ? '🔄 Ditangani' : '✅ Selesai'}</span>`;
const img = r.imgBase64 ? `<img src="${r.imgBase64}" style="width:100%;max-height:140px;object-fit:cover;border-radius:6px;margin:0 14px 8px;width:calc(100% - 28px)"/>` : '';
const actions = can('canDelete')
? `<button class="report-action-btn danger" onclick="deleteReport(${r.id})">🗑 Hapus</button>` : '';
const statusBtns = can('canViewReport')
? `<button class="report-action-btn" onclick="changeReportStatus(${r.id}, 'baru')">🆕</button>
<button class="report-action-btn" onclick="changeReportStatus(${r.id}, 'ditangani')">🔄</button>
<button class="report-action-btn" onclick="changeReportStatus(${r.id}, 'selesai')">✅</button>` : '';
return `<div class="report-card">
<div class="report-card-header">
<div><div class="report-card-name">👤 ${r.name || 'Anonim'}</div><div class="report-card-meta">${r.lokasi ? '📍 ' + r.lokasi + ' · ' : ''}${r.time}</div></div>
${statusBadge}
</div>
<div class="report-card-text">${r.text}</div>
${img}
<div class="report-card-footer">
<div class="report-card-time">#${r.id}</div>
<div class="report-card-actions">${statusBtns}${actions}</div>
</div>
</div>`;
}).join('');
}
async function changeReportStatus(reportId, newStatus) {
const r = reports.find(r => r.id === reportId);
if (!r) return;
r.status = newStatus;
filterReports();
updateReportBadge();
try {
await fetch('update_laporan.php', { method: 'POST', body: new URLSearchParams({ id: reportId, status: newStatus }) });
} catch (e) { console.error(e); }
}
async function deleteReport(reportId) {
if (!can('canDelete')) return;
if (!confirm('Hapus laporan ini?')) return;
reports = reports.filter(r => r.id !== reportId);
filterReports();
updateReportBadge();
try {
await fetch('hapus_laporan.php', { method: 'POST', body: new URLSearchParams({ id: reportId }) });
} catch (e) { console.error(e); }
}
// ============================================================
// REPORT SUBMIT
// ============================================================
let reportImgBase64 = '';
function previewImage(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5_000_000) { alert('Ukuran file maksimal 5MB'); return; }
const reader = new FileReader();
reader.onload = e => {
reportImgBase64 = e.target.result;
document.getElementById('previewImg').src = e.target.result;
document.getElementById('imgPreview').style.display = 'block';
document.getElementById('uploadArea').style.display = 'none';
};
reader.readAsDataURL(file);
}
function removeImage() {
reportImgBase64 = '';
document.getElementById('reportImg').value = '';
document.getElementById('imgPreview').style.display = 'none';
document.getElementById('uploadArea').style.display = 'block';
}
async function submitReport() {
const text = document.getElementById('reportText')?.value?.trim();
const name = document.getElementById('reportName')?.value?.trim() || 'Anonim';
const lokasi = document.getElementById('reportLocation')?.value?.trim() || '';
if (!text) { document.getElementById('reportText')?.focus(); alert('Deskripsi laporan tidak boleh kosong'); return; }
try {
const body = new URLSearchParams({ name, text, lokasi, img: reportImgBase64 });
const res = await fetch('simpan_laporan.php', { method: 'POST', body });
const data = await res.json();
if (data.success) {
const newReport = {
id: data.id, name, text, lokasi,
imgBase64: reportImgBase64 || null,
status: 'baru',
time: new Date().toLocaleDateString('id-ID', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
};
reports.unshift(newReport);
document.getElementById('reportText').value = '';
document.getElementById('reportName').value = '';
document.getElementById('reportLocation').value = '';
removeImage();
filterReports();
updateReportBadge();
alert('Laporan berhasil dikirim! Terima kasih.');
} else {
alert('Gagal kirim laporan: ' + (data.message || 'Error'));
}
} catch (e) { console.error(e); alert('Terjadi kesalahan. Coba lagi.'); }
}
// ============================================================
// DATE PICKER
// ============================================================
let _dpTargetInput = null;
let _dpTargetBtn = null;
let _dpYear = new Date().getFullYear();
let _dpMonth = new Date().getMonth();
let _dpSelected = null;
function openDatepicker(inputId, btnId) {
_dpTargetInput = document.getElementById(inputId);
_dpTargetBtn = document.getElementById(btnId);
const existing = _dpTargetInput?.value;
if (existing) {
const d = new Date(existing);
_dpYear = d.getFullYear();
_dpMonth = d.getMonth();
_dpSelected = existing;
} else {
_dpYear = new Date().getFullYear();
_dpMonth = new Date().getMonth();
_dpSelected = null;
}
renderDatepicker();
document.getElementById('datepickerOverlay').classList.remove('hidden');
}
function closeDatepicker() { document.getElementById('datepickerOverlay').classList.add('hidden'); }
function closeDatepickerIfOutside(e) { if (e.target.id === 'datepickerOverlay') closeDatepicker(); }
function renderDatepicker() {
const box = document.getElementById('datepickerBox');
const months = ['Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
const days = ['Min','Sen','Sel','Rab','Kam','Jum','Sab'];
const firstDay = new Date(_dpYear, _dpMonth, 1).getDay();
const daysInMonth = new Date(_dpYear, _dpMonth + 1, 0).getDate();
const today = new Date();
let gridHtml = days.map(d => `<div class="dp-day-label">${d}</div>`).join('');
for (let i = 0; i < firstDay; i++) {
const prevDays = new Date(_dpYear, _dpMonth, 0).getDate();
gridHtml += `<div class="dp-day other-month disabled">${prevDays - firstDay + i + 1}</div>`;
}
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = `${_dpYear}-${String(_dpMonth + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
const isToday = d === today.getDate() && _dpMonth === today.getMonth() && _dpYear === today.getFullYear();
const isSel = dateStr === _dpSelected;
const isFuture = new Date(_dpYear, _dpMonth, d) > today;
gridHtml += `<div class="dp-day${isToday ? ' today' : ''}${isSel ? ' selected' : ''}${isFuture ? ' disabled' : ''}"
onclick="${!isFuture ? `selectDate('${dateStr}')` : ''}">${d}</div>`;
}
box.innerHTML = `
<div class="dp-header">
<button class="dp-nav" onclick="dpNav(-1)">◀</button>
<div class="dp-title">${months[_dpMonth]} ${_dpYear}</div>
<button class="dp-nav" onclick="dpNav(1)">▶</button>
</div>
<div class="dp-grid">${gridHtml}</div>
<div class="dp-footer">
<button class="dp-btn" onclick="closeDatepicker()">Batal</button>
<button class="dp-btn primary" onclick="confirmDate()">Pilih</button>
</div>`;
}
function dpNav(dir) {
_dpMonth += dir;
if (_dpMonth < 0) { _dpMonth = 11; _dpYear--; }
if (_dpMonth > 11) { _dpMonth = 0; _dpYear++; }
renderDatepicker();
}
function selectDate(dateStr) {
_dpSelected = dateStr;
renderDatepicker();
}
function confirmDate() {
if (!_dpSelected || !_dpTargetInput) { closeDatepicker(); return; }
_dpTargetInput.value = _dpSelected;
if (_dpTargetBtn) _dpTargetBtn.textContent = formatDate(_dpSelected);
closeDatepicker();
}
// ============================================================
// LOAD DATA FROM BACKEND
// ============================================================
async function loadData() {
try {
const res = await fetch('get_data.php');
const data = await res.json();
if (!data.success) return;
data.centers.forEach(c => {
addCenter({ id: c.id, name: c.name, address: c.address, kas: c.kas, lat: c.latitude, lng: c.longitude, radius: c.radius });
});
data.houses.forEach(h => {
const marker = L.marker([h.latitude, h.longitude], {
icon: createHouseIcon(h.aid_status, !!h.has_data),
zIndexOffset: 100
}).addTo(map);
const house = {
id: h.id, lat: h.latitude, lng: h.longitude,
address: h.address, rt: h.rt, rw: h.rw, kelurahan: h.kelurahan,
statusMiskin: h.status_miskin, jumlahAnggota: h.jumlah_anggota,
anggota: h.anggota || [], aidStatus: h.aid_status,
hasData: !!h.has_data, marker, _geoData: null
};
marker.bindPopup(() => buildHouseMapPopup(house), { maxWidth: 270 });
marker.on('popupopen', () => marker.setPopupContent(buildHouseMapPopup(house)));
houses.push(house);
});
reports = data.reports || [];
updateStats();
updateSidebar();
updateReportBadge();
} catch (e) {
console.error('loadData', e);
}
}
// ============================================================
// INIT
// ============================================================
applyRoleUI();
loadData();