feat: refresh UI and add sidebar navigation
This commit is contained in:
@@ -18,84 +18,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 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-semibold bg-primary/10 text-primary rounded-lg">
|
||||
<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="mb-8">
|
||||
@@ -131,6 +62,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fitur Dosen Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="stat bg-white shadow rounded-2xl border border-base-200">
|
||||
<div class="stat-figure text-secondary text-3xl"><i class="fa-solid fa-people-roof"></i></div>
|
||||
<div class="stat-title text-xs uppercase font-bold opacity-50">Anggota Keluarga</div>
|
||||
<div class="stat-value text-secondary" id="statTotalAnggota">-</div>
|
||||
<div class="stat-desc" id="statAnggotaDesc">hidup / meninggal</div>
|
||||
</div>
|
||||
<div class="stat bg-white shadow rounded-2xl border border-base-200">
|
||||
<div class="stat-figure text-accent text-3xl"><i class="fa-solid fa-graduation-cap"></i></div>
|
||||
<div class="stat-title text-xs uppercase font-bold opacity-50">Pendidikan Terdata</div>
|
||||
<div class="stat-value text-accent" id="statPendidikanTerdata">-</div>
|
||||
<div class="stat-desc">anggota dengan pendidikan</div>
|
||||
</div>
|
||||
<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-hand-holding-heart"></i></div>
|
||||
<div class="stat-title text-xs uppercase font-bold opacity-50">Bantuan Bulan Ini</div>
|
||||
<div class="stat-value text-success" id="statBantuanBulanIni">-</div>
|
||||
<div class="stat-desc" id="statBantuanTotal">dari total bantuan</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-right-left"></i></div>
|
||||
<div class="stat-title text-xs uppercase font-bold opacity-50">Transfer Pending</div>
|
||||
<div class="stat-value text-error" id="statTransferPending">-</div>
|
||||
<div class="stat-desc" id="statTransferDesc">permohonan aktif</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-base-200 p-6">
|
||||
@@ -143,6 +102,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fitur Dosen Charts -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-base-200 p-6">
|
||||
<h2 class="text-sm font-bold uppercase opacity-50 mb-4">Pendidikan Anggota</h2>
|
||||
<canvas id="chartPendidikan" height="220"></canvas>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-base-200 p-6">
|
||||
<h2 class="text-sm font-bold uppercase opacity-50 mb-4">Bantuan per Jenis</h2>
|
||||
<canvas id="chartBantuanJenis" height="220"></canvas>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-base-200 p-6">
|
||||
<h2 class="text-sm font-bold uppercase opacity-50 mb-4">Status Transfer</h2>
|
||||
<canvas id="chartTransfer" height="220"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gap Analysis Section -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-base-200 p-6 mb-8">
|
||||
<h2 class="text-lg font-bold flex items-center gap-2 mb-6">
|
||||
@@ -210,6 +185,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Toast Container -->
|
||||
@@ -231,19 +207,22 @@
|
||||
return turf.distance(p1, p2, { units: 'meters' }) <= radius;
|
||||
}
|
||||
|
||||
let ibadahData = [], miskinData = [];
|
||||
let ibadahData = [], miskinData = [], dosenSummary = null;
|
||||
let chartJenis = null, chartKondisi = null, chartGap = null;
|
||||
let chartPendidikan = null, chartBantuanJenis = null, chartTransfer = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadDashboard);
|
||||
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const [ri, rm] = await Promise.all([
|
||||
const [ri, rm, ds] = await Promise.all([
|
||||
fetch('{{ url('/api/ibadah') }}').then(r => r.json()),
|
||||
fetch('{{ url('/api/miskin') }}').then(r => r.json()),
|
||||
fetch('{{ url('/api/dashboard/dosen-summary') }}').then(r => r.json()),
|
||||
]);
|
||||
ibadahData = ri;
|
||||
miskinData = rm;
|
||||
dosenSummary = ds;
|
||||
renderDashboard();
|
||||
} catch (e) {
|
||||
showToast('Gagal memuat data', 'error');
|
||||
@@ -256,6 +235,7 @@
|
||||
document.getElementById('statTotalIbadah').textContent = ibadahData.length;
|
||||
document.getElementById('statTotalMiskin').textContent = miskinData.length;
|
||||
document.getElementById('statTotalJiwa').textContent = totalJiwa;
|
||||
renderDosenSummary();
|
||||
|
||||
// ── 2. GAP ANALYSIS ──────────────────────────────────────
|
||||
const coveredSet = new Set();
|
||||
@@ -388,7 +368,7 @@
|
||||
<td class="capitalize text-xs">${esc(m.kondisi_rumah || '-')}</td>
|
||||
<td class="text-center">${m.jumlah_orang || 0}</td>
|
||||
</tr>`).join('')
|
||||
: '<tr><td colspan="5" class="text-center py-6 text-success font-semibold">Semua keluarga miskin terjangkau! 🎉</td></tr>';
|
||||
: '<tr><td colspan="5" class="text-center py-6 text-success font-semibold">Semua keluarga miskin terjangkau.</td></tr>';
|
||||
}
|
||||
|
||||
function renderOrUpdateChart(canvasId, existingChart, config, setter) {
|
||||
@@ -397,15 +377,125 @@
|
||||
setter(new Chart(ctx, config));
|
||||
}
|
||||
|
||||
function renderDosenSummary() {
|
||||
if (!dosenSummary) return;
|
||||
|
||||
const anggota = dosenSummary.anggota || {};
|
||||
const bantuan = dosenSummary.bantuan || {};
|
||||
const transfer = dosenSummary.transfer || {};
|
||||
const statusHidup = anggota.status_hidup || {};
|
||||
const pendidikan = anggota.pendidikan || {};
|
||||
|
||||
const pendidikanTerdata = Object.entries(pendidikan)
|
||||
.filter(([key]) => key !== 'tidak_diketahui')
|
||||
.reduce((total, [, value]) => total + Number(value || 0), 0);
|
||||
|
||||
document.getElementById('statTotalAnggota').textContent = anggota.total || 0;
|
||||
document.getElementById('statAnggotaDesc').textContent = `${statusHidup.hidup || 0} hidup / ${statusHidup.meninggal || 0} meninggal`;
|
||||
document.getElementById('statPendidikanTerdata').textContent = pendidikanTerdata;
|
||||
document.getElementById('statBantuanBulanIni').textContent = bantuan.bulan_ini || 0;
|
||||
document.getElementById('statBantuanTotal').textContent = `${bantuan.total || 0} total bantuan`;
|
||||
document.getElementById('statTransferPending').textContent = transfer.pending || 0;
|
||||
document.getElementById('statTransferDesc').textContent = `${transfer.disetujui || 0} disetujui / ${transfer.ditolak || 0} ditolak`;
|
||||
|
||||
const pendidikanLabels = {
|
||||
tidak_sekolah: 'Tidak Sekolah',
|
||||
sd: 'SD',
|
||||
smp: 'SMP',
|
||||
sma: 'SMA',
|
||||
d3: 'D3',
|
||||
s1_plus: 'S1+',
|
||||
tidak_diketahui: 'Tidak Diketahui',
|
||||
};
|
||||
const pendidikanEntries = chartEntries(pendidikan, pendidikanLabels);
|
||||
renderOrUpdateChart('chartPendidikan', chartPendidikan, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: pendidikanEntries.labels,
|
||||
datasets: [{
|
||||
label: 'Anggota',
|
||||
data: pendidikanEntries.values,
|
||||
backgroundColor: '#14b8a6',
|
||||
borderRadius: 6,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } },
|
||||
},
|
||||
}, c => chartPendidikan = c);
|
||||
|
||||
const bantuanEntries = chartEntries(bantuan.per_jenis || {}, {});
|
||||
renderOrUpdateChart('chartBantuanJenis', chartBantuanJenis, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: bantuanEntries.labels,
|
||||
datasets: [{
|
||||
data: bantuanEntries.values,
|
||||
backgroundColor: ['#22c55e', '#3b82f6', '#f97316', '#8b5cf6', '#ef4444', '#64748b'],
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff',
|
||||
}],
|
||||
},
|
||||
options: { plugins: { legend: { position: 'bottom' } }, cutout: '55%' },
|
||||
}, c => chartBantuanJenis = c);
|
||||
|
||||
const transferEntries = chartEntries(transfer, {
|
||||
pending: 'Pending',
|
||||
disetujui: 'Disetujui',
|
||||
ditolak: 'Ditolak',
|
||||
});
|
||||
renderOrUpdateChart('chartTransfer', chartTransfer, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: transferEntries.labels,
|
||||
datasets: [{
|
||||
data: transferEntries.values,
|
||||
backgroundColor: ['#f59e0b', '#22c55e', '#ef4444'],
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff',
|
||||
}],
|
||||
},
|
||||
options: { plugins: { legend: { position: 'bottom' } }, cutout: '55%' },
|
||||
}, c => chartTransfer = c);
|
||||
}
|
||||
|
||||
function chartEntries(source, labels) {
|
||||
const entries = Object.entries(source || {}).filter(([, value]) => Number(value || 0) > 0);
|
||||
if (!entries.length) return { labels: ['Belum ada'], values: [0] };
|
||||
|
||||
return {
|
||||
labels: entries.map(([key]) => labels[key] || key.replace(/_/g, ' ')),
|
||||
values: entries.map(([, value]) => Number(value || 0)),
|
||||
};
|
||||
}
|
||||
|
||||
// ── 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 } }
|
||||
@@ -413,19 +503,27 @@
|
||||
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');
|
||||
if (typeof loadDashboard !== 'undefined') loadDashboard();
|
||||
}
|
||||
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