1986 lines
89 KiB
JavaScript
1986 lines
89 KiB
JavaScript
/**
|
||
* 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}¢er_id=${center?center.id:0}` }).catch(()=>{});
|
||
}
|
||
function deleteCenterBackend(id) { fetch('hapus_pusat.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`id=${id}` }).catch(()=>{}); }
|
||
function deleteHouseBackend(id) { fetch('hapus_rumah.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`id=${id}` }).catch(()=>{}); }
|
||
function saveReportToBackend(r) { fetch('simpan_laporan.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`name=${encodeURIComponent(r.name)}&text=${encodeURIComponent(r.text)}&img=${encodeURIComponent(r.imgBase64||'')}` }).catch(()=>{}); }
|
||
|
||
function highlightError(id, msg) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.style.borderColor = '#dc2626'; el.placeholder = msg; el.focus();
|
||
}
|
||
|
||
// ============================================================
|
||
// VERIFIKASI AKUN (Admin)
|
||
// ============================================================
|
||
function openVerifyModal() {
|
||
document.getElementById('verifyModal').classList.remove('hidden');
|
||
switchAdminTab('verify');
|
||
loadPendingUsers();
|
||
loadResetRequests();
|
||
}
|
||
|
||
function closeVerifyModal() {
|
||
document.getElementById('verifyModal').classList.add('hidden');
|
||
}
|
||
|
||
function switchAdminTab(tab) {
|
||
const panelVerify = document.getElementById('adminPanelVerify');
|
||
const panelReset = document.getElementById('adminPanelReset');
|
||
const tabVerify = document.getElementById('vTabVerify');
|
||
const tabReset = document.getElementById('vTabReset');
|
||
|
||
if (tab === 'verify') {
|
||
panelVerify.style.display = 'block';
|
||
panelReset.style.display = 'none';
|
||
tabVerify.style.color = 'var(--primary)';
|
||
tabVerify.style.borderBottom = '3px solid var(--primary)';
|
||
tabVerify.style.fontWeight = '700';
|
||
tabReset.style.color = 'var(--text-muted)';
|
||
tabReset.style.borderBottom = '3px solid transparent';
|
||
tabReset.style.fontWeight = '600';
|
||
} else {
|
||
panelVerify.style.display = 'none';
|
||
panelReset.style.display = 'block';
|
||
tabReset.style.color = 'var(--primary)';
|
||
tabReset.style.borderBottom = '3px solid var(--primary)';
|
||
tabReset.style.fontWeight = '700';
|
||
tabVerify.style.color = 'var(--text-muted)';
|
||
tabVerify.style.borderBottom = '3px solid transparent';
|
||
tabVerify.style.fontWeight = '600';
|
||
}
|
||
}
|
||
|
||
async function loadPendingUsers() {
|
||
const container = document.getElementById('verifyListContainer');
|
||
container.innerHTML = '<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} • <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} • <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();
|
||
})(); |