Files
2026-06-10 22:26:03 +07:00

1986 lines
89 KiB
JavaScript
Raw Permalink 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.
/**
* 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: '© <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 = [];
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: `<div style="width:38px;height:38px;background:${info.grad};
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, Δλ = (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 = `<button class="popup-btn primary" onclick="openDetailModal(${house.id})">📋 Lihat 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})"> Tambahkan 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">🏠 Titik Rumah #${house.id}</div>
<div class="map-popup-addr">${
house.address && house.address !== 'Mengambil alamat...'
? house.address
: '<span style="color:var(--text-muted);font-style:italic;">⏳ Mengambil alamat...</span>'
}</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 type = getCenterType(c.name);
const info = CENTER_TYPE_MAP[type] || 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;text-transform:uppercase;letter-spacing:.5px">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:13px;font-family:var(--mono);font-weight:600;color:#16a34a;outline:none;"/>
<button onclick="saveKas(${c.id})" style="padding:6px 12px;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 10px;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:var(--orange-light);color:var(--orange);border:1px solid #fdba74;border-radius:8px;font-size:11px;font-weight:700;cursor:pointer;font-family:var(--font);">✏️ 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;font-family:var(--font);">🗑 Hapus Rumah Ibadah</button>
</div>`
: '';
return `
<div class="center-popup">
<div class="center-popup-header">
<div class="center-popup-icon" style="background:rgba(255,255,255,.4)">${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 Tersedia</span>
<span class="kas-value" id="kasVal_${c.id}">${formatRupiah(c.kas)}</span>
${kasEditHtml}
</div>
${kasFormHtml}
<div class="radius-info-box">
<span>⭕ Radius Jangkauan</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}`;
}
// ============================================================
// 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(`
<div class="form-popup">
<h3 id="fpHeader">🕌 Tambah Rumah Ibadah</h3>
<div class="form-group">
<label class="form-label">Jenis Rumah Ibadah</label>
<select class="form-input" id="fpType" onchange="updateCenterFormHeader()">
<option value="masjid">🕌 Masjid / Musholla</option>
<option value="gereja katedral">⛪ Gereja Katolik / Katedral</option>
<option value="gereja protestan">✝️ Gereja Protestan / Kapel</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 Rumah Ibadah *</label>
<input class="form-input" id="fpName" type="text" placeholder="Masjid Jami / Gereja Katedral..." autofocus/>
</div>
<div class="form-group">
<label class="form-label">Alamat</label>
<div id="fpAddrLoading" class="loading-addr">⏳ Mengambil alamat...</div>
<input class="form-input" id="fpAddr" type="text" placeholder="Alamat otomatis..." style="display:none"/>
</div>
<div class="form-group">
<label class="form-label">Kas / Dana Tersedia (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()">Batal</button>
</div>`)
.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 `<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 (luar radius) — 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 (Otomatis)</div>
<div class="info-box">
<div class="info-box-row"><span class="info-box-label">Alamat Jalan</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 / Desa</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 || 'Pontianak'}</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="${jumlah || ''}" onchange="renderMemberForms(${houseId})"/>
</div>
<div id="memberFormsContainer"></div>
</div>
`;
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 = `
<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(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 => `<option value="${s}" ${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 value="${p}" ${data.pekerjaan===p?'selected':''}>${p}</option>`).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 `
<div class="member-card">
<div class="member-card-header">
<div class="member-num">${i+1}</div>
Anggota ke-${i+1}
</div>
<div class="form-group">
<label class="form-label">Status dalam Keluarga</label>
<select class="form-input" id="am_status_${i}">${statusOptions}</select>
</div>
<div class="form-group">
<label class="form-label">Nama Lengkap *</label>
<input class="form-input" id="am_nama_${i}" type="text" placeholder="Nama lengkap..." value="${data.nama || ''}"/>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">NIK (16 digit)</label>
<input class="form-input" id="am_nik_${i}" type="text" maxlength="16" placeholder="NIK..." value="${data.nik || ''}"/>
</div>
<div class="form-group">
<label class="form-label">Tanggal Lahir</label>
<button type="button" class="date-trigger" id="am_lahir_trigger_${i}" onclick="openDatepicker(${i})">
<span id="am_lahir_display_${i}" class="${displayDate ? '' : 'date-trigger-placeholder'}">
${displayDate || '📅 Pilih tanggal'}
</span>
</button>
<input type="hidden" id="am_lahir_${i}" value="${existingDate}"/>
</div>
</div>
<div class="form-group">
<label class="form-label">Pekerjaan</label>
<select class="form-input" id="am_pekerjaan_${i}" onchange="toggleSalary(${i})">${pekerjaanOpts}</select>
</div>
<div class="form-row salary-row ${showSalary?'visible':''}" id="salaryRow_${i}">
<div class="form-group" style="flex:1">
<label class="form-label">Gaji / Penghasilan / bln (Rp)</label>
<input class="form-input" id="am_gaji_${i}" type="number" min="0" step="50000" placeholder="0" value="${data.gaji || ''}"/>
</div>
</div>
</div>`;
}
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 = `
<div class="dp-header">
<div>
<div class="dp-header-label">Tanggal Lahir</div>
<div class="dp-header-value">${displayVal}</div>
</div>
</div>
<div class="dp-tabs">
<div class="dp-tab ${_dpActiveTab==='day'?'active':''}" onclick="switchDpTab('day')">Tanggal</div>
<div class="dp-tab ${_dpActiveTab==='month'?'active':''}" onclick="switchDpTab('month')">Bulan</div>
<div class="dp-tab ${_dpActiveTab==='year'?'active':''}" onclick="switchDpTab('year')">Tahun</div>
</div>
<div class="dp-body">${bodyHtml}</div>
<div class="dp-actions">
<button class="dp-btn-cancel" onclick="closeDatepicker()">Batal</button>
<button class="dp-btn-ok" onclick="confirmDatepicker()">✓ Pilih Tanggal</button>
</div>`;
}
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 = `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<button class="dp-nav-btn" onclick="dpPrevMonth()"></button>
<span style="font-size:13px;font-weight:700;">${MONTHS_ID[_dpMonth]} ${_dpYear}</span>
<button class="dp-nav-btn" onclick="dpNextMonth()"></button>
</div>
<div class="dp-grid-7">`;
DAYS_ID.forEach(d => { html += `<div class="dp-weekday">${d}</div>`; });
// Empty cells before first day
for (let i = 0; i < firstDay; i++) html += `<div></div>`;
for (let d = 1; d <= daysInMonth; d++) {
const isSelected = d === _dpDay;
const isToday = d === today.getDate() && _dpMonth === today.getMonth() && _dpYear === today.getFullYear();
html += `<div class="dp-cell ${isSelected?'selected':''} ${isToday&&!isSelected?'today':''}" onclick="dpSelectDay(${d})">${d}</div>`;
}
html += '</div>';
return html;
}
function renderMonthGrid() {
let html = '<div class="dp-grid-3">';
MONTHS_ID.forEach((m, i) => {
const isSelected = i === _dpMonth;
html += `<div class="dp-cell ${isSelected?'selected':''}" onclick="dpSelectMonth(${i})">${m.slice(0,3)}</div>`;
});
html += '</div>';
return html;
}
function renderYearGrid() {
const endYear = _dpYearRangeStart + 11;
let html = `
<div class="dp-year-nav">
<button class="dp-nav-btn" onclick="dpPrevYearRange()">‹‹ Sebelumnya</button>
<span class="dp-year-range">${_dpYearRangeStart} ${endYear}</span>
<button class="dp-nav-btn" onclick="dpNextYearRange()">Selanjutnya ››</button>
</div>
<div class="dp-grid-3">`;
for (let y = _dpYearRangeStart; y <= endYear; y++) {
const isSelected = y === _dpYear;
html += `<div class="dp-cell ${isSelected?'selected':''}" onclick="dpSelectYear(${y})">${y}</div>`;
}
html += '</div>';
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 `<span>${info.emoji} ${c.name}</span>`; }).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 `
<div class="member-detail-card">
<div class="member-detail-name">${i+1}. ${a.nama} <span style="font-size:10px;color:var(--text-muted);font-weight:400">(${a.statusAnggota})</span></div>
<div class="member-detail-row"><span>NIK</span><strong>${a.nik||'—'}</strong></div>
<div class="member-detail-row"><span>Tanggal Lahir</span><strong>${formatDate(a.tglLahir)}${age!==null?' · '+age+' thn':''}</strong></div>
<div class="member-detail-row"><span>Pekerjaan</span><strong>${a.pekerjaan||'—'}</strong></div>
${showGaji?`<div class="member-detail-row"><span>Penghasilan</span><strong>${formatRupiah(a.gaji)}/bln</strong></div>`:''}
</div>`;
}).join('');
const aidBtnHtml = house.hasData && can('canEdit')
? (aidStatus==='helped'
? `<button class="btn-bantuan unhelp" onclick="toggleAid(${houseId},'unhelp')">↩ Batalkan Bantuan</button>`
: aidStatus==='not_helped'
? `<button class="btn-bantuan help" onclick="toggleAid(${houseId},'help')">✅ Tandai Sudah Dibantu</button>`
: '')
: '';
const editBtn = can('canEdit')
? `<button class="btn-bantuan" style="background:var(--orange-light);color:var(--orange);border:1px solid #fdba74;" onclick="closeDetailModal();openHouseModal(${houseId})">✏️ Edit</button>`
: '';
document.getElementById('detailModalBody').innerHTML = `
<div class="detail-header-band">
<div style="font-size:15px;font-weight:700">🏠 Titik Rumah #${house.id}</div>
<div class="detail-addr">📍 ${house.address}</div>
${house.rt?`<div style="font-size:10px;color:var(--text-muted);margin-top:3px">RT ${house.rt} / RW ${house.rw} · ${house.kelurahan}</div>`:''}
</div>
<div class="detail-center-row">🕌 <strong>Menangani:</strong> ${centersHtml}</div>
<div class="detail-miskin-badge ${house.statusMiskin}">${miskinLabels[house.statusMiskin]||'—'}</div>
<div class="modal-section-title">👨‍👩‍👧 Anggota Keluarga (${house.jumlahAnggota} jiwa)</div>
<div class="members-grid">${membersHtml||'<div class="empty-state">Belum ada data anggota.</div>'}</div>
<div class="bantuan-section">
<span class="bantuan-label">Status Bantuan:
<strong style="color:${aidStatus==='helped'?'var(--warning)':aidStatus==='not_helped'?'var(--danger)':'var(--success)'}">
${aidStatus==='helped'?'🟡 Sudah Dibantu':aidStatus==='not_helped'?'🔴 Belum Dibantu':'🟢 Luar Radius'}
</strong>
</span>
${aidBtnHtml}${editBtn}
</div>`;
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 = '<div class="empty-state">Tidak ada data.</div>'; 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 = `
<div class="center-item-name">${emoji} ${c.name}</div>
<div class="center-item-addr">📍 ${c.address.slice(0,42)}${c.address.length>42?'…':''}</div>
<div class="center-item-kas">💰 ${formatRupiah(c.kas)}</div>
<div class="center-item-radius">⭕ ${c.radius} m · 👥 ${jiwa} jiwa</div>
${can('canDelete') ? `<button class="item-delete" onclick="event.stopPropagation();deleteCenter(${c.id})">🗑</button>` : ''}`;
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 = '<div class="empty-state">Tidak ada data.</div>'; 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 = `
<div class="house-item-name">🏠 ${kk}</div>
<div class="house-item-sub">${h.rt?'RT '+h.rt+' RW '+h.rw+' · ':''}${h.kelurahan||''} · ${h.jumlahAnggota||0} jiwa</div>
<div class="house-item-status ${aidS==='helped'?'yellow':aidS==='not_helped'?'red':'green'}">
${statusLabel[aidS]||'🟢 Luar radius'}${h.statusMiskin?' · '+(miskinLabel[h.statusMiskin]||''):''}
</div>
${can('canDelete') ? `<button class="item-delete" onclick="event.stopPropagation();deleteHouse(${h.id})">🗑</button>` : ''}`;
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 = '<div class="empty-state" style="margin-top:20px">Belum ada laporan masuk.</div>';
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 `
<div class="report-card${hasImg}" onclick="openReportDetail(${r.id})">
<div class="report-card-top">
<div class="report-card-name">👤 ${r.name || 'Anonim'}</div>
<div class="report-card-time">🕐 ${r.time || ''}</div>
</div>
<div class="report-card-text">${r.text || ''}</div>
${r.location ? `<div class="report-card-location">📍 ${r.location}</div>` : ''}
<div class="report-card-footer">
<div class="report-status-badge ${statusClass}">${statusLabels[statusClass] || '🆕 Baru'}</div>
${r.imgBase64 ? `<img class="report-card-img" src="${r.imgBase64}" alt="Foto laporan"/>` : ''}
</div>
</div>`;
}).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') ? `
<div class="status-change-btns">
<button class="btn-status baru ${statusClass==='baru'?'active':''}" onclick="changeReportStatus(${reportId},'baru')">🆕 Baru</button>
<button class="btn-status ditangani ${statusClass==='ditangani'?'active':''}" onclick="changeReportStatus(${reportId},'ditangani')">🔄 Ditangani</button>
<button class="btn-status selesai ${statusClass==='selesai'?'active':''}" onclick="changeReportStatus(${reportId},'selesai')">✅ Selesai</button>
</div>` : '';
document.getElementById('reportDetailBody').innerHTML = `
<div class="report-detail-row">
<span class="report-detail-label">Pelapor</span>
<span class="report-detail-val">${report.name || 'Anonim'}</span>
</div>
<div class="report-detail-row">
<span class="report-detail-label">Waktu</span>
<span class="report-detail-val">${report.time || '—'}</span>
</div>
${report.location ? `
<div class="report-detail-row">
<span class="report-detail-label">Lokasi</span>
<span class="report-detail-val">${report.location}</span>
</div>` : ''}
<div class="report-detail-row">
<span class="report-detail-label">Status</span>
<span class="report-detail-val"><span class="report-status-badge ${statusClass}">${statusLabels[statusClass] || '🆕 Baru'}</span></span>
</div>
<div style="margin-top:12px;padding:12px;background:var(--bg);border-radius:var(--radius-sm);border:1px solid var(--border);">
<div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text-muted);margin-bottom:6px;">Deskripsi Laporan</div>
<div style="font-size:12px;line-height:1.6;color:var(--text);">${report.text || '—'}</div>
</div>
${report.imgBase64 ? `<img class="report-detail-img" src="${report.imgBase64}" alt="Foto laporan"/>` : ''}
${statusBtns}`;
document.getElementById('reportDetailModal').classList.remove('hidden');
}
function closeReportDetail() {
document.getElementById('reportDetailModal').classList.add('hidden');
}
function changeReportStatus(reportId, newStatus) {
const report = reports.find(r => r.id === reportId);
if (!report) return;
report.status = newStatus;
renderReportList();
updateReportBadge();
// Re-open detail to refresh status buttons
openReportDetail(reportId);
}
// ============================================================
// LOAD FROM BACKEND
// ============================================================
function loadFromBackend() {
fetch('get_data.php')
.then(r => r.json())
.then(data => {
if (data.centers) {
data.centers.forEach(c => {
addCenter({ id:c.id, name:c.name, address:c.address, kas:parseFloat(c.kas||0), lat:parseFloat(c.latitude), lng:parseFloat(c.longitude), radius:parseInt(c.radius) });
if (c.id >= nextCenterId) nextCenterId = c.id + 1;
});
}
if (data.houses) {
data.houses.forEach(h => {
const id = h.id;
const marker = L.marker([parseFloat(h.latitude), parseFloat(h.longitude)], { icon: createHouseIcon(h.aid_status||'outside', !!h.has_data), zIndexOffset:100 }).addTo(map);
const house = {
id, lat:parseFloat(h.latitude), lng:parseFloat(h.longitude),
address:h.address||'', rt:h.rt||'', rw:h.rw||'', kelurahan:h.kelurahan||'',
statusMiskin:h.status_miskin||'', jumlahAnggota:parseInt(h.jumlah_anggota)||0,
anggota: Array.isArray(h.anggota) ? h.anggota : (typeof h.anggota==='string' ? JSON.parse(h.anggota||'[]') : []),
aidStatus:h.aid_status||'outside', hasData:!!h.has_data, marker, _geoData:null
};
houses.push(house);
marker.bindPopup(() => buildHouseMapPopup(house), { maxWidth:270 });
marker.on('popupopen', () => marker.setPopupContent(buildHouseMapPopup(house)));
if (id >= nextHouseId) nextHouseId = id + 1;
});
}
if (data.reports) { data.reports.forEach(r => reports.push(r)); renderReportList(); }
updateAllHouseIcons(); updateSidebar();
})
.catch(() => console.log('Backend tidak tersedia — mode lokal.'));
}
// ============================================================
// BACKEND CALLS
// ============================================================
function saveCenterToBackend(c) {
fetch('simpan_pusat.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`name=${encodeURIComponent(c.name)}&address=${encodeURIComponent(c.address)}&kas=${c.kas}&lat=${c.lat}&lng=${c.lng}&radius=${c.radius}` })
.then(r=>r.json()).then(d=>{ if(d.id){ c.id=d.id; if(d.id>=nextCenterId) nextCenterId=d.id+1; updateCenterList(); } }).catch(()=>{});
}
function updateCenterPositionBackend(c) {
fetch('simpan_pusat.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`id=${c.id}&name=${encodeURIComponent(c.name)}&address=${encodeURIComponent(c.address)}&kas=${c.kas}&lat=${c.lat}&lng=${c.lng}&radius=${c.radius}&update=1` }).catch(()=>{});
}
function saveRadiusToBackend(c) {
fetch('update_radius.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`id=${c.id}&radius=${c.radius}` }).catch(()=>{});
}
function saveHouseToBackend(h) {
fetch('simpan_rumah.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`lat=${h.lat}&lng=${h.lng}&address=${encodeURIComponent(h.address||'')}&has_data=0` })
.then(r=>r.json()).then(d=>{ if(d.id&&d.id!==h.id){ h.id=d.id; if(d.id>=nextHouseId) nextHouseId=d.id+1; updateHouseList(); } }).catch(()=>{});
}
function saveHouseDataToBackend(h) {
const body = [
`id=${h.id}`,`lat=${h.lat}`,`lng=${h.lng}`,
`address=${encodeURIComponent(h.address||'')}`,
`rt=${encodeURIComponent(h.rt||'')}`,`rw=${encodeURIComponent(h.rw||'')}`,
`kelurahan=${encodeURIComponent(h.kelurahan||'')}`,
`status_miskin=${encodeURIComponent(h.statusMiskin||'')}`,
`jumlah_anggota=${h.jumlahAnggota||0}`,
`anggota=${encodeURIComponent(JSON.stringify(h.anggota||[]))}`,
`aid_status=${h.aidStatus||'outside'}`,`has_data=1`
].join('&');
fetch('simpan_rumah.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body }).catch(()=>{});
}
function updateHouseStatusBackend(h, center) {
fetch('update_status.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`house_id=${h.id}&status=${h.aidStatus}&center_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 = '<div class="empty-state">Memuat data...</div>';
try {
const res = await fetch('get_pending_users.php');
const data = await res.json();
if (data.success) {
if (data.data.length === 0) {
container.innerHTML = '<div class="empty-state">✅ Tidak ada akun yang menunggu verifikasi.</div>';
} else {
let html = '<div style="display:flex; flex-direction:column; gap:10px; padding:4px 0;">';
data.data.forEach(u => {
const roleLabel = ROLE_CONFIG[u.role] ? ROLE_CONFIG[u.role].label : u.role;
html += `
<div style="border:1px solid var(--border); border-radius:8px; padding:12px; display:flex; justify-content:space-between; align-items:center;">
<div>
<div style="font-weight:700; font-size:14px;">${u.nama}</div>
<div style="font-size:12px; color:var(--text-muted);">${u.email} &bull; <span style="font-weight:600; color:var(--primary);">${roleLabel}</span></div>
<div style="font-size:10px; color:var(--text-muted); margin-top:4px;">🕒 Terdaftar: ${u.waktu}</div>
</div>
<div style="display:flex; gap:8px;">
<button onclick="verifyUserAction(${u.id}, 'reject')" style="background:var(--danger-light); color:var(--danger); border:1px solid #fca5a5; padding:6px 12px; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px;">Tolak</button>
<button onclick="verifyUserAction(${u.id}, 'approve')" style="background:var(--success-light); color:var(--success); border:1px solid #86efac; padding:6px 12px; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px;">Setujui</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
} else {
container.innerHTML = `<div class="empty-state" style="color:red;">Gagal memuat: ${data.message}</div>`;
}
} catch (e) {
container.innerHTML = `<div class="empty-state" style="color:red;">Error mengambil data.</div>`;
}
}
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 = '<div class="empty-state">Memuat data...</div>';
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 = '<div class="empty-state">✅ Tidak ada permintaan reset password.</div>';
} else {
let html = '<div style="display:flex; flex-direction:column; gap:12px; padding:4px 0;">';
data.data.forEach(r => {
const roleLabel = ROLE_CONFIG[r.role] ? ROLE_CONFIG[r.role].label : r.role;
html += `
<div style="border:1px solid #fde68a; border-radius:8px; padding:14px; background:#fffbeb;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
<div>
<div style="font-weight:700; font-size:14px;">🔑 ${r.nama}</div>
<div style="font-size:12px; color:var(--text-muted);">${r.email} &bull; <span style="font-weight:600; color:var(--primary);">${roleLabel}</span></div>
<div style="font-size:10px; color:var(--text-muted); margin-top:3px;">🕒 Diminta: ${r.waktu}</div>
</div>
</div>
<div style="display:flex; gap:8px; align-items:center;">
<div style="position:relative; flex:1;">
<input type="password" id="newPw_${r.id}" placeholder="Password baru (min. 6 karakter)" style="width:100%; padding:8px 30px 8px 10px; border:1.5px solid #d97706; border-radius:6px; font-family:inherit; font-size:12px; outline:none; box-sizing:border-box;"/>
<button onclick="toggleAdminResetPassword(${r.id})" id="togglePw_${r.id}" style="position:absolute; right:8px; top:50%; transform:translateY(-50%); background:none; border:none; cursor:pointer; font-size:14px; color:var(--text-muted);">👁</button>
</div>
<button onclick="adminDoReset(${r.id}, ${r.user_id})" style="background:#d97706; color:white; border:none; padding:8px 14px; border-radius:6px; cursor:pointer; font-weight:700; font-size:12px; white-space:nowrap;">Reset Password</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
} else {
container.innerHTML = `<div class="empty-state" style="color:red;">Gagal: ${data.message}</div>`;
}
} catch (e) {
container.innerHTML = `<div class="empty-state" style="color:red;">Error mengambil data.</div>`;
}
}
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 = '<div class="empty-state" style="margin-top:40px">📭 Tidak ada laporan yang ditemukan.</div>';
return;
}
const statusLabels = {
baru: '🆕 Baru',
ditangani: '🔄 Ditangani',
selesai: '✅ Selesai'
};
container.innerHTML = list.map(r => {
const statusClass = r.status || 'baru';
return `
<div class="history-card">
<div class="history-card-top">
<div class="history-card-name">👤 ${r.name || 'Anonim'}</div>
<div class="history-card-time">🕐 ${r.time || ''}</div>
</div>
<div class="history-card-text">${r.text || ''}</div>
${r.imgBase64 ? `<img class="history-card-img" src="${r.imgBase64}" alt="Foto laporan"/>` : ''}
<div class="history-card-footer">
<div class="history-card-status ${statusClass}">${statusLabels[statusClass] || '🆕 Baru'}</div>
<div class="history-card-readonly">👁 Hanya Lihat</div>
</div>
</div>`;
}).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();
})();