Files
WebGIS_Project/05/assets/js/app.js
T
luthfihadinugroho79 e950468760 add files
2026-06-10 19:42:59 +07:00

345 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 `<span class="badge ${v.cls}">${v.label}</span>`;
},
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 `<span class="badge ${v.cls}">${v.label}</span>`;
},
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 `<span class="badge ${v.cls}">${v.label}</span>`;
},
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: `<div style="width:${size}px;height:${size}px;background:${color};border-radius:50%;
display:flex;align-items:center;justify-content:center;font-size:${size*0.5}px;
box-shadow:0 3px 10px rgba(0,0,0,0.5);border:2px solid rgba(255,255,255,0.25)">${emoji}</div>`,
iconSize: [size, size],
iconAnchor: [size/2, size/2],
popupAnchor: [0, -size/2]
});
},
forIbadah(jenis) {
const map = {
masjid: ['#3b82f6','🕌'],
gereja: ['#8b5cf6','⛪'],
vihara: ['#f59e0b','🏯'],
pura: ['#10b981','🛕'],
klenteng: ['#06b6d4','⛩️'],
};
const [color, emoji] = map[(jenis||'').toLowerCase()] || ['#64748b','🏢'];
return this.make(color, emoji);
},
forPenduduk(statusEkonomi) {
const map = {
sangat_miskin: ['#ef4444','⚠️'],
miskin: ['#f97316','🔸'],
rentan: ['#eab308','🔶'],
mampu: ['#22c55e','🔷'],
};
const [color, emoji] = map[statusEkonomi] || ['#64748b','📍'];
return this.make(color, emoji);
}
};
// ── MODAL MANAGER ─────────────────────────────────────────
const Modal = {
open(id) { document.getElementById(id)?.classList.remove('hidden'); },
close(id) { document.getElementById(id)?.classList.add('hidden'); },
toggle(id){ document.getElementById(id)?.classList.toggle('hidden'); },
};
// ── LOADING BUTTON ────────────────────────────────────────
function setLoading(btn, loading, label = '') {
if (loading) {
btn._origLabel = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = `<div class="spinner"></div> ${label || 'Memproses...'}`;
} else {
btn.disabled = false;
btn.innerHTML = btn._origLabel || label;
}
}
// ── FORM SERIALIZER ───────────────────────────────────────
function serializeForm(formId) {
const form = document.getElementById(formId);
if (!form) return {};
const data = {};
form.querySelectorAll('input, select, textarea').forEach(el => {
if (el.name && !el.disabled) {
if (el.type === 'checkbox') data[el.name] = el.checked;
else if (el.type === 'radio') { if (el.checked) data[el.name] = el.value; }
else data[el.name] = el.value;
}
});
return data;
}
// ── DEBOUNCE ──────────────────────────────────────────────
function debounce(fn, delay = 300) {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); };
}
// ── CSV EXPORT ────────────────────────────────────────────
function exportToCSV(data, filename = 'export.csv') {
if (!data.length) { Toast.warning('Tidak ada data untuk diekspor'); return; }
const headers = Object.keys(data[0]);
const rows = [headers, ...data.map(r => headers.map(h => `"${String(r[h] ?? '').replace(/"/g, '""')}"` ))];
const csv = rows.map(r => r.join(',')).join('\n');
const a = document.createElement('a');
a.href = 'data:text/csv;charset=utf-8,\uFEFF' + encodeURIComponent(csv);
a.download = filename;
a.click();
}
// ── CONFIRM DIALOG ────────────────────────────────────────
function confirmAction(message, onConfirm) {
// Buat modal konfirmasi sederhana
const id = 'confirm-' + Date.now();
const el = document.createElement('div');
el.className = 'modal-overlay animate-fade-in';
el.id = id;
el.innerHTML = `
<div class="modal" style="max-width:380px">
<div class="modal-header">
<div class="modal-title">⚠️ Konfirmasi</div>
</div>
<div class="modal-body">
<p style="font-size:14px;color:var(--text);line-height:1.6">${message}</p>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="document.getElementById('${id}').remove()">Batal</button>
<button class="btn btn-danger" id="${id}-ok">Ya, Lanjutkan</button>
</div>
</div>`;
document.body.appendChild(el);
document.getElementById(`${id}-ok`).onclick = () => { el.remove(); onConfirm(); };
}
// ── HAVERSINE (client-side) ───────────────────────────────
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371000;
const phi1 = lat1 * Math.PI / 180, phi2 = lat2 * Math.PI / 180;
const dphi = (lat2 - lat1) * Math.PI / 180;
const dl = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dphi/2)**2 + Math.cos(phi1)*Math.cos(phi2)*Math.sin(dl/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
// ── GEOLOCATION ───────────────────────────────────────────
function getCurrentLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) { reject(new Error('Geolocation tidak didukung')); return; }
navigator.geolocation.getCurrentPosition(
pos => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
err => reject(err),
{ enableHighAccuracy: true, timeout: 8000 }
);
});
}
// ── NOTIFIKASI POLLING ────────────────────────────────────
const NotifPoller = {
interval: null,
callbacks: [],
start(intervalMs = 30000) {
this.poll();
this.interval = setInterval(() => this.poll(), intervalMs);
},
stop() {
if (this.interval) clearInterval(this.interval);
},
async poll() {
try {
const d = await API.get('../api/notifikasi.php?unread=1');
this.callbacks.forEach(cb => cb(d));
} catch (e) { /* silent */ }
},
onChange(cb) { this.callbacks.push(cb); }
};
// ── STORAGE HELPER (sessionStorage) ──────────────────────
const Store = {
set(key, val) { try { sessionStorage.setItem(key, JSON.stringify(val)); } catch(e){} },
get(key) { try { return JSON.parse(sessionStorage.getItem(key)); } catch(e){ return null; } },
remove(key) { sessionStorage.removeItem(key); },
};
// ── INIT ON DOM READY ─────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', e => {
if (e.target === overlay) overlay.classList.add('hidden');
});
});
// Auto-focus first input in opened modals
document.querySelectorAll('.modal-overlay .modal-close').forEach(btn => {
btn.addEventListener('click', () => {
btn.closest('.modal-overlay')?.classList.add('hidden');
});
});
});