feat: refresh UI and add sidebar navigation
This commit is contained in:
+126
-91
@@ -21,76 +21,15 @@
|
||||
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">
|
||||
<body class="min-h-screen app-body">
|
||||
|
||||
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
|
||||
<div class="navbar bg-base-100 border-b border-base-200 shadow-sm sticky top-0 z-[1000]">
|
||||
<div class="flex-1">
|
||||
<a href="{{ url('/') }}" class="btn btn-ghost text-lg font-bold gap-2 text-primary">
|
||||
<i class="fa-solid fa-house-chimney-crack"></i>
|
||||
WebGIS Pemetaan Kemiskinan
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<ul class="menu menu-horizontal px-1 gap-1">
|
||||
<li><a href="{{ url('/map') }}" class="font-medium"><i class="fa-solid fa-map"></i> Peta</a></li>
|
||||
<li><a href="{{ url('/data') }}" class="font-semibold bg-primary/10 text-primary rounded-lg"><i class="fa-solid fa-table-list"></i> Data Tabular</a></li>
|
||||
<li>
|
||||
<a href="{{ url('/dashboard') }}" class="font-medium">
|
||||
<i class="fa-solid fa-chart-bar"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="{{ url('/permohonan') }}" class="font-medium"><i class="fa-solid fa-right-left"></i> Transfer</a></li>
|
||||
@if(auth()->user()->hasRole('administrator'))
|
||||
<li>
|
||||
<a href="{{ url('/admin/users') }}" class="font-medium">
|
||||
<i class="fa-solid fa-users-gear"></i> Admin
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url('/admin/logs') }}" class="font-medium">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i> Log
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url('/admin/settings') }}" class="font-medium">
|
||||
<i class="fa-solid fa-sliders"></i> Pengaturan
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
<!-- User dropdown -->
|
||||
<div class="dropdown dropdown-end ml-2">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost gap-2">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-8">
|
||||
<span class="text-xs font-bold">{{ substr(auth()->user()->name, 0, 1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="hidden sm:inline text-sm font-semibold">{{ auth()->user()->name }}</span>
|
||||
<i class="fa-solid fa-chevron-down text-xs opacity-50"></i>
|
||||
</div>
|
||||
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-200 mt-2">
|
||||
<li class="menu-title">
|
||||
<span class="text-xs text-base-content/50 capitalize">{{ str_replace('_', ' ', auth()->user()->getRoleNames()->first() ?? 'user') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="text-error w-full text-left flex items-center gap-2">
|
||||
<i class="fa-solid fa-right-from-bracket"></i>
|
||||
Keluar
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@include('partials.sidebar')
|
||||
|
||||
<!-- ═══ MAIN CONTENT ════════════════════════════════════════════════════ -->
|
||||
<main class="max-w-screen-xl mx-auto px-4 py-8">
|
||||
<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">
|
||||
@@ -145,9 +84,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div role="tablist" class="tabs tabs-lifted tabs-lg mb-6">
|
||||
<input type="radio" name="data_tabs" role="tab" class="tab font-bold" aria-label="🕌 Rumah Ibadah" checked />
|
||||
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6 shadow-sm">
|
||||
<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>
|
||||
@@ -175,8 +122,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="radio" name="data_tabs" role="tab" class="tab font-bold" aria-label="🏠 Rumah Miskin" />
|
||||
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6 shadow-sm">
|
||||
<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>
|
||||
@@ -204,6 +150,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modal: Edit Rumah Ibadah -->
|
||||
@@ -374,8 +321,9 @@
|
||||
@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-plus mr-2 text-info"></i>Tambah Anggota</div>
|
||||
<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>
|
||||
@@ -423,7 +371,10 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="simpanAnggota()" class="btn btn-info btn-sm mt-3 w-full"><i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Anggota</button>
|
||||
<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
|
||||
@@ -495,9 +446,21 @@
|
||||
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([
|
||||
@@ -592,10 +555,9 @@
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
const activeTab = document.querySelector('input[name="data_tabs"]:checked').getAttribute('aria-label');
|
||||
let csvContent = "data:text/csv;charset=utf-8,";
|
||||
|
||||
if (activeTab.includes("Ibadah")) {
|
||||
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`;
|
||||
@@ -837,20 +799,22 @@
|
||||
|
||||
// ── 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 } });
|
||||
const list = await res.json();
|
||||
document.getElementById('bodyAnggota').innerHTML = list.length ? list.map(a => `
|
||||
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>
|
||||
@@ -858,12 +822,45 @@
|
||||
<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 ? `<button onclick="deleteAnggota(${a.id})" class="btn btn-xs btn-ghost text-error"><i class="fa-solid fa-trash"></i></button>` : ''}</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,
|
||||
@@ -873,14 +870,22 @@
|
||||
status_hidup: document.getElementById('anggota_status').value,
|
||||
};
|
||||
if (!body.nama) { showToast('Nama wajib diisi', 'error'); return; }
|
||||
const res = await fetch(`{{ url('/api/miskin') }}/${encodeURIComponent(currentAnggotaRumah)}/anggota`, {
|
||||
method: 'POST',
|
||||
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('Anggota ditambahkan', 'success'); loadAnggota(currentAnggotaRumah); }
|
||||
else showToast('Gagal menambah anggota', 'error');
|
||||
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) {
|
||||
@@ -896,6 +901,8 @@
|
||||
// ── 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;
|
||||
@@ -904,8 +911,11 @@
|
||||
|
||||
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>' +
|
||||
ibadahData.map(ib => `<option value="${ib.id}">${esc(ib.nama)}</option>`).join('');
|
||||
availableIbadah.map(ib => `<option value="${ib.id}">${esc(ib.nama)}</option>`).join('');
|
||||
}
|
||||
|
||||
const tgl = document.getElementById('bantuan_tanggal');
|
||||
@@ -958,14 +968,31 @@
|
||||
}
|
||||
|
||||
// ── 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_IBADAH = 'notif_latest_ibadah';
|
||||
const KEY_MISKIN = 'notif_latest_miskin';
|
||||
const KEY_SEEN = 'notif_latest_seen';
|
||||
|
||||
async function pollNotifications() {
|
||||
try {
|
||||
const since = localStorage.getItem(KEY_IBADAH) || new Date(0).toISOString();
|
||||
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 } }
|
||||
@@ -973,22 +1000,30 @@
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
|
||||
if (data.new_ibadah > 0 || data.new_miskin > 0) {
|
||||
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();
|
||||
}
|
||||
if (data.latest_ibadah) localStorage.setItem(KEY_IBADAH, data.latest_ibadah);
|
||||
if (data.latest_miskin) localStorage.setItem(KEY_MISKIN, data.latest_miskin);
|
||||
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_IBADAH)) localStorage.setItem(KEY_IBADAH, new Date().toISOString());
|
||||
if (!localStorage.getItem(KEY_MISKIN)) localStorage.setItem(KEY_MISKIN, new Date().toISOString());
|
||||
if (!localStorage.getItem(KEY_SEEN)) localStorage.setItem(KEY_SEEN, new Date().toISOString());
|
||||
setInterval(pollNotifications, POLL_MS);
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user