feat: refresh UI and add sidebar navigation

This commit is contained in:
GuavaPopper
2026-06-10 18:16:46 +07:00
parent c4157cec8d
commit d85d066f6c
14 changed files with 1636 additions and 603 deletions
+55 -108
View File
@@ -22,28 +22,25 @@
<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; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
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: 20px;
left: 20px;
top: 24px;
left: 24px;
z-index: 1000;
width: 300px;
width: 316px;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.glass-card {
background: rgba(255, 255, 255, 0.85);
.map-panel {
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
border: 1px solid #e8e8ec;
}
/* Marker dragging hint */
@@ -57,102 +54,27 @@
/* Custom Layer Control */
.leaflet-control-layers {
border-radius: 12px !important;
border: none !important;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15) !important;
border-radius: 8px !important;
border: 1px solid #e8e8ec !important;
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.85) !important;
background: rgba(255, 255, 255, 0.92) !important;
padding: 8px 12px !important;
}
</style>
<link rel="stylesheet" href="{{ asset('css/genesis.css') }}">
</head>
<body>
<body class="app-body app-map-body">
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
<div class="navbar bg-base-100 border-b border-base-200 shadow-sm flex-shrink-0 z-[1001]">
<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-semibold bg-primary/10 text-primary rounded-lg">
<i class="fa-solid fa-map"></i> Peta Interaktif
</a>
</li>
<li>
<a href="{{ url('/data') }}" class="font-medium">
<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')
<!-- ═══ MAP CONTAINER ════════════════════════════════════════════════════ -->
<div class="map-wrapper">
<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 glass-card p-4">
<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
@@ -161,17 +83,17 @@
<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 btn-success justify-start gap-2" id="btnModeIbadah" onclick="setMode('ibadah')">
<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 btn-error justify-start gap-2" id="btnModeMiskin" onclick="setMode('miskin')">
<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 glass-card p-4 hidden">
<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
@@ -219,11 +141,11 @@
</div>
<!-- Hints -->
<div id="mapHint" class="badge badge-primary badge-lg py-4 px-6 gap-2 shadow-xl hidden">
<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 shadow-xl hidden">
<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>
@@ -855,7 +777,7 @@
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 text-primary">🕌 ${item.nama}</div>
<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">
@@ -871,7 +793,7 @@
function buildPopupMiskin(item) {
return `<div class="p-2 min-w-[200px]">
<div class="font-bold text-base text-error">🏠 ${item.nama_kk || 'Keluarga Miskin'}</div>
<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">
@@ -1132,14 +1054,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 } }
@@ -1147,23 +1086,31 @@
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
: typeof loadDashboard !== 'undefined' ? loadDashboard
: 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);
});
})();