chore: sync repository snapshot
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
@@ -0,0 +1,234 @@
|
||||
<!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 SPBU</title>
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></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.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);
|
||||
}
|
||||
.hero {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background: transparent !important;
|
||||
}
|
||||
/* 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);
|
||||
}
|
||||
[data-theme="dark"] .hero-content {
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 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 flex flex-col items-center justify-center overflow-hidden">
|
||||
|
||||
<!-- Background Map -->
|
||||
<div id="bgMap"></div>
|
||||
|
||||
<div class="hero min-h-screen">
|
||||
<div class="hero-content localguide-hero-panel flex-col items-start gap-3">
|
||||
<div class="localguide-kicker">
|
||||
<i class="fa-solid fa-location-dot"></i>
|
||||
Map-forward SPBU guide
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-6xl leading-none">⛽</div>
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold tracking-tight">WebGIS <span class="localguide-highlight">SPBU</span></h1>
|
||||
<span class="badge localguide-yellow-badge mt-2">Pontianak fuel station map</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="max-w-md text-base-content/70 mt-1 mb-2 leading-relaxed">
|
||||
Sistem Informasi Geografis untuk pemetaan dan pengelolaan
|
||||
titik-titik Stasiun Pengisian Bahan Bakar Umum.
|
||||
</p>
|
||||
|
||||
<div class="localguide-filter-bar" aria-label="Ringkasan kategori SPBU">
|
||||
<span class="localguide-filter-chip is-active">
|
||||
<i class="fa-solid fa-map"></i> Peta utama
|
||||
</span>
|
||||
<span class="localguide-filter-chip">
|
||||
<i class="fa-solid fa-clock"></i> Buka 24 jam
|
||||
</span>
|
||||
<span class="localguide-filter-chip">
|
||||
<i class="fa-solid fa-table-list"></i> Data lokasi
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<a href="{{ route('spbu.input') }}" class="btn btn-primary btn-lg gap-2">
|
||||
<i class="fa-solid fa-map-location-dot"></i>
|
||||
Input Data SPBU
|
||||
</a>
|
||||
<a href="{{ route('spbu.data') }}" class="btn btn-outline btn-lg gap-2">
|
||||
<i class="fa-solid fa-table-list"></i>
|
||||
Lihat Data & Peta
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Redirect progress -->
|
||||
<div class="mt-8 w-72">
|
||||
<p class="text-xs text-base-content/50 mb-2">Mengarahkan ke halaman Input dalam 5 detik...</p>
|
||||
<progress class="progress progress-primary w-full" id="redirectProgress" value="0" max="100"></progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Progress bar logic
|
||||
let val = 0;
|
||||
const bar = document.getElementById('redirectProgress');
|
||||
const tick = setInterval(() => {
|
||||
val += 100 / 50; // Slower for effect (5s)
|
||||
bar.value = val;
|
||||
if (val >= 100) clearInterval(tick);
|
||||
}, 100);
|
||||
setTimeout(() => { window.location.href = '{{ route('spbu.input') }}'; }, 5000);
|
||||
|
||||
// Background Map logic
|
||||
const map = L.map('bgMap', {
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
dragging: false,
|
||||
scrollWheelZoom: false,
|
||||
doubleClickZoom: false
|
||||
}).setView([-0.0606, 109.345], 15);
|
||||
|
||||
// Satellite Base
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// Standard Base (Optional)
|
||||
const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19
|
||||
});
|
||||
|
||||
// Layer Groups
|
||||
const layer24h = L.layerGroup().addTo(map);
|
||||
const layerRegular = L.layerGroup().addTo(map);
|
||||
|
||||
// Control (visible but subtle)
|
||||
L.control.layers({ "Satelit": satellite, "Street": osm },
|
||||
{ "Buka 24 Jam": layer24h, "Normal": layerRegular },
|
||||
{ position: 'bottomleft' }).addTo(map);
|
||||
|
||||
// Fetch SPBU points to show on background
|
||||
fetch('/api/spbu')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const markers = [];
|
||||
data.forEach(spbu => {
|
||||
const is24 = spbu.status === 'Buka 24 Jam';
|
||||
const group = is24 ? layer24h : layerRegular;
|
||||
const color = is24 ? "#EA580C" : "#0D9488";
|
||||
|
||||
L.circleMarker([+spbu.latitude, +spbu.longitude], {
|
||||
radius: 6,
|
||||
fillColor: color,
|
||||
color: "#fff",
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
}).addTo(group);
|
||||
markers.push([+spbu.latitude, +spbu.longitude]);
|
||||
});
|
||||
if (markers.length) {
|
||||
map.fitBounds(markers, { padding: [50, 50] });
|
||||
}
|
||||
|
||||
// Subtle animation: slow pan
|
||||
let lat = map.getCenter().lat;
|
||||
setInterval(() => {
|
||||
lat += 0.00001;
|
||||
map.panTo([lat, map.getCenter().lng], { animate: true, duration: 1 });
|
||||
}, 1000);
|
||||
})
|
||||
.catch(() => {});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,243 @@
|
||||
<style>
|
||||
:root {
|
||||
--lg-primary: #EA580C;
|
||||
--lg-secondary: #0D9488;
|
||||
--lg-tertiary: #EAB308;
|
||||
--lg-bg: #FFFFFF;
|
||||
--lg-surface: #FFFFFF;
|
||||
--lg-ink: #17201B;
|
||||
--lg-muted: #66736D;
|
||||
--lg-border: #E7E2DB;
|
||||
--lg-success: #16A34A;
|
||||
--lg-warning: #EAB308;
|
||||
--lg-error: #DC2626;
|
||||
--lg-info: #0D9488;
|
||||
--lg-map-tint: rgba(234, 88, 12, 0.12);
|
||||
--lg-shadow: 0 18px 45px rgba(23, 32, 27, 0.13);
|
||||
}
|
||||
|
||||
.localguide-shell {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(234, 88, 12, 0.07), rgba(255, 255, 255, 0) 260px),
|
||||
var(--lg-bg) !important;
|
||||
color: var(--lg-ink);
|
||||
}
|
||||
|
||||
.localguide-shell .btn,
|
||||
.localguide-shell .input,
|
||||
.localguide-shell .select,
|
||||
.localguide-shell .badge,
|
||||
.localguide-shell .card,
|
||||
.localguide-shell .modal-box,
|
||||
.localguide-shell .alert {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.localguide-shell .btn-primary {
|
||||
background-color: var(--lg-primary) !important;
|
||||
border-color: var(--lg-primary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.localguide-shell .btn-primary:hover {
|
||||
background-color: #C2410C !important;
|
||||
border-color: #C2410C !important;
|
||||
}
|
||||
|
||||
.localguide-shell .btn-outline {
|
||||
border-color: var(--lg-secondary) !important;
|
||||
color: var(--lg-secondary) !important;
|
||||
}
|
||||
|
||||
.localguide-shell .btn-outline:hover {
|
||||
background-color: var(--lg-secondary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.localguide-shell .text-primary,
|
||||
.localguide-shell .stat-value.text-primary,
|
||||
.localguide-shell .stat-figure.text-primary {
|
||||
color: var(--lg-primary) !important;
|
||||
}
|
||||
|
||||
.localguide-shell .progress-primary::-webkit-progress-value {
|
||||
background-color: var(--lg-primary) !important;
|
||||
}
|
||||
|
||||
.localguide-shell .progress-primary::-moz-progress-bar {
|
||||
background-color: var(--lg-primary) !important;
|
||||
}
|
||||
|
||||
.localguide-navbar {
|
||||
background: rgba(255, 255, 255, 0.93) !important;
|
||||
border-bottom: 1px solid var(--lg-border) !important;
|
||||
box-shadow: 0 10px 25px rgba(23, 32, 27, 0.08) !important;
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.localguide-brand {
|
||||
color: var(--lg-primary) !important;
|
||||
}
|
||||
|
||||
.localguide-active-link {
|
||||
background: rgba(234, 88, 12, 0.11) !important;
|
||||
color: var(--lg-primary) !important;
|
||||
border: 1px solid rgba(234, 88, 12, 0.24);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.localguide-link {
|
||||
color: var(--lg-secondary) !important;
|
||||
}
|
||||
|
||||
.localguide-hero-panel,
|
||||
.localguide-panel,
|
||||
.localguide-shell .card,
|
||||
.localguide-shell .stats,
|
||||
.localguide-shell .modal-box {
|
||||
background: rgba(255, 255, 255, 0.94) !important;
|
||||
border: 1px solid var(--lg-border) !important;
|
||||
box-shadow: var(--lg-shadow) !important;
|
||||
}
|
||||
|
||||
.localguide-hero-panel {
|
||||
max-width: 580px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.localguide-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
color: var(--lg-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.localguide-highlight {
|
||||
color: var(--lg-primary);
|
||||
}
|
||||
|
||||
.localguide-yellow-badge {
|
||||
background: rgba(234, 179, 8, 0.17) !important;
|
||||
border: 1px solid rgba(234, 179, 8, 0.28) !important;
|
||||
color: #854D0E !important;
|
||||
}
|
||||
|
||||
.localguide-filter-panel,
|
||||
.localguide-filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.localguide-filter-panel {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 999;
|
||||
max-width: min(92vw, 520px);
|
||||
padding: 0.55rem;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid var(--lg-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--lg-shadow);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.localguide-filter-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
min-height: 2.25rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border: 1px solid var(--lg-border);
|
||||
border-radius: 999px;
|
||||
background: var(--lg-surface);
|
||||
color: var(--lg-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, color 0.16s ease, background 0.16s ease;
|
||||
}
|
||||
|
||||
.localguide-filter-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(13, 148, 136, 0.42);
|
||||
color: var(--lg-secondary);
|
||||
}
|
||||
|
||||
.localguide-filter-chip.is-active {
|
||||
background: var(--lg-primary);
|
||||
border-color: var(--lg-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.localguide-map-hint {
|
||||
background: var(--lg-primary) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35) !important;
|
||||
color: #fff !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
.localguide-drag-hint {
|
||||
background: var(--lg-tertiary) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.45) !important;
|
||||
color: #3F2A05 !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
.localguide-shell .leaflet-control-layers {
|
||||
border: 1px solid var(--lg-border) !important;
|
||||
border-radius: 8px !important;
|
||||
background: rgba(255, 255, 255, 0.93) !important;
|
||||
box-shadow: var(--lg-shadow) !important;
|
||||
}
|
||||
|
||||
.localguide-shell .leaflet-control-layers-selector {
|
||||
accent-color: var(--lg-primary) !important;
|
||||
}
|
||||
|
||||
.localguide-shell .leaflet-popup-content-wrapper,
|
||||
.localguide-shell .leaflet-popup-tip {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.localguide-status-24 {
|
||||
background: rgba(234, 88, 12, 0.14) !important;
|
||||
color: var(--lg-primary) !important;
|
||||
border: 1px solid rgba(234, 88, 12, 0.26) !important;
|
||||
}
|
||||
|
||||
.localguide-status-regular {
|
||||
background: rgba(13, 148, 136, 0.13) !important;
|
||||
color: var(--lg-secondary) !important;
|
||||
border: 1px solid rgba(13, 148, 136, 0.24) !important;
|
||||
}
|
||||
|
||||
.localguide-map-legend-dot-primary {
|
||||
background: var(--lg-primary);
|
||||
}
|
||||
|
||||
.localguide-map-legend-dot-secondary {
|
||||
background: var(--lg-secondary);
|
||||
}
|
||||
|
||||
.localguide-shell .table-zebra tbody tr:nth-child(even) {
|
||||
background: rgba(13, 148, 136, 0.045);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.localguide-hero-panel {
|
||||
width: calc(100vw - 2rem);
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.localguide-filter-panel {
|
||||
left: 0.75rem;
|
||||
right: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,633 @@
|
||||
<!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: '© <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 © Esri — 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>
|
||||
@@ -0,0 +1,726 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Input 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;
|
||||
}
|
||||
html, body { height: 100%; }
|
||||
body { display: flex; flex-direction: column; overflow: hidden; }
|
||||
.map-wrapper { flex: 1; position: relative; min-height: 0; }
|
||||
#map { width: 100%; height: 100%; }
|
||||
/* Drag cursor for Leaflet marker */
|
||||
.leaflet-marker-draggable { cursor: grab !important; }
|
||||
.leaflet-dragging .leaflet-marker-draggable { cursor: grabbing !important; }
|
||||
/* Spinner inside leaflet popup */
|
||||
@keyframes _spin { to { transform: rotate(360deg); } }
|
||||
._spin { animation: _spin .7s linear infinite; }
|
||||
|
||||
/* 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">
|
||||
|
||||
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
|
||||
<div class="navbar localguide-navbar bg-base-100 border-b border-base-200 shadow-sm flex-shrink-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-semibold localguide-active-link">
|
||||
<i class="fa-solid fa-map-location-dot"></i> Input Data
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route('spbu.data') }}" class="font-medium localguide-link">
|
||||
<i class="fa-solid fa-table-list"></i> Lihat Data
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ MAP ══════════════════════════════════════════════════════════════ -->
|
||||
<div class="map-wrapper">
|
||||
<div id="map"></div>
|
||||
|
||||
<div class="localguide-filter-panel" aria-label="Filter layer SPBU">
|
||||
<button type="button" class="localguide-filter-chip is-active" data-layer-filter="all">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
Semua
|
||||
</button>
|
||||
<button type="button" class="localguide-filter-chip" data-layer-filter="24h">
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
Buka 24 Jam
|
||||
</button>
|
||||
<button type="button" class="localguide-filter-chip" data-layer-filter="regular">
|
||||
<i class="fa-solid fa-compass"></i>
|
||||
Tidak 24 Jam
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Normal hint -->
|
||||
<div id="mapHint"
|
||||
class="absolute bottom-8 left-1/2 -translate-x-1/2 z-[999] pointer-events-none
|
||||
badge localguide-map-hint badge-lg py-3 px-4 gap-2 shadow-md">
|
||||
<i class="fa-solid fa-hand-pointer"></i>
|
||||
Klik peta untuk tambah SPBU • Drag marker untuk pindah lokasi
|
||||
</div>
|
||||
|
||||
<!-- Drag hint -->
|
||||
<div id="dragHint"
|
||||
class="absolute bottom-8 left-1/2 -translate-x-1/2 z-[999] pointer-events-none hidden
|
||||
badge localguide-drag-hint badge-lg py-3 px-4 gap-2 shadow-md">
|
||||
<i class="fa-solid fa-arrows-up-down-left-right"></i>
|
||||
Lepaskan untuk menyimpan lokasi baru
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ MODAL: INPUT (dialog DaisyUI) ════════════════════════════════════ -->
|
||||
<dialog id="inputModal" class="modal">
|
||||
<div class="modal-box">
|
||||
<!-- Header -->
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
id="btnCloseModal">✕</button>
|
||||
<h3 class="font-bold text-lg flex items-center gap-2 mb-1">
|
||||
<i class="fa-solid fa-gas-pump text-primary"></i>
|
||||
Tambah Data SPBU
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/50 mb-4">Isi atribut titik lokasi baru</p>
|
||||
|
||||
<!-- Koordinat -->
|
||||
<div class="alert alert-info py-2 mb-4 text-sm">
|
||||
<i class="fa-solid fa-location-crosshairs"></i>
|
||||
<span id="coordText">Memuat koordinat...</span>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<label class="form-control mb-3">
|
||||
<div class="label"><span class="label-text font-semibold">Nama SPBU</span></div>
|
||||
<input type="text" id="namaSPBU" class="input input-bordered w-full"
|
||||
placeholder="Contoh: SPBU Jl. Ahmad Yani" autocomplete="off">
|
||||
</label>
|
||||
|
||||
<label class="form-control mb-3">
|
||||
<div class="label"><span class="label-text font-semibold">Nomor SPBU</span></div>
|
||||
<input type="text" id="nomorSPBU" class="input input-bordered w-full"
|
||||
placeholder="Contoh: 64.751.01" autocomplete="off">
|
||||
</label>
|
||||
|
||||
<label class="form-control mb-4">
|
||||
<div class="label"><span class="label-text font-semibold">Status Operasional</span></div>
|
||||
<select id="statusSPBU" class="select select-bordered w-full">
|
||||
<option value="">-- Pilih Status --</option>
|
||||
<option value="Buka 24 Jam">☀️🌙 Buka 24 Jam</option>
|
||||
<option value="Tidak Buka 24 Jam">⏰ Tidak Buka 24 Jam</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn" id="btnCancel">
|
||||
<i class="fa-solid fa-xmark"></i> Batal
|
||||
</button>
|
||||
<button class="btn btn-primary" id="btnSimpan">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Simpan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Backdrop click = close -->
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button id="btnBackdropInput">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- ═══ MODAL: EDIT ═══════════════════════════════════════════════════════ -->
|
||||
<dialog id="editModal" class="modal">
|
||||
<div class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||
id="btnCloseEditModal">✕</button>
|
||||
<h3 class="font-bold text-lg flex items-center gap-2 mb-1 text-warning">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
Edit Data SPBU
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/50 mb-4">Perbarui atribut titik lokasi</p>
|
||||
|
||||
<div class="alert alert-info py-2 mb-4 text-sm">
|
||||
<i class="fa-solid fa-location-crosshairs"></i>
|
||||
<span id="editCoordText">Memuat koordinat...</span>
|
||||
</div>
|
||||
|
||||
<label class="form-control mb-3">
|
||||
<div class="label"><span class="label-text font-semibold">Nama SPBU</span></div>
|
||||
<input type="text" id="editNamaSPBU" class="input input-bordered w-full"
|
||||
placeholder="Contoh: SPBU Jl. Ahmad Yani" autocomplete="off">
|
||||
</label>
|
||||
|
||||
<label class="form-control mb-3">
|
||||
<div class="label"><span class="label-text font-semibold">Nomor SPBU</span></div>
|
||||
<input type="text" id="editNomorSPBU" class="input input-bordered w-full"
|
||||
placeholder="Contoh: 64.751.01" autocomplete="off">
|
||||
</label>
|
||||
|
||||
<label class="form-control mb-4">
|
||||
<div class="label"><span class="label-text font-semibold">Status Operasional</span></div>
|
||||
<select id="editStatusSPBU" class="select select-bordered w-full">
|
||||
<option value="">-- Pilih Status --</option>
|
||||
<option value="Buka 24 Jam">☀️🌙 Buka 24 Jam</option>
|
||||
<option value="Tidak Buka 24 Jam">⏰ Tidak Buka 24 Jam</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn" id="btnCancelEdit">
|
||||
<i class="fa-solid fa-xmark"></i> Batal
|
||||
</button>
|
||||
<button class="btn btn-warning text-white" id="btnUpdate">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Perbarui
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button id="btnBackdropEdit">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- ═══ MODAL: KONFIRMASI HAPUS ══════════════════════════════════════════ -->
|
||||
<dialog id="deleteModal" class="modal">
|
||||
<div class="modal-box text-center max-w-sm">
|
||||
<div class="text-5xl mb-3">🗑️</div>
|
||||
<h3 class="font-bold text-lg mb-1">Hapus Data SPBU?</h3>
|
||||
<p class="text-sm text-base-content/60 mb-1">
|
||||
Data <strong id="deleteSpbuName"></strong> akan dihapus secara permanen.
|
||||
</p>
|
||||
<p class="text-sm text-error mb-5">⚠️ Tindakan ini tidak dapat dibatalkan.</p>
|
||||
|
||||
<div class="modal-action justify-center">
|
||||
<button class="btn" id="btnCancelDelete">
|
||||
<i class="fa-solid fa-xmark"></i> Batal
|
||||
</button>
|
||||
<button class="btn btn-error" id="btnConfirmDelete">
|
||||
<i class="fa-solid fa-trash"></i> Ya, Hapus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button id="btnBackdropDelete">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- ═══ TOAST CONTAINER ════════════════════════════════════════════════════ -->
|
||||
<div class="toast toast-end toast-bottom z-[99999]" id="toastContainer"></div>
|
||||
|
||||
|
||||
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
|
||||
<script>
|
||||
// ── CONFIG ──────────────────────────────────────────────────────────────
|
||||
const API = '/api/spbu';
|
||||
const LG_PRIMARY = '#EA580C';
|
||||
const LG_SECONDARY = '#0D9488';
|
||||
const LG_TERTIARY = '#EAB308';
|
||||
|
||||
// ── MODAL REFS ─────────────────────────────────────────────────────────
|
||||
const inputModal = document.getElementById('inputModal');
|
||||
const editModal = document.getElementById('editModal');
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
|
||||
// ── MAP INIT ────────────────────────────────────────────────────────────
|
||||
const map = L.map('map', {
|
||||
center: [-0.06058602521208944, 109.34498961534207],
|
||||
zoom: 16,
|
||||
zoomControl: false
|
||||
});
|
||||
|
||||
L.control.zoom({ position: 'bottomleft' }).addTo(map);
|
||||
|
||||
// 1. Base Maps
|
||||
const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <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 © Esri — 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(map);
|
||||
|
||||
// 2. Layer Groups (Overlays)
|
||||
const layer24h = L.layerGroup().addTo(map);
|
||||
const layerRegular = L.layerGroup().addTo(map);
|
||||
|
||||
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: false }).addTo(map);
|
||||
|
||||
function setLayerFilter(filter) {
|
||||
if (filter === 'all' || filter === '24h') {
|
||||
if (!map.hasLayer(layer24h)) map.addLayer(layer24h);
|
||||
} else if (map.hasLayer(layer24h)) {
|
||||
map.removeLayer(layer24h);
|
||||
}
|
||||
|
||||
if (filter === 'all' || filter === 'regular') {
|
||||
if (!map.hasLayer(layerRegular)) map.addLayer(layerRegular);
|
||||
} else if (map.hasLayer(layerRegular)) {
|
||||
map.removeLayer(layerRegular);
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-layer-filter]').forEach(button => {
|
||||
button.classList.toggle('is-active', button.dataset.layerFilter === filter);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-layer-filter]').forEach(button => {
|
||||
button.addEventListener('click', () => setLayerFilter(button.dataset.layerFilter));
|
||||
});
|
||||
|
||||
function createSpbuIcon(color = LG_PRIMARY) {
|
||||
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 pendingLatLng = null;
|
||||
let tempMarker = null;
|
||||
const spbuMap = new Map(); // id => { marker, data }
|
||||
|
||||
// ── POPUP CONTENT ───────────────────────────────────────────────────────
|
||||
function buildPopupContent(spbu) {
|
||||
const isOpen = spbu.status === 'Buka 24 Jam';
|
||||
const badgeCls = isOpen ? 'localguide-status-24' : 'localguide-status-regular';
|
||||
const lat = parseFloat(spbu.latitude).toFixed(6);
|
||||
const lng = parseFloat(spbu.longitude).toFixed(6);
|
||||
return `
|
||||
<div style="font-family:ui-sans-serif,system-ui,sans-serif;min-width:210px">
|
||||
<div style="font-weight:700;font-size:0.95rem;margin-bottom:5px">⛽ ${spbu.nama_spbu}</div>
|
||||
<div style="font-size:0.8rem;color:#6b7280;margin-bottom:6px">
|
||||
<b>No:</b> ${spbu.nomor_spbu}
|
||||
</div>
|
||||
<span class="badge ${badgeCls}" style="font-size:0.72rem">
|
||||
${isOpen ? '●' : '◆'} ${spbu.status}
|
||||
</span>
|
||||
<div style="font-size:0.7rem;color:#9ca3af;margin-top:5px">${lat}, ${lng}</div>
|
||||
<div style="display:flex;gap:6px;margin-top:10px;padding-top:8px;border-top:1px solid #f3f4f6">
|
||||
<button class="btn btn-xs btn-outline btn-primary"
|
||||
onclick="openEditModal(${spbu.id})">
|
||||
<i class="fa-solid fa-pen-to-square"></i> Edit
|
||||
</button>
|
||||
<button class="btn btn-xs btn-outline btn-error"
|
||||
onclick="openDeleteConfirm(${spbu.id})">
|
||||
<i class="fa-solid fa-trash"></i> Hapus
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── LOAD DATA ───────────────────────────────────────────────────────────
|
||||
function loadExistingData() {
|
||||
fetch(API)
|
||||
.then(r => r.json())
|
||||
.then(data => data.forEach(addSpbuMarker))
|
||||
.catch(() => console.log('Tidak dapat memuat data dari server.'));
|
||||
}
|
||||
|
||||
function addSpbuMarker(spbu) {
|
||||
const isOpen = spbu.status === 'Buka 24 Jam';
|
||||
const color = isOpen ? LG_PRIMARY : LG_SECONDARY;
|
||||
const group = isOpen ? layer24h : layerRegular;
|
||||
|
||||
const m = L.marker([parseFloat(spbu.latitude), parseFloat(spbu.longitude)], {
|
||||
icon: createSpbuIcon(color),
|
||||
draggable: true
|
||||
})
|
||||
.addTo(group)
|
||||
.bindPopup(buildPopupContent(spbu), { maxWidth: 270 });
|
||||
|
||||
const id = parseInt(spbu.id);
|
||||
spbuMap.set(id, { marker: m, data: { ...spbu } });
|
||||
|
||||
// ── DRAG START
|
||||
m.on('dragstart', function () {
|
||||
this.setOpacity(0.55);
|
||||
map.closePopup();
|
||||
document.getElementById('dragHint').classList.remove('hidden');
|
||||
document.getElementById('mapHint').classList.add('hidden');
|
||||
});
|
||||
|
||||
// ── DRAG END → update DB
|
||||
m.on('dragend', function (e) {
|
||||
this.setOpacity(1);
|
||||
document.getElementById('dragHint').classList.add('hidden');
|
||||
document.getElementById('mapHint').classList.remove('hidden');
|
||||
|
||||
const newLatLng = e.target.getLatLng();
|
||||
const entry = spbuMap.get(id);
|
||||
if (!entry) return;
|
||||
|
||||
// Loading popup
|
||||
this.bindPopup(`
|
||||
<div style="font-family:ui-sans-serif,system-ui,sans-serif;
|
||||
padding:12px;text-align:center;min-width:160px">
|
||||
<span class="loading loading-spinner loading-sm text-warning"></span>
|
||||
<span style="font-size:0.84rem;margin-left:6px;vertical-align:middle">
|
||||
Menyimpan lokasi baru...
|
||||
</span>
|
||||
</div>`, { maxWidth: 270 }).openPopup();
|
||||
|
||||
fetch(`${API}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
nama_spbu : entry.data.nama_spbu,
|
||||
nomor_spbu : entry.data.nomor_spbu,
|
||||
status : entry.data.status,
|
||||
latitude : newLatLng.lat,
|
||||
longitude : newLatLng.lng
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
entry.data.latitude = newLatLng.lat;
|
||||
entry.data.longitude = newLatLng.lng;
|
||||
this.setPopupContent(buildPopupContent(entry.data)).openPopup();
|
||||
showToast(`📍 Lokasi ${entry.data.nama_spbu} berhasil dipindahkan!`, 'success');
|
||||
} else {
|
||||
this.setLatLng([parseFloat(entry.data.latitude), parseFloat(entry.data.longitude)]);
|
||||
this.setPopupContent(buildPopupContent(entry.data)).openPopup();
|
||||
showToast('Gagal memperbarui lokasi: ' + (res.message || 'Error'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.setLatLng([parseFloat(entry.data.latitude), parseFloat(entry.data.longitude)]);
|
||||
this.setPopupContent(buildPopupContent(entry.data)).openPopup();
|
||||
showToast('Tidak dapat terhubung ke server!', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadExistingData();
|
||||
|
||||
// ── MAP CLICK → buka modal tambah ──────────────────────────────────────
|
||||
map.on('click', function (e) {
|
||||
pendingLatLng = e.latlng;
|
||||
if (tempMarker) map.removeLayer(tempMarker);
|
||||
tempMarker = L.marker(e.latlng, {
|
||||
icon: createSpbuIcon(LG_TERTIARY), opacity: 0.85
|
||||
}).addTo(map);
|
||||
|
||||
document.getElementById('coordText').textContent =
|
||||
`Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`;
|
||||
document.getElementById('namaSPBU').value = '';
|
||||
document.getElementById('nomorSPBU').value = '';
|
||||
document.getElementById('statusSPBU').value = '';
|
||||
|
||||
inputModal.showModal();
|
||||
setTimeout(() => document.getElementById('namaSPBU').focus(), 300);
|
||||
});
|
||||
|
||||
// ── CLOSE INPUT MODAL ──────────────────────────────────────────────────
|
||||
function closeModal(removeTemp = true) {
|
||||
inputModal.close();
|
||||
if (removeTemp && tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
||||
pendingLatLng = null;
|
||||
}
|
||||
|
||||
document.getElementById('btnCloseModal').addEventListener('click', () => closeModal());
|
||||
document.getElementById('btnCancel').addEventListener('click', () => closeModal());
|
||||
// Backdrop closes via method=dialog; cleanup state via 'close' event
|
||||
inputModal.addEventListener('close', () => {
|
||||
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
||||
pendingLatLng = null;
|
||||
});
|
||||
|
||||
// ── SIMPAN DATA ────────────────────────────────────────────────────────
|
||||
document.getElementById('btnSimpan').addEventListener('click', function () {
|
||||
const nama = document.getElementById('namaSPBU').value.trim();
|
||||
const nomor = document.getElementById('nomorSPBU').value.trim();
|
||||
const status = document.getElementById('statusSPBU').value;
|
||||
|
||||
if (!nama) { document.getElementById('namaSPBU').focus(); showToast('Nama SPBU tidak boleh kosong!', 'error'); return; }
|
||||
if (!nomor) { document.getElementById('nomorSPBU').focus(); showToast('Nomor SPBU tidak boleh kosong!', 'error'); return; }
|
||||
if (!status) { document.getElementById('statusSPBU').focus(); showToast('Pilih status operasional!', 'error'); return; }
|
||||
|
||||
const btn = document.getElementById('btnSimpan');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="loading loading-spinner loading-sm"></span> Menyimpan...';
|
||||
|
||||
fetch(API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama_spbu: nama, nomor_spbu: nomor, status,
|
||||
latitude: pendingLatLng.lat, longitude: pendingLatLng.lng })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i> Simpan';
|
||||
|
||||
if (res.success) {
|
||||
if (tempMarker) map.removeLayer(tempMarker);
|
||||
tempMarker = null;
|
||||
addSpbuMarker({ id: res.id, nama_spbu: nama, nomor_spbu: nomor, status,
|
||||
latitude: pendingLatLng.lat, longitude: pendingLatLng.lng });
|
||||
const entry = spbuMap.get(parseInt(res.id));
|
||||
if (entry) entry.marker.openPopup();
|
||||
inputModal.close();
|
||||
pendingLatLng = null;
|
||||
showToast('Data SPBU berhasil disimpan! ✅', 'success');
|
||||
} else {
|
||||
showToast('Gagal menyimpan: ' + (res.message || 'Error server'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i> Simpan';
|
||||
showToast('Tidak dapat terhubung ke server!', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// ── TOAST ──────────────────────────────────────────────────────────────
|
||||
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);
|
||||
}
|
||||
|
||||
// ── EDIT MODAL ─────────────────────────────────────────────────────────
|
||||
let editingId = null;
|
||||
|
||||
function openEditModal(id) {
|
||||
const entry = spbuMap.get(id);
|
||||
if (!entry) return;
|
||||
editingId = id;
|
||||
const { data } = entry;
|
||||
|
||||
document.getElementById('editNamaSPBU').value = data.nama_spbu;
|
||||
document.getElementById('editNomorSPBU').value = data.nomor_spbu;
|
||||
document.getElementById('editStatusSPBU').value = data.status;
|
||||
document.getElementById('editCoordText').textContent =
|
||||
`Lat: ${parseFloat(data.latitude).toFixed(6)}, Lng: ${parseFloat(data.longitude).toFixed(6)}`;
|
||||
|
||||
map.closePopup();
|
||||
editModal.showModal();
|
||||
setTimeout(() => document.getElementById('editNamaSPBU').focus(), 300);
|
||||
}
|
||||
|
||||
function closeEditModal() { editModal.close(); editingId = null; }
|
||||
|
||||
document.getElementById('btnCloseEditModal').addEventListener('click', closeEditModal);
|
||||
document.getElementById('btnCancelEdit').addEventListener('click', closeEditModal);
|
||||
editModal.addEventListener('close', () => { editingId = null; });
|
||||
|
||||
document.getElementById('btnUpdate').addEventListener('click', function () {
|
||||
const nama = document.getElementById('editNamaSPBU').value.trim();
|
||||
const nomor = document.getElementById('editNomorSPBU').value.trim();
|
||||
const status = document.getElementById('editStatusSPBU').value;
|
||||
|
||||
if (!nama) { showToast('Nama SPBU tidak boleh kosong!', 'error'); document.getElementById('editNamaSPBU').focus(); return; }
|
||||
if (!nomor) { showToast('Nomor SPBU tidak boleh kosong!', 'error'); document.getElementById('editNomorSPBU').focus(); return; }
|
||||
if (!status) { showToast('Pilih status operasional!', 'error'); document.getElementById('editStatusSPBU').focus(); return; }
|
||||
|
||||
const entry = spbuMap.get(editingId);
|
||||
if (!entry) return;
|
||||
|
||||
const btn = document.getElementById('btnUpdate');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="loading loading-spinner loading-sm"></span> Menyimpan...';
|
||||
|
||||
fetch(`${API}/${editingId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: editingId, nama_spbu: nama, nomor_spbu: nomor, status,
|
||||
latitude: parseFloat(entry.data.latitude),
|
||||
longitude: parseFloat(entry.data.longitude)
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i> Perbarui';
|
||||
|
||||
if (res.success) {
|
||||
const oldStatus = entry.data.status;
|
||||
entry.data.nama_spbu = nama;
|
||||
entry.data.nomor_spbu = nomor;
|
||||
entry.data.status = status;
|
||||
|
||||
const color = status === 'Buka 24 Jam' ? LG_PRIMARY : LG_SECONDARY;
|
||||
entry.marker.setIcon(createSpbuIcon(color));
|
||||
entry.marker.setPopupContent(buildPopupContent(entry.data));
|
||||
|
||||
// Move to correct layer group if status changed
|
||||
if (oldStatus !== status) {
|
||||
const oldGroup = oldStatus === 'Buka 24 Jam' ? layer24h : layerRegular;
|
||||
const newGroup = status === 'Buka 24 Jam' ? layer24h : layerRegular;
|
||||
oldGroup.removeLayer(entry.marker);
|
||||
newGroup.addLayer(entry.marker);
|
||||
}
|
||||
|
||||
closeEditModal();
|
||||
showToast('Data SPBU berhasil diperbarui! ✏️', 'success');
|
||||
} else {
|
||||
showToast('Gagal memperbarui: ' + (res.message || 'Error server'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i> Perbarui';
|
||||
showToast('Tidak dapat terhubung ke server!', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// ── DELETE CONFIRM ─────────────────────────────────────────────────────
|
||||
let deletingId = null;
|
||||
|
||||
function openDeleteConfirm(id) {
|
||||
const entry = spbuMap.get(id);
|
||||
if (!entry) return;
|
||||
deletingId = id;
|
||||
document.getElementById('deleteSpbuName').textContent = entry.data.nama_spbu;
|
||||
map.closePopup();
|
||||
deleteModal.showModal();
|
||||
}
|
||||
|
||||
function closeDeleteModal() { deleteModal.close(); deletingId = null; }
|
||||
|
||||
document.getElementById('btnCancelDelete').addEventListener('click', closeDeleteModal);
|
||||
deleteModal.addEventListener('close', () => { deletingId = null; });
|
||||
|
||||
document.getElementById('btnConfirmDelete').addEventListener('click', function () {
|
||||
if (!deletingId) return;
|
||||
const entry = spbuMap.get(deletingId);
|
||||
if (!entry) return;
|
||||
|
||||
const btn = document.getElementById('btnConfirmDelete');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="loading loading-spinner loading-sm"></span> Menghapus...';
|
||||
|
||||
fetch(`${API}/${deletingId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: deletingId })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa-solid fa-trash"></i> Ya, Hapus';
|
||||
|
||||
if (res.success) {
|
||||
map.removeLayer(entry.marker);
|
||||
spbuMap.delete(deletingId);
|
||||
closeDeleteModal();
|
||||
showToast('Data SPBU berhasil dihapus! 🗑️', 'success');
|
||||
} else {
|
||||
showToast('Gagal menghapus: ' + (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');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user