add files and folders

This commit is contained in:
2026-06-10 17:28:32 +07:00
parent 52f4cf86ab
commit 61f4fb2e71
80 changed files with 17551 additions and 0 deletions
View File
+10
View File
@@ -0,0 +1,10 @@
FROM php:8.2-apache
RUN docker-php-ext-install mysqli pdo pdo_mysql \
&& a2enmod rewrite
COPY . /var/www/html/
RUN mkdir -p /var/www/html/uploads \
&& chown -R www-data:www-data /var/www/html/uploads \
&& chown -R www-data:www-data /var/www/html
+32
View File
@@ -0,0 +1,32 @@
# WebGIS Sebaran Penduduk Miskin + Rumah Ibadah
Stack: PHP (XAMPP), MySQL/MariaDB, LeafletJS + Leaflet.draw (tanpa framework).
## Fitur
- Peta interaktif (fullscreen) + layer: rumah ibadah, radius, penduduk miskin.
- CRUD lewat aksi di peta (klik untuk point, popup untuk edit/hapus, edit lokasi + drag).
- Reverse geocoding (server proxy `php/reverse_geocode.php` ke Nominatim).
- Auto-relasi penduduk miskin → rumah ibadah terdekat **dalam radius** (jarak dihitung otomatis).
- Marker penduduk miskin:
- Merah: Belum dibantu
- Kuning: Menunggu verifikasi
- Hijau: Sudah dibantu
- Outline: menunjukkan inside/outside radius rumah ibadah
- Form penduduk miskin mendukung anggota keluarga (dinamis) + upload bukti (jpg/png/webp/pdf).
- Admin panel (login) + CRUD dasar + import/export CSV.
## Instalasi (XAMPP)
1. Pastikan folder project berada di `htdocs`, misalnya: `C:\\xampp\\htdocs\\webgis-spbu`.
2. Buat database `webgis_spbu` di phpMyAdmin.
3. Import schema: `sql/schema_webgis_spbu.sql`.
4. Sesuaikan konfigurasi DB di `php/koneksi.php`.
5. Jalankan Apache + MySQL di XAMPP.
6. Buka aplikasi:
- WebGIS: `http://localhost/webgis-spbu/`
- Admin: `http://localhost/webgis-spbu/admin/login.php`
- Jika belum ada user, jalankan sekali: `http://localhost/webgis-spbu/admin/init_admin.php`
## Catatan Upload Bukti
- File disimpan ke `uploads/bukti/`.
- Pastikan folder ini writable oleh Apache.
+18
View File
@@ -0,0 +1,18 @@
<?php
session_start();
function is_logged_in() {
return isset($_SESSION['user']) && is_array($_SESSION['user']);
}
function require_login() {
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
}
function current_user() {
return $_SESSION['user'] ?? null;
}
+39
View File
@@ -0,0 +1,39 @@
<?php
require_once __DIR__ . '/../php/koneksi.php';
require_once __DIR__ . '/_auth.php';
function h($v) { return htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8'); }
function render_header($title) {
$u = current_user();
$name = $u ? h($u['name'] ?? $u['username'] ?? '') : '';
echo '<!doctype html><html lang="id"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">';
echo '<title>' . h($title) . '</title>';
echo '<link rel="stylesheet" href="../css/style.css">';
echo '<style>
body{background:#0b1220;margin:0}
.admin-shell{max-width:1100px;margin:0 auto;padding:22px}
.admin-top{display:flex;align-items:center;justify-content:space-between;color:#e5e7eb;margin-bottom:16px}
.admin-card{background:#fff;border-radius:18px;box-shadow:0 18px 45px rgba(0,0,0,.25);padding:18px}
.admin-nav a{color:#e5e7eb;text-decoration:none;margin-right:12px;font-weight:800}
.admin-nav a:hover{text-decoration:underline}
table{width:100%;border-collapse:collapse}
th,td{padding:10px 10px;border-bottom:1px solid #e2e8f0;font-size:14px;text-align:left;vertical-align:top}
th{font-weight:900}
.tag{display:inline-block;padding:4px 8px;border-radius:999px;background:#eef2ff;color:#1d4ed8;font-weight:900;font-size:12px}
.actions{display:flex;gap:8px;flex-wrap:wrap}
.actions a,.actions button{border:0;border-radius:12px;padding:8px 10px;font-weight:900;cursor:pointer}
.actions a{background:#0f172a;color:#fff;text-decoration:none}
.actions button.danger{background:#dc2626;color:#fff}
.actions a.secondary{background:#e2e8f0;color:#0f172a}
</style></head><body>';
echo '<div class="admin-shell">';
echo '<div class="admin-top"><div><div style="font-size:22px;font-weight:900;color:#fff">Admin Panel</div><div class="admin-nav">';
echo '<a href="index.php">Dashboard</a><a href="rumah_ibadah.php">Rumah Ibadah</a><a href="penduduk_miskin.php">Penduduk Miskin</a>';
echo '</div></div><div style="color:#cbd5e1;font-weight:800">' . $name . ' <a href="logout.php" style="color:#93c5fd;text-decoration:none">Logout</a></div></div>';
}
function render_footer() {
echo '</div></body></html>';
}
+53
View File
@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login();
// Stats
$counts = [
'rumah_ibadah' => 0,
'penduduk' => 0,
'dibantu' => 0,
'belum' => 0,
'verifikasi' => 0
];
try {
$counts['rumah_ibadah'] = (int)$pdo->query("SELECT COUNT(*) FROM rumah_ibadah_points")->fetchColumn();
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('status_bantuan', $cols, true);
$counts['penduduk'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points")->fetchColumn();
if ($hasNew) {
$counts['dibantu'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Sudah dibantu'")->fetchColumn();
$counts['belum'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Belum dibantu'")->fetchColumn();
$counts['verifikasi'] = (int)$pdo->query("SELECT COUNT(*) FROM pemukiman_miskin_points WHERE status_bantuan='Menunggu verifikasi'")->fetchColumn();
}
} catch (Exception $e) {
}
render_header('Admin Dashboard');
?>
<div class="admin-card">
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px">
<div style="background:#0b1220;border-radius:18px;padding:14px;color:#fff">
<div style="font-weight:900;font-size:28px"><?= (int)$counts['rumah_ibadah'] ?></div>
<div style="color:#cbd5e1;font-weight:800">Rumah Ibadah</div>
</div>
<div style="background:#0b1220;border-radius:18px;padding:14px;color:#fff">
<div style="font-weight:900;font-size:28px"><?= (int)$counts['penduduk'] ?></div>
<div style="color:#cbd5e1;font-weight:800">Penduduk Miskin</div>
</div>
<div style="background:#052e1a;border-radius:18px;padding:14px;color:#fff">
<div style="font-weight:900;font-size:28px"><?= (int)$counts['dibantu'] ?></div>
<div style="color:#bbf7d0;font-weight:800">Sudah dibantu</div>
</div>
<div style="background:#3b0a0a;border-radius:18px;padding:14px;color:#fff">
<div style="font-weight:900;font-size:28px"><?= (int)$counts['belum'] ?></div>
<div style="color:#fecaca;font-weight:800">Belum dibantu</div>
</div>
</div>
<div style="margin-top:14px;color:#475569;font-weight:800">
Menu admin ini melengkapi WebGIS utama: `index.html`.
</div>
</div>
<?php render_footer(); ?>
+34
View File
@@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/../php/koneksi.php';
header('Content-Type: text/html; charset=utf-8');
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
username VARCHAR(60) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'operator',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
$count = (int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
if ($count > 0) {
echo "Users sudah ada. Silakan login di <a href='login.php'>login.php</a>.";
exit;
}
$username = 'admin';
$password = 'admin123';
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (name, username, password_hash, role) VALUES (:n,:u,:p,'admin')");
$stmt->execute([':n' => 'Administrator', ':u' => $username, ':p' => $hash]);
echo "Admin dibuat. Username: <b>admin</b>, Password: <b>admin123</b>. Lanjut login: <a href='login.php'>login.php</a>";
} catch (Exception $e) {
echo "Gagal init admin: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
}
+62
View File
@@ -0,0 +1,62 @@
<?php
require_once __DIR__ . '/../php/koneksi.php';
require_once __DIR__ . '/_auth.php';
if (is_logged_in()) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = (string)($_POST['password'] ?? '');
try {
$stmt = $pdo->prepare("SELECT id, name, username, password_hash, role FROM users WHERE username = :u LIMIT 1");
$stmt->execute([':u' => $username]);
$u = $stmt->fetch();
if ($u && password_verify($password, $u['password_hash'])) {
$_SESSION['user'] = ['id' => (int)$u['id'], 'name' => $u['name'], 'username' => $u['username'], 'role' => $u['role']];
header('Location: index.php');
exit;
}
$error = 'Username atau password salah.';
} catch (Exception $e) {
$error = $e->getMessage();
}
}
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Login Admin</title>
<link rel="stylesheet" href="../css/style.css">
<style>
body{background:#0b1220;margin:0;display:grid;place-items:center;min-height:100vh;padding:18px}
.card{background:#fff;border-radius:22px;box-shadow:0 18px 45px rgba(0,0,0,.25);width:100%;max-width:420px;padding:18px}
.title{font-weight:900;font-size:22px;margin:0 0 4px}
.sub{color:#64748b;margin:0 0 14px;font-weight:700}
.err{background:#fef2f2;border:1px solid #fecaca;color:#991b1b;padding:10px 12px;border-radius:14px;font-weight:800;margin-bottom:12px}
.btnx{width:100%;margin-top:12px}
a{color:#2563eb;text-decoration:none;font-weight:900}
</style>
</head>
<body>
<div class="card">
<div class="title">Login Admin</div>
<div class="sub">Jika belum ada user, jalankan <a href="init_admin.php">init_admin.php</a>.</div>
<?php if ($error): ?><div class="err"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
<form method="post">
<label>Username</label>
<input name="username" required>
<label style="margin-top:10px;display:block;">Password</label>
<input name="password" type="password" required>
<button class="btn btn-primary btnx" type="submit">Masuk</button>
</form>
</div>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/_auth.php';
session_destroy();
header('Location: login.php');
exit;
+286
View File
@@ -0,0 +1,286 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login();
function csv_cell($v) {
$s = (string)($v ?? '');
$s = str_replace('"', '""', $s);
return '"' . $s . '"';
}
// Export CSV
if (isset($_GET['export'])) {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="penduduk_miskin.csv"');
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
$out = fopen('php://output', 'w');
if ($hasNew) {
fputcsv($out, ['id','kk_nama','nik','jumlah_anggota','lat','lng','alamat','kelurahan','kecamatan','status_bantuan','jenis_bantuan','tanggal_bantuan','rumah_ibadah_id','jarak_meter']);
$rows = $pdo->query("SELECT id, kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan, rumah_ibadah_id, jarak_meter FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll();
foreach ($rows as $r) {
fputcsv($out, [
$r['id'], $r['kk_nama'], $r['nik'], $r['jumlah_anggota'],
$r['latitude'], $r['longitude'], $r['address'], $r['kelurahan'], $r['kecamatan'],
$r['status_bantuan'], $r['jenis_bantuan'], $r['tanggal_bantuan'],
$r['rumah_ibadah_id'], $r['jarak_meter']
]);
}
} else {
fputcsv($out, ['id','nama','lat','lng','alamat','rumah_ibadah_id']);
$rows = $pdo->query("SELECT id, nama, latitude, longitude, address, rumah_ibadah_id FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll();
foreach ($rows as $r) fputcsv($out, [$r['id'],$r['nama'],$r['latitude'],$r['longitude'],$r['address'],$r['rumah_ibadah_id']]);
}
fclose($out);
exit;
}
$msg = '';
$err = '';
// Delete
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
try {
$stmt = $pdo->prepare("DELETE FROM pemukiman_miskin_points WHERE id=:id");
$stmt->execute([':id' => $id]);
$msg = 'Berhasil dihapus.';
} catch (Exception $e) {
$err = $e->getMessage();
}
}
// Import CSV (minimal)
if (isset($_POST['import_csv'])) {
if (!isset($_FILES['csv_file']) || ($_FILES['csv_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
$err = 'CSV tidak ditemukan.';
} else {
try {
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
$fp = fopen($_FILES['csv_file']['tmp_name'], 'r');
$header = fgetcsv($fp);
if (!$header) throw new Exception('Header CSV kosong.');
$map = [];
foreach ($header as $i => $hcol) $map[strtolower(trim($hcol))] = $i;
$pdo->beginTransaction();
$ins = $hasNew
? $pdo->prepare("INSERT INTO pemukiman_miskin_points (kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan) VALUES (:kk,:nik,:j,:lat,:lng,:a,:kel,:kec,:s,:jb,:tb)")
: $pdo->prepare("INSERT INTO pemukiman_miskin_points (nama, latitude, longitude, address) VALUES (:kk,:lat,:lng,:a)");
$n = 0;
while (($row = fgetcsv($fp)) !== false) {
$kk = $row[$map['kk_nama'] ?? $map['nama'] ?? -1] ?? '';
$lat = $row[$map['lat'] ?? $map['latitude'] ?? -1] ?? '';
$lng = $row[$map['lng'] ?? $map['longitude'] ?? -1] ?? '';
if (trim($kk) === '' || trim($lat) === '' || trim($lng) === '') continue;
if ($hasNew) {
$ins->execute([
':kk' => $kk,
':nik' => ($row[$map['nik'] ?? -1] ?? '') ?: null,
':j' => (int)($row[$map['jumlah_anggota'] ?? -1] ?? 0),
':lat' => $lat,
':lng' => $lng,
':a' => ($row[$map['alamat'] ?? $map['address'] ?? -1] ?? '') ?: null,
':kel' => ($row[$map['kelurahan'] ?? -1] ?? '') ?: null,
':kec' => ($row[$map['kecamatan'] ?? -1] ?? '') ?: null,
':s' => ($row[$map['status_bantuan'] ?? -1] ?? 'Belum dibantu') ?: 'Belum dibantu',
':jb' => ($row[$map['jenis_bantuan'] ?? -1] ?? '') ?: null,
':tb' => ($row[$map['tanggal_bantuan'] ?? -1] ?? '') ?: null
]);
} else {
$ins->execute([
':kk' => $kk,
':lat' => $lat,
':lng' => $lng,
':a' => ($row[$map['alamat'] ?? $map['address'] ?? -1] ?? '') ?: null
]);
}
$n++;
}
$pdo->commit();
fclose($fp);
$msg = "Import selesai: $n baris.";
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
$err = $e->getMessage();
}
}
}
// Simple edit/create (without anggota details)
if (isset($_POST['save_penduduk'])) {
$id = (int)($_POST['id'] ?? 0);
$kk = trim($_POST['kk_nama'] ?? '');
$nik = trim($_POST['nik'] ?? '');
$jml = (int)($_POST['jumlah_anggota'] ?? 0);
$lat = $_POST['latitude'] ?? null;
$lng = $_POST['longitude'] ?? null;
$addr = trim($_POST['address'] ?? '');
$kel = trim($_POST['kelurahan'] ?? '');
$kec = trim($_POST['kecamatan'] ?? '');
$status = trim($_POST['status_bantuan'] ?? 'Belum dibantu');
$jb = trim($_POST['jenis_bantuan'] ?? '');
$tb = trim($_POST['tanggal_bantuan'] ?? '');
if ($kk === '' || $lat === null || $lng === null) $err = 'Data belum lengkap (nama/lat/lng).';
else {
try {
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
if (!$hasNew) throw new Exception('Schema belum mendukung fitur penduduk detail. Jalankan schema terbaru.');
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE pemukiman_miskin_points SET kk_nama=:kk, nik=:nik, jumlah_anggota=:j, latitude=:lat, longitude=:lng, address=:a, kelurahan=:kel, kecamatan=:kec, status_bantuan=:s, jenis_bantuan=:jb, tanggal_bantuan=:tb WHERE id=:id");
$stmt->execute([':kk'=>$kk,':nik'=>$nik ?: null,':j'=>$jml,':lat'=>$lat,':lng'=>$lng,':a'=>$addr ?: null,':kel'=>$kel ?: null,':kec'=>$kec ?: null,':s'=>$status ?: 'Belum dibantu',':jb'=>$jb ?: null,':tb'=>$tb ?: null,':id'=>$id]);
$msg = 'Berhasil diupdate.';
} else {
$stmt = $pdo->prepare("INSERT INTO pemukiman_miskin_points (kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan, status_bantuan, jenis_bantuan, tanggal_bantuan) VALUES (:kk,:nik,:j,:lat,:lng,:a,:kel,:kec,:s,:jb,:tb)");
$stmt->execute([':kk'=>$kk,':nik'=>$nik ?: null,':j'=>$jml,':lat'=>$lat,':lng'=>$lng,':a'=>$addr ?: null,':kel'=>$kel ?: null,':kec'=>$kec ?: null,':s'=>$status ?: 'Belum dibantu',':jb'=>$jb ?: null,':tb'=>$tb ?: null]);
$msg = 'Berhasil ditambahkan.';
}
} catch (Exception $e) { $err = $e->getMessage(); }
}
}
$edit = null;
if (isset($_GET['edit'])) {
$id = (int)$_GET['edit'];
$stmt = $pdo->prepare("SELECT * FROM pemukiman_miskin_points WHERE id=:id");
$stmt->execute([':id' => $id]);
$edit = $stmt->fetch() ?: null;
}
$rows = [];
try {
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
if ($hasNew) {
$rows = $pdo->query("
SELECT p.*, r.nama AS rumah_ibadah_nama
FROM pemukiman_miskin_points p
LEFT JOIN rumah_ibadah_points r ON r.id=p.rumah_ibadah_id
ORDER BY p.id ASC
")->fetchAll();
} else {
$rows = $pdo->query("SELECT * FROM pemukiman_miskin_points ORDER BY id ASC")->fetchAll();
}
} catch (Exception $e) {}
render_header('Admin Penduduk Miskin');
?>
<div class="admin-card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
<div style="font-weight:900;font-size:18px"><?= $edit ? 'Edit' : 'Tambah' ?> Penduduk Miskin</div>
<div class="actions">
<a class="secondary" href="penduduk_miskin.php">Reset</a>
<a href="penduduk_miskin.php?export=1">Export CSV</a>
</div>
</div>
<?php if ($msg): ?><div class="notice" style="margin-top:12px;border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="margin-top:12px;border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<form method="post" style="margin-top:12px">
<input type="hidden" name="id" value="<?= h($edit['id'] ?? 0) ?>">
<div class="grid-2">
<div>
<label>Nama Kepala Keluarga</label>
<input name="kk_nama" required value="<?= h($edit['kk_nama'] ?? '') ?>">
</div>
<div>
<label>NIK (opsional)</label>
<input name="nik" value="<?= h($edit['nik'] ?? '') ?>">
</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div>
<label>Jumlah Anggota</label>
<input name="jumlah_anggota" type="number" min="0" step="1" value="<?= h($edit['jumlah_anggota'] ?? 0) ?>">
</div>
<div>
<label>Status Bantuan</label>
<select name="status_bantuan">
<?php foreach (['Belum dibantu','Menunggu verifikasi','Sudah dibantu'] as $s): ?>
<option value="<?= h($s) ?>" <?= (($edit['status_bantuan'] ?? 'Belum dibantu') === $s ? 'selected' : '') ?>><?= h($s) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div>
<label>Jenis Bantuan</label>
<input name="jenis_bantuan" value="<?= h($edit['jenis_bantuan'] ?? '') ?>">
</div>
<div>
<label>Tanggal Bantuan</label>
<input name="tanggal_bantuan" type="date" value="<?= h($edit['tanggal_bantuan'] ?? '') ?>">
</div>
</div>
<label style="margin-top:10px;display:block;">Alamat</label>
<input name="address" value="<?= h($edit['address'] ?? '') ?>">
<div class="grid-2" style="margin-top:10px">
<div>
<label>Kelurahan</label>
<input name="kelurahan" value="<?= h($edit['kelurahan'] ?? '') ?>">
</div>
<div>
<label>Kecamatan</label>
<input name="kecamatan" value="<?= h($edit['kecamatan'] ?? '') ?>">
</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div>
<label>Latitude</label>
<input name="latitude" required value="<?= h($edit['latitude'] ?? '') ?>">
</div>
<div>
<label>Longitude</label>
<input name="longitude" required value="<?= h($edit['longitude'] ?? '') ?>">
</div>
</div>
<div class="modal-actions" style="margin-top:12px">
<button class="btn btn-primary" name="save_penduduk" value="1" type="submit"><?= $edit ? 'Update' : 'Tambah' ?></button>
<a class="btn btn-secondary" href="../index.html" style="text-align:center;line-height:38px;text-decoration:none">Kembali ke WebGIS</a>
</div>
</form>
</div>
<div class="admin-card" style="margin-top:14px">
<div style="font-weight:900;font-size:18px;margin-bottom:8px">Import CSV</div>
<form method="post" enctype="multipart/form-data">
<input type="file" name="csv_file" accept=".csv" required>
<button class="btn btn-secondary" name="import_csv" value="1" type="submit" style="margin-top:10px">Import</button>
<div style="margin-top:8px;color:#475569;font-weight:800">Kolom minimal: `kk_nama`/`nama`, `lat`, `lng`.</div>
</form>
</div>
<div class="admin-card" style="margin-top:14px">
<div style="font-weight:900;font-size:18px;margin-bottom:8px">Daftar Penduduk Miskin</div>
<table>
<thead>
<tr>
<th>ID</th><th>Kepala Keluarga</th><th>Status</th><th>Rumah Ibadah</th><th>Koordinat</th><th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= h($r['kk_nama'] ?? $r['nama'] ?? '') ?></td>
<td><span class="tag"><?= h($r['status_bantuan'] ?? '-') ?></span></td>
<td><?= h($r['rumah_ibadah_nama'] ?? '-') ?></td>
<td><?= h(($r['latitude'] ?? '') . ', ' . ($r['longitude'] ?? '')) ?></td>
<td class="actions">
<a class="secondary" href="penduduk_miskin.php?edit=<?= (int)$r['id'] ?>">Edit</a>
<a href="penduduk_miskin.php?delete=<?= (int)$r['id'] ?>" onclick="return confirm('Hapus data ini?')">Hapus</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php render_footer(); ?>
+166
View File
@@ -0,0 +1,166 @@
<?php
require_once __DIR__ . '/_layout.php';
require_login();
$msg = '';
$err = '';
// Delete
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
try {
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah_points WHERE id=:id");
$stmt->execute([':id' => $id]);
$msg = 'Berhasil dihapus.';
} catch (Exception $e) {
$err = $e->getMessage();
}
}
// Save (create/update)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$nama = trim($_POST['nama'] ?? '');
$jenis = trim($_POST['jenis'] ?? 'Lainnya');
$kontak = trim($_POST['kontak'] ?? '');
$radius = (float)($_POST['radius_meter'] ?? 300);
$lat = $_POST['latitude'] ?? null;
$lng = $_POST['longitude'] ?? null;
$address = trim($_POST['address'] ?? '');
if ($nama === '' || $radius <= 0 || $lat === null || $lng === null) {
$err = 'Data belum lengkap (nama/radius/lat/lng).';
} else {
try {
$cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasJenis = in_array('jenis', $cols, true);
$hasKontak = in_array('kontak', $cols, true);
if ($id > 0) {
if ($hasJenis && $hasKontak) {
$stmt = $pdo->prepare("UPDATE rumah_ibadah_points SET nama=:n, jenis=:j, kontak=:k, radius_meter=:r, latitude=:lat, longitude=:lng, address=:a WHERE id=:id");
$stmt->execute([':n'=>$nama,':j'=>$jenis ?: 'Lainnya',':k'=>$kontak ?: null,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null,':id'=>$id]);
} else {
$stmt = $pdo->prepare("UPDATE rumah_ibadah_points SET nama=:n, radius_meter=:r, latitude=:lat, longitude=:lng, address=:a WHERE id=:id");
$stmt->execute([':n'=>$nama,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null,':id'=>$id]);
}
$msg = 'Berhasil diupdate.';
} else {
if ($hasJenis && $hasKontak) {
$stmt = $pdo->prepare("INSERT INTO rumah_ibadah_points (nama, jenis, kontak, radius_meter, latitude, longitude, address) VALUES (:n,:j,:k,:r,:lat,:lng,:a)");
$stmt->execute([':n'=>$nama,':j'=>$jenis ?: 'Lainnya',':k'=>$kontak ?: null,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null]);
} else {
$stmt = $pdo->prepare("INSERT INTO rumah_ibadah_points (nama, radius_meter, latitude, longitude, address) VALUES (:n,:r,:lat,:lng,:a)");
$stmt->execute([':n'=>$nama,':r'=>$radius,':lat'=>$lat,':lng'=>$lng,':a'=>$address ?: null]);
}
$msg = 'Berhasil ditambahkan.';
}
} catch (Exception $e) {
$err = $e->getMessage();
}
}
}
$edit = null;
if (isset($_GET['edit'])) {
$id = (int)$_GET['edit'];
$stmt = $pdo->prepare("SELECT * FROM rumah_ibadah_points WHERE id=:id");
$stmt->execute([':id' => $id]);
$edit = $stmt->fetch() ?: null;
}
$rows = [];
try {
$rows = $pdo->query("
SELECT r.*,
(SELECT COUNT(*) FROM pemukiman_miskin_points p WHERE p.rumah_ibadah_id=r.id) AS binaan_count
FROM rumah_ibadah_points r
ORDER BY r.id ASC
")->fetchAll();
} catch (Exception $e) {}
render_header('Admin Rumah Ibadah');
?>
<div class="admin-card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
<div style="font-weight:900;font-size:18px"><?= $edit ? 'Edit' : 'Tambah' ?> Rumah Ibadah</div>
<a class="actions a secondary" href="rumah_ibadah.php" style="background:#e2e8f0;color:#0f172a;border-radius:12px;padding:8px 10px;font-weight:900;text-decoration:none">Reset</a>
</div>
<?php if ($msg): ?><div class="notice" style="margin-top:12px;border-color:#bbf7d0;background:#f0fdf4;color:#166534"><?= h($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="notice" style="margin-top:12px;border-color:#fecaca;background:#fef2f2;color:#991b1b"><?= h($err) ?></div><?php endif; ?>
<form method="post" style="margin-top:12px">
<input type="hidden" name="id" value="<?= h($edit['id'] ?? 0) ?>">
<div class="grid-2">
<div>
<label>Nama</label>
<input name="nama" required value="<?= h($edit['nama'] ?? '') ?>">
</div>
<div>
<label>Jenis</label>
<select name="jenis">
<?php foreach (['Masjid','Musholla','Gereja','Katolik','Pura','Vihara','Klenteng','Lainnya'] as $j): ?>
<option value="<?= h($j) ?>" <?= (($edit['jenis'] ?? 'Lainnya') === $j ? 'selected' : '') ?>><?= h($j) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<label style="margin-top:10px;display:block;">Kontak</label>
<input name="kontak" value="<?= h($edit['kontak'] ?? '') ?>">
<div class="grid-2" style="margin-top:10px">
<div>
<label>Radius (meter)</label>
<input name="radius_meter" type="number" min="1" step="1" required value="<?= h($edit['radius_meter'] ?? 300) ?>">
</div>
<div>
<label>Alamat</label>
<input name="address" value="<?= h($edit['address'] ?? '') ?>">
</div>
</div>
<div class="grid-2" style="margin-top:10px">
<div>
<label>Latitude</label>
<input name="latitude" required value="<?= h($edit['latitude'] ?? '') ?>">
</div>
<div>
<label>Longitude</label>
<input name="longitude" required value="<?= h($edit['longitude'] ?? '') ?>">
</div>
</div>
<div class="modal-actions" style="margin-top:12px">
<button class="btn btn-primary" type="submit"><?= $edit ? 'Update' : 'Tambah' ?></button>
<a class="btn btn-secondary" href="../index.html" style="text-align:center;line-height:38px;text-decoration:none">Kembali ke WebGIS</a>
</div>
</form>
</div>
<div class="admin-card" style="margin-top:14px">
<div style="font-weight:900;font-size:18px;margin-bottom:8px">Daftar Rumah Ibadah</div>
<table>
<thead>
<tr>
<th>ID</th><th>Nama</th><th>Jenis</th><th>Radius</th><th>Binaan</th><th>Koordinat</th><th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= h($r['nama'] ?? '') ?></td>
<td><span class="tag"><?= h($r['jenis'] ?? 'Lainnya') ?></span></td>
<td><?= (int)($r['radius_meter'] ?? 0) ?> m</td>
<td><?= (int)($r['binaan_count'] ?? 0) ?></td>
<td><?= h(($r['latitude'] ?? '') . ', ' . ($r['longitude'] ?? '')) ?></td>
<td class="actions">
<a class="secondary" href="rumah_ibadah.php?edit=<?= (int)$r['id'] ?>">Edit</a>
<a href="rumah_ibadah.php?delete=<?= (int)$r['id'] ?>" onclick="return confirm('Hapus data ini?')">Hapus</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php render_footer(); ?>
+779
View File
@@ -0,0 +1,779 @@
:root {
--bg: #0d1520;
--bg-soft: #10182c;
--bg-card: #141e2e;
--card: #ffffff;
--text: #e5e7eb;
--text-secondary: #cbd5e1;
--muted: #8b94a5;
--line: #e2e8f0;
--primary: #3b82f6;
--primary-dark: #1d4ed8;
--primary-light: #60a5fa;
--secondary: #6b7280;
--danger: #ef4444;
--success: #10b981;
--warning: #f59e0b;
--shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
--shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 25px 60px rgba(0, 0, 0, 0.3);
--radius: 16px;
}
* {
box-sizing: border-box;
}
html, body {
height: 100%;
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #0f172a;
color: #0f172a;
}
body.crosshair #map {
cursor: crosshair;
}
.hidden {
display: none !important;
}
.app-shell {
display: grid;
grid-template-columns: 320px 1fr;
height: 100vh;
}
.sidebar {
background: var(--bg);
color: var(--text);
padding: 20px;
overflow-y: auto;
border-right: 1px solid rgba(255,255,255,0.05);
box-shadow: inset 0 0 40px rgba(0,0,0,0.2);
}
.sidebar::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 6px; }
.btn-back {
display: block;
text-align: center;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
color: var(--text-secondary);
border-radius: 12px;
padding: 10px;
font-size: 13px;
margin-bottom: 20px;
text-decoration: none;
font-weight: 600;
transition: all 0.2s;
}
.btn-back:hover { background: rgba(255,255,255,0.1); color: #fff; }
.brand {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 24px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.brand-badge {
width: 56px;
height: 56px;
border-radius: 14px;
display: grid;
place-items: center;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
color: #fff;
font-weight: 900;
font-size: 16px;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
letter-spacing: -0.5px;
}
.brand h1 {
font-family: 'Outfit', sans-serif;
font-size: 20px;
margin: 0;
font-weight: 800;
background: linear-gradient(to right, #60a5fa, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
.brand p {
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.3px;
}
.mode-info {
padding: 14px 16px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(139, 92, 246, 0.1) 100%);
border: 1px solid rgba(147, 197, 253, 0.3);
border-radius: 12px;
margin-bottom: 22px;
font-size: 14px;
color: #e0f2fe;
font-weight: 600;
}
.action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 24px;
}
.btn {
appearance: none;
border: 0;
padding: 12px 10px;
border-radius: 12px;
font-weight: 600;
font-size: 12px;
cursor: pointer;
transition: all .2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
text-align: center;
}
.btn i { font-size: 16px; margin-bottom: 2px; }
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0,0,0,0.25);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-primary {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #fff;
}
.btn-secondary {
background: rgba(255,255,255,0.03);
color: var(--text-secondary);
border: 1px solid rgba(255,255,255,0.08);
}
.btn-secondary:hover {
background: rgba(255,255,255,0.08);
color: #fff;
border-color: rgba(255,255,255,0.15);
}
.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: #fff;
}
.btn.is-active {
outline: 2px solid rgba(147, 197, 253, 0.6);
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), 0 4px 12px rgba(0,0,0,0.15);
}
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 24px;
}
.stat-card {
background: linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 14px;
padding: 16px 18px;
display: grid;
gap: 4px;
backdrop-filter: blur(8px);
transition: all .2s ease;
}
.stat-value {
font-size: 28px;
font-weight: 900;
color: #fff;
letter-spacing: -0.5px;
}
.stat-label {
color: var(--muted);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.2px;
}
.panel {
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 14px;
padding: 16px 18px;
margin-bottom: 16px;
backdrop-filter: blur(8px);
}
.panel h2 {
font-size: 14px;
margin: 0 0 12px;
color: #fff;
font-weight: 800;
letter-spacing: -0.3px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
margin: 8px 0;
font-size: 13px;
color: var(--muted);
font-weight: 500;
}
.legend-sep {
margin-top: 12px;
}
.swatch {
width: 16px;
height: 16px;
border-radius: 5px;
display: inline-block;
}
.nasional { background: #ef4444; }
.provinsi { background: #3b82f6; }
.kabupaten { background: #22c55e; }
.spbu24 { background: #22c55e; }
.spbuNon { background: #ef4444; }
.shm { background: #f59e0b; }
.hgb { background: #8b5cf6; }
.hgu { background: #14b8a6; }
.hp { background: #94a3b8; }
.ri { background: #7c3aed; }
.pmGreen { background: #16a34a; }
.pmRed { background: #dc2626; }
.help-box {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.05) 100%);
border: 1px solid rgba(147, 197, 253, 0.3);
color: #e0f2fe;
padding: 16px 18px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
line-height: 1.6;
backdrop-filter: blur(8px);
}
.list {
display: grid;
gap: 8px;
max-height: 220px;
overflow: auto;
padding-right: 6px;
}
.list::-webkit-scrollbar {
width: 10px;
}
.list::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.35);
border-radius: 999px;
border: 2px solid rgba(15, 23, 42, 0.3);
}
.list-item {
width: 100%;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
color: #e5e7eb;
border-radius: 12px;
padding: 11px 13px;
cursor: pointer;
transition: all .2s ease;
}
.list-item:hover {
transform: translateX(4px);
border-color: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.08);
}
.list-item.is-active {
border-color: rgba(147, 197, 253, 0.55);
background: rgba(37, 99, 235, 0.16);
}
.list-title {
font-weight: 800;
font-size: 13px;
color: #fff;
letter-spacing: -0.2px;
}
.list-meta {
margin-top: 4px;
font-size: 11px;
color: var(--muted);
font-weight: 500;
}
.list-empty {
font-size: 13px;
color: #cbd5e1;
padding: 10px 12px;
border-radius: 14px;
border: 1px dashed rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.04);
}
.spbu-icon {
background: transparent;
border: 0;
}
.spbu-dot {
width: 14px;
height: 14px;
border-radius: 999px;
border: 2px solid #ffffff;
box-shadow: 0 10px 18px rgba(15, 23, 42, 0.22);
}
.spbu-dot--green { background: #22c55e; }
.spbu-dot--red { background: #ef4444; }
.pm-icon {
background: transparent;
border: 0;
}
.pm-hit {
width: var(--pm-size, 30px);
height: var(--pm-size, 30px);
border-radius: 999px;
background: #0f172a;
border: 2px solid #ffffff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 14px 22px rgba(15, 23, 42, 0.22);
cursor: grab;
touch-action: none;
}
.pm-hit--green {
background: #16a34a;
}
.pm-hit--red {
background: #dc2626;
}
.pm-hit:active {
cursor: grabbing;
}
.pm-svg {
display: block;
}
.ri-icon {
background: transparent;
border: 0;
}
.ri-hit {
width: 24px;
height: 24px;
border-radius: 10px;
background: #7c3aed;
border: 2px solid #ffffff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 18px rgba(15, 23, 42, 0.18);
}
.ri-svg {
display: block;
}
.map-area {
position: relative;
background: #e2e8f0;
}
.map-toolbar {
position: absolute;
top: 14px;
right: 14px;
z-index: 1200;
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(226, 232, 240, 0.9);
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.12);
backdrop-filter: blur(8px);
}
.map-toolbar-row {
display: flex;
gap: 8px;
}
.map-tool {
border: 0;
background: #0f172a;
color: #fff;
padding: 9px 12px;
border-radius: 14px;
cursor: pointer;
font-weight: 800;
font-size: 13px;
transition: transform .15s ease, opacity .15s ease;
}
.map-tool:hover {
transform: translateY(-1px);
opacity: 0.96;
}
.map-tool:disabled {
opacity: 0.55;
cursor: not-allowed;
transform: none;
}
.map-tool.is-active {
background: linear-gradient(135deg, #0f172a 0%, #1f2a44 100%);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.12);
}
.edit-bar {
position: absolute;
bottom: 14px;
right: 14px;
z-index: 800;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 10px 10px 12px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.14);
backdrop-filter: blur(8px);
}
.edit-bar-title {
font-weight: 900;
font-size: 13px;
color: #0f172a;
margin-right: 2px;
}
.edit-bar-btn {
border: 0;
background: #e2e8f0;
color: #0f172a;
padding: 9px 12px;
border-radius: 14px;
cursor: pointer;
font-weight: 900;
font-size: 13px;
transition: transform .15s ease, opacity .15s ease;
}
.edit-bar-btn.primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: #fff;
}
.edit-bar-btn.danger {
background: linear-gradient(135deg, var(--danger), #b91c1c);
color: #fff;
}
.edit-bar-btn.is-active {
outline: 2px solid rgba(15, 23, 42, 0.22);
box-shadow: 0 0 0 5px rgba(239, 68, 68, 0.12), 0 14px 30px rgba(15, 23, 42, 0.12);
}
.edit-bar-btn:hover {
transform: translateY(-1px);
opacity: 0.96;
}
.edit-bar-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
#map {
width: 100%;
height: 100%;
position: relative;
z-index: 0;
}
.leaflet-control-container .leaflet-draw {
display: none !important;
}
.modal {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.54);
display: grid;
place-items: center;
z-index: 9999;
padding: 20px;
}
.modal-card {
width: 100%;
max-width: 540px;
background: #fff;
border-radius: 22px;
box-shadow: var(--shadow);
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, #f8fafc 0%, #fff 100%);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
}
.close-btn {
border: 0;
background: transparent;
font-size: 30px;
line-height: 1;
cursor: pointer;
color: #475569;
}
#featureForm {
padding: 18px 20px 20px;
}
.form-section {
display: grid;
gap: 10px;
}
.form-section h4 {
margin: 0 0 6px;
font-size: 15px;
color: #0f172a;
}
label {
font-size: 13px;
font-weight: 700;
color: #334155;
}
input, textarea, select {
width: 100%;
border: 1px solid #cbd5e1;
background: #fff;
border-radius: 14px;
padding: 11px 12px;
font: inherit;
color: #0f172a;
}
input[readonly] {
background: #f8fafc;
color: #475569;
}
.checkbox-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
font-weight: 600;
color: var(--text-secondary);
background: rgba(255,255,255,0.03);
border-radius: 10px;
margin-bottom: 6px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.checkbox-row:hover { background: rgba(255,255,255,0.06); color: #fff; }
.checkbox-row input {
accent-color: var(--primary);
width: 16px;
height: 16px;
cursor: pointer;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.notice {
margin: 0 0 14px;
padding: 12px 14px;
border-radius: 14px;
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
font-size: 14px;
line-height: 1.45;
}
.modal-actions {
display: flex;
gap: 10px;
margin-top: 18px;
}
.modal-actions .btn {
flex: 1;
box-shadow: none;
}
.leaflet-popup-content-wrapper {
border-radius: 14px;
}
.leaflet-popup-content {
margin: 12px 14px;
font-size: 13px;
line-height: 1.5;
}
.popup-actions {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.popup-btn {
border: 0;
padding: 7px 10px;
border-radius: 10px;
cursor: pointer;
font-weight: 700;
color: #fff;
}
.popup-btn.edit { background: #2563eb; }
.popup-btn.spatial { background: #7c3aed; }
.popup-btn.delete { background: #dc2626; }
.toast {
position: fixed;
left: calc(320px + 16px);
bottom: 16px;
z-index: 10000;
max-width: min(520px, calc(100vw - 32px));
padding: 12px 14px;
border-radius: 16px;
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.22);
border: 1px solid rgba(226, 232, 240, 0.9);
background: rgba(255, 255, 255, 0.96);
color: #0f172a;
font-weight: 700;
line-height: 1.35;
cursor: pointer;
word-break: break-word;
}
.toast.toast-success {
border-color: rgba(34, 197, 94, 0.35);
}
.toast.toast-error {
border-color: rgba(239, 68, 68, 0.35);
}
.toast.toast-info {
border-color: rgba(59, 130, 246, 0.35);
}
@media (max-width: 960px) {
.app-shell {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
padding: 20px;
}
.action-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stats {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.map-area {
min-height: 60vh;
}
.map-toolbar {
top: 10px;
right: 10px;
left: 10px;
justify-content: space-between;
}
.edit-bar {
left: 10px;
right: 10px;
bottom: 10px;
justify-content: space-between;
}
.toast {
left: 16px;
}
}
+61
View File
@@ -0,0 +1,61 @@
const https = require('https');
const fs = require('fs');
const kecamatans = [
"Kecamatan Pontianak Barat",
"Kecamatan Pontianak Kota",
"Kecamatan Pontianak Selatan",
"Kecamatan Pontianak Tenggara",
"Kecamatan Pontianak Timur",
"Kecamatan Pontianak Utara"
];
const featureCollection = {
type: "FeatureCollection",
features: []
};
let completed = 0;
function fetchGeojson(name) {
const url = `https://nominatim.openstreetmap.org/search.php?q=${encodeURIComponent(name)}&polygon_geojson=1&format=jsonv2`;
const options = {
headers: { 'User-Agent': 'WebGIS-SPBU-App/1.0' }
};
https.get(url, options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const results = JSON.parse(data);
const geomResult = results.find(r => r.geojson && r.category === 'boundary' && r.type === 'administrative');
if (geomResult) {
featureCollection.features.push({
type: "Feature",
properties: {
Kecamatan: name.replace('Kecamatan ', ''),
Name: name
},
geometry: geomResult.geojson
});
console.log("Fetched " + name);
} else {
console.log("Not found for " + name);
}
completed++;
if (completed === kecamatans.length) {
fs.writeFileSync('js/pontianak_kecamatan.geojson', JSON.stringify(featureCollection, null, 2));
console.log("All done! Saved to js/pontianak_kecamatan.geojson");
}
});
}).on('error', (e) => {
console.error(e);
});
}
// Nominatim has a usage policy of 1 request per second. We will stagger the requests.
kecamatans.forEach((kec, i) => {
setTimeout(() => {
fetchGeojson(kec);
}, i * 1500); // 1.5 seconds apart
});
+29
View File
@@ -0,0 +1,29 @@
const https = require('https');
const fs = require('fs');
const query = `[out:json];
area["name"="Pontianak"]->.searchArea;
(
relation["admin_level"="6"](area.searchArea);
);
out geom;`;
const options = {
hostname: 'overpass-api.de',
path: '/api/interpreter',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
fs.writeFileSync('osm_pontianak.json', data);
console.log('Done downloading OSM data.');
});
});
req.write(query);
req.end();
+359
View File
@@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>WebGIS SPBU, Jalan, Parsil, Rumah Ibadah, dan Pemukiman</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<div class="app-shell">
<!-- SIDEBAR -->
<aside class="sidebar">
<a href="../index.html" class="btn btn-back"><i class="fa-solid fa-arrow-left"></i> Kembali ke Landing Page</a>
<div class="brand">
<div class="brand-badge" aria-hidden="true"><i class="fa-solid fa-map-location-dot"></i></div>
<div>
<h1>WebGIS SPBU</h1>
<p>Infrastruktur & Pemukiman</p>
</div>
</div>
<div id="modeInfo" class="mode-info">Mode: Tidak aktif</div>
<div class="action-grid">
<button id="btnModePoint" class="btn btn-primary" type="button" onclick="setMode('point')"><i class="fa-solid fa-gas-pump"></i> Tambah Point</button>
<button id="btnModeJalan" class="btn btn-secondary" type="button" onclick="setMode('jalan')"><i class="fa-solid fa-road"></i> Tambah Jalan</button>
<button id="btnModeParsil" class="btn btn-secondary" type="button" onclick="setMode('parsil')"><i class="fa-solid fa-draw-polygon"></i> Tambah Parsil</button>
<button id="btnModeRumahIbadah" class="btn btn-secondary" type="button" onclick="setMode('rumahIbadah')"><i class="fa-solid fa-mosque"></i> Rumah Ibadah</button>
<button id="btnModePemukiman" class="btn btn-secondary" type="button" onclick="setMode('pemukiman')"><i class="fa-solid fa-house-chimney-crack"></i> Pemukiman</button>
<button id="btnResetMode" class="btn btn-danger" type="button" onclick="resetMode()"><i class="fa-solid fa-xmark"></i> Batal Mode</button>
</div>
<div class="stats" aria-label="Statistik layer">
<div class="stat-card">
<div class="stat-value" id="countPoints">0</div>
<div class="stat-label">Point SPBU</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countJalan">0</div>
<div class="stat-label">Jalan</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countParsil">0</div>
<div class="stat-label">Parsil</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countRumahIbadah">0</div>
<div class="stat-label">Rumah Ibadah</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countPemukiman">0</div>
<div class="stat-label">Pemukiman Miskin</div>
</div>
<div class="stat-card">
<div class="stat-value" id="countSektor">0</div>
<div class="stat-label">Sektor Kecamatan</div>
</div>
</div>
<div class="panel layer-panel">
<h2><i class="fa-solid fa-layer-group"></i> Layer Peta</h2>
<label class="checkbox-row">
<input type="checkbox" id="togglePoints" checked>
<span class="chk-label">Point SPBU</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="toggleJalan" checked>
<span class="chk-label">Jaringan Jalan</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="toggleParsil" checked>
<span class="chk-label">Batas Parsil</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="toggleRumahIbadah" checked>
<span class="chk-label">Rumah Ibadah (+ radius)</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="togglePemukiman" checked>
<span class="chk-label">Pemukiman Miskin</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="toggleSektor" checked>
<span class="chk-label">Sektor Kecamatan</span>
</label>
</div>
<div class="panel">
<h2>Daftar SPBU</h2>
<div id="pointsList" class="list" aria-label="Daftar point SPBU"></div>
</div>
<div class="panel">
<h2>Daftar Jalan</h2>
<div id="jalanList" class="list" aria-label="Daftar jalan"></div>
</div>
<div class="panel">
<h2>Daftar Parsil</h2>
<div id="parsilList" class="list" aria-label="Daftar parsil"></div>
</div>
<div class="panel">
<h2>Daftar Rumah Ibadah</h2>
<div id="rumahIbadahList" class="list" aria-label="Daftar rumah ibadah"></div>
</div>
<div class="panel">
<h2>Daftar Pemukiman Miskin</h2>
<div id="pemukimanList" class="list" aria-label="Daftar pemukiman miskin"></div>
</div>
<div class="panel">
<h2>Daftar Sektor Kecamatan</h2>
<div id="sektorList" class="list" aria-label="Daftar sektor kecamatan"></div>
</div>
<div class="panel">
<h2>Legenda</h2>
<div class="legend-item"><span class="swatch spbu24"></span> SPBU 24 Jam</div>
<div class="legend-item"><span class="swatch spbuNon"></span> SPBU Tidak 24 Jam</div>
<div class="legend-item"><span class="swatch nasional"></span> Jalan Nasional</div>
<div class="legend-item"><span class="swatch provinsi"></span> Jalan Provinsi</div>
<div class="legend-item"><span class="swatch kabupaten"></span> Jalan Kabupaten</div>
<div class="legend-item legend-sep"><span class="swatch shm"></span> Parsil SHM</div>
<div class="legend-item"><span class="swatch hgb"></span> Parsil HGB</div>
<div class="legend-item"><span class="swatch hgu"></span> Parsil HGU</div>
<div class="legend-item"><span class="swatch hp"></span> Parsil HP</div>
<div class="legend-item legend-sep"><span class="swatch ri"></span> Rumah Ibadah</div>
<div class="legend-item"><span class="swatch pmGreen"></span> Pemukiman (dalam radius bantuan)</div>
<div class="legend-item"><span class="swatch pmRed"></span> Pemukiman (di luar radius bantuan)</div>
</div>
<div id="helpBox" class="help-box">
Pilih mode di atas, lalu:
<br><b>Point</b>: klik lokasi di peta.
<br><b>Jalan</b>: gambar garis, double-click untuk selesai.
<br><b>Parsil</b>: gambar poligon, klik titik awal untuk selesai.
<br><b>Rumah Ibadah</b>: klik lokasi di peta.
<br><b>Pemukiman</b>: klik lokasi di peta.
<br><br>Klik objek di peta untuk <b>Edit</b> / <b>Hapus</b> (CRUD).
</div>
</aside>
<!-- MAP -->
<main class="map-area">
<div class="map-toolbar" aria-label="Toolbar peta">
<div class="map-toolbar-row">
<button class="map-tool" id="btnResetView" type="button">Reset View</button>
<button class="map-tool" id="btnMyLocation" type="button">Lokasi Saya</button>
</div>
<div class="map-toolbar-row">
<button class="map-tool" id="btnFilter24" type="button">SPBU 24 Jam</button>
<button class="map-tool" id="btnFilterNon24" type="button">SPBU Tidak 24 Jam</button>
</div>
</div>
<div id="editBar" class="edit-bar hidden" aria-label="Edit lokasi">
<div class="edit-bar-title" id="editBarTitle">Edit Lokasi</div>
<button class="edit-bar-btn danger hidden" id="btnDeleteVertex" type="button">Hapus Vertex</button>
<button class="edit-bar-btn primary" id="btnSaveLocation" type="button">Simpan Lokasi</button>
<button class="edit-bar-btn" id="btnCancelLocation" type="button">Batal</button>
</div>
<div id="map"></div>
</main>
</div>
<!-- MODAL -->
<div id="featureModal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal-card">
<div class="modal-header">
<h3 id="modalTitle">Input Data</h3>
<button type="button" class="close-btn" onclick="closeModal()" aria-label="Tutup">&times;</button>
</div>
<form id="featureForm">
<p id="formNotice" class="notice hidden"></p>
<!-- POINT -->
<div id="pointFields" class="form-section hidden">
<h4>Point SPBU</h4>
<label for="namaPoint">Nama</label>
<input id="namaPoint" placeholder="Contoh: SPBU Sudirman" required>
<label for="noPoint">No</label>
<input id="noPoint" placeholder="Contoh: 64.781.01" required>
<label for="deskripsiPoint">Deskripsi</label>
<textarea id="deskripsiPoint" placeholder="Catatan singkat (opsional)" rows="3"></textarea>
<label class="checkbox-row">
<input type="checkbox" id="status24Jam"> 24 Jam
</label>
<div class="grid-2">
<div>
<label for="latPoint">Latitude</label>
<input id="latPoint" readonly>
</div>
<div>
<label for="lngPoint">Longitude</label>
<input id="lngPoint" readonly>
</div>
</div>
</div>
<!-- JALAN -->
<div id="roadFields" class="form-section hidden">
<h4>Jalan</h4>
<label for="namaJalan">Nama Jalan</label>
<input id="namaJalan" placeholder="Contoh: Jl. Ahmad Yani" required>
<label for="statusJalan">Status</label>
<select id="statusJalan">
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
</select>
</div>
<!-- PARSIL -->
<div id="parsilFields" class="form-section hidden">
<h4>Parsil</h4>
<label for="statusParsil">Status Kepemilikan</label>
<select id="statusParsil">
<option value="SHM">SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
</div>
<!-- RUMAH IBADAH -->
<div id="rumahIbadahFields" class="form-section hidden">
<h4>Rumah Ibadah</h4>
<label for="namaRumahIbadah">Nama</label>
<input id="namaRumahIbadah" placeholder="Contoh: Masjid Al-Ikhlas" required>
<label for="jenisRumahIbadah">Jenis</label>
<select id="jenisRumahIbadah">
<option value="Lainnya">Lainnya</option>
<option value="Masjid">Masjid</option>
<option value="Gereja">Gereja</option>
<option value="Pura">Pura</option>
<option value="Vihara">Vihara</option>
<option value="Klenteng">Klenteng</option>
</select>
<label for="kontakRumahIbadah">Kontak (opsional)</label>
<input id="kontakRumahIbadah" placeholder="No HP / PIC (opsional)">
<label for="radiusRumahIbadah">Radius Bantuan (meter)</label>
<input id="radiusRumahIbadah" type="number" min="1" step="1" placeholder="Contoh: 500" required>
<label for="alamatRumahIbadah">Alamat (opsional)</label>
<textarea id="alamatRumahIbadah" placeholder="Alamat singkat (opsional)" rows="2"></textarea>
<div class="grid-2">
<div>
<label for="latRumahIbadah">Latitude</label>
<input id="latRumahIbadah" readonly>
</div>
<div>
<label for="lngRumahIbadah">Longitude</label>
<input id="lngRumahIbadah" readonly>
</div>
</div>
</div>
<!-- PEMUKIMAN MISKIN -->
<div id="pemukimanFields" class="form-section hidden">
<h4>Pemukiman Miskin</h4>
<label for="kkNamaPemukiman">Nama KK</label>
<input id="kkNamaPemukiman" placeholder="Contoh: Budi" required>
<label for="nikPemukiman">NIK (opsional)</label>
<input id="nikPemukiman" placeholder="Contoh: 6171xxxxxxxxxxxx">
<label for="jumlahAnggotaPemukiman">Jumlah Anggota (opsional)</label>
<input id="jumlahAnggotaPemukiman" type="number" min="0" step="1" placeholder="Contoh: 4">
<label for="statusBantuanPemukiman">Status Bantuan</label>
<select id="statusBantuanPemukiman">
<option value="Belum dibantu">Belum dibantu</option>
<option value="Menunggu verifikasi">Menunggu verifikasi</option>
<option value="Sudah dibantu">Sudah dibantu</option>
</select>
<label for="jenisBantuanPemukiman">Jenis Bantuan (opsional)</label>
<input id="jenisBantuanPemukiman" placeholder="Contoh: Sembako">
<label for="tanggalBantuanPemukiman">Tanggal Bantuan (opsional)</label>
<input id="tanggalBantuanPemukiman" type="date">
<label for="alamatPemukiman">Alamat (opsional)</label>
<textarea id="alamatPemukiman" placeholder="Alamat singkat (opsional)" rows="2"></textarea>
<div class="grid-2">
<div>
<label for="kelurahanPemukiman">Kelurahan (opsional)</label>
<input id="kelurahanPemukiman" placeholder="Contoh: Sungai Jawi">
</div>
<div>
<label for="kecamatanPemukiman">Kecamatan (opsional)</label>
<input id="kecamatanPemukiman" placeholder="Contoh: Pontianak Kota">
</div>
</div>
<div class="grid-2">
<div>
<label for="latPemukiman">Latitude</label>
<input id="latPemukiman" readonly>
</div>
<div>
<label for="lngPemukiman">Longitude</label>
<input id="lngPemukiman" readonly>
</div>
</div>
</div>
<!-- BUTTON -->
<div class="modal-actions">
<button id="btnSubmitFeature" class="btn btn-primary" type="submit">Simpan Data</button>
<button class="btn btn-secondary" type="button" onclick="closeModal()">Batal</button>
</div>
</form>
</div>
</div>
<!-- TOAST -->
<div id="toast" class="toast hidden" aria-live="polite" aria-atomic="true"></div>
<!-- SCRIPT -->
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.geometryutil/0.10.1/leaflet.geometryutil.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
<?php
require_once 'php/koneksi.php';
try {
$pdo->exec("ALTER TABLE rumah_ibadah_points ADD COLUMN IF NOT EXISTS jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya' AFTER nama");
$pdo->exec("ALTER TABLE rumah_ibadah_points ADD COLUMN IF NOT EXISTS kontak VARCHAR(120) NULL AFTER jenis");
echo "Columns added successfully.\n";
} catch (PDOException $e) {
echo "Failed to add columns: " . $e->getMessage() . "\n";
}
+9
View File
@@ -0,0 +1,9 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>406 Not Acceptable</title>
</head><body>
<h1>Not Acceptable</h1>
<p>An appropriate representation of the requested resource could not be found on this server.</p>
<hr>
<address>Apache/2.4.67 (Debian) Server at overpass-api.de Port 443</address>
</body></html>
+40
View File
@@ -0,0 +1,40 @@
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('osm_pontianak.json', 'utf8'));
const geojson = {
type: 'FeatureCollection',
features: []
};
data.elements.forEach(el => {
if (el.type === 'relation' && el.tags && el.tags.admin_level === '6') {
const name = el.tags.name;
// Extract polygons from relation members
const coordinates = [];
el.members.forEach(member => {
if (member.type === 'way' && member.geometry) {
const wayCoords = member.geometry.map(pt => [pt.lon, pt.lat]);
coordinates.push(wayCoords);
}
});
// Assemble into MultiPolygon or Polygon (simplistic)
if (coordinates.length > 0) {
geojson.features.push({
type: 'Feature',
properties: {
kecamatan: name,
...el.tags
},
geometry: {
type: 'Polygon',
coordinates: coordinates
}
});
}
}
});
// Since OSM returns fragments of outer and inner rings in relations, a naive conversion might have topological issues in Leaflet (like crossing lines if not ordered).
// We should use an existing library like osmtogeojson. Let's install it and use it.
+49
View File
@@ -0,0 +1,49 @@
<?php
header('Content-Type: application/json');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["success"=>false,"message"=>"Data kosong"]);
exit;
}
try {
$geojson = json_encode($data['geometry'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare("
INSERT INTO jalan_lines
(nama_jalan, status_jalan, panjang_meter, geojson)
VALUES (?, ?, ?, ?)
");
$stmt->execute([
$data['nama_jalan'],
$data['status_jalan'],
$data['panjang_meter'],
$geojson
]);
} catch (Exception $inner) {
// Fallback untuk skema spatial: simpan ke kolom `geom` jika `geojson` tidak ada.
$stmt = $pdo->prepare("
INSERT INTO jalan_lines
(nama_jalan, status_jalan, panjang_meter, geom)
VALUES (?, ?, ?, ST_GeomFromGeoJSON(?))
");
$stmt->execute([
$data['nama_jalan'],
$data['status_jalan'],
$data['panjang_meter'],
$geojson
]);
}
echo json_encode(["success"=>true]);
} catch (Exception $e) {
echo json_encode(["success"=>false,"message"=>$e->getMessage()]);
}
?>
+65
View File
@@ -0,0 +1,65 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
// Payload expected from js/app.js:
// { status_kepemilikan, luas_m2, geometry }
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(["success" => false, "status" => "error", "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$status_kepemilikan = trim($data['status_kepemilikan'] ?? '');
$luas_m2 = (float)($data['luas_m2'] ?? 0);
$geometry = $data['geometry'] ?? null;
if ($status_kepemilikan === '' || !$geometry) {
echo json_encode(["success" => false, "status" => "error", "message" => "Data parsil belum lengkap (status/geometry)."]);
exit;
}
try {
$geomJson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare("
INSERT INTO parsil_tanah (status_kepemilikan, luas_m2, geojson)
VALUES (:status_kepemilikan, :luas_m2, :geojson)
");
$stmt->execute([
':status_kepemilikan' => $status_kepemilikan,
':luas_m2' => $luas_m2,
':geojson' => $geomJson
]);
} catch (Exception $inner) {
// Fallback untuk skema spatial: simpan ke kolom `geom` jika `geojson` tidak ada.
try {
$stmt = $pdo->prepare("
INSERT INTO parsil_tanah (status_kepemilikan, luas_m2, geom)
VALUES (:status_kepemilikan, :luas_m2, ST_GeomFromGeoJSON(:geom))
");
$stmt->execute([
':status_kepemilikan' => $status_kepemilikan,
':luas_m2' => $luas_m2,
':geom' => $geomJson
]);
} catch (Exception $inner2) {
// Last resort: minimal insert.
$stmt = $pdo->prepare("
INSERT INTO parsil_tanah (geom)
VALUES (ST_GeomFromGeoJSON(:geom))
");
$stmt->execute([':geom' => $geomJson]);
}
}
echo json_encode(["success" => true, "status" => "success", "message" => "Area Parsil berhasil disimpan."]);
} catch (Exception $e) {
echo json_encode(["success" => false, "status" => "error", "message" => $e->getMessage()]);
}
?>
+192
View File
@@ -0,0 +1,192 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function haversine_m($lat1, $lon1, $lat2, $lon2) {
$R = 6371000.0; // meters
$phi1 = deg2rad((float)$lat1);
$phi2 = deg2rad((float)$lat2);
$dphi = deg2rad((float)$lat2 - (float)$lat1);
$dlambda = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dphi/2) * sin($dphi/2) + cos($phi1) * cos($phi2) * sin($dlambda/2) * sin($dlambda/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $R * $c;
}
function get_payload() {
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
if (stripos($ct, 'multipart/form-data') !== false) {
return [
'kk_nama' => $_POST['kk_nama'] ?? '',
'nik' => $_POST['nik'] ?? '',
'jumlah_anggota' => $_POST['jumlah_anggota'] ?? '',
'latitude' => $_POST['latitude'] ?? null,
'longitude' => $_POST['longitude'] ?? null,
'address' => $_POST['address'] ?? '',
'kelurahan' => $_POST['kelurahan'] ?? '',
'kecamatan' => $_POST['kecamatan'] ?? '',
'status_bantuan' => $_POST['status_bantuan'] ?? 'Belum dibantu',
'jenis_bantuan' => $_POST['jenis_bantuan'] ?? '',
'tanggal_bantuan' => $_POST['tanggal_bantuan'] ?? '',
'anggota' => $_POST['anggota'] ?? '[]'
];
}
$json = json_decode(file_get_contents('php://input'), true);
return is_array($json) ? $json : null;
}
function compute_nearest_ibadah(PDO $pdo, $lat, $lng) {
$stmt = $pdo->query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points");
$rows = $stmt->fetchAll();
$best = null;
foreach ($rows as $r) {
$rLat = (float)$r['latitude'];
$rLng = (float)$r['longitude'];
$radius = (float)$r['radius_meter'];
if ($radius <= 0) continue;
$d = haversine_m($lat, $lng, $rLat, $rLng);
if ($d <= $radius) {
if ($best === null || $d < $best['jarak_meter']) {
$best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d];
}
}
}
return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null];
}
function save_upload_if_any() {
if (!isset($_FILES['bukti_file']) || !is_array($_FILES['bukti_file'])) return null;
if (($_FILES['bukti_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) return null;
$dir = __DIR__ . '/../uploads/bukti';
if (!is_dir($dir)) @mkdir($dir, 0775, true);
if (!is_dir($dir)) return null;
$orig = (string)($_FILES['bukti_file']['name'] ?? 'file');
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp','pdf'];
if ($ext && !in_array($ext, $allowed, true)) return null;
$safeBase = preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($orig, PATHINFO_FILENAME));
$safeBase = trim($safeBase, '_');
if ($safeBase === '') $safeBase = 'bukti';
$filename = $safeBase . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . ($ext ? ('.' . $ext) : '');
$dest = $dir . '/' . $filename;
if (!move_uploaded_file($_FILES['bukti_file']['tmp_name'], $dest)) return null;
return 'uploads/bukti/' . $filename;
}
$data = get_payload();
if (!$data) {
echo json_encode(["success" => false, "message" => "Payload kosong atau tidak valid."]);
exit;
}
$kk_nama = trim($data['kk_nama'] ?? $data['nama'] ?? '');
$nik = trim($data['nik'] ?? '');
$jumlah_anggota = (int)($data['jumlah_anggota'] ?? 0);
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$address = trim($data['address'] ?? '');
$kelurahan = trim($data['kelurahan'] ?? '');
$kecamatan = trim($data['kecamatan'] ?? '');
$status_bantuan = trim($data['status_bantuan'] ?? 'Belum dibantu');
$jenis_bantuan = trim($data['jenis_bantuan'] ?? '');
$tanggal_bantuan = trim($data['tanggal_bantuan'] ?? '');
$anggotaRaw = $data['anggota'] ?? [];
$validStatus = ['Belum dibantu', 'Sudah dibantu', 'Menunggu verifikasi'];
if (!in_array($status_bantuan, $validStatus, true)) $status_bantuan = 'Belum dibantu';
if ($kk_nama === '' || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data penduduk miskin belum lengkap (nama/lat/lng)."]);
exit;
}
try {
$lat = (float)$latitude;
$lng = (float)$longitude;
$nearest = compute_nearest_ibadah($pdo, $lat, $lng);
$uploadPath = save_upload_if_any();
$pdo->beginTransaction();
// Backward-compat: some installs still have old column `nama`.
// Prefer new schema if columns exist.
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
if ($hasNew) {
$stmt = $pdo->prepare("
INSERT INTO pemukiman_miskin_points
(kk_nama, nik, jumlah_anggota, latitude, longitude, address, kelurahan, kecamatan,
status_bantuan, jenis_bantuan, tanggal_bantuan, bukti_file, jarak_meter, rumah_ibadah_id)
VALUES
(:kk_nama, :nik, :jumlah_anggota, :latitude, :longitude, :address, :kelurahan, :kecamatan,
:status_bantuan, :jenis_bantuan, :tanggal_bantuan, :bukti_file, :jarak_meter, :rumah_ibadah_id)
");
$stmt->execute([
':kk_nama' => $kk_nama,
':nik' => ($nik === '' ? null : $nik),
':jumlah_anggota' => $jumlah_anggota,
':latitude' => $lat,
':longitude' => $lng,
':address' => ($address === '' ? null : $address),
':kelurahan' => ($kelurahan === '' ? null : $kelurahan),
':kecamatan' => ($kecamatan === '' ? null : $kecamatan),
':status_bantuan' => $status_bantuan,
':jenis_bantuan' => ($jenis_bantuan === '' ? null : $jenis_bantuan),
':tanggal_bantuan' => ($tanggal_bantuan === '' ? null : $tanggal_bantuan),
':bukti_file' => $uploadPath,
':jarak_meter' => $nearest['jarak_meter'],
':rumah_ibadah_id' => $nearest['rumah_ibadah_id']
]);
} else {
$stmt = $pdo->prepare("
INSERT INTO pemukiman_miskin_points (nama, latitude, longitude, address, rumah_ibadah_id)
VALUES (:nama, :latitude, :longitude, :address, :rumah_ibadah_id)
");
$stmt->execute([
':nama' => $kk_nama,
':latitude' => $lat,
':longitude' => $lng,
':address' => ($address === '' ? null : $address),
':rumah_ibadah_id' => $nearest['rumah_ibadah_id']
]);
}
$newId = (int)$pdo->lastInsertId();
// anggota keluarga (optional)
$anggota = is_string($anggotaRaw) ? json_decode($anggotaRaw, true) : $anggotaRaw;
if (is_array($anggota) && count($anggota) > 0 && $hasNew) {
$ins = $pdo->prepare("
INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan, keterangan)
VALUES (:penduduk_id, :nama, :hubungan, :umur, :pekerjaan, :keterangan)
");
foreach ($anggota as $a) {
$n = trim((string)($a['nama'] ?? ''));
$h = trim((string)($a['hubungan'] ?? ''));
if ($n === '' || $h === '') continue;
$ins->execute([
':penduduk_id' => $newId,
':nama' => $n,
':hubungan' => $h,
':umur' => isset($a['umur']) && $a['umur'] !== '' ? (int)$a['umur'] : null,
':pekerjaan' => ($a['pekerjaan'] ?? '') === '' ? null : (string)$a['pekerjaan'],
':keterangan' => ($a['keterangan'] ?? '') === '' ? null : (string)$a['keterangan']
]);
}
}
$pdo->commit();
echo json_encode([
"success" => true,
"id" => $newId,
"rumah_ibadah_id" => $nearest['rumah_ibadah_id'],
"jarak_meter" => $nearest['jarak_meter'],
"message" => "Penduduk miskin berhasil ditambahkan"
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+124
View File
@@ -0,0 +1,124 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(["success" => false, "status" => "error", "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$nama = trim($data['nama'] ?? '');
$no = trim($data['no'] ?? '');
$deskripsi = trim($data['deskripsi'] ?? '');
$status_24jam = isset($data['status_24jam']) ? (int)$data['status_24jam'] : 0;
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
if ($nama === '' || $no === '' || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "status" => "error", "message" => "Data point belum lengkap (nama/no/lat/lng)."]);
exit;
}
$pointGeojson = json_encode([
"type" => "Point",
"coordinates" => [(float)$longitude, (float)$latitude]
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// Optional legacy field if client ever sends it
$geomPayload = $data['geom'] ?? null;
if (!is_string($geomPayload) || $geomPayload === '') {
$geomPayload = $pointGeojson;
}
try {
// Preferred schema (non-spatial, consistent with update_point.php)
try {
$stmt = $pdo->prepare("
INSERT INTO spbu_points (nama, no, deskripsi, status_24jam, latitude, longitude)
VALUES (:nama, :no, :deskripsi, :status_24jam, :latitude, :longitude)
");
$stmt->execute([
':nama' => $nama,
':no' => $no,
':deskripsi' => $deskripsi,
':status_24jam' => $status_24jam,
':latitude' => $latitude,
':longitude' => $longitude
]);
echo json_encode([
"success" => true,
"status" => "success",
"message" => "Point SPBU berhasil ditambahkan",
"id" => (int)$pdo->lastInsertId()
]);
exit;
} catch (Exception $e1) {
// continue to fallback attempts
}
// Fallback schema: `geom` stored as LONGTEXT (GeoJSON string)
try {
$stmt = $pdo->prepare("
INSERT INTO spbu_points (nama, no, deskripsi, status_24jam, latitude, longitude, geom)
VALUES (:nama, :no, :deskripsi, :status_24jam, :latitude, :longitude, :geom)
");
$stmt->execute([
':nama' => $nama,
':no' => $no,
':deskripsi' => $deskripsi,
':status_24jam' => $status_24jam,
':latitude' => $latitude,
':longitude' => $longitude,
':geom' => $geomPayload
]);
echo json_encode([
"success" => true,
"status" => "success",
"message" => "Point SPBU berhasil ditambahkan",
"id" => (int)$pdo->lastInsertId()
]);
exit;
} catch (Exception $e2) {
// continue to fallback attempts
}
// Fallback schema: only (nama, geom) where geom is LONGTEXT GeoJSON
try {
$stmt = $pdo->prepare("INSERT INTO spbu_points (nama, geom) VALUES (:nama, :geom)");
$stmt->execute([
':nama' => $nama,
':geom' => $geomPayload
]);
echo json_encode([
"success" => true,
"status" => "success",
"message" => "Point SPBU berhasil ditambahkan",
"id" => (int)$pdo->lastInsertId()
]);
exit;
} catch (Exception $e3) {
// continue to fallback attempts
}
// Fallback schema: `geom` is spatial GEOMETRY/POINT
$stmt = $pdo->prepare("INSERT INTO spbu_points (nama, geom) VALUES (:nama, ST_GeomFromGeoJSON(:geom))");
$stmt->execute([
':nama' => $nama,
':geom' => $pointGeojson
]);
echo json_encode([
"success" => true,
"status" => "success",
"message" => "Point SPBU berhasil ditambahkan",
"id" => (int)$pdo->lastInsertId()
]);
} catch (Exception $e) {
echo json_encode(["success" => false, "status" => "error", "message" => $e->getMessage()]);
}
+60
View File
@@ -0,0 +1,60 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$nama = trim($data['nama'] ?? '');
$jenis = trim($data['jenis'] ?? 'Lainnya');
$kontak = trim($data['kontak'] ?? '');
$radius_meter = (float)($data['radius_meter'] ?? 0);
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$address = trim($data['address'] ?? '');
if ($nama === '' || $radius_meter <= 0 || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data rumah ibadah belum lengkap (nama/radius/lat/lng)."]);
exit;
}
try {
$cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasJenis = in_array('jenis', $cols, true);
$hasKontak = in_array('kontak', $cols, true);
if ($hasJenis && $hasKontak) {
$stmt = $pdo->prepare("
INSERT INTO rumah_ibadah_points (nama, jenis, kontak, radius_meter, latitude, longitude, address)
VALUES (:nama, :jenis, :kontak, :radius_meter, :latitude, :longitude, :address)
");
$stmt->execute([
':nama' => $nama,
':jenis' => ($jenis === '' ? 'Lainnya' : $jenis),
':kontak' => ($kontak === '' ? null : $kontak),
':radius_meter' => $radius_meter,
':latitude' => $latitude,
':longitude' => $longitude,
':address' => ($address === '' ? null : $address)
]);
} else {
$stmt = $pdo->prepare("
INSERT INTO rumah_ibadah_points (nama, radius_meter, latitude, longitude, address)
VALUES (:nama, :radius_meter, :latitude, :longitude, :address)
");
$stmt->execute([
':nama' => $nama,
':radius_meter' => $radius_meter,
':latitude' => $latitude,
':longitude' => $longitude,
':address' => ($address === '' ? null : $address)
]);
}
echo json_encode(["success" => true, "id" => (int)$pdo->lastInsertId(), "message" => "Rumah ibadah berhasil ditambahkan"]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+16
View File
@@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM jalan_lines WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Data jalan berhasil dihapus"]);
+16
View File
@@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM parsil_tanah WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Data parsil berhasil dihapus"]);
@@ -0,0 +1,33 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
try {
// best-effort delete uploaded proof file
try {
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
if (in_array('bukti_file', $cols, true)) {
$stmt = $pdo->prepare("SELECT bukti_file FROM pemukiman_miskin_points WHERE id = ?");
$stmt->execute([$id]);
$path = $stmt->fetchColumn();
if ($path && is_string($path) && strpos($path, 'uploads/bukti/') === 0) {
$full = __DIR__ . '/../' . $path;
if (is_file($full)) @unlink($full);
}
}
} catch (Exception $e) { /* ignore */ }
$stmt = $pdo->prepare("DELETE FROM pemukiman_miskin_points WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Pemukiman miskin berhasil dihapus"]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+16
View File
@@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
$stmt = $pdo->prepare("DELETE FROM spbu_points WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Data point berhasil dihapus"]);
+19
View File
@@ -0,0 +1,19 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
$id = (int)($data['id'] ?? 0);
if ($id <= 0) {
echo json_encode(["success" => false, "message" => "ID tidak valid"]);
exit;
}
try {
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah_points WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(["success" => true, "message" => "Rumah ibadah berhasil dihapus"]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+27
View File
@@ -0,0 +1,27 @@
<?php
$host = getenv('DB_HOST') ?: "localhost";
$port = getenv('DB_PORT') ?: "3306";
$db = getenv('DB_NAME') ?: "webgis_spbu";
$user = getenv('DB_USER') ?: "root";
$pass = getenv('DB_PASSWORD') ?: "";
$charset = "utf8mb4";
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$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, $user, $pass, $options);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode([
"success" => false,
"message" => "Koneksi database gagal: " . $e->getMessage()
]);
exit;
}
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$penduduk_id = isset($_GET['penduduk_id']) ? (int)$_GET['penduduk_id'] : 0;
if ($penduduk_id <= 0) {
echo json_encode([]);
exit;
}
try {
$stmt = $pdo->prepare("
SELECT id, penduduk_id, nama, hubungan, umur, pekerjaan, keterangan
FROM anggota_keluarga
WHERE penduduk_id = :id
ORDER BY id ASC
");
$stmt->execute([':id' => $penduduk_id]);
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
echo json_encode([]);
}
+51
View File
@@ -0,0 +1,51 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$features = [];
function rowsToFeatures($stmt) {
$features = [];
while ($row = $stmt->fetch()) {
$geometry = json_decode($row['geojson'] ?? '', true);
if (!$geometry) {
continue;
}
$features[] = [
"type" => "Feature",
"geometry" => $geometry,
"properties" => [
"id" => (int)($row['id'] ?? 0),
"nama_jalan" => $row['nama_jalan'] ?? null,
"status_jalan" => $row['status_jalan'] ?? null,
"panjang_meter" => isset($row['panjang_meter']) ? (float)$row['panjang_meter'] : null
]
];
}
return $features;
}
try {
// Preferred schema: geojson column.
$stmt = $pdo->query("SELECT id, nama_jalan, status_jalan, panjang_meter, geojson FROM jalan_lines ORDER BY id ASC");
$features = rowsToFeatures($stmt);
} catch (Exception $e) {
// Fallback schema: spatial geom.
$stmt = $pdo->query("
SELECT
id,
nama_jalan,
status_jalan,
panjang_meter,
ST_AsGeoJSON(geom) AS geojson
FROM jalan_lines
ORDER BY id ASC
");
$features = rowsToFeatures($stmt);
}
echo json_encode([
"type" => "FeatureCollection",
"features" => $features
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+49
View File
@@ -0,0 +1,49 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$features = [];
function rowsToFeatures($stmt) {
$features = [];
while ($row = $stmt->fetch()) {
$geometry = json_decode($row['geojson'] ?? '', true);
if (!$geometry) {
continue;
}
$features[] = [
"type" => "Feature",
"geometry" => $geometry,
"properties" => [
"id" => (int)($row['id'] ?? 0),
"status_kepemilikan" => $row['status_kepemilikan'] ?? null,
"luas_m2" => isset($row['luas_m2']) ? (float)$row['luas_m2'] : null
]
];
}
return $features;
}
try {
// Preferred schema: geojson column.
$stmt = $pdo->query("SELECT id, status_kepemilikan, luas_m2, geojson FROM parsil_tanah ORDER BY id ASC");
$features = rowsToFeatures($stmt);
} catch (Exception $e) {
// Fallback schema: spatial geom.
$stmt = $pdo->query("
SELECT
id,
status_kepemilikan,
luas_m2,
ST_AsGeoJSON(geom) AS geojson
FROM parsil_tanah
ORDER BY id ASC
");
$features = rowsToFeatures($stmt);
}
echo json_encode([
"type" => "FeatureCollection",
"features" => $features
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+127
View File
@@ -0,0 +1,127 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function haversine_m($lat1, $lon1, $lat2, $lon2) {
$R = 6371000.0;
$phi1 = deg2rad((float)$lat1);
$phi2 = deg2rad((float)$lat2);
$dphi = deg2rad((float)$lat2 - (float)$lat1);
$dlambda = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dphi/2) * sin($dphi/2) + cos($phi1) * cos($phi2) * sin($dlambda/2) * sin($dlambda/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $R * $c;
}
function compute_nearest_ibadah(array $ibadahRows, $lat, $lng) {
$best = null;
foreach ($ibadahRows as $r) {
$rLat = (float)($r['latitude'] ?? 0);
$rLng = (float)($r['longitude'] ?? 0);
$radius = (float)($r['radius_meter'] ?? 0);
if ($radius <= 0) continue;
$d = haversine_m($lat, $lng, $rLat, $rLng);
if ($d <= $radius) {
if ($best === null || $d < $best['jarak_meter']) {
$best = [
'rumah_ibadah_id' => (int)($r['id'] ?? 0),
'jarak_meter' => $d,
'row' => $r
];
}
}
}
return $best;
}
// Prefer new schema; fall back to old schema if needed.
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
$hasJarak = in_array('jarak_meter', $cols, true);
$hasRumahIbadahId = in_array('rumah_ibadah_id', $cols, true);
// Fetch rumah ibadah once; used for real-time coverage computation server-side.
$ibadah = [];
try {
$stmtI = $pdo->query("SELECT id, nama, jenis, kontak, radius_meter, latitude, longitude FROM rumah_ibadah_points");
$ibadah = $stmtI->fetchAll();
} catch (Exception $e) {
$ibadah = [];
}
// Load pemukiman rows
$rows = [];
try {
if ($hasNew) {
$stmt = $pdo->query("
SELECT p.*,
(
SELECT COUNT(*) FROM anggota_keluarga a WHERE a.penduduk_id = p.id
) AS anggota_count
FROM pemukiman_miskin_points p
ORDER BY p.id ASC
");
$rows = $stmt->fetchAll();
} else {
$stmt = $pdo->query("SELECT p.* FROM pemukiman_miskin_points p ORDER BY p.id ASC");
$rows = $stmt->fetchAll();
}
} catch (Exception $e) {
$rows = [];
}
// Best-effort: keep stored rumah_ibadah_id/jarak_meter in sync with latest radius rules.
$upd = null;
try {
if ($hasRumahIbadahId && $hasJarak) {
$upd = $pdo->prepare("UPDATE pemukiman_miskin_points SET rumah_ibadah_id = :rid, jarak_meter = :jarak WHERE id = :id");
} elseif ($hasRumahIbadahId) {
$upd = $pdo->prepare("UPDATE pemukiman_miskin_points SET rumah_ibadah_id = :rid WHERE id = :id");
}
} catch (Exception $e) {
$upd = null;
}
foreach ($rows as &$p) {
$lat = isset($p['latitude']) ? (float)$p['latitude'] : null;
$lng = isset($p['longitude']) ? (float)$p['longitude'] : null;
if ($lat === null || $lng === null) continue;
$best = compute_nearest_ibadah($ibadah, $lat, $lng);
$rid = $best ? (int)$best['rumah_ibadah_id'] : null;
$jarak = $best ? (float)$best['jarak_meter'] : null;
$storedRid = isset($p['rumah_ibadah_id']) ? (int)$p['rumah_ibadah_id'] : null;
// overwrite output with authoritative computed coverage
$p['rumah_ibadah_id'] = $rid;
if ($hasJarak) $p['jarak_meter'] = $jarak;
if ($best && isset($best['row']) && is_array($best['row'])) {
$p['rumah_ibadah_nama'] = $best['row']['nama'] ?? null;
$p['rumah_ibadah_jenis'] = $best['row']['jenis'] ?? null;
$p['rumah_ibadah_kontak'] = $best['row']['kontak'] ?? null;
$p['rumah_ibadah_radius'] = $best['row']['radius_meter'] ?? null;
} else {
$p['rumah_ibadah_nama'] = null;
$p['rumah_ibadah_jenis'] = null;
$p['rumah_ibadah_kontak'] = null;
$p['rumah_ibadah_radius'] = null;
}
// best-effort DB sync
try {
if ($upd && ((string)$storedRid !== (string)$rid)) {
if ($hasRumahIbadahId && $hasJarak) {
$upd->execute([':rid' => $rid, ':jarak' => $jarak, ':id' => (int)$p['id']]);
} elseif ($hasRumahIbadahId) {
$upd->execute([':rid' => $rid, ':id' => (int)$p['id']]);
}
}
} catch (Exception $e) {
// ignore
}
}
unset($p);
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+96
View File
@@ -0,0 +1,96 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function normalizePointRows($rows) {
foreach ($rows as &$row) {
if ((!isset($row['latitude']) || $row['latitude'] === null) && isset($row['geom']) && is_string($row['geom'])) {
$geom = json_decode($row['geom'], true);
if (is_array($geom) && ($geom['type'] ?? '') === 'Point') {
$coords = $geom['coordinates'] ?? null;
if (is_array($coords) && count($coords) >= 2) {
// GeoJSON: [lng, lat]
$row['longitude'] = $coords[0];
$row['latitude'] = $coords[1];
}
}
}
}
return $rows;
}
try {
// Preferred schema: has latitude/longitude columns.
$stmt = $pdo->query("
SELECT id, nama, no, deskripsi, status_24jam, latitude, longitude
FROM spbu_points
ORDER BY id ASC
");
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Exception $e) {
// continue
}
try {
// Fallback schema: latitude/longitude exists, geom stored as LONGTEXT GeoJSON.
$stmt = $pdo->query("
SELECT id, nama, no, deskripsi, status_24jam, latitude, longitude, geom
FROM spbu_points
ORDER BY id ASC
");
$rows = $stmt->fetchAll();
$rows = normalizePointRows($rows);
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Exception $e) {
// continue
}
try {
// Fallback schema: only geom LONGTEXT; derive latitude/longitude from GeoJSON.
$stmt = $pdo->query("
SELECT
id,
nama,
NULL AS no,
NULL AS deskripsi,
0 AS status_24jam,
geom
FROM spbu_points
ORDER BY id ASC
");
$rows = $stmt->fetchAll();
$rows = normalizePointRows($rows);
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Exception $e) {
// continue
}
try {
// Fallback schema: spatial geom
$stmt = $pdo->query("
SELECT
id,
nama,
NULL AS no,
NULL AS deskripsi,
0 AS status_24jam,
ST_Y(geom) AS latitude,
ST_X(geom) AS longitude
FROM spbu_points
ORDER BY id ASC
");
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Exception $e) {
// continue
}
// Last resort
$stmt = $pdo->query("SELECT * FROM spbu_points ORDER BY id ASC");
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+17
View File
@@ -0,0 +1,17 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
// Include jumlah penduduk binaan per rumah ibadah
$stmt = $pdo->query("
SELECT r.*,
(
SELECT COUNT(*)
FROM pemukiman_miskin_points p
WHERE p.rumah_ibadah_id = r.id
) AS binaan_count
FROM rumah_ibadah_points r
ORDER BY r.id ASC
");
$rows = $stmt->fetchAll();
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+42
View File
@@ -0,0 +1,42 @@
<?php
header('Content-Type: application/json; charset=utf-8');
$lat = isset($_GET['lat']) ? (float)$_GET['lat'] : null;
$lng = isset($_GET['lng']) ? (float)$_GET['lng'] : null;
if ($lat === null || $lng === null) {
echo json_encode(["success" => false, "message" => "lat/lng wajib diisi"]);
exit;
}
// Nominatim usage policy requires a valid User-Agent identifying the application.
$url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=" . urlencode($lat) . "&lon=" . urlencode($lng);
try {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 8);
curl_setopt($ch, CURLOPT_TIMEOUT, 12);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"User-Agent: webgis-spbu/1.0 (XAMPP PHP reverse geocode)"
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($resp === false || $code < 200 || $code >= 300) {
echo json_encode(["success" => false, "message" => $err ?: ("HTTP " . $code)]);
exit;
}
$json = json_decode($resp, true);
$display = $json['display_name'] ?? null;
echo json_encode([
"success" => true,
"address" => $display,
"raw" => $json
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+63
View File
@@ -0,0 +1,63 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Data kosong"]);
exit;
}
$id = (int)($data['id'] ?? 0);
$nama_jalan = trim($data['nama_jalan'] ?? '');
$status_jalan = trim($data['status_jalan'] ?? '');
$panjang_meter = (float)($data['panjang_meter'] ?? 0);
$geometry = $data['geometry'] ?? null;
if ($id <= 0 || $nama_jalan === '' || $status_jalan === '' || !$geometry) {
echo json_encode(["success" => false, "message" => "Data update jalan belum lengkap"]);
exit;
}
$sql = "UPDATE jalan_lines
SET nama_jalan = :nama_jalan,
status_jalan = :status_jalan,
panjang_meter = :panjang_meter,
geojson = :geojson
WHERE id = :id";
$geojson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare($sql);
$stmt->execute([
':nama_jalan' => $nama_jalan,
':status_jalan' => $status_jalan,
':panjang_meter' => $panjang_meter,
':geojson' => $geojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data jalan berhasil diupdate"]);
exit;
} catch (Exception $inner) {
// fallback to spatial geom schema
}
$stmt = $pdo->prepare("UPDATE jalan_lines
SET nama_jalan = :nama_jalan,
status_jalan = :status_jalan,
panjang_meter = :panjang_meter,
geom = ST_GeomFromGeoJSON(:geojson)
WHERE id = :id");
$stmt->execute([
':nama_jalan' => $nama_jalan,
':status_jalan' => $status_jalan,
':panjang_meter' => $panjang_meter,
':geojson' => $geojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data jalan berhasil diupdate"]);
+58
View File
@@ -0,0 +1,58 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Data kosong"]);
exit;
}
$id = (int)($data['id'] ?? 0);
$status_kepemilikan = trim($data['status_kepemilikan'] ?? '');
$luas_m2 = (float)($data['luas_m2'] ?? 0);
$geometry = $data['geometry'] ?? null;
if ($id <= 0 || $status_kepemilikan === '' || !$geometry) {
echo json_encode(["success" => false, "message" => "Data update parsil belum lengkap"]);
exit;
}
$sql = "UPDATE parsil_tanah
SET status_kepemilikan = :status_kepemilikan,
luas_m2 = :luas_m2,
geojson = :geojson
WHERE id = :id";
$geojson = json_encode($geometry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare($sql);
$stmt->execute([
':status_kepemilikan' => $status_kepemilikan,
':luas_m2' => $luas_m2,
':geojson' => $geojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data parsil berhasil diupdate"]);
exit;
} catch (Exception $inner) {
// fallback to spatial geom schema
}
$stmt = $pdo->prepare("UPDATE parsil_tanah
SET status_kepemilikan = :status_kepemilikan,
luas_m2 = :luas_m2,
geom = ST_GeomFromGeoJSON(:geojson)
WHERE id = :id");
$stmt->execute([
':status_kepemilikan' => $status_kepemilikan,
':luas_m2' => $luas_m2,
':geojson' => $geojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data parsil berhasil diupdate"]);
+213
View File
@@ -0,0 +1,213 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function haversine_m($lat1, $lon1, $lat2, $lon2) {
$R = 6371000.0;
$phi1 = deg2rad((float)$lat1);
$phi2 = deg2rad((float)$lat2);
$dphi = deg2rad((float)$lat2 - (float)$lat1);
$dlambda = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dphi/2) * sin($dphi/2) + cos($phi1) * cos($phi2) * sin($dlambda/2) * sin($dlambda/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $R * $c;
}
function get_payload() {
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
if (stripos($ct, 'multipart/form-data') !== false) {
return [
'id' => $_POST['id'] ?? 0,
'kk_nama' => $_POST['kk_nama'] ?? '',
'nik' => $_POST['nik'] ?? '',
'jumlah_anggota' => $_POST['jumlah_anggota'] ?? '',
'latitude' => $_POST['latitude'] ?? null,
'longitude' => $_POST['longitude'] ?? null,
'address' => $_POST['address'] ?? '',
'kelurahan' => $_POST['kelurahan'] ?? '',
'kecamatan' => $_POST['kecamatan'] ?? '',
'status_bantuan' => $_POST['status_bantuan'] ?? 'Belum dibantu',
'jenis_bantuan' => $_POST['jenis_bantuan'] ?? '',
'tanggal_bantuan' => $_POST['tanggal_bantuan'] ?? '',
'anggota' => $_POST['anggota'] ?? '[]'
];
}
$json = json_decode(file_get_contents('php://input'), true);
return is_array($json) ? $json : null;
}
function compute_nearest_ibadah(PDO $pdo, $lat, $lng) {
$stmt = $pdo->query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points");
$rows = $stmt->fetchAll();
$best = null;
foreach ($rows as $r) {
$rLat = (float)$r['latitude'];
$rLng = (float)$r['longitude'];
$radius = (float)$r['radius_meter'];
if ($radius <= 0) continue;
$d = haversine_m($lat, $lng, $rLat, $rLng);
if ($d <= $radius) {
if ($best === null || $d < $best['jarak_meter']) {
$best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d];
}
}
}
return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null];
}
function save_upload_if_any() {
if (!isset($_FILES['bukti_file']) || !is_array($_FILES['bukti_file'])) return null;
if (($_FILES['bukti_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) return null;
$dir = __DIR__ . '/../uploads/bukti';
if (!is_dir($dir)) @mkdir($dir, 0775, true);
if (!is_dir($dir)) return null;
$orig = (string)($_FILES['bukti_file']['name'] ?? 'file');
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp','pdf'];
if ($ext && !in_array($ext, $allowed, true)) return null;
$safeBase = preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($orig, PATHINFO_FILENAME));
$safeBase = trim($safeBase, '_');
if ($safeBase === '') $safeBase = 'bukti';
$filename = $safeBase . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . ($ext ? ('.' . $ext) : '');
$dest = $dir . '/' . $filename;
if (!move_uploaded_file($_FILES['bukti_file']['tmp_name'], $dest)) return null;
return 'uploads/bukti/' . $filename;
}
$data = get_payload();
if (!$data) {
echo json_encode(["success" => false, "message" => "Payload kosong atau tidak valid."]);
exit;
}
$id = (int)($data['id'] ?? 0);
$kk_nama = trim($data['kk_nama'] ?? $data['nama'] ?? '');
$nik = trim($data['nik'] ?? '');
$jumlah_anggota = (int)($data['jumlah_anggota'] ?? 0);
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$address = trim($data['address'] ?? '');
$kelurahan = trim($data['kelurahan'] ?? '');
$kecamatan = trim($data['kecamatan'] ?? '');
$status_bantuan = trim($data['status_bantuan'] ?? 'Belum dibantu');
$jenis_bantuan = trim($data['jenis_bantuan'] ?? '');
$tanggal_bantuan = trim($data['tanggal_bantuan'] ?? '');
$anggotaRaw = $data['anggota'] ?? [];
$validStatus = ['Belum dibantu', 'Sudah dibantu', 'Menunggu verifikasi'];
if (!in_array($status_bantuan, $validStatus, true)) $status_bantuan = 'Belum dibantu';
if ($id <= 0 || $kk_nama === '' || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data update penduduk miskin belum lengkap."]);
exit;
}
try {
$lat = (float)$latitude;
$lng = (float)$longitude;
$nearest = compute_nearest_ibadah($pdo, $lat, $lng);
$uploadPath = save_upload_if_any();
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
$pdo->beginTransaction();
if ($hasNew) {
if ($uploadPath === null) {
$stmt = $pdo->prepare("SELECT bukti_file FROM pemukiman_miskin_points WHERE id = :id");
$stmt->execute([':id' => $id]);
$uploadPath = $stmt->fetchColumn() ?: null;
}
$stmt = $pdo->prepare("
UPDATE pemukiman_miskin_points
SET kk_nama = :kk_nama,
nik = :nik,
jumlah_anggota = :jumlah_anggota,
latitude = :latitude,
longitude = :longitude,
address = :address,
kelurahan = :kelurahan,
kecamatan = :kecamatan,
status_bantuan = :status_bantuan,
jenis_bantuan = :jenis_bantuan,
tanggal_bantuan = :tanggal_bantuan,
bukti_file = :bukti_file,
jarak_meter = :jarak_meter,
rumah_ibadah_id = :rumah_ibadah_id
WHERE id = :id
");
$stmt->execute([
':kk_nama' => $kk_nama,
':nik' => ($nik === '' ? null : $nik),
':jumlah_anggota' => $jumlah_anggota,
':latitude' => $lat,
':longitude' => $lng,
':address' => ($address === '' ? null : $address),
':kelurahan' => ($kelurahan === '' ? null : $kelurahan),
':kecamatan' => ($kecamatan === '' ? null : $kecamatan),
':status_bantuan' => $status_bantuan,
':jenis_bantuan' => ($jenis_bantuan === '' ? null : $jenis_bantuan),
':tanggal_bantuan' => ($tanggal_bantuan === '' ? null : $tanggal_bantuan),
':bukti_file' => $uploadPath,
':jarak_meter' => $nearest['jarak_meter'],
':rumah_ibadah_id' => $nearest['rumah_ibadah_id'],
':id' => $id
]);
// anggota keluarga (replace)
$pdo->prepare("DELETE FROM anggota_keluarga WHERE penduduk_id = :id")->execute([':id' => $id]);
$anggota = is_string($anggotaRaw) ? json_decode($anggotaRaw, true) : $anggotaRaw;
if (is_array($anggota) && count($anggota) > 0) {
$ins = $pdo->prepare("
INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan, keterangan)
VALUES (:penduduk_id, :nama, :hubungan, :umur, :pekerjaan, :keterangan)
");
foreach ($anggota as $a) {
$n = trim((string)($a['nama'] ?? ''));
$h = trim((string)($a['hubungan'] ?? ''));
if ($n === '' || $h === '') continue;
$ins->execute([
':penduduk_id' => $id,
':nama' => $n,
':hubungan' => $h,
':umur' => isset($a['umur']) && $a['umur'] !== '' ? (int)$a['umur'] : null,
':pekerjaan' => ($a['pekerjaan'] ?? '') === '' ? null : (string)$a['pekerjaan'],
':keterangan' => ($a['keterangan'] ?? '') === '' ? null : (string)$a['keterangan']
]);
}
}
} else {
$stmt = $pdo->prepare("
UPDATE pemukiman_miskin_points
SET nama = :nama,
latitude = :latitude,
longitude = :longitude,
address = :address,
rumah_ibadah_id = :rumah_ibadah_id
WHERE id = :id
");
$stmt->execute([
':nama' => $kk_nama,
':latitude' => $lat,
':longitude' => $lng,
':address' => ($address === '' ? null : $address),
':rumah_ibadah_id' => $nearest['rumah_ibadah_id'],
':id' => $id
]);
}
$pdo->commit();
echo json_encode([
"success" => true,
"rumah_ibadah_id" => $nearest['rumah_ibadah_id'],
"jarak_meter" => $nearest['jarak_meter'],
"message" => "Penduduk miskin berhasil diupdate"
]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
@@ -0,0 +1,95 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
function haversine_m($lat1, $lon1, $lat2, $lon2) {
$R = 6371000.0;
$phi1 = deg2rad((float)$lat1);
$phi2 = deg2rad((float)$lat2);
$dphi = deg2rad((float)$lat2 - (float)$lat1);
$dlambda = deg2rad((float)$lon2 - (float)$lon1);
$a = sin($dphi/2) * sin($dphi/2) + cos($phi1) * cos($phi2) * sin($dlambda/2) * sin($dlambda/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $R * $c;
}
function compute_nearest_ibadah(PDO $pdo, $lat, $lng) {
$stmt = $pdo->query("SELECT id, latitude, longitude, radius_meter FROM rumah_ibadah_points");
$rows = $stmt->fetchAll();
$best = null;
foreach ($rows as $r) {
$rLat = (float)$r['latitude'];
$rLng = (float)$r['longitude'];
$radius = (float)$r['radius_meter'];
if ($radius <= 0) continue;
$d = haversine_m($lat, $lng, $rLat, $rLng);
if ($d <= $radius) {
if ($best === null || $d < $best['jarak_meter']) {
$best = ['rumah_ibadah_id' => (int)$r['id'], 'jarak_meter' => $d];
}
}
}
return $best ?: ['rumah_ibadah_id' => null, 'jarak_meter' => null];
}
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
echo json_encode(["success" => false, "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$id = (int)($data['id'] ?? 0);
$lat = isset($data['latitude']) ? (float)$data['latitude'] : null;
$lng = isset($data['longitude']) ? (float)$data['longitude'] : null;
if ($id <= 0 || $lat === null || $lng === null) {
echo json_encode(["success" => false, "message" => "Data update lokasi belum lengkap (id/lat/lng)."]);
exit;
}
try {
$nearest = compute_nearest_ibadah($pdo, $lat, $lng);
$cols = $pdo->query("SHOW COLUMNS FROM pemukiman_miskin_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasNew = in_array('kk_nama', $cols, true);
if ($hasNew) {
$stmt = $pdo->prepare("
UPDATE pemukiman_miskin_points
SET latitude = :lat,
longitude = :lng,
jarak_meter = :jarak_meter,
rumah_ibadah_id = :rumah_ibadah_id
WHERE id = :id
");
$stmt->execute([
':lat' => $lat,
':lng' => $lng,
':jarak_meter' => $nearest['jarak_meter'],
':rumah_ibadah_id' => $nearest['rumah_ibadah_id'],
':id' => $id
]);
} else {
$stmt = $pdo->prepare("
UPDATE pemukiman_miskin_points
SET latitude = :lat,
longitude = :lng,
rumah_ibadah_id = :rumah_ibadah_id
WHERE id = :id
");
$stmt->execute([
':lat' => $lat,
':lng' => $lng,
':rumah_ibadah_id' => $nearest['rumah_ibadah_id'],
':id' => $id
]);
}
echo json_encode([
"success" => true,
"rumah_ibadah_id" => $nearest['rumah_ibadah_id'],
"jarak_meter" => $nearest['jarak_meter'],
"message" => "Lokasi penduduk miskin berhasil diupdate"
]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+115
View File
@@ -0,0 +1,115 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Data kosong"]);
exit;
}
$id = (int)($data['id'] ?? 0);
$nama = trim($data['nama'] ?? '');
$no = trim($data['no'] ?? '');
$deskripsi = trim($data['deskripsi'] ?? '');
$status_24jam = isset($data['status_24jam']) ? (int)$data['status_24jam'] : 0;
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
if ($id <= 0 || $nama === '' || $no === '' || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data update point belum lengkap"]);
exit;
}
$sql = "UPDATE spbu_points
SET nama = :nama,
no = :no,
deskripsi = :deskripsi,
status_24jam = :status_24jam,
latitude = :latitude,
longitude = :longitude
WHERE id = :id";
$pointGeojson = json_encode([
"type" => "Point",
"coordinates" => [(float)$longitude, (float)$latitude]
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
try {
$stmt = $pdo->prepare($sql);
$stmt->execute([
':nama' => $nama,
':no' => $no,
':deskripsi' => $deskripsi,
':status_24jam' => $status_24jam,
':latitude' => $latitude,
':longitude' => $longitude,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]);
exit;
} catch (Exception $inner) {
// continue fallback
}
// Fallback schema: geom LONGTEXT
try {
$stmt = $pdo->prepare("UPDATE spbu_points
SET nama = :nama,
no = :no,
deskripsi = :deskripsi,
status_24jam = :status_24jam,
latitude = :latitude,
longitude = :longitude,
geom = :geom
WHERE id = :id");
$stmt->execute([
':nama' => $nama,
':no' => $no,
':deskripsi' => $deskripsi,
':status_24jam' => $status_24jam,
':latitude' => $latitude,
':longitude' => $longitude,
':geom' => $pointGeojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]);
exit;
} catch (Exception $inner2) {
// continue fallback
}
// Fallback schema: only (nama, geom) where geom is LONGTEXT GeoJSON
try {
$stmt = $pdo->prepare("UPDATE spbu_points
SET nama = :nama,
geom = :geom
WHERE id = :id");
$stmt->execute([
':nama' => $nama,
':geom' => $pointGeojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]);
exit;
} catch (Exception $inner3) {
// continue fallback
}
// Fallback schema: spatial geom
$stmt = $pdo->prepare("UPDATE spbu_points
SET nama = :nama,
geom = ST_GeomFromGeoJSON(:geom)
WHERE id = :id");
$stmt->execute([
':nama' => $nama,
':geom' => $pointGeojson,
':id' => $id
]);
echo json_encode(["success" => true, "message" => "Data point berhasil diupdate"]);
+75
View File
@@ -0,0 +1,75 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'koneksi.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(["success" => false, "message" => "Payload JSON kosong atau tidak valid."]);
exit;
}
$id = (int)($data['id'] ?? 0);
$nama = trim($data['nama'] ?? '');
$jenis = trim($data['jenis'] ?? 'Lainnya');
$kontak = trim($data['kontak'] ?? '');
$radius_meter = (float)($data['radius_meter'] ?? 0);
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$address = trim($data['address'] ?? '');
if ($id <= 0 || $nama === '' || $radius_meter <= 0 || $latitude === null || $longitude === null) {
echo json_encode(["success" => false, "message" => "Data update rumah ibadah belum lengkap."]);
exit;
}
try {
$cols = $pdo->query("SHOW COLUMNS FROM rumah_ibadah_points")->fetchAll(PDO::FETCH_COLUMN, 0);
$hasJenis = in_array('jenis', $cols, true);
$hasKontak = in_array('kontak', $cols, true);
if ($hasJenis && $hasKontak) {
$stmt = $pdo->prepare("
UPDATE rumah_ibadah_points
SET nama = :nama,
jenis = :jenis,
kontak = :kontak,
radius_meter = :radius_meter,
latitude = :latitude,
longitude = :longitude,
address = :address
WHERE id = :id
");
$stmt->execute([
':nama' => $nama,
':jenis' => ($jenis === '' ? 'Lainnya' : $jenis),
':kontak' => ($kontak === '' ? null : $kontak),
':radius_meter' => $radius_meter,
':latitude' => $latitude,
':longitude' => $longitude,
':address' => ($address === '' ? null : $address),
':id' => $id
]);
} else {
$stmt = $pdo->prepare("
UPDATE rumah_ibadah_points
SET nama = :nama,
radius_meter = :radius_meter,
latitude = :latitude,
longitude = :longitude,
address = :address
WHERE id = :id
");
$stmt->execute([
':nama' => $nama,
':radius_meter' => $radius_meter,
':latitude' => $latitude,
':longitude' => $longitude,
':address' => ($address === '' ? null : $address),
':id' => $id
]);
}
echo json_encode(["success" => true, "message" => "Rumah ibadah berhasil diupdate"]);
} catch (Exception $e) {
echo json_encode(["success" => false, "message" => $e->getMessage()]);
}
+466
View File
@@ -0,0 +1,466 @@
-- Schema WebGIS SPBU + Jalan + Parsil (MySQL / MariaDB)
-- Catatan:
-- - Data LINE/POLYGON disimpan sebagai GeoJSON geometry (tipe: geojson) di kolom LONGTEXT `geojson`.
-- - Panjang (meter) dan luas (m2) dihitung otomatis di LeafletJS, bukan input manual.
CREATE TABLE IF NOT EXISTS spbu_points (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150) NOT NULL,
no VARCHAR(60) NOT NULL,
deskripsi TEXT NULL,
status_24jam TINYINT(1) NOT NULL DEFAULT 0,
latitude DECIMAL(10,7) NOT NULL,
longitude DECIMAL(10,7) NOT NULL,
-- opsional (kompatibilitas): simpan GeoJSON Point sebagai string
geom LONGTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_spbu_status24 (status_24jam),
INDEX idx_spbu_latlng (latitude, longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS jalan_lines (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(200) NOT NULL,
status_jalan VARCHAR(20) NOT NULL, -- Nasional | Provinsi | Kabupaten
panjang_meter DOUBLE NOT NULL DEFAULT 0,
geojson LONGTEXT NOT NULL, -- GeoJSON geometry: { "type":"LineString", "coordinates":[...] }
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_jalan_status (status_jalan)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS parsil_tanah (
id INT AUTO_INCREMENT PRIMARY KEY,
status_kepemilikan VARCHAR(10) NOT NULL, -- SHM | HGB | HGU | HP
luas_m2 DOUBLE NOT NULL DEFAULT 0,
geojson LONGTEXT NOT NULL, -- GeoJSON geometry: { "type":"Polygon", "coordinates":[...] }
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_parsil_status (status_kepemilikan)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS rumah_ibadah_points (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya', -- Masjid|Musholla|Gereja|Katolik|Pura|Vihara|Klenteng|Lainnya
kontak VARCHAR(120) NULL,
radius_meter DOUBLE NOT NULL DEFAULT 300,
latitude DECIMAL(10,7) NOT NULL,
longitude DECIMAL(10,7) NOT NULL,
address TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ibadah_jenis (jenis),
INDEX idx_ibadah_latlng (latitude, longitude)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pemukiman_miskin_points (
id INT AUTO_INCREMENT PRIMARY KEY,
kk_nama VARCHAR(200) NOT NULL,
nik VARCHAR(32) NULL,
jumlah_anggota INT NOT NULL DEFAULT 0,
latitude DECIMAL(10,7) NOT NULL,
longitude DECIMAL(10,7) NOT NULL,
address TEXT NULL,
kelurahan VARCHAR(120) NULL,
kecamatan VARCHAR(120) NULL,
status_bantuan VARCHAR(24) NOT NULL DEFAULT 'Belum dibantu', -- Belum dibantu|Sudah dibantu|Menunggu verifikasi
jenis_bantuan VARCHAR(120) NULL,
tanggal_bantuan DATE NULL,
bukti_file VARCHAR(255) NULL,
jarak_meter DOUBLE NULL,
-- id rumah ibadah yang menaungi (hasil pencarian radius terdekat)
rumah_ibadah_id INT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_miskin_latlng (latitude, longitude),
INDEX idx_miskin_ibadah (rumah_ibadah_id),
INDEX idx_miskin_status (status_bantuan),
CONSTRAINT fk_miskin_ibadah FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah_points(id)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS anggota_keluarga (
id INT AUTO_INCREMENT PRIMARY KEY,
penduduk_id INT NOT NULL,
nama VARCHAR(200) NOT NULL,
hubungan VARCHAR(60) NOT NULL,
umur INT NULL,
pekerjaan VARCHAR(120) NULL,
keterangan VARCHAR(255) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_anggota_penduduk (penduduk_id),
CONSTRAINT fk_anggota_penduduk FOREIGN KEY (penduduk_id) REFERENCES pemukiman_miskin_points(id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- (Opsional) User login untuk admin/operator.
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
username VARCHAR(60) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'operator', -- admin|operator
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Jika ingin strict value, bisa pakai ENUM, misal:
-- status_jalan ENUM('Nasional','Provinsi','Kabupaten')
-- status_kepemilikan ENUM('SHM','HGB','HGU','HP')
-- ==========================================================
-- MIGRATION (ALTER) UNTUK DATABASE LAMA (TANPA HILANGKAN DATA)
-- ==========================================================
-- Kapan perlu menjalankan bagian ini?
-- - Jika database Anda dibuat dari project lama (kolom/tipe berbeda),
-- misalnya masih pakai kolom spatial `geom` atau belum punya kolom
-- `latitude/longitude/status_24jam/geojson`.
-- - Jika database Anda sudah dibuat pakai schema di atas (CREATE TABLE),
-- Anda biasanya TIDAK perlu ALTER apa-apa.
--
-- Cara pakai:
-- - Jalankan file ini di phpMyAdmin / MySQL client.
-- - Bagian CREATE TABLE aman (IF NOT EXISTS).
-- - Bagian MIGRATION ini aman (cek INFORMATION_SCHEMA sebelum ALTER).
DELIMITER $$
DROP PROCEDURE IF EXISTS migrate_webgis_spbu $$
CREATE PROCEDURE migrate_webgis_spbu()
BEGIN
-- ----------------------
-- spbu_points
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'spbu_points'
) THEN
-- Kolom atribut inti
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'no'
) THEN
ALTER TABLE spbu_points ADD COLUMN no VARCHAR(60) NULL AFTER nama;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'deskripsi'
) THEN
ALTER TABLE spbu_points ADD COLUMN deskripsi TEXT NULL AFTER no;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'status_24jam'
) THEN
ALTER TABLE spbu_points ADD COLUMN status_24jam TINYINT(1) NOT NULL DEFAULT 0 AFTER deskripsi;
END IF;
-- Lat/Lng: untuk menjaga data lama, dibuat NULL-able dulu.
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'latitude'
) THEN
ALTER TABLE spbu_points ADD COLUMN latitude DECIMAL(10,7) NULL AFTER status_24jam;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'longitude'
) THEN
ALTER TABLE spbu_points ADD COLUMN longitude DECIMAL(10,7) NULL AFTER latitude;
END IF;
-- Kolom geom (opsional) untuk kompatibilitas legacy
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'geom'
) THEN
ALTER TABLE spbu_points ADD COLUMN geom LONGTEXT NULL AFTER longitude;
END IF;
-- Index (jika belum ada)
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND index_name = 'idx_spbu_status24'
) THEN
ALTER TABLE spbu_points ADD INDEX idx_spbu_status24 (status_24jam);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND index_name = 'idx_spbu_latlng'
) THEN
ALTER TABLE spbu_points ADD INDEX idx_spbu_latlng (latitude, longitude);
END IF;
-- Backfill latitude/longitude dari geom GeoJSON (jika geom berisi JSON valid)
-- Catatan: ini hanya mengisi yang masih NULL supaya tidak menimpa data baru.
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'spbu_points' AND column_name = 'geom'
) THEN
-- Optional: JSON_VALID/JSON_EXTRACT bisa tidak tersedia pada versi tertentu.
-- Best-effort: jika fungsi tidak didukung, blok ini di-skip tanpa menghentikan migrasi.
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
UPDATE spbu_points
SET
longitude = CAST(JSON_UNQUOTE(JSON_EXTRACT(geom, '$.coordinates[0]')) AS DECIMAL(10,7)),
latitude = CAST(JSON_UNQUOTE(JSON_EXTRACT(geom, '$.coordinates[1]')) AS DECIMAL(10,7))
WHERE (latitude IS NULL OR longitude IS NULL)
AND geom IS NOT NULL
AND JSON_VALID(geom) = 1
AND JSON_UNQUOTE(JSON_EXTRACT(geom, '$.type')) = 'Point';
END;
END IF;
END IF;
-- ----------------------
-- jalan_lines
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines'
) THEN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'panjang_meter'
) THEN
ALTER TABLE jalan_lines ADD COLUMN panjang_meter DOUBLE NOT NULL DEFAULT 0 AFTER status_jalan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'geojson'
) THEN
ALTER TABLE jalan_lines ADD COLUMN geojson LONGTEXT NULL AFTER panjang_meter;
END IF;
-- Backfill geojson dari kolom spatial geom (jika ada)
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND column_name = 'geom'
) THEN
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
UPDATE jalan_lines
SET geojson = ST_AsGeoJSON(geom)
WHERE (geojson IS NULL OR geojson = '') AND geom IS NOT NULL;
END;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'jalan_lines' AND index_name = 'idx_jalan_status'
) THEN
ALTER TABLE jalan_lines ADD INDEX idx_jalan_status (status_jalan);
END IF;
END IF;
-- ----------------------
-- parsil_tanah
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah'
) THEN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'luas_m2'
) THEN
ALTER TABLE parsil_tanah ADD COLUMN luas_m2 DOUBLE NOT NULL DEFAULT 0 AFTER status_kepemilikan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'geojson'
) THEN
ALTER TABLE parsil_tanah ADD COLUMN geojson LONGTEXT NULL AFTER luas_m2;
END IF;
-- Backfill geojson dari kolom spatial geom (jika ada)
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND column_name = 'geom'
) THEN
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
UPDATE parsil_tanah
SET geojson = ST_AsGeoJSON(geom)
WHERE (geojson IS NULL OR geojson = '') AND geom IS NOT NULL;
END;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'parsil_tanah' AND index_name = 'idx_parsil_status'
) THEN
ALTER TABLE parsil_tanah ADD INDEX idx_parsil_status (status_kepemilikan);
END IF;
END IF;
-- ----------------------
-- rumah_ibadah_points
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points'
) THEN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'jenis'
) THEN
ALTER TABLE rumah_ibadah_points ADD COLUMN jenis VARCHAR(30) NOT NULL DEFAULT 'Lainnya' AFTER nama;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'kontak'
) THEN
ALTER TABLE rumah_ibadah_points ADD COLUMN kontak VARCHAR(120) NULL AFTER jenis;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'radius_meter'
) THEN
ALTER TABLE rumah_ibadah_points ADD COLUMN radius_meter DOUBLE NOT NULL DEFAULT 300 AFTER kontak;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND column_name = 'address'
) THEN
ALTER TABLE rumah_ibadah_points ADD COLUMN address TEXT NULL AFTER longitude;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'rumah_ibadah_points' AND index_name = 'idx_ibadah_latlng'
) THEN
ALTER TABLE rumah_ibadah_points ADD INDEX idx_ibadah_latlng (latitude, longitude);
END IF;
END IF;
-- ----------------------
-- pemukiman_miskin_points
-- ----------------------
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points'
) THEN
-- Backward compat: kalau masih pakai kolom `nama`, migrasikan ke `kk_nama`
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kk_nama'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN kk_nama VARCHAR(200) NULL AFTER id;
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
UPDATE pemukiman_miskin_points
SET kk_nama = COALESCE(NULLIF(kk_nama, ''), NULLIF(nama, ''))
WHERE (kk_nama IS NULL OR kk_nama = '');
END;
-- jadikan NOT NULL kalau sudah terisi
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END;
ALTER TABLE pemukiman_miskin_points MODIFY kk_nama VARCHAR(200) NOT NULL;
END;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'nik'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN nik VARCHAR(32) NULL AFTER kk_nama;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jumlah_anggota'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN jumlah_anggota INT NOT NULL DEFAULT 0 AFTER nik;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kelurahan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN kelurahan VARCHAR(120) NULL AFTER address;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'kecamatan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN kecamatan VARCHAR(120) NULL AFTER kelurahan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'status_bantuan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN status_bantuan VARCHAR(24) NOT NULL DEFAULT 'Belum dibantu' AFTER kecamatan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jenis_bantuan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN jenis_bantuan VARCHAR(120) NULL AFTER status_bantuan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'tanggal_bantuan'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN tanggal_bantuan DATE NULL AFTER jenis_bantuan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'bukti_file'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN bukti_file VARCHAR(255) NULL AFTER tanggal_bantuan;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'jarak_meter'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN jarak_meter DOUBLE NULL AFTER bukti_file;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND column_name = 'rumah_ibadah_id'
) THEN
ALTER TABLE pemukiman_miskin_points ADD COLUMN rumah_ibadah_id INT NULL AFTER jarak_meter;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND index_name = 'idx_miskin_ibadah'
) THEN
ALTER TABLE pemukiman_miskin_points ADD INDEX idx_miskin_ibadah (rumah_ibadah_id);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'pemukiman_miskin_points' AND index_name = 'idx_miskin_latlng'
) THEN
ALTER TABLE pemukiman_miskin_points ADD INDEX idx_miskin_latlng (latitude, longitude);
END IF;
END IF;
END $$
-- Jalankan migrasi (aman dijalankan berulang)
CALL migrate_webgis_spbu() $$
DROP PROCEDURE IF EXISTS migrate_webgis_spbu $$
DELIMITER ;
+1
View File
@@ -0,0 +1 @@