First commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
-- 1. Membuat database baru (misalnya kita beri nama 'gis_spbu')
|
||||
CREATE DATABASE WEB_GIS;
|
||||
|
||||
-- 2. Memilih database tersebut untuk digunakan
|
||||
USE WEB_GIS;
|
||||
|
||||
-- 3. Membuat tabel di dalam database yang sudah dipilih
|
||||
CREATE TABLE Poin_SPBU (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_spbu VARCHAR(150) NOT NULL,
|
||||
no_wa VARCHAR(20) NOT NULL,
|
||||
buka_24_jam ENUM('Ya', 'Tidak') NOT NULL,
|
||||
latitude DECIMAL(10, 8) NOT NULL,
|
||||
longitude DECIMAL(11, 8) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* WebGIS - Konfigurasi & Koneksi Database
|
||||
*/
|
||||
|
||||
// Pastikan PHP tidak menampilkan error sebagai HTML (merusak output JSON)
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('display_startup_errors', 0);
|
||||
error_reporting(0);
|
||||
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', '');
|
||||
define('DB_NAME', 'WEB_GIS');
|
||||
|
||||
/**
|
||||
* Mengembalikan koneksi MySQLi.
|
||||
* Jika koneksi gagal, langsung output JSON error dan exit.
|
||||
*/
|
||||
function getConnection()
|
||||
{
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'gagal',
|
||||
'pesan' => 'Koneksi database gagal: ' . $conn->connect_error
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn->set_charset('utf8mb4');
|
||||
return $conn;
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
/**
|
||||
* WebGIS - API Manajemen Data Jalan
|
||||
*
|
||||
* Endpoint : /api/jalan.php
|
||||
* Tabel : tb_jalan
|
||||
* Geometri : GeoJSON LineString
|
||||
*/
|
||||
|
||||
// Matikan display_errors agar PHP tidak output HTML error (merusak JSON)
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('display_startup_errors', 0);
|
||||
error_reporting(0);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
/* Handle preflight request */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
$conn = getConnection();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
try {
|
||||
|
||||
switch ($method) {
|
||||
|
||||
/* ============================================================
|
||||
READ — Ambil semua atau satu data jalan
|
||||
============================================================ */
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
/* Satu data */
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT id, nama_jalan, status_jalan, panjang_jalan, geojson, created_at
|
||||
FROM tb_jalan WHERE id = ?"
|
||||
);
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
if ($row) {
|
||||
$row['geojson'] = json_decode($row['geojson']); // parse ke objek
|
||||
echo json_encode($row);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Data jalan tidak ditemukan']);
|
||||
}
|
||||
|
||||
} else {
|
||||
/* Semua data */
|
||||
$filter = isset($_GET['status']) ? $_GET['status'] : null;
|
||||
|
||||
if ($filter) {
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT id, nama_jalan, status_jalan, panjang_jalan, geojson, created_at
|
||||
FROM tb_jalan WHERE status_jalan = ? ORDER BY created_at DESC"
|
||||
);
|
||||
$stmt->bind_param('s', $filter);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
} else {
|
||||
$result = $conn->query(
|
||||
"SELECT id, nama_jalan, status_jalan, panjang_jalan, geojson, created_at
|
||||
FROM tb_jalan ORDER BY created_at DESC"
|
||||
);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode($data);
|
||||
}
|
||||
break;
|
||||
|
||||
/* ============================================================
|
||||
CREATE — Simpan data jalan baru
|
||||
Body (JSON):
|
||||
{
|
||||
"nama_jalan" : "Jalan Ahmad Yani",
|
||||
"status_jalan" : "Nasional", // Nasional|Provinsi|Kabupaten
|
||||
"panjang_jalan" : 2345.67, // meter, dihitung oleh LeafletJS
|
||||
"geojson" : { // GeoJSON LineString
|
||||
"type": "LineString",
|
||||
"coordinates": [[lng,lat], ...]
|
||||
}
|
||||
}
|
||||
============================================================ */
|
||||
case 'POST':
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$body) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Body request tidak valid atau bukan JSON']);
|
||||
break;
|
||||
}
|
||||
|
||||
$nama_jalan = trim($body['nama_jalan'] ?? '');
|
||||
$status_jalan = trim($body['status_jalan'] ?? '');
|
||||
$panjang_jalan = (float)($body['panjang_jalan'] ?? 0);
|
||||
$geojson_raw = $body['geojson'] ?? null;
|
||||
|
||||
/* Validasi */
|
||||
$valid_status = ['Nasional', 'Provinsi', 'Kabupaten'];
|
||||
if (!$nama_jalan) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'nama_jalan tidak boleh kosong']);
|
||||
break;
|
||||
}
|
||||
if (!in_array($status_jalan, $valid_status)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'status_jalan tidak valid. Pilih: Nasional, Provinsi, atau Kabupaten']);
|
||||
break;
|
||||
}
|
||||
if (!$geojson_raw || ($geojson_raw['type'] ?? '') !== 'LineString') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'geojson harus bertipe LineString']);
|
||||
break;
|
||||
}
|
||||
|
||||
$geojson_str = json_encode($geojson_raw);
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO tb_jalan (nama_jalan, status_jalan, panjang_jalan, geojson)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('ssds', $nama_jalan, $status_jalan, $panjang_jalan, $geojson_str);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'sukses',
|
||||
'id' => $conn->insert_id,
|
||||
'pesan' => 'Data jalan berhasil disimpan'
|
||||
]);
|
||||
} else {
|
||||
throw new Exception($stmt->error);
|
||||
}
|
||||
break;
|
||||
|
||||
/* ============================================================
|
||||
UPDATE — Perbarui atribut dan/atau koordinat jalan
|
||||
Mengirim field yang ingin diubah saja:
|
||||
- Edit atribut : { "nama_jalan": "...", "status_jalan": "..." }
|
||||
- Geser vertex : { "geojson": {...}, "panjang_jalan": 1234.5 }
|
||||
- Keduanya : { semua field }
|
||||
============================================================ */
|
||||
case 'PUT':
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Parameter id diperlukan']);
|
||||
break;
|
||||
}
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$body) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Body tidak valid']);
|
||||
break;
|
||||
}
|
||||
|
||||
/* Bangun query dinamis berdasarkan field yang dikirim */
|
||||
$setClauses = [];
|
||||
$paramVals = [];
|
||||
$paramTypes = '';
|
||||
|
||||
if (array_key_exists('nama_jalan', $body)) {
|
||||
$v = trim($body['nama_jalan']);
|
||||
if (!$v) { http_response_code(400); echo json_encode(['status'=>'gagal','pesan'=>'nama_jalan tidak boleh kosong']); break; }
|
||||
$setClauses[] = 'nama_jalan = ?'; $paramVals[] = $v; $paramTypes .= 's';
|
||||
}
|
||||
if (array_key_exists('status_jalan', $body)) {
|
||||
$valid = ['Nasional','Provinsi','Kabupaten'];
|
||||
if (!in_array($body['status_jalan'], $valid)) {
|
||||
http_response_code(400); echo json_encode(['status'=>'gagal','pesan'=>'status_jalan tidak valid']); break;
|
||||
}
|
||||
$setClauses[] = 'status_jalan = ?'; $paramVals[] = $body['status_jalan']; $paramTypes .= 's';
|
||||
}
|
||||
if (array_key_exists('panjang_jalan', $body)) {
|
||||
$setClauses[] = 'panjang_jalan = ?'; $paramVals[] = (float)$body['panjang_jalan']; $paramTypes .= 'd';
|
||||
}
|
||||
if (array_key_exists('geojson', $body)) {
|
||||
$setClauses[] = 'geojson = ?'; $paramVals[] = json_encode($body['geojson']); $paramTypes .= 's';
|
||||
}
|
||||
|
||||
if (empty($setClauses)) {
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Tidak ada field yang dikirim untuk diupdate']);
|
||||
break;
|
||||
}
|
||||
|
||||
$paramVals[] = $id;
|
||||
$paramTypes .= 'i';
|
||||
|
||||
$sql = 'UPDATE tb_jalan SET ' . implode(', ', $setClauses) . ' WHERE id = ?';
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param($paramTypes, ...$paramVals);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'sukses', 'pesan' => 'Data jalan berhasil diperbarui']);
|
||||
} else {
|
||||
throw new Exception($stmt->error);
|
||||
}
|
||||
break;
|
||||
|
||||
/* ============================================================
|
||||
DELETE — Hapus data jalan
|
||||
============================================================ */
|
||||
case 'DELETE':
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Parameter id diperlukan']);
|
||||
break;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM tb_jalan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute() && $stmt->affected_rows > 0) {
|
||||
echo json_encode(['status' => 'sukses', 'pesan' => 'Data jalan berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Data tidak ditemukan atau sudah dihapus']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Method tidak didukung']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Server error: ' . $e->getMessage()]);
|
||||
} finally {
|
||||
if ($conn) $conn->close();
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
/**
|
||||
* WebGIS - API Manajemen Parsil Tanah / Kavling
|
||||
*
|
||||
* Endpoint : /api/parsil.php
|
||||
* Tabel : tb_parsil
|
||||
* Geometri : GeoJSON Polygon
|
||||
*/
|
||||
|
||||
// Matikan display_errors agar PHP tidak output HTML error (merusak JSON)
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('display_startup_errors', 0);
|
||||
error_reporting(0);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
/* Handle preflight request */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
$conn = getConnection();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
try {
|
||||
|
||||
switch ($method) {
|
||||
|
||||
/* ============================================================
|
||||
READ — Ambil semua atau satu data parsil
|
||||
============================================================ */
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
/* Satu data */
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT id, nama_parsil, status_kepemilikan, luas_tanah, geojson, created_at
|
||||
FROM tb_parsil WHERE id = ?"
|
||||
);
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
if ($row) {
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
echo json_encode($row);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Data parsil tidak ditemukan']);
|
||||
}
|
||||
|
||||
} else {
|
||||
/* Semua data */
|
||||
$filter = isset($_GET['status']) ? $_GET['status'] : null;
|
||||
|
||||
if ($filter) {
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT id, nama_parsil, status_kepemilikan, luas_tanah, geojson, created_at
|
||||
FROM tb_parsil WHERE status_kepemilikan = ? ORDER BY created_at DESC"
|
||||
);
|
||||
$stmt->bind_param('s', $filter);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
} else {
|
||||
$result = $conn->query(
|
||||
"SELECT id, nama_parsil, status_kepemilikan, luas_tanah, geojson, created_at
|
||||
FROM tb_parsil ORDER BY created_at DESC"
|
||||
);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode($data);
|
||||
}
|
||||
break;
|
||||
|
||||
/* ============================================================
|
||||
CREATE — Simpan data parsil baru
|
||||
Body (JSON):
|
||||
{
|
||||
"nama_parsil" : "Kavling A-01",
|
||||
"status_kepemilikan" : "SHM", // SHM|HGB|HGU|HP
|
||||
"luas_tanah" : 1250.5000, // m², dihitung LeafletJS
|
||||
"geojson" : { // GeoJSON Polygon
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[lng,lat], ...]]
|
||||
}
|
||||
}
|
||||
============================================================ */
|
||||
case 'POST':
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$body) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Body request tidak valid atau bukan JSON']);
|
||||
break;
|
||||
}
|
||||
|
||||
$nama_parsil = trim($body['nama_parsil'] ?? '');
|
||||
$status_kepemilikan = trim($body['status_kepemilikan'] ?? '');
|
||||
$luas_tanah = (float)($body['luas_tanah'] ?? 0);
|
||||
$geojson_raw = $body['geojson'] ?? null;
|
||||
|
||||
/* Validasi */
|
||||
$valid_status = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
if (!$nama_parsil) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'nama_parsil tidak boleh kosong']);
|
||||
break;
|
||||
}
|
||||
if (!in_array($status_kepemilikan, $valid_status)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'status_kepemilikan tidak valid. Pilih: SHM, HGB, HGU, atau HP']);
|
||||
break;
|
||||
}
|
||||
if (!$geojson_raw || ($geojson_raw['type'] ?? '') !== 'Polygon') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'geojson harus bertipe Polygon']);
|
||||
break;
|
||||
}
|
||||
|
||||
$geojson_str = json_encode($geojson_raw);
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO tb_parsil (nama_parsil, status_kepemilikan, luas_tanah, geojson)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('ssds', $nama_parsil, $status_kepemilikan, $luas_tanah, $geojson_str);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'sukses',
|
||||
'id' => $conn->insert_id,
|
||||
'pesan' => 'Data parsil berhasil disimpan'
|
||||
]);
|
||||
} else {
|
||||
throw new Exception($stmt->error);
|
||||
}
|
||||
break;
|
||||
|
||||
/* ============================================================
|
||||
UPDATE — Perbarui atribut dan/atau koordinat parsil
|
||||
Mengirim field yang ingin diubah saja:
|
||||
- Edit atribut : { "nama_parsil": "...", "status_kepemilikan": "..." }
|
||||
- Geser vertex : { "geojson": {...}, "luas_tanah": 1234.5 }
|
||||
- Keduanya : { semua field }
|
||||
============================================================ */
|
||||
case 'PUT':
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Parameter id diperlukan']);
|
||||
break;
|
||||
}
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$body) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Body tidak valid']);
|
||||
break;
|
||||
}
|
||||
|
||||
/* Bangun query dinamis berdasarkan field yang dikirim */
|
||||
$setClauses = [];
|
||||
$paramVals = [];
|
||||
$paramTypes = '';
|
||||
|
||||
if (array_key_exists('nama_parsil', $body)) {
|
||||
$v = trim($body['nama_parsil']);
|
||||
if (!$v) { http_response_code(400); echo json_encode(['status'=>'gagal','pesan'=>'nama_parsil tidak boleh kosong']); break; }
|
||||
$setClauses[] = 'nama_parsil = ?'; $paramVals[] = $v; $paramTypes .= 's';
|
||||
}
|
||||
if (array_key_exists('status_kepemilikan', $body)) {
|
||||
$valid = ['SHM','HGB','HGU','HP'];
|
||||
if (!in_array($body['status_kepemilikan'], $valid)) {
|
||||
http_response_code(400); echo json_encode(['status'=>'gagal','pesan'=>'status_kepemilikan tidak valid']); break;
|
||||
}
|
||||
$setClauses[] = 'status_kepemilikan = ?'; $paramVals[] = $body['status_kepemilikan']; $paramTypes .= 's';
|
||||
}
|
||||
if (array_key_exists('luas_tanah', $body)) {
|
||||
$setClauses[] = 'luas_tanah = ?'; $paramVals[] = (float)$body['luas_tanah']; $paramTypes .= 'd';
|
||||
}
|
||||
if (array_key_exists('geojson', $body)) {
|
||||
$setClauses[] = 'geojson = ?'; $paramVals[] = json_encode($body['geojson']); $paramTypes .= 's';
|
||||
}
|
||||
|
||||
if (empty($setClauses)) {
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Tidak ada field yang dikirim untuk diupdate']);
|
||||
break;
|
||||
}
|
||||
|
||||
$paramVals[] = $id;
|
||||
$paramTypes .= 'i';
|
||||
|
||||
$sql = 'UPDATE tb_parsil SET ' . implode(', ', $setClauses) . ' WHERE id = ?';
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param($paramTypes, ...$paramVals);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'sukses', 'pesan' => 'Data parsil berhasil diperbarui']);
|
||||
} else {
|
||||
throw new Exception($stmt->error);
|
||||
}
|
||||
break;
|
||||
|
||||
/* ============================================================
|
||||
DELETE — Hapus data parsil
|
||||
============================================================ */
|
||||
case 'DELETE':
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Parameter id diperlukan']);
|
||||
break;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM tb_parsil WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute() && $stmt->affected_rows > 0) {
|
||||
echo json_encode(['status' => 'sukses', 'pesan' => 'Data parsil berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Data tidak ditemukan atau sudah dihapus']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Method tidak didukung']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'gagal', 'pesan' => 'Server error: ' . $e->getMessage()]);
|
||||
} finally {
|
||||
if ($conn) $conn->close();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import re
|
||||
|
||||
# 1. Read 'pertemuan fix/index.html'
|
||||
with open(r"D:\laragon\www\WebGIS\pertemuan fix\index.html", "r", encoding="utf-8") as f:
|
||||
ref_content = f.read()
|
||||
|
||||
# Extract CSS
|
||||
css_match = re.search(r'<style>(.*?)</style>', ref_content, re.DOTALL)
|
||||
css_content = css_match.group(1) if css_match else ""
|
||||
|
||||
# Replace specific variables in CSS for the new context
|
||||
css_content = css_content.replace('--kemiskinan', '--jalan')
|
||||
css_content = css_content.replace('--masjid', '--parsil')
|
||||
|
||||
# We'll save the CSS as style.css
|
||||
with open(r"D:\laragon\www\WebGIS\WebGIS\style.css", "w", encoding="utf-8") as f:
|
||||
f.write(css_content)
|
||||
|
||||
print("Extracted style.css")
|
||||
@@ -0,0 +1,974 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS — Manajemen Jalan & Parsil Tanah</title>
|
||||
<meta name="description" content="Sistem Informasi Geografis untuk manajemen data jalan dan parsil tanah. Berbasis LeafletJS, PHP, dan MySQL.">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Link to the extracted premium styling -->
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
/* CSS Tambahan khusus untuk Jalan dan Parsil */
|
||||
:root {
|
||||
--jalan: #4f46e5;
|
||||
--parsil: #10b981;
|
||||
}
|
||||
.db-btn {
|
||||
width: 36px; height: 36px; border-radius: 9px;
|
||||
border: 1.5px solid rgba(255,255,255,0.07);
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: #64748b; cursor: pointer; font-size: 15px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: all 0.2s; position: relative;
|
||||
}
|
||||
.db-btn:hover { background: rgba(99,102,241,0.25); border-color: rgba(99,102,241,0.5); color: #fff; }
|
||||
.db-btn.active { background: linear-gradient(135deg,#4f46e5,#7c3aed); border-color: #a78bfa; color: #fff; box-shadow: 0 0 14px rgba(79,70,229,0.5); }
|
||||
.db-sep { height: 1px; background: rgba(255,255,255,0.06); }
|
||||
|
||||
#draw-bar {
|
||||
position: absolute; top: 50%; right: 12px;
|
||||
transform: translateY(-50%); z-index: 1000;
|
||||
display: flex; flex-direction: column; gap: 5px;
|
||||
background: rgba(13,21,38,0.93);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 14px; padding: 9px 7px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.45);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header-bar">
|
||||
<div class="header-title">
|
||||
<span style="font-size: 20px;">🗺️</span>
|
||||
<h1>WebGIS Manajemen Jalan & Parsil</h1>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div class="status-indicator">
|
||||
<div class="pulse-dot"></div>
|
||||
Sistem Online
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app-container">
|
||||
<div id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>🗺️ Sidebar Panel</h1>
|
||||
<p>Filter & Ringkasan Statistik</p>
|
||||
</div>
|
||||
<div class="sidebar-body">
|
||||
<div class="layer-info-box">
|
||||
<p>💡 Gunakan <strong>Layers Control</strong> di pojok kanan atas peta untuk menampilkan/menyembunyikan layer berdasarkan jenis datanya.</p>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- JALAN STATS -->
|
||||
<div class="section-title" style="color:var(--jalan)">🛣️ Data Jalan</div>
|
||||
<div class="count-row" style="flex-direction:column; gap:6px;">
|
||||
<div class="count-card" style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid var(--jalan); padding:8px 12px">
|
||||
<div style="text-align:left"><strong style="color:var(--jalan); font-size:12px;">Total Ruas</strong></div>
|
||||
<div class="num" id="sj-total" style="font-size:16px; color:var(--text-primary)">0</div>
|
||||
</div>
|
||||
<div class="count-card" style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid var(--jalan); padding:8px 12px">
|
||||
<div style="text-align:left"><strong style="color:var(--jalan); font-size:12px;">Panjang Keseluruhan</strong></div>
|
||||
<div class="num" id="sj-panjang" style="font-size:14px; color:var(--text-primary)">0 m</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-list" id="list-jalan">
|
||||
<div class="loading-text">⏳ Memuat data...</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- PARSIL STATS -->
|
||||
<div class="section-title" style="color:var(--parsil)">🏘️ Data Parsil Tanah</div>
|
||||
<div class="count-row" style="flex-direction:column; gap:6px;">
|
||||
<div class="count-card" style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid var(--parsil); padding:8px 12px">
|
||||
<div style="text-align:left"><strong style="color:var(--parsil); font-size:12px;">Total Bidang</strong></div>
|
||||
<div class="num" id="sp-total" style="font-size:16px; color:var(--text-primary)">0</div>
|
||||
</div>
|
||||
<div class="count-card" style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid var(--parsil); padding:8px 12px">
|
||||
<div style="text-align:left"><strong style="color:var(--parsil); font-size:12px;">Luas Keseluruhan</strong></div>
|
||||
<div class="num" id="sp-luas" style="font-size:14px; color:var(--text-primary)">0 m²</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-list" id="list-parsil">
|
||||
<div class="loading-text">⏳ Memuat data...</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<div class="legend-box">
|
||||
<div class="legend-title">Legenda Status Jalan</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#ef4444;flex-shrink:0"></div>Nasional</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#3b82f6;flex-shrink:0"></div>Provinsi</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#f59e0b;flex-shrink:0"></div>Kabupaten</div>
|
||||
</div>
|
||||
<div class="legend-box" style="border-top:none; padding-top:0;">
|
||||
<div class="legend-title">Legenda Status Parsil</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#22c55e;flex-shrink:0"></div>SHM</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#06b6d4;flex-shrink:0"></div>HGB</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#a855f7;flex-shrink:0"></div>HGU</div>
|
||||
<div class="legend-item"><div style="width:14px;height:14px;border-radius:50%;background:#f97316;flex-shrink:0"></div>HP</div>
|
||||
</div>
|
||||
</div> <!-- End sidebar-body -->
|
||||
</div> <!-- End sidebar -->
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Drawing toolbar -->
|
||||
<div id="draw-bar" style="display:none;">
|
||||
<button class="db-btn" id="db-undo" onclick="undoPt()" title="Undo (Z)">↩️</button>
|
||||
<div class="db-sep" id="db-sep1"></div>
|
||||
<button class="db-btn" id="db-finish" onclick="finishDraw()" title="Selesai (Enter)">✅</button>
|
||||
<div class="db-sep" id="db-sep2"></div>
|
||||
<button class="db-btn" id="db-cancel" onclick="cancelDraw()" title="Batal (Esc)">❌</button>
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<div id="status-bar" style="display:none; position:absolute; bottom:22px; left:50%; transform:translateX(-50%); z-index:1000; background:rgba(13,21,38,0.93); backdrop-filter:blur(12px); color:#e2e8f0; font-size:11px; font-weight:500; padding:7px 16px; border-radius:30px; border:1px solid rgba(255,255,255,0.08); box-shadow:0 4px 20px rgba(0,0,0,0.35); align-items:center; gap:8px;">
|
||||
<span style="width:7px; height:7px; border-radius:50%; background:#818cf8;"></span>
|
||||
<span id="sb-txt"></span>
|
||||
</div>
|
||||
|
||||
<!-- FAB Container -->
|
||||
<div class="fab-container">
|
||||
<div class="fab-menu" id="fab-menu">
|
||||
<div class="fab-item" onclick="toggleDraw('jalan'); document.getElementById('fab-menu').classList.remove('active')">
|
||||
🛣️ Tambah Jalan
|
||||
</div>
|
||||
<div class="fab-item" onclick="toggleDraw('parsil'); document.getElementById('fab-menu').classList.remove('active')">
|
||||
🏘️ Tambah Parsil
|
||||
</div>
|
||||
</div>
|
||||
<button class="fab-btn" id="fab-main" onclick="document.getElementById('fab-menu').classList.toggle('active')">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div> <!-- End app-container -->
|
||||
|
||||
<!-- Modal Form -->
|
||||
<div id="modal-bg" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:9999; align-items:center; justify-content:center;">
|
||||
<div id="modal" style="background:var(--bg-card); width:400px; max-width:90%; border-radius:12px; border:1px solid var(--border); box-shadow:0 10px 30px rgba(0,0,0,0.5); overflow:hidden; display:flex; flex-direction:column;">
|
||||
<div style="padding:16px 20px; border-bottom:1px solid var(--border); background:rgba(255,255,255,0.02);">
|
||||
<h3 id="modal-title" style="margin:0; font-size:16px; color:var(--text-primary);">Title</h3>
|
||||
<p id="modal-sub" style="margin:4px 0 0; font-size:11px; color:var(--text-secondary);">Subtitle</p>
|
||||
</div>
|
||||
<div style="padding:20px; display:flex; flex-direction:column; gap:16px;" class="popup-form">
|
||||
<div>
|
||||
<label>Nama</label>
|
||||
<input type="text" id="inp-nama" placeholder="Masukkan nama...">
|
||||
</div>
|
||||
<div>
|
||||
<label>Status</label>
|
||||
<select id="inp-status"></select>
|
||||
</div>
|
||||
<div id="grp-dim">
|
||||
<label id="inp-dim-lbl">Dimensi</label>
|
||||
<input type="text" id="inp-dim" readonly style="background:rgba(255,255,255,0.05); color:var(--text-muted); cursor:not-allowed;">
|
||||
<div id="inp-dim-note" style="font-size:9px; color:var(--text-muted); margin-top:4px;">Dihitung otomatis</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:16px 20px; border-top:1px solid var(--border); display:flex; justify-content:flex-end; gap:10px; background:rgba(0,0,0,0.2);">
|
||||
<button onclick="closeModal()" class="popup-btn popup-btn-cancel" style="max-width:100px;">Batal</button>
|
||||
<button onclick="saveModal()" id="btn-save" class="popup-btn popup-btn-save" style="max-width:120px;">💾 Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
KONSTANTA
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
const API_JALAN = './api/jalan.php';
|
||||
const API_PARSIL = './api/parsil.php';
|
||||
|
||||
const JALAN_COLOR = { Nasional:'#ef4444', Provinsi:'#3b82f6', Kabupaten:'#f59e0b' };
|
||||
const PARSIL_COLOR = { SHM:'#22c55e', HGB:'#06b6d4', HGU:'#a855f7', HP:'#f97316' };
|
||||
|
||||
const JALAN_STATUS_OPT = [['Nasional','🔴 Jalan Nasional'],['Provinsi','🔵 Jalan Provinsi'],['Kabupaten','🟡 Jalan Kabupaten']];
|
||||
const PARSIL_STATUS_OPT = [['SHM','🟢 Sertifikat Hak Milik (SHM)'],['HGB','🔵 Hak Guna Bangunan (HGB)'],['HGU','🟣 Hak Guna Usaha (HGU)'],['HP','🟠 Hak Pakai (HP)']];
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
STATE
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
let drawMode = null; // 'jalan' | 'parsil' | null
|
||||
let drawingPts = []; // L.LatLng[]
|
||||
let tempMkrs = [];
|
||||
let tempPoly = null;
|
||||
|
||||
let modalType = null; // 'jalan' | 'parsil'
|
||||
let modalMode = 'add'; // 'add' | 'edit'
|
||||
let modalEditId = null; // id data yang sedang diedit
|
||||
let pendingPts = null;
|
||||
let pendingDim = null;
|
||||
|
||||
let jalanData = [];
|
||||
let parsilData = [];
|
||||
let jalanLyrs = {}; // id → { polyline, vertices, label }
|
||||
let parsilLyrs = {}; // id → { polygon, vertices, label }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
INISIALISASI PETA
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
const map = L.map('map', { zoomControl: true }).setView([-0.055319, 109.349502], 13);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
KALKULASI GEOSPASIAL — MENGGUNAKAN LEAFLET JS
|
||||
|
||||
1. hitungPanjangJalan : Menghitung panjang total polyline jalan
|
||||
dalam meter menggunakan metode L.LatLng.distanceTo() dari
|
||||
library LeafletJS. Metode ini menggunakan formula Haversine
|
||||
yang sudah terintegrasi di LeafletJS.
|
||||
|
||||
2. hitungLuasParsil : Menghitung luas area poligon parsil
|
||||
dalam meter persegi menggunakan formula geodesic spherical
|
||||
area, setara dengan implementasi internal LeafletJS yang
|
||||
digunakan pada L.GeometryUtil.geodesicArea().
|
||||
Berbasis objek L.LatLng dari LeafletJS.
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Panjang total jalan (meter).
|
||||
* Menggunakan L.LatLng.distanceTo() — fungsi bawaan LeafletJS.
|
||||
* @param {L.LatLng[]} pts
|
||||
* @returns {number}
|
||||
*/
|
||||
function hitungPanjangJalan(pts) {
|
||||
let total = 0;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
// L.latLng.distanceTo() → Haversine distance (meter)
|
||||
total += L.latLng(pts[i]).distanceTo(L.latLng(pts[i + 1]));
|
||||
}
|
||||
return parseFloat(total.toFixed(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Luas parsil tanah (meter persegi).
|
||||
* Menggunakan formula geodesic spherical area berbasis L.LatLng LeafletJS.
|
||||
* Formula ini setara dengan L.GeometryUtil.geodesicArea() dari ekosistem LeafletJS.
|
||||
* @param {L.LatLng[]} pts
|
||||
* @returns {number}
|
||||
*/
|
||||
function hitungLuasParsil(pts) {
|
||||
const R = 6378137; // radius bumi WGS-84 (meter) — sama dengan yang digunakan LeafletJS
|
||||
let area = 0;
|
||||
const n = pts.length;
|
||||
for (let i = 0, j = n - 1; i < n; j = i++) {
|
||||
const pi = L.latLng(pts[i]);
|
||||
const pj = L.latLng(pts[j]);
|
||||
// Formula geodesic spherical (sumber: Leaflet source & L.GeometryUtil)
|
||||
area += (pj.lng - pi.lng) * (Math.PI / 180) *
|
||||
(2 + Math.sin(pi.lat * Math.PI / 180) + Math.sin(pj.lat * Math.PI / 180));
|
||||
}
|
||||
return parseFloat(Math.abs(area * R * R / 2).toFixed(4));
|
||||
}
|
||||
|
||||
/* ── Format helpers ─────────────────────────── */
|
||||
function fmtPanjang(m) {
|
||||
return m >= 1000 ? (m / 1000).toFixed(2) + ' km' : m.toFixed(0) + ' m';
|
||||
}
|
||||
function fmtLuas(m2) {
|
||||
return m2 >= 10000 ? (m2 / 10000).toFixed(4) + ' ha' : m2.toFixed(2) + ' m²';
|
||||
}
|
||||
|
||||
/* ── GeoJSON ↔ LeafletJS conversion ────────── */
|
||||
// GeoJSON LineString: coordinates = [[lng,lat],...]
|
||||
// GeoJSON Polygon: coordinates = [[[lng,lat],...,[lng,lat]]]
|
||||
// Leaflet: L.LatLng(lat, lng)
|
||||
|
||||
function toGeoJSONLine(lls) {
|
||||
return { type:'LineString', coordinates: lls.map(ll => [ll.lng, ll.lat]) };
|
||||
}
|
||||
function toGeoJSONPolygon(lls) {
|
||||
const ring = lls.map(ll => [ll.lng, ll.lat]);
|
||||
ring.push(ring[0]); // tutup ring
|
||||
return { type:'Polygon', coordinates: [ring] };
|
||||
}
|
||||
function fromGeoJSONLine(gj) {
|
||||
return gj.coordinates.map(c => L.latLng(c[1], c[0]));
|
||||
}
|
||||
function fromGeoJSONPolygon(gj) {
|
||||
// Ambil ring pertama, buang titik penutup
|
||||
return gj.coordinates[0].slice(0, -1).map(c => L.latLng(c[1], c[0]));
|
||||
}
|
||||
function centroid(lls) {
|
||||
return L.latLng(
|
||||
lls.reduce((s,p) => s + p.lat, 0) / lls.length,
|
||||
lls.reduce((s,p) => s + p.lng, 0) / lls.length
|
||||
);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
TAB
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
document.getElementById('tbtn-' + tab).classList.add('active');
|
||||
document.getElementById('tp-' + tab).classList.add('active');
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
DRAWING
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
function toggleDraw(type) {
|
||||
if (drawMode === type) { cancelDraw(); return; }
|
||||
if (drawMode) cancelDraw();
|
||||
drawMode = type;
|
||||
drawingPts = [];
|
||||
map.getContainer().style.cursor = 'crosshair';
|
||||
|
||||
// Show draw controls
|
||||
document.getElementById('draw-bar').style.display = 'flex';
|
||||
|
||||
setStatus(type === 'jalan'
|
||||
? '🛣️ Klik peta untuk menambah titik jalan | Enter = selesai'
|
||||
: '🏘️ Klik peta untuk menambah titik parsil | Enter = selesai'
|
||||
);
|
||||
}
|
||||
|
||||
function cancelDraw() {
|
||||
clearTemp();
|
||||
drawMode = null;
|
||||
map.getContainer().style.cursor = '';
|
||||
|
||||
// Hide draw controls
|
||||
document.getElementById('draw-bar').style.display = 'none';
|
||||
|
||||
clearStatus();
|
||||
}
|
||||
|
||||
function clearTemp() {
|
||||
drawingPts = [];
|
||||
tempMkrs.forEach(m => map.removeLayer(m));
|
||||
tempMkrs = [];
|
||||
if (tempPoly) { map.removeLayer(tempPoly); tempPoly = null; }
|
||||
}
|
||||
|
||||
/* Map click → add vertex */
|
||||
map.on('click', function(e) {
|
||||
if (!drawMode) return;
|
||||
drawingPts.push(e.latlng);
|
||||
|
||||
const m = L.circleMarker(e.latlng, {
|
||||
radius:5, color:'#fff', fillColor:'#818cf8', fillOpacity:1, weight:2
|
||||
}).addTo(map);
|
||||
tempMkrs.push(m);
|
||||
|
||||
refreshTempPoly();
|
||||
updateDrawStatus();
|
||||
});
|
||||
|
||||
function refreshTempPoly() {
|
||||
if (tempPoly) map.removeLayer(tempPoly);
|
||||
if (drawingPts.length < 2) return;
|
||||
const pts = drawMode === 'parsil' ? [...drawingPts, drawingPts[0]] : drawingPts;
|
||||
tempPoly = L.polyline(pts, { color:'#818cf8', weight:3, dashArray:'8,5', opacity:.85 }).addTo(map);
|
||||
}
|
||||
|
||||
function updateDrawStatus() {
|
||||
if (!drawMode || !drawingPts.length) return;
|
||||
let extra = '';
|
||||
if (drawMode === 'jalan' && drawingPts.length >= 2)
|
||||
extra = ` | Panjang: ${fmtPanjang(hitungPanjangJalan(drawingPts))}`;
|
||||
if (drawMode === 'parsil' && drawingPts.length >= 3)
|
||||
extra = ` | Luas: ${fmtLuas(hitungLuasParsil(drawingPts))}`;
|
||||
setStatus(`${drawMode==='jalan'?'🛣️':'🏘️'} ${drawingPts.length} titik${extra} | Enter = selesai`);
|
||||
}
|
||||
|
||||
function undoPt() {
|
||||
if (!drawingPts.length) return;
|
||||
drawingPts.pop();
|
||||
const m = tempMkrs.pop();
|
||||
if (m) map.removeLayer(m);
|
||||
refreshTempPoly();
|
||||
updateDrawStatus();
|
||||
}
|
||||
|
||||
function finishDraw() {
|
||||
if (!drawMode) return;
|
||||
if (drawMode === 'jalan' && drawingPts.length < 2) { alert('⚠ Minimal 2 titik untuk jalan!'); return; }
|
||||
if (drawMode === 'parsil' && drawingPts.length < 3) { alert('⚠ Minimal 3 titik untuk parsil!'); return; }
|
||||
|
||||
const pts = drawingPts.slice();
|
||||
const type = drawMode;
|
||||
clearTemp();
|
||||
cancelDraw();
|
||||
|
||||
let dim, dimLabel, dimStr, dimNote;
|
||||
if (type === 'jalan') {
|
||||
dim = hitungPanjangJalan(pts);
|
||||
dimLabel = 'Panjang Jalan';
|
||||
dimStr = fmtPanjang(dim) + ' (' + dim.toFixed(2) + ' m)';
|
||||
dimNote = 'Dihitung otomatis: L.LatLng.distanceTo() dari LeafletJS';
|
||||
} else {
|
||||
dim = hitungLuasParsil(pts);
|
||||
dimLabel = 'Luas Tanah';
|
||||
dimStr = fmtLuas(dim) + ' (' + dim.toFixed(4) + ' m²)';
|
||||
dimNote = 'Dihitung otomatis: geodesic spherical area (LeafletJS)';
|
||||
}
|
||||
|
||||
pendingPts = pts;
|
||||
pendingDim = dim;
|
||||
openModal(type, dimLabel, dimStr, dimNote);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
MODAL
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
/* ── Helper: isi opsi select status ────────── */
|
||||
function isiStatusOpts(type, selectedVal) {
|
||||
const sel = document.getElementById('inp-status');
|
||||
sel.innerHTML = '';
|
||||
const opts = type === 'jalan' ? JALAN_STATUS_OPT : PARSIL_STATUS_OPT;
|
||||
opts.forEach(([v, l]) => {
|
||||
const o = document.createElement('option');
|
||||
o.value = v; o.textContent = l;
|
||||
if (v === selectedVal) o.selected = true;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Buka modal TAMBAH (setelah selesai gambar) ── */
|
||||
function openModal(type, dimLbl, dimStr, dimNote) {
|
||||
modalType = type; modalMode = 'add'; modalEditId = null;
|
||||
|
||||
document.getElementById('modal-title').textContent = type === 'jalan'
|
||||
? '🛣️ Simpan Data Jalan' : '🏘️ Simpan Data Parsil Tanah';
|
||||
document.getElementById('modal-sub').textContent = type === 'jalan'
|
||||
? 'Lengkapi atribut ruas jalan baru' : 'Lengkapi atribut parsil/kavling baru';
|
||||
|
||||
isiStatusOpts(type, null);
|
||||
|
||||
document.getElementById('inp-dim-lbl').textContent = dimLbl;
|
||||
document.getElementById('inp-dim').value = dimStr;
|
||||
document.getElementById('inp-dim-note').textContent = dimNote;
|
||||
document.getElementById('inp-nama').value = '';
|
||||
document.getElementById('grp-dim').style.display = '';
|
||||
document.getElementById('btn-save').textContent = '💾 Simpan';
|
||||
|
||||
document.getElementById('modal-bg').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('inp-nama').focus(), 60);
|
||||
}
|
||||
|
||||
/* ── Buka modal EDIT atribut ─────────────────── */
|
||||
function openEditModal(type, id) {
|
||||
map.closePopup();
|
||||
let item;
|
||||
if (type === 'jalan') {
|
||||
item = jalanData.find(j => j.id == id);
|
||||
} else {
|
||||
item = parsilData.find(p => p.id == id);
|
||||
}
|
||||
if (!item) return;
|
||||
|
||||
modalType = type; modalMode = 'edit'; modalEditId = id;
|
||||
|
||||
const isJalan = (type === 'jalan');
|
||||
document.getElementById('modal-title').textContent = isJalan
|
||||
? '✏️ Edit Data Jalan' : '✏️ Edit Data Parsil Tanah';
|
||||
document.getElementById('modal-sub').textContent = isJalan
|
||||
? 'Ubah nama atau status jalan' : 'Ubah nama atau status kepemilikan parsil';
|
||||
|
||||
// Pre-fill form
|
||||
document.getElementById('inp-nama').value = isJalan ? item.nama_jalan : item.nama_parsil;
|
||||
isiStatusOpts(type, isJalan ? item.status_jalan : item.status_kepemilikan);
|
||||
|
||||
// Tampilkan info dimensi (readonly, tidak bisa diubah di sini)
|
||||
if (isJalan) {
|
||||
document.getElementById('inp-dim-lbl').textContent = 'Panjang Jalan (tidak berubah)';
|
||||
document.getElementById('inp-dim').value = fmtPanjang(parseFloat(item.panjang_jalan)) + ' (' + parseFloat(item.panjang_jalan).toFixed(2) + ' m)';
|
||||
document.getElementById('inp-dim-note').textContent = 'Geser titik pada peta untuk mengubah panjang jalan';
|
||||
} else {
|
||||
document.getElementById('inp-dim-lbl').textContent = 'Luas Tanah (tidak berubah)';
|
||||
document.getElementById('inp-dim').value = fmtLuas(parseFloat(item.luas_tanah)) + ' (' + parseFloat(item.luas_tanah).toFixed(4) + ' m²)';
|
||||
document.getElementById('inp-dim-note').textContent = 'Geser titik sudut pada peta untuk mengubah luas';
|
||||
}
|
||||
document.getElementById('grp-dim').style.display = '';
|
||||
document.getElementById('btn-save').textContent = '✏️ Simpan Perubahan';
|
||||
|
||||
document.getElementById('modal-bg').style.display = 'flex';
|
||||
setTimeout(() => document.getElementById('inp-nama').focus(), 60);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal-bg').style.display = 'none';
|
||||
modalType = null; modalMode = 'add'; modalEditId = null;
|
||||
pendingPts = null; pendingDim = null;
|
||||
}
|
||||
|
||||
function handleBgClick(e) { if (e.target === document.getElementById('modal-bg')) closeModal(); }
|
||||
|
||||
async function saveModal() {
|
||||
const nama = document.getElementById('inp-nama').value.trim();
|
||||
const status = document.getElementById('inp-status').value;
|
||||
if (!nama) { alert('Nama tidak boleh kosong!'); document.getElementById('inp-nama').focus(); return; }
|
||||
|
||||
const btn = document.getElementById('btn-save');
|
||||
const origLabel = btn.textContent;
|
||||
btn.textContent = '⏳ Menyimpan...'; btn.disabled = true;
|
||||
|
||||
try {
|
||||
/* ── MODE EDIT (update atribut saja) ── */
|
||||
if (modalMode === 'edit') {
|
||||
if (modalType === 'jalan') {
|
||||
const res = await fetch(API_JALAN + '?id=' + modalEditId, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ nama_jalan: nama, status_jalan: status })
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!text) throw new Error("Server mengembalikan respons kosong. Pastikan Anda menjalankan aplikasi ini melalui server PHP (Laragon/XAMPP), BUKAN Live Server.");
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (data.status === 'sukses') {
|
||||
// Update data lokal
|
||||
const idx = jalanData.findIndex(j => j.id == modalEditId);
|
||||
if (idx >= 0) { jalanData[idx].nama_jalan = nama; jalanData[idx].status_jalan = status; }
|
||||
closeModal();
|
||||
// Re-render layer di peta (warna & label bisa berubah)
|
||||
const l = jalanLyrs[modalEditId];
|
||||
if (l) {
|
||||
const newColor = JALAN_COLOR[status] || '#6366f1';
|
||||
l.polyline.setStyle({ color: newColor });
|
||||
l.label.setContent(nama);
|
||||
l.vertices.forEach(v => v.setStyle({ fillColor: newColor }));
|
||||
l.polyline.bindPopup(buatPopupJalan(jalanData[idx], null, l.polyline, l.label));
|
||||
}
|
||||
renderListJalan();
|
||||
} else { alert('Gagal: ' + data.pesan); }
|
||||
} else {
|
||||
const res = await fetch(API_PARSIL + '?id=' + modalEditId, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ nama_parsil: nama, status_kepemilikan: status })
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!text) throw new Error("Server mengembalikan respons kosong. Pastikan Anda menjalankan aplikasi ini melalui server PHP (Laragon/XAMPP), BUKAN Live Server.");
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (data.status === 'sukses') {
|
||||
const idx = parsilData.findIndex(p => p.id == modalEditId);
|
||||
if (idx >= 0) { parsilData[idx].nama_parsil = nama; parsilData[idx].status_kepemilikan = status; }
|
||||
closeModal();
|
||||
const l = parsilLyrs[modalEditId];
|
||||
if (l) {
|
||||
const newColor = PARSIL_COLOR[status] || '#6366f1';
|
||||
l.polygon.setStyle({ color: newColor, fillColor: newColor });
|
||||
l.label.setContent(nama);
|
||||
l.vertices.forEach(v => v.setStyle({ fillColor: newColor }));
|
||||
l.polygon.bindPopup(buatPopupParsil(parsilData[idx], null, l.polygon, l.label));
|
||||
}
|
||||
renderListParsil();
|
||||
} else { alert('Gagal: ' + data.pesan); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── MODE TAMBAH (create baru) ── */
|
||||
if (modalType === 'jalan') {
|
||||
const res = await fetch(API_JALAN, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({
|
||||
nama_jalan: nama, status_jalan: status,
|
||||
panjang_jalan: pendingDim, geojson: toGeoJSONLine(pendingPts)
|
||||
})
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!text) throw new Error("Server mengembalikan respons kosong. Pastikan Anda menjalankan aplikasi ini melalui server PHP (Laragon/XAMPP), BUKAN Live Server.");
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (data.status === 'sukses') {
|
||||
closeModal(); await muatJalan();
|
||||
const lyr = jalanLyrs[data.id];
|
||||
if (lyr) map.fitBounds(lyr.polyline.getBounds(), {padding:[50,50]});
|
||||
} else { alert('Gagal: ' + data.pesan); }
|
||||
} else {
|
||||
const res = await fetch(API_PARSIL, {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({
|
||||
nama_parsil: nama, status_kepemilikan: status,
|
||||
luas_tanah: pendingDim, geojson: toGeoJSONPolygon(pendingPts)
|
||||
})
|
||||
});
|
||||
const text = await res.text();
|
||||
if (!text) throw new Error("Server mengembalikan respons kosong. Pastikan Anda menjalankan aplikasi ini melalui server PHP (Laragon/XAMPP), BUKAN Live Server.");
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (data.status === 'sukses') {
|
||||
closeModal(); await muatParsil();
|
||||
const lyr = parsilLyrs[data.id];
|
||||
if (lyr) map.fitBounds(lyr.polygon.getBounds(), {padding:[50,50]});
|
||||
} else { alert('Gagal: ' + data.pesan); }
|
||||
}
|
||||
} catch(err) { alert('Error: ' + err.message); }
|
||||
finally { btn.textContent = origLabel; btn.disabled = false; }
|
||||
}
|
||||
|
||||
/* Shortcut global untuk tombol edit di popup & sidebar */
|
||||
window.editJalan = (id) => openEditModal('jalan', id);
|
||||
window.editParsil = (id) => openEditModal('parsil', id);
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
JALAN — CRUD
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
async function muatJalan() {
|
||||
try { const r = await fetch(API_JALAN); jalanData = await r.json(); }
|
||||
catch(e) { jalanData = []; console.warn('API Jalan:', e); }
|
||||
|
||||
// Hapus layer lama
|
||||
Object.values(jalanLyrs).forEach(l => {
|
||||
map.removeLayer(l.polyline);
|
||||
l.vertices.forEach(v => map.removeLayer(v));
|
||||
if (l.label) map.removeLayer(l.label);
|
||||
});
|
||||
jalanLyrs = {};
|
||||
|
||||
jalanData.forEach(j => renderJalan(j));
|
||||
renderListJalan();
|
||||
updateStatsJalan();
|
||||
}
|
||||
|
||||
function renderJalan(jalan) {
|
||||
const lls = fromGeoJSONLine(jalan.geojson);
|
||||
const color = JALAN_COLOR[jalan.status_jalan] || '#6366f1';
|
||||
|
||||
const polyline = L.polyline(lls, { color, weight:5, opacity:.9, lineJoin:'round' }).addTo(map);
|
||||
|
||||
// Label nama di tengah
|
||||
const midPt = lls[Math.floor(lls.length / 2)];
|
||||
const label = L.tooltip({ permanent:true, direction:'top', className:'map-label', offset:[0,-4] })
|
||||
.setContent(jalan.nama_jalan).setLatLng(midPt).addTo(map);
|
||||
|
||||
// Popup
|
||||
function mkPopup() { return buatPopupJalan(jalan, lls, polyline, label); }
|
||||
polyline.bindPopup(mkPopup());
|
||||
|
||||
// Vertex drag
|
||||
const vertices = [];
|
||||
lls.forEach((pt, idx) => {
|
||||
const vm = L.circleMarker(pt, { radius:5, color:'#fff', fillColor:color, fillOpacity:1, weight:2 }).addTo(map);
|
||||
vm.on('mousedown', e => {
|
||||
map.dragging.disable(); L.DomEvent.stopPropagation(e);
|
||||
function onMove(ev) {
|
||||
vm.setLatLng(ev.latlng); lls[idx] = ev.latlng;
|
||||
polyline.setLatLngs(lls);
|
||||
label.setLatLng(lls[Math.floor(lls.length/2)]);
|
||||
}
|
||||
function onUp() {
|
||||
map.dragging.enable();
|
||||
map.off('mousemove', onMove); map.off('mouseup', onUp);
|
||||
const newP = hitungPanjangJalan(lls);
|
||||
updateJalanAPI(jalan.id, lls, newP);
|
||||
polyline.bindPopup(buatPopupJalan({...jalan, panjang_jalan:newP}, lls, polyline, label));
|
||||
}
|
||||
map.on('mousemove', onMove); map.on('mouseup', onUp);
|
||||
});
|
||||
vertices.push(vm);
|
||||
});
|
||||
|
||||
jalanLyrs[jalan.id] = { polyline, vertices, label };
|
||||
}
|
||||
|
||||
function buatPopupJalan(j, lls, polyline, label) {
|
||||
const color = JALAN_COLOR[j.status_jalan];
|
||||
const pjg = lls ? hitungPanjangJalan(lls) : parseFloat(j.panjang_jalan);
|
||||
return `
|
||||
<div class="gis-popup">
|
||||
<div class="popup-title" style="color:${color}">🛣️ ${j.nama_jalan}</div>
|
||||
<div class="info-row"><span class="info-label">Status</span><span class="info-value"><span style="background:${color}20;color:${color};padding:2px 6px;border-radius:4px;font-size:10px;font-weight:bold">${j.status_jalan}</span></span></div>
|
||||
<div class="info-row"><span class="info-label">Panjang</span><span class="info-value">📐 ${fmtPanjang(pjg)}</span></div>
|
||||
<div class="popup-form">
|
||||
<div style="font-size:9px; color:var(--text-muted); font-style:italic; margin-top:8px;">Seret titik pada jalan untuk ubah koordinat</div>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button class="popup-btn popup-btn-edit" onclick="window.editJalan(${j.id})">✏️ Edit</button>
|
||||
<button class="popup-btn popup-btn-delete" onclick="window.hapusJalan(${j.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderListJalan() {
|
||||
// Note: We don't have fil-jalan dropdown in the new sidebar yet, if needed we can add it later.
|
||||
// Assuming no filter for now.
|
||||
const data = jalanData;
|
||||
const el = document.getElementById('list-jalan');
|
||||
|
||||
if (!data.length) {
|
||||
el.innerHTML = `<div class="loading-text">Tidak ada data jalan.</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.map(j => {
|
||||
const c = JALAN_COLOR[j.status_jalan]; const bg = c+'22';
|
||||
return `<div class="item-card" onclick="zoomJalan(${j.id})">
|
||||
<div class="item-card-header">
|
||||
<div class="item-card-name">${j.nama_jalan}</div>
|
||||
<div class="badge" style="background:${bg};color:${c};">${j.status_jalan}</div>
|
||||
</div>
|
||||
<div class="item-card-info">
|
||||
📐 Panjang: ${fmtPanjang(parseFloat(j.panjang_jalan))}
|
||||
</div>
|
||||
<div class="item-card-actions">
|
||||
<button class="btn-sm btn-zoom" title="Zoom" onclick="event.stopPropagation();zoomJalan(${j.id})">🔍 Zoom</button>
|
||||
<button class="btn-sm btn-edit" title="Edit" onclick="event.stopPropagation();window.editJalan(${j.id})">✏️ Edit</button>
|
||||
<button class="btn-sm btn-delete" title="Hapus" onclick="event.stopPropagation();window.hapusJalan(${j.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateStatsJalan() {
|
||||
const total = jalanData.length;
|
||||
const km = jalanData.reduce((s,j) => s + parseFloat(j.panjang_jalan), 0);
|
||||
document.getElementById('sj-total').textContent = total;
|
||||
document.getElementById('sj-panjang').textContent = fmtPanjang(km);
|
||||
}
|
||||
|
||||
function zoomJalan(id) {
|
||||
const l = jalanLyrs[id];
|
||||
if (l) { map.fitBounds(l.polyline.getBounds(), {padding:[60,60], maxZoom:17}); l.polyline.openPopup(); }
|
||||
}
|
||||
|
||||
window.hapusJalan = function(id) {
|
||||
map.closePopup();
|
||||
Swal.fire({
|
||||
title: 'Hapus Jalan?',
|
||||
text: "Data jalan ini akan dihapus permanen dari database!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
background: '#1e293b',
|
||||
color: '#f1f5f9',
|
||||
confirmButtonColor: '#ef4444',
|
||||
cancelButtonColor: '#4f46e5',
|
||||
confirmButtonText: '🗑️ Ya, Hapus',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const r = await fetch(API_JALAN + '?id=' + id, {method:'DELETE'});
|
||||
const d = await r.json();
|
||||
if (d.status === 'sukses') {
|
||||
const l = jalanLyrs[id];
|
||||
if (l) { map.removeLayer(l.polyline); l.vertices.forEach(v=>map.removeLayer(v)); if(l.label) map.removeLayer(l.label); }
|
||||
delete jalanLyrs[id];
|
||||
jalanData = jalanData.filter(j => j.id != id);
|
||||
renderListJalan(); updateStatsJalan();
|
||||
Swal.fire({icon: 'success', title: 'Terhapus!', text: 'Data jalan berhasil dihapus.', background: '#1e293b', color: '#f1f5f9', timer: 1500, showConfirmButton: false});
|
||||
} else { Swal.fire({icon: 'error', title: 'Gagal', text: d.pesan, background: '#1e293b', color: '#f1f5f9'}); }
|
||||
} catch(e) { Swal.fire({icon: 'error', title: 'Error', text: e.message, background: '#1e293b', color: '#f1f5f9'}); }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function updateJalanAPI(id, lls, panjang) {
|
||||
try {
|
||||
await fetch(API_JALAN + '?id=' + id, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ geojson:toGeoJSONLine(lls), panjang_jalan:panjang })
|
||||
});
|
||||
const i = jalanData.findIndex(j => j.id == id);
|
||||
if (i >= 0) jalanData[i].panjang_jalan = panjang;
|
||||
updateStatsJalan(); renderListJalan();
|
||||
} catch(e) { console.error('Update jalan:', e); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
PARSIL — CRUD
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
async function muatParsil() {
|
||||
try { const r = await fetch(API_PARSIL); parsilData = await r.json(); }
|
||||
catch(e) { parsilData = []; console.warn('API Parsil:', e); }
|
||||
|
||||
Object.values(parsilLyrs).forEach(l => {
|
||||
map.removeLayer(l.polygon);
|
||||
l.vertices.forEach(v => map.removeLayer(v));
|
||||
if (l.label) map.removeLayer(l.label);
|
||||
});
|
||||
parsilLyrs = {};
|
||||
|
||||
parsilData.forEach(p => renderParsil(p));
|
||||
renderListParsil();
|
||||
updateStatsParsil();
|
||||
}
|
||||
|
||||
function renderParsil(parsil) {
|
||||
const lls = fromGeoJSONPolygon(parsil.geojson);
|
||||
const color = PARSIL_COLOR[parsil.status_kepemilikan] || '#6366f1';
|
||||
|
||||
const polygon = L.polygon(lls, { color, fillColor:color, fillOpacity:.18, weight:2.5 }).addTo(map);
|
||||
|
||||
const label = L.tooltip({ permanent:true, direction:'center', className:'map-label', offset:[0,0] })
|
||||
.setContent(parsil.nama_parsil).setLatLng(centroid(lls)).addTo(map);
|
||||
|
||||
function mkPopup() { return buatPopupParsil(parsil, lls, polygon, label); }
|
||||
polygon.bindPopup(mkPopup());
|
||||
|
||||
const vertices = [];
|
||||
lls.forEach((pt, idx) => {
|
||||
const vm = L.circleMarker(pt, { radius:5, color:'#fff', fillColor:color, fillOpacity:1, weight:2 }).addTo(map);
|
||||
vm.on('mousedown', e => {
|
||||
map.dragging.disable(); L.DomEvent.stopPropagation(e);
|
||||
function onMove(ev) {
|
||||
vm.setLatLng(ev.latlng); lls[idx] = ev.latlng;
|
||||
polygon.setLatLngs(lls); label.setLatLng(centroid(lls));
|
||||
}
|
||||
function onUp() {
|
||||
map.dragging.enable();
|
||||
map.off('mousemove', onMove); map.off('mouseup', onUp);
|
||||
const newL = hitungLuasParsil(lls);
|
||||
updateParsilAPI(parsil.id, lls, newL);
|
||||
polygon.bindPopup(buatPopupParsil({...parsil, luas_tanah:newL}, lls, polygon, label));
|
||||
}
|
||||
map.on('mousemove', onMove); map.on('mouseup', onUp);
|
||||
});
|
||||
vertices.push(vm);
|
||||
});
|
||||
|
||||
parsilLyrs[parsil.id] = { polygon, vertices, label };
|
||||
}
|
||||
|
||||
function buatPopupParsil(p, lls, polygon, label) {
|
||||
const color = PARSIL_COLOR[p.status_kepemilikan];
|
||||
const luas = lls ? hitungLuasParsil(lls) : parseFloat(p.luas_tanah);
|
||||
const sk = { SHM:'Sertifikat Hak Milik', HGB:'Hak Guna Bangunan', HGU:'Hak Guna Usaha', HP:'Hak Pakai' };
|
||||
return `
|
||||
<div class="gis-popup">
|
||||
<div class="popup-title" style="color:${color}">🏘️ ${p.nama_parsil}</div>
|
||||
<div class="info-row"><span class="info-label">Status</span><span class="info-value"><span style="background:${color}20;color:${color};padding:2px 6px;border-radius:4px;font-size:10px;font-weight:bold">${p.status_kepemilikan}</span></span></div>
|
||||
<div class="info-row"><span class="info-label">Luas</span><span class="info-value">📐 ${fmtLuas(luas)}</span></div>
|
||||
<div class="popup-form">
|
||||
<div style="font-size:9px; color:var(--text-muted); font-style:italic; margin-top:8px;">Seret titik sudut poligon untuk ubah bentuk parsil</div>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button class="popup-btn popup-btn-edit" onclick="window.editParsil(${p.id})">✏️ Edit</button>
|
||||
<button class="popup-btn popup-btn-delete" onclick="window.hapusParsil(${p.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderListParsil() {
|
||||
const data = parsilData;
|
||||
const el = document.getElementById('list-parsil');
|
||||
|
||||
if (!data.length) {
|
||||
el.innerHTML = `<div class="loading-text">Tidak ada data parsil.</div>`;
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.map(p => {
|
||||
const c = PARSIL_COLOR[p.status_kepemilikan]; const bg = c+'22';
|
||||
return `<div class="item-card" onclick="zoomParsil(${p.id})">
|
||||
<div class="item-card-header">
|
||||
<div class="item-card-name">${p.nama_parsil}</div>
|
||||
<div class="badge" style="background:${bg};color:${c};">${p.status_kepemilikan}</div>
|
||||
</div>
|
||||
<div class="item-card-info">
|
||||
📐 Luas: ${fmtLuas(parseFloat(p.luas_tanah))}
|
||||
</div>
|
||||
<div class="item-card-actions">
|
||||
<button class="btn-sm btn-zoom" title="Zoom" onclick="event.stopPropagation();zoomParsil(${p.id})">🔍 Zoom</button>
|
||||
<button class="btn-sm btn-edit" title="Edit" onclick="event.stopPropagation();window.editParsil(${p.id})">✏️ Edit</button>
|
||||
<button class="btn-sm btn-delete" title="Hapus" onclick="event.stopPropagation();window.hapusParsil(${p.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateStatsParsil() {
|
||||
const total = parsilData.length;
|
||||
const luas = parsilData.reduce((s,p) => s + parseFloat(p.luas_tanah), 0);
|
||||
document.getElementById('sp-total').textContent = total;
|
||||
document.getElementById('sp-luas').textContent = fmtLuas(luas);
|
||||
}
|
||||
|
||||
function zoomParsil(id) {
|
||||
const l = parsilLyrs[id];
|
||||
if (l) { map.fitBounds(l.polygon.getBounds(), {padding:[60,60], maxZoom:18}); l.polygon.openPopup(); }
|
||||
}
|
||||
|
||||
window.hapusParsil = function(id) {
|
||||
map.closePopup();
|
||||
Swal.fire({
|
||||
title: 'Hapus Parsil?',
|
||||
text: "Data parsil tanah ini akan dihapus permanen dari database!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
background: '#1e293b',
|
||||
color: '#f1f5f9',
|
||||
confirmButtonColor: '#ef4444',
|
||||
cancelButtonColor: '#4f46e5',
|
||||
confirmButtonText: '🗑️ Ya, Hapus',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const r = await fetch(API_PARSIL + '?id=' + id, {method:'DELETE'});
|
||||
const d = await r.json();
|
||||
if (d.status === 'sukses') {
|
||||
const l = parsilLyrs[id];
|
||||
if (l) { map.removeLayer(l.polygon); l.vertices.forEach(v=>map.removeLayer(v)); if(l.label) map.removeLayer(l.label); }
|
||||
delete parsilLyrs[id];
|
||||
parsilData = parsilData.filter(p => p.id != id);
|
||||
renderListParsil(); updateStatsParsil();
|
||||
Swal.fire({icon: 'success', title: 'Terhapus!', text: 'Data parsil berhasil dihapus.', background: '#1e293b', color: '#f1f5f9', timer: 1500, showConfirmButton: false});
|
||||
} else { Swal.fire({icon: 'error', title: 'Gagal', text: d.pesan, background: '#1e293b', color: '#f1f5f9'}); }
|
||||
} catch(e) { Swal.fire({icon: 'error', title: 'Error', text: e.message, background: '#1e293b', color: '#f1f5f9'}); }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function updateParsilAPI(id, lls, luas) {
|
||||
try {
|
||||
await fetch(API_PARSIL + '?id=' + id, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ geojson:toGeoJSONPolygon(lls), luas_tanah:luas })
|
||||
});
|
||||
const i = parsilData.findIndex(p => p.id == id);
|
||||
if (i >= 0) parsilData[i].luas_tanah = luas;
|
||||
updateStatsParsil(); renderListParsil();
|
||||
} catch(e) { console.error('Update parsil:', e); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
STATUS BAR
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
function setStatus(msg) {
|
||||
document.getElementById('sb-txt').textContent = msg;
|
||||
document.getElementById('status-bar').classList.add('on');
|
||||
}
|
||||
function clearStatus() { document.getElementById('status-bar').classList.remove('on'); }
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
KEYBOARD
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (document.getElementById('modal-bg').classList.contains('show')) return;
|
||||
if (e.key === 'Escape') cancelDraw();
|
||||
if (e.key === 'Enter' && drawMode) finishDraw();
|
||||
if ((e.key === 'z' || e.key === 'Z') && drawMode) undoPt();
|
||||
});
|
||||
document.getElementById('inp-nama').addEventListener('keydown', e => { if(e.key==='Enter') saveModal(); });
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
INIT
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
Promise.all([muatJalan(), muatParsil()]);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
-- =========================================================
|
||||
-- WebGIS - Struktur Database
|
||||
-- Tugas: Manajemen Data Jalan & Parsil Tanah (Kavling)
|
||||
-- Database: WEB_GIS
|
||||
-- Tipe Geometri: GeoJSON (LineString & Polygon)
|
||||
-- =========================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS WEB_GIS
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE WEB_GIS;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- TABEL 1: Manajemen Data Jalan
|
||||
-- Tipe Geometri : GeoJSON LineString
|
||||
-- Panjang Jalan : Dihitung otomatis oleh LeafletJS (meter)
|
||||
-- ---------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `tb_jalan` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key jalan',
|
||||
`nama_jalan` VARCHAR(100) NOT NULL COMMENT 'Nama ruas jalan',
|
||||
`status_jalan` ENUM(
|
||||
'Nasional',
|
||||
'Provinsi',
|
||||
'Kabupaten'
|
||||
) NOT NULL COMMENT 'Status administrasi jalan',
|
||||
`panjang_jalan` DECIMAL(12,2) NOT NULL DEFAULT '0.00' COMMENT 'Panjang jalan dalam meter (dihitung otomatis LeafletJS via L.LatLng.distanceTo)',
|
||||
`geojson` JSON NOT NULL COMMENT 'Koordinat geometri dalam format GeoJSON LineString',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Tabel manajemen data jalan berdasarkan status administrasi. Geometri disimpan sebagai GeoJSON LineString.';
|
||||
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- TABEL 2: Manajemen Parsil Tanah / Kavling
|
||||
-- Tipe Geometri : GeoJSON Polygon
|
||||
-- Luas Tanah : Dihitung otomatis oleh LeafletJS (m²)
|
||||
-- ---------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `tb_parsil` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key parsil',
|
||||
`nama_parsil` VARCHAR(100) NOT NULL COMMENT 'Nama atau nomor identifikasi parsil tanah',
|
||||
`status_kepemilikan` ENUM(
|
||||
'SHM',
|
||||
'HGB',
|
||||
'HGU',
|
||||
'HP'
|
||||
) NOT NULL COMMENT 'Status sertifikat kepemilikan: SHM=Sertifikat Hak Milik, HGB=Hak Guna Bangunan, HGU=Hak Guna Usaha, HP=Hak Pakai',
|
||||
`luas_tanah` DECIMAL(16,4) NOT NULL DEFAULT '0.0000' COMMENT 'Luas tanah dalam meter persegi (dihitung otomatis LeafletJS via formula geodesic spherical)',
|
||||
`geojson` JSON NOT NULL COMMENT 'Koordinat geometri dalam format GeoJSON Polygon',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Tabel manajemen parsil/kavling tanah berdasarkan status kepemilikan. Geometri disimpan sebagai GeoJSON Polygon.';
|
||||
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- Contoh Indeks untuk performa query filter
|
||||
-- ---------------------------------------------------------
|
||||
ALTER TABLE `tb_jalan` ADD INDEX `idx_status_jalan` (`status_jalan`);
|
||||
ALTER TABLE `tb_parsil` ADD INDEX `idx_status_parsil` (`status_kepemilikan`);
|
||||
@@ -0,0 +1,822 @@
|
||||
|
||||
:root {
|
||||
--bg-dark: #0f1117;
|
||||
--bg-sidebar: #161b27;
|
||||
--bg-card: #1e2538;
|
||||
--bg-card-hover: #252d42;
|
||||
--border: #2a3248;
|
||||
--accent-blue: #3b82f6;
|
||||
--accent-green: #10b981;
|
||||
--accent-yellow: #f59e0b;
|
||||
--accent-red: #ef4444;
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #4b5563;
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--shadow: 0 4px 24px rgba(0, 0, 0, .4);
|
||||
--jalan: #f97316;
|
||||
--parsil: #8b5cf6;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#header-bar {
|
||||
background: linear-gradient(135deg, #161b27 0%, #0f1117 100%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* ── Auth / User Badge ── */
|
||||
.user-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
letter-spacing: .5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.role-badge-admin {
|
||||
background: rgba(59, 130, 246, .2);
|
||||
color: #60a5fa;
|
||||
border: 1px solid rgba(59, 130, 246, .4);
|
||||
}
|
||||
|
||||
.role-badge-pengurus {
|
||||
background: rgba(139, 92, 246, .2);
|
||||
color: #a78bfa;
|
||||
border: 1px solid rgba(139, 92, 246, .4);
|
||||
}
|
||||
|
||||
.role-badge-walikota {
|
||||
background: rgba(249, 115, 22, .2);
|
||||
color: #fb923c;
|
||||
border: 1px solid rgba(249, 115, 22, .4);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
padding: 6px 16px;
|
||||
background: linear-gradient(135deg, var(--accent-blue), #2563eb);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
opacity: .9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-lapor-header {
|
||||
padding: 6px 14px;
|
||||
background: rgba(239, 68, 68, .15);
|
||||
border: 1px solid rgba(239, 68, 68, .35);
|
||||
border-radius: 6px;
|
||||
color: #f87171;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-lapor-header:hover {
|
||||
background: rgba(239, 68, 68, .25);
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: rgba(59, 130, 246, .12);
|
||||
border: 1px solid rgba(59, 130, 246, .3);
|
||||
border-radius: 6px;
|
||||
color: #60a5fa;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
margin: 0 14px 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-export:hover {
|
||||
background: rgba(59, 130, 246, .22);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
padding: 6px 14px;
|
||||
background: rgba(239, 68, 68, .12);
|
||||
border: 1px solid rgba(239, 68, 68, .3);
|
||||
border-radius: 6px;
|
||||
color: #f87171;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: rgba(239, 68, 68, .22);
|
||||
border-color: rgba(239, 68, 68, .5);
|
||||
}
|
||||
|
||||
/* ── Role-restricted element hiding ── */
|
||||
.hide-for-role {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-title h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--accent-green);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#bottom-bar {
|
||||
background: var(--bg-sidebar);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 8px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 30px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.fab-btn {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--accent-blue), #2563eb);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fab-btn:hover {
|
||||
transform: scale(1.05);
|
||||
background: linear-gradient(135deg, #60a5fa, var(--accent-blue));
|
||||
}
|
||||
|
||||
.fab-menu {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.fab-container:hover .fab-menu,
|
||||
.fab-menu.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.fab-item {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
padding: 10px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fab-item:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: var(--accent-blue);
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
background: var(--bg-sidebar);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
#map {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px 18px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: linear-gradient(135deg, #1e2a45 0%, #161b27 100%);
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .3px;
|
||||
}
|
||||
|
||||
.sidebar-header p {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.sidebar-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 14px 14px 6px;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
display: none;
|
||||
/* Disembunyikan karena diganti FAB */
|
||||
}
|
||||
|
||||
.item-list {
|
||||
padding: 0 10px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: all .18s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: var(--accent-blue);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.item-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.item-card-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 2px 7px;
|
||||
border-radius: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.item-card-info {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.item-card-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: 'Inter', sans-serif;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.btn-zoom {
|
||||
background: rgba(59, 130, 246, .2);
|
||||
color: #60a5fa;
|
||||
border: 1px solid rgba(59, 130, 246, .3);
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: rgba(245, 158, 11, .2);
|
||||
color: #fbbf24;
|
||||
border: 1px solid rgba(245, 158, 11, .3);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: rgba(239, 68, 68, .2);
|
||||
color: #f87171;
|
||||
border: 1px solid rgba(239, 68, 68, .3);
|
||||
}
|
||||
|
||||
.legend-box {
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
.legend-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.layer-info-box {
|
||||
margin: 10px 14px;
|
||||
padding: 12px;
|
||||
background: rgba(59, 130, 246, .08);
|
||||
border: 1px solid rgba(59, 130, 246, .25);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.layer-info-box p {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.layer-info-box strong {
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.count-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 14px 10px;
|
||||
}
|
||||
|
||||
.count-card {
|
||||
flex: 1;
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.count-card .num {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.count-card .lbl {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#map {
|
||||
flex: 1;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--bg-card) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
color: var(--text-primary) !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: var(--bg-card) !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button {
|
||||
color: var(--text-secondary) !important;
|
||||
top: 8px !important;
|
||||
right: 8px !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.gis-popup {
|
||||
padding: 16px;
|
||||
min-width: 240px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.popup-form label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.popup-form label:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.popup-form input,
|
||||
.popup-form select {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
outline: none;
|
||||
transition: border-color .2s;
|
||||
}
|
||||
|
||||
.popup-form input:focus,
|
||||
.popup-form select:focus {
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.popup-btn:hover {
|
||||
opacity: .85;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.popup-btn-save {
|
||||
background: var(--accent-green);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.popup-btn-edit {
|
||||
background: var(--accent-yellow);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.popup-btn-delete {
|
||||
background: var(--accent-red);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.popup-btn-cancel {
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toast-item {
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow);
|
||||
animation: slideIn .3s ease;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background: #065f46;
|
||||
border: 1px solid #10b981;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: #7f1d1d;
|
||||
border: 1px solid #ef4444;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background: #1e3a5f;
|
||||
border: 1px solid #3b82f6;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0)
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Custom Leaflet Layer Control dark style */
|
||||
.leaflet-control-layers {
|
||||
background: var(--bg-card) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers label {
|
||||
color: var(--text-primary) !important;
|
||||
font-size: 12px !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-separator {
|
||||
border-top-color: var(--border) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 10px 14px !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-toggle {
|
||||
background-color: var(--bg-card) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
}
|
||||
|
||||
.btn-add-kemiskinan {
|
||||
background: linear-gradient(135deg, #f97316, #c2410c);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-add-masjid {
|
||||
background: linear-gradient(135deg, #8b5cf6, #5b21b6);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.radius-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.radius-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
|
||||
.radius-val {
|
||||
color: var(--parsil);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.radius-slider {
|
||||
width: 100%;
|
||||
accent-color: var(--parsil);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radius-ticks {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api_auth.php – Autentikasi Pengguna dengan Role
|
||||
// WebGIS Poverty Mapping
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
// config.php sudah set header Content-Type & CORS.
|
||||
// Jika OPTIONS preflight, sudah di-handle di config.php.
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Baca body JSON
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$body) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Request body tidak valid.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$username = isset($body['username']) ? trim($body['username']) : '';
|
||||
$password = isset($body['password']) ? $body['password'] : '';
|
||||
$roleReq = isset($body['role']) ? trim($body['role']) : '';
|
||||
|
||||
// Validasi input
|
||||
if (!$username || !$password || !$roleReq) {
|
||||
echo json_encode(['success' => false, 'message' => 'Username, password, dan role wajib diisi.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$validRoles = ['admin', 'pengurus', 'walikota'];
|
||||
if (!in_array($roleReq, $validRoles)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Role tidak dikenali.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// ── Cari user di database ──────────────────────────────────
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, username, nama_lengkap, role, password_hash
|
||||
FROM users
|
||||
WHERE username = ? AND role = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->bind_param('ss', $username, $roleReq);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows === 0) {
|
||||
$stmt->close();
|
||||
$db->close();
|
||||
echo json_encode(['success' => false, 'message' => 'Username, password, atau peran tidak sesuai.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$user = $result->fetch_assoc();
|
||||
$stmt->close();
|
||||
$db->close();
|
||||
|
||||
// ── Verifikasi password ────────────────────────────────────
|
||||
if (!password_verify($password, $user['password_hash'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Username, password, atau peran tidak sesuai.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
// ── Login berhasil ─────────────────────────────────────────
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Login berhasil.',
|
||||
'user' => [
|
||||
'id' => (int)$user['id'],
|
||||
'username' => $user['username'],
|
||||
'nama' => $user['nama_lengkap'],
|
||||
'role' => $user['role']
|
||||
]
|
||||
]);
|
||||
?>
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api_bantuan.php – CRUD Riwayat Bantuan Kemiskinan
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// GET: Ambil riwayat bantuan berdasarkan id_kemiskinan
|
||||
if ($method === 'GET') {
|
||||
$id_kemiskinan = isset($_GET['id_kemiskinan']) ? (int)$_GET['id_kemiskinan'] : 0;
|
||||
if (!$id_kemiskinan) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Parameter id_kemiskinan wajib diisi']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT * FROM riwayat_bantuan WHERE id_kemiskinan = ? ORDER BY tanggal DESC, created_at DESC");
|
||||
$stmt->bind_param('i', $id_kemiskinan);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// POST: Tambah riwayat bantuan baru
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$id_kemiskinan = isset($body['id_kemiskinan']) ? (int)$body['id_kemiskinan'] : 0;
|
||||
$pemberi = isset($body['pemberi']) ? trim($body['pemberi']) : '';
|
||||
$jenis_bantuan = isset($body['jenis_bantuan']) ? trim($body['jenis_bantuan']) : '';
|
||||
$tanggal = isset($body['tanggal']) ? trim($body['tanggal']) : '';
|
||||
$keterangan = isset($body['keterangan']) ? trim($body['keterangan']) : '';
|
||||
|
||||
if (!$id_kemiskinan || !$pemberi || !$jenis_bantuan || !$tanggal) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'id_kemiskinan, pemberi, jenis_bantuan, dan tanggal wajib diisi']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
try {
|
||||
$db->begin_transaction();
|
||||
|
||||
// 1. Insert ke riwayat_bantuan
|
||||
$stmt = $db->prepare("INSERT INTO riwayat_bantuan (id_kemiskinan, pemberi, jenis_bantuan, tanggal, keterangan) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param('issss', $id_kemiskinan, $pemberi, $jenis_bantuan, $tanggal, $keterangan);
|
||||
$stmt->execute();
|
||||
$id_riwayat = $db->insert_id;
|
||||
|
||||
// 2. Update status_bantuan di tabel data_kemiskinan menjadi 'Sudah'
|
||||
$stmtUpdate = $db->prepare("UPDATE data_kemiskinan SET status_bantuan = 'Sudah' WHERE id = ?");
|
||||
$stmtUpdate->bind_param('i', $id_kemiskinan);
|
||||
$stmtUpdate->execute();
|
||||
|
||||
$db->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Berhasil menambahkan catatan bantuan',
|
||||
'id' => $id_riwayat,
|
||||
'status_bantuan' => 'Sudah'
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$db->rollback();
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menyimpan data: ' . $e->getMessage()]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// DELETE: Hapus catatan bantuan
|
||||
if ($method === 'DELETE') {
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$id_kemiskinan = isset($_GET['id_kemiskinan']) ? (int)$_GET['id_kemiskinan'] : 0;
|
||||
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Parameter id wajib diisi']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
try {
|
||||
$db->begin_transaction();
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM riwayat_bantuan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
|
||||
// Cek apakah masih ada bantuan lain
|
||||
$stmtCek = $db->prepare("SELECT COUNT(*) AS total FROM riwayat_bantuan WHERE id_kemiskinan = ?");
|
||||
$stmtCek->bind_param('i', $id_kemiskinan);
|
||||
$stmtCek->execute();
|
||||
$res = $stmtCek->get_result()->fetch_assoc();
|
||||
$count = $res['total'];
|
||||
|
||||
$status_baru = 'Sudah';
|
||||
if ($count == 0) {
|
||||
$stmtUpdate = $db->prepare("UPDATE data_kemiskinan SET status_bantuan = 'Belum' WHERE id = ?");
|
||||
$stmtUpdate->bind_param('i', $id_kemiskinan);
|
||||
$stmtUpdate->execute();
|
||||
$status_baru = 'Belum';
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(['success' => true, 'status_bantuan' => $status_baru]);
|
||||
} catch (Exception $e) {
|
||||
$db->rollback();
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menghapus data']);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method tidak diizinkan']);
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api_dashboard.php – Data untuk Dashboard Walikota
|
||||
// Endpoint statistik ringkasan, laporan terbaru, data heatmap
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
if ($method !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method tidak diizinkan']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$action = isset($_GET['action']) ? $_GET['action'] : 'stats';
|
||||
$db = getDB();
|
||||
|
||||
// ── STATISTIK RINGKASAN ────────────────────────────────────
|
||||
if ($action === 'stats') {
|
||||
$stats = [];
|
||||
|
||||
// Total per kategori kemiskinan
|
||||
$result = $db->query("SELECT kategori, COUNT(*) as total,
|
||||
SUM(CASE WHEN status_bantuan = 'Sudah' THEN 1 ELSE 0 END) as sudah_dibantu,
|
||||
SUM(CASE WHEN status_bantuan != 'Sudah' OR status_bantuan IS NULL THEN 1 ELSE 0 END) as belum_dibantu
|
||||
FROM data_kemiskinan GROUP BY kategori");
|
||||
$kategori = [];
|
||||
$total_warga = 0;
|
||||
$total_sudah = 0;
|
||||
$total_belum = 0;
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['total'] = (int)$row['total'];
|
||||
$row['sudah_dibantu'] = (int)$row['sudah_dibantu'];
|
||||
$row['belum_dibantu'] = (int)$row['belum_dibantu'];
|
||||
$kategori[] = $row;
|
||||
$total_warga += $row['total'];
|
||||
$total_sudah += $row['sudah_dibantu'];
|
||||
$total_belum += $row['belum_dibantu'];
|
||||
}
|
||||
$stats['kategori'] = $kategori;
|
||||
$stats['total_warga'] = $total_warga;
|
||||
$stats['total_sudah'] = $total_sudah;
|
||||
$stats['total_belum'] = $total_belum;
|
||||
$stats['persen_sudah'] = $total_warga > 0 ? round(($total_sudah / $total_warga) * 100, 1) : 0;
|
||||
|
||||
// Total rumah ibadah
|
||||
$result = $db->query("SELECT jenis, COUNT(*) as total FROM data_masjid GROUP BY jenis");
|
||||
$ibadah = [];
|
||||
$total_ibadah = 0;
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['total'] = (int)$row['total'];
|
||||
$ibadah[] = $row;
|
||||
$total_ibadah += $row['total'];
|
||||
}
|
||||
$stats['rumah_ibadah'] = $ibadah;
|
||||
$stats['total_ibadah'] = $total_ibadah;
|
||||
|
||||
// Total bantuan tercatat
|
||||
$result = $db->query("SELECT COUNT(*) as total FROM riwayat_bantuan");
|
||||
$row = $result->fetch_assoc();
|
||||
$stats['total_bantuan'] = (int)$row['total'];
|
||||
|
||||
// Total laporan masyarakat per status
|
||||
$result = $db->query("SELECT status, COUNT(*) as total FROM laporan_masyarakat GROUP BY status");
|
||||
$laporan_status = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['total'] = (int)$row['total'];
|
||||
$laporan_status[] = $row;
|
||||
}
|
||||
$stats['laporan_status'] = $laporan_status;
|
||||
|
||||
// Penyakit terbanyak (aggregate)
|
||||
$result = $db->query("SELECT riwayat_penyakit, COUNT(*) as total
|
||||
FROM data_kemiskinan
|
||||
WHERE riwayat_penyakit IS NOT NULL AND riwayat_penyakit != ''
|
||||
GROUP BY riwayat_penyakit
|
||||
ORDER BY total DESC LIMIT 10");
|
||||
$penyakit = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['total'] = (int)$row['total'];
|
||||
$penyakit[] = $row;
|
||||
}
|
||||
$stats['penyakit_terbanyak'] = $penyakit;
|
||||
|
||||
echo json_encode($stats);
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ── LAPORAN TERBARU ────────────────────────────────────────
|
||||
if ($action === 'laporan_terbaru') {
|
||||
$limit = isset($_GET['limit']) ? min(50, max(1, (int)$_GET['limit'])) : 20;
|
||||
$result = $db->query("SELECT id, nama_pelapor, nik, no_wa, deskripsi, latitude, longitude, status, alasan_tolak, created_at
|
||||
FROM laporan_masyarakat ORDER BY created_at DESC LIMIT $limit");
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo json_encode($rows);
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ── DATA KEMISKINAN UNTUK TABEL ────────────────────────────
|
||||
if ($action === 'kemiskinan') {
|
||||
$result = $db->query("SELECT id, nama_kk, nik, kategori, status_bantuan, jumlah_kk, alamat, pendidikan, riwayat_penyakit, latitude, longitude
|
||||
FROM data_kemiskinan ORDER BY id DESC");
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['jumlah_kk'] = (int)$row['jumlah_kk'];
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo json_encode($rows);
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ── HEATMAP DATA ───────────────────────────────────────────
|
||||
if ($action === 'heatmap') {
|
||||
$result = $db->query("SELECT latitude, longitude, kategori FROM data_kemiskinan");
|
||||
$points = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$intensity = 0.5;
|
||||
if ($row['kategori'] === 'Sangat Miskin') $intensity = 1.0;
|
||||
else if ($row['kategori'] === 'Miskin') $intensity = 0.7;
|
||||
$points[] = [(float)$row['latitude'], (float)$row['longitude'], $intensity];
|
||||
}
|
||||
echo json_encode($points);
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Action tidak dikenali. Gunakan: stats, laporan_terbaru, kemiskinan, heatmap']);
|
||||
?>
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API DATA JALAN (Polyline/LineString)
|
||||
// File: api_jalan.php
|
||||
// Method: GET, POST, PUT, DELETE
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
// ---- GET: Ambil semua data atau satu data ----
|
||||
if ($method === 'GET') {
|
||||
$db = getDB();
|
||||
|
||||
if ($id) {
|
||||
// Get single
|
||||
$stmt = $db->prepare("SELECT id, nama_jalan, status, panjang_m, geojson FROM data_jalan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
if ($result) {
|
||||
$result['geojson'] = json_decode($result['geojson']); // decode jadi object
|
||||
echo json_encode($result);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
// Get all
|
||||
$result = $db->query("SELECT id, nama_jalan, status, panjang_m, geojson FROM data_jalan ORDER BY id DESC");
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo json_encode($rows);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- POST: Tambah data jalan baru ----
|
||||
if ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['nama_jalan']) || empty($input['status']) || empty($input['geojson'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Data tidak lengkap. Field wajib: nama_jalan, status, geojson']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$nama_jalan = trim($input['nama_jalan']);
|
||||
$status = $input['status'];
|
||||
$panjang_m = isset($input['panjang_m']) ? (float)$input['panjang_m'] : 0;
|
||||
$geojson = json_encode($input['geojson']); // simpan sebagai string JSON
|
||||
|
||||
// Validasi status
|
||||
$valid_status = ['Nasional', 'Provinsi', 'Kabupaten'];
|
||||
if (!in_array($status, $valid_status)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Status tidak valid. Pilih: Nasional, Provinsi, atau Kabupaten']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("INSERT INTO data_jalan (nama_jalan, status, panjang_m, geojson) VALUES (?, ?, ?, ?)");
|
||||
$stmt->bind_param('ssds', $nama_jalan, $status, $panjang_m, $geojson);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$new_id = $db->insert_id;
|
||||
http_response_code(201);
|
||||
echo json_encode([
|
||||
'message' => 'Data jalan berhasil disimpan',
|
||||
'id' => $new_id,
|
||||
'nama_jalan'=> $nama_jalan,
|
||||
'status' => $status,
|
||||
'panjang_m' => $panjang_m
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menyimpan data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- PUT: Update data jalan ----
|
||||
if ($method === 'PUT') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk update']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$db = getDB();
|
||||
|
||||
// Cek apakah hanya update geometri (saat edit di peta) atau full update
|
||||
if (isset($input['geojson']) && !isset($input['nama_jalan'])) {
|
||||
// Update hanya geometri & panjang (drag/edit di peta)
|
||||
$geojson = json_encode($input['geojson']);
|
||||
$panjang_m = isset($input['panjang_m']) ? (float)$input['panjang_m'] : 0;
|
||||
$stmt = $db->prepare("UPDATE data_jalan SET geojson = ?, panjang_m = ? WHERE id = ?");
|
||||
$stmt->bind_param('sdi', $geojson, $panjang_m, $id);
|
||||
} else {
|
||||
// Full update (edit atribut + geometri)
|
||||
$nama_jalan = trim($input['nama_jalan'] ?? '');
|
||||
$status = $input['status'] ?? '';
|
||||
$panjang_m = isset($input['panjang_m']) ? (float)$input['panjang_m'] : 0;
|
||||
$geojson = json_encode($input['geojson'] ?? []);
|
||||
|
||||
$valid_status = ['Nasional', 'Provinsi', 'Kabupaten'];
|
||||
if (!in_array($status, $valid_status)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Status tidak valid']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE data_jalan SET nama_jalan = ?, status = ?, panjang_m = ?, geojson = ? WHERE id = ?");
|
||||
$stmt->bind_param('ssdsi', $nama_jalan, $status, $panjang_m, $geojson, $id);
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['message' => 'Data jalan berhasil diperbarui', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal memperbarui data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- DELETE: Hapus data jalan ----
|
||||
if ($method === 'DELETE') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk hapus']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("DELETE FROM data_jalan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
if ($stmt->affected_rows > 0) {
|
||||
echo json_encode(['message' => 'Data jalan berhasil dihapus', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menghapus data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// Method tidak dikenal
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method tidak diizinkan']);
|
||||
?>
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API DATA KEMISKINAN (Point / Marker)
|
||||
// File: api_kemiskinan.php
|
||||
// Fields: nama_kk, jumlah_kk, alamat, latitude, longitude
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
// ---- GET: Ambil semua data atau satu data ----
|
||||
if ($method === 'GET') {
|
||||
$db = getDB();
|
||||
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("SELECT id, nik, nama_kk, jumlah_kk, alamat, latitude, longitude, status_bantuan, kategori, skor_penghasilan, skor_rumah, skor_makan, tanggal_lahir, pendidikan, riwayat_penyakit FROM data_kemiskinan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
if ($result) {
|
||||
// Cast tipe
|
||||
$result['jumlah_kk'] = (int)$result['jumlah_kk'];
|
||||
$result['latitude'] = (float)$result['latitude'];
|
||||
$result['longitude'] = (float)$result['longitude'];
|
||||
echo json_encode($result);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data kemiskinan tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
$result = $db->query("SELECT id, nik, nama_kk, jumlah_kk, alamat, latitude, longitude, status_bantuan, kategori, skor_penghasilan, skor_rumah, skor_makan, tanggal_lahir, pendidikan, riwayat_penyakit FROM data_kemiskinan ORDER BY id DESC");
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['jumlah_kk'] = (int)$row['jumlah_kk'];
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo json_encode($rows);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- POST: Tambah data kemiskinan baru ----
|
||||
if ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['nama_kk']) || !isset($input['jumlah_kk']) || empty($input['alamat'])
|
||||
|| !isset($input['latitude']) || !isset($input['longitude'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Data tidak lengkap. Field wajib: nama_kk, jumlah_kk, alamat, latitude, longitude']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$nama_kk = trim($input['nama_kk']);
|
||||
$nik = trim($input['nik'] ?? '');
|
||||
$jumlah_kk = max(1, (int)$input['jumlah_kk']);
|
||||
$alamat = trim($input['alamat']);
|
||||
$latitude = (float)$input['latitude'];
|
||||
$longitude = (float)$input['longitude'];
|
||||
|
||||
$tanggal_lahir = isset($input['tanggal_lahir']) && $input['tanggal_lahir'] !== '' ? $input['tanggal_lahir'] : null;
|
||||
$pendidikan = isset($input['pendidikan']) ? trim($input['pendidikan']) : null;
|
||||
$riwayat_penyakit = isset($input['riwayat_penyakit']) ? trim($input['riwayat_penyakit']) : null;
|
||||
|
||||
$kategori = isset($input['kategori']) ? trim($input['kategori']) : 'Miskin';
|
||||
$skor_penghasilan = isset($input['skor_penghasilan']) ? (int)$input['skor_penghasilan'] : 0;
|
||||
$skor_rumah = isset($input['skor_rumah']) ? (int)$input['skor_rumah'] : 0;
|
||||
$skor_makan = isset($input['skor_makan']) ? (int)$input['skor_makan'] : 0;
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("INSERT INTO data_kemiskinan (nama_kk, nik, jumlah_kk, alamat, latitude, longitude, kategori, skor_penghasilan, skor_rumah, skor_makan, tanggal_lahir, pendidikan, riwayat_penyakit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param('ssisddsiiisss', $nama_kk, $nik, $jumlah_kk, $alamat, $latitude, $longitude, $kategori, $skor_penghasilan, $skor_rumah, $skor_makan, $tanggal_lahir, $pendidikan, $riwayat_penyakit);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$new_id = $db->insert_id;
|
||||
http_response_code(201);
|
||||
echo json_encode([
|
||||
'message' => 'Data kemiskinan berhasil disimpan',
|
||||
'id' => $new_id,
|
||||
'nama_kk' => $nama_kk,
|
||||
'nik' => $nik,
|
||||
'jumlah_kk' => $jumlah_kk,
|
||||
'alamat' => $alamat,
|
||||
'latitude' => $latitude,
|
||||
'longitude' => $longitude,
|
||||
'status_bantuan' => 'Belum',
|
||||
'kategori' => $kategori,
|
||||
'skor_penghasilan' => $skor_penghasilan,
|
||||
'skor_rumah' => $skor_rumah,
|
||||
'skor_makan' => $skor_makan,
|
||||
'tanggal_lahir' => $tanggal_lahir,
|
||||
'pendidikan' => $pendidikan,
|
||||
'riwayat_penyakit' => $riwayat_penyakit
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menyimpan data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- PUT: Update data kemiskinan ----
|
||||
if ($method === 'PUT') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk update']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$db = getDB();
|
||||
|
||||
// Update posisi saja (drag marker)
|
||||
if (isset($input['latitude']) && isset($input['longitude']) && !isset($input['nama_kk'])) {
|
||||
$latitude = (float)$input['latitude'];
|
||||
$longitude = (float)$input['longitude'];
|
||||
$stmt = $db->prepare("UPDATE data_kemiskinan SET latitude = ?, longitude = ? WHERE id = ?");
|
||||
$stmt->bind_param('ddi', $latitude, $longitude, $id);
|
||||
} else {
|
||||
// Full update
|
||||
$nama_kk = trim($input['nama_kk'] ?? '');
|
||||
$nik = trim($input['nik'] ?? '');
|
||||
$jumlah_kk = max(1, (int)($input['jumlah_kk'] ?? 1));
|
||||
$alamat = trim($input['alamat'] ?? '');
|
||||
$latitude = (float)($input['latitude'] ?? 0);
|
||||
$longitude = (float)($input['longitude'] ?? 0);
|
||||
|
||||
$tanggal_lahir = isset($input['tanggal_lahir']) && $input['tanggal_lahir'] !== '' ? $input['tanggal_lahir'] : null;
|
||||
$pendidikan = isset($input['pendidikan']) ? trim($input['pendidikan']) : null;
|
||||
$riwayat_penyakit = isset($input['riwayat_penyakit']) ? trim($input['riwayat_penyakit']) : null;
|
||||
|
||||
$kategori = isset($input['kategori']) ? trim($input['kategori']) : 'Miskin';
|
||||
$skor_penghasilan = isset($input['skor_penghasilan']) ? (int)$input['skor_penghasilan'] : 0;
|
||||
$skor_rumah = isset($input['skor_rumah']) ? (int)$input['skor_rumah'] : 0;
|
||||
$skor_makan = isset($input['skor_makan']) ? (int)$input['skor_makan'] : 0;
|
||||
|
||||
if (empty($nama_kk) || empty($alamat)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'nama_kk dan alamat wajib diisi']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE data_kemiskinan SET nama_kk = ?, nik = ?, jumlah_kk = ?, alamat = ?, latitude = ?, longitude = ?, kategori = ?, skor_penghasilan = ?, skor_rumah = ?, skor_makan = ?, tanggal_lahir = ?, pendidikan = ?, riwayat_penyakit = ? WHERE id = ?");
|
||||
$stmt->bind_param('ssisddsiiisssi', $nama_kk, $nik, $jumlah_kk, $alamat, $latitude, $longitude, $kategori, $skor_penghasilan, $skor_rumah, $skor_makan, $tanggal_lahir, $pendidikan, $riwayat_penyakit, $id);
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['message' => 'Data kemiskinan berhasil diperbarui', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal memperbarui data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- DELETE: Hapus data kemiskinan ----
|
||||
if ($method === 'DELETE') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk hapus']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("DELETE FROM data_kemiskinan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
if ($stmt->affected_rows > 0) {
|
||||
echo json_encode(['message' => 'Data kemiskinan berhasil dihapus', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menghapus data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// Method tidak dikenal
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method tidak diizinkan']);
|
||||
?>
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API LAPORAN MASYARAKAT
|
||||
// File: api_lapor.php
|
||||
// Fields: nama_pelapor, nik, no_wa, deskripsi, latitude, longitude, status
|
||||
// Status: Pending | Diproses | Diverifikasi | Ditolak | Selesai
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
// ---- GET: Ambil laporan ----
|
||||
if ($method === 'GET') {
|
||||
$db = getDB();
|
||||
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("SELECT id, nama_pelapor, nik, no_wa, deskripsi, latitude, longitude, status, alasan_tolak, created_at, tanggal_lahir, pendidikan, riwayat_penyakit, skor_penghasilan, skor_rumah, skor_makan, jumlah_kk FROM laporan_masyarakat WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
if ($result) {
|
||||
$result['latitude'] = (float)$result['latitude'];
|
||||
$result['longitude'] = (float)$result['longitude'];
|
||||
echo json_encode($result);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Laporan tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
$result = $db->query("SELECT id, nama_pelapor, nik, no_wa, deskripsi, latitude, longitude, status, alasan_tolak, created_at, tanggal_lahir, pendidikan, riwayat_penyakit, skor_penghasilan, skor_rumah, skor_makan, jumlah_kk FROM laporan_masyarakat ORDER BY id DESC");
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo json_encode($rows);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- POST: Tambah laporan baru ----
|
||||
if ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['nama_pelapor']) || empty($input['deskripsi']) || !isset($input['latitude']) || !isset($input['longitude'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Data tidak lengkap. Field wajib: nama_pelapor, deskripsi, latitude, longitude']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$nama = trim($input['nama_pelapor']);
|
||||
$nik = isset($input['nik']) && $input['nik'] ? trim($input['nik']) : null;
|
||||
$no_wa = isset($input['no_wa']) && $input['no_wa'] ? trim($input['no_wa']) : null;
|
||||
$desk = trim($input['deskripsi']);
|
||||
$lat = (float)$input['latitude'];
|
||||
$lng = (float)$input['longitude'];
|
||||
|
||||
// New Fields
|
||||
$tgl_lahir = isset($input['tanggal_lahir']) && $input['tanggal_lahir'] ? $input['tanggal_lahir'] : null;
|
||||
$pdd = isset($input['pendidikan']) && $input['pendidikan'] ? $input['pendidikan'] : null;
|
||||
$pny = isset($input['riwayat_penyakit']) && $input['riwayat_penyakit'] ? $input['riwayat_penyakit'] : null;
|
||||
$sp = isset($input['skor_penghasilan']) ? (int)$input['skor_penghasilan'] : null;
|
||||
$sr = isset($input['skor_rumah']) ? (int)$input['skor_rumah'] : null;
|
||||
$sm = isset($input['skor_makan']) ? (int)$input['skor_makan'] : null;
|
||||
$jml = isset($input['jumlah_kk']) ? (int)$input['jumlah_kk'] : 1;
|
||||
|
||||
// Validasi NIK 16 digit jika diisi
|
||||
if ($nik && strlen($nik) !== 16) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'NIK harus 16 digit']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("INSERT INTO laporan_masyarakat (nama_pelapor, nik, no_wa, deskripsi, latitude, longitude, tanggal_lahir, pendidikan, riwayat_penyakit, skor_penghasilan, skor_rumah, skor_makan, jumlah_kk) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param('ssssddsssiiii', $nama, $nik, $no_wa, $desk, $lat, $lng, $tgl_lahir, $pdd, $pny, $sp, $sr, $sm, $jml);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$new_id = $db->insert_id;
|
||||
http_response_code(201);
|
||||
echo json_encode([
|
||||
'message' => 'Laporan berhasil dikirim',
|
||||
'id' => $new_id,
|
||||
'status' => 'Pending'
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menyimpan laporan: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- PUT: Update status laporan ----
|
||||
if ($method === 'PUT') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID wajib disertakan untuk update']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (empty($input['status'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Status wajib diisi (Pending/Diproses/Diverifikasi/Ditolak/Selesai)']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$status = $input['status'];
|
||||
$action = isset($input['action']) ? $input['action'] : null;
|
||||
$alasan_tolak = isset($input['alasan_tolak']) ? trim($input['alasan_tolak']) : null;
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// === VERIFIKASI: Konversi laporan menjadi data kemiskinan resmi ===
|
||||
if ($action === 'verify' && $status === 'Diverifikasi') {
|
||||
// Ambil data laporan dulu
|
||||
$stmtGet = $db->prepare("SELECT * FROM laporan_masyarakat WHERE id = ?");
|
||||
$stmtGet->bind_param('i', $id);
|
||||
$stmtGet->execute();
|
||||
$laporan = $stmtGet->get_result()->fetch_assoc();
|
||||
|
||||
if (!$laporan) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Laporan tidak ditemukan']);
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
$db->begin_transaction();
|
||||
try {
|
||||
// Hitung kategori dari skor (sama dengan logika frontend)
|
||||
$sp = (int)$laporan['skor_penghasilan'];
|
||||
$sr = (int)$laporan['skor_rumah'];
|
||||
$sm = (int)$laporan['skor_makan'];
|
||||
$total_skor = $sp + $sr + $sm;
|
||||
|
||||
$kategori = 'Miskin';
|
||||
if ($total_skor >= 7) {
|
||||
$kategori = 'Sangat Miskin';
|
||||
} else if ($total_skor >= 4) {
|
||||
$kategori = 'Miskin';
|
||||
} else {
|
||||
$kategori = 'Hampir Miskin';
|
||||
}
|
||||
|
||||
// 1. Masukkan ke data_kemiskinan sebagai data resmi
|
||||
$stmtInsert = $db->prepare(
|
||||
"INSERT INTO data_kemiskinan (nama_kk, nik, alamat, latitude, longitude, kategori, status_bantuan, tanggal_lahir, pendidikan, riwayat_penyakit, skor_penghasilan, skor_rumah, skor_makan, jumlah_kk)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'Belum', ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$alamat = $laporan['deskripsi']; // Use deskripsi directly as address/notes
|
||||
$stmtInsert->bind_param('sssddssssiiii',
|
||||
$laporan['nama_pelapor'],
|
||||
$laporan['nik'],
|
||||
$alamat,
|
||||
$laporan['latitude'],
|
||||
$laporan['longitude'],
|
||||
$kategori,
|
||||
$laporan['tanggal_lahir'],
|
||||
$laporan['pendidikan'],
|
||||
$laporan['riwayat_penyakit'],
|
||||
$laporan['skor_penghasilan'],
|
||||
$laporan['skor_rumah'],
|
||||
$laporan['skor_makan'],
|
||||
$laporan['jumlah_kk']
|
||||
);
|
||||
$stmtInsert->execute();
|
||||
$new_kemiskinan_id = $db->insert_id;
|
||||
|
||||
// 2. Update status laporan menjadi Diverifikasi
|
||||
$stmtUpdate = $db->prepare("UPDATE laporan_masyarakat SET status = 'Diverifikasi' WHERE id = ?");
|
||||
$stmtUpdate->bind_param('i', $id);
|
||||
$stmtUpdate->execute();
|
||||
|
||||
$db->commit();
|
||||
echo json_encode([
|
||||
'message' => 'Laporan diverifikasi dan data kemiskinan resmi telah dibuat',
|
||||
'id_kemiskinan' => $new_kemiskinan_id
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$db->rollback();
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal memverifikasi: ' . $e->getMessage()]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// === TOLAK: Update status + alasan ===
|
||||
if ($status === 'Ditolak') {
|
||||
$stmt = $db->prepare("UPDATE laporan_masyarakat SET status = ?, alasan_tolak = ? WHERE id = ?");
|
||||
$stmt->bind_param('ssi', $status, $alasan_tolak, $id);
|
||||
} else {
|
||||
// === Update status biasa ===
|
||||
$stmt = $db->prepare("UPDATE laporan_masyarakat SET status = ? WHERE id = ?");
|
||||
$stmt->bind_param('si', $status, $id);
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
if ($stmt->affected_rows > 0) {
|
||||
echo json_encode(['message' => 'Status laporan berhasil diupdate']);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Laporan tidak ditemukan atau tidak ada perubahan']);
|
||||
}
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal mengupdate laporan']);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API DATA RUMAH IBADAH (Point / Marker + Radius Buffer)
|
||||
// File: api_masjid.php
|
||||
// Fields: nama_masjid, jenis, pic_masjid, alamat, radius_m (100-5000), latitude, longitude
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
// ---- GET: Ambil semua data atau satu data ----
|
||||
if ($method === 'GET') {
|
||||
$db = getDB();
|
||||
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("SELECT id, nama_masjid, jenis, pic_masjid, alamat, radius_m, latitude, longitude FROM data_masjid WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
if ($result) {
|
||||
$result['radius_m'] = (int)$result['radius_m'];
|
||||
$result['latitude'] = (float)$result['latitude'];
|
||||
$result['longitude'] = (float)$result['longitude'];
|
||||
echo json_encode($result);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data masjid tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
$result = $db->query("SELECT id, nama_masjid, jenis, pic_masjid, alamat, radius_m, latitude, longitude FROM data_masjid ORDER BY id DESC");
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['radius_m'] = (int)$row['radius_m'];
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo json_encode($rows);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- POST: Tambah masjid baru ----
|
||||
if ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['nama_masjid']) || empty($input['pic_masjid']) || empty($input['alamat'])
|
||||
|| !isset($input['latitude']) || !isset($input['longitude'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Data tidak lengkap. Field wajib: nama_masjid, jenis, pic_masjid, alamat, latitude, longitude']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$nama_masjid = trim($input['nama_masjid']);
|
||||
$jenis = isset($input['jenis']) ? trim($input['jenis']) : 'Masjid';
|
||||
$pic_masjid = trim($input['pic_masjid']);
|
||||
$alamat = trim($input['alamat']);
|
||||
$radius_m = isset($input['radius_m']) ? max(100, min(5000, (int)$input['radius_m'])) : 500;
|
||||
$latitude = (float)$input['latitude'];
|
||||
$longitude = (float)$input['longitude'];
|
||||
|
||||
$valid_jenis = ['Masjid','Gereja','Pura','Vihara','Klenteng'];
|
||||
if (!in_array($jenis, $valid_jenis)) $jenis = 'Masjid';
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("INSERT INTO data_masjid (nama_masjid, jenis, pic_masjid, alamat, radius_m, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param('ssssidd', $nama_masjid, $jenis, $pic_masjid, $alamat, $radius_m, $latitude, $longitude);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$new_id = $db->insert_id;
|
||||
http_response_code(201);
|
||||
echo json_encode([
|
||||
'message' => 'Data ibadah berhasil disimpan',
|
||||
'id' => $new_id,
|
||||
'nama_masjid' => $nama_masjid,
|
||||
'jenis' => $jenis,
|
||||
'pic_masjid' => $pic_masjid,
|
||||
'alamat' => $alamat,
|
||||
'radius_m' => $radius_m,
|
||||
'latitude' => $latitude,
|
||||
'longitude' => $longitude
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menyimpan data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- PUT: Update masjid ----
|
||||
if ($method === 'PUT') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk update']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$db = getDB();
|
||||
|
||||
// Update posisi saja (drag marker)
|
||||
if (isset($input['latitude']) && isset($input['longitude'])
|
||||
&& !isset($input['nama_masjid']) && !isset($input['radius_m'])) {
|
||||
$latitude = (float)$input['latitude'];
|
||||
$longitude = (float)$input['longitude'];
|
||||
$stmt = $db->prepare("UPDATE data_masjid SET latitude = ?, longitude = ? WHERE id = ?");
|
||||
$stmt->bind_param('ddi', $latitude, $longitude, $id);
|
||||
|
||||
// Update radius saja (dari slider tanpa simpan form)
|
||||
} elseif (isset($input['radius_m']) && !isset($input['nama_masjid'])) {
|
||||
$radius_m = max(100, min(5000, (int)$input['radius_m']));
|
||||
$stmt = $db->prepare("UPDATE data_masjid SET radius_m = ? WHERE id = ?");
|
||||
$stmt->bind_param('ii', $radius_m, $id);
|
||||
|
||||
} else {
|
||||
// Full update
|
||||
$nama_masjid = trim($input['nama_masjid'] ?? '');
|
||||
$jenis = trim($input['jenis'] ?? 'Masjid');
|
||||
$pic_masjid = trim($input['pic_masjid'] ?? '');
|
||||
$alamat = trim($input['alamat'] ?? '');
|
||||
$radius_m = isset($input['radius_m']) ? max(100, min(5000, (int)$input['radius_m'])) : 500;
|
||||
$latitude = (float)($input['latitude'] ?? 0);
|
||||
$longitude = (float)($input['longitude'] ?? 0);
|
||||
|
||||
$valid_jenis = ['Masjid','Gereja','Pura','Vihara','Klenteng'];
|
||||
if (!in_array($jenis, $valid_jenis)) $jenis = 'Masjid';
|
||||
|
||||
if (empty($nama_masjid) || empty($pic_masjid) || empty($alamat)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'nama_masjid, pic_masjid, dan alamat wajib diisi']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE data_masjid SET nama_masjid = ?, jenis = ?, pic_masjid = ?, alamat = ?, radius_m = ?, latitude = ?, longitude = ? WHERE id = ?");
|
||||
$stmt->bind_param('sssiddi', $nama_masjid, $jenis, $pic_masjid, $alamat, $radius_m, $latitude, $longitude, $id);
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['message' => 'Data masjid berhasil diperbarui', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal memperbarui data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- DELETE: Hapus masjid ----
|
||||
if ($method === 'DELETE') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk hapus']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("DELETE FROM data_masjid WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
if ($stmt->affected_rows > 0) {
|
||||
echo json_encode(['message' => 'Data masjid berhasil dihapus', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menghapus data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// Method tidak dikenal
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method tidak diizinkan']);
|
||||
?>
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API DATA PARSIL TANAH (Polygon)
|
||||
// File: api_parsil.php
|
||||
// Method: GET, POST, PUT, DELETE
|
||||
// Status: SHM, HGB, HGU, HP
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
// ---- GET: Ambil semua data atau satu data ----
|
||||
if ($method === 'GET') {
|
||||
$db = getDB();
|
||||
|
||||
if ($id) {
|
||||
// Get single
|
||||
$stmt = $db->prepare("SELECT id, no_parsil, pemilik, status, luas_m2, geojson FROM data_parsil WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
if ($result) {
|
||||
$result['geojson'] = json_decode($result['geojson']);
|
||||
echo json_encode($result);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
// Get all
|
||||
$result = $db->query("SELECT id, no_parsil, pemilik, status, luas_m2, geojson FROM data_parsil ORDER BY id DESC");
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo json_encode($rows);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- POST: Tambah parsil tanah baru ----
|
||||
if ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['no_parsil']) || empty($input['pemilik']) || empty($input['status']) || empty($input['geojson'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Data tidak lengkap. Field wajib: no_parsil, pemilik, status, geojson']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$no_parsil = trim($input['no_parsil']);
|
||||
$pemilik = trim($input['pemilik']);
|
||||
$status = $input['status'];
|
||||
$luas_m2 = isset($input['luas_m2']) ? (float)$input['luas_m2'] : 0;
|
||||
$geojson = json_encode($input['geojson']);
|
||||
|
||||
// Validasi status
|
||||
$valid_status = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
if (!in_array($status, $valid_status)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Status tidak valid. Pilih: SHM, HGB, HGU, atau HP']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("INSERT INTO data_parsil (no_parsil, pemilik, status, luas_m2, geojson) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param('sssds', $no_parsil, $pemilik, $status, $luas_m2, $geojson);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$new_id = $db->insert_id;
|
||||
http_response_code(201);
|
||||
echo json_encode([
|
||||
'message' => 'Data parsil berhasil disimpan',
|
||||
'id' => $new_id,
|
||||
'no_parsil' => $no_parsil,
|
||||
'pemilik' => $pemilik,
|
||||
'status' => $status,
|
||||
'luas_m2' => $luas_m2
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menyimpan data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- PUT: Update data parsil ----
|
||||
if ($method === 'PUT') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk update']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$db = getDB();
|
||||
|
||||
// Cek apakah hanya update geometri (edit bentuk di peta)
|
||||
if (isset($input['geojson']) && !isset($input['no_parsil'])) {
|
||||
// Update geometri & luas
|
||||
$geojson = json_encode($input['geojson']);
|
||||
$luas_m2 = isset($input['luas_m2']) ? (float)$input['luas_m2'] : 0;
|
||||
$stmt = $db->prepare("UPDATE data_parsil SET geojson = ?, luas_m2 = ? WHERE id = ?");
|
||||
$stmt->bind_param('sdi', $geojson, $luas_m2, $id);
|
||||
} else {
|
||||
// Full update (edit semua atribut)
|
||||
$no_parsil = trim($input['no_parsil'] ?? '');
|
||||
$pemilik = trim($input['pemilik'] ?? '');
|
||||
$status = $input['status'] ?? '';
|
||||
$luas_m2 = isset($input['luas_m2']) ? (float)$input['luas_m2'] : 0;
|
||||
$geojson = json_encode($input['geojson'] ?? []);
|
||||
|
||||
$valid_status = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
if (!in_array($status, $valid_status)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Status tidak valid']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE data_parsil SET no_parsil = ?, pemilik = ?, status = ?, luas_m2 = ?, geojson = ? WHERE id = ?");
|
||||
$stmt->bind_param('sssdsi', $no_parsil, $pemilik, $status, $luas_m2, $geojson, $id);
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['message' => 'Data parsil berhasil diperbarui', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal memperbarui data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---- DELETE: Hapus parsil tanah ----
|
||||
if ($method === 'DELETE') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk hapus']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("DELETE FROM data_parsil WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
if ($stmt->affected_rows > 0) {
|
||||
echo json_encode(['message' => 'Data parsil berhasil dihapus', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menghapus data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// Method tidak dikenal
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Method tidak diizinkan']);
|
||||
?>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// KONFIGURASI DATABASE
|
||||
// File: config.php
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root'); // Sesuaikan dengan user MySQL Anda
|
||||
define('DB_PASS', ''); // Sesuaikan dengan password MySQL Anda
|
||||
define('DB_NAME', 'webgis_p3');
|
||||
|
||||
function getDB() {
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
die(json_encode(['error' => 'Koneksi database gagal: ' . $conn->connect_error]));
|
||||
}
|
||||
$conn->set_charset('utf8mb4');
|
||||
return $conn;
|
||||
}
|
||||
|
||||
// Header CORS & JSON untuk semua response
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
// Handle preflight OPTIONS request
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,474 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Dashboard Eksekutif Walikota – WebGIS Poverty Mapping Kota Pontianak">
|
||||
<title>Dashboard Walikota – WebGIS Poverty Mapping</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-dark: #0a0e1a;
|
||||
--bg-panel: #0f1422;
|
||||
--bg-card: #141928;
|
||||
--bg-card-hover: #1a2035;
|
||||
--border: rgba(255,255,255,.07);
|
||||
--accent-blue: #3b82f6;
|
||||
--accent-green: #10b981;
|
||||
--accent-orange: #f59e0b;
|
||||
--accent-red: #ef4444;
|
||||
--accent-purple: #8b5cf6;
|
||||
--text: #e8edf5;
|
||||
--text-sub: #8a96b0;
|
||||
--text-muted: #4b5563;
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: linear-gradient(135deg, #0f1422 0%, #0a0e1a 100%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 16px 32px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.header-left { display: flex; align-items: center; gap: 14px; }
|
||||
.header-left h1 { font-size: 18px; font-weight: 800; }
|
||||
.header-left span.tag {
|
||||
font-size: 10px; font-weight: 700; padding: 3px 10px;
|
||||
background: rgba(249,115,22,.15); border: 1px solid rgba(249,115,22,.3);
|
||||
color: #fb923c; border-radius: 20px; text-transform: uppercase; letter-spacing: .5px;
|
||||
}
|
||||
.header-right { display: flex; align-items: center; gap: 14px; }
|
||||
.user-info-hdr { text-align: right; }
|
||||
.user-info-hdr .name { font-size: 13px; font-weight: 700; }
|
||||
.user-info-hdr .role { font-size: 10px; color: #fb923c; font-weight: 600; }
|
||||
.btn-back, .btn-logout-d {
|
||||
padding: 7px 14px; border-radius: 8px; font-size: 11px; font-weight: 600;
|
||||
cursor: pointer; border: none; font-family: 'Inter', sans-serif; transition: all .2s;
|
||||
}
|
||||
.btn-back {
|
||||
background: rgba(59,130,246,.12); border: 1px solid rgba(59,130,246,.3); color: #60a5fa;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-back:hover { background: rgba(59,130,246,.22); }
|
||||
.btn-logout-d {
|
||||
background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.3); color: #f87171;
|
||||
}
|
||||
.btn-logout-d:hover { background: rgba(239,68,68,.22); }
|
||||
|
||||
/* Main Grid */
|
||||
.dashboard {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 32px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* KPI Cards Row */
|
||||
.kpi-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.kpi-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.kpi-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 3px;
|
||||
}
|
||||
.kpi-card:nth-child(1)::before { background: var(--accent-red); }
|
||||
.kpi-card:nth-child(2)::before { background: var(--accent-orange); }
|
||||
.kpi-card:nth-child(3)::before { background: var(--accent-green); }
|
||||
.kpi-card:nth-child(4)::before { background: var(--accent-purple); }
|
||||
.kpi-card:nth-child(5)::before { background: var(--accent-blue); }
|
||||
.kpi-icon { font-size: 28px; margin-bottom: 10px; }
|
||||
.kpi-value { font-size: 32px; font-weight: 800; line-height: 1; }
|
||||
.kpi-label { font-size: 11px; color: var(--text-sub); margin-top: 6px; font-weight: 500; }
|
||||
.kpi-sub { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%; height: 6px; background: rgba(255,255,255,.06);
|
||||
border-radius: 3px; margin-top: 12px; overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%; border-radius: 3px; transition: width .6s ease;
|
||||
}
|
||||
|
||||
/* Two column layout */
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.section-head {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.section-head h2 {
|
||||
font-size: 14px; font-weight: 700;
|
||||
}
|
||||
.section-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
/* Kategori bars */
|
||||
.cat-row {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 10px 0; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.cat-row:last-child { border-bottom: none; }
|
||||
.cat-label { width: 120px; font-size: 12px; font-weight: 600; }
|
||||
.cat-bar-wrap { flex: 1; }
|
||||
.cat-bar {
|
||||
height: 20px; border-radius: 4px; position: relative; overflow: hidden;
|
||||
background: rgba(255,255,255,.04);
|
||||
}
|
||||
.cat-fill {
|
||||
height: 100%; border-radius: 4px; transition: width .6s; display: flex;
|
||||
align-items: center; padding-left: 8px; font-size: 10px; font-weight: 700;
|
||||
}
|
||||
.cat-count { width: 50px; text-align: right; font-size: 13px; font-weight: 700; }
|
||||
|
||||
/* Laporan list */
|
||||
.laporan-item {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 12px;
|
||||
}
|
||||
.laporan-item:last-child { border-bottom: none; }
|
||||
.laporan-info { flex: 1; }
|
||||
.laporan-nama { font-weight: 600; margin-bottom: 2px; }
|
||||
.laporan-desc { color: var(--text-sub); font-size: 11px; }
|
||||
.status-badge {
|
||||
font-size: 10px; font-weight: 700; padding: 3px 8px;
|
||||
border-radius: 4px; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Map */
|
||||
#mini-map {
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
}
|
||||
|
||||
/* Penyakit Table */
|
||||
.simple-table { width: 100%; border-collapse: collapse; }
|
||||
.simple-table th, .simple-table td {
|
||||
padding: 8px 12px; text-align: left; font-size: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.simple-table th {
|
||||
color: var(--text-muted); font-weight: 600; text-transform: uppercase;
|
||||
font-size: 10px; letter-spacing: .5px;
|
||||
}
|
||||
|
||||
/* Export buttons */
|
||||
.btn-export-d {
|
||||
padding: 6px 14px; border-radius: 6px; font-size: 11px; font-weight: 600;
|
||||
cursor: pointer; border: none; font-family: 'Inter', sans-serif;
|
||||
background: rgba(59,130,246,.12); border: 1px solid rgba(59,130,246,.3);
|
||||
color: #60a5fa; transition: all .2s;
|
||||
}
|
||||
.btn-export-d:hover { background: rgba(59,130,246,.22); }
|
||||
|
||||
/* Loading */
|
||||
.loading { text-align: center; padding: 30px; color: var(--text-muted); font-size: 12px; }
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
.section-body-scroll { max-height: 320px; overflow-y: auto; }
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.kpi-row { grid-template-columns: repeat(3, 1fr); }
|
||||
.two-col { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.kpi-row { grid-template-columns: repeat(2, 1fr); }
|
||||
.dashboard { padding: 16px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<span style="font-size:24px">👑</span>
|
||||
<h1>Dashboard Eksekutif</h1>
|
||||
<span class="tag">Walikota Pontianak</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="user-info-hdr">
|
||||
<div class="name" id="hdr-name">—</div>
|
||||
<div class="role">👑 Walikota</div>
|
||||
</div>
|
||||
<a href="index.html" class="btn-back">🗺️ Lihat Peta</a>
|
||||
<button class="btn-logout-d" onclick="logout()">🚪 Keluar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<div class="dashboard">
|
||||
<!-- KPI Row -->
|
||||
<div class="kpi-row">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-icon">🏚️</div>
|
||||
<div class="kpi-value" id="kpi-total" style="color:var(--accent-red)">—</div>
|
||||
<div class="kpi-label">Total Warga Miskin</div>
|
||||
<div class="kpi-sub" id="kpi-total-sub">Memuat...</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-icon">✅</div>
|
||||
<div class="kpi-value" id="kpi-sudah" style="color:var(--accent-green)">—</div>
|
||||
<div class="kpi-label">Sudah Dibantu</div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="prog-sudah" style="width:0%; background:var(--accent-green)"></div></div>
|
||||
<div class="kpi-sub" id="kpi-sudah-sub">0%</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-icon">⏳</div>
|
||||
<div class="kpi-value" id="kpi-belum" style="color:var(--accent-orange)">—</div>
|
||||
<div class="kpi-label">Belum Dibantu</div>
|
||||
<div class="kpi-sub" id="kpi-belum-sub">Memuat...</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-icon">🕌</div>
|
||||
<div class="kpi-value" id="kpi-ibadah" style="color:var(--accent-purple)">—</div>
|
||||
<div class="kpi-label">Rumah Ibadah Aktif</div>
|
||||
<div class="kpi-sub" id="kpi-ibadah-sub">Memuat...</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-icon">🎁</div>
|
||||
<div class="kpi-value" id="kpi-bantuan" style="color:var(--accent-blue)">—</div>
|
||||
<div class="kpi-label">Total Bantuan Tercatat</div>
|
||||
<div class="kpi-sub">Seluruh riwayat</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Distribusi Kategori + Laporan Terbaru -->
|
||||
<div class="two-col">
|
||||
<!-- Distribusi Kategori -->
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<h2>📊 Distribusi Kategori Kemiskinan</h2>
|
||||
<button class="btn-export-d" onclick="exportCSV()">📥 Export CSV</button>
|
||||
</div>
|
||||
<div class="section-body" id="distribusi-body">
|
||||
<div class="loading">⏳ Memuat data...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Laporan Terbaru -->
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<h2>📢 Laporan Masyarakat Terbaru</h2>
|
||||
</div>
|
||||
<div class="section-body section-body-scroll" id="laporan-body">
|
||||
<div class="loading">⏳ Memuat data...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peta Heatmap + Riwayat Penyakit -->
|
||||
<div class="two-col">
|
||||
<!-- Peta Heatmap -->
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<h2>🔥 Heatmap Distribusi Kemiskinan</h2>
|
||||
</div>
|
||||
<div id="mini-map"></div>
|
||||
</div>
|
||||
|
||||
<!-- Penyakit Terbanyak -->
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<h2>🏥 Riwayat Penyakit Terbanyak</h2>
|
||||
</div>
|
||||
<div class="section-body" id="penyakit-body">
|
||||
<div class="loading">⏳ Memuat data...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
||||
<script>
|
||||
// ── Auth Check ──
|
||||
const currentUser = JSON.parse(sessionStorage.getItem('webgis_user') || 'null');
|
||||
if (!currentUser || currentUser.role !== 'walikota') {
|
||||
// Bukan walikota → redirect
|
||||
window.location.replace('login.html');
|
||||
}
|
||||
|
||||
document.getElementById('hdr-name').textContent = currentUser.nama || currentUser.username || 'Walikota';
|
||||
|
||||
function logout() {
|
||||
if (!confirm('Yakin ingin keluar?')) return;
|
||||
sessionStorage.removeItem('webgis_user');
|
||||
window.location.replace('login.html?logout=1');
|
||||
}
|
||||
|
||||
const API = 'api_dashboard.php';
|
||||
|
||||
// ── Load Stats ──
|
||||
fetch(`${API}?action=stats`).then(r => r.json()).then(stats => {
|
||||
// KPI
|
||||
document.getElementById('kpi-total').textContent = stats.total_warga;
|
||||
document.getElementById('kpi-total-sub').textContent = `Data terverifikasi`;
|
||||
document.getElementById('kpi-sudah').textContent = stats.total_sudah;
|
||||
document.getElementById('kpi-sudah-sub').textContent = `${stats.persen_sudah}% dari total`;
|
||||
document.getElementById('prog-sudah').style.width = `${stats.persen_sudah}%`;
|
||||
document.getElementById('kpi-belum').textContent = stats.total_belum;
|
||||
document.getElementById('kpi-belum-sub').textContent = `Perlu perhatian`;
|
||||
document.getElementById('kpi-ibadah').textContent = stats.total_ibadah;
|
||||
const ibadahTypes = stats.rumah_ibadah.map(i => `${i.jenis}: ${i.total}`).join(', ');
|
||||
document.getElementById('kpi-ibadah-sub').textContent = ibadahTypes || 'Belum ada data';
|
||||
document.getElementById('kpi-bantuan').textContent = stats.total_bantuan;
|
||||
|
||||
// Distribusi Kategori
|
||||
const distBody = document.getElementById('distribusi-body');
|
||||
const cats = [
|
||||
{ label: '🚨 Sangat Miskin', color: '#ef4444', key: 'Sangat Miskin' },
|
||||
{ label: '⚠️ Miskin', color: '#f97316', key: 'Miskin' },
|
||||
{ label: '💡 Hampir Miskin', color: '#eab308', key: 'Hampir Miskin' }
|
||||
];
|
||||
let distHTML = '';
|
||||
cats.forEach(c => {
|
||||
const found = stats.kategori.find(k => k.kategori === c.key);
|
||||
const total = found ? found.total : 0;
|
||||
const sudah = found ? found.sudah_dibantu : 0;
|
||||
const belum = found ? found.belum_dibantu : 0;
|
||||
const pct = stats.total_warga > 0 ? ((total / stats.total_warga) * 100).toFixed(1) : 0;
|
||||
distHTML += `<div class="cat-row">
|
||||
<div class="cat-label" style="color:${c.color}">${c.label}</div>
|
||||
<div class="cat-bar-wrap">
|
||||
<div class="cat-bar">
|
||||
<div class="cat-fill" style="width:${pct}%; background:${c.color}; color:#fff; min-width:40px">${pct}%</div>
|
||||
</div>
|
||||
<div style="font-size:10px; color:var(--text-muted); margin-top:3px">Sudah: ${sudah} | Belum: ${belum}</div>
|
||||
</div>
|
||||
<div class="cat-count" style="color:${c.color}">${total}</div>
|
||||
</div>`;
|
||||
});
|
||||
distBody.innerHTML = distHTML;
|
||||
|
||||
// Penyakit
|
||||
const pBody = document.getElementById('penyakit-body');
|
||||
if (stats.penyakit_terbanyak.length === 0) {
|
||||
pBody.innerHTML = '<div class="loading">Belum ada data riwayat penyakit</div>';
|
||||
} else {
|
||||
let tbl = '<table class="simple-table"><thead><tr><th>Penyakit</th><th>Jumlah Warga</th></tr></thead><tbody>';
|
||||
stats.penyakit_terbanyak.forEach((p, i) => {
|
||||
tbl += `<tr><td style="font-weight:600">${p.riwayat_penyakit}</td><td style="color:var(--accent-red);font-weight:700">${p.total}</td></tr>`;
|
||||
});
|
||||
tbl += '</tbody></table>';
|
||||
pBody.innerHTML = tbl;
|
||||
}
|
||||
|
||||
}).catch(err => {
|
||||
console.error('Gagal memuat statistik:', err);
|
||||
});
|
||||
|
||||
// ── Load Laporan Terbaru ──
|
||||
fetch(`${API}?action=laporan_terbaru&limit=15`).then(r => r.json()).then(data => {
|
||||
const body = document.getElementById('laporan-body');
|
||||
if (!data.length) {
|
||||
body.innerHTML = '<div class="loading">Belum ada laporan masuk</div>';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
const statusColors = { 'Pending': '#eab308', 'Diproses': '#3b82f6', 'Diverifikasi': '#10b981', 'Ditolak': '#ef4444', 'Selesai': '#10b981' };
|
||||
data.forEach(d => {
|
||||
const sc = statusColors[d.status] || '#888';
|
||||
html += `<div class="laporan-item">
|
||||
<div class="laporan-info">
|
||||
<div class="laporan-nama">${d.nama_pelapor}</div>
|
||||
<div class="laporan-desc">${d.deskripsi.substring(0, 60)}${d.deskripsi.length > 60 ? '...' : ''}</div>
|
||||
</div>
|
||||
<span class="status-badge" style="background:${sc}22;color:${sc};border:1px solid ${sc}">${d.status}</span>
|
||||
</div>`;
|
||||
});
|
||||
body.innerHTML = html;
|
||||
});
|
||||
|
||||
// ── Mini Map + Heatmap ──
|
||||
const miniMap = L.map('mini-map', { zoomControl: false }).setView([-0.0553, 109.3495], 12);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OSM', maxZoom: 18
|
||||
}).addTo(miniMap);
|
||||
|
||||
fetch(`${API}?action=heatmap`).then(r => r.json()).then(points => {
|
||||
if (points.length) {
|
||||
L.heatLayer(points, {
|
||||
radius: 25, blur: 18, maxZoom: 15,
|
||||
gradient: { 0.2: '#2563eb', 0.4: '#f59e0b', 0.6: '#f97316', 0.8: '#ef4444', 1: '#dc2626' }
|
||||
}).addTo(miniMap);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Export CSV ──
|
||||
function exportCSV() {
|
||||
fetch(`${API}?action=kemiskinan`).then(r => r.json()).then(data => {
|
||||
if (!data.length) { alert('Tidak ada data'); return; }
|
||||
const headers = ['ID','Nama KK','NIK','Kategori','Status Bantuan','Jumlah KK','Alamat','Pendidikan','Riwayat Penyakit','Lat','Lng'];
|
||||
const rows = data.map(d => [
|
||||
d.id, d.nama_kk, d.nik || '', d.kategori || '', d.status_bantuan || 'Belum',
|
||||
d.jumlah_kk, '"' + (d.alamat || '').replace(/"/g, '""') + '"',
|
||||
d.pendidikan || '', d.riwayat_penyakit || '',
|
||||
d.latitude, d.longitude
|
||||
]);
|
||||
let csv = '\uFEFF' + headers.join(',') + '\n';
|
||||
rows.forEach(r => csv += r.join(',') + '\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `laporan_kemiskinan_walikota_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,88 @@
|
||||
-- ============================================================
|
||||
-- DATABASE: webgis_p3
|
||||
-- Pertemuan 3 – Layer Groups and Layers Control
|
||||
-- Data SPBU dengan 2 Layer Group: Buka 24 Jam & Tidak 24 Jam
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_p3
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_p3;
|
||||
|
||||
-- ---- Tabel Data Jalan (Polyline) ----
|
||||
CREATE TABLE IF NOT EXISTS data_jalan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_jalan VARCHAR(200) NOT NULL,
|
||||
status ENUM('Nasional','Provinsi','Kabupaten') NOT NULL DEFAULT 'Kabupaten',
|
||||
panjang_m DOUBLE NOT NULL DEFAULT 0,
|
||||
geojson LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ---- Tabel Data Parsil Tanah (Polygon) ----
|
||||
CREATE TABLE IF NOT EXISTS data_parsil (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
no_parsil VARCHAR(100) NOT NULL,
|
||||
pemilik VARCHAR(200) NOT NULL,
|
||||
status ENUM('SHM','HGB','HGU','HP') NOT NULL DEFAULT 'SHM',
|
||||
luas_m2 DOUBLE NOT NULL DEFAULT 0,
|
||||
geojson LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ---- Tabel Data SPBU (Point/Marker) ----
|
||||
-- buka_24_jam: 'Ya' → Layer Group "SPBU 24 Jam"
|
||||
-- 'Tidak' → Layer Group "SPBU Tidak 24 Jam"
|
||||
CREATE TABLE IF NOT EXISTS data_spbu (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_spbu VARCHAR(200) NOT NULL,
|
||||
no_wa VARCHAR(20) NOT NULL,
|
||||
buka_24_jam ENUM('Ya','Tidak') NOT NULL DEFAULT 'Tidak',
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ---- Data Contoh SPBU (opsional, untuk testing) ----
|
||||
INSERT INTO data_spbu (nama_spbu, no_wa, buka_24_jam, latitude, longitude) VALUES
|
||||
('SPBU Jl. Ahmad Yani', '0812-1111-0001', 'Ya', -0.0400, 109.3200),
|
||||
('SPBU Jl. Gajah Mada', '0812-1111-0002', 'Ya', -0.0520, 109.3450),
|
||||
('SPBU Jl. Diponegoro', '0812-1111-0003', 'Tidak', -0.0610, 109.3600),
|
||||
('SPBU Jl. Sutan Syahrir', '0812-1111-0004', 'Tidak', -0.0480, 109.3700),
|
||||
('SPBU Jl. Kom. Yos Sudarso', '0812-1111-0005', 'Ya', -0.0350, 109.3550);
|
||||
|
||||
-- ============================================================
|
||||
-- Tabel Data Kemiskinan (Point/Marker)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS data_kemiskinan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_kk VARCHAR(200) NOT NULL, -- Nama kepala keluarga
|
||||
jumlah_kk INT NOT NULL DEFAULT 1, -- Jumlah anggota dalam KK
|
||||
alamat TEXT NOT NULL,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- Tabel Data Rumah Ibadah (Point/Marker + Radius Buffer)
|
||||
-- jenis: Masjid, Gereja, Pura, Vihara, Klenteng
|
||||
-- radius_m: radius lingkaran buffer dalam meter (100 – 5000)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS data_masjid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_masjid VARCHAR(200) NOT NULL,
|
||||
jenis ENUM('Masjid','Gereja','Pura','Vihara','Klenteng') NOT NULL DEFAULT 'Masjid',
|
||||
pic_masjid VARCHAR(200) NOT NULL, -- Person In Charge
|
||||
alamat TEXT NOT NULL,
|
||||
radius_m INT NOT NULL DEFAULT 500, -- Radius buffer (100-5000 m)
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -0,0 +1,132 @@
|
||||
-- ============================================================
|
||||
-- DATABASE LENGKAP: webgis_p3
|
||||
-- WebGIS Poverty Mapping + Sistem Login (3 Role)
|
||||
-- Import file ini ke Laragon phpMyAdmin
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_p3
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_p3;
|
||||
|
||||
-- ── Tabel Data Jalan (Polyline) ────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS data_jalan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_jalan VARCHAR(200) NOT NULL,
|
||||
status ENUM('Nasional','Provinsi','Kabupaten') NOT NULL DEFAULT 'Kabupaten',
|
||||
panjang_m DOUBLE NOT NULL DEFAULT 0,
|
||||
geojson LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Tabel Data Parsil Tanah (Polygon) ─────────────────────
|
||||
CREATE TABLE IF NOT EXISTS data_parsil (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
no_parsil VARCHAR(100) NOT NULL,
|
||||
pemilik VARCHAR(200) NOT NULL,
|
||||
status ENUM('SHM','HGB','HGU','HP') NOT NULL DEFAULT 'SHM',
|
||||
luas_m2 DOUBLE NOT NULL DEFAULT 0,
|
||||
geojson LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Tabel Data SPBU (Point/Marker) ────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS data_spbu (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_spbu VARCHAR(200) NOT NULL,
|
||||
no_wa VARCHAR(20) NOT NULL,
|
||||
buka_24_jam ENUM('Ya','Tidak') NOT NULL DEFAULT 'Tidak',
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Data Contoh SPBU ──────────────────────────────────────
|
||||
INSERT INTO data_spbu (nama_spbu, no_wa, buka_24_jam, latitude, longitude) VALUES
|
||||
('SPBU Jl. Ahmad Yani', '0812-1111-0001', 'Ya', -0.0400, 109.3200),
|
||||
('SPBU Jl. Gajah Mada', '0812-1111-0002', 'Ya', -0.0520, 109.3450),
|
||||
('SPBU Jl. Diponegoro', '0812-1111-0003', 'Tidak', -0.0610, 109.3600),
|
||||
('SPBU Jl. Sutan Syahrir', '0812-1111-0004', 'Tidak', -0.0480, 109.3700),
|
||||
('SPBU Jl. Kom. Yos Sudarso', '0812-1111-0005', 'Ya', -0.0350, 109.3550);
|
||||
|
||||
-- ── Tabel Data Kemiskinan (Point/Marker) ──────────────────
|
||||
CREATE TABLE IF NOT EXISTS data_kemiskinan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_kk VARCHAR(200) NOT NULL,
|
||||
jumlah_kk INT NOT NULL DEFAULT 1,
|
||||
alamat TEXT NOT NULL,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Tabel Data Rumah Ibadah (Point/Marker + Radius Buffer)
|
||||
CREATE TABLE IF NOT EXISTS data_masjid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_masjid VARCHAR(200) NOT NULL,
|
||||
jenis ENUM('Masjid','Mushola','Gereja','Pura','Vihara','Klenteng') NOT NULL DEFAULT 'Masjid',
|
||||
pic_masjid VARCHAR(200) NOT NULL,
|
||||
alamat TEXT NOT NULL,
|
||||
radius_m INT NOT NULL DEFAULT 500,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Tabel Users (Sistem Login 3 Role) ─────────────────────
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
nama_lengkap VARCHAR(200) NOT NULL,
|
||||
role ENUM('admin','petugas','pengurus','walikota') NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
aktif TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_aktif (aktif)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Akun Demo ─────────────────────────────────────────────
|
||||
-- Password di-hash menggunakan bcrypt (PHP password_hash)
|
||||
-- admin → admin123
|
||||
-- petugas → petugas123
|
||||
-- pengurus → pengurus123
|
||||
--
|
||||
-- Hash ini sudah valid, digenerate dengan PASSWORD_DEFAULT PHP 8
|
||||
INSERT INTO users (username, nama_lengkap, role, password_hash) VALUES
|
||||
(
|
||||
'admin',
|
||||
'Administrator Sistem',
|
||||
'admin',
|
||||
'$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'
|
||||
),
|
||||
(
|
||||
'petugas',
|
||||
'Petugas Lapangan',
|
||||
'petugas',
|
||||
'$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'
|
||||
),
|
||||
(
|
||||
'pengurus',
|
||||
'Pengurus Rumah Ibadah',
|
||||
'pengurus',
|
||||
'$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'
|
||||
),
|
||||
(
|
||||
'walikota',
|
||||
'Walikota Pontianak',
|
||||
'walikota',
|
||||
'$2y$10$s0387VqXY8wBXYkVytEKxumpF0DmlBlE1z3usinr0nr1RYN/4BZw.'
|
||||
);
|
||||
|
||||
-- !! CATATAN: Hash di atas adalah placeholder "password".
|
||||
-- Setelah import, jalankan generate_hash.php untuk meng-update
|
||||
-- hash dengan password yang benar (admin123, petugas123, pengurus123).
|
||||
-- URL: http://localhost/WebGIS/pertemuan fix/generate_hash.php
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// generate_hash.php – Generate bcrypt hash & insert akun demo
|
||||
// Akses sekali: http://localhost/WebGIS/pertemuan fix/generate_hash.php
|
||||
// HAPUS file ini setelah digunakan!
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
// Daftar akun demo yang akan di-insert / update
|
||||
$demoUsers = [
|
||||
['username' => 'admin', 'nama' => 'Administrator Sistem', 'role' => 'admin', 'password' => 'admin123'],
|
||||
['username' => 'pengurus', 'nama' => 'Pengurus Rumah Ibadah', 'role' => 'pengurus', 'password' => 'pengurus123'],
|
||||
['username' => 'walikota', 'nama' => 'Walikota Pontianak', 'role' => 'walikota', 'password' => 'walikota123'],
|
||||
];
|
||||
|
||||
$db = getDB();
|
||||
$results = [];
|
||||
|
||||
foreach ($demoUsers as $u) {
|
||||
$hash = password_hash($u['password'], PASSWORD_DEFAULT);
|
||||
|
||||
// INSERT or UPDATE
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO users (username, nama_lengkap, role, password_hash)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
nama_lengkap = VALUES(nama_lengkap),
|
||||
role = VALUES(role),
|
||||
password_hash = VALUES(password_hash)"
|
||||
);
|
||||
$stmt->bind_param('ssss', $u['username'], $u['nama'], $u['role'], $hash);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$results[] = [
|
||||
'status' => '✅ OK',
|
||||
'username' => $u['username'],
|
||||
'role' => $u['role'],
|
||||
'hash' => $hash
|
||||
];
|
||||
} else {
|
||||
$results[] = [
|
||||
'status' => '❌ ERROR: ' . $stmt->error,
|
||||
'username' => $u['username'],
|
||||
'role' => $u['role'],
|
||||
'hash' => '-'
|
||||
];
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
$db->close();
|
||||
|
||||
// Output sebagai HTML yang mudah dibaca
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Generate Hash – WebGIS</title>
|
||||
<style>
|
||||
body { font-family: monospace; background:#111; color:#e2e8f0; padding:30px; }
|
||||
h2 { color:#60a5fa; margin-bottom:20px; }
|
||||
table { border-collapse:collapse; width:100%; }
|
||||
th,td { border:1px solid #334155; padding:10px 14px; text-align:left; font-size:13px; }
|
||||
th { background:#1e293b; color:#94a3b8; font-weight:700; }
|
||||
.ok { color:#4ade80; }
|
||||
.err { color:#f87171; }
|
||||
.warn { background:#1c1917; border:1px solid #f59e0b; color:#fbbf24; padding:16px; border-radius:8px; margin-top:24px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>🔑 Generate Hash – Akun Demo WebGIS</h2>
|
||||
<table>
|
||||
<tr><th>Status</th><th>Username</th><th>Role</th><th>Password</th><th>Hash (bcrypt)</th></tr>
|
||||
<?php foreach($results as $r): ?>
|
||||
<tr>
|
||||
<td class="<?= str_starts_with($r['status'],'✅') ? 'ok':'err' ?>"><?= $r['status'] ?></td>
|
||||
<td><?= htmlspecialchars($r['username']) ?></td>
|
||||
<td><?= htmlspecialchars($r['role']) ?></td>
|
||||
<td><?= ($r['username'].'123') ?></td>
|
||||
<td style="font-size:11px;word-break:break-all"><?= htmlspecialchars($r['hash']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<div class="warn">
|
||||
⚠️ <strong>PERHATIAN:</strong> File ini sudah tidak diperlukan. Hapus <code>generate_hash.php</code> setelah proses selesai untuk keamanan sistem.
|
||||
</div>
|
||||
<p style="margin-top:20px;color:#64748b">
|
||||
Setelah berhasil, buka <a href="login.html" style="color:#60a5fa">login.html</a> dan coba login.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,846 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Login – WebGIS Pemetaan Kemiskinan Berbasis Partisipasi Rumah Ibadah">
|
||||
<title>Masuk – WebGIS Poverty Mapping</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0e1a;
|
||||
--panel: #0f1422;
|
||||
--card: #141928;
|
||||
--border: rgba(255, 255, 255, .07);
|
||||
--blue: #4f7cff;
|
||||
--blue-dim: rgba(79, 124, 255, .12);
|
||||
--green: #22d3a5;
|
||||
--orange: #ff8c42;
|
||||
--purple: #9b6dff;
|
||||
--text: #e8edf5;
|
||||
--muted: #5c6880;
|
||||
--sub: #8a96b0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 480px;
|
||||
}
|
||||
|
||||
/* ── LEFT PANEL ── */
|
||||
.left-panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #080c18;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.map-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 70% 60% at 30% 40%, rgba(79, 124, 255, .18) 0%, transparent 65%),
|
||||
radial-gradient(ellipse 50% 70% at 70% 70%, rgba(34, 211, 165, .1) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 40% 40% at 60% 20%, rgba(155, 109, 255, .08) 0%, transparent 55%);
|
||||
}
|
||||
|
||||
/* Animated grid lines */
|
||||
.grid-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, .025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, .025) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
animation: gridShift 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gridShift {
|
||||
from {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 60px 60px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Floating map dots */
|
||||
.dot {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dot-1 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--blue);
|
||||
box-shadow: 0 0 20px var(--blue), 0 0 40px rgba(79, 124, 255, .4);
|
||||
top: 32%;
|
||||
left: 28%;
|
||||
animation: pulseDot 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dot-2 {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 15px var(--green);
|
||||
top: 55%;
|
||||
left: 55%;
|
||||
animation: pulseDot 3s ease-in-out infinite 1s;
|
||||
}
|
||||
|
||||
.dot-3 {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--orange);
|
||||
box-shadow: 0 0 15px var(--orange);
|
||||
top: 22%;
|
||||
left: 60%;
|
||||
animation: pulseDot 3s ease-in-out infinite 2s;
|
||||
}
|
||||
|
||||
.dot-4 {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: var(--purple);
|
||||
box-shadow: 0 0 12px var(--purple);
|
||||
top: 68%;
|
||||
left: 35%;
|
||||
animation: pulseDot 3s ease-in-out infinite .5s;
|
||||
}
|
||||
|
||||
/* Connecting lines between dots */
|
||||
.conn-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.conn-lines svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@keyframes pulseDot {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.4);
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ripple rings on dots */
|
||||
.dot::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -8px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid currentColor;
|
||||
opacity: 0;
|
||||
animation: ripple 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dot-1::after {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.dot-2::after {
|
||||
color: var(--green);
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.dot-3::after {
|
||||
color: var(--orange);
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.dot-4::after {
|
||||
color: var(--purple);
|
||||
animation-delay: .5s;
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
inset: -4px;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
100% {
|
||||
inset: -24px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.left-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.left-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
background: rgba(79, 124, 255, .15);
|
||||
border: 1px solid rgba(79, 124, 255, .3);
|
||||
border-radius: 20px;
|
||||
padding: 5px 14px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #7ba4ff;
|
||||
letter-spacing: .5px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.left-tag-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--blue);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px var(--blue);
|
||||
animation: pulseDot 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.left-title {
|
||||
font-size: clamp(28px, 3.5vw, 42px);
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -.5px;
|
||||
color: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.left-title em {
|
||||
font-style: normal;
|
||||
background: linear-gradient(90deg, var(--blue), var(--green));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.left-desc {
|
||||
font-size: 14px;
|
||||
color: var(--sub);
|
||||
line-height: 1.7;
|
||||
max-width: 400px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-lbl {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-sep {
|
||||
width: 1px;
|
||||
background: var(--border);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
/* ── RIGHT PANEL ── */
|
||||
.right-panel {
|
||||
background: var(--panel);
|
||||
border-left: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 48px 40px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.logo-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #1a3a8f, var(--blue));
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 6px 20px rgba(79, 124, 255, .3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-name {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
letter-spacing: -.2px;
|
||||
}
|
||||
|
||||
.logo-sub {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
letter-spacing: -.4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-sub {
|
||||
font-size: 13px;
|
||||
color: var(--sub);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
/* Role tabs */
|
||||
.role-tabs {
|
||||
display: flex;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.role-tab {
|
||||
flex: 1;
|
||||
padding: 8px 6px;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.role-tab-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.role-tab-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.role-tab:hover {
|
||||
color: var(--text);
|
||||
background: rgba(255, 255, 255, .04);
|
||||
}
|
||||
|
||||
.role-tab.active {
|
||||
background: var(--blue-dim);
|
||||
color: var(--blue);
|
||||
border: 1px solid rgba(79, 124, 255, .25);
|
||||
}
|
||||
|
||||
.role-tab.active[data-role="pengurus"] {
|
||||
color: var(--purple);
|
||||
background: rgba(155, 109, 255, .1);
|
||||
border-color: rgba(155, 109, 255, .25);
|
||||
}
|
||||
|
||||
.role-tab.active[data-role="walikota"] {
|
||||
color: var(--orange);
|
||||
background: rgba(255, 140, 66, .1);
|
||||
border-color: rgba(255, 140, 66, .25);
|
||||
}
|
||||
|
||||
/* Fields */
|
||||
.field {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--sub);
|
||||
margin-bottom: 7px;
|
||||
letter-spacing: .3px;
|
||||
}
|
||||
|
||||
.field-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
width: 100%;
|
||||
padding: 12px 14px 12px 42px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 13.5px;
|
||||
outline: none;
|
||||
transition: border-color .2s, box-shadow .2s;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
border-color: rgba(79, 124, 255, .5);
|
||||
box-shadow: 0 0 0 3px rgba(79, 124, 255, .1);
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pw-toggle {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
transition: color .2s;
|
||||
}
|
||||
|
||||
.pw-toggle:hover {
|
||||
color: var(--sub);
|
||||
}
|
||||
|
||||
/* Error */
|
||||
.error-msg {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(239, 68, 68, .08);
|
||||
border: 1px solid rgba(239, 68, 68, .2);
|
||||
color: #fca5a5;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 12.5px;
|
||||
margin-bottom: 18px;
|
||||
animation: shake .3s ease;
|
||||
}
|
||||
|
||||
.error-msg.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Login button */
|
||||
.btn-masuk {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: var(--blue);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all .25s;
|
||||
box-shadow: 0 4px 20px rgba(79, 124, 255, .35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.btn-masuk:hover {
|
||||
background: #6b91ff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 28px rgba(79, 124, 255, .45);
|
||||
}
|
||||
|
||||
.btn-masuk:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-masuk.loading {
|
||||
opacity: .7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Quick fill */
|
||||
.quick-fill-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.quick-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
flex: 1;
|
||||
padding: 9px 8px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--sub);
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.quick-btn:hover {
|
||||
background: rgba(255, 255, 255, .05);
|
||||
border-color: rgba(255, 255, 255, .15);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
text-align: center;
|
||||
margin-top: 28px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
padding: 36px 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- LEFT: Map Visual Panel -->
|
||||
<div class="left-panel">
|
||||
<div class="map-bg"></div>
|
||||
<div class="grid-lines"></div>
|
||||
|
||||
<!-- Animated map dots -->
|
||||
<div class="dot dot-1"></div>
|
||||
<div class="dot dot-2"></div>
|
||||
<div class="dot dot-3"></div>
|
||||
<div class="dot dot-4"></div>
|
||||
|
||||
<!-- SVG connecting lines -->
|
||||
<div class="conn-lines">
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="28" y1="32" x2="55" y2="55" stroke="rgba(79,124,255,.2)" stroke-width=".3"
|
||||
stroke-dasharray="2 2" />
|
||||
<line x1="55" y1="55" x2="60" y2="22" stroke="rgba(34,211,165,.15)" stroke-width=".3"
|
||||
stroke-dasharray="2 2" />
|
||||
<line x1="28" y1="32" x2="35" y2="68" stroke="rgba(155,109,255,.15)" stroke-width=".3"
|
||||
stroke-dasharray="2 2" />
|
||||
<line x1="35" y1="68" x2="55" y2="55" stroke="rgba(255,140,66,.12)" stroke-width=".3"
|
||||
stroke-dasharray="2 2" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="left-content">
|
||||
<div class="left-tag">
|
||||
<div class="left-tag-dot"></div>
|
||||
Sistem Aktif
|
||||
</div>
|
||||
<h1 class="left-title">
|
||||
Peta Kemiskinan<br>Berbasis <em>Rumah Ibadah</em>
|
||||
</h1>
|
||||
<p class="left-desc">
|
||||
Platform pemetaan partisipatif yang menghubungkan data kemiskinan dengan jaringan rumah ibadah untuk
|
||||
perencanaan program yang lebih tepat sasaran.
|
||||
</p>
|
||||
<div class="stat-row">
|
||||
<div class="stat">
|
||||
<span class="stat-val" style="color:var(--blue)">3</span>
|
||||
<span class="stat-lbl">Jenis Peran</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat">
|
||||
<span class="stat-val" style="color:var(--green)">6+</span>
|
||||
<span class="stat-lbl">Jenis Rumah Ibadah</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat">
|
||||
<span class="stat-val" style="color:var(--orange)">GIS</span>
|
||||
<span class="stat-lbl">Berbasis Peta</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: Login Panel -->
|
||||
<div class="right-panel">
|
||||
|
||||
<div class="logo-row">
|
||||
<div class="logo-icon">🗺️</div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-name">WebGIS Poverty Mapping</span>
|
||||
<span class="logo-sub">Sistem Informasi Geografis</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="form-title">Masuk ke Sistem</h2>
|
||||
<p class="form-sub">Pilih peran, lalu masukkan kredensial Anda.</p>
|
||||
|
||||
<!-- Role Tabs -->
|
||||
<div class="role-tabs" id="role-tabs">
|
||||
<button class="role-tab" id="tab-admin" data-role="admin" onclick="pilihRole('admin')">
|
||||
<span class="role-tab-icon">🛡️</span>
|
||||
<span class="role-tab-name">Admin</span>
|
||||
</button>
|
||||
<button class="role-tab" id="tab-pengurus" data-role="pengurus" onclick="pilihRole('pengurus')">
|
||||
<span class="role-tab-icon">🕌</span>
|
||||
<span class="role-tab-name">Pengurus</span>
|
||||
</button>
|
||||
<button class="role-tab" id="tab-walikota" data-role="walikota" onclick="pilihRole('walikota')">
|
||||
<span class="role-tab-icon">👑</span>
|
||||
<span class="role-tab-name">Walikota</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div class="error-msg" id="error-msg">
|
||||
<span>⚠️</span>
|
||||
<span id="error-text">Username atau password salah.</span>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form id="login-form" onsubmit="handleLogin(event)" novalidate>
|
||||
<div class="field">
|
||||
<label class="field-label" for="inp-username">Username</label>
|
||||
<div class="field-wrap">
|
||||
<span class="field-icon">👤</span>
|
||||
<input class="field-input" type="text" id="inp-username" placeholder="Masukkan username"
|
||||
autocomplete="username" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="inp-password">Password</label>
|
||||
<div class="field-wrap">
|
||||
<span class="field-icon">🔑</span>
|
||||
<input class="field-input" type="password" id="inp-password" placeholder="Masukkan password"
|
||||
autocomplete="current-password" required>
|
||||
<button type="button" class="pw-toggle" id="pw-toggle" onclick="togglePw()">👁️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-masuk" id="btn-masuk">
|
||||
<span id="btn-text">Masuk</span>
|
||||
<span id="btn-arrow">→</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<a href="index.html" style="display:flex; justify-content:center; align-items:center; gap:8px; padding:14px; background:rgba(255,255,255,0.03); color:var(--text); border:1px solid var(--border); border-radius:10px; text-decoration:none; font-size:13.5px; font-weight:600; transition:all 0.25s;" onmouseover="this.style.background='rgba(255,255,255,0.08)'" onmouseout="this.style.background='rgba(255,255,255,0.03)'">
|
||||
<span>🗺️ Lihat Peta & Lapor (Akses Warga)</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Quick Fill -->
|
||||
<div class="quick-fill-label">Akun Demo — Klik untuk Isi Otomatis</div>
|
||||
<div class="quick-row">
|
||||
<button class="quick-btn" onclick="isiDemo('admin','admin123','admin')">🛡️ Admin</button>
|
||||
<button class="quick-btn" onclick="isiDemo('pengurus','pengurus123','pengurus')">🕌 Pengurus</button>
|
||||
<button class="quick-btn" onclick="isiDemo('walikota','walikota123','walikota')">👑 Walikota</button>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">WebGIS Poverty Mapping · 2026</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let roleSelected = null;
|
||||
|
||||
function pilihRole(role) {
|
||||
roleSelected = role;
|
||||
document.querySelectorAll('.role-tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('tab-' + role).classList.add('active');
|
||||
}
|
||||
|
||||
function isiDemo(user, pass, role) {
|
||||
document.getElementById('inp-username').value = user;
|
||||
document.getElementById('inp-password').value = pass;
|
||||
pilihRole(role);
|
||||
sembunyiError();
|
||||
}
|
||||
|
||||
function togglePw() {
|
||||
const inp = document.getElementById('inp-password');
|
||||
const btn = document.getElementById('pw-toggle');
|
||||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||
btn.textContent = inp.type === 'password' ? '👁️' : '🙈';
|
||||
}
|
||||
|
||||
function tampilError(msg) {
|
||||
const el = document.getElementById('error-msg');
|
||||
document.getElementById('error-text').textContent = msg;
|
||||
el.classList.add('show');
|
||||
el.style.animation = 'none';
|
||||
el.offsetHeight;
|
||||
el.style.animation = '';
|
||||
}
|
||||
|
||||
function sembunyiError() {
|
||||
document.getElementById('error-msg').classList.remove('show');
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
sembunyiError();
|
||||
|
||||
const username = document.getElementById('inp-username').value.trim();
|
||||
const password = document.getElementById('inp-password').value;
|
||||
|
||||
if (!username) { tampilError('Username wajib diisi.'); return; }
|
||||
if (!password) { tampilError('Password wajib diisi.'); return; }
|
||||
if (!roleSelected) { tampilError('Pilih peran terlebih dahulu.'); return; }
|
||||
|
||||
const btn = document.getElementById('btn-masuk');
|
||||
const btnText = document.getElementById('btn-text');
|
||||
const btnArrow = document.getElementById('btn-arrow');
|
||||
btn.classList.add('loading');
|
||||
btnText.textContent = 'Memeriksa...';
|
||||
btnArrow.textContent = '⏳';
|
||||
|
||||
try {
|
||||
const res = await fetch('api_auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, role: roleSelected })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
sessionStorage.setItem('webgis_user', JSON.stringify({
|
||||
id: data.user.id,
|
||||
username: data.user.username,
|
||||
nama: data.user.nama,
|
||||
role: data.user.role
|
||||
}));
|
||||
btnText.textContent = 'Berhasil!';
|
||||
btnArrow.textContent = '✓';
|
||||
btn.style.background = '#22d3a5';
|
||||
const target = data.user.role === 'walikota' ? 'dashboard_walikota.html' : 'index.html';
|
||||
setTimeout(() => { window.location.href = target; }, 500);
|
||||
} else {
|
||||
tampilError(data.message || 'Username, password, atau peran tidak sesuai.');
|
||||
btn.classList.remove('loading');
|
||||
btnText.textContent = 'Masuk';
|
||||
btnArrow.textContent = '→';
|
||||
}
|
||||
} catch {
|
||||
tampilError('Gagal terhubung ke server. Pastikan Apache & MySQL aktif.');
|
||||
btn.classList.remove('loading');
|
||||
btnText.textContent = 'Masuk';
|
||||
btnArrow.textContent = '→';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle logout redirect
|
||||
if (new URLSearchParams(location.search).get('logout')) {
|
||||
sessionStorage.removeItem('webgis_user');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,101 @@
|
||||
-- ============================================================
|
||||
-- MIGRASI: Penyesuaian Proses Bisnis WebGIS v2.0
|
||||
-- Jalankan di phpMyAdmin setelah database_lengkap.sql
|
||||
-- ============================================================
|
||||
|
||||
USE webgis_p3;
|
||||
|
||||
-- ── 1. Tambah kolom NIK, No WA, dan alasan_tolak ke laporan_masyarakat ──
|
||||
-- Periksa apakah kolom sudah ada sebelum menambahkan
|
||||
|
||||
-- Tambah kolom nik
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'webgis_p3' AND TABLE_NAME = 'laporan_masyarakat' AND COLUMN_NAME = 'nik');
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE laporan_masyarakat ADD COLUMN nik VARCHAR(20) DEFAULT NULL AFTER nama_pelapor',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Tambah kolom no_wa
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'webgis_p3' AND TABLE_NAME = 'laporan_masyarakat' AND COLUMN_NAME = 'no_wa');
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE laporan_masyarakat ADD COLUMN no_wa VARCHAR(20) DEFAULT NULL AFTER nik',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Tambah kolom alasan_tolak
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'webgis_p3' AND TABLE_NAME = 'laporan_masyarakat' AND COLUMN_NAME = 'alasan_tolak');
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE laporan_masyarakat ADD COLUMN alasan_tolak TEXT DEFAULT NULL AFTER status',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ── 2. Perluas ENUM status di laporan_masyarakat ──
|
||||
-- Tambah status: Diverifikasi, Ditolak
|
||||
ALTER TABLE laporan_masyarakat
|
||||
MODIFY COLUMN status ENUM('Pending','Diproses','Diverifikasi','Ditolak','Selesai')
|
||||
NOT NULL DEFAULT 'Pending';
|
||||
|
||||
-- ── 3. Pastikan tabel riwayat_bantuan ada ──
|
||||
CREATE TABLE IF NOT EXISTS riwayat_bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
id_kemiskinan INT NOT NULL,
|
||||
pemberi VARCHAR(200) NOT NULL,
|
||||
jenis_bantuan VARCHAR(200) NOT NULL,
|
||||
tanggal DATE NOT NULL,
|
||||
keterangan TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (id_kemiskinan) REFERENCES data_kemiskinan(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── 4. Pastikan tabel laporan_masyarakat ada ──
|
||||
CREATE TABLE IF NOT EXISTS laporan_masyarakat (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_pelapor VARCHAR(200) NOT NULL,
|
||||
nik VARCHAR(20) DEFAULT NULL,
|
||||
no_wa VARCHAR(20) DEFAULT NULL,
|
||||
deskripsi TEXT NOT NULL,
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
status ENUM('Pending','Diproses','Diverifikasi','Ditolak','Selesai') NOT NULL DEFAULT 'Pending',
|
||||
alasan_tolak TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── 5. Tambahkan kolom yang mungkin belum ada di data_kemiskinan ──
|
||||
-- status_bantuan
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'webgis_p3' AND TABLE_NAME = 'data_kemiskinan' AND COLUMN_NAME = 'status_bantuan');
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
"ALTER TABLE data_kemiskinan ADD COLUMN status_bantuan VARCHAR(20) NOT NULL DEFAULT 'Belum' AFTER longitude",
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- kategori
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'webgis_p3' AND TABLE_NAME = 'data_kemiskinan' AND COLUMN_NAME = 'kategori');
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
"ALTER TABLE data_kemiskinan ADD COLUMN kategori VARCHAR(50) NOT NULL DEFAULT 'Miskin' AFTER status_bantuan",
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ── 6. Update password walikota (gunakan generate_hash.php untuk update bcrypt) ──
|
||||
-- Password walikota: walikota123
|
||||
-- Jalankan generate_hash.php setelah migrasi ini untuk update hash
|
||||
|
||||
-- ============================================================
|
||||
-- SELESAI. Jalankan generate_hash.php untuk update password hash.
|
||||
-- URL: http://localhost/WebGIS/pertemuan fix/generate_hash.php
|
||||
-- ============================================================
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
$db = getDB();
|
||||
|
||||
$queries = [
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN tanggal_lahir DATE NULL AFTER nama_pelapor",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN pendidikan VARCHAR(50) NULL AFTER tanggal_lahir",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN riwayat_penyakit VARCHAR(255) NULL AFTER pendidikan",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN skor_penghasilan INT NULL AFTER riwayat_penyakit",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN skor_rumah INT NULL AFTER skor_penghasilan",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN skor_makan INT NULL AFTER skor_rumah",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN jumlah_kk INT DEFAULT 1 AFTER skor_makan"
|
||||
];
|
||||
|
||||
foreach ($queries as $q) {
|
||||
try {
|
||||
if ($db->query($q)) {
|
||||
echo "Success: $q\n<br>";
|
||||
} else {
|
||||
echo "Error/Skipped (might already exist): " . $db->error . " | Query: $q\n<br>";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo "Exception/Skipped: " . $e->getMessage() . " | Query: $q\n<br>";
|
||||
}
|
||||
}
|
||||
$db->close();
|
||||
echo "Migration complete.\n";
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
$db = getDB();
|
||||
mysqli_report(MYSQLI_REPORT_OFF); // REALLY OFF
|
||||
|
||||
$queries = [
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN tanggal_lahir DATE NULL AFTER nama_pelapor",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN pendidikan VARCHAR(50) NULL AFTER tanggal_lahir",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN riwayat_penyakit VARCHAR(255) NULL AFTER pendidikan",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN skor_penghasilan INT NULL AFTER riwayat_penyakit",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN skor_rumah INT NULL AFTER skor_penghasilan",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN skor_makan INT NULL AFTER skor_rumah",
|
||||
"ALTER TABLE laporan_masyarakat ADD COLUMN jumlah_kk INT DEFAULT 1 AFTER skor_makan"
|
||||
];
|
||||
|
||||
foreach ($queries as $q) {
|
||||
try {
|
||||
if (@$db->query($q)) {
|
||||
echo "Success: $q\n<br>";
|
||||
} else {
|
||||
echo "Error/Skipped: " . $db->error . " | Query: $q\n<br>";
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Exception/Skipped: " . $e->getMessage() . " | Query: $q\n<br>";
|
||||
}
|
||||
}
|
||||
$db->close();
|
||||
echo "Migration complete.\n";
|
||||
?>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
// Test 1: localhost
|
||||
$c1 = @new mysqli('localhost', 'root', '', 'webgis_p3');
|
||||
echo "localhost: " . ($c1->connect_error ? "GAGAL - ".$c1->connect_error : "OK") . "<br>";
|
||||
|
||||
// Test 2: 127.0.0.1
|
||||
$c2 = @new mysqli('127.0.0.1', 'root', '', 'webgis_p3');
|
||||
echo "127.0.0.1: " . ($c2->connect_error ? "GAGAL - ".$c2->connect_error : "OK") . "<br>";
|
||||
|
||||
phpinfo();
|
||||
?>
|
||||
@@ -0,0 +1,89 @@
|
||||
-- ============================================================
|
||||
-- MIGRASI: Tabel Users & Role-Based Access Control
|
||||
-- WebGIS Poverty Mapping – Sistem Autentikasi
|
||||
-- Jalankan di phpMyAdmin atau MySQL CLI setelah database.sql
|
||||
-- ============================================================
|
||||
|
||||
USE webgis_p3;
|
||||
|
||||
-- ── Tabel pengguna ─────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
nama_lengkap VARCHAR(200) NOT NULL,
|
||||
role ENUM('admin','petugas','pengurus','walikota') NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
aktif TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_aktif (aktif)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Akun demo (password di-hash dengan bcrypt PHP PASSWORD_DEFAULT) ──
|
||||
-- Gunakan script generate_hash.php untuk membuat hash baru jika perlu.
|
||||
--
|
||||
-- admin → admin123
|
||||
-- petugas → petugas123
|
||||
-- pengurus → pengurus123
|
||||
--
|
||||
-- Hash di bawah digenerate dengan PHP:
|
||||
-- password_hash('admin123', PASSWORD_DEFAULT) → $2y$12$...
|
||||
-- password_hash('petugas123', PASSWORD_DEFAULT) → $2y$12$...
|
||||
-- password_hash('pengurus123', PASSWORD_DEFAULT) → $2y$12$...
|
||||
--
|
||||
-- !! Jangan gunakan plain text password di produksi !!
|
||||
|
||||
INSERT INTO users (username, nama_lengkap, role, password_hash) VALUES
|
||||
(
|
||||
'admin',
|
||||
'Administrator Sistem',
|
||||
'admin',
|
||||
'$2y$12$YourHashHereReplaceMe.admin123HashGeneratedByPHP'
|
||||
),
|
||||
(
|
||||
'petugas',
|
||||
'Petugas Lapangan',
|
||||
'petugas',
|
||||
'$2y$12$YourHashHereReplaceMe.petugas123HashGeneratedByPHP'
|
||||
),
|
||||
(
|
||||
'pengurus',
|
||||
'Pengurus Rumah Ibadah',
|
||||
'pengurus',
|
||||
'$2y$12$YourHashHereReplaceMe.pengurus123HashGeneratedByPHP'
|
||||
),
|
||||
(
|
||||
'walikota',
|
||||
'Walikota Pontianak',
|
||||
'walikota',
|
||||
'$2y$10$s0387VqXY8wBXYkVytEKxumpF0DmlBlE1z3usinr0nr1RYN/4BZw.'
|
||||
);
|
||||
|
||||
-- !! PENTING: Setelah import tabel di atas, jalankan generate_hash.php
|
||||
-- untuk mengisi password_hash yang benar secara otomatis. !!
|
||||
|
||||
-- ── Catatan hak akses per role ──────────────────────────────
|
||||
/*
|
||||
ROLE: admin
|
||||
─────────────────────────────────────────────────────────────
|
||||
• CRUD semua data: SPBU, kemiskinan, rumah ibadah
|
||||
• Dapat melihat semua layer & analisis spasial
|
||||
• Fitur FAB Tambah: SPBU, Kemiskinan, Rumah Ibadah
|
||||
|
||||
ROLE: petugas
|
||||
─────────────────────────────────────────────────────────────
|
||||
• Input & edit data kemiskinan (CREATE + UPDATE + DELETE)
|
||||
• View semua data (read-only untuk SPBU & Rumah Ibadah)
|
||||
• Fitur FAB Tambah: hanya Kemiskinan
|
||||
• Tidak dapat hapus data SPBU atau Rumah Ibadah
|
||||
• Tidak dapat tambah/edit SPBU atau Rumah Ibadah
|
||||
|
||||
ROLE: pengurus
|
||||
─────────────────────────────────────────────────────────────
|
||||
• Input & edit data rumah ibadah miliknya (CREATE + UPDATE + DELETE)
|
||||
• View data kemiskinan (read-only)
|
||||
• Fitur FAB Tambah: hanya Rumah Ibadah
|
||||
• Tidak dapat hapus data SPBU atau kemiskinan
|
||||
• Tidak dapat tambah/edit SPBU atau kemiskinan
|
||||
*/
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API DATA SPBU (Point / Marker)
|
||||
// File: api_spbu.php
|
||||
// Fields: nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude
|
||||
// ============================================================
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
try {
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$jsonInput = $rawInput ? json_decode($rawInput, true) : null;
|
||||
if (!is_array($jsonInput)) {
|
||||
$jsonInput = [];
|
||||
}
|
||||
|
||||
// Fallback jika server memblokir method DELETE/PUT
|
||||
if ($method === 'POST' && isset($_GET['_method'])) {
|
||||
$method = strtoupper($_GET['_method']);
|
||||
} elseif ($method === 'POST' && !empty($jsonInput['_method'])) {
|
||||
$method = strtoupper($jsonInput['_method']);
|
||||
}
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if (!$id && !empty($jsonInput['id'])) {
|
||||
$id = (int)$jsonInput['id'];
|
||||
}
|
||||
|
||||
if ($method === 'GET') {
|
||||
$db = getDB();
|
||||
ensureSpbuSchema($db);
|
||||
|
||||
if ($id) {
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude
|
||||
FROM data_spbu WHERE id = ?"
|
||||
);
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
if ($result) {
|
||||
$result['latitude'] = (float)$result['latitude'];
|
||||
$result['longitude'] = (float)$result['longitude'];
|
||||
echo json_encode($result);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Data SPBU tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
$result = $db->query(
|
||||
"SELECT id, nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude
|
||||
FROM data_spbu ORDER BY id ASC"
|
||||
);
|
||||
if (!$result) {
|
||||
jsonError('Query gagal: ' . $db->error);
|
||||
}
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$rows[] = $row;
|
||||
}
|
||||
echo json_encode($rows, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$input = $jsonInput;
|
||||
|
||||
if (empty($input['nama_spbu']) || empty($input['alamat']) || empty($input['no_wa'])
|
||||
|| !isset($input['latitude']) || !isset($input['longitude'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Data tidak lengkap. Field wajib: nama_spbu, alamat, no_wa, latitude, longitude']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$nama_spbu = trim($input['nama_spbu']);
|
||||
$alamat = trim($input['alamat']);
|
||||
$no_wa = trim($input['no_wa']);
|
||||
$buka_24_jam = isset($input['buka_24_jam']) && $input['buka_24_jam'] === 'Ya' ? 'Ya' : 'Tidak';
|
||||
$latitude = (float)$input['latitude'];
|
||||
$longitude = (float)$input['longitude'];
|
||||
|
||||
$db = getDB();
|
||||
ensureSpbuSchema($db);
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO data_spbu (nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('ssssdd', $nama_spbu, $alamat, $no_wa, $buka_24_jam, $latitude, $longitude);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
http_response_code(201);
|
||||
echo json_encode([
|
||||
'message' => 'Data SPBU berhasil disimpan',
|
||||
'id' => $db->insert_id,
|
||||
'nama_spbu' => $nama_spbu,
|
||||
'alamat' => $alamat,
|
||||
'no_wa' => $no_wa,
|
||||
'buka_24_jam' => $buka_24_jam,
|
||||
'latitude' => $latitude,
|
||||
'longitude' => $longitude
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal menyimpan data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($method === 'PUT') {
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'ID diperlukan untuk update']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$input = $jsonInput;
|
||||
$db = getDB();
|
||||
ensureSpbuSchema($db);
|
||||
|
||||
if (isset($input['latitude']) && isset($input['longitude'])
|
||||
&& !isset($input['nama_spbu'])) {
|
||||
$latitude = (float)$input['latitude'];
|
||||
$longitude = (float)$input['longitude'];
|
||||
$stmt = $db->prepare("UPDATE data_spbu SET latitude = ?, longitude = ? WHERE id = ?");
|
||||
$stmt->bind_param('ddi', $latitude, $longitude, $id);
|
||||
} else {
|
||||
$nama_spbu = trim($input['nama_spbu'] ?? '');
|
||||
$alamat = trim($input['alamat'] ?? '');
|
||||
$no_wa = trim($input['no_wa'] ?? '');
|
||||
$buka_24_jam = isset($input['buka_24_jam']) && $input['buka_24_jam'] === 'Ya' ? 'Ya' : 'Tidak';
|
||||
$latitude = (float)($input['latitude'] ?? 0);
|
||||
$longitude = (float)($input['longitude'] ?? 0);
|
||||
|
||||
if (empty($nama_spbu) || empty($alamat) || empty($no_wa)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'nama_spbu, alamat, dan no_wa wajib diisi']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE data_spbu
|
||||
SET nama_spbu = ?, alamat = ?, no_wa = ?, buka_24_jam = ?, latitude = ?, longitude = ?
|
||||
WHERE id = ?"
|
||||
);
|
||||
$stmt->bind_param('ssssddi', $nama_spbu, $alamat, $no_wa, $buka_24_jam, $latitude, $longitude, $id);
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['message' => 'Data SPBU berhasil diperbarui', 'id' => $id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Gagal memperbarui data: ' . $stmt->error]);
|
||||
}
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($method === 'DELETE') {
|
||||
if (!$id) {
|
||||
jsonError('ID diperlukan untuk hapus', 400);
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
ensureSpbuSchema($db);
|
||||
$stmt = $db->prepare("DELETE FROM data_spbu WHERE id = ?");
|
||||
if (!$stmt) {
|
||||
jsonError('Gagal menyiapkan query hapus: ' . $db->error);
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
if ($stmt->affected_rows > 0) {
|
||||
echo json_encode(['message' => 'Data SPBU berhasil dihapus', 'id' => $id], JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
jsonError('Data SPBU tidak ditemukan', 404);
|
||||
}
|
||||
} else {
|
||||
jsonError('Gagal menghapus data: ' . $stmt->error);
|
||||
}
|
||||
$stmt->close();
|
||||
$db->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
jsonError('Method tidak diizinkan', 405);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
jsonError('Server error: ' . $e->getMessage());
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// KONFIGURASI DATABASE – WebGIS SPBU
|
||||
// File: config.php
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', '');
|
||||
define('DB_NAME', 'webgis_spbu');
|
||||
|
||||
function jsonError($message, $code = 500) {
|
||||
http_response_code($code);
|
||||
echo json_encode(['error' => $message]);
|
||||
exit();
|
||||
}
|
||||
|
||||
function getDB() {
|
||||
mysqli_report(MYSQLI_REPORT_OFF);
|
||||
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS);
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
die(json_encode(['error' => 'Koneksi MySQL gagal: ' . $conn->connect_error]));
|
||||
}
|
||||
|
||||
$dbName = DB_NAME;
|
||||
if (!$conn->query("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")) {
|
||||
http_response_code(500);
|
||||
die(json_encode(['error' => 'Gagal membuat database: ' . $conn->error]));
|
||||
}
|
||||
|
||||
if (!$conn->select_db($dbName)) {
|
||||
http_response_code(500);
|
||||
die(json_encode(['error' => 'Gagal memilih database: ' . $conn->error]));
|
||||
}
|
||||
|
||||
$conn->set_charset('utf8mb4');
|
||||
return $conn;
|
||||
}
|
||||
|
||||
function ensureSpbuSchema($db) {
|
||||
if (!$db->query("
|
||||
CREATE TABLE IF NOT EXISTS data_spbu (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_spbu VARCHAR(200) NOT NULL,
|
||||
alamat VARCHAR(300) NOT NULL DEFAULT '',
|
||||
no_wa VARCHAR(20) NOT NULL,
|
||||
buka_24_jam ENUM('Ya','Tidak') NOT NULL DEFAULT 'Tidak',
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
")) {
|
||||
jsonError('Gagal membuat tabel data_spbu: ' . $db->error);
|
||||
}
|
||||
|
||||
$check = $db->query("SHOW COLUMNS FROM data_spbu LIKE 'alamat'");
|
||||
if ($check && $check->num_rows === 0) {
|
||||
$db->query("ALTER TABLE data_spbu ADD COLUMN alamat VARCHAR(300) NOT NULL DEFAULT '' AFTER nama_spbu");
|
||||
$db->query("UPDATE data_spbu SET alamat = CONCAT(nama_spbu, ', Pontianak') WHERE alamat = ''");
|
||||
}
|
||||
|
||||
$countRes = $db->query("SELECT COUNT(*) AS total FROM data_spbu");
|
||||
if ($countRes && (int)$countRes->fetch_assoc()['total'] === 0) {
|
||||
seedSpbuSampleData($db);
|
||||
}
|
||||
}
|
||||
|
||||
function seedSpbuSampleData($db) {
|
||||
$samples = [
|
||||
['SPBU Jl. Ahmad Yani', 'Jl. Ahmad Yani No. 12, Pontianak', '0812-1111-0001', 'Ya', -0.0400, 109.3200],
|
||||
['SPBU Jl. Gajah Mada', 'Jl. Gajah Mada No. 45, Pontianak', '0812-1111-0002', 'Ya', -0.0520, 109.3450],
|
||||
['SPBU Jl. Diponegoro', 'Jl. Diponegoro No. 88, Pontianak', '0812-1111-0003', 'Tidak', -0.0610, 109.3600],
|
||||
['SPBU Jl. Sutan Syahrir', 'Jl. Sutan Syahrir No. 5, Pontianak', '0812-1111-0004', 'Tidak', -0.0480, 109.3700],
|
||||
['SPBU Jl. Kom. Yos Sudarso', 'Jl. Kom. Yos Sudarso No. 22, Pontianak', '0812-1111-0005', 'Ya', -0.0350, 109.3550],
|
||||
['SPBU Jl. Tanjung Raya', 'Jl. Tanjung Raya No. 101, Pontianak', '0812-1111-0006', 'Tidak', -0.0580, 109.3300],
|
||||
['SPBU Jl. Veteran', 'Jl. Veteran No. 33, Pontianak', '0812-1111-0007', 'Ya', -0.0450, 109.3380],
|
||||
];
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO data_spbu (nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
foreach ($samples as $row) {
|
||||
[$nama, $alamat, $wa, $jam, $lat, $lng] = $row;
|
||||
$stmt->bind_param('ssssdd', $nama, $alamat, $wa, $jam, $lat, $lng);
|
||||
$stmt->execute();
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
-- ============================================================
|
||||
-- DATABASE: webgis_spbu
|
||||
-- Pertemuan 3 – Layer Groups and Layers Control
|
||||
-- Data SPBU dengan 2 Layer Group: Buka 24 Jam & Tidak 24 Jam
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_spbu
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_spbu;
|
||||
|
||||
-- buka_24_jam: 'Ya' → Layer Group "SPBU Buka 24 Jam"
|
||||
-- 'Tidak' → Layer Group "SPBU Tidak Buka 24 Jam"
|
||||
CREATE TABLE IF NOT EXISTS data_spbu (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_spbu VARCHAR(200) NOT NULL,
|
||||
alamat VARCHAR(300) NOT NULL,
|
||||
no_wa VARCHAR(20) NOT NULL,
|
||||
buka_24_jam ENUM('Ya','Tidak') NOT NULL DEFAULT 'Tidak',
|
||||
latitude DOUBLE NOT NULL,
|
||||
longitude DOUBLE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migrasi: tambah kolom alamat jika tabel lama dari PovertyMap belum punya
|
||||
-- ALTER TABLE data_spbu ADD COLUMN alamat VARCHAR(300) NOT NULL DEFAULT '' AFTER nama_spbu;
|
||||
-- UPDATE data_spbu SET alamat = CONCAT(nama_spbu, ', Pontianak') WHERE alamat = '';
|
||||
|
||||
INSERT IGNORE INTO data_spbu (nama_spbu, alamat, no_wa, buka_24_jam, latitude, longitude) VALUES
|
||||
('SPBU Jl. Ahmad Yani', 'Jl. Ahmad Yani No. 12, Pontianak', '0812-1111-0001', 'Ya', -0.0400, 109.3200),
|
||||
('SPBU Jl. Gajah Mada', 'Jl. Gajah Mada No. 45, Pontianak', '0812-1111-0002', 'Ya', -0.0520, 109.3450),
|
||||
('SPBU Jl. Diponegoro', 'Jl. Diponegoro No. 88, Pontianak', '0812-1111-0003', 'Tidak', -0.0610, 109.3600),
|
||||
('SPBU Jl. Sutan Syahrir', 'Jl. Sutan Syahrir No. 5, Pontianak', '0812-1111-0004', 'Tidak', -0.0480, 109.3700),
|
||||
('SPBU Jl. Kom. Yos Sudarso', 'Jl. Kom. Yos Sudarso No. 22, Pontianak', '0812-1111-0005', 'Ya', -0.0350, 109.3550),
|
||||
('SPBU Jl. Tanjung Raya', 'Jl. Tanjung Raya No. 101, Pontianak', '0812-1111-0006', 'Tidak', -0.0580, 109.3300),
|
||||
('SPBU Jl. Veteran', 'Jl. Veteran No. 33, Pontianak', '0812-1111-0007', 'Ya', -0.0450, 109.3380);
|
||||
@@ -0,0 +1,562 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="WebGIS SPBU – Layer Groups & Layers Control">
|
||||
<title>WebGIS SPBU – Layer Groups</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header id="header-bar">
|
||||
<div>
|
||||
<h1>⛽ WebGIS SPBU Pontianak</h1>
|
||||
<p>Layer Groups & Layers Control – Pertemuan 2</p>
|
||||
</div>
|
||||
<div id="stats-bar">
|
||||
<span class="stat-badge green" id="count-24jam">🟢 24 Jam: 0</span>
|
||||
<span class="stat-badge orange" id="count-tidak24jam">🔴 Tidak 24 Jam: 0</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<div class="fab-container">
|
||||
<button class="fab-btn" id="fab-tambah" title="Tambah SPBU" onclick="aktifkanModeTambah()">➕</button>
|
||||
</div>
|
||||
|
||||
<div id="toast"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script>
|
||||
// Gunakan path relatif sederhana — lebih stabil dari URL constructor
|
||||
const API_SPBU = 'api_spbu.php';
|
||||
|
||||
// ============================================================
|
||||
// INISIALISASI PETA (mengikuti pola Leaflet Layers Control)
|
||||
// https://leafletjs.com/examples/layers-control/
|
||||
// ============================================================
|
||||
const map = L.map('map').setView([-0.0553, 109.3495], 13);
|
||||
|
||||
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 19
|
||||
});
|
||||
|
||||
const satelliteLayer = L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
{ attribution: 'Tiles © Esri', maxZoom: 19 }
|
||||
);
|
||||
|
||||
osmLayer.addTo(map);
|
||||
|
||||
// ============================================================
|
||||
// LAYER GROUPS – 2 grup SPBU
|
||||
// ============================================================
|
||||
const layer24Jam = L.layerGroup();
|
||||
const layerTidak24Jam = L.layerGroup();
|
||||
|
||||
// Kedua layer tampil secara default saat app dimuat
|
||||
layer24Jam.addTo(map);
|
||||
layerTidak24Jam.addTo(map);
|
||||
|
||||
// ============================================================
|
||||
// BASE MAPS & OVERLAY MAPS
|
||||
// ============================================================
|
||||
const baseMaps = {
|
||||
'🗺️ OpenStreetMap': osmLayer,
|
||||
'🛰️ Citra Satelit': satelliteLayer
|
||||
};
|
||||
|
||||
const overlayMaps = {
|
||||
'🟢 SPBU Buka 24 Jam': layer24Jam,
|
||||
'🔴 SPBU Tidak 24 Jam': layerTidak24Jam
|
||||
};
|
||||
|
||||
L.control.layers(baseMaps, overlayMaps, {
|
||||
collapsed: false,
|
||||
position: 'topright'
|
||||
}).addTo(map);
|
||||
|
||||
// ============================================================
|
||||
// IKON MARKER – warna berbeda per grup
|
||||
// ============================================================
|
||||
function ikonSpbu(buka24Jam) {
|
||||
const is24Jam = buka24Jam === 'Ya';
|
||||
const color = is24Jam ? '#10b981' : '#f97316';
|
||||
const border = is24Jam ? '#065f46' : '#9a3412';
|
||||
const emoji = '⛽';
|
||||
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="width:32px;height:32px;border-radius:50% 50% 50% 0;background:${color};transform:rotate(-45deg);border:3px solid ${border};box-shadow:0 3px 10px rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;">
|
||||
<span style="transform:rotate(45deg);font-size:14px;line-height:1">${emoji}</span>
|
||||
</div>`,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [0, -34]
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// POPUP INFO
|
||||
// ============================================================
|
||||
function statusLabel(buka24Jam) {
|
||||
return buka24Jam === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam';
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
return String(str ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function popupInfoSpbu(d) {
|
||||
const is24Jam = d.buka_24_jam === 'Ya';
|
||||
const statusClass = is24Jam ? 'status-24jam' : 'status-tidak24jam';
|
||||
const statusText = statusLabel(d.buka_24_jam);
|
||||
const titleColor = is24Jam ? '#10b981' : '#f97316';
|
||||
|
||||
return `<div class="gis-popup">
|
||||
<div class="popup-title" style="border-bottom-color:${titleColor}">⛽ ${escHtml(d.nama_spbu)}</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Alamat</span>
|
||||
<span class="info-value" style="font-size:11px">${escHtml(d.alamat)}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Status</span>
|
||||
<span class="info-value"><span class="status-badge ${statusClass}">${statusText}</span></span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">WhatsApp</span>
|
||||
<span class="info-value">${escHtml(d.no_wa)}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Koordinat</span>
|
||||
<span class="info-value" style="font-size:10px">${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</span>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button type="button" class="popup-btn popup-btn-edit btn-edit-spbu" data-id="${d.id}" onclick="event.stopPropagation(); bukaFormEditSpbu(${d.id}); return false;">✏️ Edit</button>
|
||||
<button type="button" class="popup-btn popup-btn-delete btn-delete-spbu" data-id="${d.id}" onclick="event.stopPropagation(); konfirmasiHapusSpbu(${d.id}); return false;">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function formSpbuHtml({ title, prefix, lat, lng, d = {} }) {
|
||||
const buka24 = d.buka_24_jam === 'Ya' ? 'Ya' : 'Tidak';
|
||||
return `<div class="gis-popup">
|
||||
<div class="popup-title">${title}</div>
|
||||
<div class="popup-form">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="${prefix}-nama" placeholder="Contoh: SPBU Jl. Ahmad Yani" value="${escHtml(d.nama_spbu || '')}">
|
||||
<label>Alamat</label>
|
||||
<input type="text" id="${prefix}-alamat" placeholder="Alamat lengkap" value="${escHtml(d.alamat || '')}">
|
||||
<label>WhatsApp</label>
|
||||
<input type="text" id="${prefix}-wa" placeholder="08xx-xxxx-xxxx" value="${escHtml(d.no_wa || '')}">
|
||||
<label>Status Operasional</label>
|
||||
<select id="${prefix}-24jam">
|
||||
<option value="Ya"${buka24 === 'Ya' ? ' selected' : ''}>🟢 Buka 24 Jam</option>
|
||||
<option value="Tidak"${buka24 === 'Tidak' ? ' selected' : ''}>🔴 Tidak Buka 24 Jam</option>
|
||||
</select>
|
||||
<label>Koordinat</label>
|
||||
<input type="text" id="${prefix}-koord" readonly value="${lat.toFixed(6)}, ${lng.toFixed(6)}" style="font-size:10px">
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button class="popup-btn popup-btn-save" onclick="${prefix === 'add' ? `simpanSpbu(${lat},${lng})` : `updateSpbu(${d.id})`}">💾 Simpan</button>
|
||||
<button class="popup-btn popup-btn-cancel" onclick="map.closePopup()">✖ Batal</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RENDER MARKER KE LAYER GROUP YANG SESUAI
|
||||
// ============================================================
|
||||
const spbuMarkers = {};
|
||||
const spbuDataCache = {};
|
||||
let modeTambah = false;
|
||||
let dbReady = false;
|
||||
|
||||
function getAllSpbuData() {
|
||||
return Object.values(spbuDataCache);
|
||||
}
|
||||
|
||||
function normalizeId(id) {
|
||||
return Number(id);
|
||||
}
|
||||
|
||||
function parseJsonResponse(text) {
|
||||
const clean = (text || '').replace(/^\uFEFF/, '').trim();
|
||||
if (!clean) return null;
|
||||
try {
|
||||
return JSON.parse(clean);
|
||||
} catch (e) {
|
||||
throw new Error('Respons server bukan JSON valid');
|
||||
}
|
||||
}
|
||||
|
||||
async function apiSend(method, id, body) {
|
||||
const hasId = id !== null && id !== undefined && id !== '';
|
||||
const baseUrl = hasId ? `${API_SPBU}?id=${normalizeId(id)}` : API_SPBU;
|
||||
const fetchOpts = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
};
|
||||
if (body !== undefined) {
|
||||
fetchOpts.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
let res = await fetch(baseUrl, fetchOpts);
|
||||
|
||||
if ((method === 'DELETE' || method === 'PUT') && (res.status === 405 || res.status === 501)) {
|
||||
res = await fetch(`${baseUrl}&_method=${method}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...(body || {}), _method: method, id: normalizeId(id) })
|
||||
});
|
||||
}
|
||||
|
||||
const data = parseJsonResponse(await res.text());
|
||||
if (!res.ok) {
|
||||
throw new Error((data && data.error) ? data.error : `Server error (${res.status})`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function apiGetAll() {
|
||||
const res = await fetch(API_SPBU);
|
||||
const data = parseJsonResponse(await res.text());
|
||||
if (!res.ok) {
|
||||
throw new Error((data && data.error) ? data.error : `Server error (${res.status})`);
|
||||
}
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Data SPBU harus berupa array JSON');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function renderSpbu(d) {
|
||||
const id = normalizeId(d.id);
|
||||
d.id = id;
|
||||
spbuDataCache[id] = d;
|
||||
const targetLayer = d.buka_24_jam === 'Ya' ? layer24Jam : layerTidak24Jam;
|
||||
|
||||
if (spbuMarkers[id]) {
|
||||
const oldMarker = spbuMarkers[id];
|
||||
layer24Jam.removeLayer(oldMarker);
|
||||
layerTidak24Jam.removeLayer(oldMarker);
|
||||
}
|
||||
|
||||
const marker = L.marker([d.latitude, d.longitude], {
|
||||
icon: ikonSpbu(d.buka_24_jam),
|
||||
draggable: true
|
||||
});
|
||||
|
||||
marker._data = d;
|
||||
marker._targetLayer = targetLayer;
|
||||
marker._spbuId = id;
|
||||
|
||||
marker.bindPopup(() => popupInfoSpbu(spbuDataCache[id] || d));
|
||||
marker.on('click', () => marker.openPopup());
|
||||
marker.on('dragend', function () {
|
||||
const pos = marker.getLatLng();
|
||||
const data = spbuDataCache[id];
|
||||
if (!data) return;
|
||||
|
||||
data.latitude = pos.lat;
|
||||
data.longitude = pos.lng;
|
||||
spbuDataCache[id] = data;
|
||||
|
||||
if (!dbReady) {
|
||||
showToast('📍 Posisi diperbarui (mode offline)', 'info');
|
||||
marker.getPopup().setContent(popupInfoSpbu(data));
|
||||
return;
|
||||
}
|
||||
|
||||
apiSend('PUT', id, { latitude: pos.lat, longitude: pos.lng })
|
||||
.then(() => {
|
||||
showToast('📍 Posisi SPBU diperbarui', 'success');
|
||||
marker.getPopup().setContent(popupInfoSpbu(data));
|
||||
})
|
||||
.catch(err => showToast('❌ ' + err.message, 'error'));
|
||||
});
|
||||
|
||||
targetLayer.addLayer(marker);
|
||||
spbuMarkers[id] = marker;
|
||||
updateCounts(getAllSpbuData());
|
||||
}
|
||||
|
||||
function removeSpbuFromMap(id) {
|
||||
const numId = normalizeId(id);
|
||||
const marker = spbuMarkers[numId];
|
||||
if (marker) {
|
||||
layer24Jam.removeLayer(marker);
|
||||
layerTidak24Jam.removeLayer(marker);
|
||||
marker.remove();
|
||||
delete spbuMarkers[numId];
|
||||
}
|
||||
delete spbuDataCache[numId];
|
||||
updateCounts(getAllSpbuData());
|
||||
}
|
||||
|
||||
function updateCounts(data) {
|
||||
const count24 = data.filter(d => d.buka_24_jam === 'Ya').length;
|
||||
const countTidak = data.filter(d => d.buka_24_jam === 'Tidak').length;
|
||||
document.getElementById('count-24jam').textContent = `🟢 24 Jam: ${count24}`;
|
||||
document.getElementById('count-tidak24jam').textContent = `🔴 Tidak 24 Jam: ${countTidak}`;
|
||||
}
|
||||
|
||||
function showToast(msg, type = 'info') {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.className = 'show' + (type ? ' ' + type : '');
|
||||
setTimeout(() => { el.className = ''; }, 3000);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MODE TAMBAH SPBU (FAB + klik peta)
|
||||
// ============================================================
|
||||
function aktifkanModeTambah() {
|
||||
modeTambah = !modeTambah;
|
||||
const fab = document.getElementById('fab-tambah');
|
||||
if (modeTambah) {
|
||||
fab.classList.add('active');
|
||||
fab.textContent = '✖';
|
||||
showToast('📍 Klik lokasi di peta untuk menambah SPBU baru', 'info');
|
||||
} else {
|
||||
fab.classList.remove('active');
|
||||
fab.textContent = '➕';
|
||||
}
|
||||
}
|
||||
window.aktifkanModeTambah = aktifkanModeTambah;
|
||||
|
||||
map.on('click', function (e) {
|
||||
if (!modeTambah) return;
|
||||
modeTambah = false;
|
||||
document.getElementById('fab-tambah').classList.remove('active');
|
||||
document.getElementById('fab-tambah').textContent = '➕';
|
||||
tampilkanFormTambahSpbu(e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
|
||||
function tampilkanFormTambahSpbu(lat, lng) {
|
||||
L.popup({ closeButton: true, maxWidth: 300 })
|
||||
.setLatLng([lat, lng])
|
||||
.setContent(formSpbuHtml({ title: '➕ Tambah SPBU Baru', prefix: 'add', lat, lng }))
|
||||
.openOn(map);
|
||||
}
|
||||
|
||||
function simpanSpbu(lat, lng) {
|
||||
const nama = document.getElementById('add-nama').value.trim();
|
||||
const alamat = document.getElementById('add-alamat').value.trim();
|
||||
const no_wa = document.getElementById('add-wa').value.trim();
|
||||
const buka_24_jam = document.getElementById('add-24jam').value;
|
||||
|
||||
if (!nama || !alamat || !no_wa) {
|
||||
showToast('⚠️ Nama, alamat, dan WhatsApp wajib diisi!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbReady) {
|
||||
showToast('❌ Database belum tersedia. Import database.sql terlebih dahulu.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
apiSend('POST', null, { nama_spbu: nama, alamat, no_wa, buka_24_jam, latitude: lat, longitude: lng })
|
||||
.then(res => {
|
||||
map.closePopup();
|
||||
renderSpbu({
|
||||
id: normalizeId(res.id),
|
||||
nama_spbu: res.nama_spbu,
|
||||
alamat: res.alamat,
|
||||
no_wa: res.no_wa,
|
||||
buka_24_jam: res.buka_24_jam,
|
||||
latitude: res.latitude,
|
||||
longitude: res.longitude
|
||||
});
|
||||
showToast('✅ SPBU berhasil ditambahkan', 'success');
|
||||
})
|
||||
.catch(err => showToast('❌ ' + err.message, 'error'));
|
||||
}
|
||||
window.simpanSpbu = simpanSpbu;
|
||||
|
||||
function bukaFormEditSpbu(id) {
|
||||
const d = spbuDataCache[normalizeId(id)];
|
||||
if (!d) return;
|
||||
|
||||
map.closePopup();
|
||||
L.popup({ closeButton: true, maxWidth: 300 })
|
||||
.setLatLng([d.latitude, d.longitude])
|
||||
.setContent(formSpbuHtml({
|
||||
title: '✏️ Edit SPBU',
|
||||
prefix: 'edit',
|
||||
lat: d.latitude,
|
||||
lng: d.longitude,
|
||||
d
|
||||
}))
|
||||
.openOn(map);
|
||||
}
|
||||
window.bukaFormEditSpbu = bukaFormEditSpbu;
|
||||
|
||||
function updateSpbu(id) {
|
||||
const numId = normalizeId(id);
|
||||
const nama = document.getElementById('edit-nama').value.trim();
|
||||
const alamat = document.getElementById('edit-alamat').value.trim();
|
||||
const no_wa = document.getElementById('edit-wa').value.trim();
|
||||
const buka_24_jam = document.getElementById('edit-24jam').value;
|
||||
const old = spbuDataCache[numId];
|
||||
if (!old) return;
|
||||
|
||||
if (!nama || !alamat || !no_wa) {
|
||||
showToast('⚠️ Nama, alamat, dan WhatsApp wajib diisi!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
nama_spbu: nama,
|
||||
alamat,
|
||||
no_wa,
|
||||
buka_24_jam,
|
||||
latitude: old.latitude,
|
||||
longitude: old.longitude
|
||||
};
|
||||
|
||||
if (!dbReady) {
|
||||
map.closePopup();
|
||||
renderSpbu({ ...payload, id: numId });
|
||||
showToast('✅ Data diperbarui (mode offline)', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
apiSend('PUT', numId, payload)
|
||||
.then(() => {
|
||||
map.closePopup();
|
||||
renderSpbu({ ...payload, id: numId });
|
||||
showToast('✅ SPBU berhasil diperbarui', 'success');
|
||||
})
|
||||
.catch(err => showToast('❌ ' + err.message, 'error'));
|
||||
}
|
||||
window.updateSpbu = updateSpbu;
|
||||
|
||||
// Tampilkan konfirmasi inline di dalam popup (confirm() diblokir di Leaflet popup)
|
||||
function konfirmasiHapusSpbu(id) {
|
||||
const numId = normalizeId(id);
|
||||
const d = spbuDataCache[numId];
|
||||
if (!d) { showToast('❌ Data SPBU tidak ditemukan', 'error'); return; }
|
||||
|
||||
const nama = d.nama_spbu;
|
||||
const konfirmHtml = `
|
||||
<div class="gis-popup">
|
||||
<div class="popup-title" style="color:#ef4444">🗑️ Hapus SPBU?</div>
|
||||
<p style="margin:8px 0 16px;font-size:12px;color:#94a3b8">"${escHtml(nama)}" akan dihapus permanen.</p>
|
||||
<div class="popup-actions">
|
||||
<button class="popup-btn" onclick="map.closePopup()" style="background:rgba(255,255,255,0.08);color:#94a3b8">Batal</button>
|
||||
<button class="popup-btn popup-btn-delete" onclick="hapusSpbu(${numId})">🗑️ Ya, Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const lyr = spbuMarkers[numId];
|
||||
if (lyr) {
|
||||
lyr.getPopup().setContent(konfirmHtml);
|
||||
} else {
|
||||
// fallback: langsung hapus tanpa konfirmasi popup
|
||||
hapusSpbu(numId);
|
||||
}
|
||||
}
|
||||
window.konfirmasiHapusSpbu = konfirmasiHapusSpbu;
|
||||
|
||||
async function hapusSpbu(id) {
|
||||
const numId = normalizeId(id);
|
||||
const d = spbuDataCache[numId];
|
||||
if (!d) {
|
||||
showToast('❌ Data SPBU tidak ditemukan', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbReady) {
|
||||
map.closePopup();
|
||||
removeSpbuFromMap(numId);
|
||||
showToast('🗑️ Data dihapus (mode offline)', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiSend('DELETE', numId);
|
||||
map.closePopup();
|
||||
removeSpbuFromMap(numId);
|
||||
showToast('🗑️ SPBU berhasil dihapus', 'success');
|
||||
} catch (err) {
|
||||
showToast('❌ ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
window.hapusSpbu = hapusSpbu;
|
||||
|
||||
// Data fallback jika database belum diimport
|
||||
const SAMPLE_DATA = [
|
||||
{ id: 1, nama_spbu: 'SPBU Jl. Ahmad Yani', alamat: 'Jl. Ahmad Yani No. 12, Pontianak', no_wa: '0812-1111-0001', buka_24_jam: 'Ya', latitude: -0.0400, longitude: 109.3200 },
|
||||
{ id: 2, nama_spbu: 'SPBU Jl. Gajah Mada', alamat: 'Jl. Gajah Mada No. 45, Pontianak', no_wa: '0812-1111-0002', buka_24_jam: 'Ya', latitude: -0.0520, longitude: 109.3450 },
|
||||
{ id: 3, nama_spbu: 'SPBU Jl. Diponegoro', alamat: 'Jl. Diponegoro No. 88, Pontianak', no_wa: '0812-1111-0003', buka_24_jam: 'Tidak', latitude: -0.0610, longitude: 109.3600 },
|
||||
{ id: 4, nama_spbu: 'SPBU Jl. Sutan Syahrir', alamat: 'Jl. Sutan Syahrir No. 5, Pontianak', no_wa: '0812-1111-0004', buka_24_jam: 'Tidak', latitude: -0.0480, longitude: 109.3700 },
|
||||
{ id: 5, nama_spbu: 'SPBU Jl. Kom. Yos Sudarso', alamat: 'Jl. Kom. Yos Sudarso No. 22, Pontianak', no_wa: '0812-1111-0005', buka_24_jam: 'Ya', latitude: -0.0350, longitude: 109.3550 }
|
||||
];
|
||||
|
||||
function loadSpbuData(data, fitBounds = true) {
|
||||
layer24Jam.clearLayers();
|
||||
layerTidak24Jam.clearLayers();
|
||||
Object.keys(spbuMarkers).forEach(k => delete spbuMarkers[k]);
|
||||
Object.keys(spbuDataCache).forEach(k => delete spbuDataCache[k]);
|
||||
|
||||
data.forEach(d => renderSpbu(d));
|
||||
|
||||
if (fitBounds && data.length > 0) {
|
||||
const bounds = L.latLngBounds(data.map(d => [d.latitude, d.longitude]));
|
||||
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 14 });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MUAT DATA DARI API (dengan retry jika gagal)
|
||||
// ============================================================
|
||||
async function muatDataSpbu() {
|
||||
const MAX_RETRY = 3;
|
||||
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
||||
try {
|
||||
const data = await apiGetAll();
|
||||
dbReady = true;
|
||||
loadSpbuData(data);
|
||||
showToast(data.length > 0
|
||||
? `✅ ${data.length} SPBU dimuat dari database`
|
||||
: '📭 Database kosong — klik ➕ untuk tambah SPBU', 'success');
|
||||
return; // sukses, keluar
|
||||
} catch (err) {
|
||||
console.warn(`API SPBU gagal (percobaan ${attempt}/${MAX_RETRY}):`, err);
|
||||
if (attempt < MAX_RETRY) {
|
||||
await new Promise(r => setTimeout(r, 800 * attempt)); // tunggu sebentar
|
||||
} else {
|
||||
// Semua retry gagal — mode offline
|
||||
dbReady = false;
|
||||
loadSpbuData(SAMPLE_DATA);
|
||||
const pesan = err.message || 'Tidak bisa terhubung ke server';
|
||||
showToast(`⚠️ Mode offline: ${pesan}`, 'error');
|
||||
console.error('API SPBU gagal setelah semua retry:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tunggu page load penuh sebelum fetch ke API
|
||||
if (document.readyState === 'complete') {
|
||||
muatDataSpbu();
|
||||
} else {
|
||||
window.addEventListener('load', muatDataSpbu);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,293 @@
|
||||
:root {
|
||||
--bg-dark: #0f1117;
|
||||
--border: #2a3248;
|
||||
--accent-green: #10b981;
|
||||
--accent-orange: #f97316;
|
||||
--accent-blue: #3b82f6;
|
||||
--accent-yellow: #f59e0b;
|
||||
--accent-red: #ef4444;
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--radius-sm: 6px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#header-bar {
|
||||
background: linear-gradient(135deg, #161b27 0%, #0f1117 100%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#header-bar h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#header-bar p {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
#stats-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.stat-badge.green {
|
||||
color: var(--accent-green);
|
||||
border-color: rgba(16, 185, 129, 0.35);
|
||||
}
|
||||
|
||||
.stat-badge.orange {
|
||||
color: var(--accent-orange);
|
||||
border-color: rgba(249, 115, 22, 0.35);
|
||||
}
|
||||
|
||||
#map {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.leaflet-control-layers {
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35) !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 10px 12px !important;
|
||||
}
|
||||
|
||||
.gis-popup {
|
||||
min-width: 220px;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-24jam {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-tidak24jam {
|
||||
background: rgba(249, 115, 22, 0.15);
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
#toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(80px);
|
||||
background: #1e2538;
|
||||
color: var(--text-primary);
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
#toast.error {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
#toast.success {
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
#toast.info {
|
||||
border-color: rgba(59, 130, 246, 0.5);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.fab-btn {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--accent-green), #059669);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fab-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.fab-btn.active {
|
||||
background: linear-gradient(135deg, var(--accent-orange), #ea580c);
|
||||
box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
|
||||
.popup-form label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.popup-form label:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.popup-form input,
|
||||
.popup-form select {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.popup-form input:focus,
|
||||
.popup-form select:focus {
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.popup-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.popup-btn-save {
|
||||
background: var(--accent-green);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.popup-btn-edit {
|
||||
background: var(--accent-yellow);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.popup-btn-delete {
|
||||
background: var(--accent-red);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.popup-btn-cancel {
|
||||
background: #334155;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 12px 14px;
|
||||
}
|
||||
Reference in New Issue
Block a user