add files
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api.php – REST API untuk WebGIS SPBU Pontianak
|
||||
// Endpoint:
|
||||
// GET api.php → ambil semua SPBU
|
||||
// POST api.php → tambah SPBU baru
|
||||
// PUT api.php?id=N → update SPBU
|
||||
// DELETE api.php?id=N → hapus SPBU
|
||||
// ============================================================
|
||||
|
||||
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') { exit(0); }
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
// ── Ambil semua SPBU ─────────────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$conn = getConnection();
|
||||
$res = $conn->query(
|
||||
"SELECT id, nama_spbu, nomor_wa, status_24jam,
|
||||
latitude, longitude, created_at
|
||||
FROM spbu ORDER BY created_at DESC"
|
||||
);
|
||||
$data = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['latitude'] = (float) $row['latitude'];
|
||||
$row['longitude'] = (float) $row['longitude'];
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Tambah SPBU baru ─────────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_spbu'] ?? '');
|
||||
$wa = trim($body['nomor_wa'] ?? '');
|
||||
$status = in_array($body['status_24jam'] ?? '', ['yes','no'])
|
||||
? $body['status_24jam'] : 'no';
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
if (!$nama || !$wa || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO spbu (nama_spbu, nomor_wa, status_24jam, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('sssdd', $nama, $wa, $status, $lat, $lng);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'SPBU berhasil ditambahkan',
|
||||
'id' => $stmt->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Update SPBU ──────────────────────────────────────────────
|
||||
if ($method === 'PUT' && $id) {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_spbu'] ?? '');
|
||||
$wa = trim($body['nomor_wa'] ?? '');
|
||||
$status = in_array($body['status_24jam'] ?? '', ['yes','no'])
|
||||
? $body['status_24jam'] : 'no';
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
if (!$nama || !$wa || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"UPDATE spbu SET nama_spbu=?, nomor_wa=?, status_24jam=?,
|
||||
latitude=?, longitude=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('sssddi', $nama, $wa, $status, $lat, $lng, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil diupdate']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Hapus SPBU ───────────────────────────────────────────────
|
||||
if ($method === 'DELETE' && $id) {
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare("DELETE FROM spbu WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// config.php – Konfigurasi Koneksi Database
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'root');
|
||||
define('DB_PASS', getenv('DB_PASSWORD') ?: '');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'webgis_spbu');
|
||||
|
||||
function getConnection(): mysqli {
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
die(json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Koneksi database gagal: ' . $conn->connect_error
|
||||
]));
|
||||
}
|
||||
$conn->set_charset('utf8mb4');
|
||||
return $conn;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
-- ============================================================
|
||||
-- WebGIS SPBU Pontianak
|
||||
-- Database Structure
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_spbu
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_spbu;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_spbu VARCHAR(150) NOT NULL,
|
||||
nomor_wa VARCHAR(20) NOT NULL,
|
||||
status_24jam ENUM('yes','no') NOT NULL DEFAULT 'no',
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- Sample data – beberapa SPBU di Pontianak
|
||||
-- ============================================================
|
||||
INSERT INTO spbu (nama_spbu, nomor_wa, status_24jam, latitude, longitude) VALUES
|
||||
('SPBU Pertamina Jl. Ahmad Yani', '6281234567890', 'yes', -0.0262619, 109.3425923),
|
||||
('SPBU Shell Jl. Tanjungpura', '6281298765432', 'no', -0.0318750, 109.3319870),
|
||||
('SPBU Pertamina Jl. Gajahmada', '6281211223344', 'yes', -0.0393160, 109.3266190),
|
||||
('SPBU Pertamina Jl. Sultan Hamid II', '6281355667788', 'no', -0.0159870, 109.3571200);
|
||||
+976
@@ -0,0 +1,976 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>WebGIS SPBU — Kota Pontianak</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"/>
|
||||
|
||||
<style>
|
||||
/* ══════════════════════════════════════
|
||||
RESET & VARIABLES
|
||||
══════════════════════════════════════ */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--surface2: #21262d;
|
||||
--border: #30363d;
|
||||
--text: #e6edf3;
|
||||
--muted: #8b949e;
|
||||
--accent: #f97316;
|
||||
--accent-dk: #c2591a;
|
||||
--green: #3fb950;
|
||||
--red: #f85149;
|
||||
--radius: 12px;
|
||||
--shadow: 0 8px 32px rgba(0,0,0,.55);
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════
|
||||
LAYOUT
|
||||
══════════════════════════════════════ */
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
grid-template-rows: 60px 1fr;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Header ─────────────────────────── */
|
||||
header {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 24px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 10;
|
||||
}
|
||||
.logo-icon {
|
||||
width: 34px; height: 34px;
|
||||
background: var(--accent);
|
||||
border-radius: 8px;
|
||||
display: grid; place-items: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 15px; font-weight: 700;
|
||||
letter-spacing: -.2px;
|
||||
}
|
||||
header h1 span { color: var(--accent); }
|
||||
.header-meta {
|
||||
margin-left: auto;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
background: var(--surface2);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.dot-live {
|
||||
display: inline-block;
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
margin-right: 6px;
|
||||
animation: pulse 1.8s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%,100% { opacity: 1; }
|
||||
50% { opacity: .35; }
|
||||
}
|
||||
|
||||
/* ── Sidebar ─────────────────────────── */
|
||||
aside {
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 18px 20px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sidebar-header h2 {
|
||||
font-size: 13px; font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .8px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.count-badge {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 26px; font-weight: 700;
|
||||
color: var(--accent);
|
||||
line-height: 1;
|
||||
}
|
||||
.count-badge small { font-size: 12px; color: var(--muted); font-family: inherit; }
|
||||
|
||||
/* stats row */
|
||||
.stats-row {
|
||||
display: flex; gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.stat-pill {
|
||||
flex: 1; text-align: center;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 4px;
|
||||
font-size: 11px; color: var(--muted);
|
||||
}
|
||||
.stat-pill strong { display: block; font-size: 18px; font-weight: 700; margin-bottom: 2px; }
|
||||
.stat-pill.green strong { color: var(--green); }
|
||||
.stat-pill.red strong { color: var(--red); }
|
||||
|
||||
/* search */
|
||||
.search-wrap {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.search-wrap input {
|
||||
width: 100%;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px 8px 34px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color .2s;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238b949e' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 10px center;
|
||||
}
|
||||
.search-wrap input:focus { border-color: var(--accent); }
|
||||
|
||||
/* list */
|
||||
#spbu-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
#spbu-list::-webkit-scrollbar { width: 4px; }
|
||||
#spbu-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||
|
||||
.spbu-card {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 13px 15px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: border-color .2s, transform .15s;
|
||||
position: relative;
|
||||
}
|
||||
.spbu-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
.spbu-card.active { border-color: var(--accent); background: #1d2029; }
|
||||
|
||||
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
|
||||
.card-name { font-size: 13px; font-weight: 600; line-height: 1.35; }
|
||||
.badge {
|
||||
font-size: 10px; font-weight: 700; padding: 2px 8px;
|
||||
border-radius: 20px; white-space: nowrap; flex-shrink: 0;
|
||||
letter-spacing: .4px; text-transform: uppercase;
|
||||
}
|
||||
.badge.yes { background: rgba(63,185,80,.18); color: var(--green); border: 1px solid var(--green); }
|
||||
.badge.no { background: rgba(248,81,73,.15); color: var(--red); border: 1px solid var(--red); }
|
||||
|
||||
.card-wa {
|
||||
font-size: 11px; color: var(--muted); margin-top: 5px;
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
}
|
||||
.card-wa svg { flex-shrink: 0; }
|
||||
|
||||
.card-actions {
|
||||
display: flex; gap: 6px; margin-top: 10px;
|
||||
}
|
||||
.btn-sm {
|
||||
flex: 1; padding: 5px 0;
|
||||
font-size: 11px; font-weight: 600;
|
||||
border-radius: 6px; cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--muted);
|
||||
font-family: inherit;
|
||||
transition: all .18s;
|
||||
}
|
||||
.btn-sm:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
.btn-sm.danger:hover { background: var(--red); border-color: var(--red); }
|
||||
.btn-sm.move:hover { background: #f97316; border-color: #f97316; color: #fff; }
|
||||
|
||||
/* add-new CTA */
|
||||
.cta-box {
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.btn-add {
|
||||
width: 100%;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 11px;
|
||||
font-size: 13px; font-weight: 700;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background .2s, transform .15s;
|
||||
display: flex; align-items: center; justify-content: center; gap: 7px;
|
||||
}
|
||||
.btn-add:hover { background: var(--accent-dk); transform: translateY(-1px); }
|
||||
|
||||
/* ── Map ─────────────────────────────── */
|
||||
#map {
|
||||
width: 100%; height: 100%;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════
|
||||
MODAL
|
||||
══════════════════════════════════════ */
|
||||
.overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,.7);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: opacity .25s;
|
||||
}
|
||||
.overlay.open { opacity: 1; pointer-events: all; }
|
||||
|
||||
.modal {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 28px 28px 24px;
|
||||
width: 420px; max-width: 95vw;
|
||||
box-shadow: var(--shadow);
|
||||
transform: translateY(12px);
|
||||
transition: transform .25s;
|
||||
}
|
||||
.overlay.open .modal { transform: translateY(0); }
|
||||
|
||||
.modal h3 {
|
||||
font-size: 16px; font-weight: 700; margin-bottom: 20px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.modal h3 .icon {
|
||||
width: 32px; height: 32px;
|
||||
background: var(--accent);
|
||||
border-radius: 8px;
|
||||
display: grid; place-items: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label {
|
||||
display: block; font-size: 12px; font-weight: 600;
|
||||
color: var(--muted); margin-bottom: 6px;
|
||||
text-transform: uppercase; letter-spacing: .5px;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color .2s;
|
||||
appearance: none;
|
||||
}
|
||||
.form-group input:focus,
|
||||
.form-group select:focus { border-color: var(--accent); }
|
||||
|
||||
.coord-row {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex; gap: 10px; margin-top: 20px;
|
||||
}
|
||||
.btn-cancel {
|
||||
flex: 1; padding: 11px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--muted);
|
||||
font-size: 13px; font-weight: 600;
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
.btn-cancel:hover { border-color: var(--red); color: var(--red); }
|
||||
.btn-save {
|
||||
flex: 2; padding: 11px;
|
||||
background: var(--accent);
|
||||
border: none; border-radius: var(--radius);
|
||||
color: #fff;
|
||||
font-size: 13px; font-weight: 700;
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: background .2s;
|
||||
}
|
||||
.btn-save:hover { background: var(--accent-dk); }
|
||||
|
||||
/* coord hint */
|
||||
.coord-hint {
|
||||
font-size: 11px; color: var(--muted);
|
||||
background: var(--surface2);
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px; margin-bottom: 16px;
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
}
|
||||
|
||||
/* ── Floating bar pindah titik ───────── */
|
||||
#move-bar {
|
||||
position: absolute;
|
||||
bottom: 32px; left: 50%; transform: translateX(-50%) translateY(80px);
|
||||
z-index: 900;
|
||||
background: var(--surface);
|
||||
border: 1.5px solid var(--accent);
|
||||
border-radius: 40px;
|
||||
padding: 10px 16px 10px 20px;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,.6), 0 0 0 4px rgba(249,115,22,.12);
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: transform .3s cubic-bezier(.34,1.56,.64,1), opacity .25s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#move-bar.active {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1; pointer-events: all;
|
||||
}
|
||||
.move-bar-label {
|
||||
font-size: 12px; font-weight: 600; color: var(--accent);
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
}
|
||||
.move-bar-label .pulse-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--accent);
|
||||
animation: pulse 1.2s infinite;
|
||||
}
|
||||
.btn-confirm {
|
||||
padding: 6px 16px;
|
||||
background: var(--green); border: none;
|
||||
border-radius: 20px; color: #fff;
|
||||
font-size: 12px; font-weight: 700;
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.btn-confirm:hover { opacity: .85; }
|
||||
.btn-cancel-move {
|
||||
padding: 6px 14px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px; color: var(--muted);
|
||||
font-size: 12px; font-weight: 600;
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
.btn-cancel-move:hover { border-color: var(--red); color: var(--red); }
|
||||
|
||||
/* ── Toast ───────────────────────────── */
|
||||
#toast {
|
||||
position: fixed; bottom: 24px; right: 24px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 18px;
|
||||
font-size: 13px; font-weight: 500;
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 9999;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
transition: all .3s;
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
}
|
||||
#toast.show { transform: translateY(0); opacity: 1; }
|
||||
#toast.success { border-color: var(--green); }
|
||||
#toast.error { border-color: var(--red); }
|
||||
|
||||
/* ── Leaflet popup custom ────────────── */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--surface) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
.leaflet-popup-tip { background: var(--surface) !important; }
|
||||
.leaflet-popup-content { margin: 14px 16px !important; }
|
||||
.popup-title { font-weight: 700; font-size: 14px; margin-bottom: 6px; }
|
||||
.popup-wa { font-size: 12px; color: var(--muted); }
|
||||
.popup-status { margin-top: 8px; }
|
||||
|
||||
/* empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.empty-state .emoji { font-size: 36px; margin-bottom: 10px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app { grid-template-columns: 1fr; grid-template-rows: 60px 1fr 260px; }
|
||||
aside { border-right: none; border-bottom: 1px solid var(--border); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="app">
|
||||
|
||||
<!-- ═══════════ HEADER ═══════════ -->
|
||||
<header>
|
||||
<div class="logo-icon">⛽</div>
|
||||
<h1>WebGIS <span>SPBU</span> Pontianak</h1>
|
||||
<div class="header-meta">
|
||||
<span class="dot-live"></span>Live Map
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══════════ SIDEBAR ═══════════ -->
|
||||
<aside>
|
||||
<div class="sidebar-header">
|
||||
<h2>Total SPBU</h2>
|
||||
<div class="count-badge" id="total-count">0 <small>titik</small></div>
|
||||
</div>
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat-pill green">
|
||||
<strong id="stat-24">0</strong>
|
||||
24 Jam
|
||||
</div>
|
||||
<div class="stat-pill red">
|
||||
<strong id="stat-non24">0</strong>
|
||||
Non-24 Jam
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-wrap">
|
||||
<input type="text" id="search-input" placeholder="Cari nama SPBU…" oninput="filterList()"/>
|
||||
</div>
|
||||
|
||||
<div id="spbu-list">
|
||||
<div class="empty-state">
|
||||
<div class="emoji">🗺️</div>
|
||||
Memuat data…
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta-box">
|
||||
<button class="btn-add" onclick="openAddMode()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Klik Peta untuk Tambah SPBU
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ═══════════ MAP ═══════════ -->
|
||||
<div id="map" style="position:relative;">
|
||||
<!-- Floating bar konfirmasi pindah titik -->
|
||||
<div id="move-bar">
|
||||
<div class="move-bar-label">
|
||||
<span class="pulse-dot"></span>
|
||||
Geser pin ke lokasi baru
|
||||
</div>
|
||||
<button class="btn-confirm" onclick="confirmMove()">✅ Konfirmasi</button>
|
||||
<button class="btn-cancel-move" onclick="cancelMove()">✕ Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ MODAL FORM ═══════════ -->
|
||||
<div class="overlay" id="modal-overlay">
|
||||
<div class="modal">
|
||||
<h3>
|
||||
<span class="icon">⛽</span>
|
||||
<span id="modal-title">Tambah SPBU</span>
|
||||
</h3>
|
||||
|
||||
<div class="coord-hint" id="coord-hint">
|
||||
📍 Koordinat diambil dari titik yang Anda klik di peta
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="f-nama" placeholder="Contoh: SPBU Pertamina Jl. Ahmad Yani"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nomor WhatsApp</label>
|
||||
<input type="text" id="f-wa" placeholder="Contoh: 6281234567890"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Status 24 Jam</label>
|
||||
<select id="f-status">
|
||||
<option value="yes">✅ Ya – Buka 24 Jam</option>
|
||||
<option value="no">❌ Tidak – Jam Terbatas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="coord-row">
|
||||
<div class="form-group">
|
||||
<label>Latitude</label>
|
||||
<input type="number" id="f-lat" step="0.000001" placeholder="-0.0262619"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Longitude</label>
|
||||
<input type="number" id="f-lng" step="0.000001" placeholder="109.3425923"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||
<button class="btn-save" onclick="saveSpbu()">💾 Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast"></div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STATE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
let spbuData = [];
|
||||
let markers = {}; // id → marker
|
||||
let editId = null;
|
||||
let addMode = false;
|
||||
let tempMarker = null;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MAP INIT — center: Pontianak
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
const map = L.map('map', {
|
||||
center: [-0.0263, 109.3425],
|
||||
zoom: 13,
|
||||
zoomControl: false
|
||||
});
|
||||
|
||||
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||
|
||||
// Tile layer OpenStreetMap
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// ── Custom icons ──────────────────────────────────────────
|
||||
function makeIcon(is24) {
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="
|
||||
width:36px;height:36px;
|
||||
background:${is24 ? '#3fb950' : '#f97316'};
|
||||
border:3px solid #fff;
|
||||
border-radius:50% 50% 50% 0;
|
||||
transform:rotate(-45deg);
|
||||
box-shadow:0 3px 10px rgba(0,0,0,.5);
|
||||
"></div>`,
|
||||
iconSize: [36, 36],
|
||||
iconAnchor: [18, 36],
|
||||
popupAnchor: [0, -38]
|
||||
});
|
||||
}
|
||||
|
||||
const tempIcon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="
|
||||
width:34px;height:34px;
|
||||
background:#f97316;
|
||||
border:3px solid #fff;
|
||||
border-radius:50% 50% 50% 0;
|
||||
transform:rotate(-45deg);
|
||||
box-shadow:0 3px 14px rgba(249,115,22,.7);
|
||||
animation: blink .7s infinite alternate;
|
||||
"></div>
|
||||
<style>@keyframes blink{from{opacity:1}to{opacity:.4}}</style>`,
|
||||
iconSize: [34, 34], iconAnchor: [17, 34], popupAnchor: [0, -36]
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// API CALLS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
const API = 'api.php';
|
||||
|
||||
async function fetchAll() {
|
||||
try {
|
||||
const r = await fetch(API);
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
spbuData = j.data;
|
||||
renderList();
|
||||
renderMarkers();
|
||||
updateStats();
|
||||
}
|
||||
} catch(e) { showToast('Gagal memuat data. Pastikan server berjalan.', 'error'); }
|
||||
}
|
||||
|
||||
async function saveSpbu() {
|
||||
const nama = document.getElementById('f-nama').value.trim();
|
||||
const wa = document.getElementById('f-wa').value.trim();
|
||||
const status = document.getElementById('f-status').value;
|
||||
const lat = parseFloat(document.getElementById('f-lat').value);
|
||||
const lng = parseFloat(document.getElementById('f-lng').value);
|
||||
|
||||
if (!nama || !wa || isNaN(lat) || isNaN(lng)) {
|
||||
showToast('Harap isi semua field dengan benar!', 'error'); return;
|
||||
}
|
||||
|
||||
const payload = { nama_spbu: nama, nomor_wa: wa, status_24jam: status, latitude: lat, longitude: lng };
|
||||
|
||||
try {
|
||||
const r = await fetch(API + (editId ? `?id=${editId}` : ''), {
|
||||
method : editId ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body : JSON.stringify(payload)
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
showToast(editId ? 'SPBU berhasil diperbarui! ✏️' : 'SPBU berhasil ditambahkan! ⛽', 'success');
|
||||
closeModal();
|
||||
fetchAll();
|
||||
} else {
|
||||
showToast('Gagal menyimpan: ' + j.message, 'error');
|
||||
}
|
||||
} catch(e) { showToast('Error koneksi ke server.', 'error'); }
|
||||
}
|
||||
|
||||
async function deleteSpbu(id) {
|
||||
if (!confirm('Yakin hapus SPBU ini?')) return;
|
||||
try {
|
||||
const r = await fetch(`${API}?id=${id}`, { method: 'DELETE' });
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
showToast('SPBU berhasil dihapus! 🗑️', 'success');
|
||||
fetchAll();
|
||||
}
|
||||
} catch(e) { showToast('Gagal menghapus.', 'error'); }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// RENDER
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function renderMarkers() {
|
||||
// clear old
|
||||
Object.values(markers).forEach(m => map.removeLayer(m));
|
||||
markers = {};
|
||||
|
||||
spbuData.forEach(s => {
|
||||
const m = L.marker([s.latitude, s.longitude], { icon: makeIcon(s.status_24jam === 'yes') })
|
||||
.addTo(map)
|
||||
.bindPopup(popupHTML(s));
|
||||
m.on('click', () => highlightCard(s.id));
|
||||
markers[s.id] = m;
|
||||
});
|
||||
}
|
||||
|
||||
function popupHTML(s) {
|
||||
return `
|
||||
<div class="popup-title">⛽ ${escHtml(s.nama_spbu)}</div>
|
||||
<div class="popup-wa">
|
||||
📱 ${escHtml(s.nomor_wa)}
|
||||
</div>
|
||||
<div class="popup-status">
|
||||
<span class="badge ${s.status_24jam}">
|
||||
${s.status_24jam === 'yes' ? '✅ Buka 24 Jam' : '❌ Jam Terbatas'}
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">
|
||||
<button onclick="openEdit(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #30363d;background:#21262d;color:#e6edf3;font-family:inherit">✏️ Edit</button>
|
||||
<button onclick="startMovePin(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #f97316;background:rgba(249,115,22,.1);color:#f97316;font-family:inherit">📌 Pindahkan</button>
|
||||
<button onclick="deleteSpbu(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #f85149;background:rgba(248,81,73,.1);color:#f85149;font-family:inherit">🗑️ Hapus</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderList(filter = '') {
|
||||
const list = document.getElementById('spbu-list');
|
||||
const q = filter.toLowerCase();
|
||||
const filtered = spbuData.filter(s => s.nama_spbu.toLowerCase().includes(q));
|
||||
|
||||
if (!filtered.length) {
|
||||
list.innerHTML = `<div class="empty-state"><div class="emoji">${filter ? '🔍' : '📍'}</div>${filter ? 'Tidak ditemukan' : 'Belum ada data SPBU'}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = filtered.map(s => `
|
||||
<div class="spbu-card" id="card-${s.id}" onclick="flyTo(${s.id})">
|
||||
<div class="card-top">
|
||||
<div class="card-name">${escHtml(s.nama_spbu)}</div>
|
||||
<span class="badge ${s.status_24jam}">${s.status_24jam === 'yes' ? '24 Jam' : 'Non-24'}</span>
|
||||
</div>
|
||||
<div class="card-wa">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.8 19.79 19.79 0 01.01 1.18C0 .1.68-.15 1.91.08A2 2 0 013.9 2.05v3a2 2 0 01-1.45 1.93 16 16 0 006.39 6.39A2 2 0 0110.77 12h.1l.07.06"/></svg>
|
||||
${escHtml(s.nomor_wa)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn-sm" onclick="event.stopPropagation();openEdit(${s.id})">✏️ Edit</button>
|
||||
<button class="btn-sm move" onclick="event.stopPropagation();startMovePin(${s.id})">📌 Pindahkan</button>
|
||||
<button class="btn-sm danger" onclick="event.stopPropagation();deleteSpbu(${s.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const total = spbuData.length;
|
||||
const yes = spbuData.filter(s => s.status_24jam === 'yes').length;
|
||||
document.getElementById('total-count').innerHTML = `${total} <small>titik</small>`;
|
||||
document.getElementById('stat-24').textContent = yes;
|
||||
document.getElementById('stat-non24').textContent = total - yes;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MAP INTERACTIONS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
let addModeActive = false;
|
||||
|
||||
function openAddMode() {
|
||||
addModeActive = true;
|
||||
map.getContainer().style.cursor = 'crosshair';
|
||||
showToast('🗺️ Klik pada peta untuk menentukan lokasi SPBU', 'success');
|
||||
}
|
||||
|
||||
map.on('click', function(e) {
|
||||
if (!addModeActive) return;
|
||||
addModeActive = false;
|
||||
map.getContainer().style.cursor = '';
|
||||
|
||||
if (tempMarker) map.removeLayer(tempMarker);
|
||||
tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { icon: tempIcon }).addTo(map);
|
||||
|
||||
openModal(null, e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
|
||||
function flyTo(id) {
|
||||
const s = spbuData.find(x => x.id === id);
|
||||
if (!s) return;
|
||||
map.flyTo([s.latitude, s.longitude], 16, { duration: 1.2 });
|
||||
setTimeout(() => {
|
||||
if (markers[id]) markers[id].openPopup();
|
||||
}, 1300);
|
||||
highlightCard(id);
|
||||
}
|
||||
|
||||
function highlightCard(id) {
|
||||
document.querySelectorAll('.spbu-card').forEach(c => c.classList.remove('active'));
|
||||
const card = document.getElementById(`card-${id}`);
|
||||
if (card) {
|
||||
card.classList.add('active');
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MODAL
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function openModal(id = null, lat = '', lng = '') {
|
||||
editId = id;
|
||||
const overlay = document.getElementById('modal-overlay');
|
||||
document.getElementById('modal-title').textContent = id ? 'Edit SPBU' : 'Tambah SPBU Baru';
|
||||
|
||||
if (id) {
|
||||
const s = spbuData.find(x => x.id == id);
|
||||
if (!s) { showToast('Data SPBU tidak ditemukan!', 'error'); return; }
|
||||
document.getElementById('f-nama').value = s.nama_spbu;
|
||||
document.getElementById('f-wa').value = s.nomor_wa;
|
||||
document.getElementById('f-status').value = s.status_24jam;
|
||||
document.getElementById('f-lat').value = s.latitude;
|
||||
document.getElementById('f-lng').value = s.longitude;
|
||||
document.getElementById('coord-hint').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('f-nama').value = '';
|
||||
document.getElementById('f-wa').value = '';
|
||||
document.getElementById('f-status').value = 'no';
|
||||
document.getElementById('f-lat').value = lat;
|
||||
document.getElementById('f-lng').value = lng;
|
||||
document.getElementById('coord-hint').style.display = 'flex';
|
||||
}
|
||||
|
||||
overlay.classList.add('open');
|
||||
}
|
||||
|
||||
function openEdit(id) {
|
||||
id = parseInt(id);
|
||||
if (markers[id]) markers[id].closePopup();
|
||||
openModal(id);
|
||||
}
|
||||
|
||||
function closeModal(cancelled = true) {
|
||||
document.getElementById('modal-overlay').classList.remove('open');
|
||||
editId = null;
|
||||
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PINDAHKAN TITIK – drag mode tanpa modal
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
let _moveId = null;
|
||||
let _moveOriginal = null;
|
||||
|
||||
function startMovePin(id) {
|
||||
id = parseInt(id);
|
||||
const s = spbuData.find(x => x.id == id);
|
||||
if (!s) return;
|
||||
|
||||
// Tutup popup jika terbuka
|
||||
if (markers[id]) markers[id].closePopup();
|
||||
|
||||
_moveId = id;
|
||||
_moveOriginal = { lat: s.latitude, lng: s.longitude };
|
||||
|
||||
const m = markers[id];
|
||||
m.dragging.enable();
|
||||
|
||||
// Ganti icon jadi warna biru/terang sebagai penanda aktif
|
||||
m.setIcon(L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="
|
||||
width:40px;height:40px;
|
||||
background:#3b82f6;
|
||||
border:3px solid #fff;
|
||||
border-radius:50% 50% 50% 0;
|
||||
transform:rotate(-45deg);
|
||||
box-shadow:0 0 0 6px rgba(59,130,246,.3),0 4px 14px rgba(0,0,0,.5);
|
||||
"></div>`,
|
||||
iconSize: [40,40], iconAnchor: [20,40], popupAnchor: [0,-42]
|
||||
}));
|
||||
|
||||
// Zoom ke marker agar mudah drag
|
||||
map.flyTo([s.latitude, s.longitude], 16, { duration: .8 });
|
||||
|
||||
// Tampilkan floating bar
|
||||
document.getElementById('move-bar').classList.add('active');
|
||||
|
||||
showToast('📌 Geser pin biru ke lokasi baru lalu klik Konfirmasi', 'success');
|
||||
}
|
||||
|
||||
async function confirmMove() {
|
||||
if (!_moveId) return;
|
||||
const m = markers[_moveId];
|
||||
const ll = m.getLatLng();
|
||||
const s = spbuData.find(x => x.id == _moveId);
|
||||
|
||||
try {
|
||||
const r = await fetch(`api.php?id=${_moveId}`, {
|
||||
method : 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body : JSON.stringify({
|
||||
nama_spbu : s.nama_spbu,
|
||||
nomor_wa : s.nomor_wa,
|
||||
status_24jam: s.status_24jam,
|
||||
latitude : ll.lat,
|
||||
longitude : ll.lng
|
||||
})
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
showToast('📍 Lokasi SPBU berhasil dipindahkan!', 'success');
|
||||
_endMoveMode(false);
|
||||
fetchAll();
|
||||
} else {
|
||||
showToast('Gagal menyimpan: ' + j.message, 'error');
|
||||
}
|
||||
} catch(e) { showToast('Error koneksi ke server.', 'error'); }
|
||||
}
|
||||
|
||||
function cancelMove() {
|
||||
_endMoveMode(true);
|
||||
showToast('Pemindahan dibatalkan', 'error');
|
||||
}
|
||||
|
||||
function _endMoveMode(restore) {
|
||||
if (!_moveId) return;
|
||||
const m = markers[_moveId];
|
||||
m.dragging.disable();
|
||||
|
||||
// Kembalikan posisi & icon asli jika dibatalkan
|
||||
if (restore && _moveOriginal) {
|
||||
m.setLatLng([_moveOriginal.lat, _moveOriginal.lng]);
|
||||
}
|
||||
const s = spbuData.find(x => x.id == _moveId);
|
||||
if (s) m.setIcon(makeIcon(s.status_24jam === 'yes'));
|
||||
|
||||
document.getElementById('move-bar').classList.remove('active');
|
||||
_moveId = null;
|
||||
_moveOriginal = null;
|
||||
}
|
||||
|
||||
document.getElementById('modal-overlay').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SEARCH
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function filterList() {
|
||||
const q = document.getElementById('search-input').value;
|
||||
renderList(q);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// TOAST
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
let toastTimer;
|
||||
function showToast(msg, type = 'success') {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.className = `show ${type}`;
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { t.className = ''; }, 3200);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// UTILS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function escHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// INIT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Ekspos ke window agar bisa dipanggil dari dalam popup Leaflet
|
||||
window.openEdit = openEdit;
|
||||
window.deleteSpbu = deleteSpbu;
|
||||
window.startMovePin = startMovePin;
|
||||
|
||||
fetchAll();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user