Initial commit: Upload project WebGIS dengan tema Merah Putih

This commit is contained in:
2026-06-05 20:37:25 +07:00
commit 5857b15aec
28 changed files with 6445 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
# WebGIS - Sistem Analisis Geospasial Pengentasan Kemiskinan
Sebuah aplikasi WebGIS interaktif yang dirancang untuk melakukan analisis spasial terkait sebaran penduduk miskin dan infrastruktur pendukung (seperti fasilitas publik, jalan, parsil tanah, dll) di suatu wilayah. Aplikasi ini dibangun dengan tema visual Merah Putih yang modern.
## Fitur Utama
- **Peta Interaktif**: Visualisasi peta secara *real-time* menggunakan Leaflet.js.
- **Manajemen Data Geospasial**:
- 📍 Tambah/Edit Titik (Point)
- 🛣️ Gambar Jaringan Jalan (Polyline)
- ⬠ Gambar Area/Parsil Tanah (Polygon)
- 🕌 Manajemen Data Fasilitas Publik (dengan radius jangkauan otomatis)
- 👤 Pendataan Penduduk Miskin
- **Analisis Spasial (Radius)**: Secara otomatis mendeteksi apakah seorang Penduduk Miskin berada **di dalam** atau **di luar** jangkauan radius bantuan (berbasis lokasi fasilitas publik terdekat).
- **Dashboard Statistik**: Rangkuman total data penduduk, fasilitas publik, dan persentase cakupan penanganan.
- **Pencarian Cepat (Search)**: Fitur untuk mencari data spesifik (berdasarkan nama, NIK, dll) langsung dari *sidebar*.
## Teknologi yang Digunakan
- **Front-end**: HTML5, CSS3 (Native / Tema Merah Putih), JavaScript (ES6+).
- **Library Peta**: [Leaflet.js](https://leafletjs.com/)
- **Back-end**: PHP
- **Database**: MySQL / MariaDB (via XAMPP)
## Panduan Instalasi (Localhost)
Ikuti langkah-langkah berikut untuk menjalankan aplikasi ini di komputer Anda menggunakan XAMPP:
### 1. Persiapan Environment
Pastikan Anda sudah menginstal aplikasi server lokal, seperti **[XAMPP](https://www.apachefriends.org/index.html)**.
### 2. Kloning / Salin File
1. Unduh atau *clone* repository ini:
```bash
git clone https://github.com/USERNAME_ANDA/webgis-pengentasan-kemiskinan.git
```
2. Pindahkan folder project (misalnya bernama `webgis`) ke dalam folder `htdocs` pada instalasi XAMPP Anda.
- Contoh direktori XAMPP di Windows: `C:\xampp\htdocs\webgis`
### 3. Konfigurasi Database
1. Buka aplikasi **XAMPP Control Panel**.
2. Jalankan modul **Apache** dan **MySQL** (Klik tombol *Start*).
3. Buka browser dan akses **phpMyAdmin**: `http://localhost/phpmyadmin`
4. Buat sebuah database baru dengan nama: **`webgis`**
5. Impor (*Import*) file database yang disertakan:
- Klik tab **Import** di phpMyAdmin.
- Pilih file **`webgis.sql`** yang terdapat di dalam folder project ini.
- Klik **Go** / **Kirim** di bagian bawah halaman.
### 4. Konfigurasi Koneksi (Opsional)
Jika Anda menggunakan *password* untuk root MySQL Anda, Anda perlu menyesuaikan file koneksinya.
1. Buka file `koneksi.php` dengan *text editor*.
2. Ubah konfigurasi `$user` dan `$pass` sesuai dengan pengaturan database Anda (secara *default* di XAMPP user: `root`, pass: *kosong*).
### 5. Jalankan Aplikasi
Buka web browser favorit Anda dan akses URL berikut:
```text
http://localhost/webgis
```
## Cara Penggunaan
- **Menambah Data**: Buka *Sidebar* (ikon di sebelah kiri), pilih salah satu *"Alat Peta"* (Titik, Jalan, Polygon, dll), lalu klik di area peta untuk menentukan lokasi. Sebuah *form pop-up* akan muncul untuk mengisi detailnya.
- **Melihat Data**: Klik pada titik, jalan, atau area yang sudah ada di peta untuk melihat detail informasinya.
- **Menghapus Data**: Pada *pop-up* informasi data di peta, klik tombol "Hapus".
## Kontribusi
Jika Anda ingin mengembangkan atau berkontribusi pada aplikasi ini, silakan buat *Pull Request* atau *Fork* repositori ini.
---
*Dibuat untuk keperluan Analisis Geospasial.*
## Cara Upload ke GitHub
Jika Anda ingin mengunggah (upload) project ini ke GitHub Anda sendiri, ikuti langkah berikut:
1. **Buat Repository di GitHub**: Buka github.com, login, lalu buat *New Repository* (beri nama misalnya \webgis-fasilitas-publik\). Biarkan kosong tanpa *README* dari GitHub.
2. **Buka Terminal/Command Prompt**: Arahkan ke folder project ini di komputer Anda (\C:\xampp\htdocs\webgis\).
3. **Inisialisasi Git**: Jalankan perintah \git init\.
4. **Tambahkan File**: Jalankan \git add .\
5. **Simpan Perubahan**: Jalankan \git commit -m "Initial commit webgis"\
6. **Hubungkan ke GitHub**: Copy perintah \git remote add origin https://github.com/USERNAME_ANDA/NAMA_REPO_ANDA.git\ dari halaman GitHub Anda dan jalankan di terminal.
7. **Upload (Push)**: Jalankan \git branch -M main\ lalu \git push -u origin main\.
+20
View File
@@ -0,0 +1,20 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_GET['id'] ?? 0);
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
try {
$stmt = $pdo->prepare("SELECT * FROM fasilitas_publik WHERE id=?");
$stmt->execute([$id]);
$data = $stmt->fetch();
if ($data) {
echo json_encode(['status'=>'success','data'=>$data]);
} else {
echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan.']);
}
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_GET['id'] ?? 0);
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
try {
$stmt = $pdo->prepare(
"SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm
LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id
WHERE pm.id=?"
);
$stmt->execute([$id]);
$data = $stmt->fetch();
if ($data) {
echo json_encode(['status'=>'success','data'=>$data]);
} else {
echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan.']);
}
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+23
View File
@@ -0,0 +1,23 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
try {
$totalFP = $pdo->query("SELECT COUNT(*) FROM fasilitas_publik")->fetchColumn();
$totalPM = $pdo->query("SELECT COUNT(*) FROM penduduk_miskin")->fetchColumn();
$ditangani= $pdo->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_jangkauan='Dalam Jangkauan'")->fetchColumn();
$belum = $totalPM - $ditangani;
$persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
echo json_encode([
'status' => 'success',
'total_ri' => (int)$totalFP,
'total_pm' => (int)$totalPM,
'ditangani' => (int)$ditangani,
'belum_ditangani'=> (int)$belum,
'persentase' => $persen,
]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+780
View File
@@ -0,0 +1,780 @@
/* ============================================================
WebGIS - style.css
Design: Red & White Theme (Merah Putih)
Font: Plus Jakarta Sans + JetBrains Mono
============================================================ */
/* --- Reset & Variables ---------------------------------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--sidebar-w: 300px;
--sidebar-bg: #ffffff;
--sidebar-border: rgba(0,0,0,0.08);
--sidebar-text: #334155;
--sidebar-text-muted: #64748b;
--sidebar-hover: #f1f5f9;
--accent: #e11d48; /* Merah cantik */
--accent-glow: rgba(225, 29, 72, 0.25);
--green: #16a34a;
--red: #e11d48;
--amber: #d97706;
--surface: #ffffff;
--surface-2: #f8fafc;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--shadow-md: 0 4px 24px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 40px rgba(0,0,0,0.12);
--font: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
html, body { height: 100%; overflow: hidden; }
body {
font-family: var(--font);
font-size: 14px;
background: #f1f5f9;
color: var(--sidebar-text);
}
/* --- Scrollbar ------------------------------------------ */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 4px; }
/* ============================================================
SIDEBAR
============================================================ */
.sidebar {
position: fixed;
left: 0; top: 0; bottom: 0;
width: var(--sidebar-w);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-right: 1px solid var(--sidebar-border);
display: flex;
flex-direction: column;
z-index: 900;
overflow-y: auto;
overflow-x: hidden;
transition: transform var(--transition), width var(--transition);
box-shadow: 2px 0 20px rgba(0,0,0,0.03);
}
.sidebar.collapsed {
transform: translateX(calc(-1 * var(--sidebar-w)));
}
/* Sidebar Header */
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 16px 16px;
border-bottom: 1px solid var(--sidebar-border);
flex-shrink: 0;
}
.sidebar-logo { display: flex; align-items: center; gap: 10px; }
.logo-icon {
width: 38px; height: 38px;
background: linear-gradient(135deg, var(--accent), #be123c);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
color: white;
box-shadow: 0 4px 12px var(--accent-glow);
flex-shrink: 0;
}
.logo-title {
display: block;
font-size: 16px; font-weight: 800;
color: #0f172a; letter-spacing: -0.3px;
}
.logo-sub {
display: block;
font-size: 10.5px; font-weight: 500;
color: var(--sidebar-text-muted);
letter-spacing: 0.5px; text-transform: uppercase;
}
.sidebar-toggle {
width: 30px; height: 30px;
border: 1px solid var(--sidebar-border);
border-radius: 8px; background: transparent;
color: var(--sidebar-text-muted); cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all var(--transition);
}
.sidebar-toggle:hover { background: var(--sidebar-hover); color: #0f172a; border-color: rgba(0,0,0,0.15); }
/* Sidebar Section */
.sidebar-section {
padding: 16px;
border-bottom: 1px solid var(--sidebar-border);
}
.section-label {
font-size: 10px; font-weight: 700;
color: var(--sidebar-text-muted);
letter-spacing: 1.2px; text-transform: uppercase;
margin-bottom: 12px;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 14px;
}
.stat-card {
background: var(--surface-2);
border-radius: var(--radius-md);
padding: 12px 10px;
display: flex; align-items: center; gap: 8px;
border: 1px solid var(--sidebar-border);
transition: transform var(--transition);
}
.stat-card:hover { transform: translateY(-1px); border-color: rgba(0,0,0,0.15); }
.stat-icon { font-size: 18px; line-height: 1; }
.stat-value {
font-size: 22px; font-weight: 800;
color: #0f172a; line-height: 1;
font-variant-numeric: tabular-nums;
}
.stat-label { font-size: 10px; color: var(--sidebar-text-muted); margin-top: 2px; font-weight: 600; }
.stat-blue { border-left: 3px solid #3b82f6; }
.stat-amber { border-left: 3px solid var(--amber); }
.stat-green { border-left: 3px solid var(--green); }
.stat-red { border-left: 3px solid var(--red); }
/* Coverage Bar */
.coverage-bar-wrap { }
.coverage-bar-header {
display: flex; justify-content: space-between;
font-size: 11.5px; color: var(--sidebar-text);
margin-bottom: 6px; font-weight: 600;
}
.coverage-bar-header strong { color: var(--accent); font-size: 13px; }
.coverage-bar-track {
height: 6px; background: rgba(0,0,0,0.06);
border-radius: 99px; overflow: hidden;
}
.coverage-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--red), #fb7185);
border-radius: 99px;
transition: width 0.8s cubic-bezier(0.4,0,0.2,1);
}
/* Tool Grid */
.tool-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-bottom: 10px;
}
.tool-btn {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 4px; padding: 10px 6px;
background: var(--surface);
border: 1px solid var(--sidebar-border);
border-radius: var(--radius-sm);
color: var(--sidebar-text);
cursor: pointer; font-family: var(--font);
font-size: 10px; font-weight: 600;
transition: all var(--transition);
letter-spacing: 0.3px;
}
.tool-btn:hover { background: var(--surface-2); border-color: var(--accent); color: var(--accent); }
.tool-btn.active {
background: var(--accent);
border-color: var(--accent);
color: white;
box-shadow: 0 2px 12px var(--accent-glow);
}
.tool-icon { font-size: 18px; }
.help-text {
font-size: 11px; color: var(--sidebar-text-muted);
line-height: 1.5; padding: 8px 10px;
background: var(--surface-2);
border-radius: var(--radius-sm);
border: 1px solid var(--sidebar-border);
}
/* Layer List */
.layer-list { display: flex; flex-direction: column; gap: 4px; }
.layer-item {
display: flex; align-items: center; gap: 8px;
padding: 7px 8px;
border-radius: var(--radius-sm);
cursor: pointer; transition: background var(--transition);
font-size: 12.5px; color: #0f172a; font-weight: 500;
user-select: none;
}
.layer-item:hover { background: var(--sidebar-hover); }
.layer-item input[type="checkbox"] { width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); }
.layer-dot {
width: 10px; height: 10px;
border-radius: 50%; flex-shrink: 0;
}
/* Legend */
.legend-list { display: flex; flex-direction: column; gap: 6px; }
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--sidebar-text); font-weight: 500; }
.legend-marker {
width: 12px; height: 12px;
background: #e11d48; border-radius: 50%;
border: 2px solid white;
box-shadow: 0 0 0 2px rgba(225,29,72,0.3);
flex-shrink: 0;
}
.legend-circle {
width: 14px; height: 14px;
border-radius: 50%; flex-shrink: 0;
}
.legend-line {
width: 22px; height: 3px;
border-radius: 2px; flex-shrink: 0;
}
/* Sidebar Footer */
.sidebar-footer {
margin-top: auto;
padding: 12px 16px;
border-top: 1px solid var(--sidebar-border);
font-size: 10.5px; color: var(--sidebar-text-muted);
font-weight: 500;
}
/* Open button (collapsed state) */
.sidebar-open-btn {
position: fixed;
left: 12px; top: 50%;
transform: translateY(-50%);
z-index: 901;
width: 36px; height: 36px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 10px;
color: var(--sidebar-text);
cursor: pointer;
display: flex; align-items: center; justify-content: center;
box-shadow: var(--shadow-md);
transition: all var(--transition);
}
.sidebar-open-btn:hover { background: var(--surface-2); color: #0f172a; border-color: rgba(0,0,0,0.15); }
/* ============================================================
MAP CONTAINER
============================================================ */
#mapContainer {
position: fixed;
left: var(--sidebar-w);
top: 0; right: 0; bottom: 0;
transition: left var(--transition);
}
#mapContainer.full-width { left: 0; }
#map { width: 100%; height: 100%; background: #e5e5e5; }
/* Loading overlay */
.map-loading {
position: absolute; inset: 0;
background: rgba(255,255,255,0.85);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 12px; z-index: 500;
color: #0f172a; font-size: 14px; font-weight: 600;
backdrop-filter: blur(4px);
transition: opacity 0.4s;
}
.map-loading.hidden { opacity: 0; pointer-events: none; }
.loading-spinner {
width: 38px; height: 38px;
border: 3px solid rgba(0,0,0,0.1);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ============================================================
TOAST NOTIFICATIONS
============================================================ */
.toast-container {
position: absolute;
bottom: 24px; right: 16px;
display: flex; flex-direction: column; gap: 8px;
z-index: 800; pointer-events: none;
}
.toast {
display: flex; align-items: center; gap: 10px;
padding: 12px 16px;
border-radius: var(--radius-md);
font-size: 13px; font-weight: 600;
color: white;
box-shadow: var(--shadow-md);
animation: toastIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
pointer-events: all; max-width: 320px;
backdrop-filter: blur(8px);
}
.toast.success { background: rgba(22,163,74,0.95); }
.toast.error { background: rgba(225,29,72,0.95); }
.toast.info { background: rgba(59,130,246,0.95); }
.toast.warning { background: rgba(217,119,6,0.95); }
.toast.fade-out { animation: toastOut 0.3s forwards; }
@keyframes toastIn {
from { transform: translateX(60px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes toastOut {
to { transform: translateX(60px); opacity: 0; }
}
/* ============================================================
MODAL
============================================================ */
.modal-backdrop {
position: fixed; inset: 0;
background: rgba(0,0,0,0.4);
z-index: 1000;
backdrop-filter: blur(3px);
transition: opacity var(--transition);
}
.modal-backdrop.hidden { display: none; }
.modal {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%) scale(0.96);
width: 480px; max-width: calc(100vw - 32px);
max-height: calc(100vh - 60px);
background: #ffffff;
border: 1px solid rgba(0,0,0,0.05);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: 1100;
display: flex; flex-direction: column;
overflow: hidden;
animation: modalIn 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.modal.hidden { display: none; }
.modal.modal-sm { width: 380px; }
@keyframes modalIn {
from { opacity: 0; transform: translate(-50%, -52%) scale(0.93); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
.modal-header {
padding: 18px 20px 14px;
border-bottom: 1px solid rgba(0,0,0,0.06);
display: flex; align-items: center;
justify-content: space-between; gap: 12px;
flex-shrink: 0;
background: #f8fafc;
}
.modal-title-wrap { display: flex; align-items: center; gap: 12px; }
.modal-icon {
width: 40px; height: 40px;
border-radius: var(--radius-sm);
display: flex; align-items: center;
justify-content: center; font-size: 20px;
flex-shrink: 0;
background: #ffe4e6 !important;
color: var(--accent) !important;
}
.modal-title { font-size: 16px; font-weight: 800; color: #0f172a; }
.modal-subtitle { font-size: 11px; color: var(--sidebar-text-muted); margin-top: 2px; font-weight: 500; }
.modal-close {
width: 30px; height: 30px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px; background: #ffffff;
color: var(--sidebar-text-muted); cursor: pointer;
font-size: 14px; transition: all var(--transition);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.modal-close:hover { background: #ffe4e6; border-color: var(--accent); color: var(--accent); }
.modal-body {
padding: 18px 20px;
overflow-y: auto; flex: 1;
}
.modal-footer {
padding: 14px 20px;
border-top: 1px solid rgba(0,0,0,0.06);
display: flex; gap: 8px; justify-content: flex-end;
flex-shrink: 0;
background: #f8fafc;
}
/* --- Form Elements ------------------------------------ */
.form-row {
display: grid; grid-template-columns: 1fr 1fr;
gap: 12px;
}
.form-group { margin-bottom: 14px; }
.form-group:last-child { margin-bottom: 0; }
.form-label {
display: block;
font-size: 11.5px; font-weight: 700;
color: var(--sidebar-text-muted);
letter-spacing: 0.5px; text-transform: uppercase;
margin-bottom: 6px;
}
.form-label small { font-size: 10px; text-transform: none; font-weight: 500; color: #94a3b8; }
.req { color: var(--accent); }
.form-input {
width: 100%;
background: #ffffff;
border: 1px solid rgba(0,0,0,0.15);
border-radius: var(--radius-sm);
padding: 9px 12px;
font-size: 13px;
color: #0f172a;
font-family: var(--font);
font-weight: 500;
outline: none;
transition: border-color var(--transition), box-shadow var(--transition);
resize: none;
}
.form-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.form-input::placeholder { color: #94a3b8; font-weight: 400; }
.form-input[readonly] { background: #f1f5f9; cursor: default; color: #64748b; }
.form-input.mono { font-family: var(--font-mono); font-size: 12px; }
.form-input option { background: #ffffff; }
/* Radio Tabs */
.radio-tabs { display: flex; gap: 6px; }
.radio-tab {
flex: 1; position: relative;
}
.radio-tab input { position: absolute; opacity: 0; width: 0; height: 0; }
.radio-tab span {
display: block; text-align: center;
padding: 8px;
background: #ffffff;
border: 1px solid rgba(0,0,0,0.15);
border-radius: var(--radius-sm);
font-size: 12.5px; font-weight: 600;
cursor: pointer; color: var(--sidebar-text);
transition: all var(--transition);
}
.radio-tab input:checked + span {
background: var(--accent);
border-color: var(--accent);
color: white;
box-shadow: 0 2px 8px var(--accent-glow);
}
/* Range */
.range-wrap {
display: flex; align-items: center; gap: 8px;
}
.range-min, .range-max {
font-size: 10px; color: var(--sidebar-text-muted);
font-family: var(--font-mono); flex-shrink: 0; font-weight: 600;
}
.range-input {
flex: 1; height: 4px; cursor: pointer;
accent-color: var(--accent);
-webkit-appearance: none;
background: rgba(0,0,0,0.1);
border-radius: 99px;
outline: none;
}
.range-input::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px; height: 16px;
border-radius: 50%; background: var(--accent);
box-shadow: 0 2px 6px var(--accent-glow);
cursor: pointer; border: 2px solid white;
}
/* Geocode wrap */
.geocode-wrap { position: relative; }
.geocode-status {
position: absolute; top: 8px; right: 10px;
font-size: 11px; color: var(--sidebar-text-muted); font-weight: 500;
}
/* Status Jangkauan Card */
.status-jangkauan-card {
background: #f8fafc;
border: 1px solid rgba(0,0,0,0.06);
border-radius: var(--radius-md);
padding: 12px 14px;
margin-bottom: 14px;
}
.sj-label { font-size: 10px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; color: var(--sidebar-text-muted); margin-bottom: 4px; }
.sj-value { font-size: 15px; font-weight: 800; color: #0f172a; }
.sj-value.dalam { color: var(--green); }
.sj-value.luar { color: var(--accent); }
.sj-ditangani { font-size: 11.5px; color: var(--sidebar-text-muted); margin-top: 3px; font-weight: 500; }
/* --- Buttons ------------------------------------------ */
.btn {
padding: 9px 20px;
border-radius: var(--radius-sm);
border: none; cursor: pointer;
font-family: var(--font);
font-size: 13px; font-weight: 700;
transition: all var(--transition);
display: inline-flex; align-items: center; gap: 6px;
justify-content: center;
}
.btn-primary {
background: var(--accent);
color: white;
box-shadow: 0 2px 12px var(--accent-glow);
}
.btn-primary:hover { background: #be123c; transform: translateY(-1px); box-shadow: 0 4px 16px var(--accent-glow); }
.btn-primary:active { transform: none; }
.btn-ghost {
background: #ffffff;
border: 1px solid rgba(0,0,0,0.15);
color: var(--sidebar-text);
}
.btn-ghost:hover { background: #f1f5f9; color: #0f172a; border-color: rgba(0,0,0,0.25); }
.btn-danger { background: var(--accent); color: white; }
.btn-danger:hover { background: #be123c; }
.btn-sm { padding: 5px 12px; font-size: 11.5px; }
/* --- Hidden ------------------------------------------- */
.hidden { display: none !important; }
/* ============================================================
LEAFLET CUSTOMIZATIONS
============================================================ */
/* Popup */
.leaflet-popup-content-wrapper {
background: #ffffff !important;
border: 1px solid rgba(0,0,0,0.08) !important;
border-radius: var(--radius-md) !important;
box-shadow: var(--shadow-md) !important;
color: #334155 !important;
padding: 0 !important;
overflow: hidden;
}
.leaflet-popup-tip { background: #ffffff !important; border-top: 1px solid rgba(0,0,0,0.08); border-left: 1px solid rgba(0,0,0,0.08); }
.leaflet-popup-content { margin: 0 !important; }
.popup-card {
padding: 14px 16px;
min-width: 200px;
}
.popup-title {
font-size: 14px; font-weight: 800;
color: #0f172a; margin-bottom: 8px;
display: flex; align-items: center; gap: 6px;
}
.popup-badge {
font-size: 10px; font-weight: 700;
padding: 2px 7px; border-radius: 99px;
text-transform: uppercase; letter-spacing: 0.5px;
}
.badge-dalam { background: #dcfce7; color: #166534; }
.badge-luar { background: #ffe4e6; color: #9f1239; }
.badge-fp { background: #f1f5f9; color: #475569; }
.popup-row {
display: flex; gap: 6px;
font-size: 12px; margin-bottom: 4px;
color: #64748b; font-weight: 500;
}
.popup-row strong { color: #0f172a; min-width: 80px; font-weight: 700; }
.popup-actions {
display: flex; gap: 6px;
padding: 10px 16px;
border-top: 1px solid rgba(0,0,0,0.06);
background: #f8fafc;
}
.popup-btn {
flex: 1; padding: 6px;
border: 1px solid rgba(0,0,0,0.12);
border-radius: 6px;
background: #ffffff; cursor: pointer;
font-size: 11px; font-weight: 700;
color: var(--sidebar-text); font-family: var(--font);
transition: all 0.15s;
text-align: center;
}
.popup-btn:hover { background: #f1f5f9; color: #0f172a; }
.popup-btn.del:hover { background: #ffe4e6; border-color: var(--accent); color: var(--accent); }
/* Zoom / Attribution */
.leaflet-control-zoom a {
background: #ffffff !important;
border-color: rgba(0,0,0,0.1) !important;
color: #0f172a !important;
font-weight: 700 !important;
}
.leaflet-control-zoom a:hover { background: #f8fafc !important; color: var(--accent) !important; }
.leaflet-control-attribution { background: rgba(255,255,255,0.9) !important; color: #64748b !important; font-size: 10px !important; font-weight: 500 !important; }
.leaflet-control-attribution a { color: var(--accent) !important; font-weight: 600 !important; }
/* Scale */
.leaflet-control-scale-line {
background: rgba(255,255,255,0.9) !important;
border-color: rgba(0,0,0,0.3) !important;
color: #0f172a !important;
font-size: 10px !important;
font-weight: 600 !important;
}
/* ============================================================
SEARCH BOX
============================================================ */
.sidebar-search {
padding: 16px 16px 0;
position: relative;
z-index: 50;
}
.search-input-wrap {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: #94a3b8;
}
#searchInput {
width: 100%;
background: #ffffff;
border: 1px solid rgba(0,0,0,0.15);
border-radius: var(--radius-sm);
padding: 10px 12px 10px 36px;
color: #0f172a;
font-family: var(--font);
font-size: 12.5px;
font-weight: 500;
transition: all var(--transition);
}
#searchInput:focus {
border-color: var(--accent);
outline: none;
box-shadow: 0 0 0 3px var(--accent-glow);
}
#searchInput::placeholder {
color: #94a3b8; font-weight: 400;
}
.search-results {
position: absolute;
top: calc(100% + 4px);
left: 16px; right: 16px;
background: #ffffff;
border: 1px solid rgba(0,0,0,0.1);
border-radius: var(--radius-md);
max-height: 280px;
overflow-y: auto;
box-shadow: var(--shadow-lg);
display: flex; flex-direction: column;
}
.search-results.hidden {
display: none !important;
}
.search-item {
padding: 10px 14px;
border-bottom: 1px solid rgba(0,0,0,0.05);
cursor: pointer;
display: flex; flex-direction: column; gap: 3px;
transition: background var(--transition);
}
.search-item:last-child { border-bottom: none; }
.search-item:hover { background: #f8fafc; }
.search-item-title { font-size: 13px; font-weight: 800; color: #0f172a; }
.search-item-type { font-size: 10.5px; color: var(--accent); font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase; }
.search-item-empty { padding: 14px; text-align: center; font-size: 12px; color: var(--sidebar-text-muted); font-weight: 500; }
/* ============================================================
RESPONSIVE
============================================================ */
@media (max-width: 900px) {
:root { --sidebar-w: 260px; }
}
@media (max-width: 680px) {
.sidebar {
transform: translateX(calc(-1 * var(--sidebar-w)));
width: 85%;
max-width: 300px;
z-index: 1050;
}
.sidebar.mobile-open {
transform: translateX(0);
box-shadow: 10px 0 25px rgba(0,0,0,0.2);
}
#mapContainer {
left: 0 !important;
}
.sidebar-open-btn {
display: flex !important;
top: 20px;
z-index: 1000;
}
.modal {
width: calc(100vw - 32px);
max-height: calc(100vh - 40px);
}
.form-row { grid-template-columns: 1fr; }
.toast-container {
bottom: 24px; left: 16px; right: 16px;
align-items: center;
}
.toast {
width: 100%; max-width: 100%;
justify-content: center;
}
}
/* --- Utility ------------------------------------------ */
.text-center { text-align: center; }
.mt-8 { margin-top: 8px; }
.mb-8 { margin-bottom: 8px; }
+1358
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
try {
$pdo->prepare("DELETE FROM fasilitas_publik WHERE id=?")->execute([$id]);
echo json_encode(['status'=>'success']);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+40
View File
@@ -0,0 +1,40 @@
<?php
// fasilitas_publik/simpan.php
include '../koneksi.php';
header('Content-Type: application/json');
$nama = trim($_POST['nama'] ?? '');
$jenis = trim($_POST['jenis'] ?? '');
$alamat = trim($_POST['alamat'] ?? '');
$radius = (int)($_POST['radius'] ?? 300);
$kontak = trim($_POST['kontak'] ?? '');
$latitude = trim($_POST['latitude'] ?? '');
$longitude = trim($_POST['longitude'] ?? '');
if (!$nama || !$jenis || !$latitude || !$longitude) {
echo json_encode(['status'=>'error','message'=>'Nama, Jenis, Latitude, dan Longitude wajib diisi.']);
exit;
}
if (!is_numeric($latitude) || !is_numeric($longitude) || !is_numeric($radius)) {
echo json_encode(['status'=>'error','message'=>'Koordinat dan radius harus berupa angka.']);
exit;
}
try {
$stmt = $pdo->prepare(
"INSERT INTO fasilitas_publik (nama, jenis, alamat, radius, kontak, latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([$nama, $jenis, $alamat, $radius, $kontak, $latitude, $longitude]);
$id = $pdo->lastInsertId();
$stmtSelect = $pdo->prepare("SELECT * FROM fasilitas_publik WHERE id = ?");
$stmtSelect->execute([$id]);
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
echo json_encode(['status'=>'success','data'=>$data]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$e->getMessage()]);
}
?>
+33
View File
@@ -0,0 +1,33 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? 0);
$nama = trim($_POST['nama'] ?? '');
$jenis = trim($_POST['jenis'] ?? '');
$alamat = trim($_POST['alamat'] ?? '');
$radius = (int)($_POST['radius'] ?? 300);
$kontak = trim($_POST['kontak'] ?? '');
$latitude = trim($_POST['latitude'] ?? '');
$longitude = trim($_POST['longitude'] ?? '');
if (!$id || !$nama || !$jenis || !$latitude || !$longitude) {
echo json_encode(['status'=>'error','message'=>'Data tidak lengkap.']);
exit;
}
try {
$stmt = $pdo->prepare(
"UPDATE fasilitas_publik SET nama=?, jenis=?, alamat=?, radius=?, kontak=?, latitude=?, longitude=? WHERE id=?"
);
$stmt->execute([$nama, $jenis, $alamat, $radius, $kontak, $latitude, $longitude, $id]);
$stmtSelect = $pdo->prepare("SELECT * FROM fasilitas_publik WHERE id=?");
$stmtSelect->execute([$id]);
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
echo json_encode(['status'=>'success','data'=>$data]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+36
View File
@@ -0,0 +1,36 @@
import re
# Read file
with open('assets/js/app.js', 'r', encoding='utf-8') as f:
content = f.read()
# Map of replacements
replacements = {
# Icons in toasts and popups
'âœ"': '',
'✕': '',
'ℹ': '',
'âš ': '',
'ðŸ"': '📍',
'🛣ï¸': '🛣️',
'⬠': '',
'ðŸ'¤': '👥',
'ðŸ—'': '🗑',
'🕌': '🏢',
'ðŸ\"¡': '📍',
'â€"': '',
'’': "'",
'©': '©',
'²': '²',
'â³': '',
}
# Apply replacements
for old, new in replacements.items():
content = content.replace(old, new)
# Write back
with open('assets/js/app.js', 'w', encoding='utf-8') as f:
f.write(content)
print("✓ File app.js berhasil diperbaiki!")
+32
View File
@@ -0,0 +1,32 @@
<?php
$file = 'assets/js/app.js';
$content = file_get_contents($file);
// Map of corrupted byte sequences to correct emojis
$replacements = [
// Already done - Toast icons
chr(0xC3).chr(0xA2).chr(0xC5).chr(0x93).chr(0xE2).chr(0x80).chr(0x9C) => '✓',
chr(0xC3).chr(0xA2).chr(0xC5).chr(0x93).chr(0xE2).chr(0x80).chr(0xA2) => '✕',
// Popup icons that still need fixing
chr(0xC3).chr(0xB0).chr(0xC5).chr(0xB8).chr(0xE2).chr(0x80).chr(0x9C).chr(0xC2).chr(0x8D) => '📍', // ðŸ"
chr(0xC3).chr(0xB0).chr(0xC5).chr(0xB8).chr(0xE2).chr(0x80).chr(0xBA).chr(0xC2).chr(0xA3).chr(0xC3).chr(0xAF).chr(0xC2).chr(0xB8).chr(0xC2).chr(0x8F) => '🛣️', // 🛣ï¸
chr(0xC3).chr(0xA2).chr(0xE2).chr(0x80).chr(0xAC).chr(0xC2).chr(0xA0) => '⬜', // â¬
chr(0xC3).chr(0xB0).chr(0xC5).chr(0xB8).chr(0xE2).chr(0x80).chr(0x95).chr(0xC2).chr(0x8C) => '🏢', // 🕌
chr(0xC3).chr(0xB0).chr(0xC5).chr(0xB8).chr(0xE2).chr(0x80).chr(0x91).chr(0xC2).chr(0xA4) => '👥', // ðŸ'¤
chr(0xC3).chr(0xB0).chr(0xC5).chr(0xB8).chr(0xE2).chr(0x80).chr(0x97).chr(0xC2).chr(0x91) => '🗑️', // ðŸ—'
chr(0xC3).chr(0xA2).chr(0xC2).chr(0x9A).chr(0xA0) => '⚠', // âš
chr(0xC3).chr(0xA2).chr(0xE2).chr(0x80).chr(0x9E).chr(0xC2).chr(0xB9) => 'ⓘ', // ℹ
];
foreach ($replacements as $old => $new) {
$count = 0;
$content = str_replace($old, $new, $content, $count);
if ($count > 0) {
echo "✓ Replaced → '$new' ($count times)\n";
}
}
file_put_contents($file, $content);
echo "\n✓✓ File app.js sudah diperbaiki sepenuhnya!\n";
?>
+576
View File
@@ -0,0 +1,576 @@
<?php
// ============================================================
// index.php Halaman Utama WebGIS
// ============================================================
include 'koneksi.php';
// Ambil semua data
$stmt = $pdo->query("SELECT * FROM points ORDER BY id DESC");
$points = $stmt->fetchAll();
$stmt = $pdo->query("SELECT * FROM jalan ORDER BY id DESC");
$jalan = $stmt->fetchAll();
$stmt = $pdo->query("SELECT * FROM polygon ORDER BY id DESC");
$polygons = $stmt->fetchAll();
$stmt = $pdo->query("SELECT * FROM fasilitas_publik ORDER BY id DESC");
$fasilitasPublik = $stmt->fetchAll();
$stmt = $pdo->query("SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id ORDER BY pm.id DESC");
$pendudukMiskin = $stmt->fetchAll();
// Statistik dashboard
$totalFP = count($fasilitasPublik);
$totalPM = count($pendudukMiskin);
$ditangani = count(array_filter($pendudukMiskin, fn($p) => $p['status_jangkauan'] === 'Dalam Jangkauan'));
$belumDitangani = $totalPM - $ditangani;
$persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Sistem Analisis Geospasial Pengentasan Kemiskinan</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- App CSS -->
<link rel="stylesheet" href="assets/css/style.css?v=1">
</head>
<body>
<!-- ═══════════════════════════════════════════════════════════
SIDEBAR
═══════════════════════════════════════════════════════════ -->
<aside class="sidebar" id="sidebar">
<!-- Header Sidebar -->
<div class="sidebar-header">
<div class="sidebar-logo">
<div class="logo-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" fill="currentColor" opacity=".9"/>
<circle cx="12" cy="9" r="2.5" fill="white"/>
</svg>
</div>
<div class="logo-text">
<span class="logo-title">WebGIS</span>
<span class="logo-sub">Analisis Spasial</span>
</div>
</div>
<button class="sidebar-toggle" id="sidebarToggle" title="Sembunyikan Sidebar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M15 18l-6-6 6-6"/>
</svg>
</button>
</div>
<!-- Kotak Pencarian Baru -->
<div class="sidebar-search">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" id="searchInput" placeholder="Cari nama, NIK, atau lokasi..." autocomplete="off">
</div>
<div class="search-results hidden" id="searchResults"></div>
</div>
<!-- Dashboard Statistik -->
<div class="sidebar-section">
<div class="section-label">Dashboard Statistik</div>
<div class="stats-grid">
<div class="stat-card stat-blue">
<div class="stat-icon">🏢</div>
<div class="stat-body">
<div class="stat-value" id="statTotalRI"><?= $totalFP ?></div>
<div class="stat-label">Fasilitas Publik</div>
</div>
</div>
<div class="stat-card stat-amber">
<div class="stat-icon">👥</div>
<div class="stat-body">
<div class="stat-value" id="statTotalPM"><?= $totalPM ?></div>
<div class="stat-label">Penduduk Miskin</div>
</div>
</div>
<div class="stat-card stat-green">
<div class="stat-icon">✅</div>
<div class="stat-body">
<div class="stat-value" id="statDitangani"><?= $ditangani ?></div>
<div class="stat-label">Ditangani</div>
</div>
</div>
<div class="stat-card stat-red">
<div class="stat-icon">⚠️</div>
<div class="stat-body">
<div class="stat-value" id="statBelum"><?= $belumDitangani ?></div>
<div class="stat-label">Belum Ditangani</div>
</div>
</div>
</div>
<!-- Progress cakupan -->
<div class="coverage-bar-wrap">
<div class="coverage-bar-header">
<span>Cakupan Bantuan</span>
<strong id="statPersen"><?= $persen ?>%</strong>
</div>
<div class="coverage-bar-track">
<div class="coverage-bar-fill" id="statProgressBar" style="width: <?= $persen ?>%"></div>
</div>
</div>
<!-- Tools -->
<div class="sidebar-section">
<div class="section-label">Alat Peta</div>
<div class="tool-grid">
<button class="tool-btn active" id="btnToolPoint" data-mode="point" title="Tambah Titik">
<span class="tool-icon">📍</span>
<span>Titik</span>
</button>
<button class="tool-btn" id="btnToolJalan" data-mode="jalan" title="Gambar Jalan">
<span class="tool-icon">🛣️</span>
<span>Jalan</span>
</button>
<button class="tool-btn" id="btnToolPolygon" data-mode="polygon" title="Gambar Polygon">
<span class="tool-icon">⬜</span>
<span>Polygon</span>
</button>
<button class="tool-btn" id="btnToolFasilitasPublik" data-mode="fasilitas_publik" title="Fasilitas Publik">
<span class="tool-icon">🏢</span>
<span>Fasilitas</span>
</button>
<button class="tool-btn" id="btnToolPenduduk" data-mode="penduduk_miskin" title="Penduduk Miskin">
<span class="tool-icon">👥</span>
<span>Miskin</span>
</button>
<button class="tool-btn" id="btnToolLokasi" data-mode="lokasi" title="Lokasi Saya">
<span class="tool-icon">📡</span>
<span>Lokasi</span>
</button>
</div>
<div class="help-text" id="helpText">
Pilih alat lalu klik pada peta untuk menambahkan data.
</div>
</div>
<!-- Layer Control -->
<div class="sidebar-section">
<div class="section-label">Kontrol Layer</div>
<div class="layer-list" id="layerList">
<label class="layer-item">
<input type="checkbox" id="layerFasilitasPublik" checked>
<span class="layer-dot" style="background:#e53935"></span>
<span>Fasilitas Publik</span>
</label>
<label class="layer-item">
<input type="checkbox" id="layerPendudukDalam" checked>
<span class="layer-dot" style="background:#e53935;border:2px solid #333"></span>
<span>Penduduk Ditangani</span>
</label>
<label class="layer-item">
<input type="checkbox" id="layerPendudukLuar" checked>
<span class="layer-dot" style="background:#2e7d32"></span>
<span>Penduduk Luar Jangkauan</span>
</label>
<label class="layer-item">
<input type="checkbox" id="layerTitik"><span class="layer-dot" style="background:#78909c"></span><span>Titik Lokasi (Data)</span></label><label class="layer-item"><input type="checkbox" id="layerJalan"><span class="layer-dot" style="background:#f57c00"></span><span>Jalan (Data)</span></label><label class="layer-item"><input type="checkbox" id="layerPolygon"><span class="layer-dot" style="background:#ba68c8"></span><span>Polygon (Data)</span></label><label class="layer-item"><input type="checkbox" id="layerGeoJalan">
<span class="layer-dot" style="background:#d32f2f"></span>
<span>Jaringan Jalan</span>
</label>
<label class="layer-item">
<input type="checkbox" id="layerKecamatan">
<span class="layer-dot" style="background:#ef6c00;border-radius:2px"></span>
<span>Batas Kecamatan</span>
</label>
<label class="layer-item">
<input type="checkbox" id="layerSungai">
<span class="layer-dot" style="background:#0d47a1"></span>
<span>Sungai Besar</span>
</label>
</div>
</div>
<!-- Legenda -->
<div class="sidebar-section">
<div class="section-label">Legenda</div>
<div class="legend-list">
<div class="legend-item">
<span class="legend-marker marker-ri"></span>
<span>Fasilitas Publik + Radius</span>
</div>
<div class="legend-item">
<span class="legend-circle" style="background:rgba(229,57,53,0.2);border:2px solid #e53935"></span>
<span>Penduduk Ditangani</span>
</div>
<div class="legend-item">
<span class="legend-circle" style="background:rgba(46,125,50,0.2);border:2px solid #2e7d32"></span>
<span>Penduduk Luar Jangkauan</span>
</div>
<div class="legend-item">
<span class="legend-line" style="background:#d32f2f"></span>
<span>Jalan Nasional</span>
</div>
<div class="legend-item">
<span class="legend-line" style="background:#f57c00"></span>
<span>Jalan Provinsi</span>
</div>
<div class="legend-item">
<span class="legend-line" style="background:#388e3c"></span>
<span>Jalan Kabupaten</span>
</div>
</div>
</div>
<!-- Footer -->
<div class="sidebar-footer">
<span>© 2025 WebGIS Spasial</span>
</div>
</aside>
<!-- Sidebar collapsed toggle -->
<button class="sidebar-open-btn hidden" id="sidebarOpenBtn" title="Buka Sidebar">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 18l6-6-6-6"/>
</svg>
</button>
<!-- ═══════════════════════════════════════════════════════════
MAP
═══════════════════════════════════════════════════════════ -->
<div id="mapContainer">
<div id="map"></div>
<!-- Notifikasi toast -->
<div class="toast-container" id="toastContainer"></div>
<!-- Loading overlay -->
<div class="map-loading" id="mapLoading">
<div class="loading-spinner"></div>
<span>Memuat peta...</span>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════
MODALS
═══════════════════════════════════════════════════════════ -->
<!-- BACKDROP -->
<div class="modal-backdrop hidden" id="modalBackdrop"></div>
<!-- MODAL: Tambah/Edit Titik (Point) -->
<div class="modal hidden" id="pointModal">
<div class="modal-header">
<div class="modal-title-wrap">
<div class="modal-icon" style="background:#e3f2fd;color:#1565c0">📍</div>
<div>
<h3 class="modal-title" id="pointModalTitle">Tambah Titik</h3>
<p class="modal-subtitle">Klik peta untuk menentukan lokasi</p>
</div>
</div>
<button class="modal-close" id="closePointModal">✕</button>
</div>
<div class="modal-body">
<input type="hidden" id="point_id">
<div class="form-row">
<div class="form-group">
<label class="form-label">Nama <span class="req">*</span></label>
<input type="text" class="form-input" id="point_nama" placeholder="Nama titik...">
</div>
<div class="form-group">
<label class="form-label">No. WhatsApp <span class="req">*</span></label>
<input type="text" class="form-input" id="point_no_wa" placeholder="08xx...">
</div>
</div>
<div class="form-group">
<label class="form-label">Buka 24 Jam</label>
<div class="radio-tabs">
<label class="radio-tab">
<input type="radio" name="point_buka_24_jam" value="Ya">
<span>✓ Ya</span>
</label>
<label class="radio-tab">
<input type="radio" name="point_buka_24_jam" value="Tidak" checked>
<span>✗ Tidak</span>
</label>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Latitude</label>
<input type="text" class="form-input mono" id="point_latitude" readonly placeholder="Klik peta">
</div>
<div class="form-group">
<label class="form-label">Longitude</label>
<input type="text" class="form-input mono" id="point_longitude" readonly placeholder="Klik peta">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" id="cancelPointModal">Batal</button>
<button class="btn btn-primary" id="btnSavePoint">
<span>Simpan Titik</span>
</button>
</div>
</div>
<!-- MODAL: Jalan -->
<div class="modal hidden" id="jalanModal">
<div class="modal-header">
<div class="modal-title-wrap">
<div class="modal-icon" style="background:#fce4ec;color:#c62828">🛣️</div>
<div>
<h3 class="modal-title">Data Jalan</h3>
<p class="modal-subtitle">Klik beberapa titik, double-klik selesai</p>
</div>
</div>
<button class="modal-close" id="closeJalanModal">✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Nama Jalan <span class="req">*</span></label>
<input type="text" class="form-input" id="nama_jalan" placeholder="Nama jalan...">
</div>
<div class="form-group">
<label class="form-label">Status Jalan <span class="req">*</span></label>
<select class="form-input" id="status_jalan">
<option value=""> Pilih Status </option>
<option value="Jalan Nasional">Jalan Nasional</option>
<option value="Jalan Provinsi">Jalan Provinsi</option>
<option value="Jalan Kabupaten">Jalan Kabupaten</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Panjang (meter)</label>
<input type="text" class="form-input mono" id="panjang_meter" readonly>
</div>
<div class="form-group">
<label class="form-label">Jumlah Titik</label>
<input type="text" class="form-input mono" id="jumlah_titik_jalan" readonly value="0">
</div>
</div>
<div class="form-group hidden">
<textarea class="form-input mono" id="koordinat_jalan" rows="4" readonly></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" id="cancelJalanModal">Batal</button>
<button class="btn btn-primary" id="btnSaveJalan">Simpan Jalan</button>
</div>
</div>
<!-- MODAL: Polygon -->
<div class="modal hidden" id="polygonModal">
<div class="modal-header">
<div class="modal-title-wrap">
<div class="modal-icon" style="background:#f3e5f5;color:#6a1b9a">⬜</div>
<div>
<h3 class="modal-title">Data Parsil Tanah</h3>
<p class="modal-subtitle">Min. 3 titik untuk polygon</p>
</div>
</div>
<button class="modal-close" id="closePolygonModal">✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Nama Parsil <span class="req">*</span></label>
<input type="text" class="form-input" id="nama_parsil" placeholder="Nama parsil tanah...">
</div>
<div class="form-group">
<label class="form-label">Status Kepemilikan <span class="req">*</span></label>
<select class="form-input" id="status_kepemilikan">
<option value=""> Pilih Status </option>
<option value="SHM">SHM Sertifikat Hak Milik</option>
<option value="HGB">HGB Hak Guna Bangunan</option>
<option value="HGU">HGU Hak Guna Usaha</option>
<option value="HP">HP Hak Pakai</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Luas (m²)</label>
<input type="text" class="form-input mono" id="luas_tanah" readonly>
</div>
<div class="form-group">
<label class="form-label">Jumlah Titik</label>
<input type="text" class="form-input mono" id="jumlah_titik_polygon" readonly value="0">
</div>
</div>
<div class="form-group hidden">
<textarea class="form-input mono" id="koordinat_polygon" rows="4" readonly></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" id="cancelPolygonModal">Batal</button>
<button class="btn btn-primary" id="btnSavePolygon">Simpan Polygon</button>
</div>
</div>
<!-- MODAL: Fasilitas Publik -->
<div class="modal hidden" id="fasilitasPublikModal">
<div class="modal-header">
<div class="modal-title-wrap">
<div class="modal-icon" style="background:#e8f5e9;color:#2e7d32">🏢</div>
<div>
<h3 class="modal-title" id="fpModalTitle">Tambah Fasilitas Publik</h3>
<p class="modal-subtitle">Klik peta untuk menentukan lokasi</p>
</div>
</div>
<button class="modal-close" id="closefasilitasPublikModal">✕</button>
</div>
<div class="modal-body">
<input type="hidden" id="fp_id">
<div class="form-row">
<div class="form-group">
<label class="form-label">Nama Fasilitas Publik <span class="req">*</span></label>
<input type="text" class="form-input" id="fp_nama" placeholder="Nama...">
</div>
<div class="form-group">
<label class="form-label">Jenis</label>
<select class="form-input" id="fp_jenis">
<option value=""> Pilih Jenis </option>
<option value="Kantor Desa / Pemerintahan">Kantor Desa / Pemerintahan</option>
<option value="Fasilitas Kesehatan (Puskesmas)">Fasilitas Kesehatan (Puskesmas)</option>
<option value="Rumah Ibadah (Masjid/Gereja/dll)">Rumah Ibadah (Masjid/Gereja/dll)</option>
<option value="Posko / e-Warong">Posko / e-Warong</option>
<option value="Sekolah">Sekolah</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Alamat <small>(otomatis dari koordinat)</small></label>
<div class="geocode-wrap">
<textarea class="form-input" id="fp_alamat" rows="2" placeholder="Klik peta untuk mendapatkan alamat otomatis..."></textarea>
<span class="geocode-status" id="geocodeStatus"></span>
</div>
</div>
<div class="form-group">
<label class="form-label">
Radius Pelayanan: <strong id="fp_radius_text">300</strong> meter
</label>
<div class="range-wrap">
<span class="range-min">100m</span>
<input type="range" class="range-input" id="fp_radius" min="100" max="3000" step="50" value="300">
<span class="range-max">3000m</span>
</div>
</div>
<div class="form-group">
<label class="form-label">Kontak</label>
<input type="text" class="form-input" id="fp_kontak" placeholder="No. HP / WA...">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Latitude</label>
<input type="text" class="form-input mono" id="fp_latitude" readonly placeholder="Klik peta">
</div>
<div class="form-group">
<label class="form-label">Longitude</label>
<input type="text" class="form-input mono" id="fp_longitude" readonly placeholder="Klik peta">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" id="cancelfasilitasPublikModal">Batal</button>
<button class="btn btn-primary" id="btnSaveFasilitasPublik">Simpan</button>
</div>
</div>
<!-- MODAL: Penduduk Miskin -->
<div class="modal hidden" id="pendudukModal">
<div class="modal-header">
<div class="modal-title-wrap">
<div class="modal-icon" style="background:#fff8e1;color:#e65100">👥</div>
<div>
<h3 class="modal-title" id="pmModalTitle">Tambah Penduduk Miskin</h3>
<p class="modal-subtitle">Klik peta untuk menentukan lokasi</p>
</div>
</div>
<button class="modal-close" id="closePendudukModal">✕</button>
</div>
<div class="modal-body">
<input type="hidden" id="pm_id">
<div class="form-row">
<div class="form-group">
<label class="form-label">Nama Lengkap <span class="req">*</span></label>
<input type="text" class="form-input" id="pm_nama" placeholder="Nama...">
</div>
<div class="form-group">
<label class="form-label">NIK</label>
<input type="text" class="form-input mono" id="pm_nik" placeholder="16 digit NIK...">
</div>
</div>
<div class="form-group">
<label class="form-label">Keterangan</label>
<textarea class="form-input" id="pm_keterangan" rows="2" placeholder="Kondisi ekonomi, pekerjaan, dll..."></textarea>
</div>
<!-- Status Jangkauan (auto) -->
<div class="status-jangkauan-card" id="statusJangkauanCard">
<div class="sj-label">Status Jangkauan</div>
<div class="sj-value" id="pm_status">—</div>
<div class="sj-ditangani" id="pm_ditangani"></div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Latitude</label>
<input type="text" class="form-input mono" id="pm_latitude" readonly placeholder="Klik peta">
</div>
<div class="form-group">
<label class="form-label">Longitude</label>
<input type="text" class="form-input mono" id="pm_longitude" readonly placeholder="Klik peta">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" id="cancelPendudukModal">Batal</button>
<button class="btn btn-primary" id="btnSavePenduduk">Simpan</button>
</div>
</div>
<!-- MODAL: Konfirmasi Hapus -->
<div class="modal modal-sm hidden" id="deleteModal">
<div class="modal-header">
<div class="modal-title-wrap">
<div class="modal-icon" style="background:#ffebee;color:#c62828">🗑️</div>
<div>
<h3 class="modal-title">Konfirmasi Hapus</h3>
</div>
</div>
</div>
<div class="modal-body">
<p id="deleteMessage">Apakah Anda yakin ingin menghapus data ini?</p>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" id="cancelDelete">Batal</button>
<button class="btn btn-danger" id="confirmDelete">Hapus</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════
DATA PHP → JS
═══════════════════════════════════════════════════════════ -->
<script>
const APP_DATA = {
points: <?= json_encode($points, JSON_UNESCAPED_UNICODE) ?>,
jalan: <?= json_encode($jalan, JSON_UNESCAPED_UNICODE) ?>,
polygons: <?= json_encode($polygons, JSON_UNESCAPED_UNICODE) ?>,
fasilitasPublik: <?= json_encode($fasilitasPublik, JSON_UNESCAPED_UNICODE) ?>,
pendudukMiskin:<?= json_encode($pendudukMiskin, JSON_UNESCAPED_UNICODE) ?>,
};
</script>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<!-- App JS -->
<script src="assets/js/app.js?v=1"></script>
</body>
</html>
+14
View File
@@ -0,0 +1,14 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
try {
$pdo->prepare("DELETE FROM jalan WHERE id=?")->execute([$id]);
echo json_encode(['status'=>'success']);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+43
View File
@@ -0,0 +1,43 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$nama_jalan = trim($_POST['nama_jalan'] ?? '');
$status_jalan = trim($_POST['status_jalan'] ?? '');
$panjang_meter = trim($_POST['panjang_meter'] ?? '0');
$koordinat = trim($_POST['koordinat'] ?? '');
if (!$nama_jalan || !$status_jalan || !$koordinat) {
echo json_encode(['status'=>'error','message'=>'Field wajib tidak boleh kosong.']);
exit;
}
$decoded = json_decode($koordinat, true);
if (!$decoded || ($decoded['geometry']['type'] ?? '') !== 'LineString') {
echo json_encode(['status'=>'error','message'=>'Format GeoJSON tidak valid.']);
exit;
}
$valid_status = ['Jalan Nasional','Jalan Provinsi','Jalan Kabupaten'];
if (!in_array($status_jalan, $valid_status)) {
echo json_encode(['status'=>'error','message'=>'Status jalan tidak valid.']);
exit;
}
try {
$stmt = $pdo->prepare(
"INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, koordinat)
VALUES (?, ?, ?, ?)"
);
$stmt->execute([$nama_jalan, $status_jalan, $panjang_meter, $koordinat]);
$id = $pdo->lastInsertId();
$stmtSelect = $pdo->prepare("SELECT * FROM jalan WHERE id=?");
$stmtSelect->execute([$id]);
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
echo json_encode(['status'=>'success','data'=>$data]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
// ============================================================
// koneksi.php — Database Connection (PDO)
// ============================================================
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'webgis');
define('DB_CHARSET', 'utf8mb4');
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (PDOException $e) {
http_response_code(500);
die(json_encode([
'status' => 'error',
'message' => 'Koneksi database gagal: ' . $e->getMessage()
]));
}
// Legacy mysqli compatibility (untuk file yang masih menggunakannya)
$conn = mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn) {
mysqli_set_charset($conn, DB_CHARSET);
}
?>
+14
View File
@@ -0,0 +1,14 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
try {
$pdo->prepare("DELETE FROM penduduk_miskin WHERE id=?")->execute([$id]);
echo json_encode(['status'=>'success']);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+39
View File
@@ -0,0 +1,39 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$nama = trim($_POST['nama'] ?? '');
$nik = trim($_POST['nik'] ?? '');
$keterangan = trim($_POST['keterangan'] ?? '');
$latitude = trim($_POST['latitude'] ?? '');
$longitude = trim($_POST['longitude'] ?? '');
$status_jangkauan= trim($_POST['status_jangkauan']?? 'Luar Jangkauan');
$fasilitas_publik_id = trim($_POST['fasilitas_publik_id'] ?? '');
if (!$nama || !$latitude || !$longitude) {
echo json_encode(['status'=>'error','message'=>'Nama dan lokasi wajib diisi.']);
exit;
}
$valid_status = ['Dalam Jangkauan','Luar Jangkauan'];
if (!in_array($status_jangkauan, $valid_status)) $status_jangkauan = 'Luar Jangkauan';
$fp_id = ($fasilitas_publik_id !== '') ? (int)$fasilitas_publik_id : null;
try {
$stmt = $pdo->prepare(
"INSERT INTO penduduk_miskin (nama, nik, keterangan, latitude, longitude, status_jangkauan, fasilitas_publik_id)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
$stmt->execute([$nama, $nik, $keterangan, $latitude, $longitude, $status_jangkauan, $fp_id]);
$id = $pdo->lastInsertId();
$stmtSelect = $pdo->prepare("SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id WHERE pm.id = ?");
$stmtSelect->execute([$id]);
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
echo json_encode(['status'=>'success','data'=>$data]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? 0);
$nama = trim($_POST['nama'] ?? '');
$nik = trim($_POST['nik'] ?? '');
$keterangan = trim($_POST['keterangan'] ?? '');
$latitude = trim($_POST['latitude'] ?? '');
$longitude = trim($_POST['longitude'] ?? '');
$status_jangkauan= trim($_POST['status_jangkauan']?? 'Luar Jangkauan');
$fasilitas_publik_id = trim($_POST['fasilitas_publik_id'] ?? '');
if (!$id || !$nama || !$latitude || !$longitude) {
echo json_encode(['status'=>'error','message'=>'Data tidak lengkap.']);
exit;
}
$fp_id = ($fasilitas_publik_id !== '') ? (int)$fasilitas_publik_id : null;
try {
$stmt = $pdo->prepare(
"UPDATE penduduk_miskin SET nama=?, nik=?, keterangan=?, latitude=?, longitude=?,
status_jangkauan=?, fasilitas_publik_id=? WHERE id=?"
);
$stmt->execute([$nama, $nik, $keterangan, $latitude, $longitude, $status_jangkauan, $fp_id, $id]);
$stmtSelect = $pdo->prepare("SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id WHERE pm.id = ?");
$stmtSelect->execute([$id]);
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
echo json_encode(['status'=>'success','data'=>$data]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+14
View File
@@ -0,0 +1,14 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
try {
$pdo->prepare("DELETE FROM points WHERE id=?")->execute([$id]);
echo json_encode(['status'=>'success']);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+38
View File
@@ -0,0 +1,38 @@
<?php
// point/simpan.php
include '../koneksi.php';
header('Content-Type: application/json');
$nama = trim($_POST['nama'] ?? '');
$no_wa = trim($_POST['no_wa'] ?? '');
$buka_24 = trim($_POST['buka_24_jam']?? '');
$latitude = trim($_POST['latitude'] ?? '');
$longitude = trim($_POST['longitude'] ?? '');
if (!$nama || !$no_wa || !$buka_24 || !$latitude || !$longitude) {
echo json_encode(['status'=>'error','message'=>'Semua field wajib diisi.']);
exit;
}
if (!in_array($buka_24, ['Ya','Tidak'])) {
echo json_encode(['status'=>'error','message'=>'Nilai buka_24_jam tidak valid.']);
exit;
}
try {
$stmt = $pdo->prepare(
"INSERT INTO points (nama, no_wa, buka_24_jam, latitude, longitude)
VALUES (?, ?, ?, ?, ?)"
);
$stmt->execute([$nama, $no_wa, $buka_24, $latitude, $longitude]);
$id = $pdo->lastInsertId();
$stmtSelect = $pdo->prepare("SELECT * FROM points WHERE id = ?");
$stmtSelect->execute([$id]);
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
echo json_encode(['status'=>'success','data'=>$data]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$e->getMessage()]);
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? 0);
$nama = trim($_POST['nama'] ?? '');
$no_wa = trim($_POST['no_wa'] ?? '');
$buka_24 = trim($_POST['buka_24_jam'] ?? '');
$latitude = trim($_POST['latitude'] ?? '');
$longitude = trim($_POST['longitude'] ?? '');
if (!$id || !$nama || !$no_wa || !$buka_24 || !$latitude || !$longitude) {
echo json_encode(['status'=>'error','message'=>'Data tidak lengkap.']);
exit;
}
try {
$stmt = $pdo->prepare(
"UPDATE points SET nama=?, no_wa=?, buka_24_jam=?, latitude=?, longitude=? WHERE id=?"
);
$stmt->execute([$nama, $no_wa, $buka_24, $latitude, $longitude, $id]);
$stmtSelect = $pdo->prepare("SELECT * FROM points WHERE id=?");
$stmtSelect->execute([$id]);
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
echo json_encode(['status'=>'success','data'=>$data]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+14
View File
@@ -0,0 +1,14 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
try {
$pdo->prepare("DELETE FROM polygon WHERE id=?")->execute([$id]);
echo json_encode(['status'=>'success']);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+43
View File
@@ -0,0 +1,43 @@
<?php
include '../koneksi.php';
header('Content-Type: application/json');
$nama_parsil = trim($_POST['nama_parsil'] ?? '');
$status_kepemilikan= trim($_POST['status_kepemilikan']?? '');
$luas_tanah = trim($_POST['luas_tanah'] ?? '0');
$koordinat = trim($_POST['koordinat'] ?? '');
if (!$nama_parsil || !$status_kepemilikan || !$koordinat) {
echo json_encode(['status'=>'error','message'=>'Field wajib tidak boleh kosong.']);
exit;
}
$decoded = json_decode($koordinat, true);
if (!$decoded || ($decoded['geometry']['type'] ?? '') !== 'Polygon') {
echo json_encode(['status'=>'error','message'=>'Format GeoJSON tidak valid.']);
exit;
}
$valid_status = ['SHM','HGB','HGU','HP'];
if (!in_array($status_kepemilikan, $valid_status)) {
echo json_encode(['status'=>'error','message'=>'Status kepemilikan tidak valid.']);
exit;
}
try {
$stmt = $pdo->prepare(
"INSERT INTO polygon (nama_parsil, status_kepemilikan, luas_tanah, koordinat)
VALUES (?, ?, ?, ?)"
);
$stmt->execute([$nama_parsil, $status_kepemilikan, $luas_tanah, $koordinat]);
$id = $pdo->lastInsertId();
$stmtSelect = $pdo->prepare("SELECT * FROM polygon WHERE id=?");
$stmtSelect->execute([$id]);
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
echo json_encode(['status'=>'success','data'=>$data]);
} catch (PDOException $e) {
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
}
?>
+80
View File
@@ -0,0 +1,80 @@
-- ============================================================
-- WebGIS - Sistem Informasi Geografis Pengentasan Kemiskinan
-- Database Schema
-- ============================================================
CREATE DATABASE IF NOT EXISTS webgis CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE webgis;
-- Tabel Points (SPBU / Titik Umum)
DROP TABLE IF EXISTS points;
CREATE TABLE points (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150) NOT NULL,
no_wa VARCHAR(30) NOT NULL,
buka_24_jam ENUM('Ya','Tidak') NOT NULL DEFAULT 'Tidak',
latitude DECIMAL(18,15) NOT NULL,
longitude DECIMAL(18,15) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Tabel Jalan
DROP TABLE IF EXISTS jalan;
CREATE TABLE jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(200) NOT NULL,
status_jalan ENUM('Jalan Nasional','Jalan Provinsi','Jalan Kabupaten') NOT NULL,
panjang_meter DECIMAL(14,2) NOT NULL DEFAULT 0,
koordinat LONGTEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Tabel Polygon
DROP TABLE IF EXISTS polygon;
CREATE TABLE polygon (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_parsil VARCHAR(200) NOT NULL,
status_kepemilikan ENUM('SHM','HGB','HGU','HP') NOT NULL,
luas_tanah DECIMAL(18,4) NOT NULL DEFAULT 0,
koordinat LONGTEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Tabel Rumah Ibadah
DROP TABLE IF EXISTS rumah_ibadah;
CREATE TABLE rumah_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
jenis ENUM('Masjid','Gereja','Pura','Vihara','Klenteng','Lainnya') NOT NULL DEFAULT 'Masjid',
alamat TEXT,
latitude DECIMAL(18,15) NOT NULL,
longitude DECIMAL(18,15) NOT NULL,
radius INT NOT NULL DEFAULT 300,
kontak VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Tabel Penduduk Miskin
DROP TABLE IF EXISTS penduduk_miskin;
CREATE TABLE penduduk_miskin (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
nik VARCHAR(20),
keterangan TEXT,
latitude DECIMAL(18,15) NOT NULL,
longitude DECIMAL(18,15) NOT NULL,
status_jangkauan ENUM('Dalam Jangkauan','Luar Jangkauan') NOT NULL DEFAULT 'Luar Jangkauan',
rumah_ibadah_id INT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
) ENGINE=InnoDB;
-- Sample data untuk testing
INSERT INTO rumah_ibadah (nama, jenis, alamat, latitude, longitude, radius) VALUES
('Masjid Raya Pontianak', 'Masjid', 'Jl. Tanjungpura, Pontianak', -0.0263, 109.3425, 500),
('Gereja Katedral Pontianak', 'Gereja', 'Jl. Pattimura, Pontianak', -0.0571, 109.3283, 400);
INSERT INTO penduduk_miskin (nama, nik, keterangan, latitude, longitude, status_jangkauan, rumah_ibadah_id) VALUES
('Ahmad Fauzi', '6171010101010001', 'Buruh harian lepas', -0.0280, 109.3430, 'Dalam Jangkauan', 1),
('Sari Dewi', '6171010101010002', 'Ibu rumah tangga tidak bekerja', -0.0900, 109.3900, 'Luar Jangkauan', NULL),
('Budi Santoso', '6171010101010003', 'Pekerja informal', -0.0600, 109.3280, 'Dalam Jangkauan', 2);