Files
D1041231009-WebGIS-PovertyMap/resources/views/map.blade.php
T
2026-06-10 18:16:46 +07:00

1130 lines
62 KiB
PHP

<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Peta Interaktif - WebGIS Pemetaan Kemiskinan</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Turf.js -->
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<!-- DaisyUI + Tailwind -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<!-- Plus Jakarta Sans Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<style>
body { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.map-wrapper { flex: 1; position: relative; min-height: 0; }
#map { width: 100%; height: 100%; }
/* Floating Controls */
.floating-panel {
position: absolute;
top: 24px;
left: 24px;
z-index: 1000;
width: 316px;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.map-panel {
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(10px);
border: 1px solid #e8e8ec;
}
/* Marker dragging hint */
#dragHint, #mapHint {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
}
/* Custom Layer Control */
.leaflet-control-layers {
border-radius: 8px !important;
border: 1px solid #e8e8ec !important;
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.92) !important;
padding: 8px 12px !important;
}
</style>
<link rel="stylesheet" href="{{ asset('css/genesis.css') }}">
</head>
<body class="app-body app-map-body">
@include('partials.sidebar')
<!-- ═══ MAP CONTAINER ════════════════════════════════════════════════════ -->
<div class="map-wrapper app-map-main">
<div id="map"></div>
<!-- Floating Control Panel -->
<div class="floating-panel flex flex-col gap-3">
<!-- Mode Selector -->
<div class="card map-panel p-4">
<h3 class="text-sm font-bold mb-3 flex items-center gap-2">
<i class="fa-solid fa-layer-group text-primary"></i>
Mode Operasi
</h3>
<div class="grid grid-cols-1 gap-2">
<button class="btn btn-sm btn-outline btn-primary justify-start gap-2 active" id="btnModeView" onclick="setMode('view')">
<i class="fa-solid fa-eye"></i> View & Analisis
</button>
<button class="btn btn-sm btn-outline justify-start gap-2" id="btnModeIbadah" onclick="setMode('ibadah')">
<i class="fa-solid fa-plus"></i> Tambah Rumah Ibadah
</button>
<button class="btn btn-sm btn-outline justify-start gap-2" id="btnModeMiskin" onclick="setMode('miskin')">
<i class="fa-solid fa-plus"></i> Tambah Rumah Miskin
</button>
</div>
</div>
<!-- Analysis Panel (Conditional) -->
<div id="analysisPanel" class="card map-panel p-4 hidden">
<h3 class="text-sm font-bold mb-2 flex items-center gap-2">
<i class="fa-solid fa-chart-pie text-secondary"></i>
Statistik Radius
</h3>
<div id="noSelectionMsg" class="text-xs text-base-content/50 italic py-2 text-center">
Klik Rumah Ibadah pada peta <br> untuk memulai analisis.
</div>
<div id="statsContent" class="hidden">
<div class="bg-base-200/50 rounded-lg p-3 mb-3 border border-base-300">
<div class="text-xs font-bold text-primary truncate" id="activeName">Nama Ibadah</div>
<div class="text-[10px] text-base-content/40" id="activeAddr">Alamat</div>
</div>
<div class="form-control mb-4">
<label class="label py-1">
<span class="label-text-alt font-bold">Radius: <span id="radiusVal">{{ $appSettings['radius_min'] ?? 50 }}</span>m</span>
</label>
<input type="range" min="{{ $appSettings['radius_min'] ?? 50 }}" max="{{ $appSettings['radius_max'] ?? 2000 }}" value="{{ $appSettings['radius_min'] ?? 50 }}" step="50" class="range range-xs range-primary" id="radiusSlider">
</div>
<div class="grid grid-cols-3 gap-2">
<div class="text-center p-2 bg-white rounded-lg border border-base-200 shadow-sm">
<div class="text-lg font-bold text-error" id="statRumah">0</div>
<div class="text-[8px] uppercase tracking-wider font-bold opacity-50">Rumah</div>
</div>
<div class="text-center p-2 bg-white rounded-lg border border-base-200 shadow-sm">
<div class="text-lg font-bold text-warning" id="statKk">0</div>
<div class="text-[8px] uppercase tracking-wider font-bold opacity-50">KK</div>
</div>
<div class="text-center p-2 bg-white rounded-lg border border-base-200 shadow-sm">
<div class="text-lg font-bold text-info" id="statJiwa">0</div>
<div class="text-[8px] uppercase tracking-wider font-bold opacity-50">Jiwa</div>
</div>
</div>
<div class="border-t border-base-200 pt-3 mt-3 flex flex-col gap-2">
<button onclick="exportRadiusCSV()" class="btn btn-xs btn-outline gap-1 w-full">
<i class="fa-solid fa-file-csv text-success"></i> Export CSV Radius
</button>
<a id="btnExportRadiusPDF" href="#" target="_blank" class="btn btn-xs btn-error btn-outline gap-1 w-full">
<i class="fa-solid fa-file-pdf"></i> Export PDF Radius
</a>
</div>
</div>
</div>
</div>
<!-- Hints -->
<div id="mapHint" class="badge badge-primary badge-lg py-4 px-6 gap-2 hidden">
<i class="fa-solid fa-mouse-pointer"></i>
<span>Klik peta untuk menentukan lokasi baru</span>
</div>
<div id="dragHint" class="badge badge-warning badge-lg py-4 px-6 gap-2 hidden">
<i class="fa-solid fa-arrows-up-down-left-right"></i>
<span>Lepaskan untuk menyimpan lokasi baru</span>
</div>
</div>
<!-- ═══ MODALS ═══════════════════════════════════════════════════════════ -->
<!-- Modal: Input Ibadah -->
<dialog id="modalIbadah" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg flex items-center gap-2 mb-4">
<i class="fa-solid fa-mosque text-success"></i> Data Rumah Ibadah
</h3>
<div class="form-control gap-3">
<div class="bg-base-200 p-2 rounded text-xs font-mono" id="ib_coords">Lat: -, Lng: -</div>
<div class="grid grid-cols-2 gap-3">
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Nama Rumah Ibadah</span></div>
<input type="text" id="ib_nama" placeholder="Contoh: Masjid Nurul Iman" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Jenis</span></div>
<select id="ib_jenis" class="select select-bordered w-full">
<option value="masjid">Masjid</option>
<option value="gereja">Gereja</option>
<option value="pura">Pura</option>
<option value="klenteng">Klenteng</option>
<option value="vihara">Vihara</option>
</select>
</label>
</div>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Alamat</span></div>
<textarea id="ib_alamat" class="textarea textarea-bordered h-16" placeholder="Alamat lengkap..."></textarea>
</label>
<div class="grid grid-cols-2 gap-3">
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Pengurus</span></div>
<input type="text" id="ib_pengurus" placeholder="Nama ketua/pengurus" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Telepon</span></div>
<input type="text" id="ib_telepon" placeholder="08..." class="input input-bordered w-full" />
</label>
</div>
<div class="grid grid-cols-2 gap-3">
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Jumlah Jemaah</span></div>
<input type="number" id="ib_jemaah" value="100" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Radius Analisis (m)</span></div>
<input type="number" id="ib_radius" value="500" class="input input-bordered w-full" />
</label>
</div>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Keterangan</span></div>
<textarea id="ib_ket" class="textarea textarea-bordered h-16" placeholder="Catatan tambahan..."></textarea>
</label>
</div>
<div class="modal-action">
<button class="btn btn-ghost" onclick="modalIbadah.close()">Batal</button>
<button class="btn btn-success" id="btnSaveIbadah" onclick="saveIbadah()">Simpan</button>
</div>
</div>
</dialog>
<!-- Modal: Edit Rumah Ibadah -->
<dialog id="modal_edit_ibadah" class="modal">
<div class="modal-box w-11/12 max-w-lg">
<h3 class="font-bold text-lg mb-4">
<i class="fa-solid fa-mosque mr-2 text-success"></i>Edit Rumah Ibadah
</h3>
<input type="hidden" id="edit_ib_id">
<div class="grid grid-cols-2 gap-3">
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Nama *</span></label>
<input id="edit_ib_nama" type="text" class="input input-bordered w-full" placeholder="Nama rumah ibadah">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Jenis</span></label>
<select id="edit_ib_jenis" class="select select-bordered w-full">
<option value="">Pilih Jenis</option>
<option value="masjid">Masjid</option>
<option value="gereja">Gereja</option>
<option value="pura">Pura</option>
<option value="klenteng">Klenteng</option>
<option value="vihara">Vihara</option>
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Radius Analisis (m)</span></label>
<input id="edit_ib_radius" type="number" class="input input-bordered w-full" value="500" min="50" max="2000">
</div>
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Alamat</span></label>
<textarea id="edit_ib_alamat" class="textarea textarea-bordered w-full" rows="2" placeholder="Alamat lengkap"></textarea>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Pengurus</span></label>
<input id="edit_ib_pengurus" type="text" class="input input-bordered w-full" placeholder="Nama pengurus">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Telepon</span></label>
<input id="edit_ib_telepon" type="text" class="input input-bordered w-full" placeholder="No. telepon">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Jumlah Jemaah</span></label>
<input id="edit_ib_jemaah" type="number" class="input input-bordered w-full" value="0" min="0">
</div>
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Keterangan</span></label>
<textarea id="edit_ib_ket" class="textarea textarea-bordered w-full" rows="2" placeholder="Keterangan tambahan"></textarea>
</div>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_edit_ibadah').close()" class="btn btn-ghost">Batal</button>
<button onclick="saveEditIbadah()" class="btn btn-success">
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Perubahan
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal: Input Miskin -->
<dialog id="modalMiskin" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg flex items-center gap-2 mb-4">
<i class="fa-solid fa-home text-error"></i> Data Rumah Miskin
</h3>
<div class="form-control gap-3">
<div class="bg-base-200 p-2 rounded text-xs font-mono" id="rm_coords">Lat: -, Lng: -</div>
<div class="grid grid-cols-2 gap-3">
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">ID Rumah</span></div>
<input type="text" id="rm_id" placeholder="Contoh: RM-001" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Nama Kepala Keluarga</span></div>
<input type="text" id="rm_nama_kk" placeholder="Nama lengkap KK" class="input input-bordered w-full" />
</label>
</div>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Alamat</span></div>
<textarea id="rm_alamat" class="textarea textarea-bordered h-16" placeholder="Alamat lengkap..."></textarea>
</label>
<div class="grid grid-cols-2 gap-3">
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Jumlah KK</span></div>
<input type="number" id="rm_kk" value="1" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Jumlah Jiwa</span></div>
<input type="number" id="rm_jiwa" value="4" class="input input-bordered w-full" />
</label>
</div>
<div class="grid grid-cols-2 gap-3">
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Pekerjaan</span></div>
<input type="text" id="rm_kerja" placeholder="Pekerjaan KK" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Pendapatan (Rp)</span></div>
<input type="number" id="rm_duit" placeholder="Estimasi per bulan" class="input input-bordered w-full" />
</label>
</div>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Kondisi Rumah</span></div>
<select id="rm_kondisi" class="select select-bordered w-full">
<option value="sedang">Sedang</option>
<option value="buruk">Buruk</option>
<option value="sangat_buruk">Sangat Buruk</option>
</select>
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Keterangan</span></div>
<textarea id="rm_ket" class="textarea textarea-bordered h-16" placeholder="Catatan tambahan..."></textarea>
</label>
</div>
<div class="modal-action">
<button class="btn btn-ghost" onclick="modalMiskin.close()">Batal</button>
<button class="btn btn-error" id="btnSaveMiskin" onclick="saveMiskin()">Simpan</button>
</div>
</div>
</dialog>
<!-- Modal: Edit Keluarga Miskin -->
<dialog id="modal_edit_miskin" class="modal">
<div class="modal-box w-11/12 max-w-lg">
<h3 class="font-bold text-lg mb-4">
<i class="fa-solid fa-house mr-2 text-error"></i>Edit Keluarga Miskin
</h3>
<input type="hidden" id="edit_rm_id">
<div class="grid grid-cols-2 gap-3">
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Nama Kepala Keluarga</span></label>
<input id="edit_rm_nama_kk" type="text" class="input input-bordered w-full" placeholder="Nama KK">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Jumlah KK</span></label>
<input id="edit_rm_kk" type="number" class="input input-bordered w-full" value="1" min="1">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Jumlah Jiwa</span></label>
<input id="edit_rm_jiwa" type="number" class="input input-bordered w-full" value="4" min="1">
</div>
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Alamat</span></label>
<textarea id="edit_rm_alamat" class="textarea textarea-bordered w-full" rows="2" placeholder="Alamat lengkap"></textarea>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Pekerjaan</span></label>
<input id="edit_rm_kerja" type="text" class="input input-bordered w-full" placeholder="Pekerjaan KK">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Pendapatan (Rp)</span></label>
<input id="edit_rm_duit" type="number" class="input input-bordered w-full" value="0" min="0">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Kondisi Rumah</span></label>
<select id="edit_rm_kondisi" class="select select-bordered w-full">
<option value="">Pilih Kondisi</option>
<option value="sedang">Sedang</option>
<option value="buruk">Buruk</option>
<option value="sangat_buruk">Sangat Buruk</option>
</select>
</div>
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Keterangan</span></label>
<textarea id="edit_rm_ket" class="textarea textarea-bordered w-full" rows="2" placeholder="Keterangan tambahan"></textarea>
</div>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_edit_miskin').close()" class="btn btn-ghost">Batal</button>
<button onclick="saveEditMiskin()" class="btn btn-error">
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Perubahan
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal: Confirm Delete -->
<dialog id="deleteModal" class="modal">
<div class="modal-box max-w-sm text-center">
<div class="text-5xl mb-4 text-error"><i class="fa-solid fa-trash-can"></i></div>
<h3 class="font-bold text-xl mb-2">Hapus Data?</h3>
<p class="text-sm text-base-content/60 mb-6">Data ini akan dihapus secara permanen dari sistem.</p>
<div class="flex gap-2 justify-center">
<button class="btn btn-ghost px-8" onclick="deleteModal.close()">Batal</button>
<button class="btn btn-error px-8 text-white" id="confirmDeleteBtn">Ya, Hapus</button>
</div>
</div>
</dialog>
<!-- Modal Anggota (dari popup peta) -->
<dialog id="mapmodal_anggota" class="modal">
<div class="modal-box w-11/12 max-w-2xl">
<h3 class="font-bold text-lg mb-1 flex items-center gap-2"><i class="fa-solid fa-users text-info"></i> Anggota Keluarga</h3>
<p id="map_anggota_label" class="text-xs opacity-50 mb-4">-</p>
<div class="overflow-x-auto">
<table class="table table-sm w-full">
<thead><tr class="text-xs uppercase opacity-50"><th>Nama</th><th>Hubungan</th><th>Umur</th><th>Pendidikan</th><th>Status</th></tr></thead>
<tbody id="map_bodyAnggota"><tr><td colspan="5" class="text-center py-4 opacity-30 italic">Memuat...</td></tr></tbody>
</table>
</div>
<div class="modal-action">
<button onclick="document.getElementById('mapmodal_anggota').close()" class="btn btn-ghost">Tutup</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal Bantuan (dari popup peta) -->
<dialog id="mapmodal_bantuan" class="modal">
<div class="modal-box w-11/12 max-w-2xl">
<h3 class="font-bold text-lg mb-1 flex items-center gap-2"><i class="fa-solid fa-hand-holding-heart text-success"></i> Riwayat Bantuan</h3>
<p id="map_bantuan_label" class="text-xs opacity-50 mb-4">-</p>
<div class="overflow-x-auto">
<table class="table table-sm w-full">
<thead><tr class="text-xs uppercase opacity-50"><th>Tanggal</th><th>Jenis</th><th>Dari Ibadah</th><th>Dicatat oleh</th></tr></thead>
<tbody id="map_bodyBantuan"><tr><td colspan="4" class="text-center py-4 opacity-30 italic">Memuat...</td></tr></tbody>
</table>
</div>
<div class="modal-action">
<button onclick="document.getElementById('mapmodal_bantuan').close()" class="btn btn-ghost">Tutup</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Toast Container -->
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
<script>
const authUserRole = '{{ auth()->user()->getRoleNames()->first() ?? '' }}';
const authUserId = {{ auth()->id() }};
const canEditIbadah = ['administrator', 'pengurus_ibadah'].includes(authUserRole);
const canEditMiskin = ['administrator', 'petugas_pendataan'].includes(authUserRole);
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
// ── CONFIG & STATE ──────────────────────────────────────────────────
let map, currentMode = 'view';
let layerIbadah = L.layerGroup(), layerMiskin = L.layerGroup();
let activeCircle = null, activeIbadah = null;
let ibadahData = [], miskinData = [];
let tempMarker = null;
let ibadahMarkers = new Map(); // id -> marker
let miskinMarkers = new Map(); // id_rumah -> marker
const icons = {
ibadah: (color = '#22c55e') => L.divIcon({
html: `<div style="display:flex;flex-direction:column;align-items:center;width:36px;height:50px;">
<div style="background:${color};width:36px;height:36px;border-radius:50%;border:3px solid white;box-shadow:0 4px 12px rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fa-solid fa-mosque" style="color:white;font-size:15px;"></i>
</div>
<div style="width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:14px solid ${color};margin-top:-1px;flex-shrink:0;"></div>
</div>`,
className: '', iconSize: [36, 50], iconAnchor: [18, 50], popupAnchor: [0, -52]
}),
miskin: (color = '#ef4444', glow = false) => L.divIcon({
html: `<div style="background:${color};width:24px;height:24px;border-radius:24px;border:2px solid white;display:flex;align-items:center;justify-content:center;box-shadow:0 3px 8px rgba(0,0,0,0.3);${glow ? 'box-shadow: 0 0 15px ' + color : ''}">
<i class="fa-solid fa-home" style="color:white;font-size:10px"></i>
</div>`,
className: '', iconSize: [24, 24], iconAnchor: [12, 12], popupAnchor: [0, -15]
}),
temp: () => L.divIcon({
html: `<div class="animate-pulse" style="background:#3b82f6;width:20px;height:20px;border-radius:20px;border:3px solid white;box-shadow:0 0 15px rgba(59,130,246,0.6)"></div>`,
className: '', iconSize: [20, 20], iconAnchor: [10, 10]
})
};
// ── INIT ────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
initMap();
loadData();
// Hide add buttons for roles that can't edit
if (!canEditIbadah) {
document.querySelectorAll('[data-mode="ibadah"], #btnModeIbadah, .btn-add-ibadah').forEach(el => el.classList.add('hidden'));
}
if (!canEditMiskin) {
document.querySelectorAll('[data-mode="miskin"], #btnModeMiskin, .btn-add-miskin').forEach(el => el.classList.add('hidden'));
}
});
const appSettings = @json($appSettings);
function initMap() {
const centerLat = parseFloat(appSettings.map_center_lat ?? '-0.0553');
const centerLng = parseFloat(appSettings.map_center_lng ?? '109.3463');
const defaultZoom = parseInt(appSettings.map_default_zoom ?? '17');
map = L.map('map', { zoomControl: false }).setView([centerLat, centerLng], defaultZoom);
const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 });
const satellite = L.layerGroup([
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }),
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }),
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 })
]);
satellite.addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.control.layers({ "Satelit": satellite, "Street": osm },
{ "Rumah Ibadah": layerIbadah, "Rumah Miskin": layerMiskin },
{ position: 'topright' }).addTo(map);
layerIbadah.addTo(map);
layerMiskin.addTo(map);
map.on('click', e => {
if (currentMode === 'view') return;
handleMapClick(e.latlng);
});
}
async function loadData() {
try {
const [ri, rm] = await Promise.all([
fetch('{{ url('/api/ibadah') }}').then(r => r.json()),
fetch('{{ url('/api/miskin') }}').then(r => r.json())
]);
// Validate data is array
if (!Array.isArray(ri) || !Array.isArray(rm)) {
console.error("Invalid API response", { ri, rm });
return;
}
ibadahData = ri;
miskinData = rm;
// Sync activeIbadah to point to the new object in the fresh data
if (activeIbadah) {
const matched = ibadahData.find(i => i.id == activeIbadah.id);
if (matched) activeIbadah = matched;
}
renderMarkers();
} catch (e) {
console.error(e);
showToast('Gagal memuat data', 'error');
}
}
function renderMarkers() {
layerIbadah.clearLayers();
layerMiskin.clearLayers();
ibadahMarkers.clear();
miskinMarkers.clear();
ibadahData.forEach(item => {
const m = L.marker([item.latitude, item.longitude], { icon: icons.ibadah(), draggable: true })
.bindPopup(buildPopupIbadah(item))
.on('click', () => handleIbadahClick(item));
m.on('dragend', e => updateLocation('ibadah', item.id, e.target.getLatLng(), item));
layerIbadah.addLayer(m);
ibadahMarkers.set(item.id, m);
});
miskinData.forEach(item => {
const marker = L.marker([item.latitude, item.longitude], {
icon: icons.miskin('#ef4444'),
draggable: true
}).bindPopup(buildPopupMiskin(item));
marker.on('dragend', e => updateLocation('miskin', item.id_rumah, e.target.getLatLng(), item));
layerMiskin.addLayer(marker);
miskinMarkers.set(item.id_rumah, marker);
});
// If an analysis is active, update highlights immediately
if (activeCircle) {
updateAnalysisUI(activeCircle.getLatLng(), activeCircle.getRadius());
}
}
// ── MODE & CLICK HANDLING ───────────────────────────────────────────
function setMode(mode) {
currentMode = mode;
document.querySelectorAll('.btn-outline').forEach(b => b.classList.remove('active'));
document.getElementById('btnMode' + mode.charAt(0).toUpperCase() + mode.slice(1)).classList.add('active');
document.getElementById('mapHint').classList.toggle('hidden', mode === 'view');
document.getElementById('analysisPanel').classList.toggle('hidden', mode !== 'view');
if (mode !== 'view') {
if (activeCircle) { map.removeLayer(activeCircle); activeCircle = null; }
document.getElementById('statsContent').classList.add('hidden');
document.getElementById('noSelectionMsg').classList.remove('hidden');
}
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
}
function handleMapClick(latlng) {
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker(latlng, { icon: icons.temp() }).addTo(map);
if (currentMode === 'ibadah') {
document.getElementById('ib_coords').innerText = `Lat: ${latlng.lat.toFixed(6)}, Lng: ${latlng.lng.toFixed(6)}`;
document.getElementById('ib_nama').value = '';
document.getElementById('ib_alamat').value = 'Mencari alamat...';
modalIbadah.showModal();
// Auto fetch address
reverseGeocode(latlng.lat, latlng.lng, 'ib_alamat');
} else {
document.getElementById('rm_coords').innerText = `Lat: ${latlng.lat.toFixed(6)}, Lng: ${latlng.lng.toFixed(6)}`;
document.getElementById('rm_id').value = '';
document.getElementById('rm_alamat').value = 'Mencari alamat...';
modalMiskin.showModal();
// Auto fetch address
reverseGeocode(latlng.lat, latlng.lng, 'rm_alamat');
}
}
async function reverseGeocode(lat, lng, targetId) {
try {
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`);
const data = await res.json();
if (data && data.display_name) {
document.getElementById(targetId).value = data.display_name;
} else {
document.getElementById(targetId).value = "Alamat tidak ditemukan";
}
} catch (e) {
document.getElementById(targetId).value = "Gagal mengambil alamat otomatis";
}
}
function handleIbadahClick(item) {
if (currentMode !== 'view') setMode('view');
activeIbadah = item;
document.getElementById('noSelectionMsg').classList.add('hidden');
document.getElementById('statsContent').classList.remove('hidden');
document.getElementById('activeName').innerText = item.nama;
document.getElementById('activeAddr').innerText = item.alamat;
const radius = document.getElementById('radiusSlider').value = item.radius || 500;
updateRadius(item.latitude, item.longitude, radius);
// Update PDF export link
const pdfBtn = document.getElementById('btnExportRadiusPDF');
if (pdfBtn) pdfBtn.href = `{{ url('/export/pdf/radius') }}/${item.id}/${item.radius || 500}`;
}
// ── RADIUS ANALYSIS ────────────────────────────────────────────────
function updateRadius(lat, lng, radius) {
if (activeCircle) map.removeLayer(activeCircle);
activeCircle = L.circle([lat, lng], {
radius: parseInt(radius),
color: '#2563eb',
fillColor: '#3b82f6',
fillOpacity: 0.2,
weight: 2,
dashArray: '5, 10'
}).addTo(map);
document.getElementById('radiusVal').innerText = radius;
updateAnalysisUI([lat, lng], radius);
}
function updateAnalysisUI(center, radius) {
let houses = 0, kk = 0, jiwa = 0;
miskinData.forEach(h => {
const houseMarker = miskinMarkers.get(h.id_rumah);
const isH = isInside(h, center, radius);
if (isH) {
houses++;
kk += parseInt(h.jumlah_kk);
jiwa += parseInt(h.jumlah_orang);
}
// Update marker icon in real-time
if (houseMarker) {
const currentColor = isH ? '#fbbf24' : '#ef4444';
// Only update icon if state changed (optional optimization, but setIcon is fine)
houseMarker.setIcon(icons.miskin(currentColor, isH));
houseMarker.setZIndexOffset(isH ? 1000 : 0);
}
});
document.getElementById('statRumah').innerText = houses;
document.getElementById('statKk').innerText = kk;
document.getElementById('statJiwa').innerText = jiwa;
}
function exportRadiusCSV() {
if (!activeIbadah) return;
const radius = parseInt(document.getElementById('radiusSlider').value);
const inRadius = miskinData.filter(m =>
isInside(m, { lat: activeIbadah.latitude, lng: activeIbadah.longitude }, radius)
);
let csv = "data:text/csv;charset=utf-8,ID Rumah,Nama KK,Alamat,KK,Jiwa,Pekerjaan,Pendapatan,Kondisi\n";
inRadius.forEach(m => {
csv += `"${m.id_rumah}","${m.nama_kk || ''}","${(m.alamat || '').replace(/"/g, '""')}",${m.jumlah_kk},${m.jumlah_orang},"${m.pekerjaan || ''}",${m.pendapatan || 0},"${m.kondisi_rumah || ''}"\n`;
});
const link = document.createElement('a');
link.setAttribute('href', encodeURI(csv));
link.setAttribute('download', `radius-${activeIbadah.nama.replace(/\s+/g, '-')}-${radius}m.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function isInside(point, center, radius) {
// center can be [lat, lng] or {lat, lng}
const cLat = center.lat !== undefined ? center.lat : center[0];
const cLng = center.lng !== undefined ? center.lng : center[1];
const p1 = turf.point([Number(point.longitude), Number(point.latitude)]);
const p2 = turf.point([Number(cLng), Number(cLat)]);
return turf.distance(p1, p2, { units: 'meters' }) <= radius;
}
document.getElementById('radiusSlider').addEventListener('input', e => {
if (activeIbadah) {
updateRadius(activeIbadah.latitude, activeIbadah.longitude, e.target.value);
}
});
// Save radius to DB when user finishes sliding
document.getElementById('radiusSlider').addEventListener('change', async e => {
if (!activeIbadah) return;
const newRadius = e.target.value;
try {
const res = await fetch(`{{ url('/api/ibadah') }}/${activeIbadah.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({
id: activeIbadah.id,
nama: activeIbadah.nama,
alamat: activeIbadah.alamat,
latitude: activeIbadah.latitude,
longitude: activeIbadah.longitude,
radius: newRadius
})
});
const resJson = await res.json();
if (resJson.success) {
activeIbadah.radius = newRadius;
showToast('Radius disimpan ke database', 'success');
// Update the local data copy so it persists in memory
const localItem = ibadahData.find(i => i.id == activeIbadah.id);
if (localItem) localItem.radius = newRadius;
// Update PDF export link with new radius
const pdfBtn = document.getElementById('btnExportRadiusPDF');
if (pdfBtn && activeIbadah) {
pdfBtn.href = `{{ url('/export/pdf/radius') }}/${activeIbadah.id}/${newRadius}`;
}
}
} catch (e) {
showToast('Gagal menyimpan radius ke database', 'error');
}
});
// ── POPUPS ──────────────────────────────────────────────────────────
function buildPopupIbadah(item) {
const canEditThis = canEditIbadah && (authUserRole === 'administrator' || item.user_id === authUserId);
return `<div class="p-2 min-w-[200px]">
<div class="font-bold text-base">${item.nama}</div>
<div class="badge badge-sm badge-outline mb-2 capitalize">${item.jenis || 'Ibadah'}</div>
<div class="text-[10px] text-gray-500 mb-1"><i class="fa-solid fa-location-dot"></i> ${item.alamat}</div>
<div class="grid grid-cols-2 gap-2 my-2 text-[10px] bg-base-200 p-2 rounded">
<div><strong>Pengurus:</strong><br>${item.pengurus || '-'}</div>
<div><strong>Jemaah:</strong><br>${item.jumlah_jemaah || 0}</div>
</div>
<div class="flex gap-1 border-t pt-2 mt-1">
${canEditThis ? `<button class="btn btn-xs btn-warning btn-outline" onclick="openEditIbadahById(${item.id})"><i class="fa-solid fa-pen mr-1"></i>Edit</button>` : ''}
${canEditThis ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})">Hapus</button>` : ''}
</div>
</div>`;
}
function buildPopupMiskin(item) {
return `<div class="p-2 min-w-[200px]">
<div class="font-bold text-base">${item.nama_kk || 'Keluarga Miskin'}</div>
<div class="text-[10px] text-gray-400 mb-1">ID: ${item.id_rumah}</div>
<div class="text-[10px] text-gray-500 mb-1"><i class="fa-solid fa-location-dot"></i> ${item.alamat || '-'}</div>
<div class="grid grid-cols-2 gap-2 my-2 text-[10px] bg-base-200 p-2 rounded">
<div><strong>KK/Jiwa:</strong><br>${item.jumlah_kk}/${item.jumlah_orang}</div>
<div><strong>Kondisi:</strong><br><span class="capitalize">${item.kondisi_rumah || '-'}</span></div>
</div>
<div class="flex gap-1 border-t pt-2 mt-1 flex-wrap">
<button class="btn btn-xs btn-info btn-outline" onclick="openMapAnggotaModal('${item.id_rumah}')"><i class="fa-solid fa-users mr-1"></i>Anggota</button>
<button class="btn btn-xs btn-success btn-outline" onclick="openMapBantuanModal('${item.id_rumah}')"><i class="fa-solid fa-hand-holding-heart mr-1"></i>Bantuan</button>
${canEditMiskin ? `<button class="btn btn-xs btn-warning btn-outline" onclick="openEditMiskinById('${item.id_rumah}')"><i class="fa-solid fa-pen mr-1"></i>Edit</button>` : ''}
${canEditMiskin ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')">Hapus</button>` : ''}
</div>
</div>`;
}
// ── CRUD OPERATIONS ────────────────────────────────────────────────
async function saveIbadah() {
const pos = tempMarker.getLatLng();
const data = {
nama: document.getElementById('ib_nama').value,
jenis: document.getElementById('ib_jenis').value,
alamat: document.getElementById('ib_alamat').value,
latitude: pos.lat, longitude: pos.lng,
radius: document.getElementById('ib_radius').value,
jumlah_jemaah: document.getElementById('ib_jemaah').value,
pengurus: document.getElementById('ib_pengurus').value,
telepon: document.getElementById('ib_telepon').value,
keterangan: document.getElementById('ib_ket').value
};
const btn = document.getElementById('btnSaveIbadah');
btn.disabled = true;
const res = await fetch('{{ url('/api/ibadah') }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(data)
});
const resJson = await res.json();
if (resJson.success) {
showToast('Rumah Ibadah disimpan', 'success');
modalIbadah.close();
loadData();
} else showToast(resJson.message, 'error');
btn.disabled = false;
}
async function saveMiskin() {
const pos = tempMarker.getLatLng();
const data = {
id_rumah: document.getElementById('rm_id').value,
nama_kk: document.getElementById('rm_nama_kk').value,
alamat: document.getElementById('rm_alamat').value,
jumlah_kk: document.getElementById('rm_kk').value,
jumlah_orang: document.getElementById('rm_jiwa').value,
latitude: pos.lat, longitude: pos.lng,
pekerjaan: document.getElementById('rm_kerja').value,
pendapatan: document.getElementById('rm_duit').value,
kondisi_rumah: document.getElementById('rm_kondisi').value,
keterangan: document.getElementById('rm_ket').value
};
const btn = document.getElementById('btnSaveMiskin');
btn.disabled = true;
const res = await fetch('{{ url('/api/miskin') }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(data)
});
const resJson = await res.json();
if (resJson.success) {
showToast('Rumah Miskin disimpan', 'success');
modalMiskin.close();
loadData();
} else showToast(resJson.message, 'error');
btn.disabled = false;
}
async function updateLocation(type, id, latlng, oldData) {
const endpoint = `{{ url('/api') }}/${type}/${id}`;
const payload = type === 'ibadah'
? { id, nama: oldData.nama, alamat: oldData.alamat, latitude: latlng.lat, longitude: latlng.lng, radius: oldData.radius }
: { id_rumah: id, alamat: oldData.alamat, jumlah_kk: oldData.jumlah_kk, jumlah_orang: oldData.jumlah_orang, latitude: latlng.lat, longitude: latlng.lng };
try {
const res = await fetch(endpoint, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(payload)
});
const resJson = await res.json();
if (resJson.success) {
showToast('Lokasi diperbarui', 'success');
setTimeout(loadData, 100); // Small delay to let Leaflet finish
} else {
showToast(resJson.message || 'Gagal memperbarui lokasi', 'error');
setTimeout(loadData, 100);
}
} catch (e) {
showToast('Gagal terhubung ke server', 'error');
setTimeout(loadData, 100);
}
}
let deleteTarget = null;
function prepDelete(type, id) {
deleteTarget = { type, id };
map.closePopup();
deleteModal.showModal();
}
document.getElementById('confirmDeleteBtn').onclick = async () => {
const { type, id } = deleteTarget;
const res = await fetch(`{{ url('/api') }}/${type}/${id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }
});
const resJson = await res.json();
if (resJson.success) {
showToast('Data dihapus', 'success');
deleteModal.close();
loadData();
} else showToast('Gagal menghapus', 'error');
};
// ── EDIT FORMS ─────────────────────────────────────────────
function openEditIbadahById(id) {
const item = ibadahData.find(i => i.id === id);
if (!item) return;
document.getElementById('edit_ib_id').value = item.id;
document.getElementById('edit_ib_nama').value = item.nama || '';
document.getElementById('edit_ib_jenis').value = item.jenis || '';
document.getElementById('edit_ib_alamat').value = item.alamat || '';
document.getElementById('edit_ib_pengurus').value = item.pengurus || '';
document.getElementById('edit_ib_telepon').value = item.telepon || '';
document.getElementById('edit_ib_jemaah').value = item.jumlah_jemaah || 0;
document.getElementById('edit_ib_radius').value = item.radius || 500;
document.getElementById('edit_ib_ket').value = item.keterangan || '';
document.getElementById('modal_edit_ibadah').showModal();
}
async function saveEditIbadah() {
const id = document.getElementById('edit_ib_id').value;
const item = ibadahData.find(i => i.id == id);
if (!item) return;
const body = {
nama: document.getElementById('edit_ib_nama').value,
jenis: document.getElementById('edit_ib_jenis').value,
alamat: document.getElementById('edit_ib_alamat').value,
pengurus: document.getElementById('edit_ib_pengurus').value,
telepon: document.getElementById('edit_ib_telepon').value,
jumlah_jemaah: parseInt(document.getElementById('edit_ib_jemaah').value) || 0,
radius: parseInt(document.getElementById('edit_ib_radius').value) || 500,
keterangan: document.getElementById('edit_ib_ket').value,
latitude: item.latitude,
longitude: item.longitude,
};
const res = await fetch(`{{ url('/api/ibadah') }}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(body),
});
const resJson = await res.json();
if (resJson.success) {
document.getElementById('modal_edit_ibadah').close();
showToast('Data berhasil diperbarui', 'success');
await loadData();
} else if (res.status === 403) {
showToast(resJson.message || 'Tidak diizinkan mengubah data ini', 'error');
} else {
showToast('Gagal memperbarui data', 'error');
}
}
function openEditMiskinById(idRumah) {
const item = miskinData.find(i => i.id_rumah === idRumah);
if (!item) return;
document.getElementById('edit_rm_id').value = item.id_rumah;
document.getElementById('edit_rm_nama_kk').value = item.nama_kk || '';
document.getElementById('edit_rm_kk').value = item.jumlah_kk || 1;
document.getElementById('edit_rm_jiwa').value = item.jumlah_orang || 4;
document.getElementById('edit_rm_alamat').value = item.alamat || '';
document.getElementById('edit_rm_kerja').value = item.pekerjaan || '';
document.getElementById('edit_rm_duit').value = item.pendapatan || 0;
document.getElementById('edit_rm_kondisi').value = item.kondisi_rumah || '';
document.getElementById('edit_rm_ket').value = item.keterangan || '';
document.getElementById('modal_edit_miskin').showModal();
}
async function saveEditMiskin() {
const idRumah = document.getElementById('edit_rm_id').value;
const item = miskinData.find(i => i.id_rumah === idRumah);
if (!item) return;
const body = {
nama_kk: document.getElementById('edit_rm_nama_kk').value,
jumlah_kk: parseInt(document.getElementById('edit_rm_kk').value) || 1,
jumlah_orang: parseInt(document.getElementById('edit_rm_jiwa').value) || 1,
alamat: document.getElementById('edit_rm_alamat').value,
pekerjaan: document.getElementById('edit_rm_kerja').value,
pendapatan: parseInt(document.getElementById('edit_rm_duit').value) || 0,
kondisi_rumah: document.getElementById('edit_rm_kondisi').value,
keterangan: document.getElementById('edit_rm_ket').value,
latitude: item.latitude,
longitude: item.longitude,
};
const res = await fetch(`{{ url('/api/miskin') }}/${idRumah}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(body),
});
const resJson = await res.json();
if (resJson.success) {
document.getElementById('modal_edit_miskin').close();
showToast('Data berhasil diperbarui', 'success');
await loadData();
} else {
showToast('Gagal memperbarui data', 'error');
}
}
// ── ANGGOTA & BANTUAN dari popup peta ──────────────────────────────────
async function openMapAnggotaModal(idRumah) {
const item = miskinData?.find(m => m.id_rumah === idRumah);
document.getElementById('map_anggota_label').textContent = `ID: ${idRumah} — ${item?.nama_kk || '-'}`;
document.getElementById('mapmodal_anggota').showModal();
const res = await fetch(`{{ url('/api/miskin') }}/${encodeURIComponent(idRumah)}/anggota`, { headers: { 'X-CSRF-TOKEN': csrfToken } });
const list = await res.json();
document.getElementById('map_bodyAnggota').innerHTML = list.length ? list.map(a => `
<tr>
<td class="font-semibold">${esc(a.nama)}</td>
<td class="capitalize text-xs">${esc(a.hubungan.replace(/_/g, ' '))}</td>
<td class="text-xs">${a.umur !== null ? esc(String(a.umur)) + ' th' : '-'}</td>
<td class="capitalize text-xs">${a.pendidikan ? esc(a.pendidikan.replace(/_/g, ' ')) : '-'}</td>
<td><span class="badge badge-xs ${a.status_hidup === 'hidup' ? 'badge-success' : 'badge-error'}">${esc(a.status_hidup)}</span></td>
</tr>`).join('')
: '<tr><td colspan="5" class="text-center py-4 opacity-30 italic">Belum ada anggota</td></tr>';
}
async function openMapBantuanModal(idRumah) {
const item = miskinData?.find(m => m.id_rumah === idRumah);
document.getElementById('map_bantuan_label').textContent = `ID: ${idRumah} — ${item?.nama_kk || '-'}`;
document.getElementById('mapmodal_bantuan').showModal();
const res = await fetch(`{{ url('/api/miskin') }}/${encodeURIComponent(idRumah)}/bantuan`, { headers: { 'X-CSRF-TOKEN': csrfToken } });
const list = await res.json();
document.getElementById('map_bodyBantuan').innerHTML = list.length ? list.map(b => `
<tr>
<td class="text-xs whitespace-nowrap">${esc(b.tanggal_bantuan)}</td>
<td class="font-semibold text-sm">${esc(b.jenis_bantuan)}</td>
<td class="text-xs">${esc(b.ibadah_nama || '-')}</td>
<td class="text-xs">${esc(b.pencatat_nama)}</td>
</tr>`).join('')
: '<tr><td colspan="4" class="text-center py-4 opacity-30 italic">Belum ada riwayat bantuan</td></tr>';
}
// ── NOTIFIKASI PERUBAHAN DATA ────────────────────────────────────────
function updateTransferBadge(count) {
document.querySelectorAll('a[href$="/permohonan"]').forEach(link => {
let badge = link.querySelector('.transfer-pending-badge');
if (count > 0) {
if (!badge) {
badge = document.createElement('span');
badge.className = 'badge badge-error badge-xs transfer-pending-badge';
link.appendChild(document.createTextNode(' '));
link.appendChild(badge);
}
badge.textContent = count;
badge.classList.remove('hidden');
} else if (badge) {
badge.classList.add('hidden');
}
});
}
(function initNotifPolling() {
const POLL_MS = 30000;
const KEY_SEEN = 'notif_latest_seen';
async function pollNotifications() {
try {
const since = localStorage.getItem(KEY_SEEN) || new Date(0).toISOString();
const res = await fetch(
`{{ url('/api/notifications') }}?since=${encodeURIComponent(since)}`,
{ headers: { 'X-CSRF-TOKEN': csrfToken } }
);
if (!res.ok) return;
const data = await res.json();
updateTransferBadge(Number(data.pending_transfer || 0));
const hasChanges = data.new_ibadah > 0 || data.new_miskin > 0 || data.new_anggota > 0 || data.new_bantuan > 0 || data.new_transfer > 0;
if (hasChanges) {
if (data.new_ibadah > 0) showToast(`${data.new_ibadah} rumah ibadah baru ditambahkan`, 'info');
if (data.new_miskin > 0) showToast(`${data.new_miskin} keluarga miskin baru ditambahkan`, 'info');
if (data.new_anggota > 0) showToast(`${data.new_anggota} anggota keluarga baru ditambahkan`, 'info');
if (data.new_bantuan > 0) showToast(`${data.new_bantuan} bantuan baru dicatat`, 'info');
if (data.new_transfer > 0) showToast(`${data.new_transfer} permohonan transfer baru`, 'info');
const fn = typeof loadAllData !== 'undefined' ? loadAllData
: typeof loadData !== 'undefined' ? loadData
: typeof loadDashboard !== 'undefined' ? loadDashboard
: null;
if (fn) fn();
}
const latest = [data.latest_ibadah, data.latest_miskin, data.latest_anggota, data.latest_bantuan, data.latest_transfer]
.filter(Boolean)
.sort()
.pop();
if (latest) localStorage.setItem(KEY_SEEN, latest);
} catch (e) { /* silent */ }
}
document.addEventListener('DOMContentLoaded', () => {
if (!localStorage.getItem(KEY_SEEN)) localStorage.setItem(KEY_SEEN, new Date().toISOString());
setInterval(pollNotifications, POLL_MS);
});
})();
// ── TOAST ──────────────────────────────────────────────────────────
function showToast(msg, type = 'info') {
const t = document.createElement('div');
const alertCls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
t.className = `alert ${alertCls} shadow-lg py-3 text-xs font-bold`;
t.innerHTML = `<span><i class="fa-solid fa-circle-info"></i> ${msg}</span>`;
document.getElementById('toastBox').appendChild(t);
setTimeout(() => t.remove(), 3000);
}
</script>
</body>
</html>