// assets/js/app.js — WebGIS Pontianak v2.0 Global Utilities // ── TOAST SYSTEM ────────────────────────────────────────── const Toast = (() => { let container = null; function getContainer() { if (!container) { container = document.createElement('div'); container.className = 'toast-container'; document.body.appendChild(container); } return container; } function show(message, type = 'info', duration = 3500) { const t = document.createElement('div'); const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' }; t.className = `toast toast-${type}`; t.textContent = `${icons[type] || ''} ${message}`; const c = getContainer(); c.appendChild(t); setTimeout(() => { t.style.opacity = '0'; t.style.transform = 'translateY(12px)'; t.style.transition = 'all 0.3s'; setTimeout(() => t.remove(), 300); }, duration); return t; } return { show, success: m => show(m,'success'), error: m => show(m,'error'), info: m => show(m,'info'), warning: m => show(m,'warning') }; })(); // ── API HELPER ──────────────────────────────────────────── const API = { async get(url) { const res = await fetch(url, { headers: { 'Accept': 'application/json' } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }, async post(url, data, isFormData = false) { const opts = { method: 'POST' }; if (isFormData) { opts.body = data; } else { opts.headers = { 'Content-Type': 'application/json' }; opts.body = JSON.stringify(data); } const res = await fetch(url, opts); return res.json(); }, async put(url, data) { const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return res.json(); }, async delete(url) { const res = await fetch(url, { method: 'DELETE' }); return res.json(); } }; // ── DATE & TIME HELPERS ─────────────────────────────────── const DateTime = { format(dateStr, options = {}) { if (!dateStr) return '—'; const defaults = { day: 'numeric', month: 'short', year: 'numeric' }; return new Date(dateStr).toLocaleDateString('id-ID', { ...defaults, ...options }); }, formatFull(dateStr) { if (!dateStr) return '—'; return new Date(dateStr).toLocaleString('id-ID', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }); }, ago(dateStr) { if (!dateStr) return '—'; const diff = Date.now() - new Date(dateStr).getTime(); const m = Math.floor(diff / 60000); if (m < 1) return 'Baru saja'; if (m < 60) return `${m} menit lalu`; const h = Math.floor(m / 60); if (h < 24) return `${h} jam lalu`; const d = Math.floor(h / 24); if (d < 7) return `${d} hari lalu`; return DateTime.format(dateStr); }, age(dob) { if (!dob) return 0; return Math.floor((Date.now() - new Date(dob).getTime()) / (365.25 * 24 * 3600 * 1000)); }, today() { return new Date().toISOString().split('T')[0]; } }; // ── NUMBER HELPERS ──────────────────────────────────────── const Num = { currency(val, prefix = 'Rp') { if (!val && val !== 0) return '—'; const n = parseFloat(val); if (n >= 1e9) return `${prefix}${(n/1e9).toFixed(1)}M`; if (n >= 1e6) return `${prefix}${(n/1e6).toFixed(1)}jt`; if (n >= 1e3) return `${prefix}${(n/1e3).toFixed(0)}rb`; return `${prefix}${n.toLocaleString('id-ID')}`; }, format(val) { if (!val && val !== 0) return '—'; return parseFloat(val).toLocaleString('id-ID'); }, pct(val, total) { if (!total) return '0%'; return `${((val / total) * 100).toFixed(1)}%`; } }; // ── BADGE HELPERS ───────────────────────────────────────── const Badge = { statusEkonomi(se) { const map = { sangat_miskin: { cls: 'badge-red', label: 'Sangat Miskin' }, miskin: { cls: 'badge-amber', label: 'Miskin' }, rentan: { cls: 'badge-purple', label: 'Rentan' }, mampu: { cls: 'badge-green', label: 'Mampu' }, }; const v = map[se] || { cls: 'badge-gray', label: se }; return `${v.label}`; }, statusVerif(s) { const map = { pending: { cls: 'badge-amber', label: '⏳ Pending' }, diproses: { cls: 'badge-blue', label: '🔄 Diproses' }, disetujui: { cls: 'badge-green', label: '✅ Disetujui' }, ditolak: { cls: 'badge-red', label: '❌ Ditolak' }, }; const v = map[s] || { cls: 'badge-gray', label: s }; return `${v.label}`; }, statusBantuan(s) { const map = { disalurkan: { cls: 'badge-green', label: '✅ Disalurkan' }, ditunda: { cls: 'badge-amber', label: '⏳ Ditunda' }, dibatalkan: { cls: 'badge-red', label: '❌ Dibatalkan' }, }; const v = map[s] || { cls: 'badge-gray', label: s }; return `${v.label}`; }, jenisLaporan(j) { const map = { kelahiran: '🍼 Kelahiran', kematian: '⚰️ Kematian', pindah_masuk: '📦 Pindah Masuk', pindah_keluar: '🚚 Pindah Keluar', pernikahan: '💒 Pernikahan', perceraian: '📋 Perceraian', perubahan_data:'📝 Perubahan Data', lainnya: '📌 Lainnya', }; return map[j] || j; }, jenisIbadah(j) { const map = { Masjid:'🕌', Gereja:'⛪', Vihara:'🏯', Pura:'🛕', Klenteng:'⛩️' }; return `${map[j] || '🏢'} ${j}`; } }; // ── MAP ICON FACTORY ────────────────────────────────────── const MapIcon = { make(color, emoji, size = 30) { return L.divIcon({ className: '', html: `
${message}