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
+30 -12
View File
@@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS Miskin & Ibadah</title>
<title>WebGIS Pemetaan Kemiskinan</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
@@ -16,19 +17,31 @@
<!-- Plus Jakarta Sans Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<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 {
h1,
h2,
h3,
h4,
h5,
h6,
.btn,
.card-title,
.stat-value {
font-weight: 700 !important;
letter-spacing: -0.02em;
}
#bgMap {
position: fixed;
top: 0;
@@ -39,11 +52,13 @@
opacity: 0.5;
filter: saturate(1.2) brightness(0.7);
}
.hero {
position: relative;
z-index: 10;
background: transparent !important;
}
/* Glass effect for content */
.hero-content {
background: rgba(255, 255, 255, 0.7);
@@ -53,14 +68,16 @@
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.3);
}
[data-theme="dark"] .hero-content {
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
}
</style>
</head>
<body class="min-h-screen bg-base-200 flex flex-col items-center justify-center overflow-hidden">
<!-- Background Map -->
<div id="bgMap"></div>
@@ -68,9 +85,9 @@
<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 Miskin & Ibadah</h1>
<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
Sistem Informasi Geografis untuk pemetaan rumah ibadah dan
analisis sebaran keluarga kurang mampu.
</p>
@@ -99,7 +116,7 @@
let val = 0;
const bar = document.getElementById('redirectProgress');
const tick = setInterval(() => {
val += 100 / 50;
val += 100 / 50;
bar.value = val;
if (val >= 100) clearInterval(tick);
}, 100);
@@ -133,7 +150,7 @@
fetch('api/get_miskin.php').then(r => r.json())
]).then(([ibadah, miskin]) => {
const markers = [];
ibadah.forEach(item => {
L.circleMarker([+item.latitude, +item.longitude], {
radius: 7, fillColor: "#22c55e", color: "#fff", weight: 2, opacity: 1, fillOpacity: 0.8
@@ -151,14 +168,15 @@
if (markers.length) {
map.fitBounds(markers, { padding: [50, 50] });
}
// Subtle animation: slow pan
let lat = map.getCenter().lat;
setInterval(() => {
lat += 0.000005;
map.panTo([lat, map.getCenter().lng], { animate: true, duration: 1 });
}, 1000);
}).catch(() => {});
}).catch(() => { });
</script>
</body>
</html>
</html>
+38 -13
View File
@@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Data Tabular - WebGIS Miskin & Ibadah</title>
<title>Data Tabular - WebGIS Pemetaan Kemiskinan</title>
<!-- DaisyUI + Tailwind -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
@@ -13,14 +14,34 @@
<!-- Plus Jakarta Sans Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<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; background-color: #f8fafc; }
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
* {
font-family: 'Plus Jakarta Sans', sans-serif;
}
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>
</head>
<body class="min-h-screen">
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
@@ -28,20 +49,21 @@
<div class="flex-1">
<a href="../index.html" 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">
<ul class="menu menu-horizontal px-1 gap-1">
<li><a href="map.html" class="font-medium"><i class="fa-solid fa-map"></i> Peta</a></li>
<li><a href="data.html" 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="data.html" class="font-semibold bg-primary/10 text-primary rounded-lg"><i
class="fa-solid fa-table-list"></i> Data Tabular</a></li>
</ul>
</div>
</div>
<!-- ═══ MAIN CONTENT ════════════════════════════════════════════════════ -->
<main 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">
<div>
@@ -52,7 +74,8 @@
<p class="text-base-content/50 mt-1">Daftar rumah ibadah dan keluarga miskin dalam matriks tabular.</p>
</div>
<div class="flex gap-2">
<a href="map.html" class="btn btn-primary shadow-lg"><i class="fa-solid fa-plus"></i> Tambah Data di Peta</a>
<a href="map.html" class="btn btn-primary shadow-lg"><i class="fa-solid fa-plus"></i> Tambah Data di
Peta</a>
</div>
</div>
@@ -77,7 +100,8 @@
<!-- 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 />
<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="overflow-x-auto">
<table class="table table-zebra w-full" id="tableIbadah">
@@ -189,8 +213,8 @@
document.getElementById('confirmDeleteBtn').onclick = async () => {
const { type, id } = deleteTarget;
const res = await fetch(`../api/delete_${type}.php`, {
method: 'POST', body: JSON.stringify(type === 'ibadah' ? { id } : { id_rumah: id })
const res = await fetch(`../api/delete_${type}.php`, {
method: 'POST', body: JSON.stringify(type === 'ibadah' ? { id } : { id_rumah: id })
});
const resJson = await res.json();
if (resJson.success) {
@@ -210,4 +234,5 @@
}
</script>
</body>
</html>
</html>
+104 -62
View File
@@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Peta Interaktif - WebGIS Miskin & Ibadah</title>
<title>Peta Interaktif - WebGIS Pemetaan Kemiskinan</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
@@ -18,15 +19,46 @@
<!-- Plus Jakarta Sans Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<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; }
.map-wrapper { flex: 1; position: relative; min-height: 0; }
#map { width: 100%; height: 100%; }
* {
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;
}
.map-wrapper {
flex: 1;
position: relative;
min-height: 0;
}
#map {
width: 100%;
height: 100%;
}
/* Floating Controls */
.floating-panel {
position: absolute;
@@ -37,7 +69,7 @@
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.glass-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
@@ -46,7 +78,8 @@
}
/* Marker dragging hint */
#dragHint, #mapHint {
#dragHint,
#mapHint {
position: absolute;
bottom: 24px;
left: 50%;
@@ -65,6 +98,7 @@
}
</style>
</head>
<body>
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
@@ -72,7 +106,7 @@
<div class="flex-1">
<a href="../index.html" 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">
@@ -104,13 +138,16 @@
Mode Operasi
</h3>
<div class="grid grid-cols-1 gap-2">
<button class="btn btn-sm btn-outline btn-primary justify-start gap-2 active" id="btnModeView" onclick="setMode('view')">
<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 btn-success 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 btn-error justify-start gap-2" id="btnModeMiskin"
onclick="setMode('miskin')">
<i class="fa-solid fa-plus"></i> Tambah Rumah Miskin
</button>
</div>
@@ -130,12 +167,13 @@
<div class="text-xs font-bold text-primary truncate" id="activeName">Nama Ibadah</div>
<div class="text-[10px] text-base-content/40" id="activeAddr">Alamat</div>
</div>
<div class="form-control mb-4">
<label class="label py-1">
<span class="label-text-alt font-bold">Radius: <span id="radiusVal">500</span>m</span>
</label>
<input type="range" min="50" max="2000" value="500" step="50" class="range range-xs range-primary" id="radiusSlider">
<input type="range" min="50" max="2000" value="500" step="50"
class="range range-xs range-primary" id="radiusSlider">
</div>
<div class="grid grid-cols-3 gap-2">
@@ -168,7 +206,7 @@
</div>
<!-- ═══ MODALS ═══════════════════════════════════════════════════════════ -->
<!-- Modal: Input Ibadah -->
<dialog id="modalIbadah" class="modal">
<div class="modal-box">
@@ -179,11 +217,13 @@
<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" />
<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">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-20"
placeholder="Alamat lengkap..."></textarea>
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Radius Default Analysis (Meter)</span></div>
@@ -207,7 +247,8 @@
<div class="bg-base-200 p-2 rounded text-xs font-mono" id="rm_coords">Lat: -, Lng: -</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>
<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>
@@ -255,7 +296,7 @@
let activeCircle = null, activeIbadah = null;
let ibadahData = [], miskinData = [];
let tempMarker = null;
let ibadahMarkers = new Map(); // id -> marker
let miskinMarkers = new Map(); // id_rumah -> marker
@@ -286,7 +327,7 @@
function initMap() {
map = L.map('map', { zoomControl: false }).setView([-0.0553, 109.3463], 17);
const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 });
const satellite = L.layerGroup([
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }),
@@ -296,9 +337,9 @@
satellite.addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.control.layers({ "Satelit": satellite, "Street": osm },
{ "Rumah Ibadah": layerIbadah, "Rumah Miskin": layerMiskin },
{ position: 'topright' }).addTo(map);
L.control.layers({ "Satelit": satellite, "Street": osm },
{ "Rumah Ibadah": layerIbadah, "Rumah Miskin": layerMiskin },
{ position: 'topright' }).addTo(map);
layerIbadah.addTo(map);
layerMiskin.addTo(map);
@@ -315,7 +356,7 @@
fetch('../api/get_ibadah.php').then(r => r.json()),
fetch('../api/get_miskin.php').then(r => r.json())
]);
// Validate data is array
if (!Array.isArray(ri) || !Array.isArray(rm)) {
console.error("Invalid API response", { ri, rm });
@@ -324,7 +365,7 @@
ibadahData = ri;
miskinData = rm;
// Sync activeIbadah to point to the new object in the fresh data
if (activeIbadah) {
const matched = ibadahData.find(i => i.id == activeIbadah.id);
@@ -332,9 +373,9 @@
}
renderMarkers();
} catch (e) {
} catch (e) {
console.error(e);
showToast('Gagal memuat data', 'error');
showToast('Gagal memuat data', 'error');
}
}
@@ -348,16 +389,16 @@
const m = L.marker([item.latitude, item.longitude], { icon: icons.ibadah(), draggable: true })
.bindPopup(buildPopupIbadah(item))
.on('click', () => handleIbadahClick(item));
m.on('dragend', e => updateLocation('ibadah', item.id, e.target.getLatLng(), item));
layerIbadah.addLayer(m);
ibadahMarkers.set(item.id, m);
});
miskinData.forEach(item => {
const marker = L.marker([item.latitude, item.longitude], {
const marker = L.marker([item.latitude, item.longitude], {
icon: icons.miskin('#ef4444'),
draggable: true
draggable: true
}).bindPopup(buildPopupMiskin(item));
marker.on('dragend', e => updateLocation('miskin', item.id_rumah, e.target.getLatLng(), item));
@@ -376,10 +417,10 @@
currentMode = mode;
document.querySelectorAll('.btn-outline').forEach(b => b.classList.remove('active'));
document.getElementById('btnMode' + mode.charAt(0).toUpperCase() + mode.slice(1)).classList.add('active');
document.getElementById('mapHint').classList.toggle('hidden', mode === 'view');
document.getElementById('analysisPanel').classList.toggle('hidden', mode !== 'view');
if (mode !== 'view') {
if (activeCircle) { map.removeLayer(activeCircle); activeCircle = null; }
document.getElementById('statsContent').classList.add('hidden');
@@ -391,13 +432,13 @@
function handleMapClick(latlng) {
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker(latlng, { icon: icons.temp() }).addTo(map);
if (currentMode === 'ibadah') {
document.getElementById('ib_coords').innerText = `Lat: ${latlng.lat.toFixed(6)}, Lng: ${latlng.lng.toFixed(6)}`;
document.getElementById('ib_nama').value = '';
document.getElementById('ib_alamat').value = 'Mencari alamat...';
modalIbadah.showModal();
// Auto fetch address
reverseGeocode(latlng.lat, latlng.lng, 'ib_alamat');
} else {
@@ -432,7 +473,7 @@
document.getElementById('statsContent').classList.remove('hidden');
document.getElementById('activeName').innerText = item.nama;
document.getElementById('activeAddr').innerText = item.alamat;
const radius = document.getElementById('radiusSlider').value = item.radius || 500;
updateRadius(item.latitude, item.longitude, radius);
}
@@ -441,25 +482,25 @@
function updateRadius(lat, lng, radius) {
if (activeCircle) map.removeLayer(activeCircle);
activeCircle = L.circle([lat, lng], {
radius: parseInt(radius),
color: '#2563eb',
fillColor: '#3b82f6',
fillOpacity: 0.2,
radius: parseInt(radius),
color: '#2563eb',
fillColor: '#3b82f6',
fillOpacity: 0.2,
weight: 2,
dashArray: '5, 10'
}).addTo(map);
document.getElementById('radiusVal').innerText = radius;
updateAnalysisUI([lat, lng], radius);
}
function updateAnalysisUI(center, radius) {
let houses = 0, kk = 0, jiwa = 0;
miskinData.forEach(h => {
const houseMarker = miskinMarkers.get(h.id_rumah);
const isH = isInside(h, center, radius);
if (isH) {
houses++;
kk += parseInt(h.jumlah_kk);
@@ -484,7 +525,7 @@
// center can be [lat, lng] or {lat, lng}
const cLat = center.lat !== undefined ? center.lat : center[0];
const cLng = center.lng !== undefined ? center.lng : center[1];
const p1 = turf.point([Number(point.longitude), Number(point.latitude)]);
const p2 = turf.point([Number(cLng), Number(cLat)]);
return turf.distance(p1, p2, { units: 'meters' }) <= radius;
@@ -499,7 +540,7 @@
// Save radius to DB when user finishes sliding
document.getElementById('radiusSlider').addEventListener('change', async e => {
if (!activeIbadah) return;
const newRadius = e.target.value;
try {
const res = await fetch('../api/update_ibadah.php', {
@@ -559,11 +600,11 @@
};
const btn = document.getElementById('btnSaveIbadah');
btn.disabled = true;
const res = await fetch('../api/save_ibadah.php', {
method: 'POST',
const res = await fetch('../api/save_ibadah.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
body: JSON.stringify(data)
});
const resJson = await res.json();
if (resJson.success) {
@@ -586,10 +627,10 @@
const btn = document.getElementById('btnSaveMiskin');
btn.disabled = true;
const res = await fetch('../api/save_miskin.php', {
method: 'POST',
const res = await fetch('../api/save_miskin.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
body: JSON.stringify(data)
});
const resJson = await res.json();
if (resJson.success) {
@@ -602,15 +643,15 @@
async function updateLocation(type, id, latlng, oldData) {
const endpoint = `../api/update_${type}.php`;
const payload = type === 'ibadah'
const payload = type === 'ibadah'
? { id, nama: oldData.nama, alamat: oldData.alamat, latitude: latlng.lat, longitude: latlng.lng, radius: oldData.radius }
: { 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: 'POST',
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
body: JSON.stringify(payload)
});
const resJson = await res.json();
if (resJson.success) {
@@ -635,10 +676,10 @@
document.getElementById('confirmDeleteBtn').onclick = async () => {
const { type, id } = deleteTarget;
const res = await fetch(`../api/delete_${type}.php`, {
method: 'POST',
const res = await fetch(`../api/delete_${type}.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(type === 'ibadah' ? { id } : { id_rumah: id })
body: JSON.stringify(type === 'ibadah' ? { id } : { id_rumah: id })
});
const resJson = await res.json();
if (resJson.success) {
@@ -659,4 +700,5 @@
}
</script>
</body>
</html>
</html>
+788
View File
@@ -0,0 +1,788 @@
@import url('https://api.fontshare.com/v2/css?f[]=general-sans@600,700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--genesis-primary: #6366f1;
--genesis-primary-hover: #4f46e5;
--genesis-secondary: #20970b;
--genesis-neutral: #9c9c9c;
--genesis-bg: #fafafa;
--genesis-surface: #ffffff;
--genesis-text: #0a0a0a;
--genesis-text-secondary: #6b6b6b;
--genesis-border: #e8e8ec;
--genesis-success: #10b981;
--genesis-warning: #f59e0b;
--genesis-error: #ef4444;
--app-sidebar-width: 280px;
--app-mobile-bar-height: 58px;
}
html,
body {
background: var(--genesis-bg) !important;
color: var(--genesis-text);
font-family: 'DM Sans', ui-sans-serif, system-ui, sans-serif !important;
font-size: 15px;
font-weight: 400 !important;
}
* {
letter-spacing: 0 !important;
}
h1,
h2,
h3,
h4,
h5,
h6,
.card-title,
.stat-value {
color: var(--genesis-text);
font-family: 'General Sans', 'DM Sans', ui-sans-serif, system-ui, sans-serif !important;
font-weight: 700 !important;
}
code,
kbd,
pre,
.font-mono {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace !important;
}
.text-primary,
.btn-ghost.text-primary,
.menu a.text-primary {
color: var(--genesis-primary) !important;
}
main h1 i.text-primary,
main h2 i.text-primary,
main h3 i.text-primary,
.card h3 i.text-primary,
.modal-box h3 i.text-primary,
.stat-figure.text-success,
.stat-figure.text-warning,
.stat-figure.text-error,
.stat-figure.text-info {
color: var(--genesis-text) !important;
}
.text-secondary,
.text-info {
color: var(--genesis-text-secondary) !important;
}
.text-success {
color: var(--genesis-success) !important;
}
.text-warning {
color: var(--genesis-warning) !important;
}
.text-error {
color: var(--genesis-error) !important;
}
.bg-primary,
.badge-primary {
background-color: var(--genesis-primary) !important;
border-color: var(--genesis-primary) !important;
color: #fff !important;
}
.bg-base-100,
.bg-white {
background-color: var(--genesis-surface) !important;
}
.bg-base-200,
.bg-base-200\/50 {
background-color: var(--genesis-bg) !important;
}
.border-base-200,
.border-base-300,
.border-base-content\/10 {
border-color: var(--genesis-border) !important;
}
.text-base-content\/50,
.text-base-content\/60,
.opacity-50 {
color: var(--genesis-text-secondary) !important;
}
.shadow,
.shadow-sm,
.shadow-lg,
.shadow-xl,
.shadow-2xl {
box-shadow: none !important;
}
.rounded-box,
.rounded-lg,
.rounded-xl,
.rounded-2xl,
.rounded-3xl,
.modal-box,
.card,
.stat,
.tab-content {
border-radius: 8px !important;
}
.app-sidebar-toggle {
position: fixed;
opacity: 0;
pointer-events: none;
}
.app-mobile-bar {
display: none;
}
.app-sidebar {
position: fixed;
inset: 0 auto 0 0;
z-index: 1200;
width: var(--app-sidebar-width);
display: flex;
flex-direction: column;
gap: 18px;
padding: 18px;
background: rgba(255, 255, 255, 0.94);
border-right: 1px solid var(--genesis-border);
backdrop-filter: blur(16px);
}
.app-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.app-sidebar-brand,
.app-mobile-brand {
display: inline-flex;
align-items: center;
min-width: 0;
gap: 10px;
color: var(--genesis-text) !important;
text-decoration: none;
}
.app-brand-icon {
display: grid;
width: 38px;
height: 38px;
flex: 0 0 auto;
place-items: center;
border: 1px solid var(--genesis-border);
border-radius: 8px;
background: var(--genesis-surface);
color: var(--genesis-text);
}
.app-brand-copy,
.app-user-copy {
min-width: 0;
}
.app-brand-title,
.app-user-name {
display: block;
overflow: hidden;
color: var(--genesis-text);
font-size: 14px;
font-weight: 700;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.app-brand-subtitle,
.app-user-role {
display: block;
overflow: hidden;
color: var(--genesis-text-secondary);
font-size: 12px;
font-weight: 500;
line-height: 1.4;
text-overflow: ellipsis;
text-transform: capitalize;
white-space: nowrap;
}
.app-sidebar-close {
display: none !important;
}
.app-sidebar-nav {
display: flex;
min-height: 0;
flex: 1 1 auto;
flex-direction: column;
gap: 20px;
overflow-y: auto;
padding-right: 2px;
}
.app-nav-section {
display: flex;
flex-direction: column;
gap: 6px;
}
.app-nav-section-title {
padding: 0 10px 4px;
color: var(--genesis-neutral);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.app-nav-link {
display: flex;
align-items: center;
gap: 10px;
min-height: 42px;
padding: 0 11px;
border: 1px solid transparent;
border-radius: 8px;
color: var(--genesis-text-secondary);
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease;
}
.app-nav-link i {
width: 18px;
flex: 0 0 18px;
text-align: center;
}
.app-nav-link span:not(.badge) {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.app-nav-link:hover {
background: #f3f3f5;
color: var(--genesis-text);
}
.app-nav-link.is-active {
background: rgba(99, 102, 241, 0.1);
border-color: rgba(99, 102, 241, 0.2);
color: var(--genesis-primary);
}
.app-nav-badge {
margin-left: auto;
}
.app-sidebar-footer {
display: flex;
flex-direction: column;
gap: 10px;
border-top: 1px solid var(--genesis-border);
padding-top: 14px;
}
.app-user-card {
display: flex;
align-items: center;
min-width: 0;
gap: 10px;
}
.app-logout-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 38px;
gap: 8px;
border: 1px solid var(--genesis-border);
border-radius: 8px;
background: var(--genesis-surface);
color: var(--genesis-error);
font-size: 14px;
font-weight: 700;
cursor: pointer;
}
.app-logout-button:hover {
background: rgba(239, 68, 68, 0.08);
border-color: rgba(239, 68, 68, 0.22);
}
.app-main {
min-height: 100vh;
margin-left: var(--app-sidebar-width);
}
.app-map-body {
height: 100vh !important;
display: block !important;
overflow: hidden !important;
}
.map-wrapper.app-map-main {
height: 100vh;
min-height: 100vh;
margin-left: var(--app-sidebar-width);
}
.app-sidebar-backdrop {
display: none;
}
.menu.menu-horizontal {
gap: 4px !important;
min-width: 0;
width: 100%;
display: flex !important;
flex-wrap: nowrap !important;
justify-content: center;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}
.menu.menu-horizontal::-webkit-scrollbar {
display: none;
}
.menu a,
.dropdown-content .menu a {
border-radius: 6px !important;
color: var(--genesis-text-secondary);
font-size: 14px;
font-weight: 500 !important;
white-space: nowrap;
flex-wrap: nowrap !important;
}
.menu a:hover,
.btn-ghost:hover {
background: #f3f3f5 !important;
color: var(--genesis-text) !important;
}
.menu a.bg-primary\/10,
.menu a.font-semibold {
background: rgba(99, 102, 241, 0.1) !important;
color: var(--genesis-primary) !important;
}
.btn {
min-height: 38px;
border-radius: 6px !important;
font-family: 'DM Sans', ui-sans-serif, system-ui, sans-serif !important;
font-size: 14px;
font-weight: 600 !important;
text-transform: none !important;
transition: transform 160ms ease, box-shadow 160ms ease, background-color 160ms ease, border-color 160ms ease, color 160ms ease;
}
.btn:hover {
transform: translateY(-1px);
}
.btn-primary {
background: var(--genesis-primary) !important;
border-color: var(--genesis-primary) !important;
color: #fff !important;
}
.btn-primary:hover {
background: var(--genesis-primary-hover) !important;
border-color: var(--genesis-primary-hover) !important;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.35) !important;
}
.btn-outline {
background: transparent !important;
border-color: var(--genesis-border) !important;
color: var(--genesis-text) !important;
}
.btn-outline:hover,
.btn-active {
background: #f3f3f5 !important;
border-color: var(--genesis-primary) !important;
color: var(--genesis-primary) !important;
}
.btn-success {
background: var(--genesis-success) !important;
border-color: var(--genesis-success) !important;
color: #fff !important;
}
.btn-warning,
.btn-info {
background: transparent !important;
border-color: var(--genesis-border) !important;
color: var(--genesis-text) !important;
}
.btn-warning:hover,
.btn-info:hover {
background: rgba(99, 102, 241, 0.1) !important;
border-color: rgba(99, 102, 241, 0.2) !important;
color: var(--genesis-primary) !important;
}
.btn-error {
background: var(--genesis-error) !important;
border-color: var(--genesis-error) !important;
color: #fff !important;
}
.card,
.stat,
.modal-box,
.tab-content,
.dropdown-content,
.table-shell,
.glass-card,
.map-panel {
background: var(--genesis-surface) !important;
border: 1px solid var(--genesis-border) !important;
}
.card,
.stat,
.tab-content,
.table-shell,
.glass-card,
.map-panel {
box-shadow: none !important;
}
.card:hover,
.stat:hover,
.table-shell:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
transform: translateY(-2px);
}
.card,
.stat,
.table-shell {
transition: transform 200ms ease, box-shadow 200ms ease;
}
.stat {
padding: 20px !important;
}
.stat-figure {
color: var(--genesis-text-secondary) !important;
opacity: 0.82;
}
.stat-title {
color: var(--genesis-neutral) !important;
font-size: 11px !important;
font-weight: 700 !important;
letter-spacing: 0 !important;
text-transform: uppercase;
}
.stat-value {
font-size: 32px !important;
line-height: 1.1 !important;
color: var(--genesis-text) !important;
}
.input,
.select,
.textarea {
min-height: 38px;
border-radius: 6px !important;
border-color: var(--genesis-border) !important;
background: var(--genesis-surface) !important;
color: var(--genesis-text) !important;
font-size: 14px !important;
}
.input:focus,
.select:focus,
.textarea:focus {
border-color: var(--genesis-primary) !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12) !important;
outline: none !important;
}
.label-text {
color: var(--genesis-text);
font-size: 13px !important;
font-weight: 600 !important;
}
.badge {
border-radius: 9999px !important;
font-size: 12px !important;
font-weight: 600 !important;
}
.badge-success {
background: rgba(16, 185, 129, 0.12) !important;
border-color: rgba(16, 185, 129, 0.25) !important;
color: #047857 !important;
}
.badge-warning {
background: rgba(245, 158, 11, 0.14) !important;
border-color: rgba(245, 158, 11, 0.28) !important;
color: #92400e !important;
}
.badge-error {
background: rgba(239, 68, 68, 0.12) !important;
border-color: rgba(239, 68, 68, 0.25) !important;
color: #b91c1c !important;
}
.table {
color: var(--genesis-text);
font-size: 13px;
}
.table thead tr {
border-color: var(--genesis-border);
}
.table th {
color: var(--genesis-neutral);
font-size: 11px;
font-weight: 700 !important;
text-transform: uppercase;
}
.table td,
.table th {
border-color: var(--genesis-border) !important;
padding-block: 12px !important;
}
.table-zebra tbody tr:nth-child(even) {
background: #f7f7f8 !important;
}
.tabs {
gap: 6px;
}
.tab {
border-radius: 6px !important;
color: var(--genesis-text-secondary) !important;
font-size: 14px !important;
font-weight: 600 !important;
}
.tab:is(:checked),
.tab-active {
color: var(--genesis-primary) !important;
}
.genesis-tabs {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.genesis-tab-list {
display: inline-flex;
width: fit-content;
max-width: 100%;
gap: 8px;
padding: 4px;
border: 1px solid var(--genesis-border);
border-radius: 8px;
background: var(--genesis-surface);
overflow-x: auto;
}
.genesis-tab-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 148px;
height: 38px;
padding: 0 16px;
border: 1px solid transparent;
border-radius: 6px;
color: var(--genesis-text-secondary);
background: transparent;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease;
}
.genesis-tab-button:hover {
background: #f3f3f5;
color: var(--genesis-text);
}
.genesis-tab-button.is-active {
background: rgba(99, 102, 241, 0.1);
border-color: rgba(99, 102, 241, 0.2);
color: var(--genesis-primary);
}
.genesis-tab-panel {
border: 1px solid var(--genesis-border);
border-radius: 8px;
background: var(--genesis-surface);
overflow: hidden;
}
.modal-box {
box-shadow: 0 18px 55px rgba(0, 0, 0, 0.14) !important;
}
.modal-box .text-5xl {
font-size: 32px !important;
line-height: 1.1 !important;
}
.alert {
border-radius: 8px !important;
border: 1px solid var(--genesis-border) !important;
box-shadow: none !important;
}
.toast .alert {
background: var(--genesis-surface) !important;
color: var(--genesis-text) !important;
}
.checkbox {
border-radius: 9999px !important;
border-color: #d4d4d8 !important;
}
.checkbox:checked {
background-color: var(--genesis-primary) !important;
border-color: var(--genesis-primary) !important;
}
.leaflet-control-layers,
.leaflet-control-zoom a {
border-color: var(--genesis-border) !important;
box-shadow: none !important;
font-family: 'DM Sans', ui-sans-serif, system-ui, sans-serif !important;
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
border-radius: 8px !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
}
.genesis-overline {
color: var(--genesis-neutral);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.genesis-section-title {
color: var(--genesis-text);
font-family: 'General Sans', 'DM Sans', ui-sans-serif, system-ui, sans-serif;
font-size: 32px;
font-weight: 700;
}
@media (max-width: 1024px) {
.app-mobile-bar {
position: fixed;
inset: 0 0 auto 0;
z-index: 1150;
display: flex;
align-items: center;
justify-content: space-between;
height: var(--app-mobile-bar-height);
padding: 0 14px;
background: rgba(255, 255, 255, 0.94);
border-bottom: 1px solid var(--genesis-border);
backdrop-filter: blur(16px);
}
.app-mobile-brand {
font-size: 15px;
font-weight: 700;
}
.app-sidebar {
width: min(82vw, var(--app-sidebar-width));
transform: translateX(-100%);
transition: transform 200ms ease;
}
.app-sidebar-close {
display: inline-flex !important;
}
.app-sidebar-toggle:checked ~ .app-sidebar {
transform: translateX(0);
}
.app-sidebar-toggle:checked ~ .app-sidebar-backdrop {
position: fixed;
inset: 0;
z-index: 1190;
display: block;
background: rgba(10, 10, 10, 0.36);
}
.app-main {
margin-left: 0;
padding-top: var(--app-mobile-bar-height);
}
.map-wrapper.app-map-main {
height: 100vh;
min-height: 100vh;
margin-left: 0;
}
.floating-panel {
top: calc(var(--app-mobile-bar-height) + 14px);
left: 12px;
width: min(316px, calc(100vw - 24px));
max-height: calc(100vh - var(--app-mobile-bar-height) - 32px);
}
.app-map-body .leaflet-top {
top: var(--app-mobile-bar-height);
}
}
@media (max-width: 768px) {
.stat-value {
font-size: 26px !important;
}
.genesis-tabs {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.genesis-tabs > .tab {
min-width: 0;
}
}
+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>