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
+6 -30
View File
@@ -15,38 +15,13 @@
body { font-weight: 500; background-color: #f8fafc; }
h1, h2, h3, .btn { font-weight: 700 !important; letter-spacing: -0.01em; }
</style>
<link rel="stylesheet" href="{{ asset('css/genesis.css') }}">
</head>
<body class="min-h-screen">
<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-medium"><i class="fa-solid fa-table-list"></i> Data</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>
<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-semibold bg-primary/10 text-primary rounded-lg"><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>
</ul>
<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>
</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>
<body class="min-h-screen app-body">
@include('partials.sidebar')
<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">
<div class="mb-8">
<h1 class="text-3xl font-bold flex items-center gap-3"><i class="fa-solid fa-clock-rotate-left text-primary"></i> Log Aktivitas</h1>
<p class="text-base-content/50 mt-1">200 aktivitas terakhir pengguna sistem.</p>
@@ -97,6 +72,7 @@
<tbody id="bodyLogs"></tbody>
</table>
</div>
</div>
</main>
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
+6 -30
View File
@@ -14,38 +14,13 @@
body { font-weight: 500; background-color: #f8fafc; }
h1, h2, h3, .btn { font-weight: 700 !important; letter-spacing: -0.01em; }
</style>
<link rel="stylesheet" href="{{ asset('css/genesis.css') }}">
</head>
<body class="min-h-screen">
<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-medium"><i class="fa-solid fa-table-list"></i> Data</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>
<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-semibold bg-primary/10 text-primary rounded-lg"><i class="fa-solid fa-sliders"></i> Pengaturan</a></li>
</ul>
<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>
</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>
<body class="min-h-screen app-body">
@include('partials.sidebar')
<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">
<div class="mb-8">
<h1 class="text-3xl font-bold flex items-center gap-3"><i class="fa-solid fa-sliders text-primary"></i> Konfigurasi Sistem</h1>
<p class="text-base-content/50 mt-1">Atur parameter peta dan analisis. Perubahan berlaku setelah halaman peta di-refresh.</p>
@@ -54,6 +29,7 @@
<div id="settingsForm" class="bg-white rounded-2xl shadow-sm border border-base-200 p-6 max-w-2xl">
<p class="text-center py-8 opacity-30 italic" id="settingsLoading">Memuat pengaturan...</p>
</div>
</div>
</main>
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
+6 -72
View File
@@ -16,82 +16,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-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>
<li>
<a href="{{ url('/admin/users') }}" class="font-semibold bg-primary/10 text-primary rounded-lg">
<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>
</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">
@@ -159,6 +92,7 @@
</table>
</div>
</div>
</div>
</main>
<!-- ═══ MODALS ═══════════════════════════════════════════════════════════ -->
+34 -18
View File
@@ -20,44 +20,60 @@
<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; }
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value {
font-weight: 700 !important;
letter-spacing: -0.02em;
}
#bgMap {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
opacity: 0.5;
filter: saturate(1.2) brightness(0.7);
opacity: 0.62;
filter: saturate(0.82) brightness(0.96) contrast(0.9);
}
.login-container {
position: relative;
z-index: 10;
}
.glass-card {
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.4);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.2);
.login-backdrop {
position: fixed;
inset: 0;
z-index: 1;
background:
linear-gradient(90deg, rgba(250, 250, 250, 0.96) 0%, rgba(250, 250, 250, 0.86) 45%, rgba(250, 250, 250, 0.38) 100%),
linear-gradient(180deg, rgba(250, 250, 250, 0.2) 0%, rgba(250, 250, 250, 0.88) 100%);
}
.login-card {
background: #fff;
border: 1px solid #e8e8ec;
border-radius: 8px;
padding: 32px;
}
.login-mark {
display: inline-grid;
width: 36px;
height: 36px;
place-items: center;
border: 1px solid #e8e8ec;
border-radius: 8px;
color: #0a0a0a;
font-size: 12px;
font-weight: 700;
}
</style>
<link rel="stylesheet" href="{{ asset('css/genesis.css') }}">
</head>
<body class="min-h-screen bg-base-200 flex items-center justify-center overflow-hidden p-4">
<body class="min-h-screen flex items-center justify-center overflow-hidden p-4">
<!-- Background Map -->
<div id="bgMap"></div>
<div class="login-backdrop"></div>
<div class="login-container w-full max-w-sm">
<div class="glass-card rounded-3xl p-8">
<div class="login-card">
<div class="text-center mb-7">
<div class="text-5xl mb-3">🏘️</div>
<h1 class="text-xl font-bold text-primary leading-tight">WebGIS Pemetaan Kemiskinan</h1>
<p class="text-sm text-base-content/50 mt-1">Masuk ke akun Anda untuk melanjutkan</p>
<div class="login-mark mx-auto mb-4">GIS</div>
<p class="genesis-overline mb-2">Akses operator</p>
<h1 class="text-2xl font-bold leading-tight">WebGIS Pemetaan Kemiskinan</h1>
<p class="text-sm text-base-content/50 mt-2">Masuk untuk mengelola data spasial dan tabular.</p>
</div>
@if($errors->any())
+183 -85
View File
@@ -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);
});
})();
+126 -91
View File
@@ -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);
});
})();
+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);
});
})();
+140
View File
@@ -0,0 +1,140 @@
@php
$user = auth()->user();
$roleName = $user?->getRoleNames()->first() ?? 'user';
$pendingTransferCount = $pendingTransferCount ?? 0;
$navItems = [
[
'label' => 'Peta Interaktif',
'short' => 'Peta',
'href' => url('/map'),
'icon' => 'fa-map',
'active' => request()->is('map'),
],
[
'label' => 'Data Tabular',
'short' => 'Data',
'href' => url('/data'),
'icon' => 'fa-table-list',
'active' => request()->is('data'),
],
[
'label' => 'Dashboard',
'short' => 'Dashboard',
'href' => url('/dashboard'),
'icon' => 'fa-chart-bar',
'active' => request()->is('dashboard'),
],
[
'label' => 'Transfer',
'short' => 'Transfer',
'href' => url('/permohonan'),
'icon' => 'fa-right-left',
'active' => request()->is('permohonan'),
'badge' => $pendingTransferCount,
],
];
$adminItems = [
[
'label' => 'Admin',
'href' => url('/admin/users'),
'icon' => 'fa-users-gear',
'active' => request()->is('admin/users'),
],
[
'label' => 'Log',
'href' => url('/admin/logs'),
'icon' => 'fa-clock-rotate-left',
'active' => request()->is('admin/logs'),
],
[
'label' => 'Pengaturan',
'href' => url('/admin/settings'),
'icon' => 'fa-sliders',
'active' => request()->is('admin/settings'),
],
];
@endphp
<input id="app-sidebar-toggle" type="checkbox" class="app-sidebar-toggle" autocomplete="off">
<header class="app-mobile-bar">
<label for="app-sidebar-toggle" class="btn btn-ghost btn-square app-sidebar-open" aria-label="Buka navigasi">
<i class="fa-solid fa-bars"></i>
</label>
<a href="{{ url('/') }}" class="app-mobile-brand">
<i class="fa-solid fa-house-chimney-crack"></i>
<span>WebGIS</span>
</a>
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded-full w-8">
<span class="text-xs font-bold">{{ substr($user->name, 0, 1) }}</span>
</div>
</div>
</header>
<label for="app-sidebar-toggle" class="app-sidebar-backdrop" aria-label="Tutup navigasi"></label>
<aside class="app-sidebar" aria-label="Navigasi utama">
<div class="app-sidebar-header">
<a href="{{ url('/') }}" class="app-sidebar-brand">
<span class="app-brand-icon"><i class="fa-solid fa-house-chimney-crack"></i></span>
<span class="app-brand-copy">
<span class="app-brand-title">WebGIS</span>
<span class="app-brand-subtitle">Pemetaan Kemiskinan</span>
</span>
</a>
<label for="app-sidebar-toggle" class="btn btn-ghost btn-square app-sidebar-close" aria-label="Tutup navigasi">
<i class="fa-solid fa-xmark"></i>
</label>
</div>
<nav class="app-sidebar-nav">
<div class="app-nav-section">
<div class="app-nav-section-title">Menu</div>
@foreach($navItems as $item)
<a href="{{ $item['href'] }}" class="app-nav-link {{ $item['active'] ? 'is-active' : '' }}">
<i class="fa-solid {{ $item['icon'] }}"></i>
<span>{{ $item['label'] }}</span>
@if(($item['badge'] ?? 0) > 0)
<span class="badge badge-error badge-xs transfer-pending-badge app-nav-badge">{{ $item['badge'] }}</span>
@endif
</a>
@endforeach
</div>
@if($user->hasRole('administrator'))
<div class="app-nav-section">
<div class="app-nav-section-title">Administrasi</div>
@foreach($adminItems as $item)
<a href="{{ $item['href'] }}" class="app-nav-link {{ $item['active'] ? 'is-active' : '' }}">
<i class="fa-solid {{ $item['icon'] }}"></i>
<span>{{ $item['label'] }}</span>
</a>
@endforeach
</div>
@endif
</nav>
<div class="app-sidebar-footer">
<div class="app-user-card">
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded-full w-9">
<span class="text-xs font-bold">{{ substr($user->name, 0, 1) }}</span>
</div>
</div>
<div class="app-user-copy">
<div class="app-user-name">{{ $user->name }}</div>
<div class="app-user-role">{{ str_replace('_', ' ', $roleName) }}</div>
</div>
</div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="app-logout-button">
<i class="fa-solid fa-right-from-bracket"></i>
<span>Keluar</span>
</button>
</form>
</div>
</aside>
+15 -35
View File
@@ -14,40 +14,13 @@
body { font-weight: 500; background-color: #f8fafc; }
h1, h2, h3, .btn { font-weight: 700 !important; letter-spacing: -0.01em; }
</style>
<link rel="stylesheet" href="{{ asset('css/genesis.css') }}">
</head>
<body class="min-h-screen">
<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-medium"><i class="fa-solid fa-table-list"></i> Data</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-semibold bg-primary/10 text-primary rounded-lg"><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>
<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>
</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>
<body class="min-h-screen app-body">
@include('partials.sidebar')
<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">
<div class="mb-8 flex items-start justify-between flex-wrap gap-4">
<div>
<h1 class="text-3xl font-bold flex items-center gap-3"><i class="fa-solid fa-right-left text-primary"></i> Permohonan Transfer</h1>
@@ -81,6 +54,7 @@
<tbody id="bodyPermohonan"><tr><td colspan="8" class="text-center py-10 opacity-30 italic">Memuat...</td></tr></tbody>
</table>
</div>
</div>
</main>
<!-- Modal Ajukan Permohonan -->
@@ -137,6 +111,8 @@
const statusBadge = { pending: 'badge-warning', disetujui: 'badge-success', ditolak: 'badge-error' };
const canRespond = {{ auth()->user()->hasRole('administrator') || auth()->user()->hasRole('pengurus_ibadah') ? 'true' : 'false' }};
const currentUserId = Number({{ auth()->id() ?? 0 }});
const isPengurusIbadah = {{ auth()->user()->hasRole('pengurus_ibadah') ? 'true' : 'false' }};
let allPermohonan = [];
let ibadahList = [];
@@ -153,9 +129,13 @@
ibadahList = await ibadahRes.json();
miskinList = await miskinRes.json();
const optIbadah = ibadahList.map(ib => `<option value="${ib.id}">${esc(ib.nama)}</option>`).join('');
document.getElementById('perm_dari').innerHTML = optIbadah;
document.getElementById('perm_ke').innerHTML = optIbadah;
const fromIbadah = isPengurusIbadah
? ibadahList.filter(ib => Number(ib.user_id) === currentUserId)
: ibadahList;
const optFromIbadah = fromIbadah.map(ib => `<option value="${ib.id}">${esc(ib.nama)}</option>`).join('');
const optToIbadah = ibadahList.map(ib => `<option value="${ib.id}">${esc(ib.nama)}</option>`).join('');
document.getElementById('perm_dari').innerHTML = optFromIbadah;
document.getElementById('perm_ke').innerHTML = optToIbadah;
document.getElementById('perm_miskin').innerHTML = miskinList
.map(m => `<option value="${esc(m.id_rumah)}">${esc(m.id_rumah)} — ${esc(m.nama_kk || '-')}</option>`).join('');
+105 -47
View File
@@ -20,16 +20,6 @@
<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;
}
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value {
font-weight: 700 !important;
letter-spacing: -0.02em;
}
#bgMap {
position: fixed;
top: 0;
@@ -37,73 +27,141 @@
width: 100%;
height: 100%;
z-index: 0;
opacity: 0.5;
filter: saturate(1.2) brightness(0.7);
opacity: 0.78;
filter: saturate(0.88) brightness(0.96) contrast(0.92);
}
.hero {
.landing-shell {
position: relative;
z-index: 10;
background: transparent !important;
min-height: 100vh;
display: grid;
align-items: end;
padding: 96px 24px 32px;
}
/* Glass effect for content */
.hero-content {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
padding: 3rem;
border-radius: 2rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.3);
.landing-shell::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
background:
linear-gradient(90deg, rgba(250, 250, 250, 0.94) 0%, rgba(250, 250, 250, 0.82) 42%, rgba(250, 250, 250, 0.22) 100%),
linear-gradient(180deg, rgba(250, 250, 250, 0.12) 0%, rgba(250, 250, 250, 0.84) 100%);
}
[data-theme="dark"] .hero-content {
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
.landing-content {
max-width: 760px;
}
.brand-kicker {
display: inline-flex;
align-items: center;
gap: 10px;
color: #6b6b6b;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.brand-mark {
display: inline-grid;
width: 32px;
height: 32px;
place-items: center;
border: 1px solid #e8e8ec;
border-radius: 8px;
background: #fff;
color: #0a0a0a;
font-size: 12px;
font-weight: 700;
}
.landing-title {
max-width: 720px;
margin-top: 24px;
color: #0a0a0a;
font-size: clamp(44px, 7vw, 72px);
line-height: 0.94;
}
.landing-copy {
max-width: 520px;
margin-top: 20px;
color: #6b6b6b;
font-size: 16px;
line-height: 1.7;
}
.landing-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 32px;
}
.landing-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
margin-top: 28px;
color: #6b6b6b;
font-size: 13px;
}
.redirect-strip {
width: min(420px, 100%);
margin-top: 40px;
}
.progress {
height: 4px;
background: #e8e8ec;
}
.progress::-webkit-progress-value {
background: #6366f1;
}
@media (max-width: 640px) {
.landing-shell {
padding: 80px 20px 28px;
}
.landing-title {
font-size: 44px;
}
}
</style>
<link rel="stylesheet" href="{{ asset('css/genesis.css') }}">
</head>
<body class="min-h-screen bg-base-200 flex flex-col items-center justify-center overflow-hidden">
<body class="min-h-screen overflow-hidden">
<!-- Background Map -->
<div id="bgMap"></div>
<div class="hero min-h-screen">
<div class="hero-content text-center flex-col gap-2">
<!-- Logo -->
<div class="text-7xl mb-2">🏘️</div>
<h1 class="text-4xl font-bold tracking-tight text-primary">WebGIS Pemetaan Kemiskinan</h1>
<p class="max-w-md text-base-content/60 mt-2 mb-6 leading-relaxed">
Sistem Informasi Geografis untuk pemetaan rumah ibadah dan
analisis sebaran keluarga kurang mampu.
<div class="landing-shell">
<div class="landing-content">
<div class="brand-kicker">
<span class="brand-mark">GIS</span>
Sistem informasi geografis desa
</div>
<h1 class="landing-title">Pemetaan kemiskinan berbasis radius layanan.</h1>
<p class="landing-copy">
Pantau sebaran keluarga kurang mampu, rumah ibadah, dan celah jangkauan bantuan dalam satu ruang kerja spasial yang ringkas.
</p>
<!-- Buttons -->
<div class="flex flex-col sm:flex-row gap-3">
<div class="landing-actions">
<a href="{{ url('/map') }}" class="btn btn-primary btn-lg gap-2">
<i class="fa-solid fa-map-location-dot"></i>
Buka Peta & Input
Buka Peta
</a>
<a href="{{ url('/data') }}" class="btn btn-outline btn-lg gap-2">
<i class="fa-solid fa-table-list"></i>
Lihat Data Tabular
Lihat Data
</a>
</div>
<!-- User info -->
<div class="flex items-center gap-2 text-sm text-base-content/50 mt-2">
<i class="fa-solid fa-user text-xs"></i>
<div class="landing-meta">
<span>{{ auth()->user()->name }}</span>
<span class="opacity-30"></span>
<span>/</span>
<span class="capitalize">{{ str_replace('_', ' ', auth()->user()->getRoleNames()->first() ?? '') }}</span>
<span class="opacity-30"></span>
<span>/</span>
<form method="POST" action="{{ route('logout') }}" class="inline">
@csrf
<button type="submit" class="hover:text-error transition-colors underline underline-offset-2">Keluar</button>
</form>
</div>
<!-- Redirect progress -->
<div class="mt-8 w-72">
<p class="text-xs text-base-content/40 mb-2">Mengarahkan ke halaman Peta dalam 5 detik...</p>
<progress class="progress progress-primary w-full" id="redirectProgress" value="0" max="100"></progress>
<div class="redirect-strip">
<p class="text-xs text-base-content/50 mb-2">Mengarahkan ke halaman Peta dalam 5 detik</p>
<progress class="progress w-full" id="redirectProgress" value="0" max="100"></progress>
</div>
</div>
</div>