1076 lines
56 KiB
PHP
1076 lines
56 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 - Layer Control</title>
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<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>
|
|
<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; }
|
|
|
|
@keyframes pulse {
|
|
0% { box-shadow: 0 0 0 0 rgba(13, 148, 136, 0.5); }
|
|
70% { box-shadow: 0 0 0 10px rgba(13, 148, 136, 0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(13, 148, 136, 0); }
|
|
}
|
|
.pulse-ring { animation: pulse 2s infinite; }
|
|
|
|
/* Google Maps Style Panel + Handle */
|
|
.gm-panel-wrapper {
|
|
display: flex;
|
|
align-items: stretch;
|
|
z-index: 20;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.gm-panel-wrapper-left {
|
|
flex-direction: row;
|
|
}
|
|
|
|
.gm-panel-wrapper-right {
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
.gm-panel-content {
|
|
background: white;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.gm-panel-content-left {
|
|
width: 384px;
|
|
min-width: 384px;
|
|
border-right: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.gm-panel-content-right {
|
|
width: 320px;
|
|
min-width: 320px;
|
|
border-left: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.gm-panel-content.collapsed {
|
|
width: 0 !important;
|
|
min-width: 0 !important;
|
|
border: none !important;
|
|
}
|
|
|
|
.gm-handle {
|
|
width: 23px;
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: background 0.2s;
|
|
box-shadow: 0 1px 2px rgba(60,64,67,0.3), 0 2px 6px 2px rgba(60,64,67,0.15);
|
|
flex-shrink: 0;
|
|
margin-top: auto;
|
|
margin-bottom: auto;
|
|
height: 48px;
|
|
z-index: 30;
|
|
}
|
|
|
|
.gm-handle:hover {
|
|
background: #f1f3f4;
|
|
}
|
|
|
|
.gm-handle-left {
|
|
border-radius: 0 8px 8px 0;
|
|
border-left: none;
|
|
}
|
|
|
|
.gm-handle-right {
|
|
border-radius: 8px 0 0 8px;
|
|
border-right: none;
|
|
}
|
|
|
|
.gm-handle svg {
|
|
width: 20px;
|
|
height: 20px;
|
|
color: #5f6368;
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.layer-card {
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
border: 2px solid transparent;
|
|
}
|
|
.layer-card:hover { border-color: #cbd5e1; }
|
|
.layer-card.active {
|
|
border-color: #0d9488;
|
|
background: #f0fdfa;
|
|
}
|
|
|
|
.layer-preview {
|
|
width: 100%;
|
|
height: 45px;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
border: 2px solid #e2e8f0;
|
|
transition: all 0.2s;
|
|
}
|
|
.layer-card.active .layer-preview { border-color: #0d9488; }
|
|
|
|
.toggle-switch {
|
|
width: 40px;
|
|
height: 22px;
|
|
background: #cbd5e1;
|
|
border-radius: 11px;
|
|
position: relative;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.toggle-switch.active { background: #0d9488; }
|
|
.toggle-switch::after {
|
|
content: '';
|
|
width: 18px;
|
|
height: 18px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
}
|
|
.toggle-switch.active::after { left: 20px; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-slate-100 h-screen flex flex-col overflow-hidden">
|
|
|
|
<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">
|
|
<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>
|
|
|
|
<div id="authContainer"></div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="flex flex-1 overflow-hidden relative">
|
|
|
|
<div class="gm-panel-wrapper gm-panel-wrapper-left" id="panelWrapperLeft">
|
|
<aside class="gm-panel-content gm-panel-content-left shadow-xl" id="sidebarLeft">
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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: <?php echo $isAdmin ? 'flex' : '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>
|
|
|
|
<div id="spbuList" class="flex-1 overflow-y-auto custom-scrollbar p-3 space-y-2"></div>
|
|
</aside>
|
|
|
|
<button class="gm-handle gm-handle-left" id="handleLeft" onclick="toggleLeftPanel()" title="Ciutkan panel samping">
|
|
<svg id="handleLeftIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex-1 relative" id="mapContainer">
|
|
<div id="map"></div>
|
|
|
|
<div id="mapHint" class="absolute bottom-4 left-1/2 -translate-x-1/2 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;">
|
|
💡 Klik pada peta untuk memilih lokasi SPBU baru
|
|
</div>
|
|
|
|
<div id="modeIndicator" class="absolute top-20 left-4 z-[1000] bg-teal-600 text-white px-4 py-2 rounded-lg shadow-md text-xs font-medium hidden pulse-ring">
|
|
🎯 Mode Pilih Titik - Klik pada peta
|
|
</div>
|
|
</div>
|
|
|
|
<div class="gm-panel-wrapper gm-panel-wrapper-right" id="panelWrapperRight">
|
|
<aside class="gm-panel-content gm-panel-content-right shadow-xl" id="sidebarRight">
|
|
<div class="p-4 border-b border-slate-100 bg-slate-50">
|
|
<h3 class="text-sm font-semibold text-slate-700">Pengaturan Peta</h3>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-5">
|
|
<div>
|
|
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3 block">Tipe Peta Dasar</label>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div class="layer-card active p-2 rounded-lg" onclick="switchBaseMap('light')" id="layerLight">
|
|
<div class="layer-preview mb-1.5" style="background: linear-gradient(135deg, #f0f9ff 0%, #e0e7ff 50%, #c7d2fe 100%); position: relative; overflow: hidden;">
|
|
<div style="position: absolute; top: 30%; left: 20%; width: 30%; height: 2px; background: #94a3b8;"></div>
|
|
<div style="position: absolute; top: 50%; left: 40%; width: 25%; height: 2px; background: #94a3b8;"></div>
|
|
<div style="position: absolute; top: 40%; left: 60%; width: 20%; height: 2px; background: #94a3b8;"></div>
|
|
</div>
|
|
<span class="text-[11px] font-semibold text-slate-700">Ringan</span>
|
|
</div>
|
|
|
|
<div class="layer-card p-2 rounded-lg" onclick="switchBaseMap('satellite')" id="layerSatellite">
|
|
<div class="layer-preview mb-1.5" style="background: linear-gradient(135deg, #064e3b 0%, #065f46 30%, #047857 60%, #059669 100%); position: relative; overflow: hidden;">
|
|
<div style="position: absolute; top: 20%; left: 30%; width: 40%; height: 15%; background: rgba(255,255,255,0.3); border-radius: 2px;"></div>
|
|
<div style="position: absolute; bottom: 30%; right: 20%; width: 25%; height: 10%; background: rgba(255,255,255,0.2); border-radius: 2px;"></div>
|
|
</div>
|
|
<span class="text-[11px] font-semibold text-slate-700">Satelit</span>
|
|
</div>
|
|
|
|
<div class="layer-card p-2 rounded-lg" onclick="switchBaseMap('terrain')" id="layerTerrain">
|
|
<div class="layer-preview mb-1.5" style="background: linear-gradient(135deg, #fef3c7 0%, #d4a574 40%, #8b6914 70%, #4a7c59 100%); position: relative; overflow: hidden;">
|
|
<div style="position: absolute; bottom: 0; left: 0; right: 0; height: 40%; background: linear-gradient(to top, #4a7c59, transparent);"></div>
|
|
</div>
|
|
<span class="text-[11px] font-semibold text-slate-700">Topografi</span>
|
|
</div>
|
|
|
|
<div class="layer-card p-2 rounded-lg" onclick="switchBaseMap('dark')" id="layerDark">
|
|
<div class="layer-preview mb-1.5" style="background: linear-gradient(135deg, #1e293b 0%, #334155 30%, #1e293b 60%, #0f172a 100%); position: relative; overflow: hidden;">
|
|
<div style="position: absolute; top: 25%; left: 15%; width: 35%; height: 1.5px; background: #64748b;"></div>
|
|
<div style="position: absolute; top: 45%; left: 50%; width: 20%; height: 1.5px; background: #64748b;"></div>
|
|
<div style="position: absolute; top: 35%; left: 70%; width: 15%; height: 1.5px; background: #64748b;"></div>
|
|
</div>
|
|
<span class="text-[11px] font-semibold text-slate-700">Gelap</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-3 block">Layer SPBU</label>
|
|
<div class="space-y-2">
|
|
<div class="layer-card active p-3 rounded-lg flex items-center justify-between" id="layer24JamCard">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-full bg-emerald-500 flex items-center justify-center shadow-md">
|
|
<svg class="w-4 h-4 text-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>
|
|
<div>
|
|
<p class="text-sm font-semibold text-slate-700">SPBU 24 Jam</p>
|
|
<p class="text-[10px] text-slate-400" id="count24Jam">0 titik</p>
|
|
</div>
|
|
</div>
|
|
<div class="toggle-switch active" onclick="toggleLayer('24jam', event)" id="toggle24Jam"></div>
|
|
</div>
|
|
|
|
<div class="layer-card active p-3 rounded-lg flex items-center justify-between" id="layerTerbatasCard">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-full bg-amber-500 flex items-center justify-center shadow-md">
|
|
<svg class="w-4 h-4 text-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>
|
|
<div>
|
|
<p class="text-sm font-semibold text-slate-700">SPBU Terbatas</p>
|
|
<p class="text-[10px] text-slate-400" id="countTerbatas">0 titik</p>
|
|
</div>
|
|
</div>
|
|
<div class="toggle-switch active" onclick="toggleLayer('terbatas', event)" id="toggleTerbatas"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<button class="gm-handle gm-handle-right" id="handleRight" onclick="toggleRightPanel()" title="Ciutkan panel lapisan">
|
|
<svg id="handleRightIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
let spbuData = [];
|
|
let isAdminMode = <?php echo $isAdmin ? 'true' : 'false'; ?>;
|
|
let spbuMarkers = {};
|
|
let map, searchQuery = "";
|
|
let isSelectingLocation = false;
|
|
let selectedLatLng = null;
|
|
let tempMarker = null;
|
|
let currentBaseMap = 'light';
|
|
let baseMaps = {};
|
|
let layer24Jam, layerTerbatas;
|
|
let isLeftPanelOpen = true;
|
|
let isRightPanelOpen = true;
|
|
|
|
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();
|
|
refreshLayers();
|
|
renderList();
|
|
updateStats();
|
|
updateLayerCounts();
|
|
}
|
|
|
|
function toggleLeftPanel() {
|
|
isLeftPanelOpen = !isLeftPanelOpen;
|
|
const sidebar = document.getElementById('sidebarLeft');
|
|
const icon = document.getElementById('handleLeftIcon');
|
|
|
|
if (isLeftPanelOpen) {
|
|
sidebar.classList.remove('collapsed');
|
|
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />';
|
|
document.getElementById('handleLeft').title = 'Ciutkan panel samping';
|
|
} else {
|
|
sidebar.classList.add('collapsed');
|
|
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />';
|
|
document.getElementById('handleLeft').title = 'Buka panel samping';
|
|
}
|
|
}
|
|
|
|
function toggleRightPanel() {
|
|
isRightPanelOpen = !isRightPanelOpen;
|
|
const sidebar = document.getElementById('sidebarRight');
|
|
const icon = document.getElementById('handleRightIcon');
|
|
|
|
if (isRightPanelOpen) {
|
|
sidebar.classList.remove('collapsed');
|
|
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />';
|
|
document.getElementById('handleRight').title = 'Ciutkan panel lapisan';
|
|
} else {
|
|
sidebar.classList.add('collapsed');
|
|
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />';
|
|
document.getElementById('handleRight').title = 'Buka panel lapisan';
|
|
}
|
|
}
|
|
|
|
function initMap() {
|
|
map = L.map('map', { zoomControl: true }).setView([-0.0263, 109.3425], 13);
|
|
|
|
baseMaps = {
|
|
light: L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
|
subdomains: 'abcd',
|
|
maxZoom: 19
|
|
}),
|
|
satellite: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
|
attribution: 'Tiles © Esri',
|
|
maxZoom: 18
|
|
}),
|
|
terrain: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
|
attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OSM</a> contributors, SRTM | Map style: © <a href="https://opentopomap.org">OpenTopoMap</a>',
|
|
maxZoom: 17
|
|
}),
|
|
dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
|
subdomains: 'abcd',
|
|
maxZoom: 19
|
|
})
|
|
};
|
|
|
|
baseMaps.light.addTo(map);
|
|
|
|
layer24Jam = L.layerGroup();
|
|
layerTerbatas = L.layerGroup();
|
|
|
|
map.on('click', function(e) {
|
|
if (isSelectingLocation && isAdminMode) {
|
|
selectLocation(e.latlng);
|
|
}
|
|
});
|
|
|
|
refreshData().then(() => {
|
|
renderAuthContainer();
|
|
updateAdminUI();
|
|
updateLayerCounts();
|
|
});
|
|
}
|
|
|
|
function switchBaseMap(type) {
|
|
map.removeLayer(baseMaps[currentBaseMap]);
|
|
baseMaps[type].addTo(map);
|
|
|
|
document.querySelectorAll('#sidebarRight .layer-card').forEach(card => card.classList.remove('active'));
|
|
document.getElementById(`layer${type.charAt(0).toUpperCase() + type.slice(1)}`).classList.add('active');
|
|
|
|
currentBaseMap = type;
|
|
}
|
|
|
|
function toggleLayer(type, event) {
|
|
event.stopPropagation();
|
|
|
|
const toggle = document.getElementById(`toggle${type === '24jam' ? '24Jam' : 'Terbatas'}`);
|
|
const card = document.getElementById(`layer${type === '24jam' ? '24Jam' : 'Terbatas'}Card`);
|
|
|
|
if (type === '24jam') {
|
|
if (map.hasLayer(layer24Jam)) {
|
|
map.removeLayer(layer24Jam);
|
|
toggle.classList.remove('active');
|
|
card.classList.remove('active');
|
|
} else {
|
|
layer24Jam.addTo(map);
|
|
toggle.classList.add('active');
|
|
card.classList.add('active');
|
|
}
|
|
} else {
|
|
if (map.hasLayer(layerTerbatas)) {
|
|
map.removeLayer(layerTerbatas);
|
|
toggle.classList.remove('active');
|
|
card.classList.remove('active');
|
|
} else {
|
|
layerTerbatas.addTo(map);
|
|
toggle.classList.add('active');
|
|
card.classList.add('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
function getMarkerIcon(status) {
|
|
const color = status === '24jam' ? '#10b981' : '#f59e0b';
|
|
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 refreshLayers() {
|
|
layer24Jam.clearLayers();
|
|
layerTerbatas.clearLayers();
|
|
|
|
spbuData.forEach(spbu => {
|
|
const lat = spbu.latitude || spbu.lat;
|
|
const lng = spbu.longitude || spbu.lng;
|
|
|
|
const marker = L.marker([lat, lng], {
|
|
icon: getMarkerIcon(spbu.status),
|
|
draggable: isAdminMode
|
|
});
|
|
|
|
marker.on('dragend', function(e) {
|
|
const newPos = e.target.getLatLng();
|
|
updatePosition(spbu.id, newPos.lat, newPos.lng);
|
|
spbu.latitude = newPos.lat;
|
|
spbu.longitude = newPos.lng;
|
|
marker.setPopupContent(createPopupContent(spbu));
|
|
});
|
|
|
|
marker.bindPopup(createPopupContent(spbu), { maxWidth: 300 });
|
|
|
|
if (spbu.status === '24jam') {
|
|
layer24Jam.addLayer(marker);
|
|
} else {
|
|
layerTerbatas.addLayer(marker);
|
|
}
|
|
});
|
|
|
|
if (document.getElementById('toggle24Jam') && document.getElementById('toggle24Jam').classList.contains('active')) {
|
|
layer24Jam.addTo(map);
|
|
}
|
|
if (document.getElementById('toggleTerbatas') && document.getElementById('toggleTerbatas').classList.contains('active')) {
|
|
layerTerbatas.addTo(map);
|
|
}
|
|
}
|
|
|
|
function createPopupContent(spbu) {
|
|
const nama = spbu.nama || '';
|
|
const no = spbu.no_spbu || spbu.no || '';
|
|
const lat = spbu.latitude || spbu.lat || 0;
|
|
const lng = spbu.longitude || spbu.lng || 0;
|
|
|
|
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">${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> ${no}</p>
|
|
<p class="text-xs text-slate-400">📍 ${lat.toFixed(5)}, ${lng.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>
|
|
`;
|
|
}
|
|
|
|
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: #0d9488; 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;" class="pulse-ring">
|
|
<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);
|
|
|
|
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');
|
|
}
|
|
|
|
function renderList() {
|
|
const listContainer = document.getElementById('spbuList');
|
|
const filteredData = spbuData.filter(spbu =>
|
|
(spbu.nama && spbu.nama.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
|
((spbu.no_spbu || spbu.no) && (spbu.no_spbu || spbu.no).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 || spbu.no}</p>
|
|
<p class="text-[10px] text-slate-400 font-mono">📍 ${(spbu.latitude || spbu.lat).toFixed(4)}, ${(spbu.longitude || spbu.lng).toFixed(4)}</p>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function updateStats() {
|
|
document.getElementById('totalSpbu').textContent = spbuData.length;
|
|
document.getElementById('total24Jam').textContent = spbuData.filter(s => s.status === '24jam').length;
|
|
}
|
|
|
|
function updateLayerCounts() {
|
|
const count24 = spbuData.filter(s => s.status === '24jam').length;
|
|
const countTerbatas = spbuData.filter(s => s.status === 'terbatas').length;
|
|
document.getElementById('count24Jam').textContent = `${count24} titik`;
|
|
document.getElementById('countTerbatas').textContent = `${countTerbatas} titik`;
|
|
}
|
|
|
|
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');
|
|
|
|
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();
|
|
document.getElementById('modeIndicator').classList.add('hidden');
|
|
}
|
|
|
|
refreshLayers();
|
|
renderList();
|
|
updateStats();
|
|
updateLayerCounts();
|
|
}
|
|
|
|
function zoomToSpbu(id) {
|
|
const spbu = spbuData.find(s => s.id === id);
|
|
if (spbu) {
|
|
const lat = spbu.latitude || spbu.lat;
|
|
const lng = spbu.longitude || spbu.lng;
|
|
map.flyTo([lat, lng], 16, { duration: 1.5 });
|
|
|
|
setTimeout(() => {
|
|
[layer24Jam, layerTerbatas].forEach(layer => {
|
|
layer.eachLayer(marker => {
|
|
if (marker.getLatLng().lat === lat && marker.getLatLng().lng === lng) {
|
|
marker.openPopup();
|
|
}
|
|
});
|
|
});
|
|
}, 1600);
|
|
}
|
|
}
|
|
|
|
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 || spbu.no;
|
|
document.getElementById('inputStatus').value = spbu.status;
|
|
document.getElementById('inputLat').value = spbu.latitude || spbu.lat;
|
|
document.getElementById('inputLng').value = spbu.longitude || spbu.lng;
|
|
|
|
modal.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('modalOverlay').classList.add('hidden');
|
|
if (isSelectingLocation) {
|
|
isSelectingLocation = false;
|
|
removeTempMarker();
|
|
document.getElementById('modeIndicator').classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function showNotification(message) {
|
|
const notification = document.createElement('div');
|
|
notification.className = 'fixed bottom-4 left-1/2 -translate-x-1/2 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);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initMap();
|
|
|
|
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
searchQuery = e.target.value;
|
|
renderList();
|
|
});
|
|
|
|
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();
|
|
}
|
|
});
|
|
|
|
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');
|
|
}
|
|
});
|
|
|
|
document.getElementById('btnAddSpbu').addEventListener('click', () => {
|
|
if (isAdminMode) {
|
|
isSelectingLocation = true;
|
|
document.getElementById('modeIndicator').classList.remove('hidden');
|
|
showNotification('Silakan klik pada peta untuk memilih lokasi SPBU');
|
|
}
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
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) {
|
|
data.id = parseInt(id);
|
|
await updateSpbu(data);
|
|
} else {
|
|
await addSpbu(data);
|
|
}
|
|
|
|
closeModal();
|
|
isSelectingLocation = false;
|
|
removeTempMarker();
|
|
document.getElementById('modeIndicator').classList.add('hidden');
|
|
|
|
map.flyTo([latitude, longitude], 16, { duration: 1 });
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|