879 lines
41 KiB
PHP
879 lines
41 KiB
PHP
<?php
|
|
session_start();
|
|
$isAdmin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="id">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WebGIS SPBU - Sistem Informasi SPBU</title>
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Leaflet CSS & JS -->
|
|
<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>
|
|
|
|
<!-- Fontsource Inter -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-400-normal.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-500-normal.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-600-normal.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-700-normal.css">
|
|
|
|
<style>
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
#map {
|
|
height: 100%;
|
|
width: 100%;
|
|
z-index: 1;
|
|
}
|
|
|
|
.leaflet-popup-content-wrapper {
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.leaflet-popup-content {
|
|
margin: 16px;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8;
|
|
}
|
|
|
|
.glass-panel {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.fade-in {
|
|
animation: fadeIn 0.3s ease-in-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.admin-mode .leaflet-marker-draggable {
|
|
cursor: grab;
|
|
}
|
|
|
|
.admin-mode .leaflet-marker-draggable:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.pulse {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(13, 148, 136, 0.7);
|
|
}
|
|
|
|
70% {
|
|
box-shadow: 0 0 0 10px rgba(13, 148, 136, 0);
|
|
}
|
|
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(13, 148, 136, 0);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="bg-slate-100 h-screen flex flex-col overflow-hidden">
|
|
|
|
<!-- Header -->
|
|
<header class="relative h-24 flex-shrink-0 bg-cover bg-center flex items-center justify-between px-6 shadow-lg"
|
|
style="background-image: url('https://image.qwenlm.ai/public_source/f414e259-c5c5-43c4-9181-edf462f60e8b/1c9325cad-a96a-4cea-9742-4d62d18dd61b.png');">
|
|
<div class="absolute inset-0 bg-gradient-to-r from-slate-900/80 to-slate-900/40"></div>
|
|
<div class="relative z-10 flex items-center gap-3">
|
|
<div class="bg-teal-500 p-2 rounded-lg shadow-md">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-xl font-bold text-white tracking-wide">WebGIS SPBU</h1>
|
|
<p class="text-xs text-teal-200">Sistem Informasi Lokasi Stasiun Pengisian Bahan Bakar Umum</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative z-10 flex items-center gap-4">
|
|
<!-- Search Bar -->
|
|
<div class="relative hidden md:block">
|
|
<input type="text" id="searchInput" placeholder="Cari nama SPBU..."
|
|
class="w-64 pl-10 pr-4 py-2 rounded-full bg-white/20 border border-white/30 text-white placeholder-teal-200 focus:outline-none focus:ring-2 focus:ring-teal-400 focus:bg-white/30 transition-all text-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
class="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-teal-200" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Tombol Login / Admin Toggle -->
|
|
<div id="authContainer">
|
|
<!-- Akan diisi oleh JavaScript -->
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main class="flex flex-1 overflow-hidden relative">
|
|
|
|
<!-- Sidebar Dashboard -->
|
|
<aside class="w-96 bg-white border-r border-slate-200 flex flex-col shadow-xl z-20">
|
|
<!-- Stats -->
|
|
<div class="p-4 border-b border-slate-100 bg-slate-50">
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm">
|
|
<p class="text-xs text-slate-500 font-medium">Total SPBU</p>
|
|
<p class="text-2xl font-bold text-slate-800" id="totalSpbu">0</p>
|
|
</div>
|
|
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm">
|
|
<p class="text-xs text-slate-500 font-medium">Buka 24 Jam</p>
|
|
<p class="text-2xl font-bold text-teal-600" id="total24Jam">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile Search -->
|
|
<div class="p-4 border-b border-slate-100 md:hidden">
|
|
<div class="relative">
|
|
<input type="text" id="searchInputMobile" placeholder="Cari nama SPBU..."
|
|
class="w-full pl-10 pr-4 py-2 rounded-lg bg-slate-100 text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-teal-500 text-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
class="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- List Header -->
|
|
<div class="px-4 py-3 flex items-center justify-between bg-white border-b border-slate-100">
|
|
<h2 class="font-semibold text-slate-700 text-sm uppercase tracking-wider">Daftar SPBU</h2>
|
|
<button id="btnAddSpbu"
|
|
class="bg-teal-600 hover:bg-teal-700 text-white text-xs font-medium px-3 py-1.5 rounded-lg shadow-sm transition-colors flex items-center gap-1"
|
|
style="display: none;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Tambah
|
|
</button>
|
|
</div>
|
|
|
|
<!-- SPBU List -->
|
|
<div id="spbuList" class="flex-1 overflow-y-auto custom-scrollbar p-3 space-y-2">
|
|
<!-- Items akan di-inject di sini -->
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Map Container -->
|
|
<div class="flex-1 relative" id="mapContainer">
|
|
<div id="map"></div>
|
|
<!-- Map Hint -->
|
|
<div id="mapHint"
|
|
class="absolute top-4 right-4 z-[1000] bg-white/90 backdrop-blur px-4 py-2 rounded-lg shadow-md text-xs text-slate-600 border border-slate-200 pointer-events-none"
|
|
style="display: none;">
|
|
💡 Tekan tombol tambah pada sidebar untuk menambahkan SPBU
|
|
</div>
|
|
<!-- Mode indicator -->
|
|
<div id="modeIndicator"
|
|
class="absolute top-4 left-4 z-[1000] bg-teal-600 text-white px-4 py-2 rounded-lg shadow-md text-xs font-medium hidden">
|
|
🎯 Mode Pilih Titik - Klik pada peta
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Overlay Form Login -->
|
|
<div id="loginOverlay"
|
|
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden fade-in">
|
|
<div class="bg-gradient-to-r from-teal-600 to-teal-500 px-6 py-4">
|
|
<h3 class="text-lg font-bold text-white">Login Admin</h3>
|
|
<p class="text-teal-100 text-xs mt-1">Masukkan kredensial untuk mengelola SPBU</p>
|
|
</div>
|
|
<div class="p-6">
|
|
<form id="loginForm" class="space-y-4">
|
|
<div>
|
|
<label
|
|
class="block text-xs font-semibold text-slate-600 mb-1 uppercase tracking-wide">Username</label>
|
|
<input type="text" id="loginUsername" required
|
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent text-sm"
|
|
placeholder="admin">
|
|
</div>
|
|
<div>
|
|
<label
|
|
class="block text-xs font-semibold text-slate-600 mb-1 uppercase tracking-wide">Password</label>
|
|
<input type="password" id="loginPassword" required
|
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent text-sm"
|
|
placeholder="••••••••">
|
|
</div>
|
|
<div id="loginError" class="text-red-500 text-xs hidden">Username atau password salah!</div>
|
|
<div class="flex gap-3 pt-2">
|
|
<button type="button" id="btnCancelLogin"
|
|
class="flex-1 px-4 py-2 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 font-medium text-sm transition-colors">Batal</button>
|
|
<button type="submit"
|
|
class="flex-1 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 font-medium text-sm transition-colors shadow-md shadow-teal-200">Masuk</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Form SPBU -->
|
|
<div id="modalOverlay"
|
|
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden fade-in">
|
|
<div class="bg-gradient-to-r from-teal-600 to-teal-500 px-6 py-4 flex items-center justify-between">
|
|
<h3 class="text-lg font-bold text-white" id="modalTitle">Tambah SPBU Baru</h3>
|
|
<button id="btnCloseModal" class="text-white/80 hover:text-white transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<form id="spbuForm" class="p-6 space-y-4">
|
|
<input type="hidden" id="spbuId">
|
|
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-600 mb-1 uppercase tracking-wide">Nama
|
|
SPBU</label>
|
|
<input type="text" id="inputNama" required
|
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent text-sm"
|
|
placeholder="Contoh: SPBU Pertamina Tebet">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-600 mb-1 uppercase tracking-wide">No.
|
|
SPBU</label>
|
|
<input type="text" id="inputNo" required
|
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent text-sm"
|
|
placeholder="Contoh: 34.101.01">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-600 mb-1 uppercase tracking-wide">Status
|
|
Operasional</label>
|
|
<select id="inputStatus"
|
|
class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent text-sm bg-white">
|
|
<option value="24jam">Buka 24 Jam</option>
|
|
<option value="terbatas">Jam Terbatas</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label
|
|
class="block text-xs font-semibold text-slate-600 mb-1 uppercase tracking-wide">Latitude</label>
|
|
<input type="number" step="any" id="inputLat" required readonly
|
|
class="w-full px-3 py-2 border border-slate-200 bg-slate-100 rounded-lg text-sm text-slate-600 cursor-not-allowed">
|
|
</div>
|
|
<div>
|
|
<label
|
|
class="block text-xs font-semibold text-slate-600 mb-1 uppercase tracking-wide">Longitude</label>
|
|
<input type="number" step="any" id="inputLng" required readonly
|
|
class="w-full px-3 py-2 border border-slate-200 bg-slate-100 rounded-lg text-sm text-slate-600 cursor-not-allowed">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-2 flex gap-3">
|
|
<button type="button" id="btnCancelModal"
|
|
class="flex-1 px-4 py-2 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 font-medium text-sm transition-colors">Batal</button>
|
|
<button type="submit"
|
|
class="flex-1 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 font-medium text-sm transition-colors shadow-md shadow-teal-200">Simpan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// --- State Management ---
|
|
let spbuData = [];
|
|
let isAdminMode = <?php echo $isAdmin ? 'true' : 'false'; ?>;
|
|
let spbuMarkers = {};
|
|
let map, searchQuery = "";
|
|
let isSelectingLocation = false;
|
|
let selectedLatLng = null;
|
|
let tempMarker = null;
|
|
|
|
// --- API Functions ---
|
|
async function fetchSpbuData() {
|
|
try {
|
|
const response = await fetch('api/get_spbu.php');
|
|
spbuData = await response.json();
|
|
return spbuData;
|
|
} catch (error) {
|
|
console.error('Error fetching SPBU data:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function checkSession() {
|
|
try {
|
|
const response = await fetch('api/check_session.php');
|
|
const data = await response.json();
|
|
isAdminMode = data.is_admin;
|
|
return isAdminMode;
|
|
} catch (error) {
|
|
console.error('Error checking session:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function login(username, password) {
|
|
try {
|
|
const response = await fetch('api/login.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
isAdminMode = true;
|
|
renderAuthContainer();
|
|
updateAdminUI();
|
|
document.getElementById('loginOverlay').classList.add('hidden');
|
|
document.getElementById('loginError').classList.add('hidden');
|
|
showNotification('Login berhasil! Mode admin diaktifkan.');
|
|
} else {
|
|
document.getElementById('loginError').classList.remove('hidden');
|
|
document.getElementById('loginError').textContent = data.message || 'Login gagal';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error during login:', error);
|
|
showNotification('Error: Gagal terhubung ke server');
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
if (confirm('Apakah Anda yakin ingin logout?')) {
|
|
try {
|
|
await fetch('api/logout.php');
|
|
isAdminMode = false;
|
|
isSelectingLocation = false;
|
|
removeTempMarker();
|
|
map.closePopup();
|
|
renderAuthContainer();
|
|
updateAdminUI();
|
|
showNotification('Logout berhasil! Kembali ke mode normal.');
|
|
} catch (error) {
|
|
console.error('Error during logout:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function addSpbu(data) {
|
|
try {
|
|
const response = await fetch('api/add_spbu.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
const result = await response.json();
|
|
if (response.ok) {
|
|
showNotification('SPBU berhasil ditambahkan!');
|
|
await refreshData();
|
|
return result;
|
|
} else if (response.status === 401) {
|
|
showNotification('Session expired, silakan login ulang');
|
|
await logout();
|
|
} else {
|
|
showNotification('Error: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error adding SPBU:', error);
|
|
showNotification('Error: Gagal menambahkan SPBU');
|
|
}
|
|
}
|
|
|
|
async function updateSpbu(data) {
|
|
try {
|
|
const response = await fetch('api/update_spbu.php', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
const result = await response.json();
|
|
if (response.ok) {
|
|
showNotification('SPBU berhasil diupdate!');
|
|
await refreshData();
|
|
} else if (response.status === 401) {
|
|
showNotification('Session expired, silakan login ulang');
|
|
await logout();
|
|
} else {
|
|
showNotification('Error: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating SPBU:', error);
|
|
showNotification('Error: Gagal mengupdate SPBU');
|
|
}
|
|
}
|
|
|
|
async function updatePosition(id, latitude, longitude) {
|
|
try {
|
|
const response = await fetch('api/update_position.php', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, latitude, longitude })
|
|
});
|
|
if (response.ok) {
|
|
showNotification('Posisi SPBU berhasil diupdate!');
|
|
} else if (response.status === 401) {
|
|
showNotification('Session expired, silakan login ulang');
|
|
await logout();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating position:', error);
|
|
showNotification('Error: Gagal mengupdate posisi');
|
|
}
|
|
}
|
|
|
|
async function deleteSpbu(id) {
|
|
if (confirm('Apakah Anda yakin ingin menghapus SPBU ini?')) {
|
|
try {
|
|
const response = await fetch('api/delete_spbu.php', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id })
|
|
});
|
|
if (response.ok) {
|
|
showNotification('SPBU berhasil dihapus!');
|
|
await refreshData();
|
|
} else if (response.status === 401) {
|
|
showNotification('Session expired, silakan login ulang');
|
|
await logout();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting SPBU:', error);
|
|
showNotification('Error: Gagal menghapus SPBU');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function refreshData() {
|
|
await fetchSpbuData();
|
|
renderAllMarkers();
|
|
renderList();
|
|
updateStats();
|
|
}
|
|
|
|
// --- Initialize Map ---
|
|
function initMap() {
|
|
map = L.map('map', { zoomControl: true }).setView([-0.0263, 109.3425], 13);
|
|
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
|
subdomains: 'abcd',
|
|
maxZoom: 19
|
|
}).addTo(map);
|
|
|
|
// Map click untuk memilih lokasi (hanya saat mode pilih lokasi)
|
|
map.on('click', function (e) {
|
|
if (isSelectingLocation) {
|
|
selectLocation(e.latlng);
|
|
}
|
|
});
|
|
|
|
// Load data awal
|
|
refreshData().then(() => {
|
|
renderAuthContainer();
|
|
updateAdminUI();
|
|
});
|
|
}
|
|
|
|
function selectLocation(latlng) {
|
|
selectedLatLng = latlng;
|
|
|
|
removeTempMarker();
|
|
|
|
tempMarker = L.marker([latlng.lat, latlng.lng], {
|
|
icon: L.divIcon({
|
|
className: 'custom-temp-icon',
|
|
html: `<div style="background-color: #f59e0b; width: 32px; height: 32px; border-radius: 50%; border: 3px solid white; box-shadow: 0 4px 6px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; animation: pulse 2s infinite;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" style="width: 16px; height: 16px; color: white;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
</svg>
|
|
</div>`,
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 16]
|
|
})
|
|
}).addTo(map);
|
|
|
|
// Buka modal untuk isi data
|
|
openModalForNewLocation(latlng);
|
|
}
|
|
|
|
function removeTempMarker() {
|
|
if (tempMarker) {
|
|
map.removeLayer(tempMarker);
|
|
tempMarker = null;
|
|
}
|
|
}
|
|
|
|
function openModalForNewLocation(latlng) {
|
|
const modal = document.getElementById('modalOverlay');
|
|
const title = document.getElementById('modalTitle');
|
|
const form = document.getElementById('spbuForm');
|
|
|
|
form.reset();
|
|
document.getElementById('spbuId').value = '';
|
|
title.textContent = 'Tambah SPBU Baru';
|
|
document.getElementById('inputLat').value = latlng.lat.toFixed(7);
|
|
document.getElementById('inputLng').value = latlng.lng.toFixed(7);
|
|
|
|
modal.classList.remove('hidden');
|
|
}
|
|
|
|
// --- Render All Markers ---
|
|
function renderAllMarkers() {
|
|
Object.values(spbuMarkers).forEach(m => map.removeLayer(m));
|
|
spbuMarkers = {};
|
|
|
|
spbuData.forEach(spbu => {
|
|
const icon = createSpbuIcon(spbu.status);
|
|
|
|
const marker = L.marker([spbu.latitude, spbu.longitude], {
|
|
icon: icon,
|
|
draggable: isAdminMode
|
|
}).addTo(map);
|
|
|
|
marker.on('dragend', function (e) {
|
|
const newPos = e.target.getLatLng();
|
|
updatePosition(spbu.id, newPos.lat, newPos.lng);
|
|
|
|
// Update data lokal
|
|
spbu.latitude = newPos.lat;
|
|
spbu.longitude = newPos.lng;
|
|
marker.setPopupContent(createPopupContent(spbu));
|
|
renderList();
|
|
updateStats();
|
|
});
|
|
|
|
const popupContent = createPopupContent(spbu);
|
|
marker.bindPopup(popupContent, { maxWidth: 300 });
|
|
spbuMarkers[spbu.id] = marker;
|
|
});
|
|
}
|
|
|
|
function createSpbuIcon(status = 'terbatas') {
|
|
const color = status === '24jam' ? '#3b82f6' : '#10b981'; // Biru untuk 24 jam, Hijau untuk terbatas
|
|
|
|
return L.divIcon({
|
|
className: 'custom-spbu-icon',
|
|
html: `<div style="background-color: ${color}; width: 32px; height: 32px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); border: 3px solid white; box-shadow: 0 4px 6px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" style="transform: rotate(45deg); width: 16px; height: 16px; color: white;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>`,
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 32],
|
|
popupAnchor: [0, -32]
|
|
});
|
|
}
|
|
|
|
function createPopupContent(spbu) {
|
|
return `
|
|
<div class="min-w-[200px]">
|
|
<div class="flex items-start justify-between mb-2">
|
|
<h3 class="font-bold text-slate-800 text-base leading-tight pr-2">${spbu.nama}</h3>
|
|
<span class="text-xs px-2 py-0.5 rounded-full ${spbu.status === '24jam' ? 'bg-teal-100 text-teal-700' : 'bg-amber-100 text-amber-700'} whitespace-nowrap">
|
|
${spbu.status === '24jam' ? '24 Jam' : 'Terbatas'}
|
|
</span>
|
|
</div>
|
|
<div class="space-y-1 text-sm text-slate-600 mb-3">
|
|
<p><span class="font-medium text-slate-500">No. SPBU:</span> ${spbu.no_spbu}</p>
|
|
<p class="text-xs text-slate-400">📍 ${spbu.latitude.toFixed(5)}, ${spbu.longitude.toFixed(5)}</p>
|
|
</div>
|
|
${isAdminMode ? `
|
|
<div class="flex gap-2 pt-2 border-t border-slate-100">
|
|
<button onclick="editSpbu(${spbu.id})" class="flex-1 bg-blue-50 hover:bg-blue-100 text-blue-600 text-xs font-semibold py-1.5 rounded-md transition-colors flex items-center justify-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
|
Edit
|
|
</button>
|
|
<button onclick="deleteSpbu(${spbu.id})" class="flex-1 bg-red-50 hover:bg-red-100 text-red-600 text-xs font-semibold py-1.5 rounded-md transition-colors flex items-center justify-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
|
Hapus
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-slate-400 mt-2 text-center">💡 Tahan & geser marker untuk pindahkan lokasi</p>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// --- Render Sidebar List ---
|
|
function renderList() {
|
|
const listContainer = document.getElementById('spbuList');
|
|
const filteredData = spbuData.filter(spbu =>
|
|
spbu.nama.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
spbu.no_spbu.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
|
|
if (filteredData.length === 0) {
|
|
listContainer.innerHTML = `
|
|
<div class="flex flex-col items-center justify-center py-10 text-slate-400">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mb-2 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<p class="text-sm font-medium">Tidak ada SPBU ditemukan</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
listContainer.innerHTML = filteredData.map(spbu => `
|
|
<div class="bg-white border border-slate-200 rounded-xl p-3 hover:border-teal-400 hover:shadow-md transition-all cursor-pointer group" onclick="zoomToSpbu(${spbu.id})">
|
|
<div class="flex items-start justify-between mb-1">
|
|
<h4 class="font-semibold text-slate-800 text-sm group-hover:text-teal-700 transition-colors">${spbu.nama}</h4>
|
|
<span class="text-[10px] px-2 py-0.5 rounded-full ${spbu.status === '24jam' ? 'bg-teal-100 text-teal-700' : 'bg-amber-100 text-amber-700'} font-medium">
|
|
${spbu.status === '24jam' ? '24 Jam' : 'Terbatas'}
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-slate-500 mb-1">No: ${spbu.no_spbu}</p>
|
|
<p class="text-[10px] text-slate-400 font-mono">📍 ${spbu.latitude.toFixed(4)}, ${spbu.longitude.toFixed(4)}</p>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// --- Update Stats ---
|
|
function updateStats() {
|
|
document.getElementById('totalSpbu').textContent = spbuData.length;
|
|
document.getElementById('total24Jam').textContent = spbuData.filter(s => s.status === '24jam').length;
|
|
}
|
|
|
|
// --- Notifications ---
|
|
function showNotification(message) {
|
|
const notification = document.createElement('div');
|
|
notification.className = 'fixed bottom-4 right-4 bg-teal-600 text-white px-4 py-2 rounded-lg shadow-lg z-[9999] text-sm font-medium fade-in';
|
|
notification.textContent = message;
|
|
document.body.appendChild(notification);
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
// --- UI Functions ---
|
|
function renderAuthContainer() {
|
|
const container = document.getElementById('authContainer');
|
|
if (isAdminMode) {
|
|
container.innerHTML = `
|
|
<div class="flex items-center gap-2 bg-white/10 rounded-full px-3 py-1.5 border border-white/20">
|
|
<span class="text-xs text-white font-medium">Admin</span>
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" id="adminToggle" class="sr-only peer" checked>
|
|
<div class="w-9 h-5 bg-teal-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all"></div>
|
|
</label>
|
|
<button id="btnLogout" class="text-xs text-white hover:text-teal-200 transition-colors ml-1" title="Logout">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
`;
|
|
} else {
|
|
container.innerHTML = `
|
|
<button id="btnShowLogin" class="bg-white/20 hover:bg-white/30 text-white text-sm font-medium px-4 py-2 rounded-full border border-white/30 transition-all flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
Login
|
|
</button>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function updateAdminUI() {
|
|
const btnAddSpbu = document.getElementById('btnAddSpbu');
|
|
const mapHint = document.getElementById('mapHint');
|
|
const mapContainer = document.getElementById('mapContainer');
|
|
const modeIndicator = document.getElementById('modeIndicator');
|
|
|
|
if (isAdminMode) {
|
|
btnAddSpbu.style.display = 'flex';
|
|
mapHint.style.display = 'block';
|
|
mapContainer.classList.add('admin-mode');
|
|
} else {
|
|
btnAddSpbu.style.display = 'none';
|
|
mapHint.style.display = 'none';
|
|
mapContainer.classList.remove('admin-mode');
|
|
isSelectingLocation = false;
|
|
removeTempMarker();
|
|
modeIndicator.classList.add('hidden');
|
|
}
|
|
|
|
renderAllMarkers();
|
|
renderList();
|
|
updateStats();
|
|
}
|
|
|
|
function zoomToSpbu(id) {
|
|
const spbu = spbuData.find(s => s.id === id);
|
|
if (spbu && spbuMarkers[id]) {
|
|
map.flyTo([spbu.latitude, spbu.longitude], 16, { duration: 1.5 });
|
|
spbuMarkers[id].openPopup();
|
|
}
|
|
}
|
|
|
|
function editSpbu(id) {
|
|
const spbu = spbuData.find(s => s.id === id);
|
|
if (spbu && isAdminMode) {
|
|
map.closePopup();
|
|
const modal = document.getElementById('modalOverlay');
|
|
const title = document.getElementById('modalTitle');
|
|
const form = document.getElementById('spbuForm');
|
|
|
|
form.reset();
|
|
document.getElementById('spbuId').value = spbu.id;
|
|
title.textContent = 'Edit Data SPBU';
|
|
document.getElementById('inputNama').value = spbu.nama;
|
|
document.getElementById('inputNo').value = spbu.no_spbu;
|
|
document.getElementById('inputStatus').value = spbu.status;
|
|
document.getElementById('inputLat').value = spbu.latitude;
|
|
document.getElementById('inputLng').value = spbu.longitude;
|
|
|
|
modal.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('modalOverlay').classList.add('hidden');
|
|
if (isSelectingLocation) {
|
|
isSelectingLocation = false;
|
|
removeTempMarker();
|
|
document.getElementById('modeIndicator').classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initMap();
|
|
|
|
// Search inputs
|
|
const handleSearch = (e) => {
|
|
searchQuery = e.target.value;
|
|
renderList();
|
|
};
|
|
document.getElementById('searchInput').addEventListener('input', handleSearch);
|
|
document.getElementById('searchInputMobile').addEventListener('input', handleSearch);
|
|
|
|
// Auth container events
|
|
document.getElementById('authContainer').addEventListener('click', (e) => {
|
|
if (e.target.closest('#btnShowLogin')) {
|
|
document.getElementById('loginOverlay').classList.remove('hidden');
|
|
}
|
|
if (e.target.closest('#btnLogout')) {
|
|
logout();
|
|
}
|
|
if (e.target.closest('#adminToggle')) {
|
|
logout();
|
|
}
|
|
});
|
|
|
|
// Login form
|
|
document.getElementById('loginForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
const username = document.getElementById('loginUsername').value;
|
|
const password = document.getElementById('loginPassword').value;
|
|
login(username, password);
|
|
});
|
|
document.getElementById('btnCancelLogin').addEventListener('click', () => {
|
|
document.getElementById('loginOverlay').classList.add('hidden');
|
|
});
|
|
document.getElementById('loginOverlay').addEventListener('click', (e) => {
|
|
if (e.target === document.getElementById('loginOverlay')) {
|
|
document.getElementById('loginOverlay').classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
// Tombol tambah SPBU
|
|
document.getElementById('btnAddSpbu').addEventListener('click', () => {
|
|
if (isAdminMode) {
|
|
isSelectingLocation = true;
|
|
document.getElementById('modeIndicator').classList.remove('hidden');
|
|
showNotification('Silakan klik pada peta untuk memilih lokasi SPBU');
|
|
}
|
|
});
|
|
|
|
// Modal controls
|
|
document.getElementById('btnCloseModal').addEventListener('click', closeModal);
|
|
document.getElementById('btnCancelModal').addEventListener('click', closeModal);
|
|
document.getElementById('modalOverlay').addEventListener('click', (e) => {
|
|
if (e.target === document.getElementById('modalOverlay')) closeModal();
|
|
});
|
|
|
|
// Form submit SPBU
|
|
document.getElementById('spbuForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
if (!isAdminMode) return;
|
|
|
|
const id = document.getElementById('spbuId').value;
|
|
const nama = document.getElementById('inputNama').value;
|
|
const no_spbu = document.getElementById('inputNo').value;
|
|
const status = document.getElementById('inputStatus').value;
|
|
const latitude = parseFloat(document.getElementById('inputLat').value);
|
|
const longitude = parseFloat(document.getElementById('inputLng').value);
|
|
|
|
const data = { nama, no_spbu, status, latitude, longitude };
|
|
|
|
if (id) {
|
|
// Update
|
|
data.id = parseInt(id);
|
|
await updateSpbu(data);
|
|
} else {
|
|
// Create
|
|
await addSpbu(data);
|
|
}
|
|
|
|
closeModal();
|
|
isSelectingLocation = false;
|
|
removeTempMarker();
|
|
document.getElementById('modeIndicator').classList.add('hidden');
|
|
|
|
// Fly to location
|
|
map.flyTo([latitude, longitude], 16, { duration: 1 });
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|