First commit

This commit is contained in:
2026-06-09 19:02:22 +07:00
commit c2c0774b3b
36 changed files with 10493 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
<?php
// ============================================================
// API DATA SPBU (Point / Marker)
// File: api_spbu.php
// Fields: nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude
// ============================================================
require_once 'config.php';
try {
$method = $_SERVER['REQUEST_METHOD'];
$rawInput = file_get_contents('php://input');
$jsonInput = $rawInput ? json_decode($rawInput, true) : null;
if (!is_array($jsonInput)) {
$jsonInput = [];
}
// Fallback jika server memblokir method DELETE/PUT
if ($method === 'POST' && isset($_GET['_method'])) {
$method = strtoupper($_GET['_method']);
} elseif ($method === 'POST' && !empty($jsonInput['_method'])) {
$method = strtoupper($jsonInput['_method']);
}
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if (!$id && !empty($jsonInput['id'])) {
$id = (int)$jsonInput['id'];
}
if ($method === 'GET') {
$db = getDB();
ensureSpbuSchema($db);
if ($id) {
$stmt = $db->prepare(
"SELECT id, nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude
FROM data_spbu WHERE id = ?"
);
$stmt->bind_param('i', $id);
$stmt->execute();
$result = $stmt->get_result()->fetch_assoc();
if ($result) {
$result['latitude'] = (float)$result['latitude'];
$result['longitude'] = (float)$result['longitude'];
echo json_encode($result);
} else {
http_response_code(404);
echo json_encode(['error' => 'Data SPBU tidak ditemukan']);
}
} else {
$result = $db->query(
"SELECT id, nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude
FROM data_spbu ORDER BY id ASC"
);
if (!$result) {
jsonError('Query gagal: ' . $db->error);
}
$rows = [];
while ($row = $result->fetch_assoc()) {
$row['latitude'] = (float)$row['latitude'];
$row['longitude'] = (float)$row['longitude'];
$rows[] = $row;
}
echo json_encode($rows, JSON_UNESCAPED_UNICODE);
}
$db->close();
exit();
}
if ($method === 'POST') {
$input = $jsonInput;
if (empty($input['nama_spbu']) || empty($input['alamat']) || empty($input['no_wa'])
|| !isset($input['latitude']) || !isset($input['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Data tidak lengkap. Field wajib: nama_spbu, alamat, no_wa, latitude, longitude']);
exit();
}
$nama_spbu = trim($input['nama_spbu']);
$alamat = trim($input['alamat']);
$no_wa = trim($input['no_wa']);
$buka_24_jam = isset($input['buka_24_jam']) && $input['buka_24_jam'] === 'Ya' ? 'Ya' : 'Tidak';
$latitude = (float)$input['latitude'];
$longitude = (float)$input['longitude'];
$db = getDB();
ensureSpbuSchema($db);
$stmt = $db->prepare(
"INSERT INTO data_spbu (nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?)"
);
$stmt->bind_param('ssssdd', $nama_spbu, $alamat, $no_wa, $buka_24_jam, $latitude, $longitude);
if ($stmt->execute()) {
http_response_code(201);
echo json_encode([
'message' => 'Data SPBU berhasil disimpan',
'id' => $db->insert_id,
'nama_spbu' => $nama_spbu,
'alamat' => $alamat,
'no_wa' => $no_wa,
'buka_24_jam' => $buka_24_jam,
'latitude' => $latitude,
'longitude' => $longitude
]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Gagal menyimpan data: ' . $stmt->error]);
}
$db->close();
exit();
}
if ($method === 'PUT') {
if (!$id) {
http_response_code(400);
echo json_encode(['error' => 'ID diperlukan untuk update']);
exit();
}
$input = $jsonInput;
$db = getDB();
ensureSpbuSchema($db);
if (isset($input['latitude']) && isset($input['longitude'])
&& !isset($input['nama_spbu'])) {
$latitude = (float)$input['latitude'];
$longitude = (float)$input['longitude'];
$stmt = $db->prepare("UPDATE data_spbu SET latitude = ?, longitude = ? WHERE id = ?");
$stmt->bind_param('ddi', $latitude, $longitude, $id);
} else {
$nama_spbu = trim($input['nama_spbu'] ?? '');
$alamat = trim($input['alamat'] ?? '');
$no_wa = trim($input['no_wa'] ?? '');
$buka_24_jam = isset($input['buka_24_jam']) && $input['buka_24_jam'] === 'Ya' ? 'Ya' : 'Tidak';
$latitude = (float)($input['latitude'] ?? 0);
$longitude = (float)($input['longitude'] ?? 0);
if (empty($nama_spbu) || empty($alamat) || empty($no_wa)) {
http_response_code(400);
echo json_encode(['error' => 'nama_spbu, alamat, dan no_wa wajib diisi']);
exit();
}
$stmt = $db->prepare(
"UPDATE data_spbu
SET nama_spbu = ?, alamat = ?, no_wa = ?, buka_24_jam = ?, latitude = ?, longitude = ?
WHERE id = ?"
);
$stmt->bind_param('ssssddi', $nama_spbu, $alamat, $no_wa, $buka_24_jam, $latitude, $longitude, $id);
}
if ($stmt->execute()) {
echo json_encode(['message' => 'Data SPBU berhasil diperbarui', 'id' => $id]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Gagal memperbarui data: ' . $stmt->error]);
}
$db->close();
exit();
}
if ($method === 'DELETE') {
if (!$id) {
jsonError('ID diperlukan untuk hapus', 400);
}
$db = getDB();
ensureSpbuSchema($db);
$stmt = $db->prepare("DELETE FROM data_spbu WHERE id = ?");
if (!$stmt) {
jsonError('Gagal menyiapkan query hapus: ' . $db->error);
}
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
if ($stmt->affected_rows > 0) {
echo json_encode(['message' => 'Data SPBU berhasil dihapus', 'id' => $id], JSON_UNESCAPED_UNICODE);
} else {
jsonError('Data SPBU tidak ditemukan', 404);
}
} else {
jsonError('Gagal menghapus data: ' . $stmt->error);
}
$stmt->close();
$db->close();
exit();
}
jsonError('Method tidak diizinkan', 405);
} catch (Throwable $e) {
jsonError('Server error: ' . $e->getMessage());
}
+103
View File
@@ -0,0 +1,103 @@
<?php
// ============================================================
// KONFIGURASI DATABASE WebGIS SPBU
// File: config.php
// ============================================================
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'webgis_spbu');
function jsonError($message, $code = 500) {
http_response_code($code);
echo json_encode(['error' => $message]);
exit();
}
function getDB() {
mysqli_report(MYSQLI_REPORT_OFF);
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS);
if ($conn->connect_error) {
http_response_code(500);
die(json_encode(['error' => 'Koneksi MySQL gagal: ' . $conn->connect_error]));
}
$dbName = DB_NAME;
if (!$conn->query("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")) {
http_response_code(500);
die(json_encode(['error' => 'Gagal membuat database: ' . $conn->error]));
}
if (!$conn->select_db($dbName)) {
http_response_code(500);
die(json_encode(['error' => 'Gagal memilih database: ' . $conn->error]));
}
$conn->set_charset('utf8mb4');
return $conn;
}
function ensureSpbuSchema($db) {
if (!$db->query("
CREATE TABLE IF NOT EXISTS data_spbu (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_spbu VARCHAR(200) NOT NULL,
alamat VARCHAR(300) NOT NULL DEFAULT '',
no_wa VARCHAR(20) NOT NULL,
buka_24_jam ENUM('Ya','Tidak') NOT NULL DEFAULT 'Tidak',
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
")) {
jsonError('Gagal membuat tabel data_spbu: ' . $db->error);
}
$check = $db->query("SHOW COLUMNS FROM data_spbu LIKE 'alamat'");
if ($check && $check->num_rows === 0) {
$db->query("ALTER TABLE data_spbu ADD COLUMN alamat VARCHAR(300) NOT NULL DEFAULT '' AFTER nama_spbu");
$db->query("UPDATE data_spbu SET alamat = CONCAT(nama_spbu, ', Pontianak') WHERE alamat = ''");
}
$countRes = $db->query("SELECT COUNT(*) AS total FROM data_spbu");
if ($countRes && (int)$countRes->fetch_assoc()['total'] === 0) {
seedSpbuSampleData($db);
}
}
function seedSpbuSampleData($db) {
$samples = [
['SPBU Jl. Ahmad Yani', 'Jl. Ahmad Yani No. 12, Pontianak', '0812-1111-0001', 'Ya', -0.0400, 109.3200],
['SPBU Jl. Gajah Mada', 'Jl. Gajah Mada No. 45, Pontianak', '0812-1111-0002', 'Ya', -0.0520, 109.3450],
['SPBU Jl. Diponegoro', 'Jl. Diponegoro No. 88, Pontianak', '0812-1111-0003', 'Tidak', -0.0610, 109.3600],
['SPBU Jl. Sutan Syahrir', 'Jl. Sutan Syahrir No. 5, Pontianak', '0812-1111-0004', 'Tidak', -0.0480, 109.3700],
['SPBU Jl. Kom. Yos Sudarso', 'Jl. Kom. Yos Sudarso No. 22, Pontianak', '0812-1111-0005', 'Ya', -0.0350, 109.3550],
['SPBU Jl. Tanjung Raya', 'Jl. Tanjung Raya No. 101, Pontianak', '0812-1111-0006', 'Tidak', -0.0580, 109.3300],
['SPBU Jl. Veteran', 'Jl. Veteran No. 33, Pontianak', '0812-1111-0007', 'Ya', -0.0450, 109.3380],
];
$stmt = $db->prepare(
"INSERT INTO data_spbu (nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?)"
);
foreach ($samples as $row) {
[$nama, $alamat, $wa, $jam, $lat, $lng] = $row;
$stmt->bind_param('ssssdd', $nama, $alamat, $wa, $jam, $lat, $lng);
$stmt->execute();
}
$stmt->close();
}
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
+38
View File
@@ -0,0 +1,38 @@
-- ============================================================
-- DATABASE: webgis_spbu
-- Pertemuan 3 Layer Groups and Layers Control
-- Data SPBU dengan 2 Layer Group: Buka 24 Jam & Tidak 24 Jam
-- ============================================================
CREATE DATABASE IF NOT EXISTS webgis_spbu
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE webgis_spbu;
-- buka_24_jam: 'Ya' → Layer Group "SPBU Buka 24 Jam"
-- 'Tidak' → Layer Group "SPBU Tidak Buka 24 Jam"
CREATE TABLE IF NOT EXISTS data_spbu (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_spbu VARCHAR(200) NOT NULL,
alamat VARCHAR(300) NOT NULL,
no_wa VARCHAR(20) NOT NULL,
buka_24_jam ENUM('Ya','Tidak') NOT NULL DEFAULT 'Tidak',
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Migrasi: tambah kolom alamat jika tabel lama dari PovertyMap belum punya
-- ALTER TABLE data_spbu ADD COLUMN alamat VARCHAR(300) NOT NULL DEFAULT '' AFTER nama_spbu;
-- UPDATE data_spbu SET alamat = CONCAT(nama_spbu, ', Pontianak') WHERE alamat = '';
INSERT IGNORE INTO data_spbu (nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude) VALUES
('SPBU Jl. Ahmad Yani', 'Jl. Ahmad Yani No. 12, Pontianak', '0812-1111-0001', 'Ya', -0.0400, 109.3200),
('SPBU Jl. Gajah Mada', 'Jl. Gajah Mada No. 45, Pontianak', '0812-1111-0002', 'Ya', -0.0520, 109.3450),
('SPBU Jl. Diponegoro', 'Jl. Diponegoro No. 88, Pontianak', '0812-1111-0003', 'Tidak', -0.0610, 109.3600),
('SPBU Jl. Sutan Syahrir', 'Jl. Sutan Syahrir No. 5, Pontianak', '0812-1111-0004', 'Tidak', -0.0480, 109.3700),
('SPBU Jl. Kom. Yos Sudarso', 'Jl. Kom. Yos Sudarso No. 22, Pontianak', '0812-1111-0005', 'Ya', -0.0350, 109.3550),
('SPBU Jl. Tanjung Raya', 'Jl. Tanjung Raya No. 101, Pontianak', '0812-1111-0006', 'Tidak', -0.0580, 109.3300),
('SPBU Jl. Veteran', 'Jl. Veteran No. 33, Pontianak', '0812-1111-0007', 'Ya', -0.0450, 109.3380);
+562
View File
@@ -0,0 +1,562 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="WebGIS SPBU Layer Groups & Layers Control">
<title>WebGIS SPBU Layer Groups</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header id="header-bar">
<div>
<h1>⛽ WebGIS SPBU Pontianak</h1>
<p>Layer Groups &amp; Layers Control Pertemuan 2</p>
</div>
<div id="stats-bar">
<span class="stat-badge green" id="count-24jam">🟢 24 Jam: 0</span>
<span class="stat-badge orange" id="count-tidak24jam">🔴 Tidak 24 Jam: 0</span>
</div>
</header>
<div id="map"></div>
<div class="fab-container">
<button class="fab-btn" id="fab-tambah" title="Tambah SPBU" onclick="aktifkanModeTambah()"></button>
</div>
<div id="toast"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script>
// Gunakan path relatif sederhana — lebih stabil dari URL constructor
const API_SPBU = 'api_spbu.php';
// ============================================================
// INISIALISASI PETA (mengikuti pola Leaflet Layers Control)
// https://leafletjs.com/examples/layers-control/
// ============================================================
const map = L.map('map').setView([-0.0553, 109.3495], 13);
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
});
const satelliteLayer = L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{ attribution: 'Tiles &copy; Esri', maxZoom: 19 }
);
osmLayer.addTo(map);
// ============================================================
// LAYER GROUPS 2 grup SPBU
// ============================================================
const layer24Jam = L.layerGroup();
const layerTidak24Jam = L.layerGroup();
// Kedua layer tampil secara default saat app dimuat
layer24Jam.addTo(map);
layerTidak24Jam.addTo(map);
// ============================================================
// BASE MAPS & OVERLAY MAPS
// ============================================================
const baseMaps = {
'🗺️ OpenStreetMap': osmLayer,
'🛰️ Citra Satelit': satelliteLayer
};
const overlayMaps = {
'🟢 SPBU Buka 24 Jam': layer24Jam,
'🔴 SPBU Tidak 24 Jam': layerTidak24Jam
};
L.control.layers(baseMaps, overlayMaps, {
collapsed: false,
position: 'topright'
}).addTo(map);
// ============================================================
// IKON MARKER warna berbeda per grup
// ============================================================
function ikonSpbu(buka24Jam) {
const is24Jam = buka24Jam === 'Ya';
const color = is24Jam ? '#10b981' : '#f97316';
const border = is24Jam ? '#065f46' : '#9a3412';
const emoji = '⛽';
return L.divIcon({
className: '',
html: `<div style="width:32px;height:32px;border-radius:50% 50% 50% 0;background:${color};transform:rotate(-45deg);border:3px solid ${border};box-shadow:0 3px 10px rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;">
<span style="transform:rotate(45deg);font-size:14px;line-height:1">${emoji}</span>
</div>`,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -34]
});
}
// ============================================================
// POPUP INFO
// ============================================================
function statusLabel(buka24Jam) {
return buka24Jam === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam';
}
function escHtml(str) {
return String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function popupInfoSpbu(d) {
const is24Jam = d.buka_24_jam === 'Ya';
const statusClass = is24Jam ? 'status-24jam' : 'status-tidak24jam';
const statusText = statusLabel(d.buka_24_jam);
const titleColor = is24Jam ? '#10b981' : '#f97316';
return `<div class="gis-popup">
<div class="popup-title" style="border-bottom-color:${titleColor}">⛽ ${escHtml(d.nama_spbu)}</div>
<div class="info-row">
<span class="info-label">Alamat</span>
<span class="info-value" style="font-size:11px">${escHtml(d.alamat)}</span>
</div>
<div class="info-row">
<span class="info-label">Status</span>
<span class="info-value"><span class="status-badge ${statusClass}">${statusText}</span></span>
</div>
<div class="info-row">
<span class="info-label">WhatsApp</span>
<span class="info-value">${escHtml(d.no_wa)}</span>
</div>
<div class="info-row">
<span class="info-label">Koordinat</span>
<span class="info-value" style="font-size:10px">${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</span>
</div>
<div class="popup-actions">
<button type="button" class="popup-btn popup-btn-edit btn-edit-spbu" data-id="${d.id}" onclick="event.stopPropagation(); bukaFormEditSpbu(${d.id}); return false;">✏️ Edit</button>
<button type="button" class="popup-btn popup-btn-delete btn-delete-spbu" data-id="${d.id}" onclick="event.stopPropagation(); konfirmasiHapusSpbu(${d.id}); return false;">🗑️ Hapus</button>
</div>
</div>`;
}
function formSpbuHtml({ title, prefix, lat, lng, d = {} }) {
const buka24 = d.buka_24_jam === 'Ya' ? 'Ya' : 'Tidak';
return `<div class="gis-popup">
<div class="popup-title">${title}</div>
<div class="popup-form">
<label>Nama SPBU</label>
<input type="text" id="${prefix}-nama" placeholder="Contoh: SPBU Jl. Ahmad Yani" value="${escHtml(d.nama_spbu || '')}">
<label>Alamat</label>
<input type="text" id="${prefix}-alamat" placeholder="Alamat lengkap" value="${escHtml(d.alamat || '')}">
<label>WhatsApp</label>
<input type="text" id="${prefix}-wa" placeholder="08xx-xxxx-xxxx" value="${escHtml(d.no_wa || '')}">
<label>Status Operasional</label>
<select id="${prefix}-24jam">
<option value="Ya"${buka24 === 'Ya' ? ' selected' : ''}>🟢 Buka 24 Jam</option>
<option value="Tidak"${buka24 === 'Tidak' ? ' selected' : ''}>🔴 Tidak Buka 24 Jam</option>
</select>
<label>Koordinat</label>
<input type="text" id="${prefix}-koord" readonly value="${lat.toFixed(6)}, ${lng.toFixed(6)}" style="font-size:10px">
</div>
<div class="popup-actions">
<button class="popup-btn popup-btn-save" onclick="${prefix === 'add' ? `simpanSpbu(${lat},${lng})` : `updateSpbu(${d.id})`}">💾 Simpan</button>
<button class="popup-btn popup-btn-cancel" onclick="map.closePopup()">✖ Batal</button>
</div>
</div>`;
}
// ============================================================
// RENDER MARKER KE LAYER GROUP YANG SESUAI
// ============================================================
const spbuMarkers = {};
const spbuDataCache = {};
let modeTambah = false;
let dbReady = false;
function getAllSpbuData() {
return Object.values(spbuDataCache);
}
function normalizeId(id) {
return Number(id);
}
function parseJsonResponse(text) {
const clean = (text || '').replace(/^\uFEFF/, '').trim();
if (!clean) return null;
try {
return JSON.parse(clean);
} catch (e) {
throw new Error('Respons server bukan JSON valid');
}
}
async function apiSend(method, id, body) {
const hasId = id !== null && id !== undefined && id !== '';
const baseUrl = hasId ? `${API_SPBU}?id=${normalizeId(id)}` : API_SPBU;
const fetchOpts = {
method,
headers: { 'Content-Type': 'application/json' }
};
if (body !== undefined) {
fetchOpts.body = JSON.stringify(body);
}
let res = await fetch(baseUrl, fetchOpts);
if ((method === 'DELETE' || method === 'PUT') && (res.status === 405 || res.status === 501)) {
res = await fetch(`${baseUrl}&_method=${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...(body || {}), _method: method, id: normalizeId(id) })
});
}
const data = parseJsonResponse(await res.text());
if (!res.ok) {
throw new Error((data && data.error) ? data.error : `Server error (${res.status})`);
}
return data;
}
async function apiGetAll() {
const res = await fetch(API_SPBU);
const data = parseJsonResponse(await res.text());
if (!res.ok) {
throw new Error((data && data.error) ? data.error : `Server error (${res.status})`);
}
if (!Array.isArray(data)) {
throw new Error('Data SPBU harus berupa array JSON');
}
return data;
}
function renderSpbu(d) {
const id = normalizeId(d.id);
d.id = id;
spbuDataCache[id] = d;
const targetLayer = d.buka_24_jam === 'Ya' ? layer24Jam : layerTidak24Jam;
if (spbuMarkers[id]) {
const oldMarker = spbuMarkers[id];
layer24Jam.removeLayer(oldMarker);
layerTidak24Jam.removeLayer(oldMarker);
}
const marker = L.marker([d.latitude, d.longitude], {
icon: ikonSpbu(d.buka_24_jam),
draggable: true
});
marker._data = d;
marker._targetLayer = targetLayer;
marker._spbuId = id;
marker.bindPopup(() => popupInfoSpbu(spbuDataCache[id] || d));
marker.on('click', () => marker.openPopup());
marker.on('dragend', function () {
const pos = marker.getLatLng();
const data = spbuDataCache[id];
if (!data) return;
data.latitude = pos.lat;
data.longitude = pos.lng;
spbuDataCache[id] = data;
if (!dbReady) {
showToast('📍 Posisi diperbarui (mode offline)', 'info');
marker.getPopup().setContent(popupInfoSpbu(data));
return;
}
apiSend('PUT', id, { latitude: pos.lat, longitude: pos.lng })
.then(() => {
showToast('📍 Posisi SPBU diperbarui', 'success');
marker.getPopup().setContent(popupInfoSpbu(data));
})
.catch(err => showToast('❌ ' + err.message, 'error'));
});
targetLayer.addLayer(marker);
spbuMarkers[id] = marker;
updateCounts(getAllSpbuData());
}
function removeSpbuFromMap(id) {
const numId = normalizeId(id);
const marker = spbuMarkers[numId];
if (marker) {
layer24Jam.removeLayer(marker);
layerTidak24Jam.removeLayer(marker);
marker.remove();
delete spbuMarkers[numId];
}
delete spbuDataCache[numId];
updateCounts(getAllSpbuData());
}
function updateCounts(data) {
const count24 = data.filter(d => d.buka_24_jam === 'Ya').length;
const countTidak = data.filter(d => d.buka_24_jam === 'Tidak').length;
document.getElementById('count-24jam').textContent = `🟢 24 Jam: ${count24}`;
document.getElementById('count-tidak24jam').textContent = `🔴 Tidak 24 Jam: ${countTidak}`;
}
function showToast(msg, type = 'info') {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = 'show' + (type ? ' ' + type : '');
setTimeout(() => { el.className = ''; }, 3000);
}
// ============================================================
// MODE TAMBAH SPBU (FAB + klik peta)
// ============================================================
function aktifkanModeTambah() {
modeTambah = !modeTambah;
const fab = document.getElementById('fab-tambah');
if (modeTambah) {
fab.classList.add('active');
fab.textContent = '✖';
showToast('📍 Klik lokasi di peta untuk menambah SPBU baru', 'info');
} else {
fab.classList.remove('active');
fab.textContent = '';
}
}
window.aktifkanModeTambah = aktifkanModeTambah;
map.on('click', function (e) {
if (!modeTambah) return;
modeTambah = false;
document.getElementById('fab-tambah').classList.remove('active');
document.getElementById('fab-tambah').textContent = '';
tampilkanFormTambahSpbu(e.latlng.lat, e.latlng.lng);
});
function tampilkanFormTambahSpbu(lat, lng) {
L.popup({ closeButton: true, maxWidth: 300 })
.setLatLng([lat, lng])
.setContent(formSpbuHtml({ title: ' Tambah SPBU Baru', prefix: 'add', lat, lng }))
.openOn(map);
}
function simpanSpbu(lat, lng) {
const nama = document.getElementById('add-nama').value.trim();
const alamat = document.getElementById('add-alamat').value.trim();
const no_wa = document.getElementById('add-wa').value.trim();
const buka_24_jam = document.getElementById('add-24jam').value;
if (!nama || !alamat || !no_wa) {
showToast('⚠️ Nama, alamat, dan WhatsApp wajib diisi!', 'error');
return;
}
if (!dbReady) {
showToast('❌ Database belum tersedia. Import database.sql terlebih dahulu.', 'error');
return;
}
apiSend('POST', null, { nama_spbu: nama, alamat, no_wa, buka_24_jam, latitude: lat, longitude: lng })
.then(res => {
map.closePopup();
renderSpbu({
id: normalizeId(res.id),
nama_spbu: res.nama_spbu,
alamat: res.alamat,
no_wa: res.no_wa,
buka_24_jam: res.buka_24_jam,
latitude: res.latitude,
longitude: res.longitude
});
showToast('✅ SPBU berhasil ditambahkan', 'success');
})
.catch(err => showToast('❌ ' + err.message, 'error'));
}
window.simpanSpbu = simpanSpbu;
function bukaFormEditSpbu(id) {
const d = spbuDataCache[normalizeId(id)];
if (!d) return;
map.closePopup();
L.popup({ closeButton: true, maxWidth: 300 })
.setLatLng([d.latitude, d.longitude])
.setContent(formSpbuHtml({
title: '✏️ Edit SPBU',
prefix: 'edit',
lat: d.latitude,
lng: d.longitude,
d
}))
.openOn(map);
}
window.bukaFormEditSpbu = bukaFormEditSpbu;
function updateSpbu(id) {
const numId = normalizeId(id);
const nama = document.getElementById('edit-nama').value.trim();
const alamat = document.getElementById('edit-alamat').value.trim();
const no_wa = document.getElementById('edit-wa').value.trim();
const buka_24_jam = document.getElementById('edit-24jam').value;
const old = spbuDataCache[numId];
if (!old) return;
if (!nama || !alamat || !no_wa) {
showToast('⚠️ Nama, alamat, dan WhatsApp wajib diisi!', 'error');
return;
}
const payload = {
nama_spbu: nama,
alamat,
no_wa,
buka_24_jam,
latitude: old.latitude,
longitude: old.longitude
};
if (!dbReady) {
map.closePopup();
renderSpbu({ ...payload, id: numId });
showToast('✅ Data diperbarui (mode offline)', 'success');
return;
}
apiSend('PUT', numId, payload)
.then(() => {
map.closePopup();
renderSpbu({ ...payload, id: numId });
showToast('✅ SPBU berhasil diperbarui', 'success');
})
.catch(err => showToast('❌ ' + err.message, 'error'));
}
window.updateSpbu = updateSpbu;
// Tampilkan konfirmasi inline di dalam popup (confirm() diblokir di Leaflet popup)
function konfirmasiHapusSpbu(id) {
const numId = normalizeId(id);
const d = spbuDataCache[numId];
if (!d) { showToast('❌ Data SPBU tidak ditemukan', 'error'); return; }
const nama = d.nama_spbu;
const konfirmHtml = `
<div class="gis-popup">
<div class="popup-title" style="color:#ef4444">🗑️ Hapus SPBU?</div>
<p style="margin:8px 0 16px;font-size:12px;color:#94a3b8">"${escHtml(nama)}" akan dihapus permanen.</p>
<div class="popup-actions">
<button class="popup-btn" onclick="map.closePopup()" style="background:rgba(255,255,255,0.08);color:#94a3b8">Batal</button>
<button class="popup-btn popup-btn-delete" onclick="hapusSpbu(${numId})">🗑️ Ya, Hapus</button>
</div>
</div>`;
const lyr = spbuMarkers[numId];
if (lyr) {
lyr.getPopup().setContent(konfirmHtml);
} else {
// fallback: langsung hapus tanpa konfirmasi popup
hapusSpbu(numId);
}
}
window.konfirmasiHapusSpbu = konfirmasiHapusSpbu;
async function hapusSpbu(id) {
const numId = normalizeId(id);
const d = spbuDataCache[numId];
if (!d) {
showToast('❌ Data SPBU tidak ditemukan', 'error');
return;
}
if (!dbReady) {
map.closePopup();
removeSpbuFromMap(numId);
showToast('🗑️ Data dihapus (mode offline)', 'success');
return;
}
try {
await apiSend('DELETE', numId);
map.closePopup();
removeSpbuFromMap(numId);
showToast('🗑️ SPBU berhasil dihapus', 'success');
} catch (err) {
showToast('❌ ' + err.message, 'error');
}
}
window.hapusSpbu = hapusSpbu;
// Data fallback jika database belum diimport
const SAMPLE_DATA = [
{ id: 1, nama_spbu: 'SPBU Jl. Ahmad Yani', alamat: 'Jl. Ahmad Yani No. 12, Pontianak', no_wa: '0812-1111-0001', buka_24_jam: 'Ya', latitude: -0.0400, longitude: 109.3200 },
{ id: 2, nama_spbu: 'SPBU Jl. Gajah Mada', alamat: 'Jl. Gajah Mada No. 45, Pontianak', no_wa: '0812-1111-0002', buka_24_jam: 'Ya', latitude: -0.0520, longitude: 109.3450 },
{ id: 3, nama_spbu: 'SPBU Jl. Diponegoro', alamat: 'Jl. Diponegoro No. 88, Pontianak', no_wa: '0812-1111-0003', buka_24_jam: 'Tidak', latitude: -0.0610, longitude: 109.3600 },
{ id: 4, nama_spbu: 'SPBU Jl. Sutan Syahrir', alamat: 'Jl. Sutan Syahrir No. 5, Pontianak', no_wa: '0812-1111-0004', buka_24_jam: 'Tidak', latitude: -0.0480, longitude: 109.3700 },
{ id: 5, nama_spbu: 'SPBU Jl. Kom. Yos Sudarso', alamat: 'Jl. Kom. Yos Sudarso No. 22, Pontianak', no_wa: '0812-1111-0005', buka_24_jam: 'Ya', latitude: -0.0350, longitude: 109.3550 }
];
function loadSpbuData(data, fitBounds = true) {
layer24Jam.clearLayers();
layerTidak24Jam.clearLayers();
Object.keys(spbuMarkers).forEach(k => delete spbuMarkers[k]);
Object.keys(spbuDataCache).forEach(k => delete spbuDataCache[k]);
data.forEach(d => renderSpbu(d));
if (fitBounds && data.length > 0) {
const bounds = L.latLngBounds(data.map(d => [d.latitude, d.longitude]));
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 14 });
}
}
// ============================================================
// MUAT DATA DARI API (dengan retry jika gagal)
// ============================================================
async function muatDataSpbu() {
const MAX_RETRY = 3;
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
try {
const data = await apiGetAll();
dbReady = true;
loadSpbuData(data);
showToast(data.length > 0
? `${data.length} SPBU dimuat dari database`
: '📭 Database kosong — klik untuk tambah SPBU', 'success');
return; // sukses, keluar
} catch (err) {
console.warn(`API SPBU gagal (percobaan ${attempt}/${MAX_RETRY}):`, err);
if (attempt < MAX_RETRY) {
await new Promise(r => setTimeout(r, 800 * attempt)); // tunggu sebentar
} else {
// Semua retry gagal — mode offline
dbReady = false;
loadSpbuData(SAMPLE_DATA);
const pesan = err.message || 'Tidak bisa terhubung ke server';
showToast(`⚠️ Mode offline: ${pesan}`, 'error');
console.error('API SPBU gagal setelah semua retry:', err);
}
}
}
}
// Tunggu page load penuh sebelum fetch ke API
if (document.readyState === 'complete') {
muatDataSpbu();
} else {
window.addEventListener('load', muatDataSpbu);
}
</script>
</body>
</html>
+293
View File
@@ -0,0 +1,293 @@
:root {
--bg-dark: #0f1117;
--border: #2a3248;
--accent-green: #10b981;
--accent-orange: #f97316;
--accent-blue: #3b82f6;
--accent-yellow: #f59e0b;
--accent-red: #ef4444;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--radius-sm: 6px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
#header-bar {
background: linear-gradient(135deg, #161b27 0%, #0f1117 100%);
border-bottom: 1px solid var(--border);
padding: 14px 20px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
}
#header-bar h1 {
font-size: 18px;
font-weight: 700;
}
#header-bar p {
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
}
#stats-bar {
display: flex;
gap: 12px;
font-size: 12px;
}
.stat-badge {
padding: 6px 12px;
border-radius: 20px;
font-weight: 600;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.04);
}
.stat-badge.green {
color: var(--accent-green);
border-color: rgba(16, 185, 129, 0.35);
}
.stat-badge.orange {
color: var(--accent-orange);
border-color: rgba(249, 115, 22, 0.35);
}
#map {
flex: 1;
width: 100%;
}
.leaflet-control-layers {
border-radius: 8px !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35) !important;
font-size: 13px;
}
.leaflet-control-layers-expanded {
padding: 10px 12px !important;
}
.gis-popup {
min-width: 220px;
font-family: 'Inter', system-ui, sans-serif;
}
.popup-title {
font-size: 14px;
font-weight: 700;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border);
}
.info-row {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
font-size: 12px;
}
.info-label {
color: var(--text-muted);
flex-shrink: 0;
}
.info-value {
text-align: right;
font-weight: 500;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
}
.status-24jam {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
}
.status-tidak24jam {
background: rgba(249, 115, 22, 0.15);
color: #f97316;
}
#toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(80px);
background: #1e2538;
color: var(--text-primary);
padding: 10px 20px;
border-radius: 8px;
border: 1px solid var(--border);
font-size: 13px;
z-index: 9999;
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
}
#toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
#toast.error {
border-color: rgba(239, 68, 68, 0.5);
color: #fca5a5;
}
#toast.success {
border-color: rgba(16, 185, 129, 0.5);
color: #6ee7b7;
}
#toast.info {
border-color: rgba(59, 130, 246, 0.5);
color: #93c5fd;
}
.fab-container {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 9999;
}
.fab-btn {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-green), #059669);
color: white;
border: none;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.fab-btn:hover {
transform: scale(1.05);
}
.fab-btn.active {
background: linear-gradient(135deg, var(--accent-orange), #ea580c);
box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.3);
}
.popup-form label {
display: block;
font-size: 10px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
margin-top: 10px;
}
.popup-form label:first-of-type {
margin-top: 0;
}
.popup-form input,
.popup-form select {
width: 100%;
padding: 8px 10px;
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 12px;
font-family: inherit;
outline: none;
}
.popup-form input:focus,
.popup-form select:focus {
border-color: var(--accent-blue);
}
.popup-actions {
display: flex;
gap: 6px;
margin-top: 12px;
}
.popup-btn {
flex: 1;
padding: 8px;
border: none;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: opacity 0.2s;
pointer-events: auto;
position: relative;
z-index: 10;
}
.popup-btn:hover {
opacity: 0.85;
}
.popup-btn-save {
background: var(--accent-green);
color: #fff;
}
.popup-btn-edit {
background: var(--accent-yellow);
color: #000;
}
.popup-btn-delete {
background: var(--accent-red);
color: #fff;
}
.popup-btn-cancel {
background: #334155;
color: #fff;
}
.leaflet-popup-content-wrapper {
border-radius: 10px;
}
.leaflet-popup-content {
margin: 12px 14px;
}