UAS SIG Proyek WebGis
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// api/jalan.php - CRUD API Data Jalan
|
||||
// ============================================
|
||||
|
||||
require_once '../config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ── READ: Ambil semua / satu jalan ──────────────────────────────
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$id = intval($_GET['id']);
|
||||
$stmt = $db->prepare("SELECT * FROM jalan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$row = $result->fetch_assoc();
|
||||
if ($row) {
|
||||
$row['geometry'] = json_decode($row['geometry']);
|
||||
echo json_encode(['success' => true, 'data' => $row]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
$result = $db->query("SELECT * FROM jalan ORDER BY created_at DESC");
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['geometry'] = json_decode($row['geometry']);
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['success' => true, 'data' => $data]);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── CREATE: Tambah jalan baru ───────────────────────────────────
|
||||
case 'POST':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (
|
||||
empty($input['nama_jalan']) ||
|
||||
empty($input['status_jalan']) ||
|
||||
empty($input['geometry']) ||
|
||||
!isset($input['panjang'])
|
||||
) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
|
||||
break;
|
||||
}
|
||||
|
||||
$nama_jalan = trim($input['nama_jalan']);
|
||||
$status_jalan = $input['status_jalan'];
|
||||
$panjang = floatval($input['panjang']);
|
||||
$geometry = json_encode($input['geometry']);
|
||||
|
||||
$valid_status = ['Nasional', 'Provinsi', 'Kabupaten'];
|
||||
if (!in_array($status_jalan, $valid_status)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Status jalan tidak valid']);
|
||||
break;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO jalan (nama_jalan, status_jalan, panjang, geometry) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('ssds', $nama_jalan, $status_jalan, $panjang, $geometry);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Data jalan berhasil disimpan',
|
||||
'id' => $db->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data']);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── UPDATE: Edit jalan ──────────────────────────────────────────
|
||||
case 'PUT':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['id'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'ID diperlukan']);
|
||||
break;
|
||||
}
|
||||
|
||||
$id = intval($input['id']);
|
||||
$nama_jalan = trim($input['nama_jalan']);
|
||||
$status_jalan = $input['status_jalan'];
|
||||
$panjang = floatval($input['panjang']);
|
||||
$geometry = json_encode($input['geometry']);
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE jalan SET nama_jalan=?, status_jalan=?, panjang=?, geometry=? WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('ssdsi', $nama_jalan, $status_jalan, $panjang, $geometry, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['success' => true, 'message' => 'Data jalan berhasil diupdate']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Gagal mengupdate data']);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── DELETE: Hapus jalan ─────────────────────────────────────────
|
||||
case 'DELETE':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = intval($input['id'] ?? ($_GET['id'] ?? 0));
|
||||
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'ID diperlukan']);
|
||||
break;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM jalan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute() && $stmt->affected_rows > 0) {
|
||||
echo json_encode(['success' => true, 'message' => 'Data jalan berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']);
|
||||
}
|
||||
|
||||
$db->close();
|
||||
?>
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// api/kerusakan.php - CRUD API Kerusakan Jalan
|
||||
// ============================================
|
||||
|
||||
require_once '../config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
// Handle sub-resource: ?action=histori&id=X
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
if ($action === 'histori') {
|
||||
// GET histori untuk kerusakan tertentu
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
if (!$id) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID diperlukan']); exit; }
|
||||
$stmt = $db->prepare("SELECT * FROM histori_kerusakan WHERE kerusakan_id=? ORDER BY tgl_perubahan DESC");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) $data[] = $row;
|
||||
echo json_encode(['success'=>true, 'data'=>$data]);
|
||||
$db->close(); exit;
|
||||
}
|
||||
|
||||
if ($action === 'update_status') {
|
||||
// POST update status + catat histori
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = intval($input['id'] ?? 0);
|
||||
$status_baru = $input['status_baru'] ?? '';
|
||||
$catatan = $input['catatan'] ?? '';
|
||||
if (!$id || !$status_baru) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID dan status_baru diperlukan']); exit; }
|
||||
|
||||
// Ambil status lama
|
||||
$stmt = $db->prepare("SELECT status_laporan FROM kerusakan_jalan WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
if (!$row) { http_response_code(404); echo json_encode(['success'=>false,'message'=>'Data tidak ditemukan']); exit; }
|
||||
|
||||
$status_lama = $row['status_laporan'];
|
||||
|
||||
// Update status
|
||||
$stmt2 = $db->prepare("UPDATE kerusakan_jalan SET status_laporan=? WHERE id=?");
|
||||
$stmt2->bind_param('si', $status_baru, $id);
|
||||
$stmt2->execute();
|
||||
|
||||
// Catat histori
|
||||
$stmt3 = $db->prepare("INSERT INTO histori_kerusakan (kerusakan_id, status_lama, status_baru, catatan) VALUES (?,?,?,?)");
|
||||
$stmt3->bind_param('isss', $id, $status_lama, $status_baru, $catatan);
|
||||
$stmt3->execute();
|
||||
|
||||
echo json_encode(['success'=>true, 'message'=>'Status kerusakan diupdate']);
|
||||
$db->close(); exit;
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ── READ ────────────────────────────────────────────────────────
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$id = intval($_GET['id']);
|
||||
$stmt = $db->prepare("SELECT * FROM kerusakan_jalan WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
if ($row) {
|
||||
echo json_encode(['success'=>true, 'data'=>$row]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success'=>false, 'message'=>'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
// Filter opsional: ?jalan_id=X atau ?status=X
|
||||
$where = [];
|
||||
$params = '';
|
||||
$vals = [];
|
||||
if (!empty($_GET['jalan_id'])) {
|
||||
$where[] = 'jalan_id=?'; $params .= 'i'; $vals[] = intval($_GET['jalan_id']);
|
||||
}
|
||||
if (!empty($_GET['status'])) {
|
||||
$where[] = 'status_laporan=?'; $params .= 's'; $vals[] = $_GET['status'];
|
||||
}
|
||||
$sql = "SELECT k.*, j.nama_jalan as jalan_nama FROM kerusakan_jalan k LEFT JOIN jalan j ON k.jalan_id=j.id";
|
||||
if ($where) $sql .= ' WHERE '.implode(' AND ', $where);
|
||||
$sql .= ' ORDER BY k.tgl_laporan DESC';
|
||||
$stmt = $db->prepare($sql);
|
||||
if ($params) $stmt->bind_param($params, ...$vals);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) $data[] = $row;
|
||||
|
||||
// Hitung kluster per lokasi (radius ~50m)
|
||||
foreach ($data as &$item) {
|
||||
$cnt = 0;
|
||||
foreach ($data as $other) {
|
||||
$dlat = ($item['lat'] - $other['lat']) * 111000;
|
||||
$dlng = ($item['lng'] - $other['lng']) * 111000 * cos(deg2rad($item['lat']));
|
||||
if (sqrt($dlat*$dlat + $dlng*$dlng) < 50) $cnt++;
|
||||
}
|
||||
$item['laporan_terdekat'] = $cnt;
|
||||
}
|
||||
echo json_encode(['success'=>true, 'data'=>$data]);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── CREATE ──────────────────────────────────────────────────────
|
||||
case 'POST':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$required = ['deskripsi','tingkat_urgensi','lat','lng'];
|
||||
foreach ($required as $f) {
|
||||
if (empty($input[$f]) && $input[$f] !== 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success'=>false,'message'=>"Field $f diperlukan"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$jalan_id = !empty($input['jalan_id']) ? intval($input['jalan_id']) : null;
|
||||
$nama_jalan_ref = trim($input['nama_jalan_ref'] ?? '');
|
||||
$deskripsi = trim($input['deskripsi']);
|
||||
$tingkat_urgensi = $input['tingkat_urgensi'];
|
||||
$lat = floatval($input['lat']);
|
||||
$lng = floatval($input['lng']);
|
||||
$foto_base64 = $input['foto_base64'] ?? null;
|
||||
|
||||
$valid_urgensi = ['Rendah','Sedang','Tinggi','Kritis'];
|
||||
if (!in_array($tingkat_urgensi, $valid_urgensi)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success'=>false,'message'=>'Tingkat urgensi tidak valid']);
|
||||
break;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO kerusakan_jalan (jalan_id, nama_jalan_ref, deskripsi, tingkat_urgensi, lat, lng, foto_base64) VALUES (?,?,?,?,?,?,?)"
|
||||
);
|
||||
$stmt->bind_param('isssdds', $jalan_id, $nama_jalan_ref, $deskripsi, $tingkat_urgensi, $lat, $lng, $foto_base64);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['success'=>true,'message'=>'Laporan kerusakan berhasil disimpan','id'=>$db->insert_id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success'=>false,'message'=>'Gagal menyimpan data']);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── UPDATE ──────────────────────────────────────────────────────
|
||||
case 'PUT':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = intval($input['id'] ?? 0);
|
||||
if (!$id) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID diperlukan']); break; }
|
||||
|
||||
$deskripsi = trim($input['deskripsi'] ?? '');
|
||||
$tingkat_urgensi = $input['tingkat_urgensi'] ?? '';
|
||||
$nama_jalan_ref = trim($input['nama_jalan_ref'] ?? '');
|
||||
|
||||
$stmt = $db->prepare("UPDATE kerusakan_jalan SET deskripsi=?, tingkat_urgensi=?, nama_jalan_ref=? WHERE id=?");
|
||||
$stmt->bind_param('sssi', $deskripsi, $tingkat_urgensi, $nama_jalan_ref, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['success'=>true,'message'=>'Data kerusakan diupdate']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success'=>false,'message'=>'Gagal mengupdate data']);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── DELETE ──────────────────────────────────────────────────────
|
||||
case 'DELETE':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = intval($input['id'] ?? ($_GET['id'] ?? 0));
|
||||
if (!$id) { http_response_code(400); echo json_encode(['success'=>false,'message'=>'ID diperlukan']); break; }
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM kerusakan_jalan WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
if ($stmt->execute() && $stmt->affected_rows > 0) {
|
||||
echo json_encode(['success'=>true,'message'=>'Laporan kerusakan dihapus']);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success'=>false,'message'=>'Data tidak ditemukan']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
echo json_encode(['success'=>false,'message'=>'Method tidak diizinkan']);
|
||||
}
|
||||
|
||||
$db->close();
|
||||
?>
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// api/tanah.php - CRUD API Data Parsil Tanah
|
||||
// ============================================
|
||||
|
||||
require_once '../config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ── READ ────────────────────────────────────────────────────────
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$id = intval($_GET['id']);
|
||||
$stmt = $db->prepare("SELECT * FROM tanah WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$row = $result->fetch_assoc();
|
||||
if ($row) {
|
||||
$row['geometry'] = json_decode($row['geometry']);
|
||||
echo json_encode(['success' => true, 'data' => $row]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} else {
|
||||
$result = $db->query("SELECT * FROM tanah ORDER BY created_at DESC");
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['geometry'] = json_decode($row['geometry']);
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['success' => true, 'data' => $data]);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── CREATE ──────────────────────────────────────────────────────
|
||||
case 'POST':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (
|
||||
empty($input['kode_kavling']) ||
|
||||
empty($input['nama_pemilik']) ||
|
||||
empty($input['status_tanah']) ||
|
||||
empty($input['geometry']) ||
|
||||
!isset($input['luas'])
|
||||
) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
|
||||
break;
|
||||
}
|
||||
|
||||
$kode_kavling = trim($input['kode_kavling']);
|
||||
$nama_pemilik = trim($input['nama_pemilik']);
|
||||
$status_tanah = $input['status_tanah'];
|
||||
$luas = floatval($input['luas']);
|
||||
$geometry = json_encode($input['geometry']);
|
||||
|
||||
$valid_status = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
if (!in_array($status_tanah, $valid_status)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Status tanah tidak valid']);
|
||||
break;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO tanah (kode_kavling, nama_pemilik, status_tanah, luas, geometry) VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('sssds', $kode_kavling, $nama_pemilik, $status_tanah, $luas, $geometry);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Data tanah berhasil disimpan',
|
||||
'id' => $db->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data']);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── UPDATE ──────────────────────────────────────────────────────
|
||||
case 'PUT':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (empty($input['id'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'ID diperlukan']);
|
||||
break;
|
||||
}
|
||||
|
||||
$id = intval($input['id']);
|
||||
$kode_kavling = trim($input['kode_kavling']);
|
||||
$nama_pemilik = trim($input['nama_pemilik']);
|
||||
$status_tanah = $input['status_tanah'];
|
||||
$luas = floatval($input['luas']);
|
||||
$geometry = json_encode($input['geometry']);
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE tanah SET kode_kavling=?, nama_pemilik=?, status_tanah=?, luas=?, geometry=? WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('sssdsi', $kode_kavling, $nama_pemilik, $status_tanah, $luas, $geometry, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['success' => true, 'message' => 'Data tanah berhasil diupdate']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Gagal mengupdate data']);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── DELETE ──────────────────────────────────────────────────────
|
||||
case 'DELETE':
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = intval($input['id'] ?? ($_GET['id'] ?? 0));
|
||||
|
||||
if (!$id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'ID diperlukan']);
|
||||
break;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM tanah WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute() && $stmt->affected_rows > 0) {
|
||||
echo json_encode(['success' => true, 'message' => 'Data tanah berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']);
|
||||
}
|
||||
|
||||
$db->close();
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// config.php - Konfigurasi Database
|
||||
// ============================================
|
||||
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root'); // default Laragon
|
||||
define('DB_PASS', ''); // default Laragon kosong
|
||||
define('DB_NAME', 'sig_tugas');
|
||||
|
||||
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 API
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,80 @@
|
||||
-- ============================================
|
||||
-- DATABASE SETUP - SIG Tugas Kuliah
|
||||
-- Manajemen Jalan & Parsil Tanah - Pontianak
|
||||
-- ============================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS sig_tugas CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE sig_tugas;
|
||||
|
||||
-- Tabel Data Jalan (Polyline)
|
||||
CREATE TABLE IF NOT EXISTS jalan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_jalan VARCHAR(100) NOT NULL,
|
||||
status_jalan ENUM('Nasional','Provinsi','Kabupaten') NOT NULL,
|
||||
panjang DOUBLE NOT NULL DEFAULT 0 COMMENT 'Panjang dalam meter, dihitung otomatis Leaflet',
|
||||
geometry LONGTEXT NOT NULL COMMENT 'GeoJSON LineString',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Tabel Data Parsil Tanah (Polygon)
|
||||
CREATE TABLE IF NOT EXISTS tanah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kode_kavling VARCHAR(50) NOT NULL,
|
||||
nama_pemilik VARCHAR(100) NOT NULL,
|
||||
status_tanah ENUM('SHM','HGB','HGU','HP') NOT NULL,
|
||||
luas DOUBLE NOT NULL DEFAULT 0 COMMENT 'Luas dalam meter persegi, dihitung otomatis Leaflet',
|
||||
geometry LONGTEXT NOT NULL COMMENT 'GeoJSON Polygon',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- Sample Data - Jalan di Pontianak
|
||||
-- ============================================
|
||||
INSERT INTO jalan (nama_jalan, status_jalan, panjang, geometry) VALUES
|
||||
(
|
||||
'Jl. Ahmad Yani',
|
||||
'Nasional',
|
||||
3250.5,
|
||||
'{"type":"LineString","coordinates":[[109.3425,-0.0222],[109.3480,-0.0198],[109.3540,-0.0175],[109.3600,-0.0155]]}'
|
||||
),
|
||||
(
|
||||
'Jl. Gajah Mada',
|
||||
'Provinsi',
|
||||
1820.3,
|
||||
'{"type":"LineString","coordinates":[[109.3310,-0.0290],[109.3355,-0.0265],[109.3400,-0.0242]]}'
|
||||
),
|
||||
(
|
||||
'Jl. Tanjungpura',
|
||||
'Kabupaten',
|
||||
1100.7,
|
||||
'{"type":"LineString","coordinates":[[109.3250,-0.0350],[109.3285,-0.0330],[109.3320,-0.0310]]}'
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- Sample Data - Parsil Tanah di Pontianak
|
||||
-- ============================================
|
||||
INSERT INTO tanah (kode_kavling, nama_pemilik, status_tanah, luas, geometry) VALUES
|
||||
(
|
||||
'KAV-001',
|
||||
'Budi Santoso',
|
||||
'SHM',
|
||||
1250.75,
|
||||
'{"type":"Polygon","coordinates":[[[109.3350,-0.0400],[109.3370,-0.0400],[109.3370,-0.0420],[109.3350,-0.0420],[109.3350,-0.0400]]]}'
|
||||
),
|
||||
(
|
||||
'KAV-002',
|
||||
'PT. Maju Bersama',
|
||||
'HGB',
|
||||
3400.00,
|
||||
'{"type":"Polygon","coordinates":[[[109.3280,-0.0450],[109.3310,-0.0450],[109.3310,-0.0480],[109.3280,-0.0480],[109.3280,-0.0450]]]}'
|
||||
),
|
||||
(
|
||||
'KAV-003',
|
||||
'CV. Agro Kalimantan',
|
||||
'HGU',
|
||||
5800.50,
|
||||
'{"type":"Polygon","coordinates":[[[109.3150,-0.0500],[109.3200,-0.0500],[109.3200,-0.0540],[109.3150,-0.0540],[109.3150,-0.0500]]]}'
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
-- ============================================
|
||||
-- DATABASE UPDATE - Tambah Tabel Kerusakan Jalan
|
||||
-- Jalankan query ini setelah database.sql
|
||||
-- ============================================
|
||||
|
||||
-- Tabel Laporan Kerusakan Jalan (Point)
|
||||
CREATE TABLE IF NOT EXISTS kerusakan_jalan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
jalan_id INT NULL COMMENT 'Referensi ke tabel jalan (opsional)',
|
||||
nama_jalan_ref VARCHAR(100) NULL COMMENT 'Nama jalan jika tidak ada di db',
|
||||
deskripsi TEXT NOT NULL,
|
||||
tingkat_urgensi ENUM('Rendah','Sedang','Tinggi','Kritis') NOT NULL DEFAULT 'Sedang',
|
||||
status_laporan ENUM('Dilaporkan','Diverifikasi','Dalam Perbaikan','Sudah Diperbaiki') NOT NULL DEFAULT 'Dilaporkan',
|
||||
foto_path VARCHAR(255) NULL COMMENT 'Path relatif foto',
|
||||
foto_base64 LONGTEXT NULL COMMENT 'Base64 foto (opsional, jika tidak pakai file system)',
|
||||
lat DOUBLE NOT NULL,
|
||||
lng DOUBLE NOT NULL,
|
||||
tgl_laporan TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
tgl_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Tabel Histori Kerusakan Jalan
|
||||
-- Dipakai untuk merekam perubahan status kerusakan
|
||||
CREATE TABLE IF NOT EXISTS histori_kerusakan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kerusakan_id INT NOT NULL,
|
||||
status_lama ENUM('Dilaporkan','Diverifikasi','Dalam Perbaikan','Sudah Diperbaiki') NOT NULL,
|
||||
status_baru ENUM('Dilaporkan','Diverifikasi','Dalam Perbaikan','Sudah Diperbaiki') NOT NULL,
|
||||
catatan TEXT NULL,
|
||||
tgl_perubahan TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (kerusakan_id) REFERENCES kerusakan_jalan(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Sample Data Kerusakan
|
||||
INSERT INTO kerusakan_jalan (jalan_id, nama_jalan_ref, deskripsi, tingkat_urgensi, status_laporan, lat, lng) VALUES
|
||||
(1, 'Jl. Ahmad Yani', 'Lubang besar di tengah jalan, diameter sekitar 50cm', 'Tinggi', 'Diverifikasi', -0.0210, 109.3460),
|
||||
(1, 'Jl. Ahmad Yani', 'Aspal terkelupas dan berlubang', 'Sedang', 'Dilaporkan', -0.0215, 109.3465),
|
||||
(1, 'Jl. Ahmad Yani', 'Lubang di pinggir jalan dekat got', 'Sedang', 'Dilaporkan', -0.0212, 109.3462),
|
||||
(2, 'Jl. Gajah Mada', 'Retak memanjang sepanjang 3 meter', 'Rendah', 'Sudah Diperbaiki', -0.0275, 109.3340);
|
||||
|
||||
-- Sample Histori
|
||||
INSERT INTO histori_kerusakan (kerusakan_id, status_lama, status_baru, catatan) VALUES
|
||||
(1, 'Dilaporkan', 'Diverifikasi', 'Tim survei sudah turun ke lapangan, kerusakan terkonfirmasi'),
|
||||
(4, 'Dilaporkan', 'Dalam Perbaikan', 'Jadwal perbaikan: minggu ini'),
|
||||
(4, 'Dalam Perbaikan', 'Sudah Diperbaiki', 'Perbaikan selesai tanggal 10 April 2026');
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
-- =====================================================
|
||||
-- WebGIS SPBU Pontianak — Database Schema
|
||||
-- Jalankan di phpMyAdmin atau MySQL CLI
|
||||
-- =====================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_spbu
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_spbu;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL COMMENT 'Nama SPBU',
|
||||
no VARCHAR(50) NOT NULL COMMENT 'Nomor SPBU (mis: 64.751.01)',
|
||||
status TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=Buka 24 Jam, 0=Terbatas',
|
||||
latitude DECIMAL(10,8) NOT NULL COMMENT 'Koordinat Latitude',
|
||||
longitude DECIMAL(11,8) NOT NULL COMMENT 'Koordinat Longitude',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Contoh Data SPBU Pontianak ──────────────────────
|
||||
INSERT INTO spbu (nama, no, status, latitude, longitude) VALUES
|
||||
('SPBU Jl. Ahmad Yani', '64.751.01', 1, -0.020500, 109.341200),
|
||||
('SPBU Jl. Gajahmada', '64.751.02', 0, -0.031800, 109.325600),
|
||||
('SPBU Jl. Tanjungpura', '64.751.03', 1, -0.025300, 109.332400),
|
||||
('SPBU Jl. Sutan Syahrir', '64.751.04', 0, -0.018200, 109.348700),
|
||||
('SPBU Jl. Adisucipto', '64.751.05', 1, -0.040100, 109.358900);
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// db_connect.php - Koneksi Database
|
||||
$host = "localhost";
|
||||
$dbname = "webgis_spbu";
|
||||
$username = "root";
|
||||
$password = "";
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
die(json_encode(["error" => "Koneksi gagal: " . $e->getMessage()]));
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0; url=index.php">
|
||||
<title>WebGIS SPBU Pontianak</title>
|
||||
<script>
|
||||
window.location.href = "index.php";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Mengalihkan ke halaman utama... Jika tidak otomatis, klik <a href="index.php">di sini</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
require_once 'db_connect.php';
|
||||
|
||||
// AJAX: update koordinat saat marker di-drag
|
||||
if (isset($_GET['update_coords']) && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
header('Content-Type: application/json');
|
||||
$id = intval($_POST['id']);
|
||||
$lat = floatval($_POST['lat']);
|
||||
$lng = floatval($_POST['lng']);
|
||||
if($id > 0){
|
||||
$pdo->prepare("UPDATE spbu SET latitude=:lat,longitude=:lng WHERE id=:id")
|
||||
->execute([':lat'=>$lat,':lng'=>$lng,':id'=>$id]);
|
||||
echo json_encode(['ok'=>true,'lat'=>$lat,'lng'=>$lng]);
|
||||
} else {
|
||||
echo json_encode(['ok'=>false]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// AJAX: hapus dari index.php
|
||||
if (isset($_GET['delete']) && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
header('Content-Type: application/json');
|
||||
$id = intval($_POST['id']);
|
||||
$pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>$id]);
|
||||
echo json_encode(['ok'=>true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$spbu_list = $pdo->query("SELECT * FROM spbu ORDER BY nama")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$total = count($spbu_list);
|
||||
$total_24 = count(array_filter($spbu_list, fn($s) => $s['status']==1));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Peta SPBU — WebGIS Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#f5f4f0;--card:#fff;--border:#e8e5df;--border2:#d4d0c8;
|
||||
--text:#1a1915;--muted:#7c7a72;--hint:#b0ada5;
|
||||
--accent:#e85a1a;--accent-bg:#fdf0eb;--accent-dk:#c44a12;
|
||||
--green:#1a7a45;--green-bg:#ebf7f0;
|
||||
--red:#c0392b;--red-bg:#fdf0ef;
|
||||
--r:10px;--r-sm:7px;
|
||||
}
|
||||
html,body{height:100%;font-family:'Plus Jakarta Sans',sans-serif;color:var(--text);font-size:14px}
|
||||
body{display:flex;flex-direction:column;background:var(--bg)}
|
||||
|
||||
/* NAV */
|
||||
.topnav{background:var(--card);border-bottom:1px solid var(--border);height:54px;padding:0 24px;display:flex;align-items:center;gap:6px;flex-shrink:0;z-index:9999;position:relative}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:700;font-size:14px;color:var(--text);text-decoration:none;margin-right:auto}
|
||||
.brand-mark{width:30px;height:30px;background:var(--accent);border-radius:8px;display:grid;place-items:center;color:#fff;font-size:14px;font-weight:800}
|
||||
.nav-a{padding:5px 13px;border-radius:var(--r-sm);font-size:13px;font-weight:500;color:var(--muted);text-decoration:none;transition:.12s}
|
||||
.nav-a:hover{background:var(--bg);color:var(--text)}
|
||||
.nav-a.on{background:var(--accent-bg);color:var(--accent);font-weight:600}
|
||||
|
||||
/* STATS BAR */
|
||||
.statsbar{background:var(--card);border-bottom:1px solid var(--border);padding:0 24px;height:40px;display:flex;align-items:center;gap:18px;flex-shrink:0}
|
||||
.stat{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
||||
.stat strong{color:var(--text);font-weight:600}
|
||||
.stat-dot{width:8px;height:8px;border-radius:50%}
|
||||
|
||||
/* MAIN */
|
||||
.main{display:flex;flex:1;overflow:hidden}
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar{width:300px;flex-shrink:0;background:var(--card);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
|
||||
.sb-head{padding:14px 16px 10px;border-bottom:1px solid var(--border);flex-shrink:0}
|
||||
.sb-head h2{font-size:13px;font-weight:600;margin-bottom:9px;color:var(--text)}
|
||||
.search{width:100%;padding:7px 11px;border:1px solid var(--border2);border-radius:var(--r-sm);font-family:inherit;font-size:13px;color:var(--text);background:#fff;transition:.12s}
|
||||
.search:focus{outline:none;border-color:var(--accent)}
|
||||
.sb-list{flex:1;overflow-y:auto;padding:8px}
|
||||
.sb-list::-webkit-scrollbar{width:3px}
|
||||
.sb-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||||
.sb-item{border:1px solid var(--border);border-radius:var(--r-sm);padding:10px 12px;margin-bottom:6px;cursor:pointer;transition:.12s}
|
||||
.sb-item:hover{border-color:var(--border2);background:#fafaf8}
|
||||
.sb-item.active{border-color:var(--accent);background:var(--accent-bg)}
|
||||
.sb-name{font-weight:600;font-size:13px;margin-bottom:2px}
|
||||
.sb-no{font-size:12px;color:var(--muted)}
|
||||
.sb-badge{display:inline-flex;align-items:center;gap:3px;margin-top:5px;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600}
|
||||
.sb-open{background:var(--green-bg);color:var(--green)}
|
||||
.sb-closed{background:var(--bg);color:var(--hint);border:1px solid var(--border)}
|
||||
.empty-sb{text-align:center;padding:32px 16px;color:var(--hint);font-size:13px}
|
||||
|
||||
/* MAP */
|
||||
.mapwrap{flex:1;position:relative;overflow:hidden}
|
||||
#map{width:100%;height:100%}
|
||||
|
||||
/* Rapikan tampilan Leaflet Layers Control */
|
||||
.leaflet-control-layers{
|
||||
border:1px solid var(--border) !important;
|
||||
border-radius:var(--r-sm) !important;
|
||||
box-shadow:0 2px 12px rgba(0,0,0,.09) !important;
|
||||
font-family:'Plus Jakarta Sans',sans-serif !important;
|
||||
font-size:13px !important;
|
||||
}
|
||||
.leaflet-control-layers-expanded{padding:10px 14px !important}
|
||||
.leaflet-control-layers-separator{margin:6px 0 !important;border-color:var(--border) !important}
|
||||
|
||||
/* TOAST */
|
||||
.toast{position:absolute;top:12px;left:50%;transform:translateX(-50%) translateY(-60px);z-index:1001;background:#1a1915;color:#fff;font-size:12px;font-weight:500;padding:8px 16px;border-radius:20px;transition:.25s;pointer-events:none;white-space:nowrap}
|
||||
.toast.show{transform:translateX(-50%) translateY(0)}
|
||||
|
||||
/* ATTR PANEL */
|
||||
#attr-panel{
|
||||
position:absolute;bottom:0;left:0;right:0;
|
||||
background:var(--card);border-top:2px solid var(--accent);
|
||||
transform:translateY(100%);transition:transform .28s cubic-bezier(.4,0,.2,1);
|
||||
z-index:1000;
|
||||
}
|
||||
#attr-panel.open{transform:translateY(0)}
|
||||
.ap-head{padding:12px 18px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.ap-title{font-size:13px;font-weight:700;color:var(--accent)}
|
||||
.ap-actions{display:flex;gap:7px;align-items:center}
|
||||
.ap-close{background:none;border:1px solid var(--border2);color:var(--muted);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;transition:.12s}
|
||||
.ap-close:hover{border-color:var(--muted)}
|
||||
.ap-del{background:none;border:1px solid #f0c4bf;color:var(--red);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;transition:.12s}
|
||||
.ap-del:hover{background:var(--red-bg)}
|
||||
.ap-edit{background:none;border:1px solid #f0c5ae;color:var(--accent);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;transition:.12s;text-decoration:none}
|
||||
.ap-edit:hover{background:var(--accent-bg)}
|
||||
.ap-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:0;padding:12px 18px 16px}
|
||||
@media(max-width:600px){.ap-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.ap-field{padding:6px 10px}
|
||||
.ap-label{font-size:10px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}
|
||||
.ap-val{font-size:14px;font-weight:600;color:var(--text)}
|
||||
.ap-val.green{color:var(--green)}
|
||||
.ap-val.red{color:var(--red)}
|
||||
.ap-val.mono{font-size:12px;font-family:'Courier New',monospace;font-weight:500;color:var(--muted)}
|
||||
.drag-hint{font-size:11px;color:var(--hint);padding:0 18px 10px;display:flex;align-items:center;gap:4px}
|
||||
|
||||
@media(max-width:680px){
|
||||
.sidebar{width:100%;max-height:200px;border-right:none;border-bottom:1px solid var(--border)}
|
||||
.main{flex-direction:column}
|
||||
.statsbar .stat:nth-child(n+3){display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="topnav">
|
||||
<a href="index.php" class="brand"><div class="brand-mark">S</div>WebGIS SPBU Pontianak</a>
|
||||
<a href="insert.php" class="nav-a">Input Data</a>
|
||||
<a href="index.php" class="nav-a on">Lihat Peta</a>
|
||||
</nav>
|
||||
|
||||
<div class="statsbar">
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--accent)"></div>Total <strong><?= $total ?></strong> SPBU</div>
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--green)"></div>Buka 24 Jam: <strong><?= $total_24 ?></strong></div>
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--hint)"></div>Terbatas: <strong><?= $total - $total_24 ?></strong></div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<div class="sidebar">
|
||||
<div class="sb-head">
|
||||
<h2>Daftar SPBU</h2>
|
||||
<input class="search" id="search" type="text" placeholder="Cari nama atau nomor...">
|
||||
</div>
|
||||
<div class="sb-list" id="sb-list">
|
||||
<?php if(!$total): ?>
|
||||
<div class="empty-sb">Belum ada data.<br><a href="insert.php" style="color:var(--accent)">Tambahkan →</a></div>
|
||||
<?php else: foreach($spbu_list as $s): ?>
|
||||
<div class="sb-item" data-id="<?= $s['id'] ?>">
|
||||
<div class="sb-name"><?= htmlspecialchars($s['nama']) ?></div>
|
||||
<div class="sb-no">No. <?= htmlspecialchars($s['no']) ?></div>
|
||||
<span class="sb-badge <?= $s['status']?'sb-open':'sb-closed' ?>"><?= $s['status']?'● 24 Jam':'○ Terbatas' ?></span>
|
||||
</div>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAP -->
|
||||
<div class="mapwrap">
|
||||
<div id="map"></div>
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<div id="attr-panel">
|
||||
<div class="ap-head">
|
||||
<span class="ap-title">⛽ Informasi SPBU</span>
|
||||
<div class="ap-actions">
|
||||
<a href="#" class="ap-edit" id="ap-edit-btn">Edit</a>
|
||||
<button class="ap-del" id="ap-del-btn">Hapus</button>
|
||||
<button class="ap-close" onclick="closePanel()">✕ Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ap-grid">
|
||||
<div class="ap-field"><div class="ap-label">Nama</div><div class="ap-val" id="ap-nama">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Nomor SPBU</div><div class="ap-val" id="ap-no">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Status</div><div class="ap-val" id="ap-status">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Koordinat</div><div class="ap-val mono" id="ap-coords">—</div></div>
|
||||
</div>
|
||||
<div class="drag-hint">⟳ Drag marker di peta untuk memperbarui lokasi secara langsung</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const DATA = <?= json_encode($spbu_list, JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
// ── Base Layers ──────────────────────────────────────────────────────────────
|
||||
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap', maxZoom: 19
|
||||
});
|
||||
const sat = L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
{ attribution: 'Esri', maxZoom: 19 }
|
||||
);
|
||||
|
||||
// ── Layer Groups: pisah SPBU berdasarkan status ──────────────────────────────
|
||||
const layer24Jam = L.layerGroup(); // SPBU Buka 24 Jam
|
||||
const layerTerbatas = L.layerGroup(); // SPBU Jam Terbatas
|
||||
|
||||
// ── Inisialisasi Peta ────────────────────────────────────────────────────────
|
||||
const map = L.map('map', {
|
||||
center: [-0.0263, 109.3425],
|
||||
zoom: 13,
|
||||
layers: [osm, layer24Jam, layerTerbatas]
|
||||
});
|
||||
|
||||
// ── Layers Control ───────────────────────────────────────────────────────────
|
||||
const baseLayers = {
|
||||
'🗺️ Peta (OpenStreetMap)' : osm,
|
||||
'🛰️ Satelit (Esri)' : sat
|
||||
};
|
||||
const overlays = {
|
||||
'🟢 SPBU Buka 24 Jam' : layer24Jam,
|
||||
'⚫ SPBU Jam Terbatas' : layerTerbatas
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers, overlays, { collapsed: false }).addTo(map);
|
||||
|
||||
// ── Icon Helper ──────────────────────────────────────────────────────────────
|
||||
function mkIcon(color, big) {
|
||||
const s = big ? 34 : 26;
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="width:${s}px;height:${s}px;border-radius:50%;background:${color};
|
||||
border:2.5px solid #fff;box-shadow:0 2px 10px rgba(0,0,0,.22);
|
||||
display:grid;place-items:center;font-size:${big ? 16 : 12}px;transition:.15s;">⛽</div>`,
|
||||
iconSize : [s, s],
|
||||
iconAnchor : [s/2, s/2],
|
||||
popupAnchor: [0, -s/2 - 2]
|
||||
});
|
||||
}
|
||||
|
||||
const markers = {};
|
||||
let activeId = null;
|
||||
|
||||
// ── Buat Marker & Masukkan ke Layer Group yang Sesuai ────────────────────────
|
||||
DATA.forEach(spbu => {
|
||||
const is24 = spbu.status == 1;
|
||||
const color = is24 ? '#1a7a45' : '#b0ada5';
|
||||
|
||||
const m = L.marker([spbu.latitude, spbu.longitude], {
|
||||
icon : mkIcon(color, false),
|
||||
draggable: true
|
||||
});
|
||||
|
||||
m.on('click', () => showAttr(spbu.id));
|
||||
|
||||
// ── Drag & Drop: simpan koordinat baru ke DB via AJAX ────────────────────
|
||||
m.on('dragend', function(e) {
|
||||
const pos = e.target.getLatLng();
|
||||
const lat = pos.lat.toFixed(6);
|
||||
const lng = pos.lng.toFixed(6);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', spbu.id);
|
||||
fd.append('lat', lat);
|
||||
fd.append('lng', lng);
|
||||
|
||||
fetch('index.php?update_coords=1', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
const d = DATA.find(x => x.id == spbu.id);
|
||||
if (d) { d.latitude = lat; d.longitude = lng; }
|
||||
if (activeId == spbu.id) {
|
||||
document.getElementById('ap-coords').textContent = `${lat}, ${lng}`;
|
||||
}
|
||||
showToast('📍 Lokasi berhasil diperbarui!');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('❌ Gagal memperbarui lokasi'));
|
||||
});
|
||||
|
||||
m.addTo(is24 ? layer24Jam : layerTerbatas);
|
||||
|
||||
markers[spbu.id] = { marker: m, data: spbu };
|
||||
});
|
||||
|
||||
// ── Fit Bounds ───────────────────────────────────────────────────────────────
|
||||
if (DATA.length > 0) {
|
||||
const pts = DATA.map(s => [s.latitude, s.longitude]);
|
||||
DATA.length === 1
|
||||
? map.setView(pts[0], 15)
|
||||
: map.fitBounds(pts, { padding: [40, 40] });
|
||||
}
|
||||
|
||||
// ── Panel Atribut ─────────────────────────────────────────────────────────────
|
||||
function showAttr(id) {
|
||||
const obj = markers[id];
|
||||
if (!obj) return;
|
||||
const spbu = DATA.find(x => x.id == id) || obj.data;
|
||||
|
||||
if (activeId && activeId !== id && markers[activeId]) {
|
||||
const prev = DATA.find(x => x.id == activeId) || markers[activeId].data;
|
||||
markers[activeId].marker.setIcon(mkIcon(prev.status == 1 ? '#1a7a45' : '#b0ada5', false));
|
||||
}
|
||||
|
||||
obj.marker.setIcon(mkIcon('#e85a1a', true));
|
||||
activeId = id;
|
||||
|
||||
document.getElementById('ap-nama').textContent = spbu.nama;
|
||||
document.getElementById('ap-no').textContent = spbu.no;
|
||||
const st = document.getElementById('ap-status');
|
||||
if (spbu.status == 1) {
|
||||
st.textContent = '● 24 Jam'; st.className = 'ap-val green';
|
||||
} else {
|
||||
st.textContent = '○ Terbatas'; st.className = 'ap-val red';
|
||||
}
|
||||
document.getElementById('ap-coords').textContent =
|
||||
`${parseFloat(spbu.latitude).toFixed(6)}, ${parseFloat(spbu.longitude).toFixed(6)}`;
|
||||
|
||||
document.getElementById('ap-edit-btn').href = `insert.php?edit=${id}`;
|
||||
document.getElementById('ap-del-btn').onclick = () => deleteSpbu(id, spbu.nama);
|
||||
|
||||
document.getElementById('attr-panel').classList.add('open');
|
||||
map.setView([spbu.latitude, spbu.longitude], Math.max(map.getZoom(), 15), { animate: true });
|
||||
|
||||
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
|
||||
const si = document.querySelector(`.sb-item[data-id="${id}"]`);
|
||||
if (si) { si.classList.add('active'); si.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
document.getElementById('attr-panel').classList.remove('open');
|
||||
if (activeId && markers[activeId]) {
|
||||
const prev = DATA.find(x => x.id == activeId) || markers[activeId].data;
|
||||
markers[activeId].marker.setIcon(mkIcon(prev.status == 1 ? '#1a7a45' : '#b0ada5', false));
|
||||
}
|
||||
activeId = null;
|
||||
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
|
||||
}
|
||||
|
||||
// ── Hapus SPBU ────────────────────────────────────────────────────────────────
|
||||
function deleteSpbu(id, nama) {
|
||||
if (!confirm(`Hapus SPBU "${nama}"?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
fetch('index.php?delete=1', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
if (markers[id]) {
|
||||
const spbu = DATA.find(x => x.id == id) || markers[id].data;
|
||||
(spbu.status == 1 ? layer24Jam : layerTerbatas).removeLayer(markers[id].marker);
|
||||
delete markers[id];
|
||||
}
|
||||
const si = document.querySelector(`.sb-item[data-id="${id}"]`);
|
||||
if (si) si.remove();
|
||||
closePanel();
|
||||
showToast('✕ SPBU berhasil dihapus');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.sb-item').forEach(el => {
|
||||
el.addEventListener('click', () => showAttr(parseInt(el.dataset.id)));
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('input', function() {
|
||||
const q = this.value.toLowerCase();
|
||||
document.querySelectorAll('.sb-item').forEach(el => {
|
||||
const n = el.querySelector('.sb-name').textContent.toLowerCase();
|
||||
const no = el.querySelector('.sb-no').textContent.toLowerCase();
|
||||
el.style.display = (n.includes(q) || no.includes(q)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
function showToast(msg) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.classList.add('show');
|
||||
setTimeout(() => t.classList.remove('show'), 2500);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
require_once 'db_connect.php';
|
||||
|
||||
$message = "";
|
||||
$edit_data = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
if ($_POST['action'] === 'insert') {
|
||||
$stmt = $pdo->prepare("INSERT INTO spbu (nama, no, status, latitude, longitude) VALUES (:nama,:no,:status,:lat,:lng)");
|
||||
$stmt->execute([':nama'=>trim($_POST['nama']),':no'=>trim($_POST['no']),':status'=>isset($_POST['status'])?1:0,':lat'=>floatval($_POST['latitude']),':lng'=>floatval($_POST['longitude'])]);
|
||||
$message = ['type'=>'success','text'=>'Data SPBU berhasil ditambahkan!'];
|
||||
}
|
||||
if ($_POST['action'] === 'update') {
|
||||
$stmt = $pdo->prepare("UPDATE spbu SET nama=:nama,no=:no,status=:status,latitude=:lat,longitude=:lng WHERE id=:id");
|
||||
$stmt->execute([':nama'=>trim($_POST['nama']),':no'=>trim($_POST['no']),':status'=>isset($_POST['status'])?1:0,':lat'=>floatval($_POST['latitude']),':lng'=>floatval($_POST['longitude']),':id'=>intval($_POST['id'])]);
|
||||
$message = ['type'=>'success','text'=>'Data berhasil diperbarui!'];
|
||||
}
|
||||
if ($_POST['action'] === 'delete') {
|
||||
$pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>intval($_POST['id'])]);
|
||||
$message = ['type'=>'danger','text'=>'Data berhasil dihapus.'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['edit'])) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM spbu WHERE id=:id");
|
||||
$stmt->execute([':id'=>intval($_GET['edit'])]);
|
||||
$edit_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$all = $pdo->query("SELECT * FROM spbu ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Input SPBU — WebGIS Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#f5f4f0;--card:#fff;--border:#e8e5df;--border2:#d4d0c8;
|
||||
--text:#1a1915;--muted:#7c7a72;--hint:#b0ada5;
|
||||
--accent:#e85a1a;--accent-bg:#fdf0eb;--accent-dk:#c44a12;
|
||||
--green:#1a7a45;--green-bg:#ebf7f0;
|
||||
--red:#c0392b;--red-bg:#fdf0ef;
|
||||
--r:10px;--r-sm:7px;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.05),0 3px 12px rgba(0,0,0,.04);
|
||||
}
|
||||
body{font-family:'Plus Jakarta Sans',sans-serif;background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
|
||||
|
||||
/* TOP NAV */
|
||||
.topnav{background:var(--card);border-bottom:1px solid var(--border);height:54px;padding:0 24px;display:flex;align-items:center;gap:6px;position:sticky;top:0;z-index:9999}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:700;font-size:14px;color:var(--text);text-decoration:none;margin-right:auto}
|
||||
.brand-mark{width:30px;height:30px;background:var(--accent);border-radius:8px;display:grid;place-items:center;color:#fff;font-size:14px;font-weight:800;letter-spacing:-.5px}
|
||||
.nav-a{padding:5px 13px;border-radius:var(--r-sm);font-size:13px;font-weight:500;color:var(--muted);text-decoration:none;transition:.12s}
|
||||
.nav-a:hover{background:var(--bg);color:var(--text)}
|
||||
.nav-a.on{background:var(--accent-bg);color:var(--accent);font-weight:600}
|
||||
|
||||
/* PAGE */
|
||||
.page{max-width:1140px;margin:0 auto;padding:24px 20px}
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
|
||||
@media(max-width:840px){.grid2{grid-template-columns:1fr}}
|
||||
|
||||
/* CARD */
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--shadow)}
|
||||
.ch{padding:16px 18px 13px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
|
||||
.ch-icon{width:30px;height:30px;border-radius:var(--r-sm);background:var(--bg);display:grid;place-items:center;font-size:14px;flex-shrink:0}
|
||||
.ch h2{font-size:13px;font-weight:600;color:var(--text)}
|
||||
.cb{padding:18px}
|
||||
|
||||
/* ALERT */
|
||||
.alert{display:flex;align-items:center;gap:8px;padding:10px 14px;border-radius:var(--r-sm);margin-bottom:18px;font-size:13px;font-weight:500}
|
||||
.alert.success{background:var(--green-bg);color:var(--green)}
|
||||
.alert.danger{background:var(--red-bg);color:var(--red)}
|
||||
|
||||
/* MAP */
|
||||
#map{height:310px;border-radius:var(--r-sm);border:1px solid var(--border);overflow:hidden}
|
||||
.map-tip{margin-top:8px;font-size:12px;color:var(--hint);display:flex;align-items:center;gap:4px}
|
||||
.coord-tag{display:none;margin-top:10px;background:var(--accent-bg);border:1px solid #f0c5ae;border-radius:20px;padding:4px 12px;font-size:12px;font-weight:500;color:var(--accent);width:fit-content}
|
||||
|
||||
/* FORM */
|
||||
.fg{margin-bottom:13px}
|
||||
.fg label{display:block;font-size:11px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}
|
||||
.fg input[type=text],.fg input[type=number]{width:100%;padding:8px 11px;border:1px solid var(--border2);border-radius:var(--r-sm);font-family:inherit;font-size:13px;color:var(--text);background:#fff;transition:.12s}
|
||||
.fg input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(232,90,26,.09)}
|
||||
.cols2{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
||||
|
||||
/* TOGGLE */
|
||||
.trow{display:flex;align-items:center;gap:9px;padding:8px 0}
|
||||
.sw{position:relative;width:36px;height:19px;flex-shrink:0}
|
||||
.sw input{opacity:0;position:absolute;width:0;height:0}
|
||||
.sw-t{position:absolute;inset:0;background:var(--border2);border-radius:20px;cursor:pointer;transition:.18s}
|
||||
.sw-t::after{content:'';position:absolute;width:13px;height:13px;background:#fff;border-radius:50%;top:3px;left:3px;transition:.18s}
|
||||
.sw input:checked~.sw-t{background:var(--green)}
|
||||
.sw input:checked~.sw-t::after{transform:translateX(17px)}
|
||||
.trow span{font-size:13px;font-weight:500}
|
||||
|
||||
/* BUTTONS */
|
||||
.btn{display:inline-flex;align-items:center;gap:5px;padding:8px 16px;border-radius:var(--r-sm);font-family:inherit;font-size:13px;font-weight:600;border:none;cursor:pointer;transition:.12s;text-decoration:none}
|
||||
.btn-p{background:var(--accent);color:#fff}
|
||||
.btn-p:hover{background:var(--accent-dk)}
|
||||
.btn-g{background:transparent;color:var(--muted);border:1px solid var(--border2)}
|
||||
.btn-g:hover{background:var(--bg);color:var(--text)}
|
||||
.btn-xs{padding:4px 10px;font-size:11px;border-radius:6px}
|
||||
.btn-edit{background:transparent;color:var(--accent);border:1px solid #f0c5ae}
|
||||
.btn-edit:hover{background:var(--accent-bg)}
|
||||
.btn-del{background:transparent;color:var(--red);border:1px solid #f0c4bf}
|
||||
.btn-del:hover{background:var(--red-bg)}
|
||||
.btn-row{display:flex;gap:8px;margin-top:16px}
|
||||
|
||||
/* TABLE */
|
||||
.tw{overflow-x:auto}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th{padding:9px 14px;text-align:left;font-size:11px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border);white-space:nowrap}
|
||||
td{padding:10px 14px;border-bottom:1px solid var(--bg);vertical-align:middle}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:#fafaf8}
|
||||
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:20px;font-size:11px;font-weight:600}
|
||||
.b-open{background:var(--green-bg);color:var(--green)}
|
||||
.b-closed{background:var(--bg);color:var(--hint);border:1px solid var(--border)}
|
||||
.acts{display:flex;gap:5px}
|
||||
.mono{font-family:'Courier New',monospace;font-size:12px;color:var(--muted)}
|
||||
.empty{text-align:center;padding:40px;color:var(--hint)}
|
||||
.pill{background:var(--bg);border:1px solid var(--border);border-radius:20px;padding:1px 8px;font-size:11px;color:var(--hint);margin-left:6px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="topnav">
|
||||
<a href="index.php" class="brand">
|
||||
<div class="brand-mark">S</div>
|
||||
WebGIS SPBU Pontianak
|
||||
</a>
|
||||
<a href="insert.php" class="nav-a on">Input Data</a>
|
||||
<a href="index.php" class="nav-a">Lihat Peta</a>
|
||||
</nav>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<?php if($message): ?>
|
||||
<div class="alert <?= $message['type'] ?>">
|
||||
<?= $message['type']==='success' ? '✓' : '✕' ?> <?= $message['text'] ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="ch"><div class="ch-icon">📍</div><h2>Klik Peta — Tentukan Lokasi</h2></div>
|
||||
<div class="cb">
|
||||
<div id="map"></div>
|
||||
<div class="map-tip">Klik titik mana saja untuk mengisi koordinat otomatis</div>
|
||||
<div class="coord-tag" id="coord-tag"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="ch">
|
||||
<div class="ch-icon"><?= $edit_data ? '✏️' : '➕' ?></div>
|
||||
<h2><?= $edit_data ? 'Edit Data SPBU' : 'Tambah SPBU Baru' ?></h2>
|
||||
</div>
|
||||
<div class="cb">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="<?= $edit_data ? 'update' : 'insert' ?>">
|
||||
<?php if($edit_data): ?>
|
||||
<input type="hidden" name="id" value="<?= $edit_data['id'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="fg">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" name="nama" placeholder="cth: SPBU Jl. Ahmad Yani"
|
||||
value="<?= htmlspecialchars($edit_data['nama'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>Nomor SPBU</label>
|
||||
<input type="text" name="no" placeholder="cth: 64.751.01"
|
||||
value="<?= htmlspecialchars($edit_data['no'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>Status Operasional</label>
|
||||
<div class="trow">
|
||||
<label class="sw">
|
||||
<input type="checkbox" name="status" id="chk" <?= ($edit_data['status'] ?? 0) ? 'checked' : '' ?>>
|
||||
<span class="sw-t"></span>
|
||||
</label>
|
||||
<span>Buka 24 Jam</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>Koordinat</label>
|
||||
<div class="cols2">
|
||||
<input type="number" name="latitude" id="inp-lat" step="any" placeholder="Latitude"
|
||||
value="<?= $edit_data['latitude'] ?? '' ?>" required>
|
||||
<input type="number" name="longitude" id="inp-lng" step="any" placeholder="Longitude"
|
||||
value="<?= $edit_data['longitude'] ?? '' ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button type="submit" class="btn btn-p"><?= $edit_data ? '💾 Simpan' : '📍 Simpan SPBU' ?></button>
|
||||
<?php if($edit_data): ?><a href="insert.php" class="btn btn-g">Batal</a><?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TABEL -->
|
||||
<div class="card" style="margin-top:18px">
|
||||
<div class="ch">
|
||||
<div class="ch-icon">📋</div>
|
||||
<h2>Daftar SPBU <span class="pill"><?= count($all) ?> data</span></h2>
|
||||
</div>
|
||||
<div class="tw">
|
||||
<?php if(!count($all)): ?>
|
||||
<div class="empty">Belum ada data. Tambahkan SPBU lewat form di atas.</div>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Nama</th><th>No</th><th>Status</th><th>Latitude</th><th>Longitude</th><th>Aksi</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($all as $i=>$r): ?>
|
||||
<tr>
|
||||
<td style="color:var(--hint)"><?= $i+1 ?></td>
|
||||
<td style="font-weight:600"><?= htmlspecialchars($r['nama']) ?></td>
|
||||
<td style="color:var(--muted)"><?= htmlspecialchars($r['no']) ?></td>
|
||||
<td><span class="badge <?= $r['status']?'b-open':'b-closed' ?>"><?= $r['status']?'● 24 Jam':'○ Terbatas' ?></span></td>
|
||||
<td class="mono"><?= number_format($r['latitude'],6) ?></td>
|
||||
<td class="mono"><?= number_format($r['longitude'],6) ?></td>
|
||||
<td>
|
||||
<div class="acts">
|
||||
<a href="?edit=<?= $r['id'] ?>" class="btn btn-xs btn-edit">Edit</a>
|
||||
<form method="POST" style="display:inline" onsubmit="return confirm('Hapus SPBU ini?')">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= $r['id'] ?>">
|
||||
<button type="submit" class="btn btn-xs btn-del">Hapus</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const map=L.map('map').setView([-0.0263,109.3425],13);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OSM',maxZoom:19}).addTo(map);
|
||||
|
||||
function mkIcon(color,big){
|
||||
const s=big?32:26;
|
||||
return L.divIcon({className:'',
|
||||
html:`<div style="width:${s}px;height:${s}px;border-radius:50%;background:${color};border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.2);display:grid;place-items:center;font-size:${big?15:12}px;">⛽</div>`,
|
||||
iconSize:[s,s],iconAnchor:[s/2,s/2],popupAnchor:[0,-s/2]});
|
||||
}
|
||||
|
||||
let marker=null;
|
||||
|
||||
<?php if($edit_data): ?>
|
||||
marker=L.marker([<?= $edit_data['latitude'] ?>,<?= $edit_data['longitude'] ?>],{icon:mkIcon('#e85a1a',true),draggable:true}).addTo(map);
|
||||
map.setView([<?= $edit_data['latitude'] ?>,<?= $edit_data['longitude'] ?>],16);
|
||||
marker.on('dragend',e=>{const p=e.target.getLatLng();setCoord(p.lat.toFixed(6),p.lng.toFixed(6));});
|
||||
setCoord(<?= $edit_data['latitude'] ?>,<?= $edit_data['longitude'] ?>);
|
||||
<?php endif; ?>
|
||||
|
||||
<?= json_encode($all) ?>.forEach(s=>{
|
||||
L.marker([s.latitude,s.longitude],{icon:mkIcon(s.status==1?'#1a7a45':'#b0ada5',false)})
|
||||
.addTo(map).bindPopup(`<b>${s.nama}</b><br>${s.no}<br>${s.status==1?'24 Jam':'Terbatas'}`);
|
||||
});
|
||||
|
||||
map.on('click',e=>{
|
||||
const lat=e.latlng.lat.toFixed(6),lng=e.latlng.lng.toFixed(6);
|
||||
if(marker) map.removeLayer(marker);
|
||||
marker=L.marker([lat,lng],{icon:mkIcon('#e85a1a',true),draggable:true}).addTo(map);
|
||||
marker.on('dragend',ev=>{const p=ev.target.getLatLng();setCoord(p.lat.toFixed(6),p.lng.toFixed(6));});
|
||||
setCoord(lat,lng);
|
||||
});
|
||||
|
||||
function setCoord(lat,lng){
|
||||
document.getElementById('inp-lat').value=lat;
|
||||
document.getElementById('inp-lng').value=lng;
|
||||
const t=document.getElementById('coord-tag');
|
||||
t.textContent=`📍 ${lat}, ${lng}`;
|
||||
t.style.display='block';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
require_once 'db_connect.php';
|
||||
|
||||
// AJAX: update koordinat saat marker di-drag
|
||||
if (isset($_GET['update_coords']) && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
header('Content-Type: application/json');
|
||||
$id = intval($_POST['id']);
|
||||
$lat = floatval($_POST['lat']);
|
||||
$lng = floatval($_POST['lng']);
|
||||
if($id > 0){
|
||||
$pdo->prepare("UPDATE spbu SET latitude=:lat,longitude=:lng WHERE id=:id")
|
||||
->execute([':lat'=>$lat,':lng'=>$lng,':id'=>$id]);
|
||||
echo json_encode(['ok'=>true,'lat'=>$lat,'lng'=>$lng]);
|
||||
} else {
|
||||
echo json_encode(['ok'=>false]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// AJAX: hapus dari show.php
|
||||
if (isset($_GET['delete']) && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
header('Content-Type: application/json');
|
||||
$id = intval($_POST['id']);
|
||||
$pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>$id]);
|
||||
echo json_encode(['ok'=>true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$spbu_list = $pdo->query("SELECT * FROM spbu ORDER BY nama")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$total = count($spbu_list);
|
||||
$total_24 = count(array_filter($spbu_list, fn($s) => $s['status']==1));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Peta SPBU — WebGIS Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#f5f4f0;--card:#fff;--border:#e8e5df;--border2:#d4d0c8;
|
||||
--text:#1a1915;--muted:#7c7a72;--hint:#b0ada5;
|
||||
--accent:#e85a1a;--accent-bg:#fdf0eb;--accent-dk:#c44a12;
|
||||
--green:#1a7a45;--green-bg:#ebf7f0;
|
||||
--red:#c0392b;--red-bg:#fdf0ef;
|
||||
--r:10px;--r-sm:7px;
|
||||
}
|
||||
html,body{height:100%;font-family:'Plus Jakarta Sans',sans-serif;color:var(--text);font-size:14px}
|
||||
body{display:flex;flex-direction:column;background:var(--bg)}
|
||||
|
||||
/* NAV */
|
||||
.topnav{background:var(--card);border-bottom:1px solid var(--border);height:54px;padding:0 24px;display:flex;align-items:center;gap:6px;flex-shrink:0;z-index:9999;position:relative}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:700;font-size:14px;color:var(--text);text-decoration:none;margin-right:auto}
|
||||
.brand-mark{width:30px;height:30px;background:var(--accent);border-radius:8px;display:grid;place-items:center;color:#fff;font-size:14px;font-weight:800}
|
||||
.nav-a{padding:5px 13px;border-radius:var(--r-sm);font-size:13px;font-weight:500;color:var(--muted);text-decoration:none;transition:.12s}
|
||||
.nav-a:hover{background:var(--bg);color:var(--text)}
|
||||
.nav-a.on{background:var(--accent-bg);color:var(--accent);font-weight:600}
|
||||
|
||||
/* STATS BAR */
|
||||
.statsbar{background:var(--card);border-bottom:1px solid var(--border);padding:0 24px;height:40px;display:flex;align-items:center;gap:18px;flex-shrink:0}
|
||||
.stat{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
||||
.stat strong{color:var(--text);font-weight:600}
|
||||
.stat-dot{width:8px;height:8px;border-radius:50%}
|
||||
|
||||
/* MAIN */
|
||||
.main{display:flex;flex:1;overflow:hidden}
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar{width:300px;flex-shrink:0;background:var(--card);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
|
||||
.sb-head{padding:14px 16px 10px;border-bottom:1px solid var(--border);flex-shrink:0}
|
||||
.sb-head h2{font-size:13px;font-weight:600;margin-bottom:9px;color:var(--text)}
|
||||
.search{width:100%;padding:7px 11px;border:1px solid var(--border2);border-radius:var(--r-sm);font-family:inherit;font-size:13px;color:var(--text);background:#fff;transition:.12s}
|
||||
.search:focus{outline:none;border-color:var(--accent)}
|
||||
.sb-list{flex:1;overflow-y:auto;padding:8px}
|
||||
.sb-list::-webkit-scrollbar{width:3px}
|
||||
.sb-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||||
.sb-item{border:1px solid var(--border);border-radius:var(--r-sm);padding:10px 12px;margin-bottom:6px;cursor:pointer;transition:.12s}
|
||||
.sb-item:hover{border-color:var(--border2);background:#fafaf8}
|
||||
.sb-item.active{border-color:var(--accent);background:var(--accent-bg)}
|
||||
.sb-name{font-weight:600;font-size:13px;margin-bottom:2px}
|
||||
.sb-no{font-size:12px;color:var(--muted)}
|
||||
.sb-badge{display:inline-flex;align-items:center;gap:3px;margin-top:5px;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600}
|
||||
.sb-open{background:var(--green-bg);color:var(--green)}
|
||||
.sb-closed{background:var(--bg);color:var(--hint);border:1px solid var(--border)}
|
||||
.empty-sb{text-align:center;padding:32px 16px;color:var(--hint);font-size:13px}
|
||||
|
||||
/* MAP */
|
||||
.mapwrap{flex:1;position:relative;overflow:hidden}
|
||||
#map{width:100%;height:100%}
|
||||
|
||||
/* Rapikan tampilan Leaflet Layers Control */
|
||||
.leaflet-control-layers{
|
||||
border:1px solid var(--border) !important;
|
||||
border-radius:var(--r-sm) !important;
|
||||
box-shadow:0 2px 12px rgba(0,0,0,.09) !important;
|
||||
font-family:'Plus Jakarta Sans',sans-serif !important;
|
||||
font-size:13px !important;
|
||||
}
|
||||
.leaflet-control-layers-expanded{padding:10px 14px !important}
|
||||
.leaflet-control-layers-separator{margin:6px 0 !important;border-color:var(--border) !important}
|
||||
|
||||
/* TOAST */
|
||||
.toast{position:absolute;top:12px;left:50%;transform:translateX(-50%) translateY(-60px);z-index:1001;background:#1a1915;color:#fff;font-size:12px;font-weight:500;padding:8px 16px;border-radius:20px;transition:.25s;pointer-events:none;white-space:nowrap}
|
||||
.toast.show{transform:translateX(-50%) translateY(0)}
|
||||
|
||||
/* ATTR PANEL */
|
||||
#attr-panel{
|
||||
position:absolute;bottom:0;left:0;right:0;
|
||||
background:var(--card);border-top:2px solid var(--accent);
|
||||
transform:translateY(100%);transition:transform .28s cubic-bezier(.4,0,.2,1);
|
||||
z-index:1000;
|
||||
}
|
||||
#attr-panel.open{transform:translateY(0)}
|
||||
.ap-head{padding:12px 18px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.ap-title{font-size:13px;font-weight:700;color:var(--accent)}
|
||||
.ap-actions{display:flex;gap:7px;align-items:center}
|
||||
.ap-close{background:none;border:1px solid var(--border2);color:var(--muted);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;transition:.12s}
|
||||
.ap-close:hover{border-color:var(--muted)}
|
||||
.ap-del{background:none;border:1px solid #f0c4bf;color:var(--red);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;transition:.12s}
|
||||
.ap-del:hover{background:var(--red-bg)}
|
||||
.ap-edit{background:none;border:1px solid #f0c5ae;color:var(--accent);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;transition:.12s;text-decoration:none}
|
||||
.ap-edit:hover{background:var(--accent-bg)}
|
||||
.ap-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:0;padding:12px 18px 16px}
|
||||
@media(max-width:600px){.ap-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.ap-field{padding:6px 10px}
|
||||
.ap-label{font-size:10px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}
|
||||
.ap-val{font-size:14px;font-weight:600;color:var(--text)}
|
||||
.ap-val.green{color:var(--green)}
|
||||
.ap-val.red{color:var(--red)}
|
||||
.ap-val.mono{font-size:12px;font-family:'Courier New',monospace;font-weight:500;color:var(--muted)}
|
||||
.drag-hint{font-size:11px;color:var(--hint);padding:0 18px 10px;display:flex;align-items:center;gap:4px}
|
||||
|
||||
@media(max-width:680px){
|
||||
.sidebar{width:100%;max-height:200px;border-right:none;border-bottom:1px solid var(--border)}
|
||||
.main{flex-direction:column}
|
||||
.statsbar .stat:nth-child(n+3){display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="topnav">
|
||||
<a href="index.php" class="brand"><div class="brand-mark">S</div>WebGIS SPBU Pontianak</a>
|
||||
<a href="insert.php" class="nav-a">Input Data</a>
|
||||
<a href="index.php" class="nav-a on">Lihat Peta</a>
|
||||
</nav>
|
||||
|
||||
<div class="statsbar">
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--accent)"></div>Total <strong><?= $total ?></strong> SPBU</div>
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--green)"></div>Buka 24 Jam: <strong><?= $total_24 ?></strong></div>
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--hint)"></div>Terbatas: <strong><?= $total - $total_24 ?></strong></div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<div class="sidebar">
|
||||
<div class="sb-head">
|
||||
<h2>Daftar SPBU</h2>
|
||||
<input class="search" id="search" type="text" placeholder="Cari nama atau nomor...">
|
||||
</div>
|
||||
<div class="sb-list" id="sb-list">
|
||||
<?php if(!$total): ?>
|
||||
<div class="empty-sb">Belum ada data.<br><a href="insert.php" style="color:var(--accent)">Tambahkan →</a></div>
|
||||
<?php else: foreach($spbu_list as $s): ?>
|
||||
<div class="sb-item" data-id="<?= $s['id'] ?>">
|
||||
<div class="sb-name"><?= htmlspecialchars($s['nama']) ?></div>
|
||||
<div class="sb-no">No. <?= htmlspecialchars($s['no']) ?></div>
|
||||
<span class="sb-badge <?= $s['status']?'sb-open':'sb-closed' ?>"><?= $s['status']?'● 24 Jam':'○ Terbatas' ?></span>
|
||||
</div>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAP -->
|
||||
<div class="mapwrap">
|
||||
<div id="map"></div>
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<div id="attr-panel">
|
||||
<div class="ap-head">
|
||||
<span class="ap-title">⛽ Informasi SPBU</span>
|
||||
<div class="ap-actions">
|
||||
<a href="#" class="ap-edit" id="ap-edit-btn">Edit</a>
|
||||
<button class="ap-del" id="ap-del-btn">Hapus</button>
|
||||
<button class="ap-close" onclick="closePanel()">✕ Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ap-grid">
|
||||
<div class="ap-field"><div class="ap-label">Nama</div><div class="ap-val" id="ap-nama">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Nomor SPBU</div><div class="ap-val" id="ap-no">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Status</div><div class="ap-val" id="ap-status">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Koordinat</div><div class="ap-val mono" id="ap-coords">—</div></div>
|
||||
</div>
|
||||
<div class="drag-hint">⟳ Drag marker di peta untuk memperbarui lokasi secara langsung</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const DATA = <?= json_encode($spbu_list, JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
// ── Base Layers ──────────────────────────────────────────────────────────────
|
||||
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap', maxZoom: 19
|
||||
});
|
||||
const sat = L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
{ attribution: 'Esri', maxZoom: 19 }
|
||||
);
|
||||
|
||||
// ── Layer Groups: pisah SPBU berdasarkan status ──────────────────────────────
|
||||
// Setiap group berisi marker-marker sesuai jenisnya.
|
||||
// Pengguna bisa toggle tampilan lewat checkbox di Layers Control.
|
||||
const layer24Jam = L.layerGroup(); // SPBU Buka 24 Jam
|
||||
const layerTerbatas = L.layerGroup(); // SPBU Jam Terbatas
|
||||
|
||||
// ── Inisialisasi Peta ────────────────────────────────────────────────────────
|
||||
// Sertakan kedua overlay agar keduanya aktif (ter-centang) saat halaman dimuat
|
||||
const map = L.map('map', {
|
||||
center: [-0.0263, 109.3425],
|
||||
zoom: 13,
|
||||
layers: [osm, layer24Jam, layerTerbatas]
|
||||
});
|
||||
|
||||
// ── Layers Control ───────────────────────────────────────────────────────────
|
||||
// baseLayers → radio button, hanya bisa pilih satu base map
|
||||
// overlays → checkbox, bisa pilih / sembunyikan per jenis SPBU
|
||||
const baseLayers = {
|
||||
'🗺️ Peta (OpenStreetMap)' : osm,
|
||||
'🛰️ Satelit (Esri)' : sat
|
||||
};
|
||||
const overlays = {
|
||||
'🟢 SPBU Buka 24 Jam' : layer24Jam,
|
||||
'⚫ SPBU Jam Terbatas' : layerTerbatas
|
||||
};
|
||||
|
||||
// collapsed:false → panel langsung terbuka tanpa harus hover
|
||||
L.control.layers(baseLayers, overlays, { collapsed: false }).addTo(map);
|
||||
|
||||
// ── Icon Helper ──────────────────────────────────────────────────────────────
|
||||
function mkIcon(color, big) {
|
||||
const s = big ? 34 : 26;
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="width:${s}px;height:${s}px;border-radius:50%;background:${color};
|
||||
border:2.5px solid #fff;box-shadow:0 2px 10px rgba(0,0,0,.22);
|
||||
display:grid;place-items:center;font-size:${big ? 16 : 12}px;transition:.15s;">⛽</div>`,
|
||||
iconSize : [s, s],
|
||||
iconAnchor : [s/2, s/2],
|
||||
popupAnchor: [0, -s/2 - 2]
|
||||
});
|
||||
}
|
||||
|
||||
const markers = {};
|
||||
let activeId = null;
|
||||
|
||||
// ── Buat Marker & Masukkan ke Layer Group yang Sesuai ────────────────────────
|
||||
DATA.forEach(spbu => {
|
||||
const is24 = spbu.status == 1;
|
||||
const color = is24 ? '#1a7a45' : '#b0ada5';
|
||||
|
||||
// draggable:true → aktifkan fitur drag & drop lokasi
|
||||
const m = L.marker([spbu.latitude, spbu.longitude], {
|
||||
icon : mkIcon(color, false),
|
||||
draggable: true
|
||||
});
|
||||
|
||||
// Klik marker → tampilkan panel atribut
|
||||
m.on('click', () => showAttr(spbu.id));
|
||||
|
||||
// ── Drag & Drop: simpan koordinat baru ke DB via AJAX ────────────────────
|
||||
m.on('dragend', function(e) {
|
||||
const pos = e.target.getLatLng();
|
||||
const lat = pos.lat.toFixed(6);
|
||||
const lng = pos.lng.toFixed(6);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', spbu.id);
|
||||
fd.append('lat', lat);
|
||||
fd.append('lng', lng);
|
||||
|
||||
fetch('show.php?update_coords=1', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
// Sinkronkan data lokal agar panel atribut juga update
|
||||
const d = DATA.find(x => x.id == spbu.id);
|
||||
if (d) { d.latitude = lat; d.longitude = lng; }
|
||||
if (activeId == spbu.id) {
|
||||
document.getElementById('ap-coords').textContent = `${lat}, ${lng}`;
|
||||
}
|
||||
showToast('📍 Lokasi berhasil diperbarui!');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('❌ Gagal memperbarui lokasi'));
|
||||
});
|
||||
|
||||
// Masukkan ke layer group yang tepat
|
||||
m.addTo(is24 ? layer24Jam : layerTerbatas);
|
||||
|
||||
markers[spbu.id] = { marker: m, data: spbu };
|
||||
});
|
||||
|
||||
// ── Fit Bounds ───────────────────────────────────────────────────────────────
|
||||
if (DATA.length > 0) {
|
||||
const pts = DATA.map(s => [s.latitude, s.longitude]);
|
||||
DATA.length === 1
|
||||
? map.setView(pts[0], 15)
|
||||
: map.fitBounds(pts, { padding: [40, 40] });
|
||||
}
|
||||
|
||||
// ── Panel Atribut ─────────────────────────────────────────────────────────────
|
||||
function showAttr(id) {
|
||||
const obj = markers[id];
|
||||
if (!obj) return;
|
||||
const spbu = DATA.find(x => x.id == id) || obj.data;
|
||||
|
||||
// Reset icon marker yang sebelumnya aktif
|
||||
if (activeId && activeId !== id && markers[activeId]) {
|
||||
const prev = DATA.find(x => x.id == activeId) || markers[activeId].data;
|
||||
markers[activeId].marker.setIcon(mkIcon(prev.status == 1 ? '#1a7a45' : '#b0ada5', false));
|
||||
}
|
||||
|
||||
// Besarkan & warnai marker aktif
|
||||
obj.marker.setIcon(mkIcon('#e85a1a', true));
|
||||
activeId = id;
|
||||
|
||||
// Isi panel
|
||||
document.getElementById('ap-nama').textContent = spbu.nama;
|
||||
document.getElementById('ap-no').textContent = spbu.no;
|
||||
const st = document.getElementById('ap-status');
|
||||
if (spbu.status == 1) {
|
||||
st.textContent = '● 24 Jam'; st.className = 'ap-val green';
|
||||
} else {
|
||||
st.textContent = '○ Terbatas'; st.className = 'ap-val red';
|
||||
}
|
||||
document.getElementById('ap-coords').textContent =
|
||||
`${parseFloat(spbu.latitude).toFixed(6)}, ${parseFloat(spbu.longitude).toFixed(6)}`;
|
||||
|
||||
document.getElementById('ap-edit-btn').href = `insert.php?edit=${id}`;
|
||||
document.getElementById('ap-del-btn').onclick = () => deleteSpbu(id, spbu.nama);
|
||||
|
||||
document.getElementById('attr-panel').classList.add('open');
|
||||
map.setView([spbu.latitude, spbu.longitude], Math.max(map.getZoom(), 15), { animate: true });
|
||||
|
||||
// Highlight sidebar
|
||||
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
|
||||
const si = document.querySelector(`.sb-item[data-id="${id}"]`);
|
||||
if (si) { si.classList.add('active'); si.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
document.getElementById('attr-panel').classList.remove('open');
|
||||
if (activeId && markers[activeId]) {
|
||||
const prev = DATA.find(x => x.id == activeId) || markers[activeId].data;
|
||||
markers[activeId].marker.setIcon(mkIcon(prev.status == 1 ? '#1a7a45' : '#b0ada5', false));
|
||||
}
|
||||
activeId = null;
|
||||
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
|
||||
}
|
||||
|
||||
// ── Hapus SPBU ────────────────────────────────────────────────────────────────
|
||||
function deleteSpbu(id, nama) {
|
||||
if (!confirm(`Hapus SPBU "${nama}"?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
fetch('show.php?delete=1', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
if (markers[id]) {
|
||||
// Hapus dari layer group yang tepat, bukan langsung dari map
|
||||
const spbu = DATA.find(x => x.id == id) || markers[id].data;
|
||||
(spbu.status == 1 ? layer24Jam : layerTerbatas).removeLayer(markers[id].marker);
|
||||
delete markers[id];
|
||||
}
|
||||
const si = document.querySelector(`.sb-item[data-id="${id}"]`);
|
||||
if (si) si.remove();
|
||||
closePanel();
|
||||
showToast('✕ SPBU berhasil dihapus');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sidebar: klik & search ────────────────────────────────────────────────────
|
||||
document.querySelectorAll('.sb-item').forEach(el => {
|
||||
el.addEventListener('click', () => showAttr(parseInt(el.dataset.id)));
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('input', function() {
|
||||
const q = this.value.toLowerCase();
|
||||
document.querySelectorAll('.sb-item').forEach(el => {
|
||||
const n = el.querySelector('.sb-name').textContent.toLowerCase();
|
||||
const no = el.querySelector('.sb-no').textContent.toLowerCase();
|
||||
el.style.display = (n.includes(q) || no.includes(q)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// ── Toast notifikasi ──────────────────────────────────────────────────────────
|
||||
function showToast(msg) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.classList.add('show');
|
||||
setTimeout(() => t.classList.remove('show'), 2500);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+614
@@ -0,0 +1,614 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Portal WebGIS Pontianak — SIG Hub</title>
|
||||
|
||||
<!-- Modern Typography (Plus Jakarta Sans) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome CDN for micro-icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
/* Design Tokens (Light/Bright Mode) */
|
||||
:root {
|
||||
--bg-light: hsl(210, 40%, 98%);
|
||||
--bg-card: hsla(0, 0%, 100%, 0.8);
|
||||
--border-color: hsla(217, 32%, 18%, 0.06);
|
||||
--text-main: hsl(222, 47%, 12%);
|
||||
--text-muted: hsl(215, 15%, 35%);
|
||||
--text-dim: hsl(215, 15%, 55%);
|
||||
|
||||
/* Glowing colors for individual projects (adjusted for light theme readability) */
|
||||
--color-road-primary: hsl(199, 89%, 42%);
|
||||
--color-road-bg: hsla(199, 89%, 42%, 0.08);
|
||||
|
||||
--color-spbu-primary: hsl(30, 95%, 45%);
|
||||
--color-spbu-bg: hsla(30, 95%, 45%, 0.08);
|
||||
|
||||
--color-poverty-primary: hsl(142, 65%, 32%);
|
||||
--color-poverty-bg: hsla(142, 65%, 32%, 0.08);
|
||||
|
||||
--font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
}
|
||||
|
||||
/* Base Styling */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--bg-light);
|
||||
background-image: radial-gradient(at 0% 0%, hsla(210, 100%, 98%, 1) 0, transparent 50%),
|
||||
radial-gradient(at 50% 0%, hsla(220, 100%, 97%, 1) 0, transparent 50%);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ambient Background Glows */
|
||||
.bg-ambient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
opacity: 0.12;
|
||||
animation: float 25s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 40vw;
|
||||
height: 40vw;
|
||||
background: radial-gradient(circle, var(--color-road-primary) 0%, transparent 80%);
|
||||
top: -10%;
|
||||
right: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 35vw;
|
||||
height: 35vw;
|
||||
background: radial-gradient(circle, var(--color-poverty-primary) 0%, transparent 80%);
|
||||
bottom: -10%;
|
||||
left: 10%;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 30vw;
|
||||
height: 30vw;
|
||||
background: radial-gradient(circle, var(--color-spbu-primary) 0%, transparent 80%);
|
||||
top: 40%;
|
||||
left: 45%;
|
||||
animation-delay: -10s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translate(0, 0) scale(1) rotate(0deg); }
|
||||
50% { transform: translate(6%, 4%) scale(1.05) rotate(180deg); }
|
||||
100% { transform: translate(-4%, -6%) scale(0.95) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Grid Overlay Pattern */
|
||||
.grid-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background-image:
|
||||
linear-gradient(to right, hsla(217, 32%, 18%, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, hsla(217, 32%, 18%, 0.03) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Header Panel */
|
||||
header {
|
||||
width: 100%;
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, hsl(200, 100%, 45%), hsl(260, 100%, 55%));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px hsla(200, 100%, 45%, 0.2);
|
||||
}
|
||||
|
||||
.logo-text h1 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.logo-text p {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-dim);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: hsla(0, 0%, 100%, 0.7);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 6px 14px;
|
||||
border-radius: 30px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: hsl(142, 72%, 45%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-dot::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid hsl(142, 72%, 45%);
|
||||
opacity: 0.6;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
100% { transform: scale(2.2); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Main Container */
|
||||
main {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: 1300px;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hero-tag {
|
||||
display: inline-block;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
background: linear-gradient(135deg, hsla(200, 100%, 45%, 0.08), hsla(260, 100%, 55%, 0.08));
|
||||
border: 1px solid hsla(200, 100%, 45%, 0.15);
|
||||
color: hsl(200, 100%, 35%);
|
||||
padding: 6px 16px;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Project Cards Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Card Item */
|
||||
.card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 24px;
|
||||
padding: 2.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
|
||||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
/* Hover effect */
|
||||
.card:hover {
|
||||
transform: translateY(-8px);
|
||||
background-color: #ffffff;
|
||||
border-color: var(--card-hover-border);
|
||||
box-shadow:
|
||||
0 20px 40px -15px rgba(0, 0, 0, 0.06),
|
||||
0 0 20px -2px var(--card-glow);
|
||||
}
|
||||
|
||||
/* Colors and values specific to each card */
|
||||
.card-road {
|
||||
--card-theme-color: var(--color-road-primary);
|
||||
--card-theme-bg: var(--color-road-bg);
|
||||
--card-hover-border: hsla(199, 89%, 42%, 0.3);
|
||||
--card-glow: hsla(199, 89%, 42%, 0.1);
|
||||
}
|
||||
|
||||
.card-spbu {
|
||||
--card-theme-color: var(--color-spbu-primary);
|
||||
--card-theme-bg: var(--color-spbu-bg);
|
||||
--card-hover-border: hsla(30, 95%, 45%, 0.3);
|
||||
--card-glow: hsla(30, 95%, 45%, 0.1);
|
||||
}
|
||||
|
||||
.card-poverty {
|
||||
--card-theme-color: var(--color-poverty-primary);
|
||||
--card-theme-bg: var(--color-poverty-bg);
|
||||
--card-hover-border: hsla(142, 65%, 32%, 0.3);
|
||||
--card-glow: hsla(142, 65%, 32%, 0.1);
|
||||
}
|
||||
|
||||
/* Card Top Decoration */
|
||||
.card-icon-container {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 16px;
|
||||
background-color: var(--card-theme-bg);
|
||||
border: 1px solid rgba(0, 0, 0, 0.02);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
color: var(--card-theme-color);
|
||||
margin-bottom: 2rem;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-icon-container {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-bottom: 2.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-main);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-title {
|
||||
color: var(--card-theme-color);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Badges / Tech tags */
|
||||
.tech-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
background-color: hsla(0, 0%, 0%, 0.03);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Card Action Link styles */
|
||||
.card-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 1.25rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .action-text {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.action-arrow {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: hsla(0, 0%, 0%, 0.03);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .action-arrow {
|
||||
background-color: var(--card-theme-color);
|
||||
color: white;
|
||||
border-color: var(--card-theme-color);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
width: 100%;
|
||||
max-width: 1300px;
|
||||
margin: auto auto 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-dim);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Background decorations -->
|
||||
<div class="bg-ambient">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
</div>
|
||||
<div class="grid-overlay"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div class="logo-container">
|
||||
<div class="logo-icon">
|
||||
<i class="fa-solid fa-map-location-dot"></i>
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
<h1>GIS HUB PONTIANAK</h1>
|
||||
<p>Sistem Informasi Geografis Terintegrasi</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-panel">
|
||||
<div class="status-badge">
|
||||
<div class="status-dot"></div>
|
||||
<span>Server Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<div class="hero">
|
||||
<span class="hero-tag">Geographic Information System</span>
|
||||
<h2>Portal Aplikasi WebGIS</h2>
|
||||
<p>Akses cepat ke berbagai modul pemetaan interaktif Kota Pontianak. Dibuat menggunakan teknologi web modern dan visualisasi data spasial secara real-time.</p>
|
||||
</div>
|
||||
|
||||
<!-- Grid of Projects -->
|
||||
<div class="grid">
|
||||
|
||||
<!-- Card 1: SIG Jalan & Parsil Tanah -->
|
||||
<a href="http://localhost/SIG/SIG_Tanah_Jalan/" class="card card-road">
|
||||
<div>
|
||||
<div class="card-icon-container">
|
||||
<i class="fa-solid fa-road"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">SIG Jalan & Parsil Tanah</h3>
|
||||
<p class="card-desc">Sistem monitoring inventaris jalan, bidang tanah parsil (SHM, HGB, HGU), serta pelaporan tingkat kerusakan jalan terintegrasi dengan Leaflet.draw.</p>
|
||||
<div class="tech-tags">
|
||||
<span class="tag">Leaflet.js</span>
|
||||
<span class="tag">Leaflet.draw</span>
|
||||
<span class="tag">PHP</span>
|
||||
<span class="tag">MySQL</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<span class="action-text">Buka Aplikasi</span>
|
||||
<div class="action-arrow">
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Card 2: SIG SPBU Pontianak -->
|
||||
<a href="http://localhost/SIG/SPBU_Pontianak/" class="card card-spbu">
|
||||
<div>
|
||||
<div class="card-icon-container">
|
||||
<i class="fa-solid fa-gas-pump"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">SIG Sebaran SPBU</h3>
|
||||
<p class="card-desc">Modul pemetaan lokasi sebaran Stasiun Pengisian Bahan Bakar Umum (SPBU) di Kota Pontianak, lengkap dengan input fasilitas dan galeri foto lokasi.</p>
|
||||
<div class="tech-tags">
|
||||
<span class="tag">Leaflet.js</span>
|
||||
<span class="tag">Bootstrap</span>
|
||||
<span class="tag">PHP</span>
|
||||
<span class="tag">MySQL</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<span class="action-text">Buka Aplikasi</span>
|
||||
<div class="action-arrow">
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Card 3: WebGIS PovertyMap -->
|
||||
<a href="http://localhost/SIG/sig-kemiskinan/" class="card card-poverty">
|
||||
<div>
|
||||
<div class="card-icon-container">
|
||||
<i class="fa-solid fa-people-roof"></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">WebGIS PovertyMap</h3>
|
||||
<p class="card-desc">WebGIS Monitoring Bantuan Sosial untuk memetakan konsentrasi penduduk miskin, visualisasi demografis, dan penyaluran bantuan sosial tepat saran.</p>
|
||||
<div class="tech-tags">
|
||||
<span class="tag">Leaflet.js</span>
|
||||
<span class="tag">GeoJSON</span>
|
||||
<span class="tag">Bootstrap 5</span>
|
||||
<span class="tag">Chart.js</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<span class="action-text">Buka Aplikasi</span>
|
||||
<div class="action-arrow">
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<div>
|
||||
© 2026 <strong>SIG Pontianak Hub</strong>. Semua Hak Cipta Dilindungi.
|
||||
</div>
|
||||
<div>
|
||||
Host: <a href="http://localhost/SIG/" target="_blank">localhost/SIG</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
# SIG Penduduk Miskin – Berbasis Rumah Ibadah
|
||||
|
||||
Aplikasi Sistem Informasi Geografis (SIG) untuk pendataan penduduk miskin berdasarkan wilayah radius rumah ibadah terdekat.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Struktur Folder
|
||||
|
||||
```
|
||||
sig-kemiskinan/
|
||||
├── index.html ← Halaman utama (Leaflet + JS)
|
||||
├── database.sql ← Schema + data contoh
|
||||
├── includes/
|
||||
│ └── config.php ← Koneksi DB + fungsi utilitas
|
||||
├── api/
|
||||
│ ├── rumah_ibadah.php ← CRUD Rumah Ibadah
|
||||
│ ├── penduduk.php ← CRUD Penduduk Miskin
|
||||
│ ├── geocode.php ← Reverse Geocoding (Nominatim)
|
||||
│ └── update_radius.php ← Update radius + reassign
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Setup
|
||||
|
||||
### 1. Persyaratan
|
||||
- PHP 7.4+ (atau 8.x)
|
||||
- MySQL 5.7+ / MariaDB
|
||||
- Web server (Apache/Nginx) atau PHP built-in server
|
||||
|
||||
### 2. Database
|
||||
```sql
|
||||
-- Import file database.sql ke MySQL:
|
||||
mysql -u root -p < database.sql
|
||||
```
|
||||
Atau lewat phpMyAdmin: Import → pilih `database.sql`
|
||||
|
||||
### 3. Konfigurasi DB
|
||||
Edit `includes/config.php`:
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root'); // ganti sesuai user MySQL Anda
|
||||
define('DB_PASS', ''); // ganti dengan password MySQL
|
||||
define('DB_NAME', 'sig_kemiskinan');
|
||||
```
|
||||
|
||||
### 4. Jalankan
|
||||
```bash
|
||||
# Opsi 1: PHP built-in server (development)
|
||||
cd sig-kemiskinan
|
||||
php -S localhost:8080
|
||||
|
||||
# Opsi 2: Taruh di folder htdocs (XAMPP/WAMP)
|
||||
# Copy folder ke C:/xampp/htdocs/sig-kemiskinan
|
||||
# Akses: http://localhost/sig-kemiskinan
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Cara Penggunaan
|
||||
|
||||
### Tambah Rumah Ibadah
|
||||
1. Klik tombol **"+ Rumah Ibadah"** (pojok kanan atas)
|
||||
2. Kursor berubah → **klik lokasi di peta**
|
||||
3. Isi form: Nama, Jenis, PIC, No. WA, Radius
|
||||
4. Alamat **otomatis terisi** dari reverse geocoding (Nominatim/OSM)
|
||||
5. Klik **Simpan**
|
||||
|
||||
### Tambah Penduduk Miskin
|
||||
1. Klik tombol **"+ Penduduk"**
|
||||
2. **Klik lokasi rumah** di peta
|
||||
3. Isi: Nama KK, Jumlah anggota
|
||||
4. Sistem otomatis menentukan rumah ibadah terdekat dalam radius
|
||||
5. **Titik hijau** = dalam radius | **Titik merah** = luar radius
|
||||
|
||||
### Ubah Radius
|
||||
1. Klik marker rumah ibadah → popup → tombol **📐 Radius**
|
||||
2. Atau klik ikon 📐 di list sidebar
|
||||
3. **Drag slider** untuk mengubah radius (preview langsung)
|
||||
4. Klik **Simpan** → semua penduduk di-reassign otomatis
|
||||
|
||||
### Logika Overlap
|
||||
Jika penduduk masuk dalam radius **>1 rumah ibadah**:
|
||||
→ Diassign ke **rumah ibadah yang paling dekat** (Haversine distance)
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API Endpoints
|
||||
|
||||
| Method | URL | Fungsi |
|
||||
|--------|-----|--------|
|
||||
| GET | `api/rumah_ibadah.php` | Semua rumah ibadah + stats |
|
||||
| POST | `api/rumah_ibadah.php` | Tambah rumah ibadah |
|
||||
| PUT | `api/rumah_ibadah.php` | Update rumah ibadah |
|
||||
| DELETE | `api/rumah_ibadah.php?id=X` | Hapus |
|
||||
| GET | `api/penduduk.php` | Semua penduduk |
|
||||
| POST | `api/penduduk.php` | Tambah penduduk |
|
||||
| PUT | `api/penduduk.php` | Update penduduk |
|
||||
| DELETE | `api/penduduk.php?id=X` | Hapus |
|
||||
| GET | `api/geocode.php?lat=X&lng=Y` | Reverse geocoding |
|
||||
| POST | `api/update_radius.php` | Update radius + reassign |
|
||||
|
||||
---
|
||||
|
||||
## 🧮 Algoritma Radius
|
||||
|
||||
```
|
||||
Jarak = Haversine(lat1, lng1, lat2, lng2) // dalam meter
|
||||
|
||||
Untuk setiap penduduk P:
|
||||
candidates = [ibadah | jarak(P, ibadah) <= ibadah.radius]
|
||||
if candidates:
|
||||
assign P → min(candidates, key=jarak)
|
||||
else:
|
||||
P.rumah_ibadah_id = NULL // luar radius semua
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Fitur UI
|
||||
|
||||
- Peta Leaflet dengan tile OpenStreetMap
|
||||
- Sidebar panel: list Rumah Ibadah & Penduduk
|
||||
- Stats bar: total ibadah, KK, jiwa, tercakup/tidak
|
||||
- Radius fleksibel via slider (100–3000m, default 500m)
|
||||
- Lingkaran radius real-time di peta
|
||||
- Reverse geocoding otomatis saat klik peta
|
||||
- Warna marker penduduk: hijau (dalam radius) / merah (luar)
|
||||
- Popup informatif + tombol CRUD langsung dari peta
|
||||
- Responsive mobile-friendly
|
||||
- Dark theme modern
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
// api/auth.php v2.0
|
||||
// ============================================================
|
||||
// Login / Logout / Session Check API
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ── POST /api/auth.php → Login ─────────────────────────
|
||||
case 'POST':
|
||||
$input = getInput();
|
||||
$username = trim($input['username'] ?? '');
|
||||
$password = $input['password'] ?? '';
|
||||
|
||||
if (!$username || !$password) {
|
||||
jsonResponse(['success' => false, 'error' => 'Username dan password wajib diisi'], 400);
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND aktif = 1 LIMIT 1");
|
||||
$stmt->execute([$username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !password_verify($password, $user['password'])) {
|
||||
jsonResponse(['success' => false, 'error' => 'Username atau password salah'], 401);
|
||||
}
|
||||
|
||||
// Simpan ke session
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user'] = [
|
||||
'id' => $user['id'],
|
||||
'nama' => $user['nama'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
];
|
||||
|
||||
// Update last_login
|
||||
$db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?")
|
||||
->execute([$user['id']]);
|
||||
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'message' => 'Login berhasil',
|
||||
'user' => $_SESSION['user'],
|
||||
]);
|
||||
break;
|
||||
|
||||
// ── DELETE /api/auth.php → Logout ─────────────────────
|
||||
case 'DELETE':
|
||||
session_destroy();
|
||||
jsonResponse(['success' => true, 'message' => 'Logout berhasil']);
|
||||
break;
|
||||
|
||||
// ── GET /api/auth.php → Cek session ───────────────────
|
||||
case 'GET':
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
jsonResponse(['success' => false, 'loggedIn' => false], 200);
|
||||
}
|
||||
jsonResponse(['success' => true, 'loggedIn' => true, 'user' => $_SESSION['user']]);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
// api/bantuan.php v2.0
|
||||
// ============================================================
|
||||
// API Bantuan Sosial – CRUD jenis bantuan & relasi penduduk
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
$action = $_GET['action'] ?? 'pivot'; // 'jenis' | 'pivot'
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// GET
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'GET':
|
||||
// GET jenis_bantuan (master)
|
||||
if ($action === 'jenis') {
|
||||
$rows = $db->query("SELECT * FROM jenis_bantuan ORDER BY kode")->fetchAll();
|
||||
jsonResponse(['success' => true, 'data' => $rows]);
|
||||
}
|
||||
|
||||
// GET bantuan per penduduk
|
||||
$pendudukId = $_GET['penduduk_id'] ?? null;
|
||||
if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400);
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT bp.*, jb.kode, jb.nama AS nama_bantuan, jb.warna_badge, jb.deskripsi
|
||||
FROM bantuan_penduduk bp
|
||||
JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
|
||||
WHERE bp.penduduk_id = ?
|
||||
ORDER BY jb.kode
|
||||
");
|
||||
$stmt->execute([$pendudukId]);
|
||||
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// POST – Tambah/set bantuan untuk penduduk
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'POST':
|
||||
$in = getInput();
|
||||
|
||||
// Batch upsert – terima array bantuans[]
|
||||
if (isset($in['bantuans']) && is_array($in['bantuans'])) {
|
||||
$pendudukId = (int)($in['penduduk_id'] ?? 0);
|
||||
if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400);
|
||||
|
||||
// Nonaktifkan semua bantuan lama dulu
|
||||
$db->prepare("UPDATE bantuan_penduduk SET status = 'tidak_aktif' WHERE penduduk_id = ?")
|
||||
->execute([$pendudukId]);
|
||||
|
||||
foreach ($in['bantuans'] as $b) {
|
||||
$bantuanId = (int)($b['bantuan_id'] ?? 0);
|
||||
if (!$bantuanId) continue;
|
||||
|
||||
// Cek sudah ada?
|
||||
$cek = $db->prepare("SELECT id FROM bantuan_penduduk WHERE penduduk_id = ? AND bantuan_id = ?");
|
||||
$cek->execute([$pendudukId, $bantuanId]);
|
||||
$existing = $cek->fetch();
|
||||
|
||||
if ($existing) {
|
||||
$db->prepare("
|
||||
UPDATE bantuan_penduduk
|
||||
SET status = 'aktif', tanggal_mulai = ?, nominal_bulanan = ?,
|
||||
nomor_kpj = ?, keterangan = ?, diinput_oleh = ?
|
||||
WHERE penduduk_id = ? AND bantuan_id = ?
|
||||
")->execute([
|
||||
$b['tanggal_mulai'] ?? null,
|
||||
$b['nominal_bulanan'] ?? null,
|
||||
$b['nomor_kpj'] ?? null,
|
||||
$b['keterangan'] ?? null,
|
||||
$b['diinput_oleh'] ?? 1,
|
||||
$pendudukId, $bantuanId,
|
||||
]);
|
||||
} else {
|
||||
$db->prepare("
|
||||
INSERT INTO bantuan_penduduk
|
||||
(penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, nomor_kpj, keterangan, diinput_oleh)
|
||||
VALUES (?,?,'aktif',?,?,?,?,?)
|
||||
")->execute([
|
||||
$pendudukId, $bantuanId,
|
||||
$b['tanggal_mulai'] ?? null,
|
||||
$b['nominal_bulanan'] ?? null,
|
||||
$b['nomor_kpj'] ?? null,
|
||||
$b['keterangan'] ?? null,
|
||||
$b['diinput_oleh'] ?? 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
jsonResponse(['success' => true, 'message' => 'Data bantuan berhasil disimpan']);
|
||||
}
|
||||
|
||||
// Single insert
|
||||
$required = ['penduduk_id','bantuan_id'];
|
||||
foreach ($required as $f) {
|
||||
if (empty($in[$f])) jsonResponse(['success' => false, 'error' => "Field '$f' wajib diisi"], 400);
|
||||
}
|
||||
|
||||
// Upsert
|
||||
$cek = $db->prepare("SELECT id FROM bantuan_penduduk WHERE penduduk_id = ? AND bantuan_id = ?");
|
||||
$cek->execute([(int)$in['penduduk_id'], (int)$in['bantuan_id']]);
|
||||
if ($cek->fetch()) {
|
||||
jsonResponse(['success' => false, 'error' => 'Bantuan ini sudah tercatat untuk penduduk tersebut. Gunakan PUT untuk update.'], 409);
|
||||
}
|
||||
|
||||
$db->prepare("
|
||||
INSERT INTO bantuan_penduduk (penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, nomor_kpj, keterangan, diinput_oleh)
|
||||
VALUES (?,?,?,?,?,?,?,?)
|
||||
")->execute([
|
||||
(int)$in['penduduk_id'],
|
||||
(int)$in['bantuan_id'],
|
||||
$in['status'] ?? 'aktif',
|
||||
$in['tanggal_mulai'] ?? null,
|
||||
$in['nominal_bulanan'] ?? null,
|
||||
$in['nomor_kpj'] ?? null,
|
||||
$in['keterangan'] ?? null,
|
||||
$in['diinput_oleh'] ?? 1,
|
||||
]);
|
||||
jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Bantuan berhasil ditambahkan']);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// PUT – Update status bantuan
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'PUT':
|
||||
$in = getInput();
|
||||
$id = (int)($in['id'] ?? $_GET['id'] ?? 0);
|
||||
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||
|
||||
$db->prepare("
|
||||
UPDATE bantuan_penduduk
|
||||
SET status = ?, tanggal_mulai = ?, tanggal_selesai = ?,
|
||||
nominal_bulanan = ?, nomor_kpj = ?, keterangan = ?
|
||||
WHERE id = ?
|
||||
")->execute([
|
||||
$in['status'] ?? 'aktif',
|
||||
$in['tanggal_mulai'] ?? null,
|
||||
$in['tanggal_selesai'] ?? null,
|
||||
$in['nominal_bulanan'] ?? null,
|
||||
$in['nomor_kpj'] ?? null,
|
||||
$in['keterangan'] ?? null,
|
||||
$id,
|
||||
]);
|
||||
jsonResponse(['success' => true, 'message' => 'Status bantuan diperbarui']);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// DELETE – Hapus relasi bantuan
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'DELETE':
|
||||
$id = (int)($_GET['id'] ?? getInput()['id'] ?? 0);
|
||||
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||
$db->prepare("DELETE FROM bantuan_penduduk WHERE id = ?")->execute([$id]);
|
||||
jsonResponse(['success' => true, 'message' => 'Bantuan dihapus']);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
// api/dashboard_stats.php v2.0
|
||||
// ============================================================
|
||||
// API Statistik untuk Dashboard & Grafik
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') jsonResponse(['success'=>false,'error'=>'Method tidak diizinkan'],405);
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// ── Filter opsional ────────────────────────────────────────
|
||||
$kelurahan = $_GET['kelurahan'] ?? null;
|
||||
$kecamatan = $_GET['kecamatan'] ?? null;
|
||||
$whereBase = "WHERE 1=1";
|
||||
$params = [];
|
||||
if ($kelurahan) { $whereBase .= " AND w.kelurahan = ?"; $params[] = $kelurahan; }
|
||||
if ($kecamatan) { $whereBase .= " AND w.kecamatan = ?"; $params[] = $kecamatan; }
|
||||
|
||||
$joinW = "LEFT JOIN wilayah w ON w.id = pm.wilayah_id";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 1. RINGKASAN UTAMA (KPI Cards)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
$kpi = $db->prepare("
|
||||
SELECT
|
||||
COUNT(pm.id) AS total_kk,
|
||||
SUM(pm.jumlah_anggota) AS total_jiwa,
|
||||
SUM(pm.jumlah_anak_sekolah) AS total_anak_sekolah,
|
||||
SUM(CASE WHEN pm.status_verifikasi='terverifikasi' THEN 1 ELSE 0 END) AS terverifikasi,
|
||||
SUM(CASE WHEN pm.status_verifikasi='belum_diverifikasi' THEN 1 ELSE 0 END) AS belum_verifikasi,
|
||||
SUM(CASE WHEN pm.punya_bpjs=1 THEN 1 ELSE 0 END) AS punya_bpjs,
|
||||
SUM(CASE WHEN pm.kategori_kemiskinan='sangat_miskin' THEN 1 ELSE 0 END) AS sangat_miskin,
|
||||
SUM(CASE WHEN pm.kategori_kemiskinan='miskin' THEN 1 ELSE 0 END) AS miskin,
|
||||
SUM(CASE WHEN pm.kategori_kemiskinan='hampir_miskin' THEN 1 ELSE 0 END) AS hampir_miskin,
|
||||
SUM(CASE WHEN pm.kategori_kemiskinan='mampu' THEN 1 ELSE 0 END) AS mampu,
|
||||
ROUND(AVG(pm.penghasilan_bulanan),0) AS avg_penghasilan,
|
||||
SUM(CASE WHEN bp_count.cnt > 0 THEN 1 ELSE 0 END) AS penerima_bantuan,
|
||||
SUM(CASE WHEN bp_count.cnt = 0 THEN 1 ELSE 0 END) AS belum_bantuan
|
||||
FROM penduduk_miskin pm
|
||||
$joinW
|
||||
LEFT JOIN (
|
||||
SELECT penduduk_id, COUNT(*) AS cnt
|
||||
FROM bantuan_penduduk
|
||||
WHERE status='aktif'
|
||||
GROUP BY penduduk_id
|
||||
) bp_count ON bp_count.penduduk_id = pm.id
|
||||
$whereBase
|
||||
");
|
||||
$kpi->execute($params);
|
||||
$kpiData = $kpi->fetch();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 2. DISTRIBUSI KATEGORI KEMISKINAN (Donut Chart)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
$katStmt = $db->prepare("
|
||||
SELECT pm.kategori_kemiskinan AS label, COUNT(*) AS value
|
||||
FROM penduduk_miskin pm $joinW $whereBase
|
||||
GROUP BY pm.kategori_kemiskinan
|
||||
ORDER BY FIELD(pm.kategori_kemiskinan,'sangat_miskin','miskin','hampir_miskin','mampu')
|
||||
");
|
||||
$katStmt->execute($params);
|
||||
$kategoriChart = $katStmt->fetchAll();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 3. KK PER KELURAHAN (Bar Chart)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
$kelStmt = $db->prepare("
|
||||
SELECT
|
||||
COALESCE(w.kelurahan,'Tidak Diketahui') AS label,
|
||||
COUNT(pm.id) AS total_kk,
|
||||
SUM(pm.jumlah_anggota) AS total_jiwa,
|
||||
SUM(CASE WHEN bp_c.cnt>0 THEN 1 ELSE 0 END) AS penerima
|
||||
FROM penduduk_miskin pm $joinW
|
||||
LEFT JOIN (
|
||||
SELECT penduduk_id, COUNT(*) AS cnt FROM bantuan_penduduk WHERE status='aktif' GROUP BY penduduk_id
|
||||
) bp_c ON bp_c.penduduk_id = pm.id
|
||||
$whereBase
|
||||
GROUP BY w.kelurahan
|
||||
ORDER BY total_kk DESC
|
||||
LIMIT 15
|
||||
");
|
||||
$kelStmt->execute($params);
|
||||
$kelurahanChart = $kelStmt->fetchAll();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 4. DISTRIBUSI JENIS BANTUAN (Pie Chart)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
$bantuanStmt = $db->prepare("
|
||||
SELECT jb.kode, jb.nama AS label, jb.warna_badge AS color, COUNT(bp.id) AS value
|
||||
FROM bantuan_penduduk bp
|
||||
JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
|
||||
JOIN penduduk_miskin pm ON pm.id = bp.penduduk_id
|
||||
$joinW
|
||||
" . str_replace('WHERE 1=1', 'WHERE bp.status=\'aktif\'', $whereBase) . "
|
||||
GROUP BY jb.id
|
||||
ORDER BY value DESC
|
||||
");
|
||||
$bantuanStmt->execute($params);
|
||||
$bantuanChart = $bantuanStmt->fetchAll();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 5. TREN PENDATAAN PER BULAN (Line Chart) – 12 bulan terakhir
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
$trenStmt = $db->prepare("
|
||||
SELECT
|
||||
DATE_FORMAT(pm.created_at,'%Y-%m') AS bulan,
|
||||
COUNT(pm.id) AS total
|
||||
FROM penduduk_miskin pm $joinW $whereBase
|
||||
AND pm.created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||
GROUP BY DATE_FORMAT(pm.created_at,'%Y-%m')
|
||||
ORDER BY bulan ASC
|
||||
");
|
||||
$trenStmt->execute($params);
|
||||
$trenChart = $trenStmt->fetchAll();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 6. DISTRIBUSI PEKERJAAN (Bar horizontal)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
$pekerjaanStmt = $db->prepare("
|
||||
SELECT pm.status_pekerjaan AS label, COUNT(*) AS value
|
||||
FROM penduduk_miskin pm $joinW $whereBase
|
||||
GROUP BY pm.status_pekerjaan
|
||||
ORDER BY value DESC
|
||||
");
|
||||
$pekerjaanStmt->execute($params);
|
||||
$pekerjaanChart = $pekerjaanStmt->fetchAll();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 7. DATA HEATMAP (lat/lng + bobot untuk Leaflet.heat)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
$heatStmt = $db->prepare("
|
||||
SELECT pm.lat, pm.lng,
|
||||
CASE pm.kategori_kemiskinan
|
||||
WHEN 'sangat_miskin' THEN 1.0
|
||||
WHEN 'miskin' THEN 0.6
|
||||
WHEN 'hampir_miskin' THEN 0.3
|
||||
WHEN 'mampu' THEN 0.1
|
||||
ELSE 0.1
|
||||
END AS intensity
|
||||
FROM penduduk_miskin pm $joinW $whereBase
|
||||
");
|
||||
$heatStmt->execute($params);
|
||||
$heatmap = $heatStmt->fetchAll();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 8. TOP 10 KK BELUM MENERIMA BANTUAN APAPUN
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
$belumBantuanStmt = $db->prepare("
|
||||
SELECT pm.id, pm.nama_kk, pm.nik_kk, pm.alamat,
|
||||
pm.kategori_kemiskinan, pm.jumlah_anggota,
|
||||
pm.status_verifikasi,
|
||||
COALESCE(w.kelurahan,'–') AS kelurahan,
|
||||
DATEDIFF(NOW(), pm.created_at) AS hari_terdata
|
||||
FROM penduduk_miskin pm $joinW
|
||||
$whereBase
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM bantuan_penduduk bp
|
||||
WHERE bp.penduduk_id = pm.id AND bp.status='aktif'
|
||||
)
|
||||
ORDER BY pm.kategori_kemiskinan ASC, hari_terdata DESC
|
||||
LIMIT 10
|
||||
");
|
||||
$belumBantuanStmt->execute($params);
|
||||
$belumBantuan = $belumBantuanStmt->fetchAll();
|
||||
|
||||
// ── Statistik Rumah Ibadah (tetap dari v1.1) ──────────────
|
||||
$ibadahStats = $db->query("
|
||||
SELECT COUNT(*) AS total_ibadah FROM rumah_ibadah
|
||||
")->fetch();
|
||||
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'kpi' => $kpiData,
|
||||
'chart_kategori' => $kategoriChart,
|
||||
'chart_kelurahan' => $kelurahanChart,
|
||||
'chart_bantuan' => $bantuanChart,
|
||||
'chart_tren' => $trenChart,
|
||||
'chart_pekerjaan' => $pekerjaanChart,
|
||||
'heatmap' => $heatmap,
|
||||
'belum_bantuan' => $belumBantuan,
|
||||
'total_ibadah' => (int)$ibadahStats['total_ibadah'],
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
// api/export_excel.php v2.0
|
||||
// ============================================================
|
||||
// Export Data Penduduk Miskin ke Excel (format CSV UTF-8 BOM)
|
||||
// Tidak memerlukan library eksternal
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// ── Filter dari query string ───────────────────────────────
|
||||
$kelurahan = $_GET['kelurahan'] ?? null;
|
||||
$kecamatan = $_GET['kecamatan'] ?? null;
|
||||
$kategori = $_GET['kategori'] ?? null;
|
||||
$verifikasi = $_GET['verifikasi'] ?? null;
|
||||
$bantuan = $_GET['bantuan'] ?? null;
|
||||
|
||||
$where = "WHERE 1=1";
|
||||
$params = [];
|
||||
if ($kelurahan) { $where .= " AND w.kelurahan = ?"; $params[] = $kelurahan; }
|
||||
if ($kecamatan) { $where .= " AND w.kecamatan = ?"; $params[] = $kecamatan; }
|
||||
if ($kategori) { $where .= " AND pm.kategori_kemiskinan = ?"; $params[] = $kategori; }
|
||||
if ($verifikasi) { $where .= " AND pm.status_verifikasi = ?"; $params[] = $verifikasi; }
|
||||
if ($bantuan === 'tidak') {
|
||||
$where .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') = 0";
|
||||
} elseif ($bantuan === 'ada') {
|
||||
$where .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') > 0";
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT
|
||||
pm.nik_kk, pm.nama_kk, pm.no_hp,
|
||||
pm.alamat, pm.rt, pm.rw,
|
||||
COALESCE(w.kelurahan,'–') AS kelurahan,
|
||||
COALESCE(w.kecamatan,'–') AS kecamatan,
|
||||
pm.jumlah_anggota, pm.jumlah_anak_sekolah,
|
||||
pm.penghasilan_bulanan, pm.sumber_penghasilan, pm.status_pekerjaan,
|
||||
pm.kategori_kemiskinan, pm.kondisi_rumah, pm.kepemilikan_rumah,
|
||||
CASE pm.punya_bpjs WHEN 1 THEN 'Ya' ELSE 'Tidak' END AS punya_bpjs,
|
||||
pm.jenis_bpjs,
|
||||
pm.lat, pm.lng,
|
||||
pm.status_verifikasi,
|
||||
COALESCE(u.nama,'–') AS nama_surveyor,
|
||||
DATE_FORMAT(pm.created_at,'%d/%m/%Y %H:%i') AS tgl_input,
|
||||
(SELECT GROUP_CONCAT(jb.kode ORDER BY jb.kode SEPARATOR ', ')
|
||||
FROM bantuan_penduduk bp JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
|
||||
WHERE bp.penduduk_id = pm.id AND bp.status='aktif') AS bantuan_diterima
|
||||
FROM penduduk_miskin pm
|
||||
LEFT JOIN wilayah w ON w.id = pm.wilayah_id
|
||||
LEFT JOIN users u ON u.id = pm.surveyor_id
|
||||
$where
|
||||
ORDER BY w.kelurahan, pm.nama_kk
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// ── Nama file ─────────────────────────────────────────────
|
||||
$filter = $kelurahan ? "_" . preg_replace('/\s+/', '-', $kelurahan) : '';
|
||||
$filename = "data_kemiskinan{$filter}_" . date('Ymd_His') . ".csv";
|
||||
|
||||
// ── Output header HTTP ─────────────────────────────────────
|
||||
header('Content-Type: text/csv; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
// BOM UTF-8 agar Excel baca karakter Indonesia dengan benar
|
||||
echo "\xEF\xBB\xBF";
|
||||
|
||||
$out = fopen('php://output', 'w');
|
||||
|
||||
// ── Header kolom ──────────────────────────────────────────
|
||||
fputcsv($out, [
|
||||
'No','NIK KK','Nama KK','No HP',
|
||||
'Alamat','RT','RW','Kelurahan','Kecamatan',
|
||||
'Jml Anggota','Anak Sekolah',
|
||||
'Penghasilan/Bln','Sumber Penghasilan','Status Pekerjaan',
|
||||
'Kategori Kemiskinan','Kondisi Rumah','Kepemilikan Rumah',
|
||||
'Punya BPJS','Jenis BPJS',
|
||||
'Latitude','Longitude',
|
||||
'Status Verifikasi','Bantuan Diterima',
|
||||
'Petugas Input','Tgl Input',
|
||||
], ';');
|
||||
|
||||
// ── Data rows ─────────────────────────────────────────────
|
||||
$no = 1;
|
||||
foreach ($rows as $r) {
|
||||
fputcsv($out, [
|
||||
$no++,
|
||||
$r['nik_kk'],
|
||||
$r['nama_kk'],
|
||||
$r['no_hp'] ?? '–',
|
||||
$r['alamat'] ?? '–',
|
||||
$r['rt'] ?? '–',
|
||||
$r['rw'] ?? '–',
|
||||
$r['kelurahan'],
|
||||
$r['kecamatan'],
|
||||
$r['jumlah_anggota'],
|
||||
$r['jumlah_anak_sekolah'],
|
||||
$r['penghasilan_bulanan'] ? 'Rp ' . number_format($r['penghasilan_bulanan'],0,',','.') : '–',
|
||||
$r['sumber_penghasilan'] ?? '–',
|
||||
ucwords(str_replace('_',' ', $r['status_pekerjaan'])),
|
||||
$r['kategori_kemiskinan'] === 'hampir_miskin' ? 'Nyaris Miskin' : ucwords(str_replace('_',' ', $r['kategori_kemiskinan'])),
|
||||
ucwords(str_replace('_',' ', $r['kondisi_rumah'])),
|
||||
ucwords(str_replace('_',' ', $r['kepemilikan_rumah'])),
|
||||
$r['punya_bpjs'],
|
||||
strtoupper(str_replace('_',' ', $r['jenis_bpjs'])),
|
||||
$r['lat'],
|
||||
$r['lng'],
|
||||
ucwords(str_replace('_',' ', $r['status_verifikasi'])),
|
||||
$r['bantuan_diterima'] ?? 'Belum Ada',
|
||||
$r['nama_surveyor'],
|
||||
$r['tgl_input'],
|
||||
], ';');
|
||||
}
|
||||
|
||||
fclose($out);
|
||||
exit;
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
// api/export_pdf.php v2.0
|
||||
// ============================================================
|
||||
// Export Laporan PDF menggunakan HTML → browser print
|
||||
// (Pure PHP, tanpa library eksternal)
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// ── Filter ────────────────────────────────────────────────
|
||||
$kelurahan = $_GET['kelurahan'] ?? null;
|
||||
$kecamatan = $_GET['kecamatan'] ?? null;
|
||||
$kategori = $_GET['kategori'] ?? null;
|
||||
$verifikasi = $_GET['verifikasi'] ?? null;
|
||||
|
||||
$where = "WHERE 1=1";
|
||||
$params = [];
|
||||
if ($kelurahan) { $where .= " AND w.kelurahan = ?"; $params[] = $kelurahan; }
|
||||
if ($kecamatan) { $where .= " AND w.kecamatan = ?"; $params[] = $kecamatan; }
|
||||
if ($kategori) { $where .= " AND pm.kategori_kemiskinan = ?"; $params[] = $kategori; }
|
||||
if ($verifikasi) { $where .= " AND pm.status_verifikasi = ?"; $params[] = $verifikasi; }
|
||||
|
||||
// Data penduduk
|
||||
$stmt = $db->prepare("
|
||||
SELECT pm.nik_kk, pm.nama_kk, pm.no_hp, pm.alamat, pm.rt, pm.rw,
|
||||
COALESCE(w.kelurahan,'–') AS kelurahan,
|
||||
pm.jumlah_anggota, pm.jumlah_anak_sekolah,
|
||||
pm.penghasilan_bulanan, pm.status_pekerjaan,
|
||||
pm.kategori_kemiskinan, pm.kondisi_rumah,
|
||||
CASE pm.punya_bpjs WHEN 1 THEN 'Ya' ELSE 'Tidak' END AS punya_bpjs,
|
||||
pm.status_verifikasi,
|
||||
(SELECT GROUP_CONCAT(jb.kode ORDER BY jb.kode SEPARATOR ', ')
|
||||
FROM bantuan_penduduk bp JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
|
||||
WHERE bp.penduduk_id = pm.id AND bp.status='aktif') AS bantuan
|
||||
FROM penduduk_miskin pm
|
||||
LEFT JOIN wilayah w ON w.id = pm.wilayah_id
|
||||
$where
|
||||
ORDER BY w.kelurahan, pm.kategori_kemiskinan, pm.nama_kk
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Statistik ringkas
|
||||
$statsStmt = $db->prepare("
|
||||
SELECT COUNT(*) AS total, SUM(pm.jumlah_anggota) AS jiwa,
|
||||
SUM(CASE WHEN pm.kategori_kemiskinan='sangat_miskin' THEN 1 ELSE 0 END) AS sangat_miskin,
|
||||
SUM(CASE WHEN pm.kategori_kemiskinan='miskin' THEN 1 ELSE 0 END) AS miskin,
|
||||
SUM(CASE WHEN pm.kategori_kemiskinan='hampir_miskin' THEN 1 ELSE 0 END) AS hampir_miskin,
|
||||
SUM(CASE WHEN pm.kategori_kemiskinan='mampu' THEN 1 ELSE 0 END) AS mampu,
|
||||
SUM(CASE WHEN EXISTS(SELECT 1 FROM bantuan_penduduk bp WHERE bp.penduduk_id=pm.id AND bp.status='aktif') THEN 1 ELSE 0 END) AS penerima
|
||||
FROM penduduk_miskin pm LEFT JOIN wilayah w ON w.id = pm.wilayah_id $where
|
||||
");
|
||||
$statsStmt->execute($params);
|
||||
$stats = $statsStmt->fetch();
|
||||
|
||||
$judul = "LAPORAN DATA PENDUDUK MISKIN";
|
||||
$filter = $kelurahan ? " – Kelurahan $kelurahan" : ($kecamatan ? " – Kec. $kecamatan" : " – Seluruh Wilayah");
|
||||
$tgl = date('d F Y');
|
||||
|
||||
// Warna kategori
|
||||
function warnaBg(string $k): string {
|
||||
return match($k) {
|
||||
'sangat_miskin' => '#fee2e2',
|
||||
'miskin' => '#fef3c7',
|
||||
'hampir_miskin' => '#dcfce7',
|
||||
'mampu' => '#dbeafe',
|
||||
default => '#f3f4f6',
|
||||
};
|
||||
}
|
||||
function labelKat(string $k): string {
|
||||
return match($k) {
|
||||
'sangat_miskin' => 'Sangat Miskin',
|
||||
'miskin' => 'Miskin',
|
||||
'hampir_miskin' => 'Nyaris Miskin',
|
||||
'mampu' => 'Mampu',
|
||||
default => $k,
|
||||
};
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?= htmlspecialchars($judul . $filter) ?></title>
|
||||
<style>
|
||||
@page { size: A4 landscape; margin: 15mm 10mm; }
|
||||
@media print {
|
||||
.no-print { display: none; }
|
||||
body { font-size: 8pt; }
|
||||
table { font-size: 7.5pt; }
|
||||
thead { display: table-header-group; }
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: Arial, sans-serif; font-size: 9pt; color: #111; background: #fff; }
|
||||
|
||||
/* Header laporan */
|
||||
.kop { text-align: center; border-bottom: 3px double #1e3a5f; padding-bottom: 10px; margin-bottom: 14px; }
|
||||
.kop h1 { font-size: 13pt; font-weight: 900; text-transform: uppercase; color: #1e3a5f; }
|
||||
.kop h2 { font-size: 11pt; font-weight: 700; margin: 2px 0; }
|
||||
.kop p { font-size: 8pt; color: #555; }
|
||||
|
||||
/* Statistik */
|
||||
.stat-grid { display: flex; gap: 8px; margin-bottom: 14px; }
|
||||
.stat-box {
|
||||
flex: 1; border: 1px solid #ddd; border-radius: 6px;
|
||||
padding: 7px 10px; text-align: center; background: #f8faff;
|
||||
}
|
||||
.stat-box .val { font-size: 16pt; font-weight: 900; color: #1e3a5f; }
|
||||
.stat-box .lbl { font-size: 7pt; color: #666; text-transform: uppercase; }
|
||||
|
||||
/* Tabel */
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 4px; }
|
||||
th { background: #1e3a5f; color: #fff; padding: 5px 4px; text-align: center; font-size: 7.5pt; border: 1px solid #1e3a5f; }
|
||||
td { padding: 4px; border: 1px solid #ddd; vertical-align: top; font-size: 7.5pt; }
|
||||
tr:nth-child(even) td { background: #f9fafb; }
|
||||
.badge-sm {
|
||||
display: inline-block; padding: 1px 5px; border-radius: 3px;
|
||||
font-size: 6.5pt; font-weight: bold;
|
||||
}
|
||||
.c { text-align: center; }
|
||||
.r { text-align: right; }
|
||||
|
||||
/* Footer */
|
||||
.footer { margin-top: 20px; display: flex; justify-content: space-between; font-size: 8pt; }
|
||||
.ttd-box { text-align: center; width: 200px; }
|
||||
.ttd-box .line { border-top: 1px solid #111; margin-top: 50px; padding-top: 3px; font-weight: bold; }
|
||||
|
||||
/* Print button */
|
||||
.no-print { text-align: center; margin: 12px 0; }
|
||||
.btn-print {
|
||||
background: #1e3a5f; color: #fff; border: none; padding: 8px 24px;
|
||||
border-radius: 5px; font-size: 10pt; cursor: pointer; margin-right: 8px;
|
||||
}
|
||||
.btn-print:hover { background: #274e81; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Print Button -->
|
||||
<div class="no-print">
|
||||
<button class="btn-print" onclick="window.print()">🖨 Cetak / Simpan PDF</button>
|
||||
<button class="btn-print" style="background:#555;" onclick="window.close()">✕ Tutup</button>
|
||||
</div>
|
||||
|
||||
<!-- Kop Surat -->
|
||||
<div class="kop">
|
||||
<h1><?= htmlspecialchars($judul) ?></h1>
|
||||
<h2>SISTEM INFORMASI GEOGRAFIS KEMISKINAN<?= htmlspecialchars($filter) ?></h2>
|
||||
<p>Dicetak pada: <?= $tgl ?> | Total: <?= count($rows) ?> KK</p>
|
||||
</div>
|
||||
|
||||
<!-- Statistik Ringkas -->
|
||||
<div class="stat-grid">
|
||||
<div class="stat-box"><div class="val"><?= number_format($stats['total']) ?></div><div class="lbl">Total KK</div></div>
|
||||
<div class="stat-box"><div class="val"><?= number_format($stats['jiwa']) ?></div><div class="lbl">Total Jiwa</div></div>
|
||||
<div class="stat-box" style="background:#fee2e2"><div class="val"><?= number_format($stats['sangat_miskin']) ?></div><div class="lbl">Sangat Miskin</div></div>
|
||||
<div class="stat-box" style="background:#fef3c7"><div class="val"><?= number_format($stats['miskin']) ?></div><div class="lbl">Miskin</div></div>
|
||||
<div class="stat-box" style="background:#dcfce7"><div class="val"><?= number_format($stats['hampir_miskin']) ?></div><div class="lbl">Nyaris Miskin</div></div>
|
||||
<div class="stat-box" style="background:#dbeafe"><div class="val"><?= number_format($stats['mampu']) ?></div><div class="lbl">Mampu</div></div>
|
||||
<div class="stat-box"><div class="val"><?= number_format($stats['penerima']) ?></div><div class="lbl">Penerima Bantuan</div></div>
|
||||
<div class="stat-box" style="background:#fff7ed"><div class="val"><?= number_format($stats['total'] - $stats['penerima']) ?></div><div class="lbl">Belum Bantuan</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel Data -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:2%">No</th>
|
||||
<th style="width:11%">NIK / Nama KK</th>
|
||||
<th style="width:6%">Kelurahan</th>
|
||||
<th style="width:4%">RT/RW</th>
|
||||
<th style="width:13%">Alamat</th>
|
||||
<th style="width:3%">Angg.</th>
|
||||
<th style="width:3%">Anak</th>
|
||||
<th style="width:7%">Penghasilan</th>
|
||||
<th style="width:7%">Pekerjaan</th>
|
||||
<th style="width:8%">Kategori</th>
|
||||
<th style="width:7%">Kondisi Rumah</th>
|
||||
<th style="width:4%">BPJS</th>
|
||||
<th style="width:11%">Bantuan Diterima</th>
|
||||
<th style="width:10%">Status Verifikasi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $no = 1; foreach ($rows as $r): ?>
|
||||
<tr>
|
||||
<td class="c"><?= $no++ ?></td>
|
||||
<td>
|
||||
<small style="color:#666"><?= htmlspecialchars($r['nik_kk']) ?></small><br>
|
||||
<strong><?= htmlspecialchars($r['nama_kk']) ?></strong><br>
|
||||
<small><?= htmlspecialchars($r['no_hp'] ?? '–') ?></small>
|
||||
</td>
|
||||
<td class="c"><?= htmlspecialchars($r['kelurahan']) ?></td>
|
||||
<td class="c"><?= ($r['rt'] ? $r['rt'].'/'.$r['rw'] : '–') ?></td>
|
||||
<td><?= htmlspecialchars($r['alamat'] ?? '–') ?></td>
|
||||
<td class="c"><?= $r['jumlah_anggota'] ?></td>
|
||||
<td class="c"><?= $r['jumlah_anak_sekolah'] ?></td>
|
||||
<td class="r"><?= $r['penghasilan_bulanan'] ? 'Rp '.number_format($r['penghasilan_bulanan'],0,',','.') : '–' ?></td>
|
||||
<td><?= htmlspecialchars(ucwords(str_replace('_',' ',$r['status_pekerjaan']))) ?></td>
|
||||
<td class="c">
|
||||
<span class="badge-sm" style="background:<?= warnaBg($r['kategori_kemiskinan']) ?>">
|
||||
<?= labelKat($r['kategori_kemiskinan']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="c"><?= htmlspecialchars(ucwords(str_replace('_',' ',$r['kondisi_rumah']))) ?></td>
|
||||
<td class="c"><?= $r['punya_bpjs'] ?></td>
|
||||
<td>
|
||||
<?php if ($r['bantuan']): ?>
|
||||
<?php foreach (explode(', ', $r['bantuan']) as $b): ?>
|
||||
<span class="badge-sm" style="background:#dbeafe;color:#1e40af"><?= htmlspecialchars(trim($b)) ?></span>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<span style="color:#dc2626">Belum Ada</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="c">
|
||||
<?php
|
||||
$sv = $r['status_verifikasi'];
|
||||
$svColor = match($sv) { 'terverifikasi'=>'#dcfce7', 'belum_diverifikasi'=>'#fef3c7', default=>'#fee2e2' };
|
||||
?>
|
||||
<span class="badge-sm" style="background:<?= $svColor ?>">
|
||||
<?= htmlspecialchars(ucwords(str_replace('_',' ',$sv))) ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Footer / TTD -->
|
||||
<div class="footer">
|
||||
<div>
|
||||
<p>Dicetak: <?= date('d/m/Y H:i') ?> WIB</p>
|
||||
<p>Filter: <?= htmlspecialchars(implode(', ', array_filter([$kelurahan, $kecamatan, $kategori ? "Kategori: $kategori" : null])) ?: 'Semua Data') ?></p>
|
||||
<p>Sumber: SIG Kemiskinan v2.0</p>
|
||||
</div>
|
||||
<div class="ttd-box">
|
||||
<p>Pontianak, <?= $tgl ?></p>
|
||||
<p>Mengetahui,</p>
|
||||
<div class="line">Kepala Dinas / Pejabat Berwenang</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
// api/foto.php v2.0
|
||||
// ============================================================
|
||||
// API Upload & Kelola Foto Rumah + GPS Geo-tagging
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
// Pastikan folder upload ada
|
||||
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// GET – Ambil daftar foto per penduduk
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'GET':
|
||||
$pendudukId = $_GET['penduduk_id'] ?? null;
|
||||
if (!$pendudukId) {
|
||||
header('Content-Type: application/json');
|
||||
jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400);
|
||||
}
|
||||
$stmt = $db->prepare("
|
||||
SELECT f.*, u.nama AS nama_petugas
|
||||
FROM foto_rumah f
|
||||
LEFT JOIN users u ON u.id = f.diambil_oleh
|
||||
WHERE f.penduduk_id = ?
|
||||
ORDER BY f.is_primary DESC, f.created_at ASC
|
||||
");
|
||||
$stmt->execute([$pendudukId]);
|
||||
header('Content-Type: application/json');
|
||||
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// POST – Upload foto baru
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'POST':
|
||||
header('Content-Type: application/json');
|
||||
$pendudukId = (int)($_POST['penduduk_id'] ?? 0);
|
||||
if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400);
|
||||
|
||||
// Cek penduduk ada
|
||||
$cek = $db->prepare("SELECT id FROM penduduk_miskin WHERE id = ?");
|
||||
$cek->execute([$pendudukId]);
|
||||
if (!$cek->fetch()) jsonResponse(['success' => false, 'error' => 'Data penduduk tidak ditemukan'], 404);
|
||||
|
||||
if (!isset($_FILES['foto']) || $_FILES['foto']['error'] !== UPLOAD_ERR_OK) {
|
||||
$errCode = $_FILES['foto']['error'] ?? 'no_file';
|
||||
jsonResponse(['success' => false, 'error' => "Upload gagal (kode: $errCode)"], 400);
|
||||
}
|
||||
|
||||
$file = $_FILES['foto'];
|
||||
$tmpPath = $file['tmp_name'];
|
||||
$origName = $file['name'];
|
||||
$fileSize = $file['size'];
|
||||
|
||||
// Validasi ukuran
|
||||
if ($fileSize > MAX_FOTO_SIZE) {
|
||||
jsonResponse(['success' => false, 'error' => 'Ukuran file maksimal 5 MB'], 413);
|
||||
}
|
||||
|
||||
// Validasi ekstensi & MIME type
|
||||
$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $tmpPath);
|
||||
finfo_close($finfo);
|
||||
$allowedMime = ['image/jpeg','image/png','image/webp'];
|
||||
if (!in_array($ext, ALLOWED_EXT) || !in_array($mime, $allowedMime)) {
|
||||
jsonResponse(['success' => false, 'error' => 'Format file tidak didukung (jpg/png/webp)'], 415);
|
||||
}
|
||||
|
||||
// Generate nama file unik
|
||||
$newName = 'foto_' . $pendudukId . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
$destPath = UPLOAD_DIR . $newName;
|
||||
$relPath = UPLOAD_URL . $newName;
|
||||
|
||||
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||
jsonResponse(['success' => false, 'error' => 'Gagal menyimpan file'], 500);
|
||||
}
|
||||
|
||||
// Cek apakah ini foto pertama → jadikan primary
|
||||
$countStmt = $db->prepare("SELECT COUNT(*) FROM foto_rumah WHERE penduduk_id = ?");
|
||||
$countStmt->execute([$pendudukId]);
|
||||
$isPrimary = ($countStmt->fetchColumn() == 0) ? 1 : 0;
|
||||
|
||||
// Koordinat GPS dari POST (dari browser Geolocation atau EXIF)
|
||||
$latFoto = isset($_POST['lat_foto']) && $_POST['lat_foto'] !== '' ? (float)$_POST['lat_foto'] : null;
|
||||
$lngFoto = isset($_POST['lng_foto']) && $_POST['lng_foto'] !== '' ? (float)$_POST['lng_foto'] : null;
|
||||
$accuracy = isset($_POST['accuracy_meter']) && $_POST['accuracy_meter'] !== '' ? (float)$_POST['accuracy_meter']
|
||||
: (isset($_POST['accuracy']) && $_POST['accuracy'] !== '' ? (float)$_POST['accuracy'] : null);
|
||||
|
||||
// Jika user set is_primary = 1, nonaktifkan primary lain dulu
|
||||
$isPrimaryReq = isset($_POST['is_primary']) && (int)$_POST['is_primary'] === 1;
|
||||
if ($isPrimaryReq) {
|
||||
$db->prepare("UPDATE foto_rumah SET is_primary = 0 WHERE penduduk_id = ?")
|
||||
->execute([$pendudukId]);
|
||||
$isPrimary = 1;
|
||||
}
|
||||
|
||||
// diambil_oleh: support field name lama (user_id) dan baru (diambil_oleh)
|
||||
$diambilOleh = $_POST['diambil_oleh'] ?? $_POST['user_id'] ?? null;
|
||||
|
||||
$db->prepare("
|
||||
INSERT INTO foto_rumah (penduduk_id, file_path, lat_foto, lng_foto, accuracy_meter, is_primary, ukuran_kb, keterangan, diambil_oleh)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
")->execute([
|
||||
$pendudukId,
|
||||
$relPath,
|
||||
$latFoto,
|
||||
$lngFoto,
|
||||
$accuracy,
|
||||
$isPrimary,
|
||||
(int)round($fileSize / 1024),
|
||||
$_POST['keterangan'] ?? null,
|
||||
$diambilOleh ? (int)$diambilOleh : null,
|
||||
]);
|
||||
|
||||
$newId = (int)$db->lastInsertId();
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'id' => $newId,
|
||||
'file_path' => $relPath,
|
||||
'is_primary'=> $isPrimary,
|
||||
'message' => 'Foto berhasil diupload',
|
||||
]);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// DELETE – Hapus foto
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'DELETE':
|
||||
header('Content-Type: application/json');
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM foto_rumah WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$foto = $stmt->fetch();
|
||||
if (!$foto) jsonResponse(['success' => false, 'error' => 'Foto tidak ditemukan'], 404);
|
||||
|
||||
// Hapus file fisik
|
||||
$filePath = __DIR__ . '/../' . $foto['file_path'];
|
||||
if (file_exists($filePath)) @unlink($filePath);
|
||||
|
||||
$db->prepare("DELETE FROM foto_rumah WHERE id = ?")->execute([$id]);
|
||||
|
||||
// Jika foto yang dihapus adalah primary, set foto lain jadi primary
|
||||
if ($foto['is_primary']) {
|
||||
$next = $db->prepare("SELECT id FROM foto_rumah WHERE penduduk_id = ? ORDER BY created_at ASC LIMIT 1");
|
||||
$next->execute([$foto['penduduk_id']]);
|
||||
$nextFoto = $next->fetch();
|
||||
if ($nextFoto) {
|
||||
$db->prepare("UPDATE foto_rumah SET is_primary = 1 WHERE id = ?")->execute([$nextFoto['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(['success' => true, 'message' => 'Foto berhasil dihapus']);
|
||||
break;
|
||||
|
||||
default:
|
||||
header('Content-Type: application/json');
|
||||
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// api/geocode.php
|
||||
// Reverse Geocoding via Nominatim (OpenStreetMap)
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
|
||||
$lat = $_GET['lat'] ?? null;
|
||||
$lng = $_GET['lng'] ?? null;
|
||||
|
||||
if (!$lat || !$lng) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'lat dan lng diperlukan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$lat = (float)$lat;
|
||||
$lng = (float)$lng;
|
||||
|
||||
$url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={$lat}&lon={$lng}&zoom=18&addressdetails=1";
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => "User-Agent: SIG-Kemiskinan/1.0 (sig@example.com)\r\n",
|
||||
'timeout' => 8,
|
||||
]
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
// Fallback: koordinat saja
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'alamat' => "Koordinat: {$lat}, {$lng}",
|
||||
'detail' => null
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (!$data || isset($data['error'])) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'alamat' => "Koordinat: {$lat}, {$lng}",
|
||||
'detail' => null
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Susun alamat yang rapi
|
||||
$addr = $data['address'] ?? [];
|
||||
$parts = array_filter([
|
||||
$addr['road'] ?? $addr['pedestrian'] ?? $addr['path'] ?? null,
|
||||
$addr['house_number'] ?? null,
|
||||
$addr['neighbourhood'] ?? $addr['suburb'] ?? null,
|
||||
$addr['city'] ?? $addr['town'] ?? $addr['village'] ?? $addr['county'] ?? null,
|
||||
]);
|
||||
|
||||
$alamat = !empty($parts)
|
||||
? implode(', ', $parts)
|
||||
: ($data['display_name'] ?? "Koordinat: {$lat}, {$lng}");
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'alamat' => $alamat,
|
||||
'display_name' => $data['display_name'] ?? '',
|
||||
'detail' => $addr
|
||||
]);
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
// api/notifikasi.php v2.0
|
||||
// ============================================================
|
||||
// API Sistem Notifikasi + Generate Otomatis
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// GET – Ambil notifikasi (dengan auto-generate)
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'GET':
|
||||
$userId = $_GET['user_id'] ?? null;
|
||||
$generate = isset($_GET['generate']); // ?generate → trigger auto-create
|
||||
|
||||
if ($generate) generateNotifikasi($db);
|
||||
|
||||
$sql = "SELECT n.*, pm.nama_kk FROM notifikasi n LEFT JOIN penduduk_miskin pm ON pm.id = n.penduduk_id WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($userId) {
|
||||
$sql .= " AND (n.user_id = ? OR n.user_id IS NULL)";
|
||||
$params[] = $userId;
|
||||
}
|
||||
|
||||
// Filter belum dibaca
|
||||
if (isset($_GET['unread'])) {
|
||||
$sql .= " AND n.sudah_dibaca = 0";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY n.created_at DESC LIMIT 50";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Hitung unread
|
||||
$unreadStmt = $db->prepare("SELECT COUNT(*) FROM notifikasi WHERE sudah_dibaca = 0 AND (user_id = ? OR user_id IS NULL)");
|
||||
$unreadStmt->execute([$userId ?? 0]);
|
||||
$unreadCount = (int)$unreadStmt->fetchColumn();
|
||||
|
||||
jsonResponse(['success' => true, 'data' => $rows, 'unread_count' => $unreadCount]);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// PUT – Tandai sudah dibaca
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'PUT':
|
||||
$in = getInput();
|
||||
$id = $in['id'] ?? 'all';
|
||||
|
||||
if ($id === 'all') {
|
||||
$userId = (int)($in['user_id'] ?? 0);
|
||||
$db->prepare("UPDATE notifikasi SET sudah_dibaca = 1 WHERE user_id = ? OR user_id IS NULL")
|
||||
->execute([$userId]);
|
||||
jsonResponse(['success' => true, 'message' => 'Semua notifikasi ditandai dibaca']);
|
||||
} else {
|
||||
$db->prepare("UPDATE notifikasi SET sudah_dibaca = 1 WHERE id = ?")
|
||||
->execute([(int)$id]);
|
||||
jsonResponse(['success' => true, 'message' => 'Notifikasi ditandai dibaca']);
|
||||
}
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// POST – Buat notifikasi manual
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'POST':
|
||||
$in = getInput();
|
||||
if (empty($in['judul']) || empty($in['pesan'])) {
|
||||
jsonResponse(['success' => false, 'error' => 'Judul dan pesan wajib diisi'], 400);
|
||||
}
|
||||
$db->prepare("
|
||||
INSERT INTO notifikasi (user_id, jenis, judul, pesan, penduduk_id)
|
||||
VALUES (?,?,?,?,?)
|
||||
")->execute([
|
||||
$in['user_id'] ?? null,
|
||||
$in['jenis'] ?? 'info',
|
||||
$in['judul'],
|
||||
$in['pesan'],
|
||||
$in['penduduk_id'] ?? null,
|
||||
]);
|
||||
jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Notifikasi dibuat']);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI: Generate notifikasi otomatis
|
||||
// ============================================================
|
||||
function generateNotifikasi(PDO $db): void {
|
||||
|
||||
// 1. KK belum terverifikasi
|
||||
$belumVerif = $db->query("
|
||||
SELECT id, nama_kk FROM penduduk_miskin
|
||||
WHERE status_verifikasi = 'belum_diverifikasi'
|
||||
AND id NOT IN (
|
||||
SELECT DISTINCT penduduk_id FROM notifikasi
|
||||
WHERE jenis = 'belum_verifikasi' AND sudah_dibaca = 0
|
||||
)
|
||||
LIMIT 20
|
||||
")->fetchAll();
|
||||
|
||||
foreach ($belumVerif as $p) {
|
||||
$db->prepare("
|
||||
INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id)
|
||||
VALUES ('belum_verifikasi','Data Menunggu Verifikasi',?,?)
|
||||
")->execute(["KK {$p['nama_kk']} belum diverifikasi.", $p['id']]);
|
||||
}
|
||||
|
||||
// 2. KK belum menerima bantuan apapun (sangat miskin)
|
||||
$belumBantuan = $db->query("
|
||||
SELECT id, nama_kk FROM penduduk_miskin
|
||||
WHERE kategori_kemiskinan = 'sangat_miskin'
|
||||
AND id NOT IN (
|
||||
SELECT penduduk_id FROM bantuan_penduduk WHERE status = 'aktif'
|
||||
)
|
||||
AND id NOT IN (
|
||||
SELECT DISTINCT penduduk_id FROM notifikasi
|
||||
WHERE jenis = 'belum_bantuan' AND sudah_dibaca = 0
|
||||
)
|
||||
LIMIT 20
|
||||
")->fetchAll();
|
||||
|
||||
foreach ($belumBantuan as $p) {
|
||||
$db->prepare("
|
||||
INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id)
|
||||
VALUES ('belum_bantuan','KK Sangat Miskin Tanpa Bantuan',?,?)
|
||||
")->execute(["KK {$p['nama_kk']} berkategori sangat miskin namun belum menerima bantuan apapun.", $p['id']]);
|
||||
}
|
||||
|
||||
// 3. Data usang (tidak diupdate > DATA_USANG_HARI hari)
|
||||
$usang = $db->prepare("
|
||||
SELECT id, nama_kk, DATEDIFF(NOW(), updated_at) AS hari_usang
|
||||
FROM penduduk_miskin
|
||||
WHERE updated_at < DATE_SUB(NOW(), INTERVAL ? DAY)
|
||||
AND id NOT IN (
|
||||
SELECT DISTINCT penduduk_id FROM notifikasi
|
||||
WHERE jenis = 'data_usang' AND sudah_dibaca = 0
|
||||
)
|
||||
LIMIT 10
|
||||
");
|
||||
$usang->execute([DATA_USANG_HARI]);
|
||||
|
||||
foreach ($usang->fetchAll() as $p) {
|
||||
$db->prepare("
|
||||
INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id)
|
||||
VALUES ('data_usang','Data Tidak Diperbarui',?,?)
|
||||
")->execute(["Data KK {$p['nama_kk']} belum diperbarui selama {$p['hari_usang']} hari.", $p['id']]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
// api/penduduk.php v2.0
|
||||
// ============================================================
|
||||
// CRUD API Penduduk Miskin – Field diperluas
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
// ── Query SELECT lengkap (dipakai di GET & setelah write) ──
|
||||
$SELECT_SQL = "
|
||||
SELECT
|
||||
pm.*,
|
||||
w.kelurahan, w.kecamatan, w.kota,
|
||||
ri.nama AS nama_ibadah,
|
||||
ri.jenis AS jenis_ibadah,
|
||||
u.nama AS nama_surveyor,
|
||||
v.nama AS nama_verifikator,
|
||||
fp.file_path AS foto_utama,
|
||||
(
|
||||
SELECT GROUP_CONCAT(jb.kode ORDER BY jb.kode SEPARATOR ',')
|
||||
FROM bantuan_penduduk bp
|
||||
JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
|
||||
WHERE bp.penduduk_id = pm.id AND bp.status = 'aktif'
|
||||
) AS kode_bantuan,
|
||||
(
|
||||
SELECT COUNT(*) FROM bantuan_penduduk bp
|
||||
WHERE bp.penduduk_id = pm.id AND bp.status = 'aktif'
|
||||
) AS jumlah_bantuan
|
||||
FROM penduduk_miskin pm
|
||||
LEFT JOIN wilayah w ON w.id = pm.wilayah_id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id = pm.rumah_ibadah_id
|
||||
LEFT JOIN users u ON u.id = pm.surveyor_id
|
||||
LEFT JOIN users v ON v.id = pm.verified_by
|
||||
LEFT JOIN foto_rumah fp ON fp.penduduk_id = pm.id AND fp.is_primary = 1
|
||||
";
|
||||
|
||||
try {
|
||||
switch ($method) {
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// GET – Ambil data penduduk
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'GET':
|
||||
$id = $_GET['id'] ?? null;
|
||||
$kelurahan = $_GET['kelurahan'] ?? null;
|
||||
$kecamatan = $_GET['kecamatan'] ?? null;
|
||||
$kategori = $_GET['kategori'] ?? null;
|
||||
$bantuan = $_GET['bantuan'] ?? null; // 'ada'|'tidak'
|
||||
$verifikasi = $_GET['verifikasi'] ?? null;
|
||||
$surveyor = $_GET['surveyor_id']?? null;
|
||||
$search = $_GET['search'] ?? null;
|
||||
|
||||
// GET single
|
||||
if ($id) {
|
||||
$stmt = $db->prepare($SELECT_SQL . " WHERE pm.id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404);
|
||||
|
||||
// Ambil semua foto
|
||||
$fotoStmt = $db->prepare("SELECT * FROM foto_rumah WHERE penduduk_id = ? ORDER BY is_primary DESC, created_at ASC");
|
||||
$fotoStmt->execute([$id]);
|
||||
$row['fotos'] = $fotoStmt->fetchAll();
|
||||
|
||||
// Ambil semua bantuan
|
||||
$bantuanStmt = $db->prepare("
|
||||
SELECT bp.*, jb.kode, jb.nama AS nama_bantuan, jb.warna_badge
|
||||
FROM bantuan_penduduk bp
|
||||
JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
|
||||
WHERE bp.penduduk_id = ?
|
||||
ORDER BY jb.kode
|
||||
");
|
||||
$bantuanStmt->execute([$id]);
|
||||
$row['bantuans'] = $bantuanStmt->fetchAll();
|
||||
|
||||
jsonResponse(['success' => true, 'data' => $row]);
|
||||
}
|
||||
|
||||
// GET list dengan filter
|
||||
$sql = $SELECT_SQL . " WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($kelurahan) { $sql .= " AND w.kelurahan = ?"; $params[] = $kelurahan; }
|
||||
if ($kecamatan) { $sql .= " AND w.kecamatan = ?"; $params[] = $kecamatan; }
|
||||
if ($kategori) { $sql .= " AND pm.kategori_kemiskinan = ?"; $params[] = $kategori; }
|
||||
if ($verifikasi) { $sql .= " AND pm.status_verifikasi = ?"; $params[] = $verifikasi; }
|
||||
if ($surveyor) { $sql .= " AND pm.surveyor_id = ?"; $params[] = $surveyor; }
|
||||
if ($search) { $sql .= " AND (pm.nama_kk LIKE ? OR pm.nik_kk LIKE ? OR pm.alamat LIKE ?)";
|
||||
$params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%"; }
|
||||
if ($bantuan === 'tidak') {
|
||||
$sql .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') = 0";
|
||||
} elseif ($bantuan === 'ada') {
|
||||
$sql .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') > 0";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY pm.nama_kk ASC";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Statistik ringkas
|
||||
$stats = [
|
||||
'total' => count($rows),
|
||||
'total_jiwa' => array_sum(array_column($rows, 'jumlah_anggota')),
|
||||
'penerima_bantuan' => count(array_filter($rows, fn($r) => $r['jumlah_bantuan'] > 0)),
|
||||
'belum_verifikasi' => count(array_filter($rows, fn($r) => $r['status_verifikasi'] === 'belum_diverifikasi')),
|
||||
];
|
||||
|
||||
jsonResponse(['success' => true, 'data' => $rows, 'stats' => $stats]);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// POST – Tambah penduduk baru
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'POST':
|
||||
$in = getInput();
|
||||
|
||||
// Validasi wajib
|
||||
$required = ['nik_kk', 'nama_kk', 'lat', 'lng'];
|
||||
foreach ($required as $f) {
|
||||
if (empty($in[$f])) jsonResponse(['success' => false, 'error' => "Field '$f' wajib diisi"], 400);
|
||||
}
|
||||
|
||||
// Cek NIK duplikat
|
||||
$cek = $db->prepare("SELECT id FROM penduduk_miskin WHERE nik_kk = ?");
|
||||
$cek->execute([trim($in['nik_kk'])]);
|
||||
if ($cek->fetch()) jsonResponse(['success' => false, 'error' => 'NIK sudah terdaftar di sistem'], 409);
|
||||
|
||||
// Hitung kategori otomatis
|
||||
$penghasilan = isset($in['penghasilan_bulanan']) && $in['penghasilan_bulanan'] !== ''
|
||||
? (float)$in['penghasilan_bulanan'] : null;
|
||||
$kategori = hitungKategori($penghasilan);
|
||||
|
||||
$wilayahId = isset($in['wilayah_id']) && $in['wilayah_id'] !== '' ? (int)$in['wilayah_id'] : null;
|
||||
$surveyorId = isset($in['surveyor_id']) && $in['surveyor_id'] !== '' ? (int)$in['surveyor_id'] : null;
|
||||
|
||||
$statusPekerjaan = $in['status_pekerjaan'] ?? 'tidak_bekerja';
|
||||
$allowedPekerjaan = ['tidak_bekerja','buruh_harian','petani','nelayan','pedagang','pegawai_swasta','wiraswasta','lainnya'];
|
||||
if (!in_array($statusPekerjaan, $allowedPekerjaan)) {
|
||||
$statusPekerjaan = 'lainnya';
|
||||
}
|
||||
|
||||
$db->prepare("
|
||||
INSERT INTO penduduk_miskin (
|
||||
nik_kk, nama_kk, no_hp, alamat, wilayah_id, rt, rw, lat, lng,
|
||||
jumlah_anggota, jumlah_anak_sekolah,
|
||||
penghasilan_bulanan, sumber_penghasilan, status_pekerjaan,
|
||||
kategori_kemiskinan, kondisi_rumah, kepemilikan_rumah,
|
||||
punya_bpjs, jenis_bpjs, status_verifikasi, surveyor_id
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
")->execute([
|
||||
trim($in['nik_kk']),
|
||||
trim($in['nama_kk']),
|
||||
$in['no_hp'] ?? null,
|
||||
$in['alamat'] ?? null,
|
||||
$wilayahId,
|
||||
$in['rt'] ?? null,
|
||||
$in['rw'] ?? null,
|
||||
(float)$in['lat'],
|
||||
(float)$in['lng'],
|
||||
max(1, (int)($in['jumlah_anggota'] ?? 1)),
|
||||
max(0, (int)($in['jumlah_anak_sekolah'] ?? 0)),
|
||||
$penghasilan,
|
||||
$in['sumber_penghasilan'] ?? null,
|
||||
$statusPekerjaan,
|
||||
$kategori,
|
||||
$in['kondisi_rumah'] ?? 'tidak_layak',
|
||||
$in['kepemilikan_rumah'] ?? 'milik_sendiri',
|
||||
(int)($in['punya_bpjs'] ?? 0),
|
||||
$in['jenis_bpjs'] ?? 'tidak_punya',
|
||||
'belum_diverifikasi',
|
||||
$surveyorId,
|
||||
]);
|
||||
|
||||
$newId = (int)$db->lastInsertId();
|
||||
assignRumahIbadah($newId);
|
||||
|
||||
// Catat riwayat & notifikasi – tidak block jika gagal (misal user_id tidak ada)
|
||||
try {
|
||||
catatRiwayat($newId, (int)($surveyorId ?? 1), 'submit', 'Data baru dari survei');
|
||||
} catch(\Exception $e) { /* abaikan error audit trail */ }
|
||||
|
||||
try {
|
||||
$db->prepare("
|
||||
INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id)
|
||||
VALUES ('belum_verifikasi', 'Data Baru Belum Diverifikasi', ?, ?)
|
||||
")->execute(["Data KK {$in['nama_kk']} (NIK: {$in['nik_kk']}) menunggu verifikasi admin.", $newId]);
|
||||
} catch(\Exception $e) { /* abaikan error notifikasi */ }
|
||||
|
||||
$stmt = $db->prepare($SELECT_SQL . " WHERE pm.id = ?");
|
||||
$stmt->execute([$newId]);
|
||||
jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Data penduduk berhasil ditambahkan']);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// PUT – Update data penduduk
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'PUT':
|
||||
$in = getInput();
|
||||
$id = (int)($in['id'] ?? $_GET['id'] ?? 0);
|
||||
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||
|
||||
// Ambil data lama untuk audit
|
||||
$old = $db->prepare("SELECT * FROM penduduk_miskin WHERE id = ?");
|
||||
$old->execute([$id]);
|
||||
$dataLama = $old->fetch();
|
||||
if (!$dataLama) jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404);
|
||||
|
||||
// Hitung kategori ulang jika penghasilan berubah
|
||||
$penghasilan = array_key_exists('penghasilan_bulanan', $in)
|
||||
? (($in['penghasilan_bulanan'] !== '') ? (float)$in['penghasilan_bulanan'] : null)
|
||||
: (float)$dataLama['penghasilan_bulanan'];
|
||||
$kategori = hitungKategori($penghasilan);
|
||||
|
||||
$allowed = [
|
||||
'nama_kk','no_hp','alamat','wilayah_id','rt','rw','lat','lng',
|
||||
'jumlah_anggota','jumlah_anak_sekolah','penghasilan_bulanan',
|
||||
'sumber_penghasilan','status_pekerjaan','kondisi_rumah',
|
||||
'kepemilikan_rumah','punya_bpjs','jenis_bpjs',
|
||||
'status_verifikasi','catatan_verifikasi'
|
||||
];
|
||||
|
||||
$fields = []; $values = []; $posChanged = false;
|
||||
foreach ($allowed as $f) {
|
||||
if (!array_key_exists($f, $in)) continue;
|
||||
$fields[] = "$f = ?";
|
||||
if ($f === 'lat' || $f === 'lng') { $values[] = (float)$in[$f]; $posChanged = true; }
|
||||
elseif ($f === 'wilayah_id') { $values[] = (isset($in[$f]) && $in[$f] !== '') ? (int)$in[$f] : null; }
|
||||
elseif ($f === 'status_pekerjaan') {
|
||||
$statusPekerjaan = $in[$f] ?? 'tidak_bekerja';
|
||||
$allowedPekerjaan = ['tidak_bekerja','buruh_harian','petani','nelayan','pedagang','pegawai_swasta','wiraswasta','lainnya'];
|
||||
$values[] = in_array($statusPekerjaan, $allowedPekerjaan) ? $statusPekerjaan : 'lainnya';
|
||||
}
|
||||
elseif (in_array($f, ['jumlah_anggota','jumlah_anak_sekolah','punya_bpjs'])) $values[] = (int)$in[$f];
|
||||
elseif ($f === 'penghasilan_bulanan') $values[] = $penghasilan;
|
||||
else $values[] = trim((string)$in[$f]);
|
||||
}
|
||||
|
||||
// Update kategori otomatis
|
||||
$fields[] = "kategori_kemiskinan = ?";
|
||||
$values[] = $kategori;
|
||||
|
||||
// Jika diverifikasi, catat verifikator
|
||||
if (isset($in['status_verifikasi']) && $in['status_verifikasi'] === 'terverifikasi') {
|
||||
$fields[] = "verified_by = ?"; $values[] = $in['verified_by'] ?? 1;
|
||||
$fields[] = "verified_at = NOW()";
|
||||
}
|
||||
|
||||
if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data untuk diupdate'], 400);
|
||||
|
||||
$values[] = $id;
|
||||
$db->prepare("UPDATE penduduk_miskin SET " . implode(', ', $fields) . " WHERE id = ?")
|
||||
->execute($values);
|
||||
|
||||
if ($posChanged) assignRumahIbadah($id);
|
||||
|
||||
$aksi = 'update';
|
||||
if (isset($in['status_verifikasi'])) {
|
||||
if ($in['status_verifikasi'] === 'terverifikasi') {
|
||||
$aksi = 'verifikasi';
|
||||
} elseif ($in['status_verifikasi'] === 'ditolak') {
|
||||
$aksi = 'tolak';
|
||||
}
|
||||
}
|
||||
try {
|
||||
catatRiwayat($id, (int)($in['updated_by'] ?? 1), $aksi, $in['catatan_verifikasi'] ?? null, $dataLama);
|
||||
} catch(\Exception $e) { /* abaikan error audit trail */ }
|
||||
|
||||
$stmt = $db->prepare($SELECT_SQL . " WHERE pm.id = ?");
|
||||
$stmt->execute([$id]);
|
||||
jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Data berhasil diperbarui']);
|
||||
break;
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// DELETE – Hapus penduduk
|
||||
// ════════════════════════════════════════════════════════
|
||||
case 'DELETE':
|
||||
$id = (int)($_GET['id'] ?? getInput()['id'] ?? 0);
|
||||
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||
|
||||
// Hapus foto fisik terlebih dahulu
|
||||
$fotos = $db->prepare("SELECT file_path FROM foto_rumah WHERE penduduk_id = ?");
|
||||
$fotos->execute([$id]);
|
||||
foreach ($fotos->fetchAll() as $foto) {
|
||||
$path = __DIR__ . '/../' . $foto['file_path'];
|
||||
if (file_exists($path)) @unlink($path);
|
||||
}
|
||||
|
||||
catatRiwayat($id, 1, 'hapus', 'Data dihapus oleh admin');
|
||||
$db->prepare("DELETE FROM penduduk_miskin WHERE id = ?")->execute([$id]);
|
||||
jsonResponse(['success' => true, 'message' => 'Data penduduk berhasil dihapus']);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
jsonResponse(['success' => false, 'error' => 'Database error: ' . $e->getMessage()], 500);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
// api/rumah_ibadah.php
|
||||
// CRUD API untuk Rumah Ibadah
|
||||
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ── GET semua rumah ibadah + summary penduduk
|
||||
case 'GET':
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("
|
||||
SELECT ri.*,
|
||||
COUNT(pm.id) as jumlah_kk,
|
||||
COALESCE(SUM(pm.jumlah_anggota), 0) as total_jiwa
|
||||
FROM rumah_ibadah ri
|
||||
LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id = ri.id
|
||||
WHERE ri.id = ?
|
||||
GROUP BY ri.id
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if ($row) jsonResponse(['success' => true, 'data' => $row]);
|
||||
else jsonResponse(['success' => false, 'error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$rows = $db->query("
|
||||
SELECT ri.*,
|
||||
COUNT(pm.id) as jumlah_kk,
|
||||
COALESCE(SUM(pm.jumlah_anggota), 0) as total_jiwa
|
||||
FROM rumah_ibadah ri
|
||||
LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id = ri.id
|
||||
GROUP BY ri.id
|
||||
ORDER BY ri.nama ASC
|
||||
")->fetchAll();
|
||||
jsonResponse(['success' => true, 'data' => $rows]);
|
||||
break;
|
||||
|
||||
// ── POST buat rumah ibadah baru
|
||||
case 'POST':
|
||||
$input = getInput();
|
||||
$required = ['nama', 'lat', 'lng'];
|
||||
foreach ($required as $field) {
|
||||
if (empty($input[$field])) {
|
||||
jsonResponse(['success' => false, 'error' => "Field '$field' wajib diisi"], 400);
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, lat, lng, radius)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
trim($input['nama']),
|
||||
$input['jenis'] ?? 'masjid',
|
||||
$input['alamat'] ?? '',
|
||||
$input['pic'] ?? '',
|
||||
$input['no_wa'] ?? '',
|
||||
(float)$input['lat'],
|
||||
(float)$input['lng'],
|
||||
(int)($input['radius'] ?? 500),
|
||||
]);
|
||||
|
||||
$newId = (int)$db->lastInsertId();
|
||||
reassignAll();
|
||||
|
||||
$stmt2 = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
|
||||
$stmt2->execute([$newId]);
|
||||
jsonResponse(['success' => true, 'data' => $stmt2->fetch(), 'message' => 'Rumah ibadah berhasil ditambahkan']);
|
||||
break;
|
||||
|
||||
// ── PUT update rumah ibadah
|
||||
case 'PUT':
|
||||
$input = getInput();
|
||||
$id = $input['id'] ?? $_GET['id'] ?? null;
|
||||
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||
|
||||
$fields = [];
|
||||
$values = [];
|
||||
$allowed = ['nama', 'jenis', 'alamat', 'pic', 'no_wa', 'lat', 'lng', 'radius'];
|
||||
foreach ($allowed as $f) {
|
||||
if (isset($input[$f])) {
|
||||
$fields[] = "$f = ?";
|
||||
$values[] = ($f === 'lat' || $f === 'lng') ? (float)$input[$f] :
|
||||
($f === 'radius' ? (int)$input[$f] : trim($input[$f]));
|
||||
}
|
||||
}
|
||||
if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data yang diupdate'], 400);
|
||||
|
||||
$values[] = $id;
|
||||
$db->prepare("UPDATE rumah_ibadah SET " . implode(', ', $fields) . " WHERE id = ?")->execute($values);
|
||||
|
||||
// Radius berubah → reassign semua
|
||||
if (isset($input['radius']) || isset($input['lat']) || isset($input['lng'])) {
|
||||
reassignAll();
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Berhasil diupdate']);
|
||||
break;
|
||||
|
||||
// ── DELETE hapus rumah ibadah
|
||||
case 'DELETE':
|
||||
$id = $_GET['id'] ?? getInput()['id'] ?? null;
|
||||
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||
|
||||
// Lepas referensi penduduk
|
||||
$db->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id = NULL, jarak_ke_ibadah = NULL WHERE rumah_ibadah_id = ?")
|
||||
->execute([$id]);
|
||||
$db->prepare("DELETE FROM rumah_ibadah WHERE id = ?")->execute([$id]);
|
||||
|
||||
reassignAll();
|
||||
jsonResponse(['success' => true, 'message' => 'Berhasil dihapus']);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
// api/update_radius.php
|
||||
// Update radius satu rumah ibadah + reassign penduduk
|
||||
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
|
||||
$input = getInput();
|
||||
$id = $input['id'] ?? null;
|
||||
$radius = $input['radius'] ?? null;
|
||||
|
||||
if (!$id || !$radius) {
|
||||
jsonResponse(['success' => false, 'error' => 'id dan radius diperlukan'], 400);
|
||||
}
|
||||
|
||||
$radius = max(100, min(5000, (int)$radius)); // clamp 100–5000m
|
||||
|
||||
$db = getDB();
|
||||
$db->prepare("UPDATE rumah_ibadah SET radius = ? WHERE id = ?")->execute([$radius, $id]);
|
||||
|
||||
reassignAll();
|
||||
|
||||
jsonResponse(['success' => true, 'radius' => $radius, 'message' => "Radius diubah ke {$radius}m, penduduk di-reassign"]);
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
// api/wilayah.php v2.0
|
||||
// ============================================================
|
||||
// API Data Wilayah (RT/RW/Kelurahan/Kecamatan)
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ── GET semua wilayah atau per filter ─────────────────
|
||||
case 'GET':
|
||||
$kelurahan = $_GET['kelurahan'] ?? null;
|
||||
$kecamatan = $_GET['kecamatan'] ?? null;
|
||||
|
||||
// GET /api/wilayah.php?list=kelurahan → distinct kelurahan
|
||||
if (isset($_GET['list'])) {
|
||||
$type = $_GET['list'];
|
||||
if ($type === 'kelurahan') {
|
||||
$rows = $db->query("SELECT DISTINCT kelurahan, kecamatan FROM wilayah ORDER BY kelurahan")->fetchAll();
|
||||
jsonResponse(['success' => true, 'data' => $rows]);
|
||||
}
|
||||
if ($type === 'kecamatan') {
|
||||
$rows = $db->query("SELECT DISTINCT kecamatan FROM wilayah ORDER BY kecamatan")->fetchAll();
|
||||
jsonResponse(['success' => true, 'data' => array_column($rows, 'kecamatan')]);
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM wilayah WHERE 1=1";
|
||||
$params = [];
|
||||
if ($kelurahan) { $sql .= " AND kelurahan = ?"; $params[] = $kelurahan; }
|
||||
if ($kecamatan) { $sql .= " AND kecamatan = ?"; $params[] = $kecamatan; }
|
||||
$sql .= " ORDER BY kelurahan, rt, rw";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
|
||||
break;
|
||||
|
||||
// ── POST tambah wilayah ───────────────────────────────
|
||||
case 'POST':
|
||||
$in = getInput();
|
||||
if (empty($in['kelurahan']) || empty($in['kecamatan'])) {
|
||||
jsonResponse(['success' => false, 'error' => 'Kelurahan dan kecamatan wajib diisi'], 400);
|
||||
}
|
||||
$db->prepare("INSERT INTO wilayah (kelurahan, kecamatan, kota, rt, rw) VALUES (?,?,?,?,?)")
|
||||
->execute([
|
||||
trim($in['kelurahan']),
|
||||
trim($in['kecamatan']),
|
||||
trim($in['kota'] ?? 'Pontianak'),
|
||||
$in['rt'] ?? null,
|
||||
$in['rw'] ?? null,
|
||||
]);
|
||||
jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Wilayah ditambahkan']);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,363 @@
|
||||
-- ============================================================
|
||||
-- SIG Penduduk Miskin v2.0 – Database Schema
|
||||
-- WebGIS Pendataan & Monitoring Bantuan Sosial
|
||||
-- Update: 2026 | Versi: 2.0
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS sig_kemiskinan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE sig_kemiskinan;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 1: users
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(100) NOT NULL,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL COMMENT 'bcrypt hash',
|
||||
role ENUM('admin','petugas','pimpinan') NOT NULL DEFAULT 'petugas',
|
||||
wilayah_tugas VARCHAR(100) NULL COMMENT 'Kelurahan/RT wilayah tugas petugas',
|
||||
aktif TINYINT(1) DEFAULT 1,
|
||||
last_login TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_role (role)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 2: wilayah
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS wilayah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kelurahan VARCHAR(100) NOT NULL,
|
||||
kecamatan VARCHAR(100) NOT NULL,
|
||||
kota VARCHAR(100) NOT NULL DEFAULT 'Pontianak',
|
||||
rt VARCHAR(10) NULL,
|
||||
rw VARCHAR(10) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_kelurahan (kelurahan),
|
||||
INDEX idx_kecamatan (kecamatan)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 3: rumah_ibadah (Tidak berubah)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS rumah_ibadah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(255) NOT NULL,
|
||||
jenis ENUM('masjid','musholla','gereja','pura','vihara','klenteng','lainnya') DEFAULT 'masjid',
|
||||
alamat TEXT,
|
||||
pic VARCHAR(255) COMMENT 'Person In Charge / Penanggung Jawab',
|
||||
no_wa VARCHAR(20),
|
||||
lat DECIMAL(10, 8) NOT NULL,
|
||||
lng DECIMAL(11, 8) NOT NULL,
|
||||
radius INT DEFAULT 500 COMMENT 'Radius dalam meter',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 4: penduduk_miskin (DIPERLUAS dari v1.1)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS penduduk_miskin (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
|
||||
-- Identitas Kepala Keluarga
|
||||
nik_kk VARCHAR(16) NOT NULL UNIQUE COMMENT 'NIK KK 16 digit',
|
||||
nama_kk VARCHAR(255) NOT NULL COMMENT 'Nama Kepala Keluarga',
|
||||
no_hp VARCHAR(20) NULL COMMENT 'No HP/WhatsApp',
|
||||
|
||||
-- Lokasi
|
||||
alamat TEXT NULL,
|
||||
wilayah_id INT NULL,
|
||||
rt VARCHAR(10) NULL,
|
||||
rw VARCHAR(10) NULL,
|
||||
lat DECIMAL(10,8) NOT NULL,
|
||||
lng DECIMAL(11,8) NOT NULL,
|
||||
|
||||
-- Data Keluarga
|
||||
jumlah_anggota INT NOT NULL DEFAULT 1,
|
||||
jumlah_anak_sekolah INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- Data Ekonomi
|
||||
penghasilan_bulanan DECIMAL(12,2) NULL COMMENT 'Penghasilan per bulan (Rp)',
|
||||
sumber_penghasilan VARCHAR(255) NULL,
|
||||
status_pekerjaan ENUM(
|
||||
'tidak_bekerja','buruh_harian','petani','nelayan',
|
||||
'pedagang','pegawai_swasta','wiraswasta','lainnya'
|
||||
) DEFAULT 'tidak_bekerja',
|
||||
|
||||
-- Kategori Kemiskinan (dihitung otomatis)
|
||||
kategori_kemiskinan ENUM('sangat_miskin','miskin','hampir_miskin','mampu') DEFAULT 'sangat_miskin',
|
||||
|
||||
-- Kondisi Rumah
|
||||
kondisi_rumah ENUM('sangat_tidak_layak','tidak_layak','layak') DEFAULT 'tidak_layak',
|
||||
kepemilikan_rumah ENUM('milik_sendiri','kontrak','menumpang','lainnya') DEFAULT 'milik_sendiri',
|
||||
|
||||
-- Status BPJS
|
||||
punya_bpjs TINYINT(1) DEFAULT 0,
|
||||
jenis_bpjs ENUM('PBI','mandiri','tidak_punya') DEFAULT 'tidak_punya',
|
||||
|
||||
-- Relasi ke Rumah Ibadah (kompatibel v1.1)
|
||||
rumah_ibadah_id INT NULL,
|
||||
jarak_ke_ibadah DECIMAL(10,2) NULL COMMENT 'Jarak dalam meter',
|
||||
|
||||
-- Status Verifikasi
|
||||
status_verifikasi ENUM('belum_diverifikasi','terverifikasi','ditolak') DEFAULT 'belum_diverifikasi',
|
||||
catatan_verifikasi TEXT NULL,
|
||||
|
||||
-- Audit Trail
|
||||
surveyor_id INT NULL COMMENT 'FK users - petugas input',
|
||||
verified_by INT NULL COMMENT 'FK users - admin verifikator',
|
||||
verified_at TIMESTAMP NULL,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Foreign Keys
|
||||
CONSTRAINT fk_pm_wilayah FOREIGN KEY (wilayah_id) REFERENCES wilayah(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_pm_ibadah FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_pm_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_pm_verif FOREIGN KEY (verified_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
INDEX idx_nik (nik_kk),
|
||||
INDEX idx_lat_lng (lat, lng),
|
||||
INDEX idx_kategori (kategori_kemiskinan),
|
||||
INDEX idx_wilayah (wilayah_id),
|
||||
INDEX idx_status_v (status_verifikasi),
|
||||
INDEX idx_surveyor (surveyor_id)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 5: foto_rumah
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS foto_rumah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL COMMENT 'Path relatif dari root web',
|
||||
lat_foto DECIMAL(10,8) NULL COMMENT 'Koordinat GPS saat foto diambil',
|
||||
lng_foto DECIMAL(11,8) NULL,
|
||||
accuracy_meter DECIMAL(8,2) NULL COMMENT 'Akurasi GPS dalam meter',
|
||||
is_primary TINYINT(1) DEFAULT 0 COMMENT '1 = foto utama (thumbnail peta)',
|
||||
ukuran_kb INT NULL,
|
||||
keterangan VARCHAR(255) NULL,
|
||||
diambil_oleh INT NULL COMMENT 'FK users',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_fr_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_fr_user FOREIGN KEY (diambil_oleh) REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
INDEX idx_penduduk (penduduk_id),
|
||||
INDEX idx_primary (penduduk_id, is_primary)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 6: jenis_bantuan
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS jenis_bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kode VARCHAR(20) NOT NULL UNIQUE,
|
||||
nama VARCHAR(100) NOT NULL,
|
||||
deskripsi TEXT NULL,
|
||||
warna_badge VARCHAR(7) DEFAULT '#4f8ef7' COMMENT 'Hex color badge',
|
||||
aktif TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 7: bantuan_penduduk (Many-to-Many pivot)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS bantuan_penduduk (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL,
|
||||
bantuan_id INT NOT NULL,
|
||||
status ENUM('aktif','tidak_aktif','dicabut') DEFAULT 'aktif',
|
||||
tanggal_mulai DATE NULL,
|
||||
tanggal_selesai DATE NULL,
|
||||
nominal_bulanan DECIMAL(12,2) NULL,
|
||||
nomor_kpj VARCHAR(50) NULL COMMENT 'Nomor kartu/KPJ bantuan',
|
||||
keterangan TEXT NULL,
|
||||
diinput_oleh INT NULL COMMENT 'FK users',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_bp_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_bp_bantuan FOREIGN KEY (bantuan_id) REFERENCES jenis_bantuan(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_bp_user FOREIGN KEY (diinput_oleh) REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
UNIQUE KEY uq_penduduk_bantuan (penduduk_id, bantuan_id),
|
||||
INDEX idx_penduduk (penduduk_id),
|
||||
INDEX idx_bantuan (bantuan_id),
|
||||
INDEX idx_status (status)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 8: riwayat_verifikasi (Audit Trail)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS riwayat_verifikasi (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
aksi ENUM('submit','verifikasi','tolak','update','hapus') NOT NULL,
|
||||
catatan TEXT NULL,
|
||||
data_lama JSON NULL COMMENT 'Snapshot data sebelum perubahan',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_rv_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_rv_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
|
||||
|
||||
INDEX idx_penduduk (penduduk_id),
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_tanggal (created_at)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL 9: notifikasi
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS notifikasi (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NULL COMMENT 'NULL = broadcast semua admin',
|
||||
jenis ENUM('belum_bantuan','belum_verifikasi','data_usang','info') DEFAULT 'info',
|
||||
judul VARCHAR(255) NOT NULL,
|
||||
pesan TEXT NOT NULL,
|
||||
penduduk_id INT NULL COMMENT 'Referensi penduduk terkait',
|
||||
sudah_dibaca TINYINT(1) DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_notif_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_notif_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE SET NULL,
|
||||
|
||||
INDEX idx_user_baca (user_id, sudah_dibaca),
|
||||
INDEX idx_jenis (jenis)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- INDEX SPASIAL
|
||||
-- ============================================================
|
||||
CREATE INDEX idx_ri_lat_lng ON rumah_ibadah(lat, lng);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ============================================================
|
||||
-- DATA AWAL: users
|
||||
-- ============================================================
|
||||
INSERT INTO users (nama, username, password, role) VALUES
|
||||
('Administrator', 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin'),
|
||||
('Petugas Survei', 'petugas', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'petugas'),
|
||||
('Kepala Dinas', 'pimpinan', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'pimpinan');
|
||||
-- Password semua akun: "password" (bcrypt)
|
||||
|
||||
-- ============================================================
|
||||
-- DATA AWAL: wilayah
|
||||
-- ============================================================
|
||||
INSERT INTO wilayah (kelurahan, kecamatan, kota, rt, rw) VALUES
|
||||
('Siantan Tengah', 'Pontianak Utara', 'Pontianak', '01', '01'),
|
||||
('Siantan Tengah', 'Pontianak Utara', 'Pontianak', '02', '01'),
|
||||
('Siantan Hilir', 'Pontianak Utara', 'Pontianak', '01', '02'),
|
||||
('Batu Layang', 'Pontianak Utara', 'Pontianak', '01', '01'),
|
||||
('Batu Layang', 'Pontianak Utara', 'Pontianak', '02', '02'),
|
||||
('Saigon', 'Pontianak Timur', 'Pontianak', '01', '01'),
|
||||
('Saigon', 'Pontianak Timur', 'Pontianak', '02', '01'),
|
||||
('Tanjung Hulu', 'Pontianak Timur', 'Pontianak', '01', '01'),
|
||||
('Parit Mayor', 'Pontianak Timur', 'Pontianak', '01', '01'),
|
||||
('Pontianak Kota', 'Pontianak Kota', 'Pontianak', '01', '01');
|
||||
|
||||
-- ============================================================
|
||||
-- DATA AWAL: rumah_ibadah
|
||||
-- ============================================================
|
||||
INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, lat, lng, radius) VALUES
|
||||
('Masjid Al-Ikhlas', 'masjid', 'Jl. Merdeka No. 1, Pontianak', 'Ahmad Fauzi', '081234567890', -0.0263303, 109.3425094, 500),
|
||||
('Masjid Ar-Rahmat', 'masjid', 'Jl. Gajah Mada No. 45, Pontianak', 'Budi Santoso', '082345678901', -0.0320000, 109.3380000, 500),
|
||||
('Gereja Bethel', 'gereja', 'Jl. Diponegoro No. 12, Pontianak', 'Pendeta Yusuf', '083456789012', -0.0290000, 109.3450000, 500);
|
||||
|
||||
-- ============================================================
|
||||
-- DATA AWAL: jenis_bantuan
|
||||
-- ============================================================
|
||||
INSERT INTO jenis_bantuan (kode, nama, deskripsi, warna_badge) VALUES
|
||||
('PKH', 'Program Keluarga Harapan', 'Bantuan tunai bersyarat untuk KK miskin', '#22c55e'),
|
||||
('BPNT', 'Bantuan Pangan Non Tunai', 'Sembako via e-warong setiap 2 bulan', '#3b82f6'),
|
||||
('KIS', 'Kartu Indonesia Sehat', 'Akses layanan kesehatan gratis (BPJS PBI)', '#ec4899'),
|
||||
('BLT', 'Bantuan Langsung Tunai', 'Bantuan tunai darurat/sementara dari desa/kel', '#f59e0b'),
|
||||
('BST', 'Bantuan Sosial Tunai', 'Bantuan sosial tunai dari Kemensos', '#8b5cf6'),
|
||||
('PIP', 'Program Indonesia Pintar', 'Bantuan pendidikan untuk anak sekolah', '#06b6d4');
|
||||
|
||||
-- ============================================================
|
||||
-- DATA AWAL: penduduk_miskin (contoh)
|
||||
-- ============================================================
|
||||
INSERT INTO penduduk_miskin (
|
||||
nik_kk, nama_kk, no_hp, alamat, wilayah_id, rt, rw,
|
||||
lat, lng, jumlah_anggota, jumlah_anak_sekolah,
|
||||
penghasilan_bulanan, sumber_penghasilan, status_pekerjaan,
|
||||
kategori_kemiskinan, kondisi_rumah, kepemilikan_rumah,
|
||||
punya_bpjs, jenis_bpjs, status_verifikasi, surveyor_id
|
||||
) VALUES
|
||||
('6171010101010001', 'Suyatno', '081200000001', 'Jl. Merdeka Gg. III No. 5', 1, '01', '01', -0.0270000, 109.3430000, 4, 2, 850000, 'Buruh bangunan', 'buruh_harian', 'miskin', 'tidak_layak', 'menumpang', 1, 'PBI', 'terverifikasi', 1),
|
||||
('6171010101010002', 'Rohani', '081200000002', 'Jl. Merdeka Gg. IV No. 2', 1, '02', '01', -0.0255000, 109.3420000, 3, 1, 450000, 'Jualan kecil', 'pedagang', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'terverifikasi', 1),
|
||||
('6171010101010003', 'Hamid Husin', '081200000003', 'Jl. Gajah Mada Gg. II No. 8', 2, '01', '02', -0.0315000, 109.3375000, 5, 3, 1200000, 'Ojek online', 'buruh_harian', 'mampu', 'tidak_layak', 'kontrak', 0, 'tidak_punya','belum_diverifikasi',2),
|
||||
('6171010101010004', 'Yanti Susanti', '081200000004', 'Jl. Veteran No. 99', 3, '01', '01', -0.0500000, 109.3600000, 2, 0, 300000, 'Tidak ada', 'tidak_bekerja', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'terverifikasi', 1),
|
||||
('6171010101010005', 'Supriyadi', '081200000005', 'Jl. Budi Utomo No. 12', 4, '02', '01', -0.0340000, 109.3510000, 6, 4, 600000, 'Petani kecil', 'petani', 'miskin', 'tidak_layak', 'milik_sendiri', 1, 'PBI', 'terverifikasi', 1),
|
||||
('6171010101010006', 'Marlina', '081200000006', 'Gg. Damai No. 3', 5, '01', '02', -0.0410000, 109.3480000, 3, 1, 200000, 'Buruh cuci', 'buruh_harian', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 0, 'tidak_punya','belum_diverifikasi',2),
|
||||
('6171010101010007', 'Junaidi', '081200000007', 'Jl. Khatulistiwa No. 45', 6, '01', '01', -0.0380000, 109.3550000, 4, 2, 950000, 'Nelayan', 'nelayan', 'miskin', 'tidak_layak', 'milik_sendiri', 1, 'mandiri', 'terverifikasi', 1),
|
||||
('6171010101010008', 'Siti Aminah', '081200000008', 'Gg. Melati No. 7', 7, '02', '02', -0.0290000, 109.3395000, 5, 3, 400000, 'Pembantu RT', 'buruh_harian', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'belum_diverifikasi',2),
|
||||
('6171010101010009', 'Rahman', '081200000009', 'Jl. Tanjung Pura No. 88', 8, '01', '01', -0.0220000, 109.3460000, 3, 1, 750000, 'Pedagang kaki lima','pedagang', 'miskin', 'tidak_layak', 'kontrak', 0, 'tidak_punya','terverifikasi', 1),
|
||||
('6171010101010010', 'Dewi Kartika', '081200000010', 'Gg. Bahagia No. 2', 9, '01', '01', -0.0450000, 109.3530000, 4, 2, 500000, 'Buruh pabrik', 'buruh_harian', 'sangat_miskin', 'tidak_layak', 'kontrak', 1, 'PBI', 'terverifikasi', 1);
|
||||
|
||||
-- ============================================================
|
||||
-- DATA AWAL: bantuan_penduduk
|
||||
-- ============================================================
|
||||
INSERT INTO bantuan_penduduk (penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, diinput_oleh) VALUES
|
||||
(1, 1, 'aktif', '2024-01-01', 500000, 1), -- Suyatno: PKH
|
||||
(1, 2, 'aktif', '2024-01-01', 200000, 1), -- Suyatno: BPNT
|
||||
(2, 1, 'aktif', '2023-07-01', 500000, 1), -- Rohani: PKH
|
||||
(2, 3, 'aktif', '2023-07-01', 0, 1), -- Rohani: KIS
|
||||
(4, 1, 'aktif', '2024-03-01', 500000, 1), -- Yanti: PKH
|
||||
(4, 3, 'aktif', '2024-03-01', 0, 1), -- Yanti: KIS
|
||||
(5, 2, 'aktif', '2024-02-01', 200000, 1), -- Supriyadi: BPNT
|
||||
(7, 3, 'aktif', '2023-09-01', 0, 1), -- Junaidi: KIS
|
||||
(9, 2, 'aktif', '2024-01-01', 200000, 1), -- Rahman: BPNT
|
||||
(10, 1, 'aktif','2024-04-01', 500000, 1), -- Dewi: PKH
|
||||
(10, 3, 'aktif','2024-04-01', 0, 1); -- Dewi: KIS
|
||||
|
||||
-- ============================================================
|
||||
-- Update rumah_ibadah assignment (kompatibel v1.1)
|
||||
-- ============================================================
|
||||
UPDATE penduduk_miskin pm
|
||||
SET
|
||||
rumah_ibadah_id = (
|
||||
SELECT ri.id FROM rumah_ibadah ri
|
||||
WHERE (
|
||||
6371000 * ACOS(GREATEST(-1, LEAST(1,
|
||||
COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) *
|
||||
COS(RADIANS(ri.lng) - RADIANS(pm.lng)) +
|
||||
SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat))
|
||||
)))
|
||||
) <= ri.radius
|
||||
ORDER BY (
|
||||
6371000 * ACOS(GREATEST(-1, LEAST(1,
|
||||
COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) *
|
||||
COS(RADIANS(ri.lng) - RADIANS(pm.lng)) +
|
||||
SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat))
|
||||
)))
|
||||
) ASC
|
||||
LIMIT 1
|
||||
),
|
||||
jarak_ke_ibadah = (
|
||||
SELECT ROUND(
|
||||
6371000 * ACOS(GREATEST(-1, LEAST(1,
|
||||
COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) *
|
||||
COS(RADIANS(ri.lng) - RADIANS(pm.lng)) +
|
||||
SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat))
|
||||
))), 2
|
||||
) FROM rumah_ibadah ri
|
||||
WHERE (
|
||||
6371000 * ACOS(GREATEST(-1, LEAST(1,
|
||||
COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) *
|
||||
COS(RADIANS(ri.lng) - RADIANS(pm.lng)) +
|
||||
SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat))
|
||||
)))
|
||||
) <= ri.radius
|
||||
LIMIT 1
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
// includes/auth.php v2.0
|
||||
// ============================================================
|
||||
// Middleware Autentikasi berbasis PHP Session
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
session_start();
|
||||
|
||||
// ── Cek apakah user sudah login ───────────────────────────
|
||||
function requireAuth(array $allowedRoles = []): array {
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
jsonResponse(['success' => false, 'error' => 'Unauthorized – silakan login', 'redirect' => '/index.html'], 401);
|
||||
}
|
||||
$user = $_SESSION['user'];
|
||||
if (!empty($allowedRoles) && !in_array($user['role'], $allowedRoles)) {
|
||||
jsonResponse(['success' => false, 'error' => 'Akses ditolak untuk role: ' . $user['role']], 403);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
// ── Ambil user saat ini (null jika belum login) ───────────
|
||||
function currentUser(): ?array {
|
||||
return $_SESSION['user'] ?? null;
|
||||
}
|
||||
|
||||
// ── Cek apakah sudah login (tanpa redirect) ───────────────
|
||||
function isLoggedIn(): bool {
|
||||
return isset($_SESSION['user_id']);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
// includes/config.php v2.0
|
||||
// ============================================================
|
||||
// Konfigurasi Database & Konstanta Aplikasi
|
||||
// ============================================================
|
||||
|
||||
// ── Database ──────────────────────────────────────────────
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', '');
|
||||
define('DB_NAME', 'sig_kemiskinan');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// ── Aplikasi ──────────────────────────────────────────────
|
||||
define('APP_NAME', 'SIG Kemiskinan v2.0');
|
||||
define('NOMINATIM_URL', 'https://nominatim.openstreetmap.org');
|
||||
|
||||
// ── Upload Foto ────────────────────────────────────────────
|
||||
define('UPLOAD_DIR', __DIR__ . '/../assets/img/uploads/');
|
||||
define('UPLOAD_URL', 'assets/img/uploads/');
|
||||
define('MAX_FOTO_SIZE', 5 * 1024 * 1024); // 5 MB
|
||||
define('ALLOWED_EXT', ['jpg','jpeg','png','webp']);
|
||||
|
||||
// ── Threshold Kategori Kemiskinan (Rp/bulan) ──────────────
|
||||
// Disesuaikan dengan standar BPS & UMR Kalimantan Barat 2024
|
||||
define('BATAS_SANGAT_MISKIN', 1500000); // < 1.5jt → sangat_miskin
|
||||
define('BATAS_MISKIN', 3000000); // < 3jt → miskin
|
||||
define('BATAS_HAMPIR_MISKIN', 5000000); // < 5jt → hampir_miskin
|
||||
// >= 5jt → mampu
|
||||
// ── Notifikasi ────────────────────────────────────────────
|
||||
define('DATA_USANG_HARI', 30); // data > 30 hari belum update = notif usang
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI: Koneksi Database (Singleton PDO)
|
||||
// ============================================================
|
||||
function getDB(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
die(json_encode(['success' => false, 'error' => 'Database connection failed: ' . $e->getMessage()]));
|
||||
}
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI: Response JSON
|
||||
// ============================================================
|
||||
function jsonResponse(mixed $data, int $code = 200): void {
|
||||
http_response_code($code);
|
||||
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, Authorization');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI: Baca Input (JSON body atau $_POST)
|
||||
// ============================================================
|
||||
function getInput(): array {
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$data = $_POST;
|
||||
}
|
||||
return $data ?? [];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI: Hitung Jarak Haversine (meter)
|
||||
// ============================================================
|
||||
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||
$R = 6371000;
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLng = deg2rad($lng2 - $lng1);
|
||||
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
|
||||
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI: Hitung Kategori Kemiskinan Otomatis
|
||||
// ============================================================
|
||||
function hitungKategori(?float $penghasilan): string {
|
||||
if ($penghasilan === null || $penghasilan < BATAS_SANGAT_MISKIN) return 'sangat_miskin';
|
||||
if ($penghasilan < BATAS_MISKIN) return 'miskin';
|
||||
if ($penghasilan < BATAS_HAMPIR_MISKIN) return 'hampir_miskin';
|
||||
return 'mampu';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI: Assign Rumah Ibadah (kompatibel v1.1)
|
||||
// ============================================================
|
||||
function assignRumahIbadah(int $pendudukId): void {
|
||||
try {
|
||||
$db = getDB();
|
||||
$p = $db->prepare("SELECT lat, lng FROM penduduk_miskin WHERE id = ?");
|
||||
$p->execute([$pendudukId]);
|
||||
$row = $p->fetch();
|
||||
if (!$row) return;
|
||||
|
||||
$ibadahList = $db->query("SELECT id, lat, lng, radius FROM rumah_ibadah")->fetchAll();
|
||||
$closest = null;
|
||||
$closestDist = PHP_FLOAT_MAX;
|
||||
|
||||
foreach ($ibadahList as $ri) {
|
||||
$dist = haversine((float)$row['lat'], (float)$row['lng'], (float)$ri['lat'], (float)$ri['lng']);
|
||||
if ($dist <= $ri['radius'] && $dist < $closestDist) {
|
||||
$closest = $ri['id'];
|
||||
$closestDist = $dist;
|
||||
}
|
||||
}
|
||||
|
||||
$db->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id = ?, jarak_ke_ibadah = ? WHERE id = ?")
|
||||
->execute([$closest, $closest ? round($closestDist, 2) : null, $pendudukId]);
|
||||
} catch (\Exception $e) { /* abaikan error assign ibadah */ }
|
||||
}
|
||||
|
||||
function reassignAll(): void {
|
||||
$db = getDB();
|
||||
$all = $db->query("SELECT id FROM penduduk_miskin")->fetchAll();
|
||||
foreach ($all as $row) assignRumahIbadah((int)$row['id']);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI: Catat Riwayat Verifikasi
|
||||
// ============================================================
|
||||
function catatRiwayat(int $pendudukId, int $userId, string $aksi, ?string $catatan = null, ?array $dataLama = null): void {
|
||||
$db = getDB();
|
||||
$db->prepare("
|
||||
INSERT INTO riwayat_verifikasi (penduduk_id, user_id, aksi, catatan, data_lama)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
")->execute([$pendudukId, $userId, $aksi, $catatan, $dataLama ? json_encode($dataLama, JSON_UNESCAPED_UNICODE) : null]);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/config.php';
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
try {
|
||||
$db = getDB();
|
||||
|
||||
// 1. Update ENUM to include 'mampu'
|
||||
echo "Updating ENUM for kategori_kemiskinan...\n";
|
||||
$db->exec("ALTER TABLE penduduk_miskin MODIFY kategori_kemiskinan ENUM('sangat_miskin','miskin','hampir_miskin','mampu') DEFAULT 'sangat_miskin'");
|
||||
echo "ENUM updated successfully!\n\n";
|
||||
|
||||
// 2. Re-calculate all existing records
|
||||
echo "Re-calculating categories for existing records...\n";
|
||||
$stmt = $db->query("SELECT id, penghasilan_bulanan FROM penduduk_miskin");
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$update = $db->prepare("UPDATE penduduk_miskin SET kategori_kemiskinan = ? WHERE id = ?");
|
||||
$counts = ['sangat_miskin' => 0, 'miskin' => 0, 'hampir_miskin' => 0, 'mampu' => 0];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$p = $row['penghasilan_bulanan'] !== null ? (float)$row['penghasilan_bulanan'] : null;
|
||||
if ($p === null || $p < BATAS_SANGAT_MISKIN) $kat = 'sangat_miskin';
|
||||
elseif ($p < BATAS_MISKIN) $kat = 'miskin';
|
||||
elseif ($p < BATAS_HAMPIR_MISKIN) $kat = 'hampir_miskin';
|
||||
else $kat = 'mampu';
|
||||
$update->execute([$kat, $row['id']]);
|
||||
$counts[$kat]++;
|
||||
}
|
||||
|
||||
echo "Done! Updated " . count($rows) . " records:\n";
|
||||
foreach ($counts as $kat => $c) {
|
||||
echo " $kat: $c\n";
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
echo "ERROR: " . $e->getMessage() . "\n";
|
||||
}
|
||||
Reference in New Issue
Block a user