Initial commit
This commit is contained in:
@@ -0,0 +1,935 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================
|
||||||
|
// api.php — REST API WebGIS Rumah Ibadah
|
||||||
|
// Endpoint: /api.php?resource=rumah_ibadah&action=...
|
||||||
|
// /api.php?resource=lokasi_bantuan&action=...
|
||||||
|
// /api.php?resource=sumbangan&action=...
|
||||||
|
// /api.php?resource=penyaluran&action=...
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
require_once 'koneksi.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
// Preflight CORS
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$resource = $_GET['resource'] ?? '';
|
||||||
|
$action = $_GET['action'] ?? 'list';
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ROUTER
|
||||||
|
// ============================================================
|
||||||
|
try {
|
||||||
|
switch ($resource) {
|
||||||
|
case 'rumah_ibadah':
|
||||||
|
handleRumahIbadah($method, $action, $id, $body);
|
||||||
|
break;
|
||||||
|
case 'lokasi_bantuan':
|
||||||
|
handleLokasiBantuan($method, $action, $id, $body);
|
||||||
|
break;
|
||||||
|
case 'sumbangan':
|
||||||
|
handleSumbangan($method, $action, $id, $body);
|
||||||
|
break;
|
||||||
|
case 'penyaluran':
|
||||||
|
handlePenyaluran($method, $action, $id, $body);
|
||||||
|
break;
|
||||||
|
case 'spbu':
|
||||||
|
handleSpbu($method, $action, $id, $body);
|
||||||
|
break;
|
||||||
|
case 'jalan':
|
||||||
|
handleJalan($method, $action, $id, $body);
|
||||||
|
break;
|
||||||
|
case 'parsil':
|
||||||
|
handleParsil($method, $action, $id, $body);
|
||||||
|
break;
|
||||||
|
case 'setup':
|
||||||
|
handleSetup();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
jsonError('Resource tidak ditemukan.', 404);
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
jsonError('Database error: ' . $e->getMessage(), 500);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonError($e->getMessage(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// RUMAH IBADAH
|
||||||
|
// ============================================================
|
||||||
|
function handleRumahIbadah(string $method, string $action, ?int $id, array $body): void {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// GET LIST
|
||||||
|
if ($method === 'GET' && $action === 'list') {
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT id, nama, jenis, alamat, narahubung, latitude, longitude, radius_aktif,
|
||||||
|
created_at
|
||||||
|
FROM rumah_ibadah
|
||||||
|
ORDER BY nama ASC"
|
||||||
|
)->fetchAll();
|
||||||
|
jsonOk($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET DETAIL + jumlah penerima dalam radius
|
||||||
|
elseif ($method === 'GET' && $action === 'detail' && $id) {
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT id, nama, jenis, alamat, narahubung, latitude, longitude,
|
||||||
|
radius_aktif, created_at
|
||||||
|
FROM rumah_ibadah WHERE id = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$ri = $stmt->fetch();
|
||||||
|
if (!$ri) jsonError('Data tidak ditemukan.', 404);
|
||||||
|
|
||||||
|
// Hitung penerima dalam radius
|
||||||
|
$stmtCount = $db->prepare(
|
||||||
|
"SELECT COUNT(*) AS total
|
||||||
|
FROM lokasi_bantuan lb, rumah_ibadah ri
|
||||||
|
WHERE ri.id = ?
|
||||||
|
AND ST_Distance_Sphere(lb.koordinat, ri.koordinat) <= ri.radius_aktif"
|
||||||
|
);
|
||||||
|
$stmtCount->execute([$id]);
|
||||||
|
$ri['jumlah_dalam_radius'] = (int)$stmtCount->fetchColumn();
|
||||||
|
|
||||||
|
jsonOk($ri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST CREATE
|
||||||
|
elseif ($method === 'POST' && $action === 'create') {
|
||||||
|
$v = validasiRI($body);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"INSERT INTO rumah_ibadah
|
||||||
|
(nama, jenis, alamat, narahubung, latitude, longitude, koordinat, radius_aktif)
|
||||||
|
VALUES
|
||||||
|
(:nama, :jenis, :alamat, :narahubung, :lat, :lng,
|
||||||
|
ST_GeomFromText(:titik, 4326), :radius)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama' => $v['nama'],
|
||||||
|
':jenis' => $v['jenis'],
|
||||||
|
':alamat' => $v['alamat'],
|
||||||
|
':narahubung' => $v['narahubung'],
|
||||||
|
':lat' => $v['latitude'],
|
||||||
|
':lng' => $v['longitude'],
|
||||||
|
':titik' => "POINT({$v['latitude']} {$v['longitude']})",
|
||||||
|
':radius' => $v['radius_aktif'],
|
||||||
|
]);
|
||||||
|
$newId = (int)$db->lastInsertId();
|
||||||
|
jsonOk(['message' => 'Rumah ibadah berhasil ditambahkan.', 'id' => $newId], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT UPDATE
|
||||||
|
elseif ($method === 'PUT' && $action === 'update' && $id) {
|
||||||
|
$v = validasiRI($body);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE rumah_ibadah SET
|
||||||
|
nama = :nama, jenis = :jenis, alamat = :alamat,
|
||||||
|
narahubung = :narahubung, latitude = :lat, longitude = :lng,
|
||||||
|
koordinat = ST_GeomFromText(:titik, 4326), radius_aktif = :radius
|
||||||
|
WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama' => $v['nama'],
|
||||||
|
':jenis' => $v['jenis'],
|
||||||
|
':alamat' => $v['alamat'],
|
||||||
|
':narahubung' => $v['narahubung'],
|
||||||
|
':lat' => $v['latitude'],
|
||||||
|
':lng' => $v['longitude'],
|
||||||
|
':titik' => "POINT({$v['latitude']} {$v['longitude']})",
|
||||||
|
':radius' => $v['radius_aktif'],
|
||||||
|
':id' => $id,
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'Data rumah ibadah berhasil diperbarui.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
elseif ($method === 'DELETE' && $action === 'delete' && $id) {
|
||||||
|
$stmt = $db->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
jsonOk(['message' => 'Rumah ibadah berhasil dihapus.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
jsonError('Aksi tidak dikenali.', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validasiRI(array $b): array {
|
||||||
|
$nama = trim($b['nama'] ?? '');
|
||||||
|
$jenis = trim($b['jenis'] ?? '');
|
||||||
|
$alamat = trim($b['alamat'] ?? '');
|
||||||
|
$lat = isset($b['latitude']) ? (float)$b['latitude'] : null;
|
||||||
|
$lng = isset($b['longitude']) ? (float)$b['longitude'] : null;
|
||||||
|
$radius = isset($b['radius_aktif']) ? (int)$b['radius_aktif'] : 500;
|
||||||
|
|
||||||
|
$jenisList = ['Masjid','Gereja','Pura','Vihara','Kelenteng'];
|
||||||
|
|
||||||
|
if (!$nama) throw new Exception('Nama wajib diisi.');
|
||||||
|
if (!in_array($jenis, $jenisList)) throw new Exception('Jenis tidak valid.');
|
||||||
|
if (!$alamat) throw new Exception('Alamat wajib diisi.');
|
||||||
|
if ($lat === null || $lat < -90 || $lat > 90) throw new Exception('Latitude tidak valid.');
|
||||||
|
if ($lng === null || $lng < -180 || $lng > 180) throw new Exception('Longitude tidak valid.');
|
||||||
|
if ($radius < 0 || $radius > 1500) throw new Exception('Radius harus antara 0–1500 meter.');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nama' => $nama,
|
||||||
|
'jenis' => $jenis,
|
||||||
|
'alamat' => $alamat,
|
||||||
|
'narahubung' => trim($b['narahubung'] ?? ''),
|
||||||
|
'latitude' => $lat,
|
||||||
|
'longitude' => $lng,
|
||||||
|
'radius_aktif'=> $radius,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// LOKASI BANTUAN
|
||||||
|
// ============================================================
|
||||||
|
function handleLokasiBantuan(string $method, string $action, ?int $id, array $body): void {
|
||||||
|
$db = getDB();
|
||||||
|
$radius = isset($_GET['radius']) ? (int)$_GET['radius'] : 500;
|
||||||
|
$radius = max(0, min(1500, $radius));
|
||||||
|
|
||||||
|
// GET LIST + status warna
|
||||||
|
if ($method === 'GET' && $action === 'list') {
|
||||||
|
// Cek apakah ada rumah ibadah
|
||||||
|
$adaRI = (int)$db->query("SELECT COUNT(*) FROM rumah_ibadah")->fetchColumn();
|
||||||
|
|
||||||
|
if ($adaRI === 0) {
|
||||||
|
// Kembalikan list tanpa status jarak
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT id, nama_kk, jml_anggota, kategori, alamat, keterangan,
|
||||||
|
latitude, longitude, NULL AS jarak_meter,
|
||||||
|
NULL AS nama_ri, NULL AS id_ri, 'MERAH' AS status_warna
|
||||||
|
FROM lokasi_bantuan ORDER BY nama_kk ASC"
|
||||||
|
)->fetchAll();
|
||||||
|
jsonOk($rows);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung jarak ke rumah ibadah terdekat per titik
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT lb.id, lb.nama_kk, lb.jml_anggota, lb.kategori,
|
||||||
|
lb.alamat, lb.keterangan, lb.latitude, lb.longitude
|
||||||
|
FROM lokasi_bantuan lb ORDER BY lb.nama_kk ASC"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
$stmtJarak = $db->prepare(
|
||||||
|
"SELECT ri.id AS id_ri, ri.nama AS nama_ri, ri.radius_aktif,
|
||||||
|
ST_Distance_Sphere(lb.koordinat, ri.koordinat) AS jarak_meter
|
||||||
|
FROM lokasi_bantuan lb
|
||||||
|
CROSS JOIN rumah_ibadah ri
|
||||||
|
WHERE lb.id = ?
|
||||||
|
ORDER BY jarak_meter ASC
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
foreach ($rows as $lb) {
|
||||||
|
$stmtJarak->execute([$lb['id']]);
|
||||||
|
$terdekat = $stmtJarak->fetch();
|
||||||
|
$jarak = $terdekat ? (float)$terdekat['jarak_meter'] : null;
|
||||||
|
$lb['jarak_meter'] = $terdekat ? round($jarak, 1) : null;
|
||||||
|
$lb['nama_ri'] = $terdekat['nama_ri'] ?? null;
|
||||||
|
$lb['id_ri'] = $terdekat['id_ri'] ?? null;
|
||||||
|
$lb['radius_ri'] = $terdekat['radius_aktif'] ?? null;
|
||||||
|
// HIJAU = dalam jangkauan, MERAH = di luar jangkauan
|
||||||
|
$lb['status_warna'] = ($terdekat && $jarak <= $radius) ? 'HIJAU' : 'MERAH';
|
||||||
|
$result[] = $lb;
|
||||||
|
}
|
||||||
|
jsonOk($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET DETAIL
|
||||||
|
elseif ($method === 'GET' && $action === 'detail' && $id) {
|
||||||
|
$stmt = $db->prepare("SELECT * FROM lokasi_bantuan WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$lb = $stmt->fetch();
|
||||||
|
if (!$lb) jsonError('Data tidak ditemukan.', 404);
|
||||||
|
jsonOk($lb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST CREATE
|
||||||
|
elseif ($method === 'POST' && $action === 'create') {
|
||||||
|
$adaRI = (int)$db->query("SELECT COUNT(*) FROM rumah_ibadah")->fetchColumn();
|
||||||
|
if ($adaRI === 0) {
|
||||||
|
jsonError('Tambahkan minimal satu rumah ibadah terlebih dahulu.', 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$v = validasiLB($body);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"INSERT INTO lokasi_bantuan
|
||||||
|
(nama_kk, jml_anggota, kategori, alamat, keterangan, latitude, longitude, koordinat)
|
||||||
|
VALUES
|
||||||
|
(:nama_kk, :jml, :kategori, :alamat, :ket, :lat, :lng,
|
||||||
|
ST_GeomFromText(:titik, 4326))"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama_kk' => $v['nama_kk'],
|
||||||
|
':jml' => $v['jml_anggota'],
|
||||||
|
':kategori' => $v['kategori'],
|
||||||
|
':alamat' => $v['alamat'],
|
||||||
|
':ket' => $v['keterangan'],
|
||||||
|
':lat' => $v['latitude'],
|
||||||
|
':lng' => $v['longitude'],
|
||||||
|
':titik' => "POINT({$v['latitude']} {$v['longitude']})",
|
||||||
|
]);
|
||||||
|
$newId = (int)$db->lastInsertId();
|
||||||
|
jsonOk(['message' => 'Lokasi penerima bantuan berhasil ditambahkan.', 'id' => $newId], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT UPDATE
|
||||||
|
elseif ($method === 'PUT' && $action === 'update' && $id) {
|
||||||
|
$v = validasiLB($body);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE lokasi_bantuan SET
|
||||||
|
nama_kk = :nama_kk, jml_anggota = :jml, kategori = :kategori,
|
||||||
|
alamat = :alamat, keterangan = :ket,
|
||||||
|
latitude = :lat, longitude = :lng,
|
||||||
|
koordinat = ST_GeomFromText(:titik, 4326)
|
||||||
|
WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama_kk' => $v['nama_kk'],
|
||||||
|
':jml' => $v['jml_anggota'],
|
||||||
|
':kategori' => $v['kategori'],
|
||||||
|
':alamat' => $v['alamat'],
|
||||||
|
':ket' => $v['keterangan'],
|
||||||
|
':lat' => $v['latitude'],
|
||||||
|
':lng' => $v['longitude'],
|
||||||
|
':titik' => "POINT({$v['latitude']} {$v['longitude']})",
|
||||||
|
':id' => $id,
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'Data lokasi bantuan berhasil diperbarui.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
elseif ($method === 'DELETE' && $action === 'delete' && $id) {
|
||||||
|
$stmt = $db->prepare("DELETE FROM lokasi_bantuan WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
jsonOk(['message' => 'Lokasi bantuan berhasil dihapus.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
jsonError('Aksi tidak dikenali.', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validasiLB(array $b): array {
|
||||||
|
$nama_kk = trim($b['nama_kk'] ?? '');
|
||||||
|
$jml = isset($b['jml_anggota']) ? (int)$b['jml_anggota'] : 1;
|
||||||
|
$kategori = trim($b['kategori'] ?? '');
|
||||||
|
$alamat = trim($b['alamat'] ?? '');
|
||||||
|
$lat = isset($b['latitude']) ? (float)$b['latitude'] : null;
|
||||||
|
$lng = isset($b['longitude']) ? (float)$b['longitude'] : null;
|
||||||
|
|
||||||
|
$katList = ['Kemiskinan','Bencana','Lansia','Lainnya'];
|
||||||
|
|
||||||
|
if (!$nama_kk) throw new Exception('Nama kepala keluarga wajib diisi.');
|
||||||
|
if ($jml < 1) throw new Exception('Jumlah anggota minimal 1.');
|
||||||
|
if (!in_array($kategori, $katList)) throw new Exception('Kategori tidak valid.');
|
||||||
|
if (!$alamat) throw new Exception('Alamat wajib diisi.');
|
||||||
|
if ($lat === null || $lat < -90 || $lat > 90) throw new Exception('Latitude tidak valid.');
|
||||||
|
if ($lng === null || $lng < -180 || $lng > 180) throw new Exception('Longitude tidak valid.');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nama_kk' => $nama_kk,
|
||||||
|
'jml_anggota' => $jml,
|
||||||
|
'kategori' => $kategori,
|
||||||
|
'alamat' => $alamat,
|
||||||
|
'keterangan' => trim($b['keterangan'] ?? ''),
|
||||||
|
'latitude' => $lat,
|
||||||
|
'longitude' => $lng,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SUMBANGAN (Stok donasi di rumah ibadah)
|
||||||
|
// ============================================================
|
||||||
|
function handleSumbangan(string $method, string $action, ?int $id, array $body): void {
|
||||||
|
$db = getDB();
|
||||||
|
$riId = isset($_GET['rumah_ibadah_id']) ? (int)$_GET['rumah_ibadah_id'] : null;
|
||||||
|
|
||||||
|
// GET LIST by rumah_ibadah_id
|
||||||
|
if ($method === 'GET' && $action === 'list') {
|
||||||
|
if (!$riId) jsonError('Parameter rumah_ibadah_id diperlukan.', 400);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT s.id, s.rumah_ibadah_id, s.jenis, s.jumlah, s.satuan, s.keterangan,
|
||||||
|
s.created_at,
|
||||||
|
COALESCE(SUM(p.jumlah_disalurkan), 0) AS total_disalurkan,
|
||||||
|
(s.jumlah - COALESCE(SUM(p.jumlah_disalurkan), 0)) AS sisa
|
||||||
|
FROM sumbangan s
|
||||||
|
LEFT JOIN penyaluran_bantuan p ON p.sumbangan_id = s.id
|
||||||
|
WHERE s.rumah_ibadah_id = ?
|
||||||
|
GROUP BY s.id
|
||||||
|
ORDER BY s.created_at DESC"
|
||||||
|
);
|
||||||
|
$stmt->execute([$riId]);
|
||||||
|
jsonOk($stmt->fetchAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST CREATE
|
||||||
|
elseif ($method === 'POST' && $action === 'create') {
|
||||||
|
$v = validasiSumbangan($body);
|
||||||
|
// Cek rumah ibadah ada
|
||||||
|
$cek = $db->prepare("SELECT id FROM rumah_ibadah WHERE id = ?");
|
||||||
|
$cek->execute([$v['rumah_ibadah_id']]);
|
||||||
|
if (!$cek->fetch()) jsonError('Rumah ibadah tidak ditemukan.', 404);
|
||||||
|
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"INSERT INTO sumbangan (rumah_ibadah_id, jenis, jumlah, satuan, keterangan)
|
||||||
|
VALUES (:ri_id, :jenis, :jumlah, :satuan, :ket)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':ri_id' => $v['rumah_ibadah_id'],
|
||||||
|
':jenis' => $v['jenis'],
|
||||||
|
':jumlah' => $v['jumlah'],
|
||||||
|
':satuan' => $v['satuan'],
|
||||||
|
':ket' => $v['keterangan'],
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'Sumbangan berhasil ditambahkan.', 'id' => (int)$db->lastInsertId()], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT UPDATE
|
||||||
|
elseif ($method === 'PUT' && $action === 'update' && $id) {
|
||||||
|
$v = validasiSumbangan($body);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE sumbangan SET jenis=:jenis, jumlah=:jumlah, satuan=:satuan, keterangan=:ket
|
||||||
|
WHERE id=:id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':jenis' => $v['jenis'],
|
||||||
|
':jumlah' => $v['jumlah'],
|
||||||
|
':satuan' => $v['satuan'],
|
||||||
|
':ket' => $v['keterangan'],
|
||||||
|
':id' => $id,
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'Sumbangan berhasil diperbarui.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
elseif ($method === 'DELETE' && $action === 'delete' && $id) {
|
||||||
|
// Cek apakah sudah ada penyaluran
|
||||||
|
$cek = $db->prepare("SELECT COUNT(*) FROM penyaluran_bantuan WHERE sumbangan_id = ?");
|
||||||
|
$cek->execute([$id]);
|
||||||
|
if ((int)$cek->fetchColumn() > 0) {
|
||||||
|
jsonError('Sumbangan ini sudah memiliki riwayat penyaluran dan tidak dapat dihapus.', 422);
|
||||||
|
}
|
||||||
|
$db->prepare("DELETE FROM sumbangan WHERE id = ?")->execute([$id]);
|
||||||
|
jsonOk(['message' => 'Sumbangan berhasil dihapus.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
jsonError('Aksi tidak dikenali.', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validasiSumbangan(array $b): array {
|
||||||
|
$riId = isset($b['rumah_ibadah_id']) ? (int)$b['rumah_ibadah_id'] : 0;
|
||||||
|
$jenis = trim($b['jenis'] ?? '');
|
||||||
|
$jumlah = isset($b['jumlah']) ? (float)$b['jumlah'] : null;
|
||||||
|
$satuan = trim($b['satuan'] ?? '');
|
||||||
|
|
||||||
|
if (!$riId) throw new Exception('rumah_ibadah_id wajib diisi.');
|
||||||
|
if (!$jenis) throw new Exception('Jenis sumbangan wajib diisi.');
|
||||||
|
if ($jumlah === null || $jumlah <= 0) throw new Exception('Jumlah harus lebih dari 0.');
|
||||||
|
if (!$satuan) throw new Exception('Satuan wajib diisi.');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'rumah_ibadah_id' => $riId,
|
||||||
|
'jenis' => $jenis,
|
||||||
|
'jumlah' => $jumlah,
|
||||||
|
'satuan' => $satuan,
|
||||||
|
'keterangan' => trim($b['keterangan'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// PENYALURAN BANTUAN
|
||||||
|
// ============================================================
|
||||||
|
function handlePenyaluran(string $method, string $action, ?int $id, array $body): void {
|
||||||
|
$db = getDB();
|
||||||
|
$lbId = isset($_GET['lokasi_bantuan_id']) ? (int)$_GET['lokasi_bantuan_id'] : null;
|
||||||
|
$riId = isset($_GET['rumah_ibadah_id']) ? (int)$_GET['rumah_ibadah_id'] : null;
|
||||||
|
|
||||||
|
// GET LIST riwayat by lokasi_bantuan atau rumah_ibadah
|
||||||
|
if ($method === 'GET' && $action === 'list') {
|
||||||
|
$where = '';
|
||||||
|
$params = [];
|
||||||
|
if ($lbId) { $where = 'WHERE p.lokasi_bantuan_id = ?'; $params[] = $lbId; }
|
||||||
|
elseif ($riId) { $where = 'WHERE s.rumah_ibadah_id = ?'; $params[] = $riId; }
|
||||||
|
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT p.id, p.sumbangan_id, p.lokasi_bantuan_id, p.jumlah_disalurkan,
|
||||||
|
p.tanggal, p.keterangan, p.created_at,
|
||||||
|
s.jenis AS jenis_sumbangan, s.satuan,
|
||||||
|
lb.nama_kk, ri.nama AS nama_ri
|
||||||
|
FROM penyaluran_bantuan p
|
||||||
|
JOIN sumbangan s ON s.id = p.sumbangan_id
|
||||||
|
JOIN rumah_ibadah ri ON ri.id = s.rumah_ibadah_id
|
||||||
|
JOIN lokasi_bantuan lb ON lb.id = p.lokasi_bantuan_id
|
||||||
|
$where
|
||||||
|
ORDER BY p.created_at DESC"
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
jsonOk($stmt->fetchAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET sumbangan tersedia untuk lokasi bantuan tertentu (semua rumah ibadah dalam jangkauan)
|
||||||
|
elseif ($method === 'GET' && $action === 'tersedia' && $lbId) {
|
||||||
|
$stmtRI = $db->prepare(
|
||||||
|
"SELECT ri.id, ri.nama, ri.radius_aktif,
|
||||||
|
ST_Distance_Sphere(lb.koordinat, ri.koordinat) AS jarak
|
||||||
|
FROM lokasi_bantuan lb
|
||||||
|
CROSS JOIN rumah_ibadah ri
|
||||||
|
WHERE lb.id = ?
|
||||||
|
ORDER BY jarak ASC"
|
||||||
|
);
|
||||||
|
$stmtRI->execute([$lbId]);
|
||||||
|
$riList = $stmtRI->fetchAll();
|
||||||
|
|
||||||
|
if (!$riList) jsonError('Tidak ada rumah ibadah di sistem.', 404);
|
||||||
|
|
||||||
|
$radiusGlobal = isset($_GET['radius']) ? (int)$_GET['radius'] : 500;
|
||||||
|
$tersedia = [];
|
||||||
|
|
||||||
|
$stmtS = $db->prepare(
|
||||||
|
"SELECT s.id, s.jenis, s.satuan, s.keterangan,
|
||||||
|
s.jumlah,
|
||||||
|
COALESCE(SUM(p.jumlah_disalurkan), 0) AS total_disalurkan,
|
||||||
|
(s.jumlah - COALESCE(SUM(p.jumlah_disalurkan), 0)) AS sisa
|
||||||
|
FROM sumbangan s
|
||||||
|
LEFT JOIN penyaluran_bantuan p ON p.sumbangan_id = s.id
|
||||||
|
WHERE s.rumah_ibadah_id = ?
|
||||||
|
GROUP BY s.id
|
||||||
|
HAVING sisa > 0
|
||||||
|
ORDER BY s.jenis ASC"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($riList as $ri) {
|
||||||
|
$batasRadius = isset($_GET['radius']) ? $radiusGlobal : (int)$ri['radius_aktif'];
|
||||||
|
if ((float)$ri['jarak'] <= $batasRadius) {
|
||||||
|
// Ambil stok
|
||||||
|
$stmtS->execute([$ri['id']]);
|
||||||
|
$sumbangan = $stmtS->fetchAll();
|
||||||
|
|
||||||
|
$tersedia[] = [
|
||||||
|
'rumah_ibadah' => ['id' => $ri['id'], 'nama' => $ri['nama']],
|
||||||
|
'jarak_meter' => round((float)$ri['jarak'], 1),
|
||||||
|
'sumbangan' => $sumbangan
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($tersedia)) {
|
||||||
|
jsonError('Lokasi bantuan ini berada di luar jangkauan rumah ibadah manapun.', 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonOk($tersedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST CREATE penyaluran baru
|
||||||
|
elseif ($method === 'POST' && $action === 'create') {
|
||||||
|
$sumbanganId = isset($body['sumbangan_id']) ? (int)$body['sumbangan_id'] : 0;
|
||||||
|
$lbIdPost = isset($body['lokasi_bantuan_id']) ? (int)$body['lokasi_bantuan_id'] : 0;
|
||||||
|
$jumlah = isset($body['jumlah_disalurkan']) ? (float)$body['jumlah_disalurkan'] : 0;
|
||||||
|
$tanggal = trim($body['tanggal'] ?? date('Y-m-d'));
|
||||||
|
$ket = trim($body['keterangan'] ?? '');
|
||||||
|
|
||||||
|
if (!$sumbanganId) throw new Exception('sumbangan_id wajib diisi.');
|
||||||
|
if (!$lbIdPost) throw new Exception('lokasi_bantuan_id wajib diisi.');
|
||||||
|
if ($jumlah <= 0) throw new Exception('Jumlah disalurkan harus lebih dari 0.');
|
||||||
|
|
||||||
|
// Cek stok mencukupi
|
||||||
|
$stmtSisa = $db->prepare(
|
||||||
|
"SELECT s.jumlah, s.jenis, s.satuan,
|
||||||
|
(s.jumlah - COALESCE(SUM(p.jumlah_disalurkan), 0)) AS sisa
|
||||||
|
FROM sumbangan s
|
||||||
|
LEFT JOIN penyaluran_bantuan p ON p.sumbangan_id = s.id
|
||||||
|
WHERE s.id = ?
|
||||||
|
GROUP BY s.id"
|
||||||
|
);
|
||||||
|
$stmtSisa->execute([$sumbanganId]);
|
||||||
|
$stok = $stmtSisa->fetch();
|
||||||
|
if (!$stok) throw new Exception('Sumbangan tidak ditemukan.');
|
||||||
|
if ((float)$stok['sisa'] < $jumlah) {
|
||||||
|
throw new Exception("Stok {$stok['jenis']} tidak mencukupi. Sisa: {$stok['sisa']} {$stok['satuan']}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"INSERT INTO penyaluran_bantuan
|
||||||
|
(sumbangan_id, lokasi_bantuan_id, jumlah_disalurkan, tanggal, keterangan)
|
||||||
|
VALUES (?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
$stmt->execute([$sumbanganId, $lbIdPost, $jumlah, $tanggal, $ket]);
|
||||||
|
jsonOk(['message' => 'Penyaluran bantuan berhasil dicatat.', 'id' => (int)$db->lastInsertId()], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
jsonError('Aksi tidak dikenali.', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SPBU
|
||||||
|
// ============================================================
|
||||||
|
function handleSpbu(string $method, string $action, ?int $id, array $body): void {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// GET LIST
|
||||||
|
if ($method === 'GET' && $action === 'list') {
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT id, nama, buka_24_jam, kontak, latitude, longitude, created_at
|
||||||
|
FROM spbu
|
||||||
|
ORDER BY nama ASC"
|
||||||
|
)->fetchAll();
|
||||||
|
jsonOk($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST CREATE
|
||||||
|
elseif ($method === 'POST' && $action === 'create') {
|
||||||
|
$v = validasiSpbu($body);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"INSERT INTO spbu (nama, buka_24_jam, kontak, latitude, longitude, koordinat)
|
||||||
|
VALUES (:nama, :buka_24_jam, :kontak, :lat, :lng, ST_GeomFromText(:titik, 4326))"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama' => $v['nama'],
|
||||||
|
':buka_24_jam' => $v['buka_24_jam'],
|
||||||
|
':kontak' => $v['kontak'],
|
||||||
|
':lat' => $v['latitude'],
|
||||||
|
':lng' => $v['longitude'],
|
||||||
|
':titik' => "POINT({$v['latitude']} {$v['longitude']})"
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'SPBU berhasil ditambahkan.', 'id' => (int)$db->lastInsertId()], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT UPDATE (Bisa update koordinat saja saat drag-and-drop, atau full data)
|
||||||
|
elseif ($method === 'PUT' && $action === 'update' && $id) {
|
||||||
|
// Cek jika update koordinat saja (drag and drop)
|
||||||
|
if (isset($body['latitude']) && isset($body['longitude']) && !isset($body['nama'])) {
|
||||||
|
$lat = (float)$body['latitude'];
|
||||||
|
$lng = (float)$body['longitude'];
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE spbu SET latitude = :lat, longitude = :lng, koordinat = ST_GeomFromText(:titik, 4326) WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':lat' => $lat,
|
||||||
|
':lng' => $lng,
|
||||||
|
':titik' => "POINT({$lat} {$lng})",
|
||||||
|
':id' => $id
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'Posisi SPBU berhasil diperbarui.']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full update
|
||||||
|
$v = validasiSpbu($body);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE spbu SET nama = :nama, buka_24_jam = :buka_24_jam, kontak = :kontak,
|
||||||
|
latitude = :lat, longitude = :lng, koordinat = ST_GeomFromText(:titik, 4326)
|
||||||
|
WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama' => $v['nama'],
|
||||||
|
':buka_24_jam' => $v['buka_24_jam'],
|
||||||
|
':kontak' => $v['kontak'],
|
||||||
|
':lat' => $v['latitude'],
|
||||||
|
':lng' => $v['longitude'],
|
||||||
|
':titik' => "POINT({$v['latitude']} {$v['longitude']})",
|
||||||
|
':id' => $id
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'Data SPBU berhasil diperbarui.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
elseif ($method === 'DELETE' && $action === 'delete' && $id) {
|
||||||
|
$db->prepare("DELETE FROM spbu WHERE id = ?")->execute([$id]);
|
||||||
|
jsonOk(['message' => 'SPBU berhasil dihapus.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
jsonError('Aksi tidak dikenali.', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validasiSpbu(array $b): array {
|
||||||
|
$nama = trim($b['nama'] ?? '');
|
||||||
|
$lat = isset($b['latitude']) ? (float)$b['latitude'] : null;
|
||||||
|
$lng = isset($b['longitude']) ? (float)$b['longitude'] : null;
|
||||||
|
|
||||||
|
if (!$nama) throw new Exception('Nama SPBU wajib diisi.');
|
||||||
|
if ($lat === null || $lat < -90 || $lat > 90) throw new Exception('Latitude tidak valid.');
|
||||||
|
if ($lng === null || $lng < -180 || $lng > 180) throw new Exception('Longitude tidak valid.');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nama' => $nama,
|
||||||
|
'buka_24_jam' => isset($b['buka_24_jam']) ? (int)$b['buka_24_jam'] : 0,
|
||||||
|
'kontak' => trim($b['kontak'] ?? ''),
|
||||||
|
'latitude' => $lat,
|
||||||
|
'longitude' => $lng,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SETUP — Buat tabel otomatis jika belum ada
|
||||||
|
// ============================================================
|
||||||
|
function handleSetup(): void {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS rumah_ibadah (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
jenis ENUM('Masjid','Gereja','Pura','Vihara','Kelenteng') NOT NULL,
|
||||||
|
alamat TEXT NOT NULL,
|
||||||
|
narahubung VARCHAR(255) DEFAULT '',
|
||||||
|
latitude DECIMAL(10,7) NOT NULL,
|
||||||
|
longitude DECIMAL(10,7) NOT NULL,
|
||||||
|
koordinat POINT NOT NULL SRID 4326,
|
||||||
|
radius_aktif INT NOT NULL DEFAULT 500,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
SPATIAL INDEX (koordinat)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS lokasi_bantuan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_kk VARCHAR(255) NOT NULL,
|
||||||
|
jml_anggota INT NOT NULL DEFAULT 1,
|
||||||
|
kategori ENUM('Kemiskinan','Bencana','Lansia','Lainnya') NOT NULL,
|
||||||
|
alamat TEXT NOT NULL,
|
||||||
|
keterangan TEXT,
|
||||||
|
latitude DECIMAL(10,7) NOT NULL,
|
||||||
|
longitude DECIMAL(10,7) NOT NULL,
|
||||||
|
koordinat POINT NOT NULL SRID 4326,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
SPATIAL INDEX (koordinat)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS sumbangan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
rumah_ibadah_id INT NOT NULL,
|
||||||
|
jenis VARCHAR(100) NOT NULL,
|
||||||
|
jumlah DECIMAL(12,2) NOT NULL,
|
||||||
|
satuan VARCHAR(30) NOT NULL,
|
||||||
|
keterangan TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS penyaluran_bantuan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
sumbangan_id INT NOT NULL,
|
||||||
|
lokasi_bantuan_id INT NOT NULL,
|
||||||
|
jumlah_disalurkan DECIMAL(12,2) NOT NULL,
|
||||||
|
tanggal DATE NOT NULL,
|
||||||
|
keterangan TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (sumbangan_id) REFERENCES sumbangan(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY (lokasi_bantuan_id) REFERENCES lokasi_bantuan(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS spbu (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
buka_24_jam TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
kontak VARCHAR(100) DEFAULT '',
|
||||||
|
latitude DECIMAL(10,7) NOT NULL,
|
||||||
|
longitude DECIMAL(10,7) NOT NULL,
|
||||||
|
koordinat POINT NOT NULL SRID 4326,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
SPATIAL INDEX (koordinat)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS jalan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
status VARCHAR(100) NOT NULL,
|
||||||
|
panjang DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||||
|
geom LINESTRING NOT NULL SRID 4326,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
SPATIAL INDEX (geom)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS parsil (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_pemilik VARCHAR(255) NOT NULL,
|
||||||
|
status VARCHAR(100) NOT NULL,
|
||||||
|
luas DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||||
|
geom POLYGON NOT NULL SRID 4326,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
SPATIAL INDEX (geom)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
jsonOk(['message' => 'Tabel berhasil dibuat / sudah ada.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HELPER
|
||||||
|
// ============================================================
|
||||||
|
function jsonOk(mixed $data, int $code = 200): void {
|
||||||
|
http_response_code($code);
|
||||||
|
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonError(string $msg, int $code = 400): void {
|
||||||
|
http_response_code($code);
|
||||||
|
echo json_encode(['error' => $msg], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// JALAN & PARSIL
|
||||||
|
// ============================================================
|
||||||
|
function handleJalan(string $method, string $action, ?int $id, array $body): void {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
if ($method === 'GET' && $action === 'list') {
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT id, nama, status, panjang,
|
||||||
|
ST_AsGeoJSON(geom) AS geom_json,
|
||||||
|
created_at
|
||||||
|
FROM jalan"
|
||||||
|
)->fetchAll();
|
||||||
|
foreach ($rows as &$r) {
|
||||||
|
$r['geom'] = json_decode($r['geom_json'], true);
|
||||||
|
unset($r['geom_json']);
|
||||||
|
}
|
||||||
|
jsonOk($rows);
|
||||||
|
}
|
||||||
|
elseif ($method === 'POST' && $action === 'create') {
|
||||||
|
if (empty($body['nama']) || empty($body['status']) || empty($body['geom'])) {
|
||||||
|
jsonError('Data jalan tidak lengkap.');
|
||||||
|
}
|
||||||
|
$geomJson = json_encode($body['geom']);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"INSERT INTO jalan (nama, status, panjang, geom)
|
||||||
|
VALUES (:nama, :status, :panjang, ST_GeomFromGeoJSON(:geom, 2, 4326))"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama' => $body['nama'],
|
||||||
|
':status' => $body['status'],
|
||||||
|
':panjang' => $body['panjang'] ?? 0,
|
||||||
|
':geom' => $geomJson
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'Data jalan berhasil disimpan.', 'id' => $db->lastInsertId()]);
|
||||||
|
}
|
||||||
|
elseif ($method === 'PUT' && $action === 'update' && $id) {
|
||||||
|
$geomJson = isset($body['geom']) ? json_encode($body['geom']) : null;
|
||||||
|
if ($geomJson) {
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE jalan SET
|
||||||
|
nama = :nama, status = :status, panjang = :panjang,
|
||||||
|
geom = ST_GeomFromGeoJSON(:geom, 2, 4326)
|
||||||
|
WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama' => $body['nama'],
|
||||||
|
':status' => $body['status'],
|
||||||
|
':panjang' => $body['panjang'],
|
||||||
|
':geom' => $geomJson,
|
||||||
|
':id' => $id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $db->prepare("UPDATE jalan SET nama = :nama, status = :status WHERE id = :id");
|
||||||
|
$stmt->execute([
|
||||||
|
':nama' => $body['nama'],
|
||||||
|
':status' => $body['status'],
|
||||||
|
':id' => $id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
jsonOk(['message' => 'Data jalan berhasil diperbarui.']);
|
||||||
|
}
|
||||||
|
elseif ($method === 'DELETE' && $action === 'delete' && $id) {
|
||||||
|
$stmt = $db->prepare("DELETE FROM jalan WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
jsonOk(['message' => 'Data jalan dihapus.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleParsil(string $method, string $action, ?int $id, array $body): void {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
if ($method === 'GET' && $action === 'list') {
|
||||||
|
$rows = $db->query(
|
||||||
|
"SELECT id, nama_pemilik, status, luas,
|
||||||
|
ST_AsGeoJSON(geom) AS geom_json,
|
||||||
|
created_at
|
||||||
|
FROM parsil"
|
||||||
|
)->fetchAll();
|
||||||
|
foreach ($rows as &$r) {
|
||||||
|
$r['geom'] = json_decode($r['geom_json'], true);
|
||||||
|
unset($r['geom_json']);
|
||||||
|
}
|
||||||
|
jsonOk($rows);
|
||||||
|
}
|
||||||
|
elseif ($method === 'POST' && $action === 'create') {
|
||||||
|
if (empty($body['nama_pemilik']) || empty($body['status']) || empty($body['geom'])) {
|
||||||
|
jsonError('Data parsil tidak lengkap.');
|
||||||
|
}
|
||||||
|
$geomJson = json_encode($body['geom']);
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"INSERT INTO parsil (nama_pemilik, status, luas, geom)
|
||||||
|
VALUES (:nama_pemilik, :status, :luas, ST_GeomFromGeoJSON(:geom, 2, 4326))"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama_pemilik' => $body['nama_pemilik'],
|
||||||
|
':status' => $body['status'],
|
||||||
|
':luas' => $body['luas'] ?? 0,
|
||||||
|
':geom' => $geomJson
|
||||||
|
]);
|
||||||
|
jsonOk(['message' => 'Data parsil berhasil disimpan.', 'id' => $db->lastInsertId()]);
|
||||||
|
}
|
||||||
|
elseif ($method === 'PUT' && $action === 'update' && $id) {
|
||||||
|
$geomJson = isset($body['geom']) ? json_encode($body['geom']) : null;
|
||||||
|
if ($geomJson) {
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"UPDATE parsil SET
|
||||||
|
nama_pemilik = :nama_pemilik, status = :status, luas = :luas,
|
||||||
|
geom = ST_GeomFromGeoJSON(:geom, 2, 4326)
|
||||||
|
WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':nama_pemilik' => $body['nama_pemilik'],
|
||||||
|
':status' => $body['status'],
|
||||||
|
':luas' => $body['luas'],
|
||||||
|
':geom' => $geomJson,
|
||||||
|
':id' => $id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $db->prepare("UPDATE parsil SET nama_pemilik = :nama_pemilik, status = :status WHERE id = :id");
|
||||||
|
$stmt->execute([
|
||||||
|
':nama_pemilik' => $body['nama_pemilik'],
|
||||||
|
':status' => $body['status'],
|
||||||
|
':id' => $id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
jsonOk(['message' => 'Data parsil berhasil diperbarui.']);
|
||||||
|
}
|
||||||
|
elseif ($method === 'DELETE' && $action === 'delete' && $id) {
|
||||||
|
$stmt = $db->prepare("DELETE FROM parsil WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
jsonOk(['message' => 'Data parsil dihapus.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard WebGIS Partisipasi</title>
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
||||||
|
<style>
|
||||||
|
/* ===== RESET ===== */
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
background: #f0f2f5;
|
||||||
|
color: #333;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== HEADER ===== */
|
||||||
|
header {
|
||||||
|
background: linear-gradient(135deg, #1a6b3c, #2d9e5f);
|
||||||
|
color: #fff;
|
||||||
|
padding: 15px 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
header small {
|
||||||
|
font-size: .85rem;
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MAIN CONTENT ===== */
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 40px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #1a6b3c;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== CARDS ===== */
|
||||||
|
.cards-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 25px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, .1);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 12px 20px rgba(0, 0, 0, .15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
height: 140px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 4rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-green { background: linear-gradient(135deg, #1a6b3c, #2d9e5f); }
|
||||||
|
.bg-orange { background: linear-gradient(135deg, #e08020, #f59e42); }
|
||||||
|
.bg-blue { background: linear-gradient(135deg, #1976d2, #42a5f5); }
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #777;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .card-footer {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #1a6b3c;
|
||||||
|
}
|
||||||
|
.card.card-orange:hover .card-footer { background: #fff3e0; color: #e08020; }
|
||||||
|
.card.card-blue:hover .card-footer { background: #e3f2fd; color: #1976d2; }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<i class="fa-solid fa-map-location-dot fa-2x"></i>
|
||||||
|
<div>
|
||||||
|
<h1>Portal WebGIS Terpadu</h1>
|
||||||
|
<small>Sistem Informasi Geografis Multi-Sektor</small>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="welcome-text">
|
||||||
|
<h2>Selamat Datang</h2>
|
||||||
|
<p>Silakan pilih modul peta yang ingin Anda akses di bawah ini. Setiap modul memiliki fungsi dan data pemetaan yang spesifik.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards-container">
|
||||||
|
<!-- Card 1: Bantuan -->
|
||||||
|
<a href="map_bantuan.html" class="card">
|
||||||
|
<div class="card-icon bg-green">
|
||||||
|
<i class="fa-solid fa-hands-holding-child"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>Peta Bantuan & Rumah Ibadah</h3>
|
||||||
|
<p>Pemetaan partisipasi rumah ibadah dalam pengentasan kemiskinan dan distribusi bantuan.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">Buka Peta <i class="fa-solid fa-arrow-right"></i></div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Card 2: SPBU -->
|
||||||
|
<a href="map_spbu.html" class="card card-orange">
|
||||||
|
<div class="card-icon bg-orange">
|
||||||
|
<i class="fa-solid fa-gas-pump"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>Peta SPBU</h3>
|
||||||
|
<p>Pemetaan lokasi Stasiun Pengisian Bahan Bakar Umum (SPBU), status operasional 24 jam, dan info kontak.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer" style="color: #e08020;">Buka Peta <i class="fa-solid fa-arrow-right"></i></div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Card 3: Parsil -->
|
||||||
|
<a href="map_parsil.html" class="card card-blue">
|
||||||
|
<div class="card-icon bg-blue">
|
||||||
|
<i class="fa-solid fa-road"></i>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>Peta Parsil & Jalan</h3>
|
||||||
|
<p>Informasi jaringan jalan dan bidang tanah (parsil) untuk keperluan perencanaan wilayah.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer" style="color: #1976d2;">Buka Peta <i class="fa-solid fa-arrow-right"></i></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================
|
||||||
|
// KONFIGURASI DATABASE — Sesuaikan dengan Laragon Anda
|
||||||
|
// ============================================================
|
||||||
|
define('DB_HOST', '127.0.0.1');
|
||||||
|
define('DB_PORT', '3306');
|
||||||
|
define('DB_NAME', 'webgis_rumah_ibadah');
|
||||||
|
define('DB_USER', 'root');
|
||||||
|
define('DB_PASS', ''); // Laragon default: kosong
|
||||||
|
define('DB_CHAR', 'utf8mb4');
|
||||||
|
|
||||||
|
function getDB(): PDO {
|
||||||
|
static $pdo = null;
|
||||||
|
if ($pdo === null) {
|
||||||
|
$dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT
|
||||||
|
. ';dbname=' . DB_NAME . ';charset=' . DB_CHAR;
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['error' => 'Koneksi database gagal: ' . $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
+1822
File diff suppressed because it is too large
Load Diff
+462
@@ -0,0 +1,462 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGIS Parsil & Jalan</title>
|
||||||
|
|
||||||
|
<!-- Leaflet CSS -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
||||||
|
<!-- Leaflet Geoman CSS -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@geoman-io/leaflet-geoman-free@latest/dist/leaflet-geoman.css" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Segoe UI', Arial, sans-serif; height: 100vh; display: flex; flex-direction: column; background: #f0f2f5; }
|
||||||
|
|
||||||
|
header { background: linear-gradient(135deg, #1976d2, #42a5f5); color: #fff; padding: 10px 18px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 2px 8px rgba(0, 0, 0, .3); z-index: 1000; flex-shrink: 0; }
|
||||||
|
header .title-area { display: flex; align-items: center; gap: 12px; }
|
||||||
|
header h1 { font-size: 1rem; font-weight: 700; }
|
||||||
|
header small { font-size: .7rem; opacity: .85; }
|
||||||
|
.back-btn { color: white; text-decoration: none; font-size: 0.9rem; border: 1px solid rgba(255,255,255,0.5); padding: 5px 10px; border-radius: 5px; transition: background 0.2s;}
|
||||||
|
.back-btn:hover { background: rgba(255,255,255,0.2); }
|
||||||
|
|
||||||
|
.app-body { display: flex; flex: 1; overflow: hidden; position: relative; }
|
||||||
|
|
||||||
|
#sidebar { width: 320px; background: #fff; display: flex; flex-direction: column; border-right: 1px solid #dde; z-index: 900; transition: transform .3s; flex-shrink: 0; }
|
||||||
|
#sidebar.hide { transform: translateX(-100%); position: absolute; top: 0; left: 0; height: 100%; box-shadow: 4px 0 12px rgba(0, 0, 0, .2); }
|
||||||
|
|
||||||
|
/* Sidebar Tabs */
|
||||||
|
.sb-tabs { display: flex; background: #fff; border-bottom: 1px solid #ddd; flex-shrink: 0; }
|
||||||
|
.sb-tab { flex: 1; text-align: center; padding: 12px 0; font-size: 0.85rem; font-weight: 700; color: #777; cursor: pointer; border-bottom: 3px solid transparent; transition: all 0.2s; }
|
||||||
|
.sb-tab:hover { background: #f8f8f8; color: #333; }
|
||||||
|
.sb-tab.active { color: #1976d2; border-bottom: 3px solid #1976d2; background: #f0f7ff; }
|
||||||
|
|
||||||
|
.sb-head { background: #1976d2; color: #fff; padding: 10px 14px; font-weight: 700; font-size: .88rem; }
|
||||||
|
|
||||||
|
.list-wrap { padding: 10px; flex: 1; overflow-y: auto; background: #fafafa; }
|
||||||
|
.list-item { background: #fff; border: 1px solid #eee; padding: 10px; border-radius: 6px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
||||||
|
.list-item h4 { margin-bottom: 4px; color: #333; font-size: 0.95rem; }
|
||||||
|
.list-item .attr { font-size: 0.8rem; color: #666; margin-bottom: 2px; }
|
||||||
|
.list-item .actions { margin-top: 8px; text-align: right; }
|
||||||
|
|
||||||
|
#map { flex: 1; z-index: 1; }
|
||||||
|
#btn-toggle { position: absolute; top: 10px; left: 0; z-index: 950; background: #1976d2; color: #fff; border: none; padding: 8px 7px; border-radius: 0 6px 6px 0; cursor: pointer; font-size: 1rem; box-shadow: 2px 0 6px rgba(0, 0, 0, .25); }
|
||||||
|
|
||||||
|
/* Buttons & Forms */
|
||||||
|
.btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: .8rem; font-weight: 600; display: inline-flex; align-items: center; gap: 6px; transition: opacity .2s; }
|
||||||
|
.btn:hover { opacity: .85; }
|
||||||
|
.btn-red { background: #d93025; color: #fff; }
|
||||||
|
.btn-blue { background: #1976d2; color: #fff; }
|
||||||
|
.btn-green { background: #1a6b3c; color: #fff; }
|
||||||
|
.btn-gray { background: #e0e0e0; color: #333; }
|
||||||
|
|
||||||
|
.fg { margin-bottom: 12px; }
|
||||||
|
.fg label { display: block; font-size: .8rem; font-weight: 600; margin-bottom: 4px; color: #555; }
|
||||||
|
.fg input, .fg select { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-family: inherit; font-size: .85rem; }
|
||||||
|
.fg input[readonly] { background: #eee; color: #555; }
|
||||||
|
|
||||||
|
/* Modal Overlay */
|
||||||
|
.overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,.5); display: none; align-items: center; justify-content: center; z-index: 9999; }
|
||||||
|
.overlay.on { display: flex; }
|
||||||
|
.modal { background: #fff; padding: 20px; border-radius: 8px; width: 90%; max-width: 400px; box-shadow: 0 4px 15px rgba(0,0,0,.3); }
|
||||||
|
.modal h2 { margin-bottom: 15px; font-size: 1.2rem; color: #333; border-bottom: 1px solid #eee; padding-bottom: 10px; }
|
||||||
|
.modal-footer { margin-top: 20px; text-align: right; border-top: 1px solid #eee; padding-top: 15px; }
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
#toast { position: fixed; bottom: 20px; right: 20px; padding: 10px 20px; border-radius: 5px; color: #fff; font-size: .9rem; opacity: 0; pointer-events: none; transition: opacity .3s; z-index: 10000; box-shadow: 0 2px 10px rgba(0,0,0,.3); }
|
||||||
|
#toast.on { opacity: 1; pointer-events: auto; }
|
||||||
|
#toast.ok { background: #1a6b3c; }
|
||||||
|
#toast.err { background: #d93025; }
|
||||||
|
|
||||||
|
.empty { text-align: center; color: #888; font-size: 0.9rem; padding: 20px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="title-area">
|
||||||
|
<i class="fa-solid fa-road fa-lg"></i>
|
||||||
|
<div>
|
||||||
|
<h1>WebGIS Parsil & Jalan</h1>
|
||||||
|
<small>Pemetaan Bidang Tanah & Jaringan Jalan</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="index.html" class="back-btn"><i class="fa-solid fa-arrow-left"></i> Kembali ke Dashboard</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="app-body">
|
||||||
|
<div id="sidebar">
|
||||||
|
<div class="sb-tabs">
|
||||||
|
<div class="sb-tab active" id="tab-jalan" onclick="gantiTab('jalan')"><i class="fa-solid fa-road"></i> Data Jalan</div>
|
||||||
|
<div class="sb-tab" id="tab-parsil" onclick="gantiTab('parsil')"><i class="fa-solid fa-draw-polygon"></i> Data Parsil</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Konten Jalan -->
|
||||||
|
<div id="content-jalan" style="display: flex; flex-direction: column; flex: 1; overflow: hidden;">
|
||||||
|
<div class="sb-head"><i class="fa-solid fa-list"></i> Daftar Jalan</div>
|
||||||
|
<div class="list-wrap" id="list-jalan">
|
||||||
|
<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Konten Parsil -->
|
||||||
|
<div id="content-parsil" style="display: none; flex-direction: column; flex: 1; overflow: hidden;">
|
||||||
|
<div class="sb-head" style="background:#27ae60;"><i class="fa-solid fa-list"></i> Daftar Parsil (Kavling)</div>
|
||||||
|
<div class="list-wrap" id="list-parsil">
|
||||||
|
<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
<button id="btn-toggle" onclick="toggleSidebar()"><i class="fa-solid fa-bars"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL FORM JALAN -->
|
||||||
|
<div class="overlay" id="ovl-jalan">
|
||||||
|
<div class="modal">
|
||||||
|
<h2><i class="fa-solid fa-road"></i> Data Jalan</h2>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Nama Jalan</label>
|
||||||
|
<input type="text" id="j-nama" placeholder="Contoh: Jl. Ahmad Yani">
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Status Jalan</label>
|
||||||
|
<select id="j-status">
|
||||||
|
<option value="Nasional">Jalan Nasional</option>
|
||||||
|
<option value="Provinsi">Jalan Provinsi</option>
|
||||||
|
<option value="Kabupaten">Jalan Kabupaten</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Panjang (Otomatis)</label>
|
||||||
|
<input type="text" id="j-panjang" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-gray" onclick="batalSimpan('jalan')">Batal</button>
|
||||||
|
<button class="btn btn-blue" onclick="simpanData('jalan')"><i class="fa-solid fa-save"></i> Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL FORM PARSIL -->
|
||||||
|
<div class="overlay" id="ovl-parsil">
|
||||||
|
<div class="modal">
|
||||||
|
<h2><i class="fa-solid fa-draw-polygon"></i> Data Parsil Tanah</h2>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Nama Pemilik</label>
|
||||||
|
<input type="text" id="p-nama" placeholder="Nama pemilik tanah">
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Status Kepemilikan</label>
|
||||||
|
<select id="p-status">
|
||||||
|
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
|
||||||
|
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
|
||||||
|
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
|
||||||
|
<option value="HP">Sertifikat Hak Pakai (HP)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Luas (Otomatis)</label>
|
||||||
|
<input type="text" id="p-luas" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-gray" onclick="batalSimpan('parsil')">Batal</button>
|
||||||
|
<button class="btn btn-green" onclick="simpanData('parsil')"><i class="fa-solid fa-save"></i> Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://unpkg.com/@geoman-io/leaflet-geoman-free@latest/dist/leaflet-geoman.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/@turf/turf/turf.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API = 'api.php';
|
||||||
|
let map;
|
||||||
|
let layerJalan = L.layerGroup();
|
||||||
|
let layerParsil = L.layerGroup();
|
||||||
|
|
||||||
|
// State
|
||||||
|
let tempLayer = null;
|
||||||
|
let tempGeoJSON = null;
|
||||||
|
let activeId = null;
|
||||||
|
|
||||||
|
// Config warna
|
||||||
|
const warnaJalan = {
|
||||||
|
'Nasional': '#d32f2f', // Merah
|
||||||
|
'Provinsi': '#1976d2', // Biru
|
||||||
|
'Kabupaten': '#388e3c' // Hijau
|
||||||
|
};
|
||||||
|
const warnaParsil = {
|
||||||
|
'SHM': '#27ae60', // Hijau
|
||||||
|
'HGB': '#2980b9', // Biru
|
||||||
|
'HGU': '#e67e22', // Orange
|
||||||
|
'HP': '#8e44ad' // Ungu
|
||||||
|
};
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
map = L.map('map').setView([-0.0380, 109.3440], 13);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
layerJalan.addTo(map);
|
||||||
|
layerParsil.addTo(map);
|
||||||
|
|
||||||
|
// Inisialisasi Leaflet-Geoman (Drawing toolbar)
|
||||||
|
map.pm.addControls({
|
||||||
|
position: 'topright',
|
||||||
|
drawMarker: false,
|
||||||
|
drawCircleMarker: false,
|
||||||
|
drawCircle: false,
|
||||||
|
drawRectangle: false,
|
||||||
|
drawText: false,
|
||||||
|
drawPolygon: true, // Untuk Parsil
|
||||||
|
drawLine: true, // Untuk Jalan
|
||||||
|
editMode: true,
|
||||||
|
dragMode: true,
|
||||||
|
cutPolygon: false,
|
||||||
|
removalMode: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Translate geoman
|
||||||
|
map.pm.setLang('id');
|
||||||
|
|
||||||
|
// Event saat selesai menggambar
|
||||||
|
map.on('pm:create', (e) => {
|
||||||
|
const layer = e.layer;
|
||||||
|
const shape = e.shape;
|
||||||
|
const geojson = layer.toGeoJSON().geometry;
|
||||||
|
|
||||||
|
tempLayer = layer;
|
||||||
|
tempGeoJSON = geojson;
|
||||||
|
activeId = null;
|
||||||
|
|
||||||
|
if (shape === 'Line') {
|
||||||
|
// Hitung panjang dengan turf
|
||||||
|
const length = turf.length(geojson, {units: 'meters'});
|
||||||
|
document.getElementById('j-nama').value = '';
|
||||||
|
document.getElementById('j-status').value = 'Nasional';
|
||||||
|
document.getElementById('j-panjang').value = length.toFixed(2);
|
||||||
|
document.getElementById('ovl-jalan').classList.add('on');
|
||||||
|
} else if (shape === 'Polygon') {
|
||||||
|
// Hitung luas dengan turf
|
||||||
|
const area = turf.area(geojson); // in square meters
|
||||||
|
document.getElementById('p-nama').value = '';
|
||||||
|
document.getElementById('p-status').value = 'SHM';
|
||||||
|
document.getElementById('p-luas').value = area.toFixed(2);
|
||||||
|
document.getElementById('ovl-parsil').classList.add('on');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event saat menghapus via Geoman toolbar
|
||||||
|
map.on('pm:remove', async (e) => {
|
||||||
|
const layer = e.layer;
|
||||||
|
if (layer.feature && layer.feature.properties) {
|
||||||
|
const props = layer.feature.properties;
|
||||||
|
const res = await fetch(`${API}?resource=${props.type}&action=delete&id=${props.id}`, { method: 'DELETE' });
|
||||||
|
if(res.ok) {
|
||||||
|
toast('Data berhasil dihapus dari database.', 'ok');
|
||||||
|
muatData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event saat mengedit (drag/vertex)
|
||||||
|
map.on('pm:globaleditmodetoggled', (e) => {
|
||||||
|
if(!e.enabled) {
|
||||||
|
// Selesai edit, kita harus simpan perubahan yang terjadi.
|
||||||
|
// Cara paling mudah: tambahkan event layer.on('pm:update') ke setiap layer saat diload
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function muatData() {
|
||||||
|
try {
|
||||||
|
// Muat Jalan
|
||||||
|
const resJ = await fetch(`${API}?resource=jalan&action=list`);
|
||||||
|
const dataJ = await resJ.json();
|
||||||
|
layerJalan.clearLayers();
|
||||||
|
let htmlJ = '';
|
||||||
|
|
||||||
|
dataJ.forEach(j => {
|
||||||
|
if(!j.geom) return;
|
||||||
|
const l = L.geoJSON(j.geom, {
|
||||||
|
style: {
|
||||||
|
color: warnaJalan[j.status] || '#333',
|
||||||
|
weight: 5,
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind properties for geoman removal/update
|
||||||
|
l.eachLayer(lyr => {
|
||||||
|
lyr.feature = { properties: { type: 'jalan', id: j.id, nama: j.nama, status: j.status, panjang: j.panjang } };
|
||||||
|
lyr.bindPopup(`<b>${j.nama}</b><br>Status: ${j.status}<br>Panjang: ${j.panjang} m`);
|
||||||
|
|
||||||
|
lyr.on('pm:update', async (e) => {
|
||||||
|
updateGeom('jalan', j.id, j.nama, j.status, e.layer.toGeoJSON().geometry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
l.addTo(layerJalan);
|
||||||
|
|
||||||
|
htmlJ += `
|
||||||
|
<div class="list-item">
|
||||||
|
<h4>${j.nama}</h4>
|
||||||
|
<div class="attr"><i class="fa-solid fa-tag"></i> ${j.status}</div>
|
||||||
|
<div class="attr"><i class="fa-solid fa-ruler"></i> ${j.panjang} m</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('list-jalan').innerHTML = htmlJ || '<div class="empty">Belum ada data jalan.</div>';
|
||||||
|
|
||||||
|
// Muat Parsil
|
||||||
|
const resP = await fetch(`${API}?resource=parsil&action=list`);
|
||||||
|
const dataP = await resP.json();
|
||||||
|
layerParsil.clearLayers();
|
||||||
|
let htmlP = '';
|
||||||
|
|
||||||
|
dataP.forEach(p => {
|
||||||
|
if(!p.geom) return;
|
||||||
|
const l = L.geoJSON(p.geom, {
|
||||||
|
style: {
|
||||||
|
color: warnaParsil[p.status] || '#333',
|
||||||
|
fillColor: warnaParsil[p.status] || '#333',
|
||||||
|
fillOpacity: 0.4,
|
||||||
|
weight: 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
l.eachLayer(lyr => {
|
||||||
|
lyr.feature = { properties: { type: 'parsil', id: p.id, nama: p.nama_pemilik, status: p.status, luas: p.luas } };
|
||||||
|
lyr.bindPopup(`<b>Pemilik: ${p.nama_pemilik}</b><br>Status: ${p.status}<br>Luas: ${p.luas} m²`);
|
||||||
|
|
||||||
|
lyr.on('pm:update', async (e) => {
|
||||||
|
updateGeom('parsil', p.id, p.nama_pemilik, p.status, e.layer.toGeoJSON().geometry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
l.addTo(layerParsil);
|
||||||
|
|
||||||
|
htmlP += `
|
||||||
|
<div class="list-item">
|
||||||
|
<h4>${p.nama_pemilik}</h4>
|
||||||
|
<div class="attr"><i class="fa-solid fa-file-signature"></i> ${p.status}</div>
|
||||||
|
<div class="attr"><i class="fa-solid fa-maximize"></i> ${p.luas} m²</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('list-parsil').innerHTML = htmlP || '<div class="empty">Belum ada data parsil.</div>';
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGeom(type, id, nama, status, geojson) {
|
||||||
|
let payload = {};
|
||||||
|
if(type === 'jalan') {
|
||||||
|
const len = turf.length(geojson, {units: 'meters'});
|
||||||
|
payload = { nama, status, panjang: len.toFixed(2), geom: geojson };
|
||||||
|
} else {
|
||||||
|
const area = turf.area(geojson);
|
||||||
|
payload = { nama_pemilik: nama, status, luas: area.toFixed(2), geom: geojson };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}?resource=${type}&action=update&id=${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if(res.ok) {
|
||||||
|
toast('Geometri diperbarui!', 'ok');
|
||||||
|
muatData();
|
||||||
|
}
|
||||||
|
} catch(e) { toast('Gagal update', 'err'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simpanData(type) {
|
||||||
|
let payload = {};
|
||||||
|
if (type === 'jalan') {
|
||||||
|
const nama = document.getElementById('j-nama').value.trim();
|
||||||
|
const status = document.getElementById('j-status').value;
|
||||||
|
const panjang = document.getElementById('j-panjang').value;
|
||||||
|
if(!nama) return alert('Nama wajib diisi');
|
||||||
|
payload = { nama, status, panjang, geom: tempGeoJSON };
|
||||||
|
} else {
|
||||||
|
const nama = document.getElementById('p-nama').value.trim();
|
||||||
|
const status = document.getElementById('p-status').value;
|
||||||
|
const luas = document.getElementById('p-luas').value;
|
||||||
|
if(!nama) return alert('Nama pemilik wajib diisi');
|
||||||
|
payload = { nama_pemilik: nama, status, luas, geom: tempGeoJSON };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}?resource=${type}&action=create`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if(res.ok) {
|
||||||
|
toast('Berhasil disimpan!', 'ok');
|
||||||
|
document.getElementById(`ovl-${type}`).classList.remove('on');
|
||||||
|
if (tempLayer) { map.removeLayer(tempLayer); tempLayer = null; }
|
||||||
|
muatData();
|
||||||
|
} else {
|
||||||
|
toast(json.error || 'Gagal menyimpan', 'err');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
toast('Network error', 'err');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function batalSimpan(type) {
|
||||||
|
document.getElementById(`ovl-${type}`).classList.remove('on');
|
||||||
|
if (tempLayer) {
|
||||||
|
map.removeLayer(tempLayer);
|
||||||
|
tempLayer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
document.getElementById('sidebar').classList.toggle('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
function gantiTab(tab) {
|
||||||
|
document.getElementById('tab-jalan').classList.remove('active');
|
||||||
|
document.getElementById('tab-parsil').classList.remove('active');
|
||||||
|
document.getElementById('content-jalan').style.display = 'none';
|
||||||
|
document.getElementById('content-parsil').style.display = 'none';
|
||||||
|
|
||||||
|
document.getElementById(`tab-${tab}`).classList.add('active');
|
||||||
|
document.getElementById(`content-${tab}`).style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
let toastT;
|
||||||
|
function toast(msg, tipe = '') {
|
||||||
|
const el = document.getElementById('toast');
|
||||||
|
el.textContent = msg; el.className = 'on ' + tipe;
|
||||||
|
clearTimeout(toastT);
|
||||||
|
toastT = setTimeout(() => el.className = '', 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = () => {
|
||||||
|
initMap();
|
||||||
|
muatData();
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+469
@@ -0,0 +1,469 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGIS SPBU</title>
|
||||||
|
|
||||||
|
<!-- Leaflet CSS -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Menggunakan style dasar yang sama dengan map_bantuan */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Segoe UI', Arial, sans-serif; height: 100vh; display: flex; flex-direction: column; background: #f0f2f5; }
|
||||||
|
header { background: linear-gradient(135deg, #e08020, #f59e42); color: #fff; padding: 10px 18px; display: flex; align-items: center; gap: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, .3); z-index: 1000; flex-shrink: 0; justify-content: space-between;}
|
||||||
|
header .title-area { display: flex; align-items: center; gap: 12px; }
|
||||||
|
header h1 { font-size: 1rem; font-weight: 700; }
|
||||||
|
header small { font-size: .7rem; opacity: .85; }
|
||||||
|
.back-btn { color: white; text-decoration: none; font-size: 0.9rem; border: 1px solid rgba(255,255,255,0.5); padding: 5px 10px; border-radius: 5px; transition: background 0.2s;}
|
||||||
|
.back-btn:hover { background: rgba(255,255,255,0.2); }
|
||||||
|
|
||||||
|
.app-body { display: flex; flex: 1; overflow: hidden; position: relative; }
|
||||||
|
|
||||||
|
#sidebar { width: 280px; background: #fff; display: flex; flex-direction: column; border-right: 1px solid #dde; z-index: 900; transition: transform .3s; flex-shrink: 0; }
|
||||||
|
#sidebar.hide { transform: translateX(-100%); position: absolute; top: 0; left: 0; height: 100%; box-shadow: 4px 0 12px rgba(0, 0, 0, .2); }
|
||||||
|
.sb-head { background: #e08020; color: #fff; padding: 10px 14px; font-weight: 700; font-size: .88rem; }
|
||||||
|
|
||||||
|
.filter-box { padding: 10px 14px; border-bottom: 1px solid #eee; background: #fff3e0; }
|
||||||
|
.filter-box label { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; font-weight: 600; color: #e08020; cursor: pointer; }
|
||||||
|
.filter-box input[type="checkbox"] { accent-color: #e08020; width: 16px; height: 16px; cursor: pointer;}
|
||||||
|
|
||||||
|
.btn-row { padding: 10px 14px; border-bottom: 1px solid #eee; }
|
||||||
|
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 8px 12px; border: none; border-radius: 6px; cursor: pointer; font-size: .8rem; font-weight: 700; transition: filter .15s; width: 100%; color: white;}
|
||||||
|
.btn:hover { filter: brightness(.9); }
|
||||||
|
.btn-orange { background: #e08020; }
|
||||||
|
.btn-green { background: #1a6b3c; }
|
||||||
|
.btn-gray { background: #e0e0e0; color: #333; }
|
||||||
|
.btn-red { background: #d93025; }
|
||||||
|
|
||||||
|
.sb-search { padding: 7px 14px; border-bottom: 1px solid #eee; }
|
||||||
|
.sb-search input { width: 100%; padding: 7px 10px; border: 1px solid #ccc; border-radius: 6px; font-size: .82rem; }
|
||||||
|
|
||||||
|
#sb-list { overflow-y: auto; flex: 1; }
|
||||||
|
.list-item { padding: 10px 14px; border-bottom: 1px solid #f0f0f0; cursor: pointer; display: flex; align-items: center; gap: 9px; transition: background .15s; }
|
||||||
|
.list-item:hover { background: #fff8f0; }
|
||||||
|
.list-item .ico { font-size: 1.3rem; width: 26px; text-align: center; }
|
||||||
|
.list-item .inf { flex: 1; }
|
||||||
|
.list-item .inf .nm { font-weight: 700; font-size: .84rem; }
|
||||||
|
.list-item .inf .jn { font-size: .7rem; color: #777; margin-top: 2px;}
|
||||||
|
.badge { font-size: .65rem; padding: 2px 6px; border-radius: 10px; font-weight: 700; white-space: nowrap; }
|
||||||
|
.badge.green { background: #e8f5e9; color: #1a6b3c; }
|
||||||
|
.badge.red { background: #fdecea; color: #d93025; }
|
||||||
|
.empty { padding: 16px; text-align: center; color: #bbb; font-size: .82rem; }
|
||||||
|
|
||||||
|
#map { flex: 1; z-index: 1; }
|
||||||
|
#btn-toggle { position: absolute; top: 10px; left: 0; z-index: 950; background: #e08020; color: #fff; border: none; padding: 8px 7px; border-radius: 0 6px 6px 0; cursor: pointer; font-size: 1rem; box-shadow: 2px 0 6px rgba(0, 0, 0, .25); }
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.overlay { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, .5); z-index: 2000; align-items: center; justify-content: center; }
|
||||||
|
.overlay.on { display: flex; }
|
||||||
|
.modal { background: #fff; border-radius: 12px; width: 92%; max-width: 450px; max-height: 92vh; overflow-y: auto; padding: 22px; box-shadow: 0 8px 32px rgba(0, 0, 0, .25); }
|
||||||
|
.modal h2 { font-size: 1rem; color: #e08020; margin-bottom: 14px; border-bottom: 2px solid #fff3e0; padding-bottom: 8px; }
|
||||||
|
.fg { margin-bottom: 12px; }
|
||||||
|
.fg label { display: block; font-size: .78rem; font-weight: 700; color: #444; margin-bottom: 4px; }
|
||||||
|
.fg input, .fg select { width: 100%; padding: 8px 11px; border: 1px solid #ccc; border-radius: 6px; font-size: .84rem; }
|
||||||
|
.fg input:focus, .fg select:focus { border-color: #e08020; outline: none; box-shadow: 0 0 0 3px rgba(224, 128, 32, .1); }
|
||||||
|
.fg-row { display: flex; gap: 8px; }
|
||||||
|
.fg-row .fg { flex: 1; }
|
||||||
|
.fg-hint { font-size: .7rem; color: #aaa; margin-top: 3px; }
|
||||||
|
.err { color: #d93025; font-size: .75rem; margin-top: 3px; display: none; }
|
||||||
|
.err.on { display: block; }
|
||||||
|
.modal-footer { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
|
||||||
|
.btn-petaclick { background: #fff3e0; color: #e08020; border: 1px dashed #e08020; width: 100%; margin-top: 4px; }
|
||||||
|
.btn-petaclick.active { background: #e08020; color: #fff; }
|
||||||
|
|
||||||
|
#toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: #323232; color: #fff; padding: 9px 20px; border-radius: 8px; font-size: .84rem; z-index: 3000; opacity: 0; transition: opacity .3s; pointer-events: none; }
|
||||||
|
#toast.on { opacity: 1; }
|
||||||
|
#toast.ok { background: #1a6b3c; }
|
||||||
|
#toast.err { background: #d93025; }
|
||||||
|
|
||||||
|
.legenda { background: rgba(255, 255, 255, .95); padding: 9px 13px; border-radius: 8px; font-size: .77rem; box-shadow: 0 2px 8px rgba(0, 0, 0, .15); }
|
||||||
|
.legenda h4 { font-size: .78rem; margin-bottom: 5px; }
|
||||||
|
.leg-row { display: flex; align-items: center; gap: 7px; margin-top: 3px; }
|
||||||
|
.dot { width: 13px; height: 13px; border-radius: 50%; display: inline-block; border: 2px solid rgba(0, 0, 0, .15); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="title-area">
|
||||||
|
<i class="fa-solid fa-gas-pump fa-lg"></i>
|
||||||
|
<div>
|
||||||
|
<h1>WebGIS Lokasi SPBU</h1>
|
||||||
|
<small>Informasi Operasional & Pemetaan SPBU</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="index.html" class="back-btn"><i class="fa-solid fa-arrow-left"></i> Kembali ke Dashboard</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="app-body">
|
||||||
|
<div id="sidebar">
|
||||||
|
<div class="sb-head"><i class="fa-solid fa-list-ul"></i> Daftar SPBU</div>
|
||||||
|
|
||||||
|
<div class="filter-box">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="filter-24j" onchange="loadSPBU()">
|
||||||
|
Tampilkan hanya SPBU 24 Jam
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn btn-orange" onclick="bukaForm()">
|
||||||
|
<i class="fa-solid fa-plus"></i> Tambah SPBU
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sb-search">
|
||||||
|
<input type="text" id="cari-input" placeholder="🔍 Cari SPBU..." oninput="filterList(this.value)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sb-list">
|
||||||
|
<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<button id="btn-toggle" onclick="toggleSidebar()"><i class="fa-solid fa-bars"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Form -->
|
||||||
|
<div class="overlay" id="ovl-form">
|
||||||
|
<div class="modal">
|
||||||
|
<h2 id="form-title"><i class="fa-solid fa-gas-pump"></i> Tambah SPBU</h2>
|
||||||
|
<input type="hidden" id="f-id">
|
||||||
|
|
||||||
|
<div class="fg">
|
||||||
|
<label>Nama SPBU <span style="color:red">*</span></label>
|
||||||
|
<input type="text" id="f-nama" placeholder="Contoh: SPBU Pertamina 14.213.11">
|
||||||
|
<div class="err" id="err-nama">Nama wajib diisi.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fg-row">
|
||||||
|
<div class="fg">
|
||||||
|
<label>Buka 24 Jam?</label>
|
||||||
|
<select id="f-buka">
|
||||||
|
<option value="1">Ya, 24 Jam</option>
|
||||||
|
<option value="0" selected>Tidak</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Kontak / Telp</label>
|
||||||
|
<input type="text" id="f-kontak" placeholder="0812...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fg-row">
|
||||||
|
<div class="fg">
|
||||||
|
<label>Latitude <span style="color:red">*</span></label>
|
||||||
|
<input type="number" id="f-lat" step="any" placeholder="-0.0354">
|
||||||
|
</div>
|
||||||
|
<div class="fg">
|
||||||
|
<label>Longitude <span style="color:red">*</span></label>
|
||||||
|
<input type="number" id="f-lng" step="any" placeholder="109.3424">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="err" id="err-coord">Koordinat tidak valid.</div>
|
||||||
|
|
||||||
|
<button class="btn btn-petaclick" id="btn-pc" onclick="aktifKlik()">
|
||||||
|
<i class="fa-solid fa-map-pin"></i> Pilih Koordinat dari Peta
|
||||||
|
</button>
|
||||||
|
<p class="fg-hint">Tutup form lalu klik langsung di peta untuk memilih koordinat.</p>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-gray" style="width:auto" onclick="tutupForm()">Batal</button>
|
||||||
|
<button class="btn btn-orange" style="width:auto" onclick="simpan()"><i class="fa-solid fa-floppy-disk"></i> Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast">Notifikasi</div>
|
||||||
|
|
||||||
|
<!-- Leaflet JS -->
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script>
|
||||||
|
const API_URL = 'api.php?resource=spbu';
|
||||||
|
let map, markersLayer;
|
||||||
|
let modeKlik = false;
|
||||||
|
let dataGlobal = [];
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
const iconGreen = L.icon({
|
||||||
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
||||||
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconRed = L.icon({
|
||||||
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
||||||
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
||||||
|
});
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
// Default center Pontianak (menyesuaikan koordinat map_bantuan)
|
||||||
|
map = L.map('map').setView([-0.0380, 109.3440], 13);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
markersLayer = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
|
map.on('click', (e) => {
|
||||||
|
if (!modeKlik) return;
|
||||||
|
document.getElementById('f-lat').value = e.latlng.lat.toFixed(6);
|
||||||
|
document.getElementById('f-lng').value = e.latlng.lng.toFixed(6);
|
||||||
|
|
||||||
|
// Matikan mode klik
|
||||||
|
modeKlik = false;
|
||||||
|
document.getElementById('btn-pc').classList.remove('active');
|
||||||
|
document.getElementById('btn-pc').innerHTML = '<i class="fa-solid fa-map-pin"></i> Pilih Koordinat dari Peta';
|
||||||
|
|
||||||
|
// Buka form lagi
|
||||||
|
document.getElementById('ovl-form').classList.add('on');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legenda
|
||||||
|
const legend = L.control({ position: 'bottomright' });
|
||||||
|
legend.onAdd = function () {
|
||||||
|
const div = L.DomUtil.create('div', 'legenda');
|
||||||
|
div.innerHTML = `
|
||||||
|
<h4>Legenda SPBU</h4>
|
||||||
|
<div class="leg-row"><div class="dot" style="background:#28a745"></div> 24 Jam</div>
|
||||||
|
<div class="leg-row"><div class="dot" style="background:#dc3545"></div> Tidak 24 Jam</div>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
legend.addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSPBU() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}&action=list`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Gagal memuat data');
|
||||||
|
dataGlobal = data;
|
||||||
|
renderData();
|
||||||
|
} catch (e) {
|
||||||
|
showToast(e.message, true);
|
||||||
|
document.getElementById('sb-list').innerHTML = `<div class="empty">Gagal memuat data.</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderData() {
|
||||||
|
markersLayer.clearLayers();
|
||||||
|
const list = document.getElementById('sb-list');
|
||||||
|
const q = document.getElementById('cari-input').value.toLowerCase();
|
||||||
|
const filter24 = document.getElementById('filter-24j').checked;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
dataGlobal.forEach(d => {
|
||||||
|
// Filter 24 Jam
|
||||||
|
if (filter24 && d.buka_24_jam != 1) return;
|
||||||
|
// Filter Pencarian
|
||||||
|
if (q && !d.nama.toLowerCase().includes(q)) return;
|
||||||
|
|
||||||
|
count++;
|
||||||
|
const is24 = d.buka_24_jam == 1;
|
||||||
|
|
||||||
|
// List HTML
|
||||||
|
html += `
|
||||||
|
<div class="list-item" onclick="flyTo(${d.latitude}, ${d.longitude})">
|
||||||
|
<div class="ico" style="color: ${is24 ? '#1a6b3c' : '#d93025'}"><i class="fa-solid fa-gas-pump"></i></div>
|
||||||
|
<div class="inf">
|
||||||
|
<div class="nm">${d.nama}</div>
|
||||||
|
<div class="jn"><i class="fa-solid fa-phone"></i> ${d.kontak || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="badge ${is24 ? 'green' : 'red'}">${is24 ? '24 Jam' : 'Non-24 Jam'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Map Marker (Make it draggable)
|
||||||
|
const marker = L.marker([d.latitude, d.longitude], {
|
||||||
|
icon: is24 ? iconGreen : iconRed,
|
||||||
|
draggable: true // FITUR DRAG AND DROP
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.bindPopup(`
|
||||||
|
<div style="font-family:'Segoe UI',Arial; min-width:200px">
|
||||||
|
<h4 style="margin:0 0 5px; color:#e08020; font-size:14px;"><i class="fa-solid fa-gas-pump"></i> ${d.nama}</h4>
|
||||||
|
<div style="font-size:12px; margin-bottom:10px;">
|
||||||
|
<span class="badge ${is24 ? 'green' : 'red'}" style="display:inline-block; margin-bottom:5px;">${is24 ? '24 Jam' : 'Non-24 Jam'}</span><br>
|
||||||
|
<i class="fa-solid fa-phone"></i> Kontak: <b>${d.kontak || '-'}</b>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:5px; padding-top:8px; border-top:1px solid #eee">
|
||||||
|
<button class="btn btn-orange btn-sm" onclick="editData(${d.id})" style="flex:1; padding:4px"><i class="fa-solid fa-pen"></i> Edit</button>
|
||||||
|
<button class="btn btn-red btn-sm" onclick="hapusData(${d.id})" style="padding:4px 8px"><i class="fa-solid fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:10px; color:#999; margin-top:8px; text-align:center;">
|
||||||
|
<i class="fa-solid fa-arrows-up-down-left-right"></i> Drag marker untuk memindahkan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Event ketika marker selesai di-drag
|
||||||
|
marker.on('dragend', function(event) {
|
||||||
|
const newLatLng = event.target.getLatLng();
|
||||||
|
updatePosisiSPBU(d.id, newLatLng.lat, newLatLng.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
markersLayer.addLayer(marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
html = `<div class="empty">Tidak ada SPBU ditemukan.</div>`;
|
||||||
|
}
|
||||||
|
list.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePosisiSPBU(id, lat, lng) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}&action=update&id=${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ latitude: lat, longitude: lng })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error);
|
||||||
|
|
||||||
|
showToast('Posisi SPBU diperbarui.');
|
||||||
|
// Update data lokal
|
||||||
|
const item = dataGlobal.find(x => x.id == id);
|
||||||
|
if (item) {
|
||||||
|
item.latitude = lat;
|
||||||
|
item.longitude = lng;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Gagal memindahkan marker: ' + e.message, true);
|
||||||
|
loadSPBU(); // Kembalikan ke posisi awal jika gagal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterList() { renderData(); }
|
||||||
|
function flyTo(lat, lng) { map.flyTo([lat, lng], 16); }
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
document.getElementById('sidebar').classList.toggle('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FORM LOGIC ---
|
||||||
|
function bukaForm() {
|
||||||
|
document.getElementById('form-title').innerHTML = '<i class="fa-solid fa-gas-pump"></i> Tambah SPBU';
|
||||||
|
document.getElementById('f-id').value = '';
|
||||||
|
document.getElementById('f-nama').value = '';
|
||||||
|
document.getElementById('f-buka').value = '0';
|
||||||
|
document.getElementById('f-kontak').value = '';
|
||||||
|
document.getElementById('f-lat').value = '';
|
||||||
|
document.getElementById('f-lng').value = '';
|
||||||
|
|
||||||
|
document.getElementById('err-nama').classList.remove('on');
|
||||||
|
document.getElementById('err-coord').classList.remove('on');
|
||||||
|
|
||||||
|
document.getElementById('ovl-form').classList.add('on');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tutupForm() {
|
||||||
|
document.getElementById('ovl-form').classList.remove('on');
|
||||||
|
modeKlik = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editData(id) {
|
||||||
|
const d = dataGlobal.find(x => x.id == id);
|
||||||
|
if (!d) return;
|
||||||
|
map.closePopup();
|
||||||
|
|
||||||
|
document.getElementById('form-title').innerHTML = '<i class="fa-solid fa-pen"></i> Edit SPBU';
|
||||||
|
document.getElementById('f-id').value = d.id;
|
||||||
|
document.getElementById('f-nama').value = d.nama;
|
||||||
|
document.getElementById('f-buka').value = d.buka_24_jam;
|
||||||
|
document.getElementById('f-kontak').value = d.kontak;
|
||||||
|
document.getElementById('f-lat').value = d.latitude;
|
||||||
|
document.getElementById('f-lng').value = d.longitude;
|
||||||
|
|
||||||
|
document.getElementById('err-nama').classList.remove('on');
|
||||||
|
document.getElementById('err-coord').classList.remove('on');
|
||||||
|
|
||||||
|
document.getElementById('ovl-form').classList.add('on');
|
||||||
|
}
|
||||||
|
|
||||||
|
function aktifKlik() {
|
||||||
|
modeKlik = true;
|
||||||
|
document.getElementById('btn-pc').classList.add('active');
|
||||||
|
document.getElementById('btn-pc').innerHTML = '<i class="fa-solid fa-crosshairs"></i> Menunggu Klik di Peta...';
|
||||||
|
document.getElementById('ovl-form').classList.remove('on'); // Sembunyikan form sementara
|
||||||
|
showToast('Klik di area peta untuk menentukan koordinat SPBU.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simpan() {
|
||||||
|
const id = document.getElementById('f-id').value;
|
||||||
|
const payload = {
|
||||||
|
nama: document.getElementById('f-nama').value,
|
||||||
|
buka_24_jam: document.getElementById('f-buka').value,
|
||||||
|
kontak: document.getElementById('f-kontak').value,
|
||||||
|
latitude: document.getElementById('f-lat').value,
|
||||||
|
longitude: document.getElementById('f-lng').value
|
||||||
|
};
|
||||||
|
|
||||||
|
let valid = true;
|
||||||
|
if (!payload.nama) { document.getElementById('err-nama').classList.add('on'); valid = false; }
|
||||||
|
else { document.getElementById('err-nama').classList.remove('on'); }
|
||||||
|
|
||||||
|
if (!payload.latitude || !payload.longitude) { document.getElementById('err-coord').classList.add('on'); valid = false; }
|
||||||
|
else { document.getElementById('err-coord').classList.remove('on'); }
|
||||||
|
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
const url = id ? `${API_URL}&action=update&id=${id}` : `${API_URL}&action=create`;
|
||||||
|
const method = id ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error);
|
||||||
|
|
||||||
|
showToast(data.message);
|
||||||
|
tutupForm();
|
||||||
|
loadSPBU();
|
||||||
|
} catch (e) {
|
||||||
|
showToast(e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hapusData(id) {
|
||||||
|
if (!confirm('Hapus SPBU ini?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}&action=delete&id=${id}`, { method: 'DELETE' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error);
|
||||||
|
showToast(data.message);
|
||||||
|
map.closePopup();
|
||||||
|
loadSPBU();
|
||||||
|
} catch (e) {
|
||||||
|
showToast(e.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(msg, isErr = false) {
|
||||||
|
const t = document.getElementById('toast');
|
||||||
|
t.textContent = msg;
|
||||||
|
t.className = 'on ' + (isErr ? 'err' : 'ok');
|
||||||
|
setTimeout(() => t.className = '', 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init
|
||||||
|
initMap();
|
||||||
|
loadSPBU();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user