Files
2026-06-10 18:16:46 +07:00

1033 lines
59 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>Data Tabular - WebGIS Pemetaan Kemiskinan</title>
<!-- 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>
* { font-family: 'Plus Jakarta Sans', sans-serif; }
body { font-weight: 500; background-color: #f8fafc; }
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
</style>
<link rel="stylesheet" href="{{ asset('css/genesis.css') }}">
</head>
<body class="min-h-screen app-body">
@include('partials.sidebar')
<!-- ═══ MAIN CONTENT ════════════════════════════════════════════════════ -->
<main class="app-main">
<div class="max-w-screen-xl mx-auto px-4 py-8">
<!-- Header -->
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
<div>
<h1 class="text-3xl font-bold flex items-center gap-3">
<i class="fa-solid fa-database text-primary"></i>
Manajemen Data
</h1>
<p class="text-base-content/50 mt-1">Daftar rumah ibadah dan keluarga miskin dalam matriks tabular.</p>
</div>
<div class="flex flex-wrap gap-2">
<div class="join">
<input class="input input-bordered join-item input-md w-64" placeholder="Cari data..." id="searchInput" onkeyup="filterData()"/>
<button class="btn btn-md join-item btn-ghost border-base-300"><i class="fa-solid fa-search"></i></button>
</div>
@if(in_array(auth()->user()->getRoleNames()->first(), ['administrator', 'petugas_pendataan', 'pengurus_ibadah', 'pemangku_kebijakan']))
<button onclick="exportCSV()" class="btn btn-md btn-outline border-base-300 gap-2">
<i class="fa-solid fa-file-csv"></i> Ekspor CSV
</button>
<a href="{{ route('export.pdf.all') }}" target="_blank" class="btn btn-md btn-outline border-base-300 gap-2">
<i class="fa-solid fa-file-pdf text-error"></i> Ekspor PDF
</a>
@endif
@if(auth()->user()->hasRole('administrator'))
<button onclick="document.getElementById('modal_import').showModal()" class="btn btn-md btn-outline border-base-300 gap-2">
<i class="fa-solid fa-file-import text-info"></i> Import CSV
</button>
@endif
<a href="{{ url('/map') }}" class="btn btn-md btn-primary shadow-lg gap-2">
<i class="fa-solid fa-plus"></i> Tambah Data
</a>
</div>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="stat bg-white shadow rounded-2xl border border-base-200">
<div class="stat-figure text-success text-3xl"><i class="fa-solid fa-mosque"></i></div>
<div class="stat-title text-xs uppercase font-bold opacity-50">Total Rumah Ibadah</div>
<div class="stat-value text-success" id="countIbadah">-</div>
</div>
<div class="stat bg-white shadow rounded-2xl border border-base-200">
<div class="stat-figure text-error text-3xl"><i class="fa-solid fa-house-user"></i></div>
<div class="stat-title text-xs uppercase font-bold opacity-50">Total Rumah Miskin</div>
<div class="stat-value text-error" id="countMiskin">-</div>
</div>
<div class="stat bg-white shadow rounded-2xl border border-base-200">
<div class="stat-figure text-warning text-3xl"><i class="fa-solid fa-users"></i></div>
<div class="stat-title text-xs uppercase font-bold opacity-50">Total Jiwa Terdata</div>
<div class="stat-value text-warning" id="countJiwa">-</div>
</div>
</div>
<!-- Tabs -->
<div class="genesis-tabs">
<div class="genesis-tab-list" role="tablist" aria-label="Jenis data">
<button type="button" class="genesis-tab-button is-active" id="tab_ibadah" role="tab" aria-selected="true" aria-controls="panel_ibadah" onclick="setDataTab('ibadah')">
Rumah Ibadah
</button>
<button type="button" class="genesis-tab-button" id="tab_miskin" role="tab" aria-selected="false" aria-controls="panel_miskin" onclick="setDataTab('miskin')">
Rumah Miskin
</button>
</div>
<div id="panel_ibadah" role="tabpanel" aria-labelledby="tab_ibadah" class="genesis-tab-panel">
@if(auth()->user()->hasRole('administrator'))
<div id="bulkBar_ibadah" class="hidden alert alert-warning py-2 mb-2 flex items-center justify-between">
<span class="text-sm font-semibold"><span id="bulkCount_ibadah">0</span> item terpilih</span>
<button onclick="bulkDelete('ibadah')" class="btn btn-xs btn-error text-white"><i class="fa-solid fa-trash mr-1"></i>Hapus Terpilih</button>
</div>
@endif
<div class="overflow-x-auto">
<table class="table table-zebra w-full" id="tableIbadah">
<thead>
<tr class="text-xs font-bold uppercase opacity-50">
@if(auth()->user()->hasRole('administrator'))
<th><input type="checkbox" class="checkbox checkbox-xs" onchange="toggleSelectAll('ibadah', this.checked)"></th>
@endif
<th>#</th>
<th>Nama</th>
<th>Jenis</th>
<th>Pengurus/Telp</th>
<th>Radius</th>
<th>Koordinat</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody id="bodyIbadah"></tbody>
</table>
</div>
</div>
<div id="panel_miskin" role="tabpanel" aria-labelledby="tab_miskin" class="genesis-tab-panel hidden">
@if(auth()->user()->hasRole('administrator'))
<div id="bulkBar_miskin" class="hidden alert alert-warning py-2 mb-2 flex items-center justify-between">
<span class="text-sm font-semibold"><span id="bulkCount_miskin">0</span> item terpilih</span>
<button onclick="bulkDelete('miskin')" class="btn btn-xs btn-error text-white"><i class="fa-solid fa-trash mr-1"></i>Hapus Terpilih</button>
</div>
@endif
<div class="overflow-x-auto">
<table class="table table-zebra w-full" id="tableMiskin">
<thead>
<tr class="text-xs font-bold uppercase opacity-50">
@if(auth()->user()->hasRole('administrator'))
<th><input type="checkbox" class="checkbox checkbox-xs" onchange="toggleSelectAll('miskin', this.checked)"></th>
@endif
<th>ID</th>
<th>Nama KK</th>
<th>KK/Jiwa</th>
<th>Pekerjaan</th>
<th>Kondisi</th>
<th>Koordinat</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody id="bodyMiskin"></tbody>
</table>
</div>
</div>
</div>
</div>
</main>
<!-- 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: 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-triangle-exclamation"></i></div>
<h3 class="font-bold text-xl mb-2">Hapus Item?</h3>
<p class="text-sm text-base-content/60 mb-6">Penghapusan tidak dapat dibatalkan.</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">Konfirmasi Hapus</button>
</div>
</div>
</dialog>
<!-- Modal: Import CSV -->
<dialog id="modal_import" class="modal">
<div class="modal-box w-11/12 max-w-md">
<h3 class="font-bold text-lg mb-4"><i class="fa-solid fa-file-import mr-2 text-info"></i>Import Data dari CSV</h3>
<div class="form-control mb-3">
<label class="label"><span class="label-text font-bold">Jenis Data</span></label>
<select id="import_type" class="select select-bordered w-full">
<option value="miskin">Keluarga Miskin</option>
<option value="ibadah">Rumah Ibadah</option>
</select>
</div>
<div class="form-control mb-3">
<label class="label"><span class="label-text font-bold">File CSV</span></label>
<input id="import_file" type="file" accept=".csv,text/csv" class="file-input file-input-bordered w-full">
</div>
<div class="text-xs text-base-content/60 bg-base-200 p-3 rounded">
<strong>Format kolom (sama dengan hasil Ekspor CSV):</strong><br>
<span id="import_format_hint"></span>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_import').close()" class="btn btn-ghost">Batal</button>
<button onclick="submitImport()" class="btn btn-info text-white"><i class="fa-solid fa-upload mr-1"></i>Import</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal Anggota Keluarga -->
<dialog id="modal_anggota" class="modal">
<div class="modal-box w-11/12 max-w-3xl">
<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="anggota_rumah_label" class="text-xs opacity-50 mb-4">-</p>
<div class="overflow-x-auto mb-4">
<table class="table table-sm w-full">
<thead><tr class="text-xs uppercase opacity-50"><th>Nama</th><th>Hubungan</th><th>L/P</th><th>Umur</th><th>Pendidikan</th><th>Status</th><th></th></tr></thead>
<tbody id="bodyAnggota"><tr><td colspan="7" class="text-center py-4 opacity-30 italic">Memuat...</td></tr></tbody>
</table>
</div>
@if(auth()->user()->hasRole('administrator') || auth()->user()->hasRole('petugas_pendataan'))
<div class="collapse collapse-arrow border border-base-300 rounded-lg">
<input type="checkbox" id="toggleFormAnggota">
<div class="collapse-title text-sm font-bold"><i class="fa-solid fa-user-pen mr-2 text-info"></i><span id="formAnggotaTitle">Tambah Anggota</span></div>
<div class="collapse-content">
<input type="hidden" id="anggota_edit_id">
<div class="grid grid-cols-2 gap-3 mt-2">
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Nama</span></label>
<input id="anggota_nama" type="text" class="input input-bordered w-full" placeholder="Nama lengkap">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Hubungan</span></label>
<select id="anggota_hubungan" class="select select-bordered w-full">
<option value="kepala_keluarga">Kepala Keluarga</option>
<option value="istri">Istri</option>
<option value="anak">Anak</option>
<option value="orang_tua">Orang Tua</option>
<option value="lainnya" selected>Lainnya</option>
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Jenis Kelamin</span></label>
<select id="anggota_kelamin" class="select select-bordered w-full">
<option value="">-- pilih --</option>
<option value="laki_laki">Laki-laki</option>
<option value="perempuan">Perempuan</option>
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Tanggal Lahir</span></label>
<input id="anggota_tgl_lahir" type="date" class="input input-bordered w-full">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Pendidikan</span></label>
<select id="anggota_pendidikan" class="select select-bordered w-full">
<option value="">-- pilih --</option>
<option value="tidak_sekolah">Tidak Sekolah</option>
<option value="sd">SD</option>
<option value="smp">SMP</option>
<option value="sma">SMA</option>
<option value="d3">D3</option>
<option value="s1_plus">S1+</option>
</select>
</div>
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Status Hidup</span></label>
<select id="anggota_status" class="select select-bordered w-full">
<option value="hidup">Hidup</option>
<option value="meninggal">Meninggal</option>
</select>
</div>
</div>
<div class="flex gap-2 mt-3">
<button onclick="resetAnggotaForm()" class="btn btn-ghost btn-sm flex-1">Reset</button>
<button onclick="simpanAnggota()" class="btn btn-info btn-sm flex-1"><i class="fa-solid fa-floppy-disk mr-1"></i><span id="btnSimpanAnggotaText">Simpan Anggota</span></button>
</div>
</div>
</div>
@endif
<div class="modal-action">
<button onclick="document.getElementById('modal_anggota').close()" class="btn btn-ghost">Tutup</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal Riwayat Bantuan -->
<dialog id="modal_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="bantuan_rumah_label" class="text-xs opacity-50 mb-4">-</p>
<div class="overflow-x-auto mb-4">
<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><th></th></tr></thead>
<tbody id="bodyBantuan"><tr><td colspan="5" class="text-center py-4 opacity-30 italic">Memuat...</td></tr></tbody>
</table>
</div>
@if(auth()->user()->hasRole('administrator') || auth()->user()->hasRole('petugas_pendataan') || auth()->user()->hasRole('pengurus_ibadah'))
<div class="collapse collapse-arrow border border-base-300 rounded-lg">
<input type="checkbox" id="toggleFormBantuan">
<div class="collapse-title text-sm font-bold"><i class="fa-solid fa-plus mr-2 text-success"></i>Catat Bantuan Baru</div>
<div class="collapse-content">
<div class="grid grid-cols-2 gap-3 mt-2">
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Jenis Bantuan</span></label>
<input id="bantuan_jenis" type="text" class="input input-bordered w-full" placeholder="Contoh: sembako, uang, pakaian">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Tanggal</span></label>
<input id="bantuan_tanggal" type="date" class="input input-bordered w-full">
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Dari Rumah Ibadah</span></label>
<select id="bantuan_ibadah" class="select select-bordered w-full">
<option value="">-- Pilih (opsional) --</option>
</select>
</div>
<div class="form-control col-span-2">
<label class="label"><span class="label-text font-bold">Keterangan</span></label>
<textarea id="bantuan_ket" class="textarea textarea-bordered w-full" rows="2" placeholder="Keterangan tambahan"></textarea>
</div>
</div>
<button onclick="simpanBantuan()" class="btn btn-success btn-sm mt-3 w-full"><i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Bantuan</button>
</div>
</div>
@endif
<div class="modal-action">
<button onclick="document.getElementById('modal_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>
<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');
const isAdmin = authUserRole === 'administrator';
let selectedIbadah = new Set();
let selectedMiskin = new Set();
let ibadahData = [], miskinData = [];
let activeDataTab = 'ibadah';
document.addEventListener('DOMContentLoaded', loadAllData);
const searchInput = document.getElementById('searchInput');
function setDataTab(type) {
activeDataTab = type;
const isIbadah = type === 'ibadah';
document.getElementById('tab_ibadah').classList.toggle('is-active', isIbadah);
document.getElementById('tab_miskin').classList.toggle('is-active', !isIbadah);
document.getElementById('tab_ibadah').setAttribute('aria-selected', isIbadah ? 'true' : 'false');
document.getElementById('tab_miskin').setAttribute('aria-selected', isIbadah ? 'false' : 'true');
document.getElementById('panel_ibadah').classList.toggle('hidden', !isIbadah);
document.getElementById('panel_miskin').classList.toggle('hidden', isIbadah);
}
async function loadAllData() {
try {
const [ri, rm] = await Promise.all([
fetch('{{ url('/api/ibadah') }}').then(r => r.json()),
fetch('{{ url('/api/miskin') }}').then(r => r.json())
]);
ibadahData = ri;
miskinData = rm;
renderTables();
} catch (e) { showToast('Gagal memuat data', 'error'); }
}
function renderTables(filteredI = null, filteredM = null) {
const dataI = filteredI || ibadahData;
const dataM = filteredM || miskinData;
// Stats
document.getElementById('countIbadah').innerText = ibadahData.length;
document.getElementById('countMiskin').innerText = miskinData.length;
document.getElementById('countJiwa').innerText = miskinData.reduce((acc, h) => acc + parseInt(h.jumlah_orang), 0);
// Ibadah Table
const bodyI = document.getElementById('bodyIbadah');
bodyI.innerHTML = dataI.length ? dataI.map((item, i) => {
const canEditThis = canEditIbadah && (authUserRole === 'administrator' || item.user_id === authUserId);
return `
<tr>
${isAdmin ? `<td><input type="checkbox" class="checkbox checkbox-xs" ${selectedIbadah.has(String(item.id)) ? 'checked' : ''} onchange="toggleSelect('ibadah', '${item.id}', this.checked)"></td>` : ''}
<td class="text-xs font-bold opacity-30">${i + 1}</td>
<td>
<div class="font-bold text-primary">${item.nama}</div>
<div class="text-[10px] opacity-50 truncate max-w-[200px]">${item.alamat}</div>
</td>
<td class="capitalize text-xs">${item.jenis || '-'}</td>
<td class="text-xs">
<div class="font-semibold">${item.pengurus || '-'}</div>
<div class="opacity-50">${item.telepon || '-'}</div>
</td>
<td><span class="badge badge-outline badge-sm">${item.radius}m</span></td>
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(4)}, ${parseFloat(item.longitude).toFixed(4)}</td>
<td class="text-center">
<div class="flex gap-1 justify-center">
${canEditThis ? `<button class="btn btn-xs btn-warning btn-outline" onclick="openEditIbadahById(${item.id})"><i class="fa-solid fa-pen"></i></button>` : ''}
${canEditThis ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})"><i class="fa-solid fa-trash"></i></button>` : ''}
</div>
</td>
</tr>`;
}).join('') : '<tr><td colspan="8" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
// Miskin Table
const bodyM = document.getElementById('bodyMiskin');
bodyM.innerHTML = dataM.length ? dataM.map(item => `
<tr>
${isAdmin ? `<td><input type="checkbox" class="checkbox checkbox-xs" ${selectedMiskin.has(String(item.id_rumah)) ? 'checked' : ''} onchange="toggleSelect('miskin', '${item.id_rumah}', this.checked)"></td>` : ''}
<td class="font-bold text-error">${item.id_rumah}</td>
<td>
<div class="font-bold">${item.nama_kk || '-'}</div>
<div class="text-[10px] opacity-50 truncate max-w-[200px]">${item.alamat || '-'}</div>
</td>
<td>
<div class="badge badge-warning badge-xs">${item.jumlah_kk} KK</div>
<div class="badge badge-info badge-xs">${item.jumlah_orang} Jiwa</div>
</td>
<td class="text-xs">${item.pekerjaan || '-'}</td>
<td class="capitalize text-xs">${item.kondisi_rumah || '-'}</td>
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(4)}, ${parseFloat(item.longitude).toFixed(4)}</td>
<td class="text-center">
<div class="flex gap-1 justify-center flex-wrap">
<button class="btn btn-xs btn-info btn-outline" onclick="openAnggotaModal('${item.id_rumah}')" title="Anggota Keluarga"><i class="fa-solid fa-users"></i></button>
<button class="btn btn-xs btn-success btn-outline" onclick="openBantuanModal('${item.id_rumah}')" title="Riwayat Bantuan"><i class="fa-solid fa-hand-holding-heart"></i></button>
${canEditMiskin ? `<button class="btn btn-xs btn-warning btn-outline" onclick="openEditMiskinById('${item.id_rumah}')"><i class="fa-solid fa-pen"></i></button>` : ''}
${canEditMiskin ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')"><i class="fa-solid fa-trash"></i></button>` : ''}
</div>
</td>
</tr>
`).join('') : '<tr><td colspan="8" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
}
function filterData() {
const query = searchInput.value.toLowerCase();
const filteredI = ibadahData.filter(i =>
i.nama.toLowerCase().includes(query) ||
(i.alamat && i.alamat.toLowerCase().includes(query)) ||
(i.pengurus && i.pengurus.toLowerCase().includes(query))
);
const filteredM = miskinData.filter(m =>
(m.nama_kk && m.nama_kk.toLowerCase().includes(query)) ||
(m.alamat && m.alamat.toLowerCase().includes(query)) ||
m.id_rumah.toLowerCase().includes(query)
);
renderTables(filteredI, filteredM);
}
function exportCSV() {
let csvContent = "data:text/csv;charset=utf-8,";
if (activeDataTab === 'ibadah') {
csvContent += "ID,Nama,Jenis,Alamat,Radius,Jemaah,Pengurus,Telepon,Lat,Lon\n";
ibadahData.forEach(i => {
csvContent += `${i.id},"${i.nama}","${i.jenis}","${i.alamat}",${i.radius},${i.jumlah_jemaah},"${i.pengurus}","${i.telepon}",${i.latitude},${i.longitude}\n`;
});
downloadFile(csvContent, "data_rumah_ibadah.csv");
} else {
csvContent += "ID Rumah,Nama KK,Alamat,KK,Jiwa,Pekerjaan,Pendapatan,Kondisi,Lat,Lon\n";
miskinData.forEach(m => {
csvContent += `"${m.id_rumah}","${m.nama_kk}","${m.alamat}",${m.jumlah_kk},${m.jumlah_orang},"${m.pekerjaan}",${m.pendapatan},"${m.kondisi_rumah}",${m.latitude},${m.longitude}\n`;
});
downloadFile(csvContent, "data_rumah_miskin.csv");
}
}
function downloadFile(content, filename) {
const encodedUri = encodeURI(content);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// ── 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 loadAllData();
} 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 loadAllData();
} else {
showToast('Gagal memperbarui data', 'error');
}
}
// ── CSV IMPORT ──────────────────────────────────────────────
const importHints = {
miskin: 'ID Rumah, Nama KK, Alamat, KK, Jiwa, Pekerjaan, Pendapatan, Kondisi, Lat, Lon',
ibadah: 'ID (diabaikan), Nama, Jenis, Alamat, Radius, Jemaah, Pengurus, Telepon, Lat, Lon',
};
document.getElementById('import_type')?.addEventListener('change', e => {
document.getElementById('import_format_hint').textContent = importHints[e.target.value];
});
document.addEventListener('DOMContentLoaded', () => {
const h = document.getElementById('import_format_hint');
if (h) h.textContent = importHints.miskin;
});
async function submitImport() {
const type = document.getElementById('import_type').value;
const fileInput = document.getElementById('import_file');
if (!fileInput.files.length) { showToast('Pilih file CSV dulu', 'error'); return; }
const fd = new FormData();
fd.append('file', fileInput.files[0]);
const res = await fetch(`{{ url('/api/admin/import') }}/${type}`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken },
body: fd,
});
const json = await res.json();
if (json.success) {
document.getElementById('modal_import').close();
fileInput.value = '';
showToast(`Import selesai: ${json.imported} berhasil, ${json.failed} gagal`, 'success');
loadAllData();
} else {
showToast(json.message || 'Import gagal', 'error');
}
}
// ── BULK DELETE ─────────────────────────────────────────────
function toggleSelect(type, id, checked) {
const set = type === 'ibadah' ? selectedIbadah : selectedMiskin;
if (checked) set.add(String(id)); else set.delete(String(id));
updateBulkBar(type);
}
function toggleSelectAll(type, checked) {
const set = type === 'ibadah' ? selectedIbadah : selectedMiskin;
set.clear();
if (checked) {
(type === 'ibadah' ? ibadahData : miskinData).forEach(it =>
set.add(String(type === 'ibadah' ? it.id : it.id_rumah)));
}
renderTables();
updateBulkBar(type);
}
function updateBulkBar(type) {
const set = type === 'ibadah' ? selectedIbadah : selectedMiskin;
const bar = document.getElementById(`bulkBar_${type}`);
if (!bar) return;
bar.classList.toggle('hidden', set.size === 0);
document.getElementById(`bulkCount_${type}`).textContent = set.size;
}
async function bulkDelete(type) {
const set = type === 'ibadah' ? selectedIbadah : selectedMiskin;
const ids = [...set];
if (!ids.length) return;
if (!confirm(`Hapus ${ids.length} data terpilih secara permanen?`)) return;
const res = await fetch(`{{ url('/api/admin') }}/${type}/bulk-delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({ ids }),
});
const json = await res.json();
if (json.success) {
showToast(`${json.deleted} data dihapus`, 'success');
set.clear();
updateBulkBar(type);
loadAllData();
} else {
showToast('Gagal hapus massal', 'error');
}
}
let deleteTarget = null;
function prepDelete(type, id) {
deleteTarget = { type, id };
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();
loadAllData();
} else showToast('Gagal menghapus', 'error');
};
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 mr-2"></i>${msg}</span>`;
document.getElementById('toastBox').appendChild(t);
setTimeout(() => t.remove(), 3000);
}
// ── ANGGOTA KELUARGA ───────────────────────────────────────────────────
let currentAnggotaRumah = null;
let anggotaData = [];
const canEditAnggota = {{ auth()->user()->hasRole('administrator') || auth()->user()->hasRole('petugas_pendataan') ? 'true' : 'false' }};
async function openAnggotaModal(idRumah) {
currentAnggotaRumah = idRumah;
const rm = miskinData.find(m => m.id_rumah === idRumah);
document.getElementById('anggota_rumah_label').textContent = `ID: ${idRumah} — ${rm?.nama_kk || '-'}`;
resetAnggotaForm();
document.getElementById('modal_anggota').showModal();
await loadAnggota(idRumah);
}
async function loadAnggota(idRumah) {
const res = await fetch(`{{ url('/api/miskin') }}/${encodeURIComponent(idRumah)}/anggota`, { headers: { 'X-CSRF-TOKEN': csrfToken } });
anggotaData = await res.json();
document.getElementById('bodyAnggota').innerHTML = anggotaData.length ? anggotaData.map(a => `
<tr>
<td class="font-semibold">${esc(a.nama)}</td>
<td class="capitalize text-xs">${esc(a.hubungan.replace('_', ' '))}</td>
<td class="text-xs">${a.jenis_kelamin === 'laki_laki' ? 'L' : a.jenis_kelamin === 'perempuan' ? 'P' : '-'}</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>
<td>${canEditAnggota ? `<div class="flex justify-end gap-1">
<button onclick="editAnggota(${a.id})" class="btn btn-xs btn-ghost text-warning" title="Edit"><i class="fa-solid fa-pen"></i></button>
<button onclick="deleteAnggota(${a.id})" class="btn btn-xs btn-ghost text-error" title="Hapus"><i class="fa-solid fa-trash"></i></button>
</div>` : ''}</td>
</tr>`).join('')
: '<tr><td colspan="7" class="text-center py-4 opacity-30 italic">Belum ada anggota</td></tr>';
}
function resetAnggotaForm() {
const editId = document.getElementById('anggota_edit_id');
if (!editId) return;
editId.value = '';
document.getElementById('formAnggotaTitle').textContent = 'Tambah Anggota';
document.getElementById('btnSimpanAnggotaText').textContent = 'Simpan Anggota';
document.getElementById('anggota_nama').value = '';
document.getElementById('anggota_hubungan').value = 'lainnya';
document.getElementById('anggota_kelamin').value = '';
document.getElementById('anggota_tgl_lahir').value = '';
document.getElementById('anggota_pendidikan').value = '';
document.getElementById('anggota_status').value = 'hidup';
}
function editAnggota(id) {
const item = anggotaData.find(a => Number(a.id) === Number(id));
if (!item) return;
document.getElementById('anggota_edit_id').value = item.id;
document.getElementById('formAnggotaTitle').textContent = 'Edit Anggota';
document.getElementById('btnSimpanAnggotaText').textContent = 'Simpan Perubahan';
document.getElementById('anggota_nama').value = item.nama || '';
document.getElementById('anggota_hubungan').value = item.hubungan || 'lainnya';
document.getElementById('anggota_kelamin').value = item.jenis_kelamin || '';
document.getElementById('anggota_tgl_lahir').value = item.tanggal_lahir || '';
document.getElementById('anggota_pendidikan').value = item.pendidikan || '';
document.getElementById('anggota_status').value = item.status_hidup || 'hidup';
document.getElementById('toggleFormAnggota').checked = true;
}
async function simpanAnggota() {
const editId = document.getElementById('anggota_edit_id')?.value || '';
const body = {
nama: document.getElementById('anggota_nama').value.trim(),
hubungan: document.getElementById('anggota_hubungan').value,
jenis_kelamin: document.getElementById('anggota_kelamin').value || null,
tanggal_lahir: document.getElementById('anggota_tgl_lahir').value || null,
pendidikan: document.getElementById('anggota_pendidikan').value || null,
status_hidup: document.getElementById('anggota_status').value,
};
if (!body.nama) { showToast('Nama wajib diisi', 'error'); return; }
const url = editId
? `{{ url('/api/miskin') }}/${encodeURIComponent(currentAnggotaRumah)}/anggota/${editId}`
: `{{ url('/api/miskin') }}/${encodeURIComponent(currentAnggotaRumah)}/anggota`;
const res = await fetch(url, {
method: editId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) {
showToast(editId ? 'Anggota diperbarui' : 'Anggota ditambahkan', 'success');
resetAnggotaForm();
loadAnggota(currentAnggotaRumah);
} else {
showToast(editId ? 'Gagal memperbarui anggota' : 'Gagal menambah anggota', 'error');
}
}
async function deleteAnggota(id) {
if (!confirm('Hapus anggota ini?')) return;
const res = await fetch(`{{ url('/api/miskin') }}/${encodeURIComponent(currentAnggotaRumah)}/anggota/${id}`, {
method: 'DELETE', headers: { 'X-CSRF-TOKEN': csrfToken },
});
const json = await res.json();
if (json.success) { showToast('Anggota dihapus', 'success'); loadAnggota(currentAnggotaRumah); }
else showToast('Gagal hapus', 'error');
}
// ── RIWAYAT BANTUAN ────────────────────────────────────────────────────
let currentBantuanRumah = null;
const canDeleteBantuan = {{ auth()->user()->hasRole('administrator') ? 'true' : 'false' }};
const currentUserId = Number({{ auth()->id() ?? 0 }});
const isPengurusIbadah = {{ auth()->user()->hasRole('pengurus_ibadah') ? 'true' : 'false' }};
async function openBantuanModal(idRumah) {
currentBantuanRumah = idRumah;
const rm = miskinData.find(m => m.id_rumah === idRumah);
document.getElementById('bantuan_rumah_label').textContent = `ID: ${idRumah} — ${rm?.nama_kk || '-'}`;
const sel = document.getElementById('bantuan_ibadah');
if (sel) {
const availableIbadah = isPengurusIbadah
? ibadahData.filter(ib => Number(ib.user_id) === currentUserId)
: ibadahData;
sel.innerHTML = '<option value="">-- Pilih (opsional) --</option>' +
availableIbadah.map(ib => `<option value="${ib.id}">${esc(ib.nama)}</option>`).join('');
}
const tgl = document.getElementById('bantuan_tanggal');
if (tgl) tgl.value = new Date().toISOString().split('T')[0];
document.getElementById('modal_bantuan').showModal();
await loadBantuan(idRumah);
}
async function loadBantuan(idRumah) {
const res = await fetch(`{{ url('/api/miskin') }}/${encodeURIComponent(idRumah)}/bantuan`, { headers: { 'X-CSRF-TOKEN': csrfToken } });
const list = await res.json();
document.getElementById('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>
<td>${canDeleteBantuan ? `<button onclick="deleteBantuan(${b.id})" class="btn btn-xs btn-ghost text-error"><i class="fa-solid fa-trash"></i></button>` : ''}</td>
</tr>`).join('')
: '<tr><td colspan="5" class="text-center py-4 opacity-30 italic">Belum ada riwayat bantuan</td></tr>';
}
async function simpanBantuan() {
const body = {
jenis_bantuan: document.getElementById('bantuan_jenis').value.trim(),
tanggal_bantuan: document.getElementById('bantuan_tanggal').value,
keterangan: document.getElementById('bantuan_ket').value.trim() || null,
ibadah_id: document.getElementById('bantuan_ibadah').value || null,
};
if (!body.jenis_bantuan || !body.tanggal_bantuan) { showToast('Jenis bantuan dan tanggal wajib diisi', 'error'); return; }
const res = await fetch(`{{ url('/api/miskin') }}/${encodeURIComponent(currentBantuanRumah)}/bantuan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.success) { showToast('Bantuan dicatat', 'success'); loadBantuan(currentBantuanRumah); }
else showToast('Gagal menyimpan bantuan', 'error');
}
async function deleteBantuan(id) {
if (!confirm('Hapus riwayat bantuan ini?')) return;
const res = await fetch(`{{ url('/api/miskin') }}/${encodeURIComponent(currentBantuanRumah)}/bantuan/${id}`, {
method: 'DELETE', headers: { 'X-CSRF-TOKEN': csrfToken },
});
const json = await res.json();
if (json.success) { showToast('Bantuan dihapus', 'success'); loadBantuan(currentBantuanRumah); }
else showToast('Gagal hapus', 'error');
}
// ── 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
: 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);
});
})();
</script>
</body>
</html>