feat: implement full CRUD functionality for jalan, parsil, and spbu modules including database integration and map interaction features

This commit is contained in:
Syariffullah
2026-06-11 16:40:58 +07:00
parent d10253a001
commit 0ae903c0d1
38 changed files with 3098 additions and 2578 deletions
-30
View File
@@ -1,30 +0,0 @@
// --- Fitur Geolocation ---
// Tambahkan tombol di dalam wadah zoom control
const geoBtn = document.createElement('button');
geoBtn.className = 'custom-layer-btn';
geoBtn.style.position = 'relative';
geoBtn.style.top = '0';
geoBtn.style.left = '0';
geoBtn.style.right = 'auto';
geoBtn.innerHTML = '<i class="fas fa-crosshairs fa-lg"></i>';
geoBtn.title = 'Lokasi Saya';
document.querySelector('.custom-zoom-control').appendChild(geoBtn);
let userMarker = null;
geoBtn.addEventListener('click', function() {
map.locate({setView: true, maxZoom: 16});
});
map.on('locationfound', function(e) {
if (userMarker) {
map.removeLayer(userMarker);
}
userMarker = L.marker(e.latlng).addTo(map)
.bindPopup("Anda berada di sini!").openPopup();
});
map.on('locationerror', function(e) {
alert("Gagal mendapatkan lokasi Anda.");
});
+466 -160
View File
@@ -1,17 +1,38 @@
<!DOCTYPE html>
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebGIS — Sistem Informasi Geografis | Portofolio Tugas</title>
<meta name="description" content="Portofolio proyek WebGIS mata kuliah Sistem Informasi Geografis. Kumpulan aplikasi pemetaan interaktif berbasis web menggunakan Leaflet.js, PHP, dan MySQL." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>WebGIS — Sistem Informasi Geografis | Portofolio Tugas</title>
<meta
name="description"
content="Portofolio proyek WebGIS mata kuliah Sistem Informasi Geografis. Kumpulan aplikasi pemetaan interaktif berbasis web menggunakan Leaflet.js, PHP, dan MySQL."
/>
<link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
/>
<style>
/* ===== CSS RESET & BASE ===== */
*, *::before, *::after {
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
@@ -31,17 +52,19 @@
--clr-white: #ffffff;
--grad-primary: linear-gradient(135deg, #4f46e5, #7c3aed);
--grad-card: linear-gradient(145deg, #ffffff, #f8fafc);
--shadow-glow: 0 0 40px rgba(79, 70, 229, 0.10);
--shadow-glow: 0 0 40px rgba(79, 70, 229, 0.1);
--shadow-card: 0 4px 24px rgba(15, 23, 42, 0.08);
--radius: 16px;
--radius-sm: 10px;
--transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
html { scroll-behavior: smooth; }
html {
scroll-behavior: smooth;
}
body {
font-family: 'Inter', sans-serif;
font-family: "Inter", sans-serif;
background-color: var(--clr-bg);
color: var(--clr-text);
line-height: 1.6;
@@ -49,15 +72,24 @@
}
/* ===== SCROLLBAR ===== */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--clr-bg); }
::-webkit-scrollbar-thumb { background: var(--clr-primary); border-radius: 3px; }
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--clr-bg);
}
::-webkit-scrollbar-thumb {
background: var(--clr-primary);
border-radius: 3px;
}
/* ===== CANVAS BG ===== */
#bg-canvas {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
opacity: 0.5;
@@ -75,7 +107,9 @@
/* ===== NAVBAR ===== */
.navbar {
position: fixed;
top: 0; left: 0; right: 0;
top: 0;
left: 0;
right: 0;
z-index: 100;
padding: 16px 0;
background: rgba(240, 244, 248, 0.75);
@@ -122,7 +156,7 @@
}
.nav-logo-text {
font-family: 'Space Grotesk', sans-serif;
font-family: "Space Grotesk", sans-serif;
font-weight: 700;
font-size: 1.1rem;
color: var(--clr-text);
@@ -162,13 +196,19 @@
}
.hero::before {
content: '';
content: "";
position: absolute;
top: 50%; left: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 900px;
height: 900px;
background: radial-gradient(circle, rgba(79,70,229,0.07) 0%, rgba(124,58,237,0.04) 40%, transparent 70%);
background: radial-gradient(
circle,
rgba(79, 70, 229, 0.07) 0%,
rgba(124, 58, 237, 0.04) 40%,
transparent 70%
);
pointer-events: none;
}
@@ -189,15 +229,23 @@
animation: fadeSlideDown 0.8s ease both;
}
.hero-badge i { font-size: 0.75rem; animation: pulse 2s infinite; }
.hero-badge i {
font-size: 0.75rem;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.hero-title {
font-family: 'Space Grotesk', sans-serif;
font-family: "Space Grotesk", sans-serif;
font-size: clamp(2.4rem, 6vw, 4.2rem);
font-weight: 800;
line-height: 1.1;
@@ -237,7 +285,7 @@
}
.hero-stat-num {
font-family: 'Space Grotesk', sans-serif;
font-family: "Space Grotesk", sans-serif;
font-size: 2rem;
font-weight: 700;
background: var(--grad-primary);
@@ -318,8 +366,14 @@
}
@keyframes scrollDown {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(10px); }
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(10px);
}
}
/* ===== SECTION ===== */
@@ -337,7 +391,7 @@
}
.section-title {
font-family: 'Space Grotesk', sans-serif;
font-family: "Space Grotesk", sans-serif;
font-size: clamp(1.8rem, 4vw, 2.6rem);
font-weight: 700;
color: var(--clr-text);
@@ -387,7 +441,9 @@
.project-card:hover {
transform: translateY(-6px);
border-color: rgba(79, 70, 229, 0.35);
box-shadow: 0 16px 48px rgba(15, 23, 42, 0.14), 0 0 0 1px rgba(79,70,229,0.1);
box-shadow:
0 16px 48px rgba(15, 23, 42, 0.14),
0 0 0 1px rgba(79, 70, 229, 0.1);
}
.project-card:hover .card-arrow {
@@ -420,7 +476,7 @@
}
.card-num {
font-family: 'Space Grotesk', sans-serif;
font-family: "Space Grotesk", sans-serif;
font-size: 0.75rem;
font-weight: 600;
color: var(--clr-muted);
@@ -449,7 +505,7 @@
}
.card-title {
font-family: 'Space Grotesk', sans-serif;
font-family: "Space Grotesk", sans-serif;
font-size: 1.25rem;
font-weight: 700;
color: var(--clr-text);
@@ -481,11 +537,31 @@
letter-spacing: 0.02em;
}
.tag-blue { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
.tag-purple { background: #f5f3ff; color: #6d28d9; border: 1px solid #ddd6fe; }
.tag-green { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
.tag-orange { background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; }
.tag-pink { background: #fdf2f8; color: #9d174d; border: 1px solid #fbcfe8; }
.tag-blue {
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
}
.tag-purple {
background: #f5f3ff;
color: #6d28d9;
border: 1px solid #ddd6fe;
}
.tag-green {
background: #ecfdf5;
color: #065f46;
border: 1px solid #a7f3d0;
}
.tag-orange {
background: #fff7ed;
color: #9a3412;
border: 1px solid #fed7aa;
}
.tag-pink {
background: #fdf2f8;
color: #9d174d;
border: 1px solid #fbcfe8;
}
.card-footer {
border-top: 1px solid var(--clr-border);
@@ -523,19 +599,34 @@
animation: statusPulse 2s infinite;
}
.status-dot.active { background: var(--clr-accent); box-shadow: 0 0 8px var(--clr-accent); }
.status-dot.pending { background: var(--clr-warn); box-shadow: 0 0 8px var(--clr-warn); }
.status-dot.inactive { background: var(--clr-muted); }
.status-dot.active {
background: var(--clr-accent);
box-shadow: 0 0 8px var(--clr-accent);
}
.status-dot.pending {
background: var(--clr-warn);
box-shadow: 0 0 8px var(--clr-warn);
}
.status-dot.inactive {
background: var(--clr-muted);
}
@keyframes statusPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
/* ===== CARD ACCENT LINES ===== */
.card-glow-line {
position: absolute;
top: 0; left: 0; right: 0;
top: 0;
left: 0;
right: 0;
height: 2px;
border-radius: var(--radius) var(--radius) 0 0;
}
@@ -600,7 +691,9 @@
transition: var(--transition);
}
.tech-item:hover { color: var(--clr-text); }
.tech-item:hover {
color: var(--clr-text);
}
.tech-item i {
font-size: 1.4rem;
@@ -618,7 +711,7 @@
}
.footer-logo {
font-family: 'Space Grotesk', sans-serif;
font-family: "Space Grotesk", sans-serif;
font-size: 1.4rem;
font-weight: 700;
color: var(--clr-text);
@@ -657,19 +750,33 @@
/* ===== ANIMATIONS ===== */
@keyframes fadeSlideDown {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.7s ease, transform 0.7s ease;
transition:
opacity 0.7s ease,
transform 0.7s ease;
}
.reveal.visible {
@@ -679,52 +786,112 @@
/* ===== RESPONSIVE ===== */
@media (max-width: 640px) {
.hero-stats { gap: 24px; }
.projects-grid { grid-template-columns: 1fr; }
.section-header { flex-direction: column; align-items: flex-start; }
.hero-stats {
gap: 24px;
}
.projects-grid {
grid-template-columns: 1fr;
}
.section-header {
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
<body>
<!-- ===== CANVAS BACKGROUND ===== -->
<canvas id="bg-canvas"></canvas>
<!-- ===== NAVBAR ===== -->
<nav class="navbar" id="navbar">
<nav
class="navbar"
id="navbar"
>
<div class="nav-inner">
<a href="#" class="nav-logo" id="logo-link">
<div class="nav-logo-icon"><i class="fa-solid fa-map-location-dot"></i></div>
<a
href="#"
class="nav-logo"
id="logo-link"
>
<div class="nav-logo-icon">
<i class="fa-solid fa-map-location-dot"></i>
</div>
<div class="nav-logo-text">
WebGIS
<span>Sistem Informasi Geografis</span>
</div>
</a>
<div class="nav-badge"><i class="fa-solid fa-graduation-cap"></i> &nbsp;Portofolio Tugas</div>
<div class="nav-badge">
<i class="fa-solid fa-graduation-cap"></i> &nbsp;Portofolio Tugas
</div>
</div>
</nav>
<!-- ===== HERO ===== -->
<section class="hero" id="home">
<section
class="hero"
id="home"
>
<div class="wrapper">
<div class="hero-badge" id="hero-badge">
<div
class="hero-badge"
id="hero-badge"
>
<i class="fa-solid fa-circle"></i>
Sistem Informasi Geografis
</div>
<h1 class="hero-title" id="hero-title">
<h1
class="hero-title"
id="hero-title"
>
Portofolio Proyek<br />
<span class="gradient-text">WebGIS Interaktif</span>
</h1>
<p class="hero-sub" id="hero-sub">
Kumpulan tugas pemetaan berbasis web menggunakan teknologi geospasial modern —
dari peta jalan, persil, hingga visualisasi data sosial.
<p
class="hero-sub"
id="hero-sub"
>
Kumpulan tugas pemetaan berbasis web menggunakan teknologi geospasial
modern — dari peta jalan, persil, hingga visualisasi data sosial.
</p>
<div class="hero-stats" id="hero-stats">
<div id="student-card" style="
display: inline-flex;
align-items: center;
gap: 14px;
background: rgba(79, 70, 229, 0.07);
border: 1px solid rgba(79, 70, 229, 0.2);
border-radius: 14px;
padding: 12px 22px;
margin-top: 8px;
margin-bottom: 4px;
">
<div style="width: 42px; height: 42px; border-radius: 50%; background: var(--grad-primary); display: flex; align-items: center; justify-content: center; font-size: 1.2rem;">👤</div>
<div style="text-align: left;">
<div style="font-weight: 700; font-size: 0.95rem; color: var(--clr-text);">Syariffullah</div>
<div style="font-size: 0.78rem; color: var(--clr-muted); font-family: 'Space Grotesk', monospace; letter-spacing: 0.03em;">NIM: D1041231018</div>
</div>
<div style="width: 1px; height: 32px; background: rgba(79,70,229,0.2);"></div>
<div style="font-size: 0.78rem; color: var(--clr-muted); text-align: left;">
<div><i class="fa-solid fa-graduation-cap" style="color: var(--clr-primary);"></i> Sistem Informasi Geografis</div>
<div style="margin-top:2px;"><i class="fa-solid fa-microchip" style="color: var(--clr-secondary);"></i> Teknik Informatika</div>
</div>
</div>
<div
class="hero-stats"
id="hero-stats"
>
<div class="hero-stat">
<div class="hero-stat-num" id="stat-projects">4</div>
<div
class="hero-stat-num"
id="stat-projects"
>
4
</div>
<div class="hero-stat-label">Proyek</div>
</div>
<div class="hero-divider"></div>
@@ -739,7 +906,11 @@
</div>
</div>
<a href="#projects" class="hero-cta" id="hero-cta">
<a
href="#projects"
class="hero-cta"
id="hero-cta"
>
<i class="fa-solid fa-layer-group"></i>
Lihat Semua Proyek
</a>
@@ -757,27 +928,45 @@
<p class="tech-strip-label">Teknologi yang Digunakan</p>
<div class="tech-items">
<div class="tech-item">
<i class="fa-solid fa-map" style="color:#4f46e5"></i>
<i
class="fa-solid fa-map"
style="color: #4f46e5"
></i>
Leaflet.js
</div>
<div class="tech-item">
<i class="fa-brands fa-php" style="color:#7c3aed"></i>
<i
class="fa-brands fa-php"
style="color: #7c3aed"
></i>
PHP
</div>
<div class="tech-item">
<i class="fa-solid fa-database" style="color:#059669"></i>
<i
class="fa-solid fa-database"
style="color: #059669"
></i>
MySQL
</div>
<div class="tech-item">
<i class="fa-brands fa-html5" style="color:#ea580c"></i>
<i
class="fa-brands fa-html5"
style="color: #ea580c"
></i>
HTML / CSS / JS
</div>
<div class="tech-item">
<i class="fa-solid fa-globe" style="color:#db2777"></i>
<i
class="fa-solid fa-globe"
style="color: #db2777"
></i>
GeoJSON
</div>
<div class="tech-item">
<i class="fa-solid fa-server" style="color:#b45309"></i>
<i
class="fa-solid fa-server"
style="color: #b45309"
></i>
XAMPP
</div>
</div>
@@ -785,32 +974,58 @@
</div>
<!-- ===== PROJECTS ===== -->
<section class="section" id="projects">
<section
class="section"
id="projects"
>
<div class="wrapper">
<div class="section-header reveal">
<div>
<p class="section-label"><i class="fa-solid fa-folder-open"></i> &nbsp;Daftar Proyek</p>
<p class="section-label">
<i class="fa-solid fa-folder-open"></i> &nbsp;Daftar Proyek
</p>
<h2 class="section-title">Semua Tugas WebGIS</h2>
<p class="section-desc">Klik pada kartu proyek untuk membuka aplikasi pemetaan interaktif.</p>
<p class="section-desc">
Klik pada kartu proyek untuk membuka aplikasi pemetaan interaktif.
</p>
</div>
</div>
<div class="projects-grid">
<!-- ===== PROJECT 1: POVERTY ===== -->
<a href="/webgis/poverty/index.php" class="project-card" id="card-poverty" style="animation-delay: 0.05s;">
<div class="card-glow-line" style="background: linear-gradient(90deg, #38bdf8, #818cf8);"></div>
<div class="card-arrow"><i class="fa-solid fa-arrow-up-right"></i></div>
<a
href="/webgis/poverty/index.php"
class="project-card"
id="card-poverty"
style="animation-delay: 0.05s"
>
<div
class="card-glow-line"
style="background: linear-gradient(90deg, #38bdf8, #818cf8)"
></div>
<div class="card-arrow">
<i class="fa-solid fa-arrow-up-right"></i>
</div>
<div class="card-header">
<div class="card-icon" style="background: rgba(56,189,248,0.12); border: 1px solid rgba(56,189,248,0.2);">
<div
class="card-icon"
style="
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.2);
"
>
🏠
</div>
<span class="card-num">01</span>
</div>
<div class="card-body">
<h3 class="card-title">Pemetaan Kemiskinan<br/>Kota Pontianak</h3>
<h3 class="card-title">
Pemetaan Kemiskinan<br />Kota Pontianak
</h3>
<p class="card-desc">
Aplikasi WebGIS untuk memetakan sebaran penduduk miskin, rumah ibadah, dan log bantuan sosial di Kota Pontianak. Dilengkapi manajemen data spasial berbasis peran.
Aplikasi WebGIS untuk memetakan sebaran penduduk miskin, rumah
ibadah, dan log bantuan sosial di Kota Pontianak. Dilengkapi
manajemen data spasial berbasis peran.
</p>
<div class="card-tags">
<span class="tag tag-blue">Leaflet.js</span>
@@ -826,16 +1041,35 @@
</span>
<div class="card-status">
<div class="status-dot active"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem;">Aktif</span>
<span style="color: var(--clr-muted); font-size: 0.75rem"
>Aktif</span
>
</div>
</div>
</a>
<!-- ===== PROJECT 2: JALAN ===== -->
<a href="/webgis/jalan/" class="project-card" id="card-jalan" style="animation-delay: 0.1s; cursor: default;" onclick="return false;">
<div class="card-glow-line" style="background: linear-gradient(90deg, #818cf8, #f472b6);"></div>
<a
href="/webgis/jalan/index.php"
class="project-card"
id="card-jalan"
style="animation-delay: 0.1s"
>
<div
class="card-glow-line"
style="background: linear-gradient(90deg, #818cf8, #f472b6)"
></div>
<div class="card-arrow">
<i class="fa-solid fa-arrow-up-right"></i>
</div>
<div class="card-header">
<div class="card-icon" style="background: rgba(129,140,248,0.12); border: 1px solid rgba(129,140,248,0.2);">
<div
class="card-icon"
style="
background: rgba(129, 140, 248, 0.12);
border: 1px solid rgba(129, 140, 248, 0.2);
"
>
🛣️
</div>
<span class="card-num">02</span>
@@ -843,7 +1077,9 @@
<div class="card-body">
<h3 class="card-title">Pemetaan Jaringan<br />Jalan</h3>
<p class="card-desc">
Visualisasi dan analisis jaringan jalan menggunakan data spasial. Mencakup klasifikasi jalan, kondisi, dan data atribut terkait infrastruktur jalan.
Visualisasi dan analisis jaringan jalan menggunakan data
spasial. Mencakup klasifikasi jalan, kondisi, dan data atribut
terkait infrastruktur jalan.
</p>
<div class="card-tags">
<span class="tag tag-purple">WebGIS</span>
@@ -852,24 +1088,40 @@
</div>
</div>
<div class="card-footer">
<span class="card-link-btn" style="color: var(--clr-muted);">
Segera Hadir <i class="fa-solid fa-clock"></i>
<span class="card-link-btn">
Buka Proyek <i class="fa-solid fa-arrow-right"></i>
</span>
<div class="card-status">
<div class="status-dot pending"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem;">Segera</span>
<div class="status-dot active"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem"
>Aktif</span
>
</div>
</div>
<div class="card-overlay">
<div class="coming-soon-badge">🚧 &nbsp;Coming Soon</div>
</div>
</a>
<!-- ===== PROJECT 3: PARSIL ===== -->
<a href="/webgis/parsil/" class="project-card" id="card-parsil" style="animation-delay: 0.15s; cursor: default;" onclick="return false;">
<div class="card-glow-line" style="background: linear-gradient(90deg, #34d399, #38bdf8);"></div>
<a
href="/webgis/parsil/index.php"
class="project-card"
id="card-parsil"
style="animation-delay: 0.15s"
>
<div
class="card-glow-line"
style="background: linear-gradient(90deg, #34d399, #38bdf8)"
></div>
<div class="card-arrow">
<i class="fa-solid fa-arrow-up-right"></i>
</div>
<div class="card-header">
<div class="card-icon" style="background: rgba(52,211,153,0.12); border: 1px solid rgba(52,211,153,0.2);">
<div
class="card-icon"
style="
background: rgba(52, 211, 153, 0.12);
border: 1px solid rgba(52, 211, 153, 0.2);
"
>
🗺️
</div>
<span class="card-num">03</span>
@@ -877,7 +1129,9 @@
<div class="card-body">
<h3 class="card-title">Pemetaan Bidang<br />Tanah (Parsil)</h3>
<p class="card-desc">
Sistem informasi geografis untuk pemetaan bidang tanah, kepemilikan lahan, dan informasi pertanahan berbasis web yang terintegrasi dengan data spasial.
Sistem informasi geografis untuk pemetaan bidang tanah,
kepemilikan lahan, dan informasi pertanahan berbasis web yang
terintegrasi dengan data spasial.
</p>
<div class="card-tags">
<span class="tag tag-green">Land Parcel</span>
@@ -886,24 +1140,40 @@
</div>
</div>
<div class="card-footer">
<span class="card-link-btn" style="color: var(--clr-muted);">
Segera Hadir <i class="fa-solid fa-clock"></i>
<span class="card-link-btn">
Buka Proyek <i class="fa-solid fa-arrow-right"></i>
</span>
<div class="card-status">
<div class="status-dot pending"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem;">Segera</span>
<div class="status-dot active"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem"
>Aktif</span
>
</div>
</div>
<div class="card-overlay">
<div class="coming-soon-badge">🚧 &nbsp;Coming Soon</div>
</div>
</a>
<!-- ===== PROJECT 4: SPBU ===== -->
<a href="/webgis/spbu/" class="project-card" id="card-spbu" style="animation-delay: 0.2s; cursor: default;" onclick="return false;">
<div class="card-glow-line" style="background: linear-gradient(90deg, #fb923c, #facc15);"></div>
<a
href="/webgis/spbu/index.php"
class="project-card"
id="card-spbu"
style="animation-delay: 0.2s"
>
<div
class="card-glow-line"
style="background: linear-gradient(90deg, #fb923c, #facc15)"
></div>
<div class="card-arrow">
<i class="fa-solid fa-arrow-up-right"></i>
</div>
<div class="card-header">
<div class="card-icon" style="background: rgba(251,146,60,0.12); border: 1px solid rgba(251,146,60,0.2);">
<div
class="card-icon"
style="
background: rgba(251, 146, 60, 0.12);
border: 1px solid rgba(251, 146, 60, 0.2);
"
>
</div>
<span class="card-num">04</span>
@@ -911,7 +1181,9 @@
<div class="card-body">
<h3 class="card-title">Pemetaan Lokasi<br />SPBU</h3>
<p class="card-desc">
Peta interaktif untuk menampilkan sebaran Stasiun Pengisian Bahan Bakar Umum (SPBU) beserta informasi detail, analisis jangkauan, dan fasilitas yang tersedia.
Peta interaktif untuk menampilkan sebaran Stasiun Pengisian
Bahan Bakar Umum (SPBU) beserta informasi detail, analisis
jangkauan, dan fasilitas yang tersedia.
</p>
<div class="card-tags">
<span class="tag tag-orange">POI Mapping</span>
@@ -920,19 +1192,17 @@
</div>
</div>
<div class="card-footer">
<span class="card-link-btn" style="color: var(--clr-muted);">
Segera Hadir <i class="fa-solid fa-clock"></i>
<span class="card-link-btn">
Buka Proyek <i class="fa-solid fa-arrow-right"></i>
</span>
<div class="card-status">
<div class="status-dot pending"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem;">Segera</span>
<div class="status-dot active"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem"
>Aktif</span
>
</div>
</div>
<div class="card-overlay">
<div class="coming-soon-badge">🚧 &nbsp;Coming Soon</div>
</div>
</a>
</div>
</div>
</section>
@@ -943,27 +1213,48 @@
<div class="footer-inner">
<div class="footer-logo">Web<span>GIS</span></div>
<div class="footer-course">
Mata Kuliah Sistem Informasi Geografis &nbsp;·&nbsp; Portofolio Tugas Mahasiswa
Mata Kuliah Sistem Informasi Geografis &nbsp;·&nbsp; Portofolio
Tugas Mahasiswa
</div>
<div style="display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap;">
<span style="font-size:0.8rem; color: var(--clr-muted);">
<i class="fa-solid fa-map-location-dot" style="color: var(--clr-primary);"></i>
<div
style="
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
"
>
<span style="font-size: 0.8rem; color: var(--clr-muted)">
<i
class="fa-solid fa-map-location-dot"
style="color: var(--clr-primary)"
></i>
&nbsp;Leaflet.js
</span>
<span style="color: var(--clr-border);">|</span>
<span style="font-size:0.8rem; color: var(--clr-muted);">
<i class="fa-solid fa-database" style="color: var(--clr-accent);"></i>
<span style="color: var(--clr-border)">|</span>
<span style="font-size: 0.8rem; color: var(--clr-muted)">
<i
class="fa-solid fa-database"
style="color: var(--clr-accent)"
></i>
&nbsp;MySQL Spatial
</span>
<span style="color: var(--clr-border);">|</span>
<span style="font-size:0.8rem; color: var(--clr-muted);">
<i class="fa-brands fa-php" style="color: var(--clr-secondary);"></i>
<span style="color: var(--clr-border)">|</span>
<span style="font-size: 0.8rem; color: var(--clr-muted)">
<i
class="fa-brands fa-php"
style="color: var(--clr-secondary)"
></i>
&nbsp;PHP / XAMPP
</span>
</div>
<div class="footer-divider"></div>
<p style="font-size: 0.9rem; color: var(--clr-text); font-weight: 600; margin-bottom: 6px;">
Syariffullah &nbsp;<span style="font-family: 'Space Grotesk', monospace; color: var(--clr-primary); font-size: 0.82rem; background: rgba(79,70,229,0.08); padding: 2px 8px; border-radius: 6px;">D1041231018</span>
</p>
<p class="footer-copy">
&copy; 2025 WebGIS Portfolio &nbsp;·&nbsp; Sistem Informasi Geografis
&copy; 2025 WebGIS Portfolio &nbsp;&middot;&nbsp; Sistem Informasi Geografis
</p>
</div>
</div>
@@ -973,8 +1264,8 @@
<script>
/* ----- CANVAS PARTICLE / GRID BG ----- */
(function () {
const canvas = document.getElementById('bg-canvas');
const ctx = canvas.getContext('2d');
const canvas = document.getElementById("bg-canvas");
const ctx = canvas.getContext("2d");
let w, h, animId;
const DOTS = [];
const LINES = [];
@@ -985,18 +1276,24 @@
h = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
window.addEventListener("resize", resize);
// Grid lines
function drawGrid() {
ctx.strokeStyle = 'rgba(79,70,229,0.06)';
ctx.strokeStyle = "rgba(79,70,229,0.06)";
ctx.lineWidth = 1;
const step = 60;
for (let x = 0; x < w; x += step) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
}
for (let y = 0; y < h; y += step) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
}
@@ -1008,7 +1305,7 @@
vx: (Math.random() - 0.5) * 0.3,
vy: (Math.random() - 0.5) * 0.3,
r: Math.random() * 2 + 0.5,
a: Math.random()
a: Math.random(),
});
}
@@ -1017,8 +1314,9 @@
drawGrid();
// Move and draw dots
DOTS.forEach(d => {
d.x += d.vx; d.y += d.vy;
DOTS.forEach((d) => {
d.x += d.vx;
d.y += d.vy;
if (d.x < 0) d.x = w;
if (d.x > w) d.x = 0;
if (d.y < 0) d.y = h;
@@ -1053,45 +1351,53 @@
})();
/* ----- NAVBAR SCROLL ----- */
const navbar = document.getElementById('navbar');
window.addEventListener('scroll', () => {
navbar.classList.toggle('scrolled', window.scrollY > 40);
const navbar = document.getElementById("navbar");
window.addEventListener("scroll", () => {
navbar.classList.toggle("scrolled", window.scrollY > 40);
});
/* ----- SCROLL REVEAL ----- */
const revealEls = document.querySelectorAll('.reveal');
const revealObs = new IntersectionObserver((entries) => {
entries.forEach(e => {
const revealEls = document.querySelectorAll(".reveal");
const revealObs = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('visible');
e.target.classList.add("visible");
revealObs.unobserve(e.target);
}
});
}, { threshold: 0.15 });
revealEls.forEach(el => revealObs.observe(el));
},
{ threshold: 0.15 },
);
revealEls.forEach((el) => revealObs.observe(el));
/* ----- CARD REVEAL ----- */
const cardEls = document.querySelectorAll('.project-card');
const cardObs = new IntersectionObserver((entries) => {
entries.forEach(e => {
const cardEls = document.querySelectorAll(".project-card");
const cardObs = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.style.opacity = '1';
e.target.style.transform = 'translateY(0)';
e.target.style.opacity = "1";
e.target.style.transform = "translateY(0)";
cardObs.unobserve(e.target);
}
});
}, { threshold: 0.1 });
cardEls.forEach(card => {
card.style.opacity = '0';
card.style.transform = 'translateY(30px)';
card.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
},
{ threshold: 0.1 },
);
cardEls.forEach((card) => {
card.style.opacity = "0";
card.style.transform = "translateY(30px)";
card.style.transition = "opacity 0.6s ease, transform 0.6s ease";
cardObs.observe(card);
});
/* ----- SMOOTH HERO CTA ----- */
document.getElementById('hero-cta').addEventListener('click', (e) => {
document.getElementById("hero-cta").addEventListener("click", (e) => {
e.preventDefault();
document.getElementById('projects').scrollIntoView({ behavior: 'smooth' });
document
.getElementById("projects")
.scrollIntoView({ behavior: "smooth" });
});
</script>
</body>
+155
View File
@@ -0,0 +1,155 @@
// --- Fitur Autentikasi dan Manajemen User (RBAC) ---
(function() {
window.currentUser = null;
// Elements
const authWidget = document.getElementById('authWidget');
const loginModal = document.getElementById('loginModal');
const loginUsernameInput = document.getElementById('loginUsername');
const loginPasswordInput = document.getElementById('loginPassword');
const loginSubmitBtn = document.getElementById('loginSubmitBtn');
const loginErrorMsg = document.getElementById('loginErrorMsg');
const closeLoginModal = document.getElementById('closeLoginModal');
// Startup Session Check
function checkSession() {
fetch('../poverty/api/check_session.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.isLoggedIn) {
window.currentUser = data.data;
} else {
window.currentUser = null;
}
updateAuthUI();
refreshAllLayers();
})
.catch(err => {
console.error("Session check failed:", err);
window.currentUser = null;
updateAuthUI();
});
}
// Update UI based on logged-in state
function updateAuthUI() {
if (!authWidget) return;
if (window.currentUser) {
// Logged In state
const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola';
authWidget.innerHTML = `
<div class="user-profile-pill">
<div class="user-profile-info">
<div class="user-profile-name">👤 ${escHtml(window.currentUser.username)}</div>
<div class="user-profile-role">${roleLabel}</div>
</div>
<button class="btn-profile-logout" id="authLogoutBtn" title="Logout">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
`;
document.getElementById('authLogoutBtn').addEventListener('click', logout);
} else {
// Logged Out state
authWidget.innerHTML = `
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
`;
document.getElementById('authLoginBtn').addEventListener('click', showLoginModal);
}
}
function refreshAllLayers() {
if (typeof loadJalan === 'function') loadJalan();
if (window.refreshActivePanel) window.refreshActivePanel();
}
// Login Modal logic
function showLoginModal() {
loginUsernameInput.value = '';
loginPasswordInput.value = '';
loginErrorMsg.style.display = 'none';
loginModal.classList.add('show');
}
function hideLoginModal() {
loginModal.classList.remove('show');
}
if (closeLoginModal) {
closeLoginModal.addEventListener('click', hideLoginModal);
}
if (loginSubmitBtn) {
loginSubmitBtn.addEventListener('click', function() {
const username = loginUsernameInput.value.trim();
const password = loginPasswordInput.value;
if (!username || !password) {
loginErrorMsg.textContent = 'Username dan password wajib diisi';
loginErrorMsg.style.display = 'block';
return;
}
fetch('../poverty/api/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
window.currentUser = data.data;
hideLoginModal();
updateAuthUI();
refreshAllLayers();
showToast('Login berhasil', 'success');
} else {
loginErrorMsg.textContent = data.message || 'Login gagal';
loginErrorMsg.style.display = 'block';
}
})
.catch(err => {
console.error(err);
loginErrorMsg.textContent = 'Koneksi ke server terputus';
loginErrorMsg.style.display = 'block';
});
});
}
function logout() {
fetch('../poverty/api/logout.php')
.then(res => res.json())
.then(data => {
window.currentUser = null;
updateAuthUI();
refreshAllLayers();
showToast('Logout berhasil', 'success');
})
.catch(err => {
console.error(err);
showToast('Gagal logout', 'error');
});
}
// Helper functions
function escHtml(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Toast Notification helper
function showToast(message, type = 'success') {
if (typeof window.showToast === 'function') {
window.showToast(message, type);
return;
}
alert(message);
}
// Run session check on initialization
checkSession();
})();
+1 -2
View File
@@ -29,12 +29,11 @@ const drawControl = new L.Control.Draw({
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
map.on(L.Draw.Event.DRAWSTART, function (e) {
isDrawingMode = true;
window.currentDrawMode = window.currentDrawMode || 'drawing';
});
map.on(L.Draw.Event.DRAWSTOP, function (e) {
setTimeout(() => {
isDrawingMode = false;
window.currentDrawMode = null;
window.activeDrawHandler = null;
window.deactivateAddMode();
+2 -3
View File
@@ -15,8 +15,6 @@ function makeSpbuIcon(is24) {
const spbuGreenIcon = makeSpbuIcon(true);
const spbuRedIcon = makeSpbuIcon(false);
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
// Fetch SPBU
function loadSpbu() {
@@ -75,7 +73,8 @@ function addSpbuMarker(item) {
// Map Click -> Form Add
map.on('click', function(e) {
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
// Jangan muncul form jika sedang dalam draw mode (jalan/parsil)
if (window.currentDrawMode) return;
// Hanya proses jika mode tambah SPBU aktif
if (window.currentAddMode === 'spbu') {
+179
View File
@@ -0,0 +1,179 @@
// Inisialisasi Peta Jalan
// Koordinat awal: [-0.0263, 109.3425] (Pontianak)
const map = L.map("map", { zoomControl: false }).setView(
[-0.0263, 109.3425],
13,
);
// ===== Toast Notification =====
window.showToast = function (message, type = "success", duration = 3500) {
const icons = { success: "✅", error: "❌", info: "️", warning: "⚠️" };
const container = document.getElementById("toastContainer");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.innerHTML = `<span class="toast-icon">${icons[type] || "️"}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add("hiding");
toast.addEventListener("animationend", () => toast.remove());
}, duration);
};
// Custom Zoom Control Logic
document.getElementById("zoomInBtn").addEventListener("click", function () {
map.zoomIn();
});
document.getElementById("zoomOutBtn").addEventListener("click", function () {
map.zoomOut();
});
// Base Map dari OpenStreetMap
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
// Inisialisasi FeatureGroups untuk masing-masing layer
const jalanLayer = L.featureGroup().addTo(map);
const searchInput = document.getElementById("searchInput");
const searchClear = document.getElementById("searchClear");
searchInput.addEventListener("focus", function () {
if (typeof window.deactivateAddMode === "function")
window.deactivateAddMode();
});
searchInput.addEventListener("input", function () {
const val = this.value.toLowerCase();
if (val.length > 0) {
searchClear.style.visibility = "visible";
window.showSearchResults(val);
} else {
searchClear.style.visibility = "hidden";
document.getElementById("searchResults").style.display = "none";
}
});
searchClear.addEventListener("click", function () {
searchInput.value = "";
searchClear.style.visibility = "hidden";
document.getElementById("searchResults").style.display = "none";
searchInput.focus();
});
// UI Logic: Custom Layer Control Toggle
const layerBtn = document.getElementById("layerBtn");
const layerPanel = document.getElementById("layerPanel");
if (layerBtn && layerPanel) {
layerBtn.addEventListener("click", function () {
if (typeof window.deactivateAddMode === "function")
window.deactivateAddMode();
layerPanel.style.display =
layerPanel.style.display === "block" ? "none" : "block";
});
}
// Sembunyikan layer panel saat klik di peta
map.on("click", function () {
if (layerPanel) layerPanel.style.display = "none";
});
// Logic untuk Toggle Layer Visibility
const setupLayerToggle = (id, layer) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener("change", function (e) {
e.target.checked ? map.addLayer(layer) : map.removeLayer(layer);
});
}
};
setupLayerToggle("layerJalan", jalanLayer);
// --- Sub-layer Toggle (expand/collapse) ---
window.toggleSubLayer = function (subId, iconEl) {
const sub = document.getElementById(subId);
if (!sub) return;
const isHidden = sub.style.display === "none";
sub.style.display = isHidden ? "" : "none";
iconEl.classList.toggle("collapsed", !isHidden);
};
// --- Sub-layer Filter ---
window.applySubFilter = function (type) {
if (type === "jalan") {
const checked = [...document.querySelectorAll(".sub-jalan:checked")].map(
(el) => el.value,
);
jalanLayer.eachLayer((layer) => {
if (!layer.jalanData) return;
const visible = checked.includes(layer.jalanData.status);
layer.setStyle({ opacity: visible ? 1 : 0, fillOpacity: visible ? 0.5 : 0 });
});
}
};
// --- Modal Logic ---
const unifiedModal = document.getElementById("unifiedModal");
const modalTitle = document.getElementById("modalTitle");
const modalBody = document.getElementById("modalBody");
window.openModal = function (title, bodyHTML, saveCallback) {
if (!unifiedModal) return;
modalTitle.textContent = title;
modalBody.innerHTML = bodyHTML;
unifiedModal.classList.add("show");
const currentSaveBtn = document.getElementById("modalSaveBtn");
if (currentSaveBtn) {
const newSaveBtn = currentSaveBtn.cloneNode(true);
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
newSaveBtn.addEventListener("click", saveCallback);
}
};
window.closeModal = function () {
if (unifiedModal) unifiedModal.classList.remove("show");
};
const confirmModal = document.getElementById("confirmModal");
const confirmMessage = document.getElementById("confirmMessage");
window.openConfirmModal = function (msg, confirmCallback) {
if (!confirmModal) return;
confirmMessage.textContent = msg;
confirmModal.classList.add("show");
const currentYesBtn = document.getElementById("confirmYesBtn");
if (currentYesBtn) {
const newYesBtn = currentYesBtn.cloneNode(true);
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
newYesBtn.addEventListener("click", function () {
confirmModal.classList.remove("show");
confirmCallback();
});
}
};
window.closeConfirmModal = function () {
if (confirmModal) confirmModal.classList.remove("show");
};
// --- Add Mode (untuk draw control) ---
window.currentDrawMode = null;
window.activeDrawHandler = null;
window.deactivateAddMode = function () {
if (window.activeDrawHandler) {
try { window.activeDrawHandler.disable(); } catch(e) {}
window.activeDrawHandler = null;
}
window.currentDrawMode = null;
document.getElementById("map").style.cursor = "";
};
+159
View File
@@ -0,0 +1,159 @@
// ===== Panel Management =====
// Mengelola Left Sidebar Toggle + Right Panel + Layer Control
(function () {
// ---- State ----
let activePanel = null;
// ---- Elements ----
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
const leftSidebar = document.getElementById('leftSidebar');
const rightPanel = document.getElementById('rightPanel');
const rightPanelTitle = document.getElementById('rightPanelTitle');
const rightPanelActions = document.getElementById('rightPanelActions');
const rightPanelBody = document.getElementById('rightPanelBody');
const rightPanelClose = document.getElementById('rightPanelClose');
// ---- Sidebar Toggle ----
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function () {
const isOpen = leftSidebar.style.display !== 'none';
if (isOpen) {
leftSidebar.style.display = 'none';
sidebarToggleBtn.classList.remove('open');
sidebarToggleIcon.className = 'fas fa-chevron-right';
} else {
leftSidebar.style.display = 'flex';
sidebarToggleBtn.classList.add('open');
sidebarToggleIcon.className = 'fas fa-chevron-left';
}
});
}
// ---- Sidebar Buttons ----
document.querySelectorAll('.sidebar-btn').forEach(btn => {
btn.addEventListener('click', function () {
const panel = this.dataset.panel;
if (activePanel === panel) {
closePanel();
} else {
openPanel(panel);
}
});
});
if (rightPanelClose) {
rightPanelClose.addEventListener('click', closePanel);
}
// ---- Open / Close Panel ----
function openPanel(panel) {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
activePanel = panel;
rightPanel.classList.add('open');
document.body.classList.add('right-panel-open');
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
const activeBtn = document.querySelector(`.sidebar-btn[data-panel="${panel}"]`);
if (activeBtn) activeBtn.classList.add('active');
renderPanel(panel);
}
function closePanel() {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
activePanel = null;
rightPanel.classList.remove('open');
document.body.classList.remove('right-panel-open');
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
}
window.refreshActivePanel = function () {
if (activePanel && activePanel !== 'layer') renderPanel(activePanel);
};
// ---- Render Panel by Type ----
function renderPanel(type) {
rightPanelBody.innerHTML = '<div class="panel-empty">Memuat data...</div>';
rightPanelActions.innerHTML = '';
switch (type) {
case 'jalan': renderJalanPanel(); break;
}
}
// ==========================
// ===== Jalan Panel ========
// ==========================
function renderJalanPanel() {
rightPanelTitle.textContent = '🛣️ Daftar Jalan';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
if (isAdmin) {
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddJalan">
<i class="fas fa-plus"></i> Tambah Jalan
</button>`;
document.getElementById('btnPanelAddJalan').addEventListener('click', () => {
window.activateDraw('polyline');
});
} else {
rightPanelActions.innerHTML = '';
}
const items = [];
jalanLayer.eachLayer(l => {
if (l.jalanData) items.push(l.jalanData);
});
if (!items.length) {
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data Jalan</div>';
return;
}
rightPanelBody.innerHTML = '';
items.forEach(d => {
const card = document.createElement('div');
card.className = 'data-card';
let actionButtons = '';
if (isAdmin) {
actionButtons = `
<div class="data-card-actions">
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
`;
}
card.innerHTML = `
<div class="data-card-main">
<div class="data-card-icon" style="background:#e0e7ff; color:#4f46e5; padding:8px; border-radius:8px; font-size:18px;">🛣️</div>
<div class="data-card-info">
<div class="data-card-name">${escHtml(d.nama)}</div>
<div class="data-card-sub">📏 ${d.panjang.toFixed(2)} m • 🏷️ ${escHtml(d.status)}</div>
</div>
${actionButtons}
</div>`;
if (isAdmin) {
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); window.openEditJalanModal(d.id); });
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); window.deleteJalan(d.id); });
}
card.addEventListener('click', () => {
jalanLayer.eachLayer(l => {
if (l.jalanData && l.jalanData.id == d.id) {
map.fitBounds(l.getBounds()); l.openPopup();
}
});
});
rightPanelBody.appendChild(card);
});
}
// ---- Utility ----
function escHtml(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ---- Expose ----
window.openRightPanel = openPanel;
})();
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+16
View File
@@ -0,0 +1,16 @@
-- =============================================
-- DATABASE: webgis_jalan
-- Tabel: jalan (Pemetaan Jaringan Jalan)
-- =============================================
CREATE DATABASE IF NOT EXISTS webgis_jalan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE webgis_jalan;
CREATE TABLE IF NOT EXISTS jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
status ENUM('Nasional', 'Provinsi', 'Kabupaten') NOT NULL DEFAULT 'Kabupaten',
panjang DOUBLE NOT NULL DEFAULT 0,
geom LINESTRING NOT NULL,
SPATIAL INDEX (geom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+1 -1
View File
@@ -2,7 +2,7 @@
$host = "localhost";
$user = "root";
$pass = "";
$db = "webgis";
$db = "webgis_jalan";
// Coba koneksi ke server dan pilih database
$conn = new mysqli($host, $user, $pass, $db);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+42 -187
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pemetaan Kemiskinan</title>
<title>WebGIS Pemetaan Jaringan Jalan</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -30,7 +30,7 @@
<div class="search-icon">
<i class="fas fa-search"></i>
</div>
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah...">
<input type="text" class="search-input" id="searchInput" placeholder="Cari jalan...">
<button class="search-clear" id="searchClear">
<i class="fas fa-times"></i>
</button>
@@ -51,39 +51,50 @@
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button>
<!-- Tombol Kembali ke Beranda -->
<a href="/webgis/index.html" class="btn-back-home" title="Kembali ke Beranda">
<i class="fas fa-arrow-left"></i>
<span>Beranda</span>
</a>
<style>
.btn-back-home {
position: fixed;
top: 16px;
right: 70px;
z-index: 1100;
display: flex;
align-items: center;
gap: 7px;
background: #ffffff;
color: #4f46e5;
border: 1.5px solid rgba(79, 70, 229, 0.25);
border-radius: 10px;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
font-family: 'Inter', sans-serif;
text-decoration: none;
box-shadow: 0 2px 12px rgba(79, 70, 229, 0.12);
transition: all 0.2s ease;
}
.btn-back-home:hover {
background: #4f46e5;
color: #ffffff;
border-color: #4f46e5;
box-shadow: 0 4px 16px rgba(79, 70, 229, 0.3);
transform: translateY(-1px);
}
.btn-back-home i { font-size: 12px; }
</style>
<!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
<div class="sidebar-section-label">Umum</div>
<button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
<span class="sidebar-emoji"></span>
<span class="sidebar-label">SPBU</span>
</button>
<div class="sidebar-section-label">Menu</div>
<button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
<span class="sidebar-emoji">🛣️</span>
<span class="sidebar-label">Jalan</span>
</button>
<button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
<span class="sidebar-emoji">🗺️</span>
<span class="sidebar-label">Parsil</span>
</button>
<div class="sidebar-divider"></div>
<div class="sidebar-section-label">Poverty</div>
<button class="sidebar-btn" id="menuIbadah" data-panel="ibadah" title="Rumah Ibadah">
<span class="sidebar-emoji">🕌</span>
<span class="sidebar-label">Ibadah</span>
</button>
<button class="sidebar-btn" id="menuMiskin" data-panel="miskin" title="Penduduk Miskin">
<span class="sidebar-emoji">🏠</span>
<span class="sidebar-label">Miskin</span>
</button>
<div class="sidebar-divider" id="sidebarAdminDivider" style="display:none;"></div>
<button class="sidebar-btn" id="menuUsers" style="display:none;" title="Manajemen Pengguna">
<span class="sidebar-emoji">👥</span>
<span class="sidebar-label">User</span>
</button>
</nav>
@@ -100,24 +111,6 @@
<div class="layer-section-title">Infrastruktur & Lahan</div>
<!-- SPBU -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerSpbu" checked> SPBU
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subSpbu', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subSpbu" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="1" checked onchange="applySubFilter('spbu')"> 24 Jam
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="0" checked onchange="applySubFilter('spbu')"> Tidak 24 Jam
</label>
</div>
</div>
<!-- Jalan -->
<div class="layer-group-item">
<div class="layer-group-header">
@@ -139,30 +132,6 @@
</div>
</div>
<!-- Parsil -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerParsil" checked> Parsil Tanah
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subParsil', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subParsil" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="SHM" checked onchange="applySubFilter('parsil')"> SHM
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGB" checked onchange="applySubFilter('parsil')"> HGB
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
</label>
</div>
</div>
<div class="layer-section-title">Pemetaan Kemiskinan</div>
<!-- Rumah Ibadah -->
@@ -255,55 +224,7 @@
</div>
</div>
<!-- Log Bantuan Modal -->
<div id="logBantuanModal" class="unified-modal">
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
<div class="modal-header">
<h3 id="logBantuanTitle">Log Bantuan</h3>
<span class="modal-close" onclick="closeLogBantuanModal()">&times;</span>
</div>
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
<!-- Riwayat -->
<div style="padding:15px; border-bottom:1px solid #eee;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
</div>
</div>
<!-- Form Tambah -->
<div style="padding:15px;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
<div class="form-group">
<label>Rumah Ibadah</label>
<select id="logIbadahId"></select>
</div>
<div class="form-group">
<label>Tipe Bantuan</label>
<select id="logTipeBantuan">
<option value="Sembako">Sembako</option>
<option value="Uang Tunai">Uang Tunai</option>
<option value="Pakaian">Pakaian</option>
<option value="Pendidikan">Pendidikan</option>
<option value="Kesehatan">Kesehatan</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="form-group">
<label>Tanggal</label>
<input type="date" id="logTanggal">
</div>
<div class="form-group">
<label>Keterangan (opsional)</label>
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
</div>
</div>
</div>
<!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget">
@@ -334,68 +255,7 @@
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="unified-modal">
<div class="modal-content" style="width: 700px; max-width: 95%;">
<div class="modal-header">
<h3>Manajemen Pengguna (Admin)</h3>
<span class="modal-close" id="closeUserManagementModal">&times;</span>
</div>
<div class="modal-body" style="gap: 15px;">
<!-- Form Tambah/Edit User -->
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
<input type="hidden" id="manageUserId" value="">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Username</label>
<input type="text" id="manageUsername" placeholder="Username">
</div>
<div class="form-group">
<label>Password (Kosongkan jika tidak diubah)</label>
<input type="password" id="managePassword" placeholder="Password">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Role</label>
<select id="manageRole">
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group" id="manageIbadahGroup">
<label>Rumah Ibadah yang Dikelola</label>
<select id="manageIbadahId">
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
</select>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
</div>
</div>
<!-- Tabel Daftar User -->
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
<th style="padding: 10px 12px;">Username</th>
<th style="padding: 10px 12px;">Role</th>
<th style="padding: 10px 12px;">Rumah Ibadah</th>
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- Diisi dinamis -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer"></div>
@@ -412,17 +272,12 @@
<!-- Main Map Initialization -->
<script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen -->
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script>
<script src="assets/js/features/jalan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/parsil.js?v=<?= time() ?>"></script>
<script src="assets/js/features/draw_control.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
<script src="assets/js/features/kemiskinan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
<!-- Core Auth Feature -->
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
</body>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
include_once 'db_connect.php';
$query = "SELECT id, nama, status, panjang, ST_AsGeoJSON(geom) as geom FROM jalan";
$result = $conn->query($query);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+1 -2
View File
@@ -29,12 +29,11 @@ const drawControl = new L.Control.Draw({
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
map.on(L.Draw.Event.DRAWSTART, function (e) {
isDrawingMode = true;
window.currentDrawMode = window.currentDrawMode || 'drawing';
});
map.on(L.Draw.Event.DRAWSTOP, function (e) {
setTimeout(() => {
isDrawingMode = false;
window.currentDrawMode = null;
window.activeDrawHandler = null;
window.deactivateAddMode();
+2 -3
View File
@@ -15,8 +15,6 @@ function makeSpbuIcon(is24) {
const spbuGreenIcon = makeSpbuIcon(true);
const spbuRedIcon = makeSpbuIcon(false);
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
// Fetch SPBU
function loadSpbu() {
@@ -75,7 +73,8 @@ function addSpbuMarker(item) {
// Map Click -> Form Add
map.on('click', function(e) {
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
// Jangan muncul form jika sedang dalam draw mode (jalan/parsil)
if (window.currentDrawMode) return;
// Hanya proses jika mode tambah SPBU aktif
if (window.currentAddMode === 'spbu') {
+128 -109
View File
@@ -1,246 +1,265 @@
// Inisialisasi Peta
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
const map = L.map("map", { zoomControl: false }).setView(
[-0.0263, 109.3425],
13,
);
// ===== Toast Notification =====
window.showToast = function(message, type = 'success', duration = 3500) {
const icons = { success: '✅', error: '❌', info: '️', warning: '⚠️' };
const container = document.getElementById('toastContainer');
window.showToast = function (message, type = "success", duration = 3500) {
const icons = { success: "✅", error: "❌", info: "️", warning: "⚠️" };
const container = document.getElementById("toastContainer");
if (!container) return;
const toast = document.createElement('div');
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.innerHTML = `<span class="toast-icon">${icons[type] || '️'}</span><span>${message}</span>`;
toast.innerHTML = `<span class="toast-icon">${icons[type] || "️"}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('hiding');
toast.addEventListener('animationend', () => toast.remove());
toast.classList.add("hiding");
toast.addEventListener("animationend", () => toast.remove());
}, duration);
};
// Custom Zoom Control Logic
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); });
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); });
document.getElementById("zoomInBtn").addEventListener("click", function () {
map.zoomIn();
});
document.getElementById("zoomOutBtn").addEventListener("click", function () {
map.zoomOut();
});
// Base Map dari OpenStreetMap
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
// Inisialisasi FeatureGroups untuk masing-masing layer
const spbuLayer = L.featureGroup().addTo(map);
const jalanLayer = L.featureGroup().addTo(map);
const parsilLayer = L.featureGroup().addTo(map);
const rumahIbadahLayer = L.featureGroup().addTo(map);
const pendudukMiskinLayer = L.featureGroup().addTo(map);
const geoJsonLayer = L.featureGroup().addTo(map);
const searchInput = document.getElementById('searchInput');
const searchClear = document.getElementById('searchClear');
const searchInput = document.getElementById("searchInput");
const searchClear = document.getElementById("searchClear");
if (searchInput) {
searchInput.addEventListener('focus', function() {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
searchInput.addEventListener("focus", function () {
if (typeof window.deactivateAddMode === "function")
window.deactivateAddMode();
});
searchInput.addEventListener('input', function() {
searchInput.addEventListener("input", function () {
const val = this.value.toLowerCase();
if (val.length > 0) {
searchClear.style.visibility = 'visible';
searchClear.style.visibility = "visible";
window.showSearchResults(val);
} else {
searchClear.style.visibility = 'hidden';
document.getElementById('searchResults').style.display = 'none';
searchClear.style.visibility = "hidden";
document.getElementById("searchResults").style.display = "none";
}
});
}
if (searchClear) {
searchClear.addEventListener('click', function() {
searchInput.value = '';
searchClear.style.visibility = 'hidden';
document.getElementById('searchResults').style.display = 'none';
searchClear.addEventListener("click", function () {
searchInput.value = "";
searchClear.style.visibility = "hidden";
document.getElementById("searchResults").style.display = "none";
searchInput.focus();
});
}
window.showSearchResults = function (query) {
const resultsContainer = document.getElementById('searchResults');
const resultsContainer = document.getElementById("searchResults");
if (!resultsContainer) return;
resultsContainer.innerHTML = '';
resultsContainer.innerHTML = "";
let results = [];
if (typeof spbuLayer !== 'undefined') {
spbuLayer.eachLayer(layer => {
if (layer.spbuData && layer.spbuData.nama && layer.spbuData.nama.toLowerCase().includes(query)) {
results.push({ type: 'SPBU', nama: layer.spbuData.nama, layer: layer });
}
if (typeof parsilLayer !== "undefined") {
parsilLayer.eachLayer((layer) => {
if (
layer.parsilData &&
layer.parsilData.nama &&
layer.parsilData.nama.toLowerCase().includes(query)
) {
results.push({
type: "Parsil",
nama: layer.parsilData.nama,
layer: layer,
});
}
if (typeof jalanLayer !== 'undefined') {
jalanLayer.eachLayer(layer => {
if (layer.jalanData && layer.jalanData.nama && layer.jalanData.nama.toLowerCase().includes(query)) {
results.push({ type: 'Jalan', nama: layer.jalanData.nama, layer: layer });
}
});
}
if (typeof parsilLayer !== 'undefined') {
parsilLayer.eachLayer(layer => {
if (layer.parsilData && layer.parsilData.nama && layer.parsilData.nama.toLowerCase().includes(query)) {
results.push({ type: 'Parsil', nama: layer.parsilData.nama, layer: layer });
}
});
}
if (results.length === 0) {
resultsContainer.innerHTML = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
resultsContainer.innerHTML =
'<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
} else {
results.forEach(res => {
const item = document.createElement('div');
item.style.padding = '10px 15px';
item.style.cursor = 'pointer';
item.style.borderBottom = '1px solid #eee';
item.style.fontSize = '14px';
results.forEach((res) => {
const item = document.createElement("div");
item.style.padding = "10px 15px";
item.style.cursor = "pointer";
item.style.borderBottom = "1px solid #eee";
item.style.fontSize = "14px";
item.innerHTML = `<strong>${res.type}:</strong> ${res.nama}`;
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f8f9fa');
item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white');
item.addEventListener('click', () => {
item.addEventListener(
"mouseenter",
() => (item.style.backgroundColor = "#f8f9fa"),
);
item.addEventListener(
"mouseleave",
() => (item.style.backgroundColor = "white"),
);
item.addEventListener("click", () => {
if (res.layer instanceof L.Marker) {
map.setView(res.layer.getLatLng(), 17);
} else if (res.layer.getBounds) {
map.fitBounds(res.layer.getBounds());
}
res.layer.openPopup();
resultsContainer.style.display = 'none';
resultsContainer.style.display = "none";
});
resultsContainer.appendChild(item);
});
}
resultsContainer.style.display = 'block';
resultsContainer.style.display = "block";
};
// UI Logic: Custom Layer Control Toggle
const layerBtn = document.getElementById('layerBtn');
const layerPanel = document.getElementById('layerPanel');
const layerBtn = document.getElementById("layerBtn");
const layerPanel = document.getElementById("layerPanel");
if (layerBtn && layerPanel) {
layerBtn.addEventListener('click', function() {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
layerPanel.style.display = layerPanel.style.display === 'block' ? 'none' : 'block';
layerBtn.addEventListener("click", function () {
if (typeof window.deactivateAddMode === "function")
window.deactivateAddMode();
layerPanel.style.display =
layerPanel.style.display === "block" ? "none" : "block";
});
}
// Sembunyikan layer panel saat klik di peta
map.on('click', function() {
if (layerPanel) layerPanel.style.display = 'none';
map.on("click", function () {
if (layerPanel) layerPanel.style.display = "none";
});
// Logic untuk Toggle Layer Visibility
const setupLayerToggle = (id, layer) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', function(e) {
el.addEventListener("change", function (e) {
e.target.checked ? map.addLayer(layer) : map.removeLayer(layer);
});
}
};
setupLayerToggle('layerSpbu', spbuLayer);
setupLayerToggle('layerJalan', jalanLayer);
setupLayerToggle('layerParsil', parsilLayer);
setupLayerToggle('layerRumahIbadah', rumahIbadahLayer);
setupLayerToggle('layerMiskin', pendudukMiskinLayer);
setupLayerToggle("layerParsil", parsilLayer);
// --- Sub-layer Toggle (expand/collapse) ---
window.toggleSubLayer = function (subId, iconEl) {
const sub = document.getElementById(subId);
if (!sub) return;
const isHidden = sub.style.display === 'none';
sub.style.display = isHidden ? '' : 'none';
iconEl.classList.toggle('collapsed', !isHidden);
const isHidden = sub.style.display === "none";
sub.style.display = isHidden ? "" : "none";
iconEl.classList.toggle("collapsed", !isHidden);
};
// --- Sub-layer Filter ---
window.applySubFilter = function (type) {
if (type === 'spbu') {
const checked = [...document.querySelectorAll('.sub-spbu:checked')].map(el => el.value === '1');
spbuLayer.eachLayer(layer => {
if (type === "spbu") {
const checked = [...document.querySelectorAll(".sub-spbu:checked")].map(
(el) => el.value === "1",
);
spbuLayer.eachLayer((layer) => {
if (layer.spbuData === undefined) return;
if (checked.includes(layer.spbuData.is_24_jam)) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "");
} else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "none");
}
});
} else if (type === 'jalan') {
const checked = [...document.querySelectorAll('.sub-jalan:checked')].map(el => el.value);
jalanLayer.eachLayer(layer => {
} else if (type === "jalan") {
const checked = [...document.querySelectorAll(".sub-jalan:checked")].map(
(el) => el.value,
);
jalanLayer.eachLayer((layer) => {
if (!layer.jalanData) return;
if (checked.includes(layer.jalanData.status)) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "");
} else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "none");
}
});
} else if (type === 'parsil') {
const checked = [...document.querySelectorAll('.sub-parsil:checked')].map(el => el.value);
parsilLayer.eachLayer(layer => {
} else if (type === "parsil") {
const checked = [...document.querySelectorAll(".sub-parsil:checked")].map(
(el) => el.value,
);
parsilLayer.eachLayer((layer) => {
if (!layer.parsilData) return;
if (checked.includes(layer.parsilData.status)) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "");
} else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "none");
}
});
}
};
// --- Modal Logic ---
const unifiedModal = document.getElementById('unifiedModal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const unifiedModal = document.getElementById("unifiedModal");
const modalTitle = document.getElementById("modalTitle");
const modalBody = document.getElementById("modalBody");
window.openModal = function (title, bodyHTML, saveCallback) {
if (!unifiedModal) return;
modalTitle.textContent = title;
modalBody.innerHTML = bodyHTML;
unifiedModal.classList.add('show');
unifiedModal.classList.add("show");
const currentSaveBtn = document.getElementById('modalSaveBtn');
const currentSaveBtn = document.getElementById("modalSaveBtn");
if (currentSaveBtn) {
const newSaveBtn = currentSaveBtn.cloneNode(true);
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
newSaveBtn.addEventListener('click', saveCallback);
newSaveBtn.addEventListener("click", saveCallback);
}
};
window.closeModal = function () {
if (unifiedModal) unifiedModal.classList.remove('show');
if (unifiedModal) unifiedModal.classList.remove("show");
};
const confirmModal = document.getElementById('confirmModal');
const confirmMessage = document.getElementById('confirmMessage');
const confirmModal = document.getElementById("confirmModal");
const confirmMessage = document.getElementById("confirmMessage");
window.openConfirmModal = function (msg, confirmCallback) {
if (!confirmModal) return;
confirmMessage.textContent = msg;
confirmModal.classList.add('show');
confirmModal.classList.add("show");
const currentYesBtn = document.getElementById('confirmYesBtn');
const currentYesBtn = document.getElementById("confirmYesBtn");
if (currentYesBtn) {
const newYesBtn = currentYesBtn.cloneNode(true);
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
newYesBtn.addEventListener('click', function() {
confirmModal.classList.remove('show');
newYesBtn.addEventListener("click", function () {
confirmModal.classList.remove("show");
confirmCallback();
});
}
};
window.closeConfirmModal = function () {
if (confirmModal) confirmModal.classList.remove('show');
if (confirmModal) confirmModal.classList.remove("show");
};
window.currentAddMode = null; // 'spbu', 'jalan', 'parsil'
@@ -250,10 +269,10 @@ window.activateAddMode = function(mode) {
return;
}
window.currentAddMode = mode;
document.getElementById('map').style.cursor = 'crosshair';
document.getElementById("map").style.cursor = "crosshair";
};
window.deactivateAddMode = function () {
window.currentAddMode = null;
document.getElementById('map').style.cursor = '';
document.getElementById("map").style.cursor = "";
};
+20
View File
@@ -153,7 +153,17 @@
rightPanelTitle.textContent = '🛣️ Daftar Jalan';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
if (isAdmin) {
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddJalan">
<i class="fas fa-plus"></i> Tambah Jalan
</button>`;
document.getElementById('btnPanelAddJalan').addEventListener('click', () => {
window.activateDraw('polyline');
});
} else {
rightPanelActions.innerHTML = '';
}
const items = [];
jalanLayer.eachLayer(l => {
@@ -209,7 +219,17 @@
rightPanelTitle.textContent = '🗺️ Daftar Parsil Tanah';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
if (isAdmin) {
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddParsil">
<i class="fas fa-plus"></i> Tambah Parsil
</button>`;
document.getElementById('btnPanelAddParsil').addEventListener('click', () => {
window.activateDraw('polygon');
});
} else {
rightPanelActions.innerHTML = '';
}
const items = [];
parsilLayer.eachLayer(l => {
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+16
View File
@@ -0,0 +1,16 @@
-- =============================================
-- DATABASE: webgis_parsil
-- Tabel: parsil (Bidang Tanah / Parsil)
-- =============================================
CREATE DATABASE IF NOT EXISTS webgis_parsil CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE webgis_parsil;
CREATE TABLE IF NOT EXISTS parsil (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL DEFAULT '',
status ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL DEFAULT 'SHM',
luas DOUBLE NOT NULL DEFAULT 0,
geom POLYGON NOT NULL,
SPATIAL INDEX (geom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+17
View File
@@ -0,0 +1,17 @@
<?php
$host = "localhost";
$user = "root";
$pass = "";
$db = "webgis_parsil";
// Coba koneksi ke server dan pilih database
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die("Koneksi gagal: " . $conn->connect_error);
}
// Set charset
$conn->set_charset("utf8mb4");
// Jika di-include oleh file API, biarkan $conn tersedia
?>
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+42 -167
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pemetaan Jaringan Jalan</title>
<title>WebGIS Pemetaan Bidang Tanah (Parsil)</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -30,7 +30,7 @@
<div class="search-icon">
<i class="fas fa-search"></i>
</div>
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah...">
<input type="text" class="search-input" id="searchInput" placeholder="Cari parsil...">
<button class="search-clear" id="searchClear">
<i class="fas fa-times"></i>
</button>
@@ -51,22 +51,50 @@
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button>
<!-- Tombol Kembali ke Beranda -->
<a href="/webgis/index.html" class="btn-back-home" title="Kembali ke Beranda">
<i class="fas fa-arrow-left"></i>
<span>Beranda</span>
</a>
<style>
.btn-back-home {
position: fixed;
top: 16px;
right: 70px;
z-index: 1100;
display: flex;
align-items: center;
gap: 7px;
background: #ffffff;
color: #4f46e5;
border: 1.5px solid rgba(79, 70, 229, 0.25);
border-radius: 10px;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
font-family: 'Inter', sans-serif;
text-decoration: none;
box-shadow: 0 2px 12px rgba(79, 70, 229, 0.12);
transition: all 0.2s ease;
}
.btn-back-home:hover {
background: #4f46e5;
color: #ffffff;
border-color: #4f46e5;
box-shadow: 0 4px 16px rgba(79, 70, 229, 0.3);
transform: translateY(-1px);
}
.btn-back-home i { font-size: 12px; }
</style>
<!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
<div class="sidebar-section-label">Umum</div>
<button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
<span class="sidebar-emoji"></span>
<span class="sidebar-label">SPBU</span>
</button>
<button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
<span class="sidebar-emoji">🛣️</span>
<span class="sidebar-label">Jalan</span>
</button>
<div class="sidebar-section-label">Menu</div>
<button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
<span class="sidebar-emoji">🗺️</span>
<span class="sidebar-label">Parsil</span>
</button>
</nav>
@@ -83,45 +111,6 @@
<div class="layer-section-title">Infrastruktur & Lahan</div>
<!-- SPBU -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerSpbu" checked> SPBU
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subSpbu', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subSpbu" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="1" checked onchange="applySubFilter('spbu')"> 24 Jam
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="0" checked onchange="applySubFilter('spbu')"> Tidak 24 Jam
</label>
</div>
</div>
<!-- Jalan -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerJalan" checked> Jalan
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subJalan', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subJalan" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Nasional" checked onchange="applySubFilter('jalan')"> Nasional
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Provinsi" checked onchange="applySubFilter('jalan')"> Provinsi
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Kabupaten" checked onchange="applySubFilter('jalan')"> Kabupaten
</label>
</div>
</div>
<!-- Parsil -->
<div class="layer-group-item">
<div class="layer-group-header">
@@ -215,55 +204,7 @@
</div>
</div>
<!-- Log Bantuan Modal -->
<div id="logBantuanModal" class="unified-modal">
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
<div class="modal-header">
<h3 id="logBantuanTitle">Log Bantuan</h3>
<span class="modal-close" onclick="closeLogBantuanModal()">&times;</span>
</div>
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
<!-- Riwayat -->
<div style="padding:15px; border-bottom:1px solid #eee;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
</div>
</div>
<!-- Form Tambah -->
<div style="padding:15px;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
<div class="form-group">
<label>Rumah Ibadah</label>
<select id="logIbadahId"></select>
</div>
<div class="form-group">
<label>Tipe Bantuan</label>
<select id="logTipeBantuan">
<option value="Sembako">Sembako</option>
<option value="Uang Tunai">Uang Tunai</option>
<option value="Pakaian">Pakaian</option>
<option value="Pendidikan">Pendidikan</option>
<option value="Kesehatan">Kesehatan</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="form-group">
<label>Tanggal</label>
<input type="date" id="logTanggal">
</div>
<div class="form-group">
<label>Keterangan (opsional)</label>
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
</div>
</div>
</div>
<!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget">
@@ -294,68 +235,7 @@
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="unified-modal">
<div class="modal-content" style="width: 700px; max-width: 95%;">
<div class="modal-header">
<h3>Manajemen Pengguna (Admin)</h3>
<span class="modal-close" id="closeUserManagementModal">&times;</span>
</div>
<div class="modal-body" style="gap: 15px;">
<!-- Form Tambah/Edit User -->
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
<input type="hidden" id="manageUserId" value="">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Username</label>
<input type="text" id="manageUsername" placeholder="Username">
</div>
<div class="form-group">
<label>Password (Kosongkan jika tidak diubah)</label>
<input type="password" id="managePassword" placeholder="Password">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Role</label>
<select id="manageRole">
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group" id="manageIbadahGroup">
<label>Rumah Ibadah yang Dikelola</label>
<select id="manageIbadahId">
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
</select>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
</div>
</div>
<!-- Tabel Daftar User -->
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
<th style="padding: 10px 12px;">Username</th>
<th style="padding: 10px 12px;">Role</th>
<th style="padding: 10px 12px;">Rumah Ibadah</th>
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- Diisi dinamis -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer"></div>
@@ -372,17 +252,12 @@
<!-- Main Map Initialization -->
<script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen -->
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script>
<script src="assets/js/features/jalan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/parsil.js?v=<?= time() ?>"></script>
<script src="assets/js/features/draw_control.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
<!-- Core Auth Feature -->
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
</body>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
include_once 'db_connect.php';
$query = "SELECT id, nama, status, luas, ST_AsGeoJSON(geom) as geom FROM parsil";
$result = $conn->query($query);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+1 -1
Submodule poverty updated: 3aa1182a09...227a7091b5
+1 -2
View File
@@ -29,12 +29,11 @@ const drawControl = new L.Control.Draw({
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
map.on(L.Draw.Event.DRAWSTART, function (e) {
isDrawingMode = true;
window.currentDrawMode = window.currentDrawMode || 'drawing';
});
map.on(L.Draw.Event.DRAWSTOP, function (e) {
setTimeout(() => {
isDrawingMode = false;
window.currentDrawMode = null;
window.activeDrawHandler = null;
window.deactivateAddMode();
+2 -3
View File
@@ -15,8 +15,6 @@ function makeSpbuIcon(is24) {
const spbuGreenIcon = makeSpbuIcon(true);
const spbuRedIcon = makeSpbuIcon(false);
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
// Fetch SPBU
function loadSpbu() {
@@ -75,7 +73,8 @@ function addSpbuMarker(item) {
// Map Click -> Form Add
map.on('click', function(e) {
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
// Jangan muncul form jika sedang dalam draw mode (jalan/parsil)
if (window.currentDrawMode) return;
// Hanya proses jika mode tambah SPBU aktif
if (window.currentAddMode === 'spbu') {
+11 -46
View File
@@ -29,11 +29,6 @@ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
// Inisialisasi FeatureGroups untuk masing-masing layer
const spbuLayer = L.featureGroup().addTo(map);
const jalanLayer = L.featureGroup().addTo(map);
const parsilLayer = L.featureGroup().addTo(map);
const rumahIbadahLayer = L.featureGroup().addTo(map);
const pendudukMiskinLayer = L.featureGroup().addTo(map);
const geoJsonLayer = L.featureGroup().addTo(map);
const searchInput = document.getElementById('searchInput');
const searchClear = document.getElementById('searchClear');
@@ -78,22 +73,6 @@ window.showSearchResults = function(query) {
});
}
if (typeof jalanLayer !== 'undefined') {
jalanLayer.eachLayer(layer => {
if (layer.jalanData && layer.jalanData.nama && layer.jalanData.nama.toLowerCase().includes(query)) {
results.push({ type: 'Jalan', nama: layer.jalanData.nama, layer: layer });
}
});
}
if (typeof parsilLayer !== 'undefined') {
parsilLayer.eachLayer(layer => {
if (layer.parsilData && layer.parsilData.nama && layer.parsilData.nama.toLowerCase().includes(query)) {
results.push({ type: 'Parsil', nama: layer.parsilData.nama, layer: layer });
}
});
}
if (results.length === 0) {
resultsContainer.innerHTML = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
} else {
@@ -148,10 +127,6 @@ const setupLayerToggle = (id, layer) => {
};
setupLayerToggle('layerSpbu', spbuLayer);
setupLayerToggle('layerJalan', jalanLayer);
setupLayerToggle('layerParsil', parsilLayer);
setupLayerToggle('layerRumahIbadah', rumahIbadahLayer);
setupLayerToggle('layerMiskin', pendudukMiskinLayer);
// --- Sub-layer Toggle (expand/collapse) ---
window.toggleSubLayer = function(subId, iconEl) {
@@ -174,26 +149,6 @@ window.applySubFilter = function(type) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
}
});
} else if (type === 'jalan') {
const checked = [...document.querySelectorAll('.sub-jalan:checked')].map(el => el.value);
jalanLayer.eachLayer(layer => {
if (!layer.jalanData) return;
if (checked.includes(layer.jalanData.status)) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
} else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
}
});
} else if (type === 'parsil') {
const checked = [...document.querySelectorAll('.sub-parsil:checked')].map(el => el.value);
parsilLayer.eachLayer(layer => {
if (!layer.parsilData) return;
if (checked.includes(layer.parsilData.status)) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
} else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
}
});
}
};
@@ -243,7 +198,11 @@ window.closeConfirmModal = function() {
if (confirmModal) confirmModal.classList.remove('show');
};
window.currentAddMode = null; // 'spbu', 'jalan', 'parsil'
// --- Add Mode (untuk SPBU klik peta) ---
window.currentAddMode = null;
window.currentDrawMode = null;
window.activeDrawHandler = null;
window.activateAddMode = function(mode) {
if (window.currentAddMode === mode) {
window.deactivateAddMode();
@@ -254,6 +213,12 @@ window.activateAddMode = function(mode) {
};
window.deactivateAddMode = function() {
if (window.activeDrawHandler) {
try { window.activeDrawHandler.disable(); } catch(e) {}
window.activeDrawHandler = null;
}
window.currentAddMode = null;
window.currentDrawMode = null;
document.getElementById('map').style.cursor = '';
};
+20
View File
@@ -153,7 +153,17 @@
rightPanelTitle.textContent = '🛣️ Daftar Jalan';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
if (isAdmin) {
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddJalan">
<i class="fas fa-plus"></i> Tambah Jalan
</button>`;
document.getElementById('btnPanelAddJalan').addEventListener('click', () => {
window.activateDraw('polyline');
});
} else {
rightPanelActions.innerHTML = '';
}
const items = [];
jalanLayer.eachLayer(l => {
@@ -209,7 +219,17 @@
rightPanelTitle.textContent = '🗺️ Daftar Parsil Tanah';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
if (isAdmin) {
rightPanelActions.innerHTML = `
<button class="btn-panel-add" id="btnPanelAddParsil">
<i class="fas fa-plus"></i> Tambah Parsil
</button>`;
document.getElementById('btnPanelAddParsil').addEventListener('click', () => {
window.activateDraw('polygon');
});
} else {
rightPanelActions.innerHTML = '';
}
const items = [];
parsilLayer.eachLayer(l => {
+2 -2
View File
@@ -1,8 +1,8 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
require_once('../poverty/api/auth_helper.php');
requireAdmin();
include_once '../db_connect.php';
include_once('db_connect.php');
$data = json_decode(file_get_contents("php://input"));
+16
View File
@@ -0,0 +1,16 @@
-- =============================================
-- DATABASE: webgis_spbu
-- Tabel: spbu (Lokasi SPBU)
-- =============================================
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 AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
no_wa VARCHAR(20) NOT NULL DEFAULT '',
is_24_jam TINYINT(1) NOT NULL DEFAULT 0,
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+17
View File
@@ -0,0 +1,17 @@
<?php
$host = "localhost";
$user = "root";
$pass = "";
$db = "webgis_spbu";
// Coba koneksi ke server dan pilih database
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die("Koneksi gagal: " . $conn->connect_error);
}
// Set charset
$conn->set_charset("utf8mb4");
// Jika di-include oleh file API, biarkan $conn tersedia
?>
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+42 -174
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pemetaan Jaringan Jalan</title>
<title>WebGIS Pemetaan Lokasi SPBU</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -30,7 +30,7 @@
<div class="search-icon">
<i class="fas fa-search"></i>
</div>
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah...">
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU...">
<button class="search-clear" id="searchClear">
<i class="fas fa-times"></i>
</button>
@@ -51,22 +51,50 @@
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button>
<!-- Tombol Kembali ke Beranda -->
<a href="/webgis/index.html" class="btn-back-home" title="Kembali ke Beranda">
<i class="fas fa-arrow-left"></i>
<span>Beranda</span>
</a>
<style>
.btn-back-home {
position: fixed;
top: 16px;
right: 70px;
z-index: 1100;
display: flex;
align-items: center;
gap: 7px;
background: #ffffff;
color: #4f46e5;
border: 1.5px solid rgba(79, 70, 229, 0.25);
border-radius: 10px;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
font-family: 'Inter', sans-serif;
text-decoration: none;
box-shadow: 0 2px 12px rgba(79, 70, 229, 0.12);
transition: all 0.2s ease;
}
.btn-back-home:hover {
background: #4f46e5;
color: #ffffff;
border-color: #4f46e5;
box-shadow: 0 4px 16px rgba(79, 70, 229, 0.3);
transform: translateY(-1px);
}
.btn-back-home i { font-size: 12px; }
</style>
<!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
<div class="sidebar-section-label">Umum</div>
<div class="sidebar-section-label">Menu</div>
<button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
<span class="sidebar-emoji"></span>
<span class="sidebar-label">SPBU</span>
</button>
<button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
<span class="sidebar-emoji">🛣️</span>
<span class="sidebar-label">Jalan</span>
</button>
<button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
<span class="sidebar-emoji">🗺️</span>
<span class="sidebar-label">Parsil</span>
</button>
</nav>
@@ -101,51 +129,6 @@
</div>
</div>
<!-- Jalan -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerJalan" checked> Jalan
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subJalan', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subJalan" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Nasional" checked onchange="applySubFilter('jalan')"> Nasional
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Provinsi" checked onchange="applySubFilter('jalan')"> Provinsi
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Kabupaten" checked onchange="applySubFilter('jalan')"> Kabupaten
</label>
</div>
</div>
<!-- Parsil -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerParsil" checked> Parsil Tanah
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subParsil', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subParsil" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="SHM" checked onchange="applySubFilter('parsil')"> SHM
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGB" checked onchange="applySubFilter('parsil')"> HGB
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
</label>
</div>
</div>
<!-- GeoJSON Eksternal -->
@@ -215,55 +198,7 @@
</div>
</div>
<!-- Log Bantuan Modal -->
<div id="logBantuanModal" class="unified-modal">
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
<div class="modal-header">
<h3 id="logBantuanTitle">Log Bantuan</h3>
<span class="modal-close" onclick="closeLogBantuanModal()">&times;</span>
</div>
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
<!-- Riwayat -->
<div style="padding:15px; border-bottom:1px solid #eee;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
</div>
</div>
<!-- Form Tambah -->
<div style="padding:15px;">
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
<div class="form-group">
<label>Rumah Ibadah</label>
<select id="logIbadahId"></select>
</div>
<div class="form-group">
<label>Tipe Bantuan</label>
<select id="logTipeBantuan">
<option value="Sembako">Sembako</option>
<option value="Uang Tunai">Uang Tunai</option>
<option value="Pakaian">Pakaian</option>
<option value="Pendidikan">Pendidikan</option>
<option value="Kesehatan">Kesehatan</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="form-group">
<label>Tanggal</label>
<input type="date" id="logTanggal">
</div>
<div class="form-group">
<label>Keterangan (opsional)</label>
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
</div>
</div>
</div>
<!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget">
@@ -294,68 +229,7 @@
</div>
</div>
<!-- User Management Modal -->
<div id="userManagementModal" class="unified-modal">
<div class="modal-content" style="width: 700px; max-width: 95%;">
<div class="modal-header">
<h3>Manajemen Pengguna (Admin)</h3>
<span class="modal-close" id="closeUserManagementModal">&times;</span>
</div>
<div class="modal-body" style="gap: 15px;">
<!-- Form Tambah/Edit User -->
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
<input type="hidden" id="manageUserId" value="">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Username</label>
<input type="text" id="manageUsername" placeholder="Username">
</div>
<div class="form-group">
<label>Password (Kosongkan jika tidak diubah)</label>
<input type="password" id="managePassword" placeholder="Password">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>Role</label>
<select id="manageRole">
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group" id="manageIbadahGroup">
<label>Rumah Ibadah yang Dikelola</label>
<select id="manageIbadahId">
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
</select>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
</div>
</div>
<!-- Tabel Daftar User -->
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
<th style="padding: 10px 12px;">Username</th>
<th style="padding: 10px 12px;">Role</th>
<th style="padding: 10px 12px;">Rumah Ibadah</th>
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- Diisi dinamis -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer"></div>
@@ -373,16 +247,10 @@
<script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen -->
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script>
<script src="assets/js/features/jalan.js?v=<?= time() ?>"></script>
<script src="assets/js/features/parsil.js?v=<?= time() ?>"></script>
<script src="assets/js/features/draw_control.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
<!-- Core Auth Feature -->
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
</body>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
include_once 'db_connect.php';
$query = "SELECT * FROM spbu";
$result = $conn->query($query);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php';
requireAdmin();
include_once '../db_connect.php';
include_once 'db_connect.php';
$data = json_decode(file_get_contents("php://input"));
+101 -32
View File
@@ -5,9 +5,11 @@
box-sizing: border-box;
}
body, html {
body,
html {
height: 100%;
font-family: 'Google Sans Flex', 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family:
"Google Sans Flex", "Inter", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
@@ -131,7 +133,6 @@ body.right-panel-open .custom-layer-panel {
right: 340px;
}
.custom-layer-panel h3 {
margin-bottom: 10px;
font-size: 16px;
@@ -431,12 +432,24 @@ body.right-panel-open .custom-layer-panel {
flex-shrink: 0;
}
.card-icon-spbu { background: #f0fdf4; }
.card-icon-jalan { background: #eff6ff; }
.card-icon-parsil { background: #fefce8; }
.card-icon-ibadah { background: #fff7ed; }
.card-icon-miskin-out { background: #f0fdf4; }
.card-icon-miskin-in { background: #fef2f2; }
.card-icon-spbu {
background: #f0fdf4;
}
.card-icon-jalan {
background: #eff6ff;
}
.card-icon-parsil {
background: #fefce8;
}
.card-icon-ibadah {
background: #fff7ed;
}
.card-icon-miskin-out {
background: #f0fdf4;
}
.card-icon-miskin-in {
background: #fef2f2;
}
.data-card-info {
flex: 1;
@@ -470,11 +483,26 @@ body.right-panel-open .custom-layer-panel {
display: inline-block;
}
.badge-green { background: #dcfce7; color: #16a34a; }
.badge-red { background: #fee2e2; color: #dc2626; }
.badge-blue { background: #dbeafe; color: #2563eb; }
.badge-orange { background: #ffedd5; color: #ea580c; }
.badge-yellow { background: #fef9c3; color: #ca8a04; }
.badge-green {
background: #dcfce7;
color: #16a34a;
}
.badge-red {
background: #fee2e2;
color: #dc2626;
}
.badge-blue {
background: #dbeafe;
color: #2563eb;
}
.badge-orange {
background: #ffedd5;
color: #ea580c;
}
.badge-yellow {
background: #fef9c3;
color: #ca8a04;
}
.data-card-actions {
display: flex;
@@ -520,7 +548,9 @@ body.right-panel-open .custom-layer-panel {
.data-card-expand {
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease, padding 0.2s;
transition:
max-height 0.3s ease,
padding 0.2s;
border-top: 0px solid #f3f4f6;
}
@@ -623,6 +653,8 @@ body.right-panel-open .custom-layer-panel {
}
.popup-form input[type="text"],
.form-group input[type="password"],
.popup-form input[type="password"],
.popup-form input[type="number"] {
width: 100%;
padding: 6px;
@@ -720,6 +752,7 @@ body.right-panel-open .custom-layer-panel {
.modal-body input[type="text"],
.modal-body input[type="number"],
.modal-body input[type="password"],
.modal-body input[type="date"],
.modal-body select,
.modal-body textarea {
@@ -862,16 +895,30 @@ body.right-panel-open .custom-layer-panel {
}
@keyframes markerPop {
0% { transform: rotate(-45deg) scale(0); }
100% { transform: rotate(-45deg) scale(1); }
0% {
transform: rotate(-45deg) scale(0);
}
100% {
transform: rotate(-45deg) scale(1);
}
}
/* Warna bubble per tipe */
.emoji-marker .bubble.spbu-24 { background: linear-gradient(135deg, #22c55e, #16a34a); }
.emoji-marker .bubble.spbu-not24 { background: linear-gradient(135deg, #ef4444, #dc2626); }
.emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); }
.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); }
.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); }
.emoji-marker .bubble.spbu-24 {
background: linear-gradient(135deg, #22c55e, #16a34a);
}
.emoji-marker .bubble.spbu-not24 {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.emoji-marker .bubble.ibadah {
background: linear-gradient(135deg, #f97316, #ea580c);
}
.emoji-marker .bubble.miskin-in {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.emoji-marker .bubble.miskin-out {
background: linear-gradient(135deg, #22c55e, #16a34a);
}
/* ===== Toast Notification ===== */
#toastContainer {
@@ -908,21 +955,44 @@ body.right-panel-open .custom-layer-panel {
animation: toastSlideOut 0.3s ease forwards;
}
.toast-success { background: linear-gradient(135deg, #22c55e, #16a34a); }
.toast-error { background: linear-gradient(135deg, #ef4444, #dc2626); }
.toast-info { background: linear-gradient(135deg, #6366f1, #4f46e5); }
.toast-warning { background: linear-gradient(135deg, #f59e0b, #d97706); }
.toast-success {
background: linear-gradient(135deg, #22c55e, #16a34a);
}
.toast-error {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.toast-info {
background: linear-gradient(135deg, #6366f1, #4f46e5);
}
.toast-warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
.toast-icon { font-size: 16px; flex-shrink: 0; }
.toast-icon {
font-size: 16px;
flex-shrink: 0;
}
@keyframes toastSlideIn {
0% { opacity: 0; transform: translateY(20px) scale(0.9); }
100% { opacity: 1; transform: translateY(0) scale(1); }
0% {
opacity: 0;
transform: translateY(20px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes toastSlideOut {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(10px) scale(0.95); }
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(10px) scale(0.95);
}
}
/* Auth Widget Styling */
@@ -1023,4 +1093,3 @@ body.right-panel-open .auth-widget {
padding: 10px 12px;
border-bottom: 1px solid #e2e8f0;
}