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
+166 -44
View File
@@ -3,7 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Peta Interaktif - WebGIS Miskin & Ibadah</title>
<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">
@@ -72,7 +73,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">
@@ -88,6 +89,32 @@
</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>
@@ -177,17 +204,49 @@
</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>
<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>
<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-20" placeholder="Alamat lengkap..."></textarea>
<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">Radius Default Analysis (Meter)</span></div>
<input type="number" id="ib_radius" value="500" class="input input-bordered 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">
@@ -205,13 +264,19 @@
</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-20" placeholder="Alamat lengkap..."></textarea>
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">ID Rumah / Nama Pemilik</span></div>
<input type="text" id="rm_id" placeholder="Contoh: RM-001" class="input input-bordered w-full" />
<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">
@@ -223,6 +288,28 @@
<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>
@@ -249,6 +336,11 @@
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
<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');
// ── CONFIG & STATE ──────────────────────────────────────────────────
let map, currentMode = 'view';
let layerIbadah = L.layerGroup(), layerMiskin = L.layerGroup();
@@ -261,10 +353,13 @@
const icons = {
ibadah: (color = '#22c55e') => L.divIcon({
html: `<div style="background:${color};width:32px;height:32px;border-radius:32px 32px 0 32px;border:3px solid white;transform:rotate(-45deg);display:flex;align-items:center;justify-center;box-shadow:0 4px 10px rgba(0,0,0,0.3);position:relative">
<i class="fa-solid fa-mosque" style="transform:rotate(45deg);color:white;font-size:14px;margin-left:6px;margin-top:2px"></i>
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: [32, 32], iconAnchor: [0, 32], popupAnchor: [16, -35]
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 : ''}">
@@ -282,6 +377,14 @@
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'));
}
});
function initMap() {
@@ -505,7 +608,7 @@
try {
const res = await fetch(`{{ url('/api/ibadah') }}/${activeIbadah.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({
id: activeIbadah.id,
nama: activeIbadah.nama,
@@ -530,22 +633,31 @@
// ── POPUPS ──────────────────────────────────────────────────────────
function buildPopupIbadah(item) {
return `<div class="p-1">
<div class="font-bold text-sm">🕌 ${item.nama}</div>
<div class="text-[10px] text-gray-500 mb-2">${item.alamat}</div>
return `<div class="p-2 min-w-[200px]">
<div class="font-bold text-base text-primary">🕌 ${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">
<button class="btn btn-xs btn-outline btn-error" onclick="prepDelete('ibadah', ${item.id})">Hapus</button>
${canEditIbadah ? `<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-1">
<div class="font-bold text-sm">🏠 ID: ${item.id_rumah}</div>
<div class="text-[10px] text-gray-500 mb-1">${item.alamat || 'Alamat tidak tersedia'}</div>
<div class="text-[10px] text-gray-400">KK: ${item.jumlah_kk} | Jiwa: ${item.jumlah_orang}</div>
return `<div class="p-2 min-w-[200px]">
<div class="font-bold text-base text-error">🏠 ${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">
<button class="btn btn-xs btn-outline btn-error" onclick="prepDelete('miskin', '${item.id_rumah}')">Hapus</button>
${canEditMiskin ? `<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')">Hapus</button>` : ''}
</div>
</div>`;
}
@@ -555,17 +667,22 @@
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
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' },
body: JSON.stringify(data)
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) {
@@ -580,18 +697,23 @@
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
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' },
body: JSON.stringify(data)
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) {
@@ -609,10 +731,10 @@
: { 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' },
body: JSON.stringify(payload)
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) {
@@ -637,9 +759,9 @@
document.getElementById('confirmDeleteBtn').onclick = async () => {
const { type, id } = deleteTarget;
const res = await fetch(`{{ url('/api') }}/${type}/${id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
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) {