feat: add auth navbar and role-aware UI to all views

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
GuavaPopper
2026-06-02 19:34:17 +07:00
parent f609c1efa7
commit 5683cd3b0e
4 changed files with 367 additions and 96 deletions
+162 -50
View File
@@ -3,7 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Data Tabular - WebGIS Miskin & Ibadah</title>
<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">
@@ -28,7 +29,7 @@
<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 Miskin & Ibadah
WebGIS Pemetaan Kemiskinan
</a>
</div>
<div class="flex-none">
@@ -36,6 +37,32 @@
<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>
</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>
@@ -51,8 +78,19 @@
</h1>
<p class="text-base-content/50 mt-1">Daftar rumah ibadah dan keluarga miskin dalam matriks tabular.</p>
</div>
<div class="flex gap-2">
<a href="{{ url('/map') }}" class="btn btn-primary shadow-lg"><i class="fa-solid fa-plus"></i> Tambah Data di Peta</a>
<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>
@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>
@@ -84,9 +122,10 @@
<thead>
<tr class="text-xs font-bold uppercase opacity-50">
<th>#</th>
<th>Nama Rumah Ibadah</th>
<th>Alamat</th>
<th>Radius Analisis</th>
<th>Nama</th>
<th>Jenis</th>
<th>Pengurus/Telp</th>
<th>Radius</th>
<th>Koordinat</th>
<th class="text-center">Aksi</th>
</tr>
@@ -102,10 +141,11 @@
<table class="table table-zebra w-full" id="tableMiskin">
<thead>
<tr class="text-xs font-bold uppercase opacity-50">
<th>ID Rumah</th>
<th>Alamat</th>
<th>Jumlah KK</th>
<th>Jumlah Jiwa</th>
<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>
@@ -134,7 +174,14 @@
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
<script>
const authUserRole = '{{ auth()->user()->getRoleNames()->first() ?? '' }}';
const canEditIbadah = ['administrator', 'pengurus_ibadah'].includes(authUserRole);
const canEditMiskin = ['administrator', 'petugas_pendataan'].includes(authUserRole);
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
let ibadahData = [], miskinData = [];
document.addEventListener('DOMContentLoaded', loadAllData);
const searchInput = document.getElementById('searchInput');
async function loadAllData() {
try {
@@ -142,45 +189,110 @@
fetch('{{ url('/api/ibadah') }}').then(r => r.json()),
fetch('{{ url('/api/miskin') }}').then(r => r.json())
]);
// Stats
document.getElementById('countIbadah').innerText = ri.length;
document.getElementById('countMiskin').innerText = rm.length;
document.getElementById('countJiwa').innerText = rm.reduce((acc, h) => acc + parseInt(h.jumlah_orang), 0);
// Ibadah Table
const bodyI = document.getElementById('bodyIbadah');
bodyI.innerHTML = ri.length ? ri.map((item, i) => `
<tr>
<td class="text-xs font-bold opacity-30">${i + 1}</td>
<td class="font-bold text-primary">${item.nama}</td>
<td class="text-sm opacity-70 max-w-xs truncate">${item.alamat}</td>
<td><span class="badge badge-outline badge-sm">${item.radius}m</span></td>
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(6)}, ${parseFloat(item.longitude).toFixed(6)}</td>
<td class="text-center">
<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})"><i class="fa-solid fa-trash"></i></button>
</td>
</tr>
`).join('') : '<tr><td colspan="6" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
// Miskin Table
const bodyM = document.getElementById('bodyMiskin');
bodyM.innerHTML = rm.length ? rm.map(item => `
<tr>
<td class="font-bold text-error">${item.id_rumah}</td>
<td class="text-xs opacity-70 max-w-xs truncate">${item.alamat || '-'}</td>
<td><span class="badge badge-warning badge-sm">${item.jumlah_kk} KK</span></td>
<td><span class="badge badge-info badge-sm">${item.jumlah_orang} Jiwa</span></td>
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(6)}, ${parseFloat(item.longitude).toFixed(6)}</td>
<td class="text-center">
<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')"><i class="fa-solid fa-trash"></i></button>
</td>
</tr>
`).join('') : '<tr><td colspan="6" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
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) => `
<tr>
<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">
${canEditIbadah ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})"><i class="fa-solid fa-trash"></i></button>` : ''}
</td>
</tr>
`).join('') : '<tr><td colspan="7" 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>
<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">
${canEditMiskin ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')"><i class="fa-solid fa-trash"></i></button>` : ''}
</td>
</tr>
`).join('') : '<tr><td colspan="7" 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() {
const activeTab = document.querySelector('input[name="data_tabs"]:checked').getAttribute('aria-label');
let csvContent = "data:text/csv;charset=utf-8,";
if (activeTab.includes("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);
}
let deleteTarget = null;
function prepDelete(type, id) {
deleteTarget = { type, id };
@@ -189,9 +301,9 @@
document.getElementById('confirmDeleteBtn').onclick = async () => {
const { type, id } = deleteTarget;
const res = await fetch(`{{ url('/api') }}/${type}/${id}`, {
const res = await fetch(`{{ url('/api') }}/${type}/${id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }
});
const resJson = await res.json();
if (resJson.success) {
@@ -205,7 +317,7 @@
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>${msg}</span>`;
t.innerHTML = `<span><i class="fa-solid fa-circle-info mr-2"></i>${msg}</span>`;
document.getElementById('toastBox').appendChild(t);
setTimeout(() => t.remove(), 3000);
}