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"> <html lang="id">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta
<title>WebGIS — Sistem Informasi Geografis | Portofolio Tugas</title> name="viewport"
<meta name="description" content="Portofolio proyek WebGIS mata kuliah Sistem Informasi Geografis. Kumpulan aplikasi pemetaan interaktif berbasis web menggunakan Leaflet.js, PHP, dan MySQL." /> content="width=device-width, initial-scale=1.0"
<link rel="preconnect" href="https://fonts.googleapis.com" /> />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <title>WebGIS — Sistem Informasi Geografis | Portofolio Tugas</title>
<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" /> <meta
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" /> 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> <style>
/* ===== CSS RESET & BASE ===== */ /* ===== CSS RESET & BASE ===== */
*, *::before, *::after { *,
*::before,
*::after {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
@@ -31,17 +52,19 @@
--clr-white: #ffffff; --clr-white: #ffffff;
--grad-primary: linear-gradient(135deg, #4f46e5, #7c3aed); --grad-primary: linear-gradient(135deg, #4f46e5, #7c3aed);
--grad-card: linear-gradient(145deg, #ffffff, #f8fafc); --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); --shadow-card: 0 4px 24px rgba(15, 23, 42, 0.08);
--radius: 16px; --radius: 16px;
--radius-sm: 10px; --radius-sm: 10px;
--transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); --transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
} }
html { scroll-behavior: smooth; } html {
scroll-behavior: smooth;
}
body { body {
font-family: 'Inter', sans-serif; font-family: "Inter", sans-serif;
background-color: var(--clr-bg); background-color: var(--clr-bg);
color: var(--clr-text); color: var(--clr-text);
line-height: 1.6; line-height: 1.6;
@@ -49,15 +72,24 @@
} }
/* ===== SCROLLBAR ===== */ /* ===== SCROLLBAR ===== */
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar {
::-webkit-scrollbar-track { background: var(--clr-bg); } width: 6px;
::-webkit-scrollbar-thumb { background: var(--clr-primary); border-radius: 3px; } }
::-webkit-scrollbar-track {
background: var(--clr-bg);
}
::-webkit-scrollbar-thumb {
background: var(--clr-primary);
border-radius: 3px;
}
/* ===== CANVAS BG ===== */ /* ===== CANVAS BG ===== */
#bg-canvas { #bg-canvas {
position: fixed; position: fixed;
top: 0; left: 0; top: 0;
width: 100%; height: 100%; left: 0;
width: 100%;
height: 100%;
z-index: 0; z-index: 0;
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
@@ -75,7 +107,9 @@
/* ===== NAVBAR ===== */ /* ===== NAVBAR ===== */
.navbar { .navbar {
position: fixed; position: fixed;
top: 0; left: 0; right: 0; top: 0;
left: 0;
right: 0;
z-index: 100; z-index: 100;
padding: 16px 0; padding: 16px 0;
background: rgba(240, 244, 248, 0.75); background: rgba(240, 244, 248, 0.75);
@@ -122,7 +156,7 @@
} }
.nav-logo-text { .nav-logo-text {
font-family: 'Space Grotesk', sans-serif; font-family: "Space Grotesk", sans-serif;
font-weight: 700; font-weight: 700;
font-size: 1.1rem; font-size: 1.1rem;
color: var(--clr-text); color: var(--clr-text);
@@ -162,13 +196,19 @@
} }
.hero::before { .hero::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 50%; left: 50%; top: 50%;
left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 900px; width: 900px;
height: 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; pointer-events: none;
} }
@@ -189,15 +229,23 @@
animation: fadeSlideDown 0.8s ease both; 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 { @keyframes pulse {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.4; } 100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
} }
.hero-title { .hero-title {
font-family: 'Space Grotesk', sans-serif; font-family: "Space Grotesk", sans-serif;
font-size: clamp(2.4rem, 6vw, 4.2rem); font-size: clamp(2.4rem, 6vw, 4.2rem);
font-weight: 800; font-weight: 800;
line-height: 1.1; line-height: 1.1;
@@ -237,7 +285,7 @@
} }
.hero-stat-num { .hero-stat-num {
font-family: 'Space Grotesk', sans-serif; font-family: "Space Grotesk", sans-serif;
font-size: 2rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
background: var(--grad-primary); background: var(--grad-primary);
@@ -318,8 +366,14 @@
} }
@keyframes scrollDown { @keyframes scrollDown {
0% { opacity: 1; transform: translateY(0); } 0% {
100% { opacity: 0; transform: translateY(10px); } opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(10px);
}
} }
/* ===== SECTION ===== */ /* ===== SECTION ===== */
@@ -337,7 +391,7 @@
} }
.section-title { .section-title {
font-family: 'Space Grotesk', sans-serif; font-family: "Space Grotesk", sans-serif;
font-size: clamp(1.8rem, 4vw, 2.6rem); font-size: clamp(1.8rem, 4vw, 2.6rem);
font-weight: 700; font-weight: 700;
color: var(--clr-text); color: var(--clr-text);
@@ -387,7 +441,9 @@
.project-card:hover { .project-card:hover {
transform: translateY(-6px); transform: translateY(-6px);
border-color: rgba(79, 70, 229, 0.35); 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 { .project-card:hover .card-arrow {
@@ -420,7 +476,7 @@
} }
.card-num { .card-num {
font-family: 'Space Grotesk', sans-serif; font-family: "Space Grotesk", sans-serif;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: var(--clr-muted); color: var(--clr-muted);
@@ -449,7 +505,7 @@
} }
.card-title { .card-title {
font-family: 'Space Grotesk', sans-serif; font-family: "Space Grotesk", sans-serif;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 700; font-weight: 700;
color: var(--clr-text); color: var(--clr-text);
@@ -481,11 +537,31 @@
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.tag-blue { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; } .tag-blue {
.tag-purple { background: #f5f3ff; color: #6d28d9; border: 1px solid #ddd6fe; } background: #eff6ff;
.tag-green { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; } color: #1d4ed8;
.tag-orange { background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; } border: 1px solid #bfdbfe;
.tag-pink { background: #fdf2f8; color: #9d174d; border: 1px solid #fbcfe8; } }
.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 { .card-footer {
border-top: 1px solid var(--clr-border); border-top: 1px solid var(--clr-border);
@@ -523,19 +599,34 @@
animation: statusPulse 2s infinite; animation: statusPulse 2s infinite;
} }
.status-dot.active { background: var(--clr-accent); box-shadow: 0 0 8px var(--clr-accent); } .status-dot.active {
.status-dot.pending { background: var(--clr-warn); box-shadow: 0 0 8px var(--clr-warn); } background: var(--clr-accent);
.status-dot.inactive { background: var(--clr-muted); } 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 { @keyframes statusPulse {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.4; } 100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
} }
/* ===== CARD ACCENT LINES ===== */ /* ===== CARD ACCENT LINES ===== */
.card-glow-line { .card-glow-line {
position: absolute; position: absolute;
top: 0; left: 0; right: 0; top: 0;
left: 0;
right: 0;
height: 2px; height: 2px;
border-radius: var(--radius) var(--radius) 0 0; border-radius: var(--radius) var(--radius) 0 0;
} }
@@ -600,7 +691,9 @@
transition: var(--transition); transition: var(--transition);
} }
.tech-item:hover { color: var(--clr-text); } .tech-item:hover {
color: var(--clr-text);
}
.tech-item i { .tech-item i {
font-size: 1.4rem; font-size: 1.4rem;
@@ -618,7 +711,7 @@
} }
.footer-logo { .footer-logo {
font-family: 'Space Grotesk', sans-serif; font-family: "Space Grotesk", sans-serif;
font-size: 1.4rem; font-size: 1.4rem;
font-weight: 700; font-weight: 700;
color: var(--clr-text); color: var(--clr-text);
@@ -657,19 +750,33 @@
/* ===== ANIMATIONS ===== */ /* ===== ANIMATIONS ===== */
@keyframes fadeSlideDown { @keyframes fadeSlideDown {
from { opacity: 0; transform: translateY(-20px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
@keyframes fadeSlideUp { @keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(30px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.reveal { .reveal {
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(30px);
transition: opacity 0.7s ease, transform 0.7s ease; transition:
opacity 0.7s ease,
transform 0.7s ease;
} }
.reveal.visible { .reveal.visible {
@@ -679,52 +786,112 @@
/* ===== RESPONSIVE ===== */ /* ===== RESPONSIVE ===== */
@media (max-width: 640px) { @media (max-width: 640px) {
.hero-stats { gap: 24px; } .hero-stats {
.projects-grid { grid-template-columns: 1fr; } gap: 24px;
.section-header { flex-direction: column; align-items: flex-start; } }
.projects-grid {
grid-template-columns: 1fr;
}
.section-header {
flex-direction: column;
align-items: flex-start;
}
} }
</style> </style>
</head> </head>
<body> <body>
<!-- ===== CANVAS BACKGROUND ===== --> <!-- ===== CANVAS BACKGROUND ===== -->
<canvas id="bg-canvas"></canvas> <canvas id="bg-canvas"></canvas>
<!-- ===== NAVBAR ===== --> <!-- ===== NAVBAR ===== -->
<nav class="navbar" id="navbar"> <nav
class="navbar"
id="navbar"
>
<div class="nav-inner"> <div class="nav-inner">
<a href="#" class="nav-logo" id="logo-link"> <a
<div class="nav-logo-icon"><i class="fa-solid fa-map-location-dot"></i></div> 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"> <div class="nav-logo-text">
WebGIS WebGIS
<span>Sistem Informasi Geografis</span> <span>Sistem Informasi Geografis</span>
</div> </div>
</a> </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> </div>
</nav> </nav>
<!-- ===== HERO ===== --> <!-- ===== HERO ===== -->
<section class="hero" id="home"> <section
class="hero"
id="home"
>
<div class="wrapper"> <div class="wrapper">
<div class="hero-badge" id="hero-badge"> <div
class="hero-badge"
id="hero-badge"
>
<i class="fa-solid fa-circle"></i> <i class="fa-solid fa-circle"></i>
Sistem Informasi Geografis Sistem Informasi Geografis
</div> </div>
<h1 class="hero-title" id="hero-title"> <h1
class="hero-title"
id="hero-title"
>
Portofolio Proyek<br /> Portofolio Proyek<br />
<span class="gradient-text">WebGIS Interaktif</span> <span class="gradient-text">WebGIS Interaktif</span>
</h1> </h1>
<p class="hero-sub" id="hero-sub"> <p
Kumpulan tugas pemetaan berbasis web menggunakan teknologi geospasial modern — class="hero-sub"
dari peta jalan, persil, hingga visualisasi data sosial. id="hero-sub"
>
Kumpulan tugas pemetaan berbasis web menggunakan teknologi geospasial
modern — dari peta jalan, persil, hingga visualisasi data sosial.
</p> </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">
<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 class="hero-stat-label">Proyek</div>
</div> </div>
<div class="hero-divider"></div> <div class="hero-divider"></div>
@@ -739,7 +906,11 @@
</div> </div>
</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> <i class="fa-solid fa-layer-group"></i>
Lihat Semua Proyek Lihat Semua Proyek
</a> </a>
@@ -757,27 +928,45 @@
<p class="tech-strip-label">Teknologi yang Digunakan</p> <p class="tech-strip-label">Teknologi yang Digunakan</p>
<div class="tech-items"> <div class="tech-items">
<div class="tech-item"> <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 Leaflet.js
</div> </div>
<div class="tech-item"> <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 PHP
</div> </div>
<div class="tech-item"> <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 MySQL
</div> </div>
<div class="tech-item"> <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 HTML / CSS / JS
</div> </div>
<div class="tech-item"> <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 GeoJSON
</div> </div>
<div class="tech-item"> <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 XAMPP
</div> </div>
</div> </div>
@@ -785,32 +974,58 @@
</div> </div>
<!-- ===== PROJECTS ===== --> <!-- ===== PROJECTS ===== -->
<section class="section" id="projects"> <section
class="section"
id="projects"
>
<div class="wrapper"> <div class="wrapper">
<div class="section-header reveal"> <div class="section-header reveal">
<div> <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> <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> </div>
<div class="projects-grid"> <div class="projects-grid">
<!-- ===== PROJECT 1: POVERTY ===== --> <!-- ===== PROJECT 1: POVERTY ===== -->
<a href="/webgis/poverty/index.php" class="project-card" id="card-poverty" style="animation-delay: 0.05s;"> <a
<div class="card-glow-line" style="background: linear-gradient(90deg, #38bdf8, #818cf8);"></div> href="/webgis/poverty/index.php"
<div class="card-arrow"><i class="fa-solid fa-arrow-up-right"></i></div> 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-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> </div>
<span class="card-num">01</span> <span class="card-num">01</span>
</div> </div>
<div class="card-body"> <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"> <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> </p>
<div class="card-tags"> <div class="card-tags">
<span class="tag tag-blue">Leaflet.js</span> <span class="tag tag-blue">Leaflet.js</span>
@@ -826,16 +1041,35 @@
</span> </span>
<div class="card-status"> <div class="card-status">
<div class="status-dot active"></div> <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>
</div> </div>
</a> </a>
<!-- ===== PROJECT 2: JALAN ===== --> <!-- ===== PROJECT 2: JALAN ===== -->
<a href="/webgis/jalan/" class="project-card" id="card-jalan" style="animation-delay: 0.1s; cursor: default;" onclick="return false;"> <a
<div class="card-glow-line" style="background: linear-gradient(90deg, #818cf8, #f472b6);"></div> 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-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> </div>
<span class="card-num">02</span> <span class="card-num">02</span>
@@ -843,7 +1077,9 @@
<div class="card-body"> <div class="card-body">
<h3 class="card-title">Pemetaan Jaringan<br />Jalan</h3> <h3 class="card-title">Pemetaan Jaringan<br />Jalan</h3>
<p class="card-desc"> <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> </p>
<div class="card-tags"> <div class="card-tags">
<span class="tag tag-purple">WebGIS</span> <span class="tag tag-purple">WebGIS</span>
@@ -852,24 +1088,40 @@
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<span class="card-link-btn" style="color: var(--clr-muted);"> <span class="card-link-btn">
Segera Hadir <i class="fa-solid fa-clock"></i> Buka Proyek <i class="fa-solid fa-arrow-right"></i>
</span> </span>
<div class="card-status"> <div class="card-status">
<div class="status-dot pending"></div> <div class="status-dot active"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem;">Segera</span> <span style="color: var(--clr-muted); font-size: 0.75rem"
>Aktif</span
>
</div> </div>
</div> </div>
<div class="card-overlay">
<div class="coming-soon-badge">🚧 &nbsp;Coming Soon</div>
</div>
</a> </a>
<!-- ===== PROJECT 3: PARSIL ===== --> <!-- ===== PROJECT 3: PARSIL ===== -->
<a href="/webgis/parsil/" class="project-card" id="card-parsil" style="animation-delay: 0.15s; cursor: default;" onclick="return false;"> <a
<div class="card-glow-line" style="background: linear-gradient(90deg, #34d399, #38bdf8);"></div> 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-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> </div>
<span class="card-num">03</span> <span class="card-num">03</span>
@@ -877,7 +1129,9 @@
<div class="card-body"> <div class="card-body">
<h3 class="card-title">Pemetaan Bidang<br />Tanah (Parsil)</h3> <h3 class="card-title">Pemetaan Bidang<br />Tanah (Parsil)</h3>
<p class="card-desc"> <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> </p>
<div class="card-tags"> <div class="card-tags">
<span class="tag tag-green">Land Parcel</span> <span class="tag tag-green">Land Parcel</span>
@@ -886,24 +1140,40 @@
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<span class="card-link-btn" style="color: var(--clr-muted);"> <span class="card-link-btn">
Segera Hadir <i class="fa-solid fa-clock"></i> Buka Proyek <i class="fa-solid fa-arrow-right"></i>
</span> </span>
<div class="card-status"> <div class="card-status">
<div class="status-dot pending"></div> <div class="status-dot active"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem;">Segera</span> <span style="color: var(--clr-muted); font-size: 0.75rem"
>Aktif</span
>
</div> </div>
</div> </div>
<div class="card-overlay">
<div class="coming-soon-badge">🚧 &nbsp;Coming Soon</div>
</div>
</a> </a>
<!-- ===== PROJECT 4: SPBU ===== --> <!-- ===== PROJECT 4: SPBU ===== -->
<a href="/webgis/spbu/" class="project-card" id="card-spbu" style="animation-delay: 0.2s; cursor: default;" onclick="return false;"> <a
<div class="card-glow-line" style="background: linear-gradient(90deg, #fb923c, #facc15);"></div> 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-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> </div>
<span class="card-num">04</span> <span class="card-num">04</span>
@@ -911,7 +1181,9 @@
<div class="card-body"> <div class="card-body">
<h3 class="card-title">Pemetaan Lokasi<br />SPBU</h3> <h3 class="card-title">Pemetaan Lokasi<br />SPBU</h3>
<p class="card-desc"> <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> </p>
<div class="card-tags"> <div class="card-tags">
<span class="tag tag-orange">POI Mapping</span> <span class="tag tag-orange">POI Mapping</span>
@@ -920,19 +1192,17 @@
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<span class="card-link-btn" style="color: var(--clr-muted);"> <span class="card-link-btn">
Segera Hadir <i class="fa-solid fa-clock"></i> Buka Proyek <i class="fa-solid fa-arrow-right"></i>
</span> </span>
<div class="card-status"> <div class="card-status">
<div class="status-dot pending"></div> <div class="status-dot active"></div>
<span style="color: var(--clr-muted); font-size: 0.75rem;">Segera</span> <span style="color: var(--clr-muted); font-size: 0.75rem"
>Aktif</span
>
</div> </div>
</div> </div>
<div class="card-overlay">
<div class="coming-soon-badge">🚧 &nbsp;Coming Soon</div>
</div>
</a> </a>
</div> </div>
</div> </div>
</section> </section>
@@ -943,27 +1213,48 @@
<div class="footer-inner"> <div class="footer-inner">
<div class="footer-logo">Web<span>GIS</span></div> <div class="footer-logo">Web<span>GIS</span></div>
<div class="footer-course"> <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>
<div style="display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap;"> <div
<span style="font-size:0.8rem; color: var(--clr-muted);"> style="
<i class="fa-solid fa-map-location-dot" style="color: var(--clr-primary);"></i> 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 &nbsp;Leaflet.js
</span> </span>
<span style="color: var(--clr-border);">|</span> <span style="color: var(--clr-border)">|</span>
<span style="font-size:0.8rem; color: var(--clr-muted);"> <span style="font-size: 0.8rem; color: var(--clr-muted)">
<i class="fa-solid fa-database" style="color: var(--clr-accent);"></i> <i
class="fa-solid fa-database"
style="color: var(--clr-accent)"
></i>
&nbsp;MySQL Spatial &nbsp;MySQL Spatial
</span> </span>
<span style="color: var(--clr-border);">|</span> <span style="color: var(--clr-border)">|</span>
<span style="font-size:0.8rem; color: var(--clr-muted);"> <span style="font-size: 0.8rem; color: var(--clr-muted)">
<i class="fa-brands fa-php" style="color: var(--clr-secondary);"></i> <i
class="fa-brands fa-php"
style="color: var(--clr-secondary)"
></i>
&nbsp;PHP / XAMPP &nbsp;PHP / XAMPP
</span> </span>
</div> </div>
<div class="footer-divider"></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"> <p class="footer-copy">
&copy; 2025 WebGIS Portfolio &nbsp;·&nbsp; Sistem Informasi Geografis &copy; 2025 WebGIS Portfolio &nbsp;&middot;&nbsp; Sistem Informasi Geografis
</p> </p>
</div> </div>
</div> </div>
@@ -973,8 +1264,8 @@
<script> <script>
/* ----- CANVAS PARTICLE / GRID BG ----- */ /* ----- CANVAS PARTICLE / GRID BG ----- */
(function () { (function () {
const canvas = document.getElementById('bg-canvas'); const canvas = document.getElementById("bg-canvas");
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
let w, h, animId; let w, h, animId;
const DOTS = []; const DOTS = [];
const LINES = []; const LINES = [];
@@ -985,18 +1276,24 @@
h = canvas.height = window.innerHeight; h = canvas.height = window.innerHeight;
} }
resize(); resize();
window.addEventListener('resize', resize); window.addEventListener("resize", resize);
// Grid lines // Grid lines
function drawGrid() { function drawGrid() {
ctx.strokeStyle = 'rgba(79,70,229,0.06)'; ctx.strokeStyle = "rgba(79,70,229,0.06)";
ctx.lineWidth = 1; ctx.lineWidth = 1;
const step = 60; const step = 60;
for (let x = 0; x < w; x += step) { 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) { 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, vx: (Math.random() - 0.5) * 0.3,
vy: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3,
r: Math.random() * 2 + 0.5, r: Math.random() * 2 + 0.5,
a: Math.random() a: Math.random(),
}); });
} }
@@ -1017,8 +1314,9 @@
drawGrid(); drawGrid();
// Move and draw dots // Move and draw dots
DOTS.forEach(d => { DOTS.forEach((d) => {
d.x += d.vx; d.y += d.vy; d.x += d.vx;
d.y += d.vy;
if (d.x < 0) d.x = w; if (d.x < 0) d.x = w;
if (d.x > w) d.x = 0; if (d.x > w) d.x = 0;
if (d.y < 0) d.y = h; if (d.y < 0) d.y = h;
@@ -1053,45 +1351,53 @@
})(); })();
/* ----- NAVBAR SCROLL ----- */ /* ----- NAVBAR SCROLL ----- */
const navbar = document.getElementById('navbar'); const navbar = document.getElementById("navbar");
window.addEventListener('scroll', () => { window.addEventListener("scroll", () => {
navbar.classList.toggle('scrolled', window.scrollY > 40); navbar.classList.toggle("scrolled", window.scrollY > 40);
}); });
/* ----- SCROLL REVEAL ----- */ /* ----- SCROLL REVEAL ----- */
const revealEls = document.querySelectorAll('.reveal'); const revealEls = document.querySelectorAll(".reveal");
const revealObs = new IntersectionObserver((entries) => { const revealObs = new IntersectionObserver(
entries.forEach(e => { (entries) => {
entries.forEach((e) => {
if (e.isIntersecting) { if (e.isIntersecting) {
e.target.classList.add('visible'); e.target.classList.add("visible");
revealObs.unobserve(e.target); revealObs.unobserve(e.target);
} }
}); });
}, { threshold: 0.15 }); },
revealEls.forEach(el => revealObs.observe(el)); { threshold: 0.15 },
);
revealEls.forEach((el) => revealObs.observe(el));
/* ----- CARD REVEAL ----- */ /* ----- CARD REVEAL ----- */
const cardEls = document.querySelectorAll('.project-card'); const cardEls = document.querySelectorAll(".project-card");
const cardObs = new IntersectionObserver((entries) => { const cardObs = new IntersectionObserver(
entries.forEach(e => { (entries) => {
entries.forEach((e) => {
if (e.isIntersecting) { if (e.isIntersecting) {
e.target.style.opacity = '1'; e.target.style.opacity = "1";
e.target.style.transform = 'translateY(0)'; e.target.style.transform = "translateY(0)";
cardObs.unobserve(e.target); cardObs.unobserve(e.target);
} }
}); });
}, { threshold: 0.1 }); },
cardEls.forEach(card => { { threshold: 0.1 },
card.style.opacity = '0'; );
card.style.transform = 'translateY(30px)'; cardEls.forEach((card) => {
card.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; card.style.opacity = "0";
card.style.transform = "translateY(30px)";
card.style.transition = "opacity 0.6s ease, transform 0.6s ease";
cardObs.observe(card); cardObs.observe(card);
}); });
/* ----- SMOOTH HERO CTA ----- */ /* ----- SMOOTH HERO CTA ----- */
document.getElementById('hero-cta').addEventListener('click', (e) => { document.getElementById("hero-cta").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
document.getElementById('projects').scrollIntoView({ behavior: 'smooth' }); document
.getElementById("projects")
.scrollIntoView({ behavior: "smooth" });
}); });
</script> </script>
</body> </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 // Flag untuk menghindari munculnya form SPBU saat sedang menggambar
map.on(L.Draw.Event.DRAWSTART, function (e) { map.on(L.Draw.Event.DRAWSTART, function (e) {
isDrawingMode = true; window.currentDrawMode = window.currentDrawMode || 'drawing';
}); });
map.on(L.Draw.Event.DRAWSTOP, function (e) { map.on(L.Draw.Event.DRAWSTOP, function (e) {
setTimeout(() => { setTimeout(() => {
isDrawingMode = false;
window.currentDrawMode = null; window.currentDrawMode = null;
window.activeDrawHandler = null; window.activeDrawHandler = null;
window.deactivateAddMode(); window.deactivateAddMode();
+2 -3
View File
@@ -15,8 +15,6 @@ function makeSpbuIcon(is24) {
const spbuGreenIcon = makeSpbuIcon(true); const spbuGreenIcon = makeSpbuIcon(true);
const spbuRedIcon = makeSpbuIcon(false); const spbuRedIcon = makeSpbuIcon(false);
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
// Fetch SPBU // Fetch SPBU
function loadSpbu() { function loadSpbu() {
@@ -75,7 +73,8 @@ function addSpbuMarker(item) {
// Map Click -> Form Add // Map Click -> Form Add
map.on('click', function(e) { 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 // Hanya proses jika mode tambah SPBU aktif
if (window.currentAddMode === 'spbu') { 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 <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; include_once 'db_connect.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $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"; $host = "localhost";
$user = "root"; $user = "root";
$pass = ""; $pass = "";
$db = "webgis"; $db = "webgis_jalan";
// Coba koneksi ke server dan pilih database // Coba koneksi ke server dan pilih database
$conn = new mysqli($host, $user, $pass, $db); $conn = new mysqli($host, $user, $pass, $db);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; include_once 'db_connect.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
+42 -187
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pemetaan Kemiskinan</title> <title>WebGIS Pemetaan Jaringan Jalan</title>
<!-- Google Fonts --> <!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <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"> <div class="search-icon">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</div> </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"> <button class="search-clear" id="searchClear">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
@@ -51,39 +51,50 @@
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i> <i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button> </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 --> <!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;"> <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"> <button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
<span class="sidebar-emoji">🛣️</span> <span class="sidebar-emoji">🛣️</span>
<span class="sidebar-label">Jalan</span> <span class="sidebar-label">Jalan</span>
</button> </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> </nav>
@@ -100,24 +111,6 @@
<div class="layer-section-title">Infrastruktur & Lahan</div> <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 --> <!-- Jalan -->
<div class="layer-group-item"> <div class="layer-group-item">
<div class="layer-group-header"> <div class="layer-group-header">
@@ -139,30 +132,6 @@
</div> </div>
</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> <div class="layer-section-title">Pemetaan Kemiskinan</div>
<!-- Rumah Ibadah --> <!-- Rumah Ibadah -->
@@ -255,55 +224,7 @@
</div> </div>
</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 --> <!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget"> <div id="authWidget" class="auth-widget">
@@ -334,68 +255,7 @@
</div> </div>
</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 --> <!-- Toast Notification Container -->
<div id="toastContainer"></div> <div id="toastContainer"></div>
@@ -412,17 +272,12 @@
<!-- Main Map Initialization --> <!-- Main Map Initialization -->
<script src="assets/js/map.js?v=<?= time() ?>"></script> <script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen --> <!-- 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/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/draw_control.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geolocation.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/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/features/auth.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); 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"; $query = "SELECT id, nama, status, panjang, ST_AsGeoJSON(geom) as geom FROM jalan";
$result = $conn->query($query); $result = $conn->query($query);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; include_once 'db_connect.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $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 // Flag untuk menghindari munculnya form SPBU saat sedang menggambar
map.on(L.Draw.Event.DRAWSTART, function (e) { map.on(L.Draw.Event.DRAWSTART, function (e) {
isDrawingMode = true; window.currentDrawMode = window.currentDrawMode || 'drawing';
}); });
map.on(L.Draw.Event.DRAWSTOP, function (e) { map.on(L.Draw.Event.DRAWSTOP, function (e) {
setTimeout(() => { setTimeout(() => {
isDrawingMode = false;
window.currentDrawMode = null; window.currentDrawMode = null;
window.activeDrawHandler = null; window.activeDrawHandler = null;
window.deactivateAddMode(); window.deactivateAddMode();
+2 -3
View File
@@ -15,8 +15,6 @@ function makeSpbuIcon(is24) {
const spbuGreenIcon = makeSpbuIcon(true); const spbuGreenIcon = makeSpbuIcon(true);
const spbuRedIcon = makeSpbuIcon(false); const spbuRedIcon = makeSpbuIcon(false);
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
// Fetch SPBU // Fetch SPBU
function loadSpbu() { function loadSpbu() {
@@ -75,7 +73,8 @@ function addSpbuMarker(item) {
// Map Click -> Form Add // Map Click -> Form Add
map.on('click', function(e) { 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 // Hanya proses jika mode tambah SPBU aktif
if (window.currentAddMode === 'spbu') { if (window.currentAddMode === 'spbu') {
+128 -109
View File
@@ -1,246 +1,265 @@
// Inisialisasi Peta // 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 ===== // ===== Toast Notification =====
window.showToast = function(message, type = 'success', duration = 3500) { window.showToast = function (message, type = "success", duration = 3500) {
const icons = { success: '✅', error: '❌', info: '️', warning: '⚠️' }; const icons = { success: "✅", error: "❌", info: "️", warning: "⚠️" };
const container = document.getElementById('toastContainer'); const container = document.getElementById("toastContainer");
if (!container) return; if (!container) return;
const toast = document.createElement('div'); const toast = document.createElement("div");
toast.className = `toast toast-${type}`; 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); container.appendChild(toast);
setTimeout(() => { setTimeout(() => {
toast.classList.add('hiding'); toast.classList.add("hiding");
toast.addEventListener('animationend', () => toast.remove()); toast.addEventListener("animationend", () => toast.remove());
}, duration); }, duration);
}; };
// Custom Zoom Control Logic // Custom Zoom Control Logic
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); }); document.getElementById("zoomInBtn").addEventListener("click", function () {
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); }); map.zoomIn();
});
document.getElementById("zoomOutBtn").addEventListener("click", function () {
map.zoomOut();
});
// Base Map dari OpenStreetMap // Base Map dari OpenStreetMap
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map); }).addTo(map);
// Inisialisasi FeatureGroups untuk masing-masing layer // 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 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 searchInput = document.getElementById("searchInput");
const searchClear = document.getElementById('searchClear'); const searchClear = document.getElementById("searchClear");
if (searchInput) { if (searchInput) {
searchInput.addEventListener('focus', function() { searchInput.addEventListener("focus", function () {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode(); if (typeof window.deactivateAddMode === "function")
window.deactivateAddMode();
}); });
searchInput.addEventListener('input', function() { searchInput.addEventListener("input", function () {
const val = this.value.toLowerCase(); const val = this.value.toLowerCase();
if (val.length > 0) { if (val.length > 0) {
searchClear.style.visibility = 'visible'; searchClear.style.visibility = "visible";
window.showSearchResults(val); window.showSearchResults(val);
} else { } else {
searchClear.style.visibility = 'hidden'; searchClear.style.visibility = "hidden";
document.getElementById('searchResults').style.display = 'none'; document.getElementById("searchResults").style.display = "none";
} }
}); });
} }
if (searchClear) { if (searchClear) {
searchClear.addEventListener('click', function() { searchClear.addEventListener("click", function () {
searchInput.value = ''; searchInput.value = "";
searchClear.style.visibility = 'hidden'; searchClear.style.visibility = "hidden";
document.getElementById('searchResults').style.display = 'none'; document.getElementById("searchResults").style.display = "none";
searchInput.focus(); searchInput.focus();
}); });
} }
window.showSearchResults = function (query) { window.showSearchResults = function (query) {
const resultsContainer = document.getElementById('searchResults'); const resultsContainer = document.getElementById("searchResults");
if (!resultsContainer) return; if (!resultsContainer) return;
resultsContainer.innerHTML = ''; resultsContainer.innerHTML = "";
let results = []; let results = [];
if (typeof spbuLayer !== 'undefined') { if (typeof parsilLayer !== "undefined") {
spbuLayer.eachLayer(layer => { parsilLayer.eachLayer((layer) => {
if (layer.spbuData && layer.spbuData.nama && layer.spbuData.nama.toLowerCase().includes(query)) { if (
results.push({ type: 'SPBU', nama: layer.spbuData.nama, layer: layer }); 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) { 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 { } else {
results.forEach(res => { results.forEach((res) => {
const item = document.createElement('div'); const item = document.createElement("div");
item.style.padding = '10px 15px'; item.style.padding = "10px 15px";
item.style.cursor = 'pointer'; item.style.cursor = "pointer";
item.style.borderBottom = '1px solid #eee'; item.style.borderBottom = "1px solid #eee";
item.style.fontSize = '14px'; item.style.fontSize = "14px";
item.innerHTML = `<strong>${res.type}:</strong> ${res.nama}`; item.innerHTML = `<strong>${res.type}:</strong> ${res.nama}`;
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f8f9fa'); item.addEventListener(
item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white'); "mouseenter",
item.addEventListener('click', () => { () => (item.style.backgroundColor = "#f8f9fa"),
);
item.addEventListener(
"mouseleave",
() => (item.style.backgroundColor = "white"),
);
item.addEventListener("click", () => {
if (res.layer instanceof L.Marker) { if (res.layer instanceof L.Marker) {
map.setView(res.layer.getLatLng(), 17); map.setView(res.layer.getLatLng(), 17);
} else if (res.layer.getBounds) { } else if (res.layer.getBounds) {
map.fitBounds(res.layer.getBounds()); map.fitBounds(res.layer.getBounds());
} }
res.layer.openPopup(); res.layer.openPopup();
resultsContainer.style.display = 'none'; resultsContainer.style.display = "none";
}); });
resultsContainer.appendChild(item); resultsContainer.appendChild(item);
}); });
} }
resultsContainer.style.display = 'block'; resultsContainer.style.display = "block";
}; };
// UI Logic: Custom Layer Control Toggle // UI Logic: Custom Layer Control Toggle
const layerBtn = document.getElementById('layerBtn'); const layerBtn = document.getElementById("layerBtn");
const layerPanel = document.getElementById('layerPanel'); const layerPanel = document.getElementById("layerPanel");
if (layerBtn && layerPanel) { if (layerBtn && layerPanel) {
layerBtn.addEventListener('click', function() { layerBtn.addEventListener("click", function () {
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode(); if (typeof window.deactivateAddMode === "function")
layerPanel.style.display = layerPanel.style.display === 'block' ? 'none' : 'block'; window.deactivateAddMode();
layerPanel.style.display =
layerPanel.style.display === "block" ? "none" : "block";
}); });
} }
// Sembunyikan layer panel saat klik di peta // Sembunyikan layer panel saat klik di peta
map.on('click', function() { map.on("click", function () {
if (layerPanel) layerPanel.style.display = 'none'; if (layerPanel) layerPanel.style.display = "none";
}); });
// Logic untuk Toggle Layer Visibility // Logic untuk Toggle Layer Visibility
const setupLayerToggle = (id, layer) => { const setupLayerToggle = (id, layer) => {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) { if (el) {
el.addEventListener('change', function(e) { el.addEventListener("change", function (e) {
e.target.checked ? map.addLayer(layer) : map.removeLayer(layer); e.target.checked ? map.addLayer(layer) : map.removeLayer(layer);
}); });
} }
}; };
setupLayerToggle('layerSpbu', spbuLayer); setupLayerToggle("layerParsil", parsilLayer);
setupLayerToggle('layerJalan', jalanLayer);
setupLayerToggle('layerParsil', parsilLayer);
setupLayerToggle('layerRumahIbadah', rumahIbadahLayer);
setupLayerToggle('layerMiskin', pendudukMiskinLayer);
// --- Sub-layer Toggle (expand/collapse) --- // --- Sub-layer Toggle (expand/collapse) ---
window.toggleSubLayer = function (subId, iconEl) { window.toggleSubLayer = function (subId, iconEl) {
const sub = document.getElementById(subId); const sub = document.getElementById(subId);
if (!sub) return; if (!sub) return;
const isHidden = sub.style.display === 'none'; const isHidden = sub.style.display === "none";
sub.style.display = isHidden ? '' : 'none'; sub.style.display = isHidden ? "" : "none";
iconEl.classList.toggle('collapsed', !isHidden); iconEl.classList.toggle("collapsed", !isHidden);
}; };
// --- Sub-layer Filter --- // --- Sub-layer Filter ---
window.applySubFilter = function (type) { window.applySubFilter = function (type) {
if (type === 'spbu') { if (type === "spbu") {
const checked = [...document.querySelectorAll('.sub-spbu:checked')].map(el => el.value === '1'); const checked = [...document.querySelectorAll(".sub-spbu:checked")].map(
spbuLayer.eachLayer(layer => { (el) => el.value === "1",
);
spbuLayer.eachLayer((layer) => {
if (layer.spbuData === undefined) return; if (layer.spbuData === undefined) return;
if (checked.includes(layer.spbuData.is_24_jam)) { 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 { } else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none'); layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "none");
} }
}); });
} else if (type === 'jalan') { } else if (type === "jalan") {
const checked = [...document.querySelectorAll('.sub-jalan:checked')].map(el => el.value); const checked = [...document.querySelectorAll(".sub-jalan:checked")].map(
jalanLayer.eachLayer(layer => { (el) => el.value,
);
jalanLayer.eachLayer((layer) => {
if (!layer.jalanData) return; if (!layer.jalanData) return;
if (checked.includes(layer.jalanData.status)) { if (checked.includes(layer.jalanData.status)) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = ''); layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "");
} else { } else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none'); layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "none");
} }
}); });
} else if (type === 'parsil') { } else if (type === "parsil") {
const checked = [...document.querySelectorAll('.sub-parsil:checked')].map(el => el.value); const checked = [...document.querySelectorAll(".sub-parsil:checked")].map(
parsilLayer.eachLayer(layer => { (el) => el.value,
);
parsilLayer.eachLayer((layer) => {
if (!layer.parsilData) return; if (!layer.parsilData) return;
if (checked.includes(layer.parsilData.status)) { if (checked.includes(layer.parsilData.status)) {
layer.getElement && layer.getElement() && (layer.getElement().style.display = ''); layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "");
} else { } else {
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none'); layer.getElement &&
layer.getElement() &&
(layer.getElement().style.display = "none");
} }
}); });
} }
}; };
// --- Modal Logic --- // --- Modal Logic ---
const unifiedModal = document.getElementById('unifiedModal'); const unifiedModal = document.getElementById("unifiedModal");
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById("modalTitle");
const modalBody = document.getElementById('modalBody'); const modalBody = document.getElementById("modalBody");
window.openModal = function (title, bodyHTML, saveCallback) { window.openModal = function (title, bodyHTML, saveCallback) {
if (!unifiedModal) return; if (!unifiedModal) return;
modalTitle.textContent = title; modalTitle.textContent = title;
modalBody.innerHTML = bodyHTML; modalBody.innerHTML = bodyHTML;
unifiedModal.classList.add('show'); unifiedModal.classList.add("show");
const currentSaveBtn = document.getElementById('modalSaveBtn'); const currentSaveBtn = document.getElementById("modalSaveBtn");
if (currentSaveBtn) { if (currentSaveBtn) {
const newSaveBtn = currentSaveBtn.cloneNode(true); const newSaveBtn = currentSaveBtn.cloneNode(true);
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn); currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
newSaveBtn.addEventListener('click', saveCallback); newSaveBtn.addEventListener("click", saveCallback);
} }
}; };
window.closeModal = function () { window.closeModal = function () {
if (unifiedModal) unifiedModal.classList.remove('show'); if (unifiedModal) unifiedModal.classList.remove("show");
}; };
const confirmModal = document.getElementById('confirmModal'); const confirmModal = document.getElementById("confirmModal");
const confirmMessage = document.getElementById('confirmMessage'); const confirmMessage = document.getElementById("confirmMessage");
window.openConfirmModal = function (msg, confirmCallback) { window.openConfirmModal = function (msg, confirmCallback) {
if (!confirmModal) return; if (!confirmModal) return;
confirmMessage.textContent = msg; confirmMessage.textContent = msg;
confirmModal.classList.add('show'); confirmModal.classList.add("show");
const currentYesBtn = document.getElementById('confirmYesBtn'); const currentYesBtn = document.getElementById("confirmYesBtn");
if (currentYesBtn) { if (currentYesBtn) {
const newYesBtn = currentYesBtn.cloneNode(true); const newYesBtn = currentYesBtn.cloneNode(true);
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn); currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
newYesBtn.addEventListener('click', function() { newYesBtn.addEventListener("click", function () {
confirmModal.classList.remove('show'); confirmModal.classList.remove("show");
confirmCallback(); confirmCallback();
}); });
} }
}; };
window.closeConfirmModal = function () { window.closeConfirmModal = function () {
if (confirmModal) confirmModal.classList.remove('show'); if (confirmModal) confirmModal.classList.remove("show");
}; };
window.currentAddMode = null; // 'spbu', 'jalan', 'parsil' window.currentAddMode = null; // 'spbu', 'jalan', 'parsil'
@@ -250,10 +269,10 @@ window.activateAddMode = function(mode) {
return; return;
} }
window.currentAddMode = mode; window.currentAddMode = mode;
document.getElementById('map').style.cursor = 'crosshair'; document.getElementById("map").style.cursor = "crosshair";
}; };
window.deactivateAddMode = function () { window.deactivateAddMode = function () {
window.currentAddMode = null; window.currentAddMode = null;
document.getElementById('map').style.cursor = ''; document.getElementById("map").style.cursor = "";
}; };
+20
View File
@@ -153,7 +153,17 @@
rightPanelTitle.textContent = '🛣️ Daftar Jalan'; rightPanelTitle.textContent = '🛣️ Daftar Jalan';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin'); 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 = ''; rightPanelActions.innerHTML = '';
}
const items = []; const items = [];
jalanLayer.eachLayer(l => { jalanLayer.eachLayer(l => {
@@ -209,7 +219,17 @@
rightPanelTitle.textContent = '🗺️ Daftar Parsil Tanah'; rightPanelTitle.textContent = '🗺️ Daftar Parsil Tanah';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin'); 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 = ''; rightPanelActions.innerHTML = '';
}
const items = []; const items = [];
parsilLayer.eachLayer(l => { parsilLayer.eachLayer(l => {
+1 -3
View File
@@ -1,8 +1,6 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; include_once 'db_connect.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $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 <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; include_once 'db_connect.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
+42 -167
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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 --> <!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <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"> <div class="search-icon">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</div> </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"> <button class="search-clear" id="searchClear">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
@@ -51,22 +51,50 @@
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i> <i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button> </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 --> <!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;"> <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"> <button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
<span class="sidebar-emoji">🗺️</span> <span class="sidebar-emoji">🗺️</span>
<span class="sidebar-label">Parsil</span> <span class="sidebar-label">Parsil</span>
</button> </button>
</nav> </nav>
@@ -83,45 +111,6 @@
<div class="layer-section-title">Infrastruktur & Lahan</div> <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 --> <!-- Parsil -->
<div class="layer-group-item"> <div class="layer-group-item">
<div class="layer-group-header"> <div class="layer-group-header">
@@ -215,55 +204,7 @@
</div> </div>
</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 --> <!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget"> <div id="authWidget" class="auth-widget">
@@ -294,68 +235,7 @@
</div> </div>
</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 --> <!-- Toast Notification Container -->
<div id="toastContainer"></div> <div id="toastContainer"></div>
@@ -372,17 +252,12 @@
<!-- Main Map Initialization --> <!-- Main Map Initialization -->
<script src="assets/js/map.js?v=<?= time() ?>"></script> <script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen --> <!-- 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/parsil.js?v=<?= time() ?>"></script>
<script src="assets/js/features/draw_control.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/geolocation.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geojson.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/features/auth.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); 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"; $query = "SELECT id, nama, status, luas, ST_AsGeoJSON(geom) as geom FROM parsil";
$result = $conn->query($query); $result = $conn->query($query);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; include_once 'db_connect.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $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 // Flag untuk menghindari munculnya form SPBU saat sedang menggambar
map.on(L.Draw.Event.DRAWSTART, function (e) { map.on(L.Draw.Event.DRAWSTART, function (e) {
isDrawingMode = true; window.currentDrawMode = window.currentDrawMode || 'drawing';
}); });
map.on(L.Draw.Event.DRAWSTOP, function (e) { map.on(L.Draw.Event.DRAWSTOP, function (e) {
setTimeout(() => { setTimeout(() => {
isDrawingMode = false;
window.currentDrawMode = null; window.currentDrawMode = null;
window.activeDrawHandler = null; window.activeDrawHandler = null;
window.deactivateAddMode(); window.deactivateAddMode();
+2 -3
View File
@@ -15,8 +15,6 @@ function makeSpbuIcon(is24) {
const spbuGreenIcon = makeSpbuIcon(true); const spbuGreenIcon = makeSpbuIcon(true);
const spbuRedIcon = makeSpbuIcon(false); const spbuRedIcon = makeSpbuIcon(false);
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
// Fetch SPBU // Fetch SPBU
function loadSpbu() { function loadSpbu() {
@@ -75,7 +73,8 @@ function addSpbuMarker(item) {
// Map Click -> Form Add // Map Click -> Form Add
map.on('click', function(e) { 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 // Hanya proses jika mode tambah SPBU aktif
if (window.currentAddMode === 'spbu') { 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 // Inisialisasi FeatureGroups untuk masing-masing layer
const spbuLayer = L.featureGroup().addTo(map); 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 searchInput = document.getElementById('searchInput');
const searchClear = document.getElementById('searchClear'); 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) { 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 { } else {
@@ -148,10 +127,6 @@ const setupLayerToggle = (id, layer) => {
}; };
setupLayerToggle('layerSpbu', spbuLayer); setupLayerToggle('layerSpbu', spbuLayer);
setupLayerToggle('layerJalan', jalanLayer);
setupLayerToggle('layerParsil', parsilLayer);
setupLayerToggle('layerRumahIbadah', rumahIbadahLayer);
setupLayerToggle('layerMiskin', pendudukMiskinLayer);
// --- Sub-layer Toggle (expand/collapse) --- // --- Sub-layer Toggle (expand/collapse) ---
window.toggleSubLayer = function(subId, iconEl) { window.toggleSubLayer = function(subId, iconEl) {
@@ -174,26 +149,6 @@ window.applySubFilter = function(type) {
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 => {
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'); 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) { window.activateAddMode = function(mode) {
if (window.currentAddMode === mode) { if (window.currentAddMode === mode) {
window.deactivateAddMode(); window.deactivateAddMode();
@@ -254,6 +213,12 @@ window.activateAddMode = function(mode) {
}; };
window.deactivateAddMode = function() { window.deactivateAddMode = function() {
if (window.activeDrawHandler) {
try { window.activeDrawHandler.disable(); } catch(e) {}
window.activeDrawHandler = null;
}
window.currentAddMode = null; window.currentAddMode = null;
window.currentDrawMode = null;
document.getElementById('map').style.cursor = ''; document.getElementById('map').style.cursor = '';
}; };
+20
View File
@@ -153,7 +153,17 @@
rightPanelTitle.textContent = '🛣️ Daftar Jalan'; rightPanelTitle.textContent = '🛣️ Daftar Jalan';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin'); 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 = ''; rightPanelActions.innerHTML = '';
}
const items = []; const items = [];
jalanLayer.eachLayer(l => { jalanLayer.eachLayer(l => {
@@ -209,7 +219,17 @@
rightPanelTitle.textContent = '🗺️ Daftar Parsil Tanah'; rightPanelTitle.textContent = '🗺️ Daftar Parsil Tanah';
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin'); 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 = ''; rightPanelActions.innerHTML = '';
}
const items = []; const items = [];
parsilLayer.eachLayer(l => { parsilLayer.eachLayer(l => {
+2 -2
View File
@@ -1,8 +1,8 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; require_once('../poverty/api/auth_helper.php');
requireAdmin(); requireAdmin();
include_once '../db_connect.php'; include_once('db_connect.php');
$data = json_decode(file_get_contents("php://input")); $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 <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; include_once 'db_connect.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
+42 -174
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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 --> <!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <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"> <div class="search-icon">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
</div> </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"> <button class="search-clear" id="searchClear">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
@@ -51,22 +51,50 @@
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i> <i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
</button> </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 --> <!-- Left Sidebar Menu -->
<nav class="left-sidebar" id="leftSidebar" style="display:none;"> <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"> <button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
<span class="sidebar-emoji"></span> <span class="sidebar-emoji"></span>
<span class="sidebar-label">SPBU</span> <span class="sidebar-label">SPBU</span>
</button> </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> </nav>
@@ -101,51 +129,6 @@
</div> </div>
</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 --> <!-- GeoJSON Eksternal -->
@@ -215,55 +198,7 @@
</div> </div>
</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 --> <!-- Floating Profile / Auth Control -->
<div id="authWidget" class="auth-widget"> <div id="authWidget" class="auth-widget">
@@ -294,68 +229,7 @@
</div> </div>
</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 --> <!-- Toast Notification Container -->
<div id="toastContainer"></div> <div id="toastContainer"></div>
@@ -373,16 +247,10 @@
<script src="assets/js/map.js?v=<?= time() ?>"></script> <script src="assets/js/map.js?v=<?= time() ?>"></script>
<!-- Fitur & Komponen --> <!-- Fitur & Komponen -->
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script> <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/geolocation.js?v=<?= time() ?>"></script>
<script src="assets/js/features/geojson.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/features/auth.js?v=<?= time() ?>"></script>
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -1,6 +1,6 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php'; include_once 'db_connect.php';
$query = "SELECT * FROM spbu"; $query = "SELECT * FROM spbu";
$result = $conn->query($query); $result = $conn->query($query);
+1 -3
View File
@@ -1,8 +1,6 @@
<?php <?php
header("Content-Type: application/json; charset=UTF-8"); header("Content-Type: application/json; charset=UTF-8");
require_once '../auth_helper.php'; include_once 'db_connect.php';
requireAdmin();
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input")); $data = json_decode(file_get_contents("php://input"));
+101 -32
View File
@@ -5,9 +5,11 @@
box-sizing: border-box; box-sizing: border-box;
} }
body, html { body,
html {
height: 100%; 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; overflow: hidden;
} }
@@ -131,7 +133,6 @@ body.right-panel-open .custom-layer-panel {
right: 340px; right: 340px;
} }
.custom-layer-panel h3 { .custom-layer-panel h3 {
margin-bottom: 10px; margin-bottom: 10px;
font-size: 16px; font-size: 16px;
@@ -431,12 +432,24 @@ body.right-panel-open .custom-layer-panel {
flex-shrink: 0; flex-shrink: 0;
} }
.card-icon-spbu { background: #f0fdf4; } .card-icon-spbu {
.card-icon-jalan { background: #eff6ff; } background: #f0fdf4;
.card-icon-parsil { background: #fefce8; } }
.card-icon-ibadah { background: #fff7ed; } .card-icon-jalan {
.card-icon-miskin-out { background: #f0fdf4; } background: #eff6ff;
.card-icon-miskin-in { background: #fef2f2; } }
.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 { .data-card-info {
flex: 1; flex: 1;
@@ -470,11 +483,26 @@ body.right-panel-open .custom-layer-panel {
display: inline-block; display: inline-block;
} }
.badge-green { background: #dcfce7; color: #16a34a; } .badge-green {
.badge-red { background: #fee2e2; color: #dc2626; } background: #dcfce7;
.badge-blue { background: #dbeafe; color: #2563eb; } color: #16a34a;
.badge-orange { background: #ffedd5; color: #ea580c; } }
.badge-yellow { background: #fef9c3; color: #ca8a04; } .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 { .data-card-actions {
display: flex; display: flex;
@@ -520,7 +548,9 @@ body.right-panel-open .custom-layer-panel {
.data-card-expand { .data-card-expand {
overflow: hidden; overflow: hidden;
max-height: 0; 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; border-top: 0px solid #f3f4f6;
} }
@@ -623,6 +653,8 @@ body.right-panel-open .custom-layer-panel {
} }
.popup-form input[type="text"], .popup-form input[type="text"],
.form-group input[type="password"],
.popup-form input[type="password"],
.popup-form input[type="number"] { .popup-form input[type="number"] {
width: 100%; width: 100%;
padding: 6px; padding: 6px;
@@ -720,6 +752,7 @@ body.right-panel-open .custom-layer-panel {
.modal-body input[type="text"], .modal-body input[type="text"],
.modal-body input[type="number"], .modal-body input[type="number"],
.modal-body input[type="password"],
.modal-body input[type="date"], .modal-body input[type="date"],
.modal-body select, .modal-body select,
.modal-body textarea { .modal-body textarea {
@@ -862,16 +895,30 @@ body.right-panel-open .custom-layer-panel {
} }
@keyframes markerPop { @keyframes markerPop {
0% { transform: rotate(-45deg) scale(0); } 0% {
100% { transform: rotate(-45deg) scale(1); } transform: rotate(-45deg) scale(0);
}
100% {
transform: rotate(-45deg) scale(1);
}
} }
/* Warna bubble per tipe */ /* Warna bubble per tipe */
.emoji-marker .bubble.spbu-24 { background: linear-gradient(135deg, #22c55e, #16a34a); } .emoji-marker .bubble.spbu-24 {
.emoji-marker .bubble.spbu-not24 { background: linear-gradient(135deg, #ef4444, #dc2626); } background: linear-gradient(135deg, #22c55e, #16a34a);
.emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); } }
.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); } .emoji-marker .bubble.spbu-not24 {
.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); } 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 ===== */ /* ===== Toast Notification ===== */
#toastContainer { #toastContainer {
@@ -908,21 +955,44 @@ body.right-panel-open .custom-layer-panel {
animation: toastSlideOut 0.3s ease forwards; animation: toastSlideOut 0.3s ease forwards;
} }
.toast-success { background: linear-gradient(135deg, #22c55e, #16a34a); } .toast-success {
.toast-error { background: linear-gradient(135deg, #ef4444, #dc2626); } background: linear-gradient(135deg, #22c55e, #16a34a);
.toast-info { background: linear-gradient(135deg, #6366f1, #4f46e5); } }
.toast-warning { background: linear-gradient(135deg, #f59e0b, #d97706); } .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 { @keyframes toastSlideIn {
0% { opacity: 0; transform: translateY(20px) scale(0.9); } 0% {
100% { opacity: 1; transform: translateY(0) scale(1); } opacity: 0;
transform: translateY(20px) scale(0.9);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
} }
@keyframes toastSlideOut { @keyframes toastSlideOut {
0% { opacity: 1; transform: translateY(0) scale(1); } 0% {
100% { opacity: 0; transform: translateY(10px) scale(0.95); } opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(10px) scale(0.95);
}
} }
/* Auth Widget Styling */ /* Auth Widget Styling */
@@ -1023,4 +1093,3 @@ body.right-panel-open .auth-widget {
padding: 10px 12px; padding: 10px 12px;
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;
} }