Files
D1041231009-WebGIS-SPBU/resources/views/spbu/index.blade.php
T
2026-06-11 14:53:35 +07:00

634 lines
30 KiB
PHP

<!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 SPBU - WebGIS</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- DaisyUI + Tailwind -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<!-- 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">
<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.01em;
}
#previewMap { height: 380px; width: 100%; }
/* Custom Leaflet Control Layers Styling */
.leaflet-control-layers {
border-radius: 12px !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15) !important;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.85) !important;
padding: 8px 12px !important;
font-family: inherit !important;
font-weight: 700 !important;
font-size: 0.85rem !important;
transition: all 0.3s ease !important;
}
.leaflet-control-layers:hover {
background: rgba(255, 255, 255, 0.95) !important;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.25) !important;
}
.leaflet-control-layers-list label {
margin-bottom: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
border-radius: 6px;
font-weight: 600 !important;
}
.leaflet-control-layers-list label:hover {
background: rgba(0, 0, 0, 0.05);
}
.leaflet-control-layers-separator {
margin: 8px 0 !important;
border-top: 1px solid rgba(0, 0, 0, 0.1) !important;
}
.leaflet-control-layers-selector {
accent-color: var(--lg-primary, #EA580C);
width: 16px;
height: 16px;
margin-top: 0 !important;
}
</style>
@include('partials.localguide-theme')
</head>
<body class="localguide-shell min-h-screen bg-base-200">
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
<div class="navbar localguide-navbar bg-base-100 border-b border-base-200 shadow-sm sticky top-0 z-[1000]">
<div class="flex-1">
<a href="{{ route('home') }}" class="btn btn-ghost text-lg font-bold gap-2 localguide-brand">
<i class="fa-solid fa-gas-pump"></i>
WebGIS SPBU
</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1 gap-1">
<li>
<a href="{{ route('spbu.input') }}" class="font-medium localguide-link">
<i class="fa-solid fa-map-location-dot"></i> Input Data
</a>
</li>
<li>
<a href="{{ route('spbu.data') }}"
class="font-semibold localguide-active-link">
<i class="fa-solid fa-table-list"></i> Lihat Data
</a>
</li>
</ul>
</div>
</div>
<!-- ═══ MAIN ══════════════════════════════════════════════════════════════ -->
<main class="max-w-screen-xl mx-auto px-4 py-6">
<!-- Page header -->
<div class="flex flex-wrap items-center justify-between gap-3 mb-6">
<div>
<div class="localguide-kicker mb-1">
<i class="fa-solid fa-compass"></i>
Local discovery dashboard
</div>
<h1 class="text-2xl font-bold">Data <span class="localguide-highlight">SPBU</span></h1>
<p class="text-sm text-base-content/50 mt-1">
Daftar seluruh titik SPBU yang telah terdata dalam sistem
</p>
</div>
<a href="{{ route('spbu.input') }}" class="btn btn-primary gap-2">
<i class="fa-solid fa-plus"></i> Tambah SPBU
</a>
</div>
<!-- Stats -->
<div class="stats stats-horizontal shadow bg-base-100 w-full mb-6 overflow-x-auto">
<div class="stat">
<div class="stat-figure text-primary">
<i class="fa-solid fa-gas-pump text-3xl"></i>
</div>
<div class="stat-title">Total SPBU</div>
<div class="stat-value text-primary" id="statTotal"></div>
</div>
<div class="stat">
<div class="stat-figure text-success">
<i class="fa-solid fa-clock-rotate-left text-3xl"></i>
</div>
<div class="stat-title">Buka 24 Jam</div>
<div class="stat-value text-success" id="stat24j"></div>
</div>
<div class="stat">
<div class="stat-figure text-error">
<i class="fa-solid fa-circle-xmark text-3xl"></i>
</div>
<div class="stat-title">Tidak 24 Jam</div>
<div class="stat-value text-error" id="statNon24"></div>
</div>
</div>
<div class="localguide-filter-bar mb-5" aria-label="Filter kategori SPBU">
<button type="button" class="localguide-filter-chip is-active" data-filter="all">
<i class="fa-solid fa-layer-group"></i>
Semua
</button>
<button type="button" class="localguide-filter-chip" data-filter="24h">
<i class="fa-solid fa-clock"></i>
Buka 24 Jam
</button>
<button type="button" class="localguide-filter-chip" data-filter="regular">
<i class="fa-solid fa-compass"></i>
Tidak 24 Jam
</button>
</div>
<!-- Content grid -->
<div class="grid grid-cols-1 lg:grid-cols-[1fr_380px] gap-5 items-start">
<!-- ── TABEL ─────────────────────────────────────────────────── -->
<div class="card bg-base-100 shadow">
<div class="card-body p-0">
<!-- Card header -->
<div class="flex items-center justify-between px-5 pt-5 pb-3 border-b border-base-200">
<h2 class="card-title text-base">
<i class="fa-solid fa-list text-primary"></i>
Daftar SPBU
</h2>
<span class="text-xs text-base-content/50" id="totalBadge">Memuat...</span>
</div>
<!-- Search -->
<div class="px-5 py-3 border-b border-base-200">
<label class="input input-bordered input-sm flex items-center gap-2 w-full">
<i class="fa-solid fa-magnifying-glass text-base-content/40"></i>
<input type="text" id="searchInput" class="grow"
placeholder="Cari nama atau nomor SPBU...">
</label>
</div>
<!-- Table -->
<div id="loadingTable" class="flex items-center justify-center gap-3 py-16
text-sm text-base-content/50">
<span class="loading loading-spinner loading-md"></span>
Memuat data...
</div>
<div class="overflow-x-auto">
<table class="table table-zebra table-sm" id="dataTable" style="display:none">
<thead>
<tr class="text-xs text-base-content/60">
<th>#</th>
<th>Nama SPBU</th>
<th>Nomor</th>
<th>Status</th>
<th>Koordinat</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<!-- Empty state -->
<div id="emptyState" class="text-center py-16 hidden">
<div class="text-4xl mb-3">📭</div>
<p class="font-semibold text-base-content/60">Belum ada data SPBU</p>
<p class="text-sm text-base-content/40 mt-1">
Tambahkan SPBU melalui halaman Input Data
</p>
</div>
</div>
</div>
<!-- ── SIDEBAR ───────────────────────────────────────────────── -->
<div class="flex flex-col gap-5">
<!-- Mini Map -->
<div class="card bg-base-100 shadow overflow-hidden">
<div class="card-body p-0">
<div class="flex items-center gap-2 px-4 py-3 border-b border-base-200">
<i class="fa-solid fa-map text-primary"></i>
<span class="font-semibold text-sm">Peta Sebaran</span>
</div>
<div id="previewMap"></div>
<div class="flex gap-4 px-4 py-2 border-t border-base-200 text-xs text-base-content/60">
<span class="flex items-center gap-1">
<span class="inline-block w-2.5 h-2.5 rounded-full localguide-map-legend-dot-primary"></span>
Buka 24 Jam
</span>
<span class="flex items-center gap-1">
<span class="inline-block w-2.5 h-2.5 rounded-full localguide-map-legend-dot-secondary"></span>
Tidak 24 Jam
</span>
</div>
</div>
</div>
<!-- Quick List -->
<div class="card bg-base-100 shadow">
<div class="card-body p-0">
<div class="flex items-center gap-2 px-4 py-3 border-b border-base-200">
<i class="fa-solid fa-list-ul text-primary"></i>
<span class="font-semibold text-sm">Daftar Cepat</span>
</div>
<div class="max-h-80 overflow-y-auto" id="quickList">
<div class="flex items-center justify-center gap-2 py-10
text-sm text-base-content/40">
<span class="loading loading-spinner loading-sm"></span>
Memuat...
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- ═══ MODAL: KONFIRMASI HAPUS ══════════════════════════════════════════ -->
<dialog id="confirmModal" class="modal">
<div class="modal-box max-w-sm">
<!-- Header merah -->
<div class="flex items-center gap-3 mb-5">
<div class="bg-error/10 text-error rounded-full w-11 h-11 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-triangle-exclamation text-lg"></i>
</div>
<div>
<h3 class="font-bold">Hapus Data SPBU</h3>
<p class="text-xs text-base-content/50">Tindakan ini tidak dapat dibatalkan</p>
</div>
<button class="btn btn-sm btn-circle btn-ghost ml-auto" id="btnTutupKonfirmasi"></button>
</div>
<p class="text-sm text-base-content/70 mb-2">
Apakah kamu yakin ingin menghapus data SPBU berikut?
</p>
<div class="bg-base-200 border-l-4 border-error rounded px-3 py-2 font-semibold mb-4"
id="confirmName"></div>
<div class="alert alert-error alert-sm py-2 text-xs mb-4">
<i class="fa-solid fa-circle-info"></i>
Data yang dihapus tidak bisa dikembalikan.
</div>
<div class="modal-action mt-0">
<button class="btn btn-sm" id="btnBatal">
<i class="fa-solid fa-xmark"></i> Batal
</button>
<button class="btn btn-sm btn-error" id="btnYaHapus">
<i class="fa-solid fa-trash"></i> Ya, Hapus
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- ═══ TOAST ══════════════════════════════════════════════════════════════ -->
<div class="toast toast-end toast-bottom z-[99999]" id="toastContainer"></div>
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
<script>
const API = '/api/spbu';
const LG_PRIMARY = '#EA580C';
const LG_SECONDARY = '#0D9488';
// ── MAP ─────────────────────────────────────────────────────────────────
const previewMap = L.map('previewMap', {
center: [-0.0606, 109.345],
zoom: 14
});
// 1. Base Maps
const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
});
const esriSatellite = L.layerGroup([
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19,
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
}),
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19
}),
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19
})
]).addTo(previewMap);
// 2. Layer Groups (Overlays)
const layer24h = L.layerGroup().addTo(previewMap);
const layerRegular = L.layerGroup().addTo(previewMap);
const baseMaps = {
"Satelit (Hybrid)": esriSatellite,
"Standard Map": osm
};
const overlays = {
"SPBU Buka 24 Jam": layer24h,
"SPBU Tidak Buka 24 Jam": layerRegular
};
L.control.layers(baseMaps, overlays, { collapsed: true }).addTo(previewMap);
function makeIcon(color) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 42" width="32" height="42">
<path d="M16 0C7.163 0 0 7.163 0 16c0 12 16 26 16 26S32 28 32 16C32 7.163 24.837 0 16 0z"
fill="${color}" stroke="white" stroke-width="2"/>
<text x="16" y="21" text-anchor="middle" fill="white"
font-size="13" font-family="Arial" font-weight="bold">⛽</text>
</svg>`;
return L.divIcon({ html: svg, className: '', iconSize:[32,42], iconAnchor:[16,42], popupAnchor:[0,-44] });
}
// ── STATE ────────────────────────────────────────────────────────────────
let allData = [];
let mapMarkers = {};
let activeFilter = 'all';
// ── MODAL ────────────────────────────────────────────────────────────────
const confirmModal = document.getElementById('confirmModal');
// ── HELPERS ──────────────────────────────────────────────────────────────
function esc(str) {
const d = document.createElement('div');
d.appendChild(document.createTextNode(str));
return d.innerHTML;
}
function showToast(msg, type = 'info') {
const t = document.createElement('div');
const cls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
t.className = `alert ${cls} shadow-md`;
t.innerHTML = `<span>${msg}</span>`;
document.getElementById('toastContainer').appendChild(t);
setTimeout(() => t.remove(), 3500);
}
// ── LOAD ─────────────────────────────────────────────────────────────────
function loadData() {
document.getElementById('loadingTable').style.display = 'flex';
document.getElementById('dataTable').style.display = 'none';
document.getElementById('emptyState').classList.add('hidden');
// Clear previous layer groups
layer24h.clearLayers();
layerRegular.clearLayers();
mapMarkers = {};
fetch(API)
.then(r => r.json())
.then(data => {
allData = data;
renderStats(data);
renderMarkers(data);
applyFilters();
})
.catch(() => {
document.getElementById('loadingTable').style.display = 'none';
document.getElementById('emptyState').classList.remove('hidden');
document.getElementById('totalBadge').textContent = 'Gagal memuat data';
renderStats([]);
});
}
// ── STATS ────────────────────────────────────────────────────────────────
function renderStats(data) {
const total = data.length;
const buka = data.filter(d => d.status === 'Buka 24 Jam').length;
document.getElementById('statTotal').textContent = total;
document.getElementById('stat24j').textContent = buka;
document.getElementById('statNon24').textContent = total - buka;
document.getElementById('totalBadge').textContent = total + ' data';
}
function matchesActiveFilter(spbu) {
if (activeFilter === '24h') {
return spbu.status === 'Buka 24 Jam';
}
if (activeFilter === 'regular') {
return spbu.status !== 'Buka 24 Jam';
}
return true;
}
function getFilteredData() {
const q = document.getElementById('searchInput').value.toLowerCase();
return allData.filter(spbu => matchesActiveFilter(spbu))
.filter(spbu =>
spbu.nama_spbu.toLowerCase().includes(q) ||
spbu.nomor_spbu.toLowerCase().includes(q)
);
}
function applyFilters() {
const filtered = getFilteredData();
renderTable(filtered);
renderQuickList(filtered);
document.getElementById('totalBadge').textContent = filtered.length + ' data';
}
// ── TABLE ────────────────────────────────────────────────────────────────
function renderTable(data) {
document.getElementById('loadingTable').style.display = 'none';
if (!data.length) {
document.getElementById('emptyState').classList.remove('hidden');
document.getElementById('dataTable').style.display = 'none';
return;
}
document.getElementById('emptyState').classList.add('hidden');
document.getElementById('dataTable').style.display = 'table';
document.getElementById('tableBody').innerHTML = data.map((spbu, i) => {
const is24 = spbu.status === 'Buka 24 Jam';
const badgeCls = is24 ? 'localguide-status-24' : 'localguide-status-regular';
const badgeIco = is24 ? '●' : '◆';
const lat = parseFloat(spbu.latitude).toFixed(5);
const lng = parseFloat(spbu.longitude).toFixed(5);
const namaJS = spbu.nama_spbu.replace(/'/g, "\\'");
return `
<tr class="hover cursor-pointer">
<td class="text-base-content/50 text-xs font-semibold"
onclick="focusMap(${spbu.latitude},${spbu.longitude},${spbu.id})">${i+1}</td>
<td onclick="focusMap(${spbu.latitude},${spbu.longitude},${spbu.id})">
<span class="font-semibold">${esc(spbu.nama_spbu)}</span>
</td>
<td class="text-xs text-base-content/60"
onclick="focusMap(${spbu.latitude},${spbu.longitude},${spbu.id})">
${esc(spbu.nomor_spbu)}
</td>
<td onclick="focusMap(${spbu.latitude},${spbu.longitude},${spbu.id})">
<span class="badge ${badgeCls} badge-sm gap-1">${badgeIco} ${esc(spbu.status)}</span>
</td>
<td class="font-mono text-xs text-base-content/50"
onclick="focusMap(${spbu.latitude},${spbu.longitude},${spbu.id})">
${lat}<br>${lng}
</td>
<td class="text-center">
<button class="btn btn-xs btn-outline btn-error gap-1"
onclick="bukaKonfirmasi(${spbu.id}, '${namaJS}')">
<i class="fa-solid fa-trash"></i> Hapus
</button>
</td>
</tr>`;
}).join('');
}
// ── MARKERS ───────────────────────────────────────────────────────────────
function renderMarkers(data) {
const bounds = [];
data.forEach(spbu => {
const is24 = spbu.status === 'Buka 24 Jam';
const color = is24 ? LG_PRIMARY : LG_SECONDARY;
const group = is24 ? layer24h : layerRegular;
const m = L.marker([+spbu.latitude, +spbu.longitude], { icon: makeIcon(color) })
.addTo(group)
.bindPopup(`
<div style="font-family:ui-sans-serif,system-ui,sans-serif;min-width:150px">
<b>⛽ ${esc(spbu.nama_spbu)}</b><br>
<span style="font-size:0.78rem;color:#6b7280">No: ${esc(spbu.nomor_spbu)}</span><br>
<span class="badge ${spbu.status === 'Buka 24 Jam' ? 'localguide-status-24' : 'localguide-status-regular'}"
style="font-size:0.7rem;margin-top:5px">${esc(spbu.status)}</span>
</div>`);
mapMarkers[spbu.id] = m;
bounds.push([+spbu.latitude, +spbu.longitude]);
});
if (bounds.length) previewMap.fitBounds(bounds, { padding: [28, 28] });
}
function focusMap(lat, lng, id) {
previewMap.flyTo([lat, lng], 17, { animate: true, duration: 1 });
if (mapMarkers[id]) mapMarkers[id].openPopup();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// ── QUICK LIST ─────────────────────────────────────────────────────────
function renderQuickList(data) {
const el = document.getElementById('quickList');
if (!data.length) {
el.innerHTML = `<div class="text-center py-10 text-sm text-base-content/40">
📭 Belum ada data</div>`;
return;
}
el.innerHTML = data.map(spbu => {
const is24 = spbu.status === 'Buka 24 Jam';
return `
<div class="flex gap-3 items-start px-4 py-3 border-b border-base-200
hover:bg-base-200 cursor-pointer transition-colors"
onclick="focusMap(${spbu.latitude},${spbu.longitude},${spbu.id})">
<span class="text-xl mt-0.5"></span>
<div>
<div class="font-semibold text-sm">${esc(spbu.nama_spbu)}</div>
<div class="text-xs text-base-content/50">${esc(spbu.nomor_spbu)}</div>
<span class="badge ${is24 ? 'localguide-status-24' : 'localguide-status-regular'} badge-sm mt-1">
${is24 ? '●' : '◆'} ${esc(spbu.status)}
</span>
</div>
</div>`;
}).join('');
}
// ── SEARCH ────────────────────────────────────────────────────────────
document.getElementById('searchInput').addEventListener('input', function () {
applyFilters();
});
document.querySelectorAll('[data-filter]').forEach(button => {
button.addEventListener('click', function () {
activeFilter = this.dataset.filter;
document.querySelectorAll('[data-filter]').forEach(item => {
item.classList.toggle('is-active', item === this);
});
applyFilters();
});
});
// ── HAPUS ─────────────────────────────────────────────────────────────
let pendingId = null;
function bukaKonfirmasi(id, nama) {
pendingId = id;
document.getElementById('confirmName').textContent = '⛽ ' + nama;
confirmModal.showModal();
}
function tutupKonfirmasi() { confirmModal.close(); pendingId = null; }
document.getElementById('btnBatal').addEventListener('click', tutupKonfirmasi);
document.getElementById('btnTutupKonfirmasi').addEventListener('click', tutupKonfirmasi);
confirmModal.addEventListener('close', () => { pendingId = null; });
document.getElementById('btnYaHapus').addEventListener('click', function () {
if (!pendingId) return;
const btn = this;
btn.disabled = true;
btn.innerHTML = '<span class="loading loading-spinner loading-xs"></span> Menghapus...';
fetch(`${API}/${pendingId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: pendingId })
})
.then(r => r.json())
.then(res => {
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-trash"></i> Ya, Hapus';
if (res.success) {
tutupKonfirmasi();
showToast('Data SPBU berhasil dihapus! 🗑️', 'success');
loadData();
} else {
showToast('Gagal: ' + (res.message || 'Error server'), 'error');
}
})
.catch(() => {
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-trash"></i> Ya, Hapus';
showToast('Tidak dapat terhubung ke server!', 'error');
});
});
// ── INIT ─────────────────────────────────────────────────────────────
loadData();
</script>
</body>
</html>