initial commit

This commit is contained in:
cygouw
2026-06-10 22:01:03 +07:00
commit a9b40ecf29
26 changed files with 14175 additions and 0 deletions
+517
View File
@@ -0,0 +1,517 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Portal WebGIS & Aplikasi Spasial</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #2563EB;
--primary-dark: #1D4ED8;
--primary-light: #60A5FA;
--primary-pale: #EFF6FF;
--bg: #F8FAFC;
--surface: #FFFFFF;
--text-primary: #0F172A;
--text-secondary: #475569;
--text-muted: #94A3B8;
--border: #E2E8F0;
--border-light: #F1F5F9;
--radius: 18px;
--radius-sm: 10px;
--shadow: 0 10px 30px -10px rgba(37, 99, 235, 0.08), 0 1px 3px rgba(0, 0, 0, 0.02);
--shadow-hover: 0 20px 40px -15px rgba(37, 99, 235, 0.15), 0 1px 10px rgba(37, 99, 235, 0.05);
--tr: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: var(--bg);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
line-height: 1.5;
overflow-x: hidden;
position: relative;
}
/* Abstract Background Graphics */
body::before {
content: '';
position: absolute;
top: -200px;
right: -200px;
width: 600px;
height: 600px;
border-radius: 50%;
background: radial-gradient(circle, rgba(96, 165, 250, 0.12) 0%, rgba(255, 255, 255, 0) 70%);
z-index: -1;
pointer-events: none;
}
body::after {
content: '';
position: absolute;
bottom: -150px;
left: -150px;
width: 500px;
height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(37, 99, 235, 0.06) 0%, rgba(255, 255, 255, 0) 70%);
z-index: -1;
pointer-events: none;
}
/* HEADER */
header {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-light);
position: sticky;
top: 0;
z-index: 1000;
padding: 18px 24px;
transition: var(--tr);
}
.header-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
}
.brand-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
}
.brand-text {
display: flex;
flex-direction: column;
}
.brand-title {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.5px;
}
.brand-subtitle {
font-size: 11px;
color: var(--text-secondary);
font-weight: 500;
}
/* HERO SECTION */
.hero {
padding: 60px 24px 40px;
text-align: center;
max-width: 800px;
margin: 0 auto;
}
.hero-tag {
display: inline-block;
padding: 6px 14px;
background: var(--primary-pale);
color: var(--primary);
border-radius: 30px;
font-size: 12px;
font-weight: 700;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid rgba(37, 99, 235, 0.12);
}
.hero h1 {
font-size: 38px;
font-weight: 800;
color: var(--text-primary);
letter-spacing: -1px;
line-height: 1.25;
margin-bottom: 16px;
}
.hero h1 span {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero p {
font-size: 16px;
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
line-height: 1.6;
}
/* MAIN CONTENT & GRID */
main {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px 80px;
flex: 1;
width: 100%;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 30px;
margin-top: 20px;
}
/* PROJECT CARDS */
.card {
background: var(--surface);
border: 1px solid var(--border-light);
border-radius: var(--radius);
padding: 32px;
display: flex;
flex-direction: column;
box-shadow: var(--shadow);
transition: var(--tr);
text-decoration: none;
color: inherit;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: var(--accent, var(--primary));
opacity: 0.8;
transition: var(--tr);
}
.card:hover {
transform: translateY(-6px);
border-color: rgba(37, 99, 235, 0.15);
box-shadow: var(--shadow-hover);
}
.card:hover::before {
height: 6px;
}
.card-icon-wrap {
width: 60px;
height: 60px;
border-radius: 16px;
background: var(--accent-bg, var(--primary-pale));
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
color: var(--accent, var(--primary));
transition: var(--tr);
}
.card:hover .card-icon-wrap {
transform: scale(1.08) rotate(3deg);
}
.card h3 {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
letter-spacing: -0.4px;
line-height: 1.35;
}
.card p {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 24px;
flex: 1;
}
/* BADGES */
.badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 30px;
}
.badge {
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
background: var(--border-light);
color: var(--text-secondary);
border-radius: 30px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
/* ACTION BUTTON */
.card-action {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 18px;
border-top: 1px solid var(--border-light);
font-weight: 700;
font-size: 14px;
color: var(--accent, var(--primary));
transition: var(--tr);
}
.card-action span {
display: flex;
align-items: center;
gap: 6px;
}
.card-action svg {
width: 16px;
height: 16px;
fill: currentColor;
transition: var(--tr);
}
.card:hover .card-action svg {
transform: translateX(4px);
}
/* FOOTER */
footer {
background: #FFFFFF;
border-top: 1px solid var(--border-light);
padding: 24px;
text-align: center;
font-size: 13px;
color: var(--text-secondary);
}
.footer-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 8px;
}
/* User Profile Badge */
.user-badge {
display: flex;
align-items: center;
gap: 10px;
background: var(--primary-pale);
border: 1px solid rgba(37, 99, 235, 0.12);
padding: 6px 14px;
border-radius: 30px;
transition: var(--tr);
}
.user-badge:hover {
background: #E0EEFF;
border-color: rgba(37, 99, 235, 0.25);
}
.user-avatar {
width: 28px;
height: 28px;
background: var(--primary);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
}
.user-info {
display: flex;
flex-direction: column;
text-align: left;
}
.user-name {
font-size: 12px;
font-weight: 700;
color: var(--text-primary);
}
.user-nim {
font-size: 10px;
color: var(--text-secondary);
font-weight: 500;
}
/* RESPONSIVE */
@media (max-width: 768px) {
.hero {
padding: 40px 16px 20px;
}
.hero h1 {
font-size: 30px;
}
.hero p {
font-size: 14px;
}
main {
padding: 0 16px 60px;
}
.grid {
grid-template-columns: 1fr;
gap: 20px;
}
.card {
padding: 24px;
}
}
</style>
</head>
<body>
<!-- HEADER -->
<header>
<div class="header-container">
<a href="#" class="brand">
<div class="brand-icon">🌐</div>
<div class="brand-text">
<span class="brand-title">Portal WebGIS</span>
<span class="brand-subtitle">Tugas & UAS Sistem Informasi Geografis</span>
</div>
</a>
<div class="user-badge">
<div class="user-avatar">CY</div>
<div class="user-info">
<span class="user-name">Cecilia Yvon Gunawan</span>
<span class="user-nim">NIM D1041231056</span>
</div>
</div>
</div>
</header>
<!-- HERO SECTION -->
<section class="hero">
<div class="hero-tag">Kumpulan Proyek SIG</div>
<h1>Kumpulan Project <span>WebGIS Interaktif</span></h1>
<p>Hub aplikasi spasial interaktif hasil pengerjaan tugas praktikum dan Ujian Akhir Semester (UAS) untuk mata kuliah Sistem Informasi Geografis.</p>
</section>
<!-- MAIN CONTAINER -->
<main>
<div class="grid">
<!-- PROJECT 1: Poverty Map -->
<a href="http://localhost/webgis/povertymap/" class="card" style="--accent: #4F46E5; --accent-bg: #EEF2FF;">
<div class="card-icon-wrap">
<!-- Hand holding heart / charity icon -->
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
</div>
<h3>WebGIS Poverty Mapping</h3>
<p>Sistem informasi geografis untuk memetakan sebaran kemiskinan warga di sekitar wilayah tempat ibadah guna penargetan program bantuan sosial.</p>
<div class="badges">
<span class="badge">Leaflet.js</span>
<span class="badge">MySQL</span>
<span class="badge">PHP Backend</span>
<span class="badge">Geotagging</span>
</div>
<div class="card-action">
<span>Buka Aplikasi</span>
<svg viewBox="0 0 24 24">
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</a>
<!-- PROJECT 2: SPBU Map -->
<a href="http://localhost/webgis/spbu/" class="card" style="--accent: #0D9488; --accent-bg: #F0FDF4;">
<div class="card-icon-wrap">
<!-- Gas pump icon -->
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 22V2h10v20M13 10h5M18 10l3 3v6M18 20v2M9 6h1"/>
<circle cx="8.5" cy="11.5" r="1.5"/>
</svg>
</div>
<h3>WebGIS SPBU</h3>
<p>Aplikasi pemetaan lokasi Stasiun Pengisian Bahan Bakar Umum (SPBU) di wilayah Pontianak, lengkap dengan informasi status operasional 24 jam.</p>
<div class="badges">
<span class="badge">Leaflet.js</span>
<span class="badge">MySQL</span>
<span class="badge">PHP WebApp</span>
<span class="badge">Interactive Map</span>
</div>
<div class="card-action">
<span>Buka Aplikasi</span>
<svg viewBox="0 0 24 24">
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</a>
<!-- PROJECT 3: Parsil Jalan Laporan -->
<a href="http://localhost/webgis/parsil_jalan_jalanrusak/" class="card" style="--accent: #2563EB; --accent-bg: #EFF6FF;">
<div class="card-icon-wrap">
<!-- Road/infrastructure icon -->
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 20h18M6 20v-4a6 6 0 0112 0v4M12 3v10M12 17h.01M9 7l3-3 3 3"/>
</svg>
</div>
<h3>WebGIS Jalan, Parsil Tanah, & Jalan Rusak</h3>
<p>Portal pemetaan spasial terintegrasi untuk jaringan rute jalan raya, batas legalitas parsil tanah, serta pelaporan kerusakan jalan berbasis partisipasi warga.</p>
<div class="badges">
<span class="badge">Turf.js</span>
<span class="badge">Leaflet.js</span>
<span class="badge">MySQL</span>
<span class="badge">Geotagging</span>
</div>
<div class="card-action">
<span>Buka Aplikasi</span>
<svg viewBox="0 0 24 24">
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</a>
</div>
</main>
<!-- FOOTER -->
<footer>
<div class="footer-container">
<p style="font-weight: 700; color: var(--text-primary); margin-bottom: 2px;">Cecilia Yvon Gunawan — NIM D1041231056</p>
<p>&copy; 2026 Portal WebGIS Spasial. All rights reserved.</p>
<p style="font-size: 11px; color: var(--text-muted);">Dikembangkan menggunakan HTML5, Vanilla CSS, Javascript & LeafletJS.</p>
</div>
</footer>
</body>
</html>
+206
View File
@@ -0,0 +1,206 @@
# 🗺️ WebGIS Manajemen Jalan & Parsil Tanah
Sistem Informasi Geografis berbasis web untuk manajemen data spasial jalan dan parsil tanah.
---
## 📁 Struktur File
```
webgis/
├── index.html ← Halaman utama WebGIS
├── database.sql ← Schema & sample data MySQL
├── README.md ← Dokumentasi ini
└── api/
├── config.php ← Konfigurasi koneksi database
├── jalan.php ← API CRUD Data Jalan
└── parsil.php ← API CRUD Data Parsil Tanah
```
---
## ⚙️ Cara Install
### 1. Persyaratan
- **Web Server**: Apache / Nginx dengan PHP 7.4+
- **Database**: MySQL 5.7+ atau MariaDB 10.3+
- **Browser**: Chrome, Firefox, Edge (modern)
### 2. Setup Database
```sql
-- Jalankan di MySQL / phpMyAdmin
SOURCE database.sql;
```
Atau import file `database.sql` melalui phpMyAdmin.
### 3. Konfigurasi Database
Edit file `api/config.php`:
```php
define('DB_HOST', 'localhost'); // Host database
define('DB_USER', 'root'); // Username MySQL
define('DB_PASS', ''); // Password MySQL
define('DB_NAME', 'webgis_db'); // Nama database
```
### 4. Deploy ke Web Server
**Menggunakan XAMPP/WAMP/LAMP:**
```bash
# Copy folder webgis/ ke:
# Windows XAMPP: C:/xampp/htdocs/webgis/
# Linux LAMP: /var/www/html/webgis/
```
**Akses aplikasi:**
```
http://localhost/polyline_polygon/index.html
```
---
## 🌟 Fitur Utama
### 🛣️ Manajemen Data Jalan (Polyline)
| Fitur | Deskripsi |
|-------|-----------|
| **Tambah Jalan** | Gambar polyline di peta atau input koordinat manual |
| **Status Jalan** | Nasional (merah), Provinsi (oranye), Kabupaten (hijau) |
| **Panjang Otomatis** | Dihitung menggunakan `L.latLng.distanceTo()` dari LeafletJS |
| **Edit & Hapus** | CRUD lengkap dengan konfirmasi hapus |
| **Drag Titik** | Titik koordinat bisa diubah langsung di tabel |
### 🏘️ Manajemen Parsil Tanah (Polygon)
| Fitur | Deskripsi |
|-------|-----------|
| **Tambah Parsil** | Gambar polygon di peta atau input koordinat manual |
| **Status Kepemilikan** | SHM, HGB, HGU, HP — masing-masing warna berbeda |
| **Luas Otomatis** | Dihitung dengan formula Spherical Excess dari LeafletJS |
| **Data Lengkap** | Nama parsil, nomor sertifikat, nama pemilik, keterangan |
### 🗺️ Fitur Peta
- **Basemap**: OpenStreetMap + Esri Satellite (switchable)
- **Draw Mode**: Klik peta untuk menggambar fitur spasial
- **Popup Interaktif**: Klik fitur di peta untuk melihat detail
- **Zoom to Feature**: Langsung zoom ke lokasi dari sidebar
- **Koordinat Real-time**: Tampil di pojok kiri bawah
---
## 🎨 Kode Warna
### Jalan
| Status | Warna | Hex |
|--------|-------|-----|
| Jalan Nasional | 🔴 Merah | `#E63946` |
| Jalan Provinsi | 🟠 Oranye | `#F4A261` |
| Jalan Kabupaten | 🟢 Hijau | `#2A9D8F` |
### Parsil Tanah
| Status | Warna | Hex |
|--------|-------|-----|
| SHM | 🔵 Biru | `#4361EE` |
| HGB | 🟣 Ungu | `#7209B7` |
| HGU | 🌸 Pink | `#F72585` |
| HP | 🩵 Cyan | `#4CC9F0` |
---
## 📡 API Endpoints
### Jalan
```
GET api/jalan.php → Ambil semua data jalan
GET api/jalan.php?id={id} → Ambil data jalan by ID
POST api/jalan.php → Tambah data jalan baru
PUT api/jalan.php?id={id} → Update data jalan
DELETE api/jalan.php?id={id} → Hapus data jalan
```
### Parsil Tanah
```
GET api/parsil.php → Ambil semua data parsil
GET api/parsil.php?id={id} → Ambil data parsil by ID
POST api/parsil.php → Tambah data parsil baru
PUT api/parsil.php?id={id} → Update data parsil
DELETE api/parsil.php?id={id} → Hapus data parsil
```
### Contoh Request Body (POST/PUT Jalan)
```json
{
"nama_jalan": "Jalan Ahmad Yani",
"status_jalan": "Jalan Nasional",
"panjang_meter": 1250.5,
"koordinat": [
[0.0032, -109.3245],
[0.0051, -109.3180],
[0.0068, -109.3110]
],
"keterangan": "Jalan utama kota"
}
```
### Contoh Request Body (POST/PUT Parsil)
```json
{
"nama_parsil": "Kavling A-001",
"nomor_sertifikat": "SHM-2024-001",
"status_kepemilikan": "SHM",
"luas_meter2": 450.0,
"koordinat": [
[0.0010, -109.3280],
[0.0010, -109.3260],
[0.0020, -109.3260],
[0.0020, -109.3280]
],
"nama_pemilik": "Budi Santoso",
"keterangan": "Kavling perumahan"
}
```
---
## 💡 Cara Penggunaan
### Menambah Data Jalan Baru
1. Klik tab **"Data Jalan"** di topbar
2. Klik **"✚ Tambah Data Baru"** di sidebar
3. **Opsi A**: Klik **"✏️ Gambar di Peta"** → klik titik-titik di peta → klik **"⏹ Selesai Gambar"**
4. **Opsi B**: Isi koordinat manual di tabel (lat/lng double)
5. Isi nama jalan, pilih status, tambah keterangan
6. Panjang jalan otomatis terhitung oleh LeafletJS
7. Klik **"💾 Simpan Data"**
### Menambah Data Parsil Baru
1. Klik tab **"Parsil Tanah"** di topbar
2. Klik **"✚ Tambah Data Baru"**
3. Gambar polygon di peta (min. 3 titik) atau isi koordinat manual
4. Luas tanah otomatis terhitung oleh LeafletJS
5. Isi nama parsil, nomor sertifikat, status kepemilikan, nama pemilik
6. Klik **"💾 Simpan Data"**
### Edit Data
1. Klik **"✏️ Edit"** pada kartu data di sidebar, atau klik fitur di peta → klik Edit di popup
2. Ubah data yang diperlukan, termasuk koordinat
3. Klik **"💾 Simpan Data"**
---
## 🔧 Mode Demo
Jika server PHP belum terkonfigurasi, aplikasi otomatis berjalan dalam **Mode Demo** dengan data sample. Semua operasi CRUD tetap berfungsi secara lokal di browser (tidak tersimpan ke database).
---
## 📚 Teknologi
- **Frontend**: HTML5, CSS3 (Custom Properties), Vanilla JavaScript
- **Peta**: LeafletJS 1.9.4 + Leaflet.draw 1.0.4
- **Backend**: PHP 7.4+ (REST API)
- **Database**: MySQL / MariaDB
- **Basemap**: OpenStreetMap, Esri World Imagery
- **Font**: Plus Jakarta Sans, DM Mono (Google Fonts)
+26
View File
@@ -0,0 +1,26 @@
<?php
// config.php - Konfigurasi Database
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'polyline_polygon');
function getDB() {
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
die(json_encode(['success' => false, 'message' => 'Koneksi database gagal: ' . $conn->connect_error]));
}
$conn->set_charset('utf8mb4');
return $conn;
}
// Set header untuk API
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
?>
+147
View File
@@ -0,0 +1,147 @@
<?php
// api/jalan.php - API CRUD untuk Data Jalan (Format GeoJSON)
require_once 'config.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
switch ($method) {
case 'GET': getJalan($id); break;
case 'POST': createJalan(); break;
case 'PUT': updateJalan($id); break;
case 'DELETE': deleteJalan($id); break;
default:
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']);
}
// ─────────────────────────────────────────────
// Helper: baris DB → array siap kirim ke client
// Mengembalikan struktur flat yang dipakai frontend,
// dengan field `koordinat` berisi [[lat,lng],...] agar
// Leaflet langsung bisa dipakai, PLUS field `geojson`
// berisi GeoJSON Feature asli.
// ─────────────────────────────────────────────
function rowToResponse($row) {
$feature = json_decode($row['geojson'], true);
// GeoJSON coords = [lng, lat] → ubah ke [lat, lng] untuk Leaflet
$coords_geojson = $feature['geometry']['coordinates'] ?? [];
$koordinat_leaflet = array_map(fn($c) => [$c[1], $c[0]], $coords_geojson);
return [
'id' => (int)$row['id'],
'nama_jalan' => $row['nama_jalan'],
'status_jalan' => $row['status_jalan'],
'panjang_meter' => (float)$row['panjang_meter'],
'koordinat' => $koordinat_leaflet, // [lat,lng] untuk Leaflet
'geojson' => $feature, // GeoJSON Feature asli
'created_at' => $row['created_at'],
'updated_at' => $row['updated_at'],
];
}
// ─────────────────────────────────────────────
// Helper: koordinat [[lat,lng],...] → GeoJSON Feature string
// ─────────────────────────────────────────────
function buildGeoJSON($nama, $status, $panjang, $koordinat_leaflet) {
// Leaflet: [lat, lng] → GeoJSON: [lng, lat]
$coords = array_map(fn($c) => [(float)$c[1], (float)$c[0]], $koordinat_leaflet);
$feature = [
'type' => 'Feature',
'properties' => [
'nama_jalan' => $nama,
'status_jalan' => $status,
'panjang_meter' => (float)$panjang,
],
'geometry' => [
'type' => 'LineString',
'coordinates' => $coords,
],
];
return json_encode($feature, JSON_UNESCAPED_UNICODE);
}
function getJalan($id = null) {
$db = getDB();
if ($id) {
$stmt = $db->prepare("SELECT * FROM jalan WHERE id = ?");
$stmt->bind_param('i', $id);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if ($row) {
echo json_encode(['success' => true, 'data' => rowToResponse($row)]);
} else {
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
}
} else {
$result = $db->query("SELECT * FROM jalan ORDER BY created_at DESC");
$rows = [];
while ($row = $result->fetch_assoc()) {
$rows[] = rowToResponse($row);
}
echo json_encode(['success' => true, 'data' => $rows, 'total' => count($rows)]);
}
$db->close();
}
function createJalan() {
$db = getDB();
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['nama_jalan']) || empty($input['status_jalan']) || empty($input['koordinat'])) {
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
return;
}
$nama = $db->real_escape_string($input['nama_jalan']);
$status = $db->real_escape_string($input['status_jalan']);
$panjang = (float)($input['panjang_meter'] ?? 0);
$geojson = buildGeoJSON($nama, $status, $panjang, $input['koordinat']);
$stmt = $db->prepare("INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, geojson) VALUES (?, ?, ?, ?)");
$stmt->bind_param('ssds', $nama, $status, $panjang, $geojson);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Data jalan berhasil disimpan', 'id' => $db->insert_id]);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $db->error]);
}
$db->close();
}
function updateJalan($id) {
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
$db = getDB();
$input = json_decode(file_get_contents('php://input'), true);
$nama = $db->real_escape_string($input['nama_jalan']);
$status = $db->real_escape_string($input['status_jalan']);
$panjang = (float)($input['panjang_meter'] ?? 0);
$geojson = buildGeoJSON($nama, $status, $panjang, $input['koordinat']);
$stmt = $db->prepare("UPDATE jalan SET nama_jalan=?, status_jalan=?, panjang_meter=?, geojson=?, updated_at=NOW() WHERE id=?");
$stmt->bind_param('ssdsi', $nama, $status, $panjang, $geojson, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Data jalan berhasil diperbarui']);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal memperbarui data: ' . $db->error]);
}
$db->close();
}
function deleteJalan($id) {
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
$db = getDB();
$stmt = $db->prepare("DELETE FROM jalan WHERE id = ?");
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Data jalan berhasil dihapus']);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal menghapus data']);
}
$db->close();
}
?>
+245
View File
@@ -0,0 +1,245 @@
<?php
// api/laporan.php - API CRUD untuk Laporan Kerusakan Jalan (Format GeoJSON Point)
require_once 'config.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
switch ($method) {
case 'GET': getLaporan($id); break;
case 'POST': createLaporan(); break;
case 'PUT': updateLaporan($id); break;
case 'DELETE': deleteLaporan($id); break;
default:
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']);
}
// ─────────────────────────────────────────────
// Info anggaran berdasarkan status jalan
// ─────────────────────────────────────────────
function getInfoAnggaran($status_jalan) {
switch ($status_jalan) {
case 'Jalan Nasional':
return ['pengelola' => 'Kementerian PUPR', 'sumber' => 'APBN'];
case 'Jalan Provinsi':
return ['pengelola' => 'Dinas PUPR Provinsi', 'sumber' => 'APBD Provinsi'];
case 'Jalan Kabupaten':
return ['pengelola' => 'Dinas PUPR Kabupaten/Kota', 'sumber' => 'APBD Kabupaten'];
default:
return ['pengelola' => 'Tidak Diketahui', 'sumber' => '-'];
}
}
// ─────────────────────────────────────────────
// Helper: baris DB → array siap kirim ke client
// ─────────────────────────────────────────────
function rowToResponse($row) {
$feature = json_decode($row['geojson'], true);
$coords_geojson = $feature['geometry']['coordinates'] ?? [0, 0];
return [
'id' => (int)$row['id'],
'jalan_id' => (int)$row['jalan_id'],
'nama_jalan' => $row['nama_jalan'],
'status_jalan' => $row['status_jalan'],
'info_anggaran' => getInfoAnggaran($row['status_jalan']),
'nama_pelapor' => $row['nama_pelapor'],
'kategori_rusak' => $row['kategori_rusak'],
'tanggal_laporan' => $row['tanggal_laporan'],
'keterangan' => $row['keterangan'],
'foto_url' => $row['foto_url'],
'lat' => $coords_geojson[1],
'lng' => $coords_geojson[0],
'geojson' => $feature,
'created_at' => $row['created_at'],
'updated_at' => $row['updated_at'],
];
}
function getLaporan($id = null) {
$db = getDB();
// Query parameter opsional: filter bulan, tahun, jalan_id
$where = ['1=1'];
$params = [];
$types = '';
if (isset($_GET['bulan_terakhir'])) {
$n = (int)$_GET['bulan_terakhir'];
$where[] = "tanggal_laporan >= DATE_SUB(CURDATE(), INTERVAL ? MONTH)";
$params[] = $n;
$types .= 'i';
}
if (isset($_GET['tahun_terakhir'])) {
$n = (int)$_GET['tahun_terakhir'];
$where[] = "tanggal_laporan >= DATE_SUB(CURDATE(), INTERVAL ? YEAR)";
$params[] = $n;
$types .= 'i';
}
if (isset($_GET['jalan_id'])) {
$where[] = "jalan_id = ?";
$params[] = (int)$_GET['jalan_id'];
$types .= 'i';
}
if (isset($_GET['kategori'])) {
$where[] = "kategori_rusak = ?";
$params[] = $_GET['kategori'];
$types .= 's';
}
if ($id) {
$stmt = $db->prepare("SELECT * FROM laporan_kerusakan WHERE id = ?");
$stmt->bind_param('i', $id);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if ($row) {
echo json_encode(['success' => true, 'data' => rowToResponse($row)]);
} else {
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
}
} else {
$sql = "SELECT * FROM laporan_kerusakan WHERE " . implode(' AND ', $where) . " ORDER BY tanggal_laporan DESC";
$stmt = $db->prepare($sql);
if ($params) {
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
$result = $stmt->get_result();
$rows = [];
while ($row = $result->fetch_assoc()) {
$rows[] = rowToResponse($row);
}
// Hitung jalan yang sering rusak (muncul >= 3 laporan dalam 3 tahun)
$res_sering = $db->query("
SELECT jalan_id, nama_jalan, COUNT(*) as jumlah
FROM laporan_kerusakan
WHERE tanggal_laporan >= DATE_SUB(CURDATE(), INTERVAL 3 YEAR)
GROUP BY jalan_id, nama_jalan
HAVING jumlah >= 3
ORDER BY jumlah DESC
");
$sering_rusak = [];
while ($sr = $res_sering->fetch_assoc()) {
$sering_rusak[] = ['jalan_id' => (int)$sr['jalan_id'], 'nama_jalan' => $sr['nama_jalan'], 'jumlah_laporan' => (int)$sr['jumlah']];
}
echo json_encode([
'success' => true,
'data' => $rows,
'total' => count($rows),
'sering_rusak' => $sering_rusak
]);
}
$db->close();
}
function createLaporan() {
$db = getDB();
// Cek apakah ada upload foto
$foto_url = null;
if (!empty($_FILES['foto']['tmp_name'])) {
$ext = pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION);
$filename = 'laporan_' . time() . '_' . rand(1000,9999) . '.' . strtolower($ext);
$upload_dir = '../uploads/laporan/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
if (move_uploaded_file($_FILES['foto']['tmp_name'], $upload_dir . $filename)) {
$foto_url = 'uploads/laporan/' . $filename;
}
}
// Baca JSON dari body (jika bukan multipart)
$input = [];
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (strpos($contentType, 'application/json') !== false) {
$input = json_decode(file_get_contents('php://input'), true) ?? [];
// Foto bisa base64 embedded
if (!empty($input['foto_base64'])) {
$imgData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $input['foto_base64']));
$filename = 'laporan_' . time() . '_' . rand(1000,9999) . '.jpg';
$upload_dir = '../uploads/laporan/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
file_put_contents($upload_dir . $filename, $imgData);
$foto_url = 'uploads/laporan/' . $filename;
}
} else {
$input = $_POST;
}
if (empty($input['lat']) || empty($input['lng']) || empty($input['kategori_rusak'])) {
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap (lat, lng, kategori_rusak wajib)']);
return;
}
$lat = (float)$input['lat'];
$lng = (float)$input['lng'];
$jalan_id = (int)($input['jalan_id'] ?? 0);
$nama_jalan = $db->real_escape_string($input['nama_jalan'] ?? 'Tidak Diketahui');
$status_jalan = $db->real_escape_string($input['status_jalan'] ?? '');
$nama_pelapor = $db->real_escape_string($input['nama_pelapor'] ?? 'Anonim');
$kategori = $db->real_escape_string($input['kategori_rusak']);
$tanggal = $db->real_escape_string($input['tanggal_laporan'] ?? date('Y-m-d'));
$keterangan = $db->real_escape_string($input['keterangan'] ?? '');
// Build GeoJSON Point
$geojson = json_encode([
'type' => 'Feature',
'properties' => [
'nama_jalan' => $nama_jalan,
'status_jalan' => $status_jalan,
'kategori_rusak' => $kategori,
'tanggal_laporan'=> $tanggal,
'nama_pelapor' => $nama_pelapor,
],
'geometry' => [
'type' => 'Point',
'coordinates' => [$lng, $lat], // GeoJSON: [lng, lat]
],
], JSON_UNESCAPED_UNICODE);
$stmt = $db->prepare("INSERT INTO laporan_kerusakan (jalan_id, nama_jalan, status_jalan, nama_pelapor, kategori_rusak, tanggal_laporan, keterangan, foto_url, geojson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param('issssssss', $jalan_id, $nama_jalan, $status_jalan, $nama_pelapor, $kategori, $tanggal, $keterangan, $foto_url, $geojson);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Laporan berhasil disimpan', 'id' => $db->insert_id]);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan: ' . $db->error]);
}
$db->close();
}
function updateLaporan($id) {
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
$db = getDB();
$input = json_decode(file_get_contents('php://input'), true);
$kategori = $db->real_escape_string($input['kategori_rusak'] ?? '');
$tanggal = $db->real_escape_string($input['tanggal_laporan'] ?? date('Y-m-d'));
$keterangan = $db->real_escape_string($input['keterangan'] ?? '');
$stmt = $db->prepare("UPDATE laporan_kerusakan SET kategori_rusak=?, tanggal_laporan=?, keterangan=?, updated_at=NOW() WHERE id=?");
$stmt->bind_param('sssi', $kategori, $tanggal, $keterangan, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Laporan berhasil diperbarui']);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal memperbarui: ' . $db->error]);
}
$db->close();
}
function deleteLaporan($id) {
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
$db = getDB();
$stmt = $db->prepare("DELETE FROM laporan_kerusakan WHERE id = ?");
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Laporan berhasil dihapus']);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal menghapus: ' . $db->error]);
}
$db->close();
}
?>
+161
View File
@@ -0,0 +1,161 @@
<?php
// api/parsil.php - API CRUD untuk Data Parsil Tanah (Format GeoJSON)
require_once 'config.php';
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
switch ($method) {
case 'GET': getParsil($id); break;
case 'POST': createParsil(); break;
case 'PUT': updateParsil($id); break;
case 'DELETE': deleteParsil($id); break;
default:
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']);
}
// ─────────────────────────────────────────────
// Helper: baris DB → array siap kirim ke client
// ─────────────────────────────────────────────
function rowToResponse($row) {
$feature = json_decode($row['geojson'], true);
// GeoJSON Polygon coords = array of rings, ring[0] = outer ring
// Setiap titik [lng, lat] → ubah ke [lat, lng] untuk Leaflet
// Hilangkan titik penutup (sama dengan titik pertama) yang GeoJSON haruskan
$ring_geojson = $feature['geometry']['coordinates'][0] ?? [];
$koordinat_leaflet = array_map(fn($c) => [$c[1], $c[0]], $ring_geojson);
// Hapus titik penutup jika sama dengan titik pertama
$n = count($koordinat_leaflet);
if ($n > 1 && $koordinat_leaflet[0] === $koordinat_leaflet[$n - 1]) {
array_pop($koordinat_leaflet);
}
return [
'id' => (int)$row['id'],
'nama_parsil' => $row['nomor_sertifikat'] ?? $row['nama_pemilik'] ?? 'Parsil',
'nomor_sertifikat' => $row['nomor_sertifikat'],
'status_kepemilikan' => $row['status_kepemilikan'],
'luas_meter2' => (float)$row['luas_meter2'],
'koordinat' => $koordinat_leaflet, // [lat,lng] untuk Leaflet
'geojson' => $feature, // GeoJSON Feature asli
'nama_pemilik' => $row['nama_pemilik'],
'created_at' => $row['created_at'],
'updated_at' => $row['updated_at'],
];
}
// ─────────────────────────────────────────────
// Helper: koordinat [[lat,lng],...] → GeoJSON Feature string
// GeoJSON Polygon ring harus ditutup (titik pertama = terakhir)
// ─────────────────────────────────────────────
function buildGeoJSON($nosert, $status, $luas, $pemilik, $koordinat_leaflet) {
// Leaflet: [lat, lng] → GeoJSON: [lng, lat]
$coords = array_map(fn($c) => [(float)$c[1], (float)$c[0]], $koordinat_leaflet);
// Tutup ring: tambahkan titik pertama di akhir jika belum
if (!empty($coords) && $coords[0] !== $coords[count($coords) - 1]) {
$coords[] = $coords[0];
}
$feature = [
'type' => 'Feature',
'properties' => [
'nomor_sertifikat' => $nosert,
'status_kepemilikan' => $status,
'luas_meter2' => (float)$luas,
'nama_pemilik' => $pemilik,
],
'geometry' => [
'type' => 'Polygon',
'coordinates' => [$coords], // array of rings
],
];
return json_encode($feature, JSON_UNESCAPED_UNICODE);
}
function getParsil($id = null) {
$db = getDB();
if ($id) {
$stmt = $db->prepare("SELECT * FROM parsil_tanah WHERE id = ?");
$stmt->bind_param('i', $id);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if ($row) {
echo json_encode(['success' => true, 'data' => rowToResponse($row)]);
} else {
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
}
} else {
$result = $db->query("SELECT * FROM parsil_tanah ORDER BY created_at DESC");
$rows = [];
while ($row = $result->fetch_assoc()) {
$rows[] = rowToResponse($row);
}
echo json_encode(['success' => true, 'data' => $rows, 'total' => count($rows)]);
}
$db->close();
}
function createParsil() {
$db = getDB();
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['status_kepemilikan']) || empty($input['koordinat'])) {
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
return;
}
$nosert = $db->real_escape_string($input['nomor_sertifikat'] ?? '');
$status = $db->real_escape_string($input['status_kepemilikan']);
$luas = (float)($input['luas_meter2'] ?? 0);
$pemilik = $db->real_escape_string($input['nama_pemilik'] ?? '');
$geojson = buildGeoJSON($nosert, $status, $luas, $pemilik, $input['koordinat']);
$stmt = $db->prepare("INSERT INTO parsil_tanah (nomor_sertifikat, status_kepemilikan, luas_meter2, geojson, nama_pemilik) VALUES (?, ?, ?, ?, ?)");
$stmt->bind_param('ssdss', $nosert, $status, $luas, $geojson, $pemilik);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Data parsil berhasil disimpan', 'id' => $db->insert_id]);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $db->error]);
}
$db->close();
}
function updateParsil($id) {
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
$db = getDB();
$input = json_decode(file_get_contents('php://input'), true);
$nosert = $db->real_escape_string($input['nomor_sertifikat'] ?? '');
$status = $db->real_escape_string($input['status_kepemilikan']);
$luas = (float)($input['luas_meter2'] ?? 0);
$pemilik = $db->real_escape_string($input['nama_pemilik'] ?? '');
$geojson = buildGeoJSON($nosert, $status, $luas, $pemilik, $input['koordinat']);
$stmt = $db->prepare("UPDATE parsil_tanah SET nomor_sertifikat=?, status_kepemilikan=?, luas_meter2=?, geojson=?, nama_pemilik=?, updated_at=NOW() WHERE id=?");
$stmt->bind_param('ssdssi', $nosert, $status, $luas, $geojson, $pemilik, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Data parsil berhasil diperbarui']);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal memperbarui data: ' . $db->error]);
}
$db->close();
}
function deleteParsil($id) {
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
$db = getDB();
$stmt = $db->prepare("DELETE FROM parsil_tanah WHERE id = ?");
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Data parsil berhasil dihapus']);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal menghapus data']);
}
$db->close();
}
?>
+206
View File
@@ -0,0 +1,206 @@
-- ============================================
-- WebGIS Manajemen Jalan & Parsil Tanah
-- Database Schema — Format GeoJSON
-- ============================================
CREATE DATABASE IF NOT EXISTS polyline_polygon CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE polyline_polygon;
-- ============================================
-- Tabel Data Jalan (LineString GeoJSON)
-- ============================================
CREATE TABLE IF NOT EXISTS jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(255) NOT NULL,
status_jalan ENUM('Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten') NOT NULL,
panjang_meter DOUBLE DEFAULT 0,
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON Feature object with geometry type LineString',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- ============================================
-- Tabel Data Parsil Tanah (Polygon GeoJSON)
-- ============================================
CREATE TABLE IF NOT EXISTS parsil_tanah (
id INT AUTO_INCREMENT PRIMARY KEY,
nomor_sertifikat VARCHAR(100),
status_kepemilikan ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
luas_meter2 DOUBLE DEFAULT 0,
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON Feature object with geometry type Polygon',
nama_pemilik VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- ============================================
-- Tabel Laporan Kerusakan Jalan (Point GeoJSON)
-- ============================================
CREATE TABLE IF NOT EXISTS laporan_kerusakan (
id INT AUTO_INCREMENT PRIMARY KEY,
jalan_id INT DEFAULT 0 COMMENT 'FK ke tabel jalan (0 jika tidak terdeteksi)',
nama_jalan VARCHAR(255) DEFAULT 'Tidak Diketahui',
status_jalan VARCHAR(50) DEFAULT '',
nama_pelapor VARCHAR(255) DEFAULT 'Anonim',
kategori_rusak ENUM('Ringan', 'Sedang', 'Berat') NOT NULL,
tanggal_laporan DATE NOT NULL,
keterangan TEXT,
foto_url VARCHAR(500) DEFAULT NULL COMMENT 'Path foto kerusakan yang diupload',
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON Feature Point [lng, lat]',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_jalan_id (jalan_id),
INDEX idx_tanggal (tanggal_laporan),
INDEX idx_kategori (kategori_rusak)
);
-- ============================================
-- Sample Data Jalan
-- Catatan: GeoJSON menggunakan [lng, lat] (bukan [lat, lng])
-- ============================================
INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, geojson) VALUES
(
'Jalan Ahmad Yani',
'Jalan Nasional',
1250.5,
'{
"type": "Feature",
"properties": {
"nama_jalan": "Jalan Ahmad Yani",
"status_jalan": "Jalan Nasional",
"panjang_meter": 1250.5
},
"geometry": {
"type": "LineString",
"coordinates": [
[109.3300, -0.0220],
[109.3340, -0.0235],
[109.3375, -0.0250],
[109.3410, -0.0263]
]
}
}'
),
(
'Jalan Gajah Mada',
'Jalan Provinsi',
870.3,
'{
"type": "Feature",
"properties": {
"nama_jalan": "Jalan Gajah Mada",
"status_jalan": "Jalan Provinsi",
"panjang_meter": 870.3
},
"geometry": {
"type": "LineString",
"coordinates": [
[109.3280, -0.0280],
[109.3330, -0.0270],
[109.3380, -0.0260]
]
}
}'
),
(
'Jalan Pahlawan',
'Jalan Kabupaten',
540.7,
'{
"type": "Feature",
"properties": {
"nama_jalan": "Jalan Pahlawan",
"status_jalan": "Jalan Kabupaten",
"panjang_meter": 540.7
},
"geometry": {
"type": "LineString",
"coordinates": [
[109.3420, -0.0300],
[109.3450, -0.0290],
[109.3480, -0.0280]
]
}
}'
);
-- ============================================
-- Sample Data Parsil Tanah
-- Polygon: koordinat pertama dan terakhir harus sama (closed ring)
-- ============================================
INSERT INTO parsil_tanah (nomor_sertifikat, status_kepemilikan, luas_meter2, geojson, nama_pemilik) VALUES
(
'SHM-2024-001',
'SHM',
450.0,
'{
"type": "Feature",
"properties": {
"nomor_sertifikat": "SHM-2024-001",
"status_kepemilikan": "SHM",
"luas_meter2": 450.0,
"nama_pemilik": "Budi Santoso"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[109.3355, -0.0255],
[109.3370, -0.0255],
[109.3370, -0.0245],
[109.3355, -0.0245],
[109.3355, -0.0255]
]]
}
}',
'Budi Santoso'
),
(
'HGB-2024-002',
'HGB',
620.5,
'{
"type": "Feature",
"properties": {
"nomor_sertifikat": "HGB-2024-002",
"status_kepemilikan": "HGB",
"luas_meter2": 620.5,
"nama_pemilik": "PT. Maju Jaya"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[109.3395, -0.0270],
[109.3415, -0.0270],
[109.3415, -0.0258],
[109.3395, -0.0258],
[109.3395, -0.0270]
]]
}
}',
'PT. Maju Jaya'
),
(
'HGU-2024-003',
'HGU',
1200.0,
'{
"type": "Feature",
"properties": {
"nomor_sertifikat": "HGU-2024-003",
"status_kepemilikan": "HGU",
"luas_meter2": 1200.0,
"nama_pemilik": "CV. Agro Lestari"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[109.3310, -0.0285],
[109.3335, -0.0285],
[109.3335, -0.0268],
[109.3310, -0.0268],
[109.3310, -0.0285]
]]
}
}',
'CV. Agro Lestari'
);
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
<?php
require_once __DIR__ . '/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
jsonResponse(['error' => 'Method tidak didukung'], 405);
}
$admin = requireRole('admin');
$db = getDB();
// Pagination
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = min(100, max(10, (int)($_GET['limit'] ?? 50)));
$offset = ($page - 1) * $limit;
// Filter
$filterAksi = $_GET['aksi'] ?? null;
$filterUser = $_GET['user_id'] ?? null;
$where = [];
$params = [];
if ($filterAksi) {
$where[] = "al.aksi LIKE ?";
$params[] = "%$filterAksi%";
}
if ($filterUser) {
$where[] = "al.user_id = ?";
$params[] = (int)$filterUser;
}
$whereClause = count($where) > 0 ? 'WHERE ' . implode(' AND ', $where) : '';
// Total count
$countSql = "SELECT COUNT(*) FROM audit_logs al $whereClause";
$countStmt = $db->prepare($countSql);
$countStmt->execute($params);
$total = (int)$countStmt->fetchColumn();
// Data
$sql = "
SELECT al.*, u.username, u.nama_lengkap
FROM audit_logs al
LEFT JOIN users u ON u.id = al.user_id
$whereClause
ORDER BY al.created_at DESC
LIMIT $limit OFFSET $offset
";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
jsonResponse([
'data' => $rows,
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit),
]);
+183
View File
@@ -0,0 +1,183 @@
<?php
require_once __DIR__ . '/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$db = getDB();
$action = $_GET['action'] ?? '';
// ══════════════════════════════════════════════
// LOGIN
// ══════════════════════════════════════════════
if ($action === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$username = trim($body['username'] ?? '');
$password = $body['password'] ?? '';
if (!$username || !$password) {
jsonResponse(['success' => false, 'error' => 'Username dan password wajib diisi'], 422);
}
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND is_active = 1");
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
jsonResponse(['success' => false, 'error' => 'Username atau password salah'], 401);
}
// Set session
$_SESSION['user'] = [
'id' => (int)$user['id'],
'username' => $user['username'],
'nama_lengkap' => $user['nama_lengkap'],
'role' => $user['role'],
];
auditLog($db, (int)$user['id'], 'LOGIN', 'users', (int)$user['id']);
jsonResponse([
'success' => true,
'user' => $_SESSION['user']
]);
}
// ══════════════════════════════════════════════
// LOGOUT
// ══════════════════════════════════════════════
if ($action === 'logout' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$user = getCurrentUser();
if ($user) {
auditLog($db, $user['id'], 'LOGOUT', 'users', $user['id']);
}
session_destroy();
jsonResponse(['success' => true]);
}
// ══════════════════════════════════════════════
// ME — cek session aktif
// ══════════════════════════════════════════════
if ($action === 'me' && $_SERVER['REQUEST_METHOD'] === 'GET') {
$user = getCurrentUser();
if (!$user) {
jsonResponse(['success' => false, 'logged_in' => false], 200);
}
jsonResponse(['success' => true, 'logged_in' => true, 'user' => $user]);
}
// ══════════════════════════════════════════════
// REGISTER — admin only
// ══════════════════════════════════════════════
if ($action === 'register' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$admin = requireRole('admin');
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$username = trim($body['username'] ?? '');
$password = $body['password'] ?? '';
$nama_lengkap = trim($body['nama_lengkap'] ?? '');
$role = $body['role'] ?? 'surveyor';
if (!$username || !$password || !$nama_lengkap) {
jsonResponse(['success' => false, 'error' => 'Semua field wajib diisi'], 422);
}
if (strlen($password) < 6) {
jsonResponse(['success' => false, 'error' => 'Password minimal 6 karakter'], 422);
}
if (!in_array($role, ['admin', 'surveyor', 'pemangku'])) {
jsonResponse(['success' => false, 'error' => 'Role tidak valid'], 422);
}
// Cek username unik
$check = $db->prepare("SELECT id FROM users WHERE username = ?");
$check->execute([$username]);
if ($check->fetch()) {
jsonResponse(['success' => false, 'error' => 'Username sudah digunakan'], 409);
}
$hashed = password_hash($password, PASSWORD_BCRYPT);
$stmt = $db->prepare("INSERT INTO users (username, password, nama_lengkap, role) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $hashed, $nama_lengkap, $role]);
$newId = (int)$db->lastInsertId();
auditLog($db, $admin['id'], 'CREATE_USER', 'users', $newId, null, [
'username' => $username, 'nama_lengkap' => $nama_lengkap, 'role' => $role
]);
jsonResponse(['success' => true, 'id' => $newId]);
}
// ══════════════════════════════════════════════
// LIST USERS — admin only
// ══════════════════════════════════════════════
if ($action === 'users' && $_SERVER['REQUEST_METHOD'] === 'GET') {
requireRole('admin');
$rows = $db->query("SELECT id, username, nama_lengkap, role, is_active, created_at FROM users ORDER BY created_at DESC")->fetchAll();
jsonResponse(['data' => $rows]);
}
// ══════════════════════════════════════════════
// UPDATE USER — admin only
// ══════════════════════════════════════════════
if ($action === 'update' && $_SERVER['REQUEST_METHOD'] === 'PUT') {
$admin = requireRole('admin');
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
// Ambil data lama
$old = $db->prepare("SELECT id, username, nama_lengkap, role, is_active FROM users WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success' => false, 'error' => 'User tidak ditemukan'], 404);
$fields = []; $vals = [];
if (!empty($body['nama_lengkap'])) {
$fields[] = "nama_lengkap = ?"; $vals[] = trim($body['nama_lengkap']);
}
if (!empty($body['role']) && in_array($body['role'], ['admin','surveyor','pemangku'])) {
$fields[] = "role = ?"; $vals[] = $body['role'];
}
if (isset($body['is_active'])) {
$fields[] = "is_active = ?"; $vals[] = $body['is_active'] ? 1 : 0;
}
if (!empty($body['password']) && strlen($body['password']) >= 6) {
$fields[] = "password = ?"; $vals[] = password_hash($body['password'], PASSWORD_BCRYPT);
}
if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data diubah'], 422);
$vals[] = $id;
$db->prepare("UPDATE users SET " . implode(',', $fields) . " WHERE id = ?")->execute($vals);
auditLog($db, $admin['id'], 'UPDATE_USER', 'users', $id, $oldData, $body);
jsonResponse(['success' => true]);
}
// ══════════════════════════════════════════════
// DELETE (deactivate) USER — admin only
// ══════════════════════════════════════════════
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'DELETE') {
$admin = requireRole('admin');
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
if ($id === $admin['id']) {
jsonResponse(['success' => false, 'error' => 'Tidak bisa menghapus akun sendiri'], 400);
}
$db->prepare("UPDATE users SET is_active = 0 WHERE id = ?")->execute([$id]);
auditLog($db, $admin['id'], 'DEACTIVATE_USER', 'users', $id);
jsonResponse(['success' => true]);
}
jsonResponse(['error' => 'Action tidak valid'], 400);
+106
View File
@@ -0,0 +1,106 @@
<?php
// ── Konfigurasi Database ──────────────────────
// Sesuaikan dengan pengaturan XAMPP Anda
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // default XAMPP
define('DB_PASS', ''); // default XAMPP (kosong)
define('DB_NAME', 'webgis_poverty_mapping');
define('DB_CHARSET', 'utf8mb4');
// ── Upload Config ─────────────────────────────
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
define('MAX_UPLOAD_SIZE', 5 * 1024 * 1024); // 5MB
// ── Session ───────────────────────────────────
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function getDB(): PDO {
static $pdo = null;
if ($pdo === null) {
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=%s', DB_HOST, DB_NAME, 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(['success' => false, 'error' => 'Koneksi DB gagal: ' . $e->getMessage()]));
}
}
return $pdo;
}
// ── JSON response helper ──────────────────────
function jsonResponse(array $data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin) {
header('Access-Control-Allow-Origin: ' . $origin);
} else {
header('Access-Control-Allow-Origin: *');
}
header('Access-Control-Allow-Credentials: true');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// ── Auth Helpers ──────────────────────────────
/**
* Cek apakah user sudah login.
* Return data user atau null.
*/
function getCurrentUser(): ?array {
return $_SESSION['user'] ?? null;
}
/**
* Paksa login. Jika belum login, return 401.
*/
function requireAuth(): array {
$user = getCurrentUser();
if (!$user) {
jsonResponse(['success' => false, 'error' => 'Unauthorized — silakan login'], 401);
}
return $user;
}
/**
* Paksa role tertentu. Jika role tidak sesuai, return 403.
* @param array|string $roles Role yang diizinkan
*/
function requireRole($roles): array {
$user = requireAuth();
if (is_string($roles)) $roles = [$roles];
if (!in_array($user['role'], $roles)) {
jsonResponse(['success' => false, 'error' => 'Forbidden — Anda tidak memiliki akses'], 403);
}
return $user;
}
// ── Audit Log Helper ──────────────────────────
/**
* Catat aktivitas ke audit_logs
*/
function auditLog(PDO $db, int $userId, string $aksi, ?string $targetType = null, ?int $targetId = null, $before = null, $after = null): void {
$stmt = $db->prepare("
INSERT INTO audit_logs (user_id, aksi, target_type, target_id, data_before, data_after, ip_address)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$userId,
$aksi,
$targetType,
$targetId,
$before ? json_encode($before, JSON_UNESCAPED_UNICODE) : null,
$after ? json_encode($after, JSON_UNESCAPED_UNICODE) : null,
$_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'
]);
}
+37
View File
@@ -0,0 +1,37 @@
<?php
require_once __DIR__ . '/config.php';
// ── Haversine distance (meter) ──────────────
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371000;
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
}
// ── Cari rumah ibadah terdekat yang menjangkau koordinat ──
// Hanya rumah ibadah yang sudah verified
function findNearestIbadah(PDO $db, float $lat, float $lng): ?int {
$ibadahs = $db->query("SELECT id, lat, lng, radius FROM rumah_ibadah WHERE status = 'verified'")->fetchAll();
$nearest = null;
$minDist = PHP_FLOAT_MAX;
foreach ($ibadahs as $ib) {
$d = haversine($lat, $lng, (float)$ib['lat'], (float)$ib['lng']);
if ($d <= $ib['radius'] && $d < $minDist) {
$nearest = (int)$ib['id'];
$minDist = $d;
}
}
return $nearest;
}
// ── Re-assign SEMUA warga verified (dipanggil saat radius/ibadah berubah) ──
function reassignAllWarga(PDO $db): void {
$wargas = $db->query("SELECT id, lat, lng FROM warga_miskin WHERE status = 'verified'")->fetchAll();
$stmt = $db->prepare("UPDATE warga_miskin SET ibadah_id = ? WHERE id = ?");
foreach ($wargas as $w) {
$ibadahId = findNearestIbadah($db, (float)$w['lat'], (float)$w['lng']);
$stmt->execute([$ibadahId, $w['id']]);
}
}
+181
View File
@@ -0,0 +1,181 @@
<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
// ── GET: list semua / satu ───────────────────
if ($method === 'GET') {
$user = getCurrentUser();
if ($id) {
$stmt = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
jsonResponse($row ?: ['error' => 'Tidak ditemukan'], $row ? 200 : 404);
}
// Admin, Surveyor, & Pemangku melihat semua, lainnya hanya verified
if ($user && ($user['role'] === 'admin' || $user['role'] === 'surveyor' || $user['role'] === 'pemangku')) {
$rows = $db->query("
SELECT ri.*,
COUNT(wm.id) AS jumlah_kk,
COALESCE(SUM(wm.jumlah_anggota),0) AS jumlah_jiwa,
u.nama_lengkap AS surveyor_nama
FROM rumah_ibadah ri
LEFT JOIN warga_miskin wm ON wm.ibadah_id = ri.id AND wm.status = 'verified'
LEFT JOIN users u ON u.id = ri.surveyor_id
GROUP BY ri.id
ORDER BY ri.created_at DESC
")->fetchAll();
} else {
// Pemangku / publik: hanya verified
$rows = $db->query("
SELECT ri.*,
COUNT(wm.id) AS jumlah_kk,
COALESCE(SUM(wm.jumlah_anggota),0) AS jumlah_jiwa
FROM rumah_ibadah ri
LEFT JOIN warga_miskin wm ON wm.ibadah_id = ri.id AND wm.status = 'verified'
WHERE ri.status = 'verified'
GROUP BY ri.id
ORDER BY ri.created_at DESC
")->fetchAll();
}
jsonResponse(['data' => $rows]);
}
// ── POST: tambah ─────────────────────────────
if ($method === 'POST') {
$user = requireRole(['admin', 'surveyor']);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$required = ['nama','jenis','pic','lat','lng'];
foreach ($required as $f) {
if (empty($body[$f])) jsonResponse(['success'=>false,'error'=>"Field '$f' wajib diisi"], 422);
}
// Surveyor → pending, Admin → verified
$status = ($user['role'] === 'admin') ? 'verified' : 'pending';
$stmt = $db->prepare("
INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, radius, lat, lng, foto_path, surveyor_id, status, kecamatan)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$body['nama'],
$body['jenis'],
$body['alamat'] ?? '',
$body['pic'],
$body['no_wa'] ?? '',
(int)($body['radius'] ?? 500),
(float)$body['lat'],
(float)$body['lng'],
$body['foto_path'] ?? null,
$user['id'],
$status,
$body['kecamatan'] ?? null,
]);
$newId = (int)$db->lastInsertId();
// re-assign warga hanya jika langsung verified
if ($status === 'verified') {
reassignAllWarga($db);
}
auditLog($db, $user['id'], 'CREATE_IBADAH', 'rumah_ibadah', $newId, null, $body);
jsonResponse(['success'=>true, 'id'=>$newId, 'status'=>$status]);
}
// ── PUT: update ──────────────────────────────
if ($method === 'PUT') {
$user = requireAuth();
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
// Cek permission
$old = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
// Surveyor hanya bisa edit milik sendiri yang masih pending
if ($user['role'] === 'surveyor') {
if ((int)$oldData['surveyor_id'] !== $user['id']) {
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa edit data milik Anda'], 403);
}
if ($oldData['status'] !== 'pending') {
jsonResponse(['success'=>false,'error'=>'Data yang sudah diverifikasi tidak bisa diedit'], 403);
}
}
// Pemangku tidak boleh edit
if ($user['role'] === 'pemangku') {
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses edit'], 403);
}
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$fields = []; $vals = [];
foreach (['nama','jenis','alamat','pic','no_wa','radius','lat','lng','foto_path','kecamatan'] as $f) {
if (array_key_exists($f, $body)) {
$fields[] = "$f = ?";
$vals[] = in_array($f,['lat','lng']) ? (float)$body[$f] : ($f==='radius' ? (int)$body[$f] : $body[$f]);
}
}
if (empty($fields)) jsonResponse(['success'=>false,'error'=>'Tidak ada data diubah'], 422);
$vals[] = $id;
$db->prepare("UPDATE rumah_ibadah SET ".implode(',',$fields)." WHERE id = ?")->execute($vals);
// re-assign ulang jika data verified
if ($oldData['status'] === 'verified') {
reassignAllWarga($db);
}
auditLog($db, $user['id'], 'UPDATE_IBADAH', 'rumah_ibadah', $id, $oldData, $body);
jsonResponse(['success'=>true]);
}
// ── DELETE ───────────────────────────────────
if ($method === 'DELETE') {
$user = requireAuth();
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
$old = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
// Surveyor bisa hapus milik sendiri
if ($user['role'] === 'surveyor') {
if ((int)$oldData['surveyor_id'] !== $user['id']) {
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa hapus data milik Anda'], 403);
}
}
if ($user['role'] === 'pemangku') {
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses hapus'], 403);
}
// warga_miskin.ibadah_id akan SET NULL (FK ON DELETE SET NULL)
$db->prepare("DELETE FROM rumah_ibadah WHERE id = ?")->execute([$id]);
// Jika rumah ibadah terverifikasi dihapus, reassign warga sekitar
if ($oldData['status'] === 'verified') {
reassignAllWarga($db);
}
auditLog($db, $user['id'], 'DELETE_IBADAH', 'rumah_ibadah', $id, $oldData);
jsonResponse(['success'=>true]);
}
jsonResponse(['error'=>'Method tidak didukung'], 405);
+74
View File
@@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Credentials: true');
$db = getDB();
$user = getCurrentUser();
// Statistik utama — hanya data verified (digabung dalam 1 query)
$stats = $db->query("
SELECT
(SELECT COUNT(*) FROM rumah_ibadah WHERE status = 'verified') AS ibadah,
COUNT(*) AS kk_total,
COALESCE(SUM(CASE WHEN ibadah_id IS NOT NULL THEN 1 ELSE 0 END), 0) AS kk_terlayani,
COALESCE(SUM(jumlah_anggota), 0) AS jiwa_total,
COALESCE(SUM(CASE WHEN ibadah_id IS NOT NULL THEN jumlah_anggota ELSE 0 END), 0) AS jiwa_terlayani,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'sembako' THEN 1 ELSE 0 END), 0) AS bantuan_sembako,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'beasiswa' THEN 1 ELSE 0 END), 0) AS bantuan_beasiswa,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'modal' THEN 1 ELSE 0 END), 0) AS bantuan_modal,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'tunai' THEN 1 ELSE 0 END), 0) AS bantuan_tunai,
COALESCE(SUM(CASE WHEN bantuan_diterima = 'tidak_ada' THEN 1 ELSE 0 END), 0) AS bantuan_tidak_ada
FROM warga_miskin
WHERE status = 'verified'
")->fetch(PDO::FETCH_ASSOC);
$totalIbadah = (int)($stats['ibadah'] ?? 0);
$totalWarga = (int)($stats['kk_total'] ?? 0);
$totalCovered = (int)($stats['kk_terlayani'] ?? 0);
$totalJiwa = (int)($stats['jiwa_total'] ?? 0);
$jiwaLayan = (int)($stats['jiwa_terlayani'] ?? 0);
$response = [
'ibadah' => $totalIbadah,
'kk_total' => $totalWarga,
'kk_terlayani' => $totalCovered,
'kk_belum' => $totalWarga - $totalCovered,
'jiwa_total' => $totalJiwa,
'jiwa_terlayani' => $jiwaLayan,
'bantuan_sembako' => (int)($stats['bantuan_sembako'] ?? 0),
'bantuan_beasiswa' => (int)($stats['bantuan_beasiswa'] ?? 0),
'bantuan_modal' => (int)($stats['bantuan_modal'] ?? 0),
'bantuan_tunai' => (int)($stats['bantuan_tunai'] ?? 0),
'bantuan_tidak_ada' => (int)($stats['bantuan_tidak_ada'] ?? 0),
];
// Data pending — hanya untuk admin (digabung dalam 1 query)
if ($user && $user['role'] === 'admin') {
$adminStats = $db->query("
SELECT
(SELECT COUNT(*) FROM rumah_ibadah WHERE status = 'pending') AS pending_ibadah,
(SELECT COUNT(*) FROM warga_miskin WHERE status = 'pending') AS pending_warga,
(SELECT COUNT(*) FROM users WHERE is_active = 1) AS total_users
")->fetch(PDO::FETCH_ASSOC);
$response['pending_ibadah'] = (int)($adminStats['pending_ibadah'] ?? 0);
$response['pending_warga'] = (int)($adminStats['pending_warga'] ?? 0);
$response['total_users'] = (int)($adminStats['total_users'] ?? 0);
}
// Data pending milik surveyor (digabung dalam 1 query)
if ($user && $user['role'] === 'surveyor') {
$stmt = $db->prepare("
SELECT
(SELECT COUNT(*) FROM warga_miskin WHERE surveyor_id = ? AND status = 'pending') AS my_pending_warga,
(SELECT COUNT(*) FROM rumah_ibadah WHERE surveyor_id = ? AND status = 'pending') AS my_pending_ibadah
");
$stmt->execute([$user['id'], $user['id']]);
$surveyorStats = $stmt->fetch(PDO::FETCH_ASSOC);
$response['my_pending_warga'] = (int)($surveyorStats['my_pending_warga'] ?? 0);
$response['my_pending_ibadah'] = (int)($surveyorStats['my_pending_ibadah'] ?? 0);
}
jsonResponse($response);
+118
View File
@@ -0,0 +1,118 @@
<?php
require_once __DIR__ . '/config.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method tidak didukung'], 405);
}
$user = requireRole(['admin', 'surveyor']);
// ── Validasi file ────────────────────────────
if (!isset($_FILES['foto']) || $_FILES['foto']['error'] !== UPLOAD_ERR_OK) {
jsonResponse(['success' => false, 'error' => 'File foto wajib diunggah'], 422);
}
$file = $_FILES['foto'];
// Cek ukuran
if ($file['size'] > MAX_UPLOAD_SIZE) {
jsonResponse(['success' => false, 'error' => 'Ukuran file melebihi batas 5MB'], 422);
}
// Cek tipe file
$allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
jsonResponse(['success' => false, 'error' => 'Hanya file JPEG dan PNG yang diizinkan'], 422);
}
// ── Buat folder uploads jika belum ada ───────
if (!is_dir(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0755, true);
}
// ── Generate nama file unik ──────────────────
$ext = ($mimeType === 'image/png') ? 'png' : 'jpg';
$filename = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$destPath = UPLOAD_DIR . $filename;
$webPath = 'uploads/' . $filename;
// ── Pindahkan file ───────────────────────────
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
jsonResponse(['success' => false, 'error' => 'Gagal menyimpan file'], 500);
}
// ── Extract EXIF GPS ─────────────────────────
$lat = null;
$lng = null;
$hasGps = false;
if ($mimeType === 'image/jpeg' || $mimeType === 'image/jpg') {
$exif = @exif_read_data($destPath, 'GPS', true);
if ($exif && isset($exif['GPS']['GPSLatitude']) && isset($exif['GPS']['GPSLongitude'])) {
$lat = exifGpsToDecimal(
$exif['GPS']['GPSLatitude'],
$exif['GPS']['GPSLatitudeRef'] ?? 'N'
);
$lng = exifGpsToDecimal(
$exif['GPS']['GPSLongitude'],
$exif['GPS']['GPSLongitudeRef'] ?? 'E'
);
$hasGps = true;
}
}
auditLog(getDB(), $user['id'], 'UPLOAD_FOTO', null, null, null, [
'filename' => $filename, 'has_gps' => $hasGps
]);
jsonResponse([
'success' => true,
'foto_path' => $webPath,
'lat' => $lat,
'lng' => $lng,
'has_gps' => $hasGps,
]);
// ══════════════════════════════════════════════
// EXIF GPS Helper
// ══════════════════════════════════════════════
/**
* Konversi EXIF GPS coordinate (DMS) ke desimal
* @param array $coord Array of 3 fractions [degrees, minutes, seconds]
* @param string $ref Hemisphere reference (N/S/E/W)
*/
function exifGpsToDecimal(array $coord, string $ref): float {
$degrees = exifFractionToFloat($coord[0]);
$minutes = exifFractionToFloat($coord[1]);
$seconds = exifFractionToFloat($coord[2]);
$decimal = $degrees + ($minutes / 60) + ($seconds / 3600);
if ($ref === 'S' || $ref === 'W') {
$decimal *= -1;
}
return round($decimal, 8);
}
/**
* Konversi fraction string "num/den" ke float
*/
function exifFractionToFloat(string $fraction): float {
$parts = explode('/', $fraction);
if (count($parts) === 2 && (float)$parts[1] !== 0.0) {
return (float)$parts[0] / (float)$parts[1];
}
return (float)$parts[0];
}
+99
View File
@@ -0,0 +1,99 @@
<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$type = $_GET['type'] ?? ''; // 'ibadah' atau 'warga'
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
if (!in_array($type, ['ibadah', 'warga'])) {
jsonResponse(['error' => 'Parameter type harus "ibadah" atau "warga"'], 400);
}
$table = $type === 'ibadah' ? 'rumah_ibadah' : 'warga_miskin';
// ── GET: list data pending ───────────────────
if ($method === 'GET') {
$admin = requireRole('admin');
$status = $_GET['status'] ?? 'pending';
if (!in_array($status, ['pending', 'verified', 'rejected'])) $status = 'pending';
if ($type === 'ibadah') {
$stmt = $db->prepare("
SELECT ri.*, u.nama_lengkap AS surveyor_nama
FROM rumah_ibadah ri
LEFT JOIN users u ON u.id = ri.surveyor_id
WHERE ri.status = ?
ORDER BY ri.created_at DESC
");
} else {
$stmt = $db->prepare("
SELECT wm.*,
ri.nama AS ibadah_nama,
u.nama_lengkap AS surveyor_nama
FROM warga_miskin wm
LEFT JOIN rumah_ibadah ri ON ri.id = wm.ibadah_id
LEFT JOIN users u ON u.id = wm.surveyor_id
WHERE wm.status = ?
ORDER BY wm.created_at DESC
");
}
$stmt->execute([$status]);
jsonResponse(['data' => $stmt->fetchAll()]);
}
// ── PUT: approve / reject ────────────────────
if ($method === 'PUT') {
$admin = requireRole('admin');
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$newStatus = $body['status'] ?? '';
if (!in_array($newStatus, ['verified', 'rejected'])) {
jsonResponse(['success' => false, 'error' => 'Status harus "verified" atau "rejected"'], 422);
}
// Ambil data lama
$old = $db->prepare("SELECT * FROM $table WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) {
jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404);
}
// Update status
$alasan = ($newStatus === 'rejected') ? ($body['alasan_penolakan'] ?? '') : null;
$stmt = $db->prepare("UPDATE $table SET status = ?, alasan_penolakan = ? WHERE id = ?");
$stmt->execute([$newStatus, $alasan, $id]);
// Jika diverifikasi, proses spasial
if ($newStatus === 'verified') {
if ($type === 'warga') {
// Cari ibadah terdekat untuk warga yang baru verified
$ibadahId = findNearestIbadah($db, (float)$oldData['lat'], (float)$oldData['lng']);
$db->prepare("UPDATE warga_miskin SET ibadah_id = ? WHERE id = ?")->execute([$ibadahId, $id]);
} elseif ($type === 'ibadah') {
// Ibadah baru verified → re-assign semua warga
reassignAllWarga($db);
}
}
$aksi = ($newStatus === 'verified') ? 'VERIFY_' : 'REJECT_';
$aksi .= strtoupper($type);
auditLog($db, $admin['id'], $aksi, $table, $id, $oldData, [
'status' => $newStatus, 'alasan_penolakan' => $alasan
]);
jsonResponse(['success' => true, 'status' => $newStatus]);
}
jsonResponse(['error' => 'Method tidak didukung'], 405);
+233
View File
@@ -0,0 +1,233 @@
<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/helpers.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Credentials: true');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
// ── GET ───────────────────────────────────────
if ($method === 'GET') {
$user = getCurrentUser();
// Filter tambahan: kecamatan
$filterKec = $_GET['kecamatan'] ?? null;
$where = [];
$params = [];
if ($filterKec) { $where[] = "wm.kecamatan = ?"; $params[] = $filterKec; }
if ($user && ($user['role'] === 'admin' || $user['role'] === 'surveyor' || $user['role'] === 'pemangku')) {
// Admin, Surveyor, & Pemangku lihat semua data (untuk mencegah input ganda & analisis lengkap)
$statusFilter = "";
} else {
// Publik: hanya verified
$statusFilter = "AND wm.status = 'verified'";
}
$whereClause = count($where) > 0 ? 'WHERE ' . implode(' AND ', $where) . ' ' . $statusFilter : ($statusFilter ? 'WHERE 1=1 ' . $statusFilter : '');
$sql = "
SELECT wm.*,
ri.nama AS ibadah_nama,
ri.jenis AS ibadah_jenis,
u.nama_lengkap AS surveyor_nama
FROM warga_miskin wm
LEFT JOIN rumah_ibadah ri ON ri.id = wm.ibadah_id
LEFT JOIN users u ON u.id = wm.surveyor_id
$whereClause
ORDER BY wm.created_at DESC
";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
jsonResponse(['data' => $rows]);
}
// ── POST: tambah warga ───────────────────────
if ($method === 'POST') {
$user = requireRole(['admin', 'surveyor']);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
foreach (['nama_kk','lat','lng'] as $f) {
if (empty($body[$f])) jsonResponse(['success'=>false,'error'=>"Field '$f' wajib diisi"], 422);
}
// Validasi NIK: harus 16 digit angka jika diisi
$nik = $body['nik'] ?? null;
if ($nik !== null && $nik !== '') {
$nik = trim($nik);
if (!preg_match('/^\d{16}$/', $nik)) {
jsonResponse(['success'=>false,'error'=>'NIK harus tepat 16 digit angka'], 422);
}
// Cek duplikasi NIK
$chk = $db->prepare("SELECT id FROM warga_miskin WHERE nik = ?");
$chk->execute([$nik]);
if ($chk->fetch()) {
jsonResponse(['success'=>false,'error'=>'NIK Kepala Keluarga sudah terdaftar di sistem'], 422);
}
} else {
$nik = null;
}
$lat = (float)$body['lat'];
$lng = (float)$body['lng'];
// Surveyor → pending, Admin → verified
$status = ($user['role'] === 'admin') ? 'verified' : 'pending';
// otomatis cari rumah ibadah terdekat (hanya jika verified)
$ibadahId = ($status === 'verified') ? findNearestIbadah($db, $lat, $lng) : null;
$bantuan = $body['bantuan_diterima'] ?? 'tidak_ada';
if (!in_array($bantuan, ['tidak_ada', 'sembako', 'beasiswa', 'modal', 'tunai'])) {
$bantuan = 'tidak_ada';
}
$stmt = $db->prepare("
INSERT INTO warga_miskin (nama_kk, jumlah_anggota, keterangan, lat, lng, ibadah_id, foto_path, surveyor_id, status, alamat, nik, kecamatan, bantuan_diterima)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$body['nama_kk'],
(int)($body['jumlah_anggota'] ?? 1),
$body['keterangan'] ?? '',
$lat, $lng, $ibadahId,
$body['foto_path'] ?? null,
$user['id'],
$status,
$body['alamat'] ?? null,
$nik,
$body['kecamatan'] ?? null,
$bantuan,
]);
$newId = (int)$db->lastInsertId();
// ambil nama ibadah untuk respon
$ibadahNama = null;
if ($ibadahId) {
$r = $db->prepare("SELECT nama FROM rumah_ibadah WHERE id = ?");
$r->execute([$ibadahId]);
$ibadahNama = $r->fetchColumn();
}
auditLog($db, $user['id'], 'CREATE_WARGA', 'warga_miskin', $newId, null, $body);
jsonResponse([
'success' => true,
'id' => $newId,
'status' => $status,
'ibadah_id' => $ibadahId,
'ibadah_nama' => $ibadahNama,
]);
}
// ── PUT: update warga ────────────────────────
if ($method === 'PUT') {
$user = requireAuth();
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
$old = $db->prepare("SELECT * FROM warga_miskin WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
// Surveyor hanya bisa edit milik sendiri yang masih pending
if ($user['role'] === 'surveyor') {
if ((int)$oldData['surveyor_id'] !== $user['id']) {
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa edit data milik Anda'], 403);
}
if ($oldData['status'] !== 'pending') {
jsonResponse(['success'=>false,'error'=>'Data yang sudah diverifikasi tidak bisa diedit'], 403);
}
}
if ($user['role'] === 'pemangku') {
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses edit'], 403);
}
$body = json_decode(file_get_contents('php://input'), true) ?? [];
// Validasi NIK jika diubah
if (array_key_exists('nik', $body) && $body['nik'] !== null && $body['nik'] !== '') {
$nikVal = trim($body['nik']);
if (!preg_match('/^\d{16}$/', $nikVal)) {
jsonResponse(['success'=>false,'error'=>'NIK harus tepat 16 digit angka'], 422);
}
// Cek duplikasi NIK (kecuali data sendiri)
$chk = $db->prepare("SELECT id FROM warga_miskin WHERE nik = ? AND id != ?");
$chk->execute([$nikVal, $id]);
if ($chk->fetch()) {
jsonResponse(['success'=>false,'error'=>'NIK Kepala Keluarga sudah terdaftar di sistem'], 422);
}
}
$fields = []; $vals = [];
foreach (['nama_kk','jumlah_anggota','keterangan','lat','lng','foto_path','alamat','nik','kecamatan','bantuan_diterima'] as $f) {
if (array_key_exists($f, $body)) {
$fields[] = "$f = ?";
if ($f === 'nik') {
$nikVal = $body['nik'] !== null ? trim($body['nik']) : '';
$vals[] = ($nikVal === '') ? null : $nikVal;
} else {
$vals[] = in_array($f,['lat','lng']) ? (float)$body[$f]
: ($f==='jumlah_anggota' ? (int)$body[$f] : $body[$f]);
}
}
}
if (empty($fields)) jsonResponse(['success'=>false,'error'=>'Tidak ada data diubah'], 422);
// jika koordinat berubah dan data verified, cari ulang ibadah terdekat
if ($oldData['status'] === 'verified' && (array_key_exists('lat', $body) || array_key_exists('lng', $body))) {
$lat = array_key_exists('lat',$body) ? (float)$body['lat'] : (float)$oldData['lat'];
$lng = array_key_exists('lng',$body) ? (float)$body['lng'] : (float)$oldData['lng'];
$ibadahId = findNearestIbadah($db, $lat, $lng);
$fields[] = "ibadah_id = ?";
$vals[] = $ibadahId;
}
$vals[] = $id;
$db->prepare("UPDATE warga_miskin SET ".implode(',',$fields)." WHERE id = ?")->execute($vals);
auditLog($db, $user['id'], 'UPDATE_WARGA', 'warga_miskin', $id, $oldData, $body);
jsonResponse(['success'=>true]);
}
// ── DELETE ───────────────────────────────────
if ($method === 'DELETE') {
$user = requireAuth();
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
$old = $db->prepare("SELECT * FROM warga_miskin WHERE id = ?");
$old->execute([$id]);
$oldData = $old->fetch();
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
// Surveyor bisa hapus milik sendiri
if ($user['role'] === 'surveyor') {
if ((int)$oldData['surveyor_id'] !== $user['id']) {
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa hapus data milik Anda'], 403);
}
}
if ($user['role'] === 'pemangku') {
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses hapus'], 403);
}
$db->prepare("DELETE FROM warga_miskin WHERE id = ?")->execute([$id]);
auditLog($db, $user['id'], 'DELETE_WARGA', 'warga_miskin', $id, $oldData);
jsonResponse(['success'=>true]);
}
jsonResponse(['error'=>'Method tidak didukung'], 405);
+85
View File
@@ -0,0 +1,85 @@
CREATE DATABASE IF NOT EXISTS webgis_poverty_mapping
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE webgis_poverty_mapping;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
nama_lengkap VARCHAR(150) NOT NULL,
role ENUM('admin','surveyor','pemangku') NOT NULL DEFAULT 'surveyor',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Admin default — password: admin123
-- Hash di-generate dengan password_hash('admin123', PASSWORD_BCRYPT)
INSERT INTO users (username, password, nama_lengkap, role) VALUES
('admin', '$2y$10$Xb5FU6uo67rVETXAlSYJ..wn8IudyYwGzUbyMe8yQ1TLFlvt.S9F6', 'Administrator', 'admin');
CREATE TABLE rumah_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
jenis ENUM('masjid','gereja','pura','klenteng','vihara','lain') NOT NULL DEFAULT 'masjid',
alamat TEXT,
pic VARCHAR(150) NOT NULL,
no_wa VARCHAR(30) DEFAULT NULL,
radius INT NOT NULL DEFAULT 500, -- meter
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
foto_path VARCHAR(255) DEFAULT NULL,
surveyor_id INT DEFAULT NULL,
status ENUM('pending','verified','rejected') NOT NULL DEFAULT 'verified',
alasan_penolakan TEXT DEFAULT NULL,
kecamatan VARCHAR(100) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_ri_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE warga_miskin (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_kk VARCHAR(200) NOT NULL,
nik VARCHAR(16) DEFAULT NULL,
jumlah_anggota INT NOT NULL DEFAULT 1,
keterangan TEXT,
alamat TEXT DEFAULT NULL,
kecamatan VARCHAR(100) DEFAULT NULL,
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
ibadah_id INT DEFAULT NULL, -- NULL = belum terjangkau
foto_path VARCHAR(255) DEFAULT NULL,
surveyor_id INT DEFAULT NULL,
status ENUM('pending','verified','rejected') NOT NULL DEFAULT 'verified',
alasan_penolakan TEXT DEFAULT NULL,
bantuan_diterima ENUM('sembako','beasiswa','modal','tunai','tidak_ada') NOT NULL DEFAULT 'tidak_ada',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uq_warga_nik UNIQUE (nik),
CONSTRAINT fk_wm_ibadah FOREIGN KEY (ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL,
CONSTRAINT fk_wm_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE audit_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
aksi VARCHAR(100) NOT NULL,
target_type VARCHAR(50) DEFAULT NULL,
target_id INT DEFAULT NULL,
data_before JSON DEFAULT NULL,
data_after JSON DEFAULT NULL,
ip_address VARCHAR(45) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_audit_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE INDEX idx_ri_status ON rumah_ibadah(status);
CREATE INDEX idx_ri_kecamatan ON rumah_ibadah(kecamatan);
CREATE INDEX idx_wm_status ON warga_miskin(status);
CREATE INDEX idx_wm_kecamatan ON warga_miskin(kecamatan);
CREATE INDEX idx_wm_ibadah ON warga_miskin(ibadah_id);
CREATE INDEX idx_audit_user ON audit_logs(user_id);
CREATE INDEX idx_audit_aksi ON audit_logs(aksi);
CREATE INDEX idx_audit_created ON audit_logs(created_at);
File diff suppressed because it is too large Load Diff
+3306
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
# Hanya izinkan akses file gambar
<FilesMatch "\.(jpg|jpeg|png|gif|webp)$">
Require all granted
</FilesMatch>
# Blok semua file lain
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
# Disable directory listing
Options -Indexes
# Disable PHP execution
php_flag engine off
+155
View File
@@ -0,0 +1,155 @@
<?php
// ============================================
// api.php — API Backend WebGIS SPBU
// ============================================
ini_set('display_errors', 0);
error_reporting(0);
register_shutdown_function(function() {
$err = error_get_last();
if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
if (!headers_sent()) header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'PHP Error: ' . $err['message']]);
}
});
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once 'config.php';
$action = $_GET['action'] ?? $_POST['action'] ?? '';
// ── GET ALL ───────────────────────────────────────────
if ($action === 'all') {
$conn = getConnection();
$check = $conn->query("SHOW TABLES LIKE 'spbu'");
if ($check->num_rows === 0) {
echo json_encode(['success' => false, 'message' => "Tabel 'spbu' tidak ditemukan. Pastikan Anda sudah import file database.sql"]);
$conn->close(); exit;
}
$result = $conn->query(
"SELECT id, nama, nomor_spbu, status, latitude, longitude,
DATE_FORMAT(created_at, '%d %b %Y %H:%i') AS created_at
FROM spbu ORDER BY created_at DESC"
);
if (!$result) {
echo json_encode(['success' => false, 'message' => 'Query error: ' . $conn->error]);
$conn->close(); exit;
}
$rows = [];
while ($row = $result->fetch_assoc()) $rows[] = $row;
echo json_encode(['success' => true, 'data' => $rows]);
$conn->close();
// ── SAVE ──────────────────────────────────────────────
} elseif ($action === 'save') {
$nama = trim($_POST['nama'] ?? '');
$nomor_spbu = trim($_POST['nomor_spbu'] ?? '');
$status = trim($_POST['status'] ?? '');
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
if (!$nama || !$nomor_spbu || !$status || $latitude === '' || $longitude === '') {
echo json_encode(['success' => false, 'message' => 'Semua field wajib diisi']); exit;
}
if (!in_array($status, ['buka 24 jam', 'tidak'])) {
echo json_encode(['success' => false, 'message' => 'Status tidak valid']); exit;
}
$lat = (double) $latitude;
$lng = (double) $longitude;
$conn = getConnection();
$stmt = $conn->prepare("INSERT INTO spbu (nama, nomor_spbu, status, latitude, longitude) VALUES (?,?,?,?,?)");
if (!$stmt) { echo json_encode(['success' => false, 'message' => 'Prepare error: ' . $conn->error]); exit; }
$stmt->bind_param('sssdd', $nama, $nomor_spbu, $status, $lat, $lng);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'SPBU berhasil disimpan', 'id' => $stmt->insert_id]);
} else {
echo json_encode(['success' => false, 'message' => 'Execute error: ' . $conn->error]);
}
$stmt->close(); $conn->close();
// ── UPDATE ────────────────────────────────────────────
} elseif ($action === 'update') {
$id = (int) ($_POST['id'] ?? 0);
$nama = trim($_POST['nama'] ?? '');
$nomor_spbu = trim($_POST['nomor_spbu'] ?? '');
$status = trim($_POST['status'] ?? '');
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
if (!$id || !$nama || !$nomor_spbu || !$status || $latitude === '' || $longitude === '') {
echo json_encode(['success' => false, 'message' => 'Semua field wajib diisi']); exit;
}
$lat = (double) $latitude;
$lng = (double) $longitude;
$conn = getConnection();
$stmt = $conn->prepare("UPDATE spbu SET nama=?, nomor_spbu=?, status=?, latitude=?, longitude=? WHERE id=?");
if (!$stmt) { echo json_encode(['success' => false, 'message' => 'Prepare error: ' . $conn->error]); exit; }
$stmt->bind_param('sssddi', $nama, $nomor_spbu, $status, $lat, $lng, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Data berhasil diupdate']);
} else {
echo json_encode(['success' => false, 'message' => 'Execute error: ' . $conn->error]);
}
$stmt->close(); $conn->close();
// ── UPDATE KOORDINAT SAJA ─────────────────────────────
} elseif ($action === 'update_coords') {
$id = (int) ($_POST['id'] ?? 0);
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
if (!$id || $latitude === '' || $longitude === '') {
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']); exit;
}
$lat = (double) $latitude;
$lng = (double) $longitude;
$conn = getConnection();
$stmt = $conn->prepare("UPDATE spbu SET latitude=?, longitude=? WHERE id=?");
if (!$stmt) { echo json_encode(['success' => false, 'message' => 'Prepare error: ' . $conn->error]); exit; }
$stmt->bind_param('ddi', $lat, $lng, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Koordinat berhasil diupdate']);
} else {
echo json_encode(['success' => false, 'message' => 'Execute error: ' . $conn->error]);
}
$stmt->close(); $conn->close();
// ── DELETE ────────────────────────────────────────────
} elseif ($action === 'delete') {
$id = (int) ($_POST['id'] ?? 0);
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID tidak valid']); exit; }
$conn = getConnection();
$stmt = $conn->prepare("DELETE FROM spbu WHERE id=?");
if (!$stmt) { echo json_encode(['success' => false, 'message' => 'Prepare error: ' . $conn->error]); exit; }
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Data berhasil dihapus']);
} else {
echo json_encode(['success' => false, 'message' => 'Execute error: ' . $conn->error]);
}
$stmt->close(); $conn->close();
// ── CEK KONEKSI (debug) ───────────────────────────────
} elseif ($action === 'ping') {
$conn = getConnection();
echo json_encode(['success' => true, 'message' => 'Koneksi OK', 'db' => DB_NAME]);
$conn->close();
} else {
echo json_encode(['success' => false, 'message' => 'Action tidak dikenali: ' . $action]);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
// ============================================
// config.php — Konfigurasi Database SPBU
// ============================================
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // Ganti jika perlu
define('DB_PASS', ''); // Ganti jika perlu
define('DB_NAME', 'webgis_spbu');
function getConnection() {
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
$conn->set_charset('utf8mb4');
if ($conn->connect_error) {
http_response_code(500);
die(json_encode(['success' => false, 'message' => 'Koneksi gagal: ' . $conn->connect_error]));
}
return $conn;
}
?>
+23
View File
@@ -0,0 +1,23 @@
-- ============================================
-- WebGIS SPBU — Database Setup
-- ============================================
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,
nomor_spbu VARCHAR(100) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'tidak',
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Contoh data awal
INSERT INTO spbu (nama, nomor_spbu, status, latitude, longitude) VALUES
('SPBU Pontianak Kota', '64.761.01', 'buka 24 jam', -0.0263, 109.3425),
('SPBU Sungai Raya', '64.761.02', 'tidak', -0.0512, 109.3610),
('SPBU Ahmad Yani', '64.761.03', 'buka 24 jam', -0.0198, 109.3580);
+1393
View File
File diff suppressed because it is too large Load Diff
+1177
View File
File diff suppressed because it is too large Load Diff