This commit is contained in:
2026-05-12 10:16:16 +07:00
parent bae0569701
commit c2efc4414f
37 changed files with 2222 additions and 18 deletions
+30
View File
@@ -37,6 +37,36 @@ $statements = [
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_penduduk_coords (latitude, longitude),
INDEX idx_penduduk_rumah_ibadah (rumah_ibadah_id)
)",
"CREATE TABLE IF NOT EXISTS verifikasi_lapangan (
id INT AUTO_INCREMENT PRIMARY KEY,
jenis_target VARCHAR(50) NOT NULL DEFAULT 'penduduk_miskin',
target_nama VARCHAR(255) DEFAULT NULL,
petugas_nama VARCHAR(255) NOT NULL,
status_verifikasi VARCHAR(30) NOT NULL DEFAULT 'menunggu',
hasil_temuan TEXT,
tindak_lanjut TEXT,
foto VARCHAR(255) DEFAULT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_verifikasi_coords (latitude, longitude),
INDEX idx_verifikasi_target (jenis_target, target_nama)
)",
"CREATE TABLE IF NOT EXISTS aduan_warga (
id INT AUTO_INCREMENT PRIMARY KEY,
kategori VARCHAR(100) NOT NULL,
pelapor_nama VARCHAR(255) NOT NULL,
kontak_pelapor VARCHAR(100) DEFAULT NULL,
deskripsi TEXT NOT NULL,
status_tindak_lanjut VARCHAR(30) NOT NULL DEFAULT 'baru',
tindak_lanjut TEXT,
foto VARCHAR(255) DEFAULT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_aduan_coords (latitude, longitude),
INDEX idx_aduan_status (status_tindak_lanjut)
)"
];
+38
View File
@@ -0,0 +1,38 @@
<?php
header('Content-Type: application/json');
include 'db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID aduan tidak valid']);
$conn->close();
exit;
}
$stmt = $conn->prepare('DELETE FROM aduan_warga WHERE id = ?');
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Aduan warga berhasil dihapus']);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>
+38
View File
@@ -0,0 +1,38 @@
<?php
header('Content-Type: application/json');
include 'db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID verifikasi tidak valid']);
$conn->close();
exit;
}
$stmt = $conn->prepare('DELETE FROM verifikasi_lapangan WHERE id = ?');
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Verifikasi lapangan berhasil dihapus']);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>
+3
View File
@@ -0,0 +1,3 @@
<?php
header('Content-Type: application/json');
echo json_encode(['success'=>true,'msg'=>'API placeholder untuk fitur jalan/parsel']);
+46
View File
@@ -0,0 +1,46 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? '';
if (empty($id)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID wajib diisi']);
exit;
}
// Ambil data gambar untuk dihapus
$stmt = $conn->prepare("SELECT gambar FROM jalan_rusak WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
if ($row && $row['gambar'] && file_exists(__DIR__ . '/../' . $row['gambar'])) {
unlink(__DIR__ . '/../' . $row['gambar']);
}
$stmt->close();
// Hapus record
$stmt = $conn->prepare("DELETE FROM jalan_rusak WHERE id = ?");
$stmt->bind_param("i", $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Laporan jalan rusak berhasil dihapus']);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>
+29
View File
@@ -0,0 +1,29 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
$sql = "SELECT id, latitude, longitude, jenis_kerusakan, deskripsi, severity, gambar, created_at
FROM jalan_rusak
ORDER BY created_at DESC";
$result = $conn->query($sql);
$data = [];
if (!$result) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$row['latitude'] = (float)$row['latitude'];
$row['longitude'] = (float)$row['longitude'];
$data[] = $row;
}
}
echo json_encode($data);
$conn->close();
?>
+49
View File
@@ -0,0 +1,49 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fitur Jalan & Parsel</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<style>html,body,#map{height:100%;margin:0} .controls{position:absolute;top:8px;left:8px;z-index:400;background:#fff;padding:6px}</style>
</head>
<body>
<div class="controls">
<label>Tipe Jalan: <select id="tipe"><option value="nasional">Jalan Nasional</option><option value="provinsi">Jalan Provinsi</option><option value="kabupaten">Jalan Kabupaten</option></select></label>
</div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
<script>
const map = L.map('map').setView([-0.03,109.34],13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
const drawnItems = new L.FeatureGroup().addTo(map);
const drawControl = new L.Control.Draw({ edit: { featureGroup: drawnItems } });
map.addControl(drawControl);
function styleByRoad(type){
if(type==='nasional') return {color:'#d62728',weight:5};
if(type==='provinsi') return {color:'#ff7f0e',weight:4};
return {color:'#2ca02c',weight:3};
}
map.on(L.Draw.Event.CREATED, function (e) {
const layer = e.layer;
if (layer instanceof L.Polyline && !(layer instanceof L.Polygon)){
const tipe = document.getElementById('tipe').value;
layer.setStyle(styleByRoad(tipe));
layer.properties = {type:'road', road_type:tipe};
} else if (layer instanceof L.Polygon){
const geo = layer.toGeoJSON();
const area = turf.area(geo); // in square meters
layer.properties = {type:'parsel', luas_m2: Math.round(area)};
layer.bindPopup('Parsel — Luas: '+Math.round(area)+' m²');
}
drawnItems.addLayer(layer);
});
</script>
</body>
</html>
+72
View File
@@ -0,0 +1,72 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
$jenis_kerusakan = $_POST['jenis_kerusakan'] ?? '';
$deskripsi = $_POST['deskripsi'] ?? '';
$severity = $_POST['severity'] ?? 'sedang';
if (empty($latitude) || empty($longitude) || empty($jenis_kerusakan)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Latitude, longitude, dan jenis kerusakan wajib diisi']);
exit;
}
$gambar = null;
// Handle file upload
if (isset($_FILES['gambar']) && $_FILES['gambar']['error'] === UPLOAD_ERR_OK) {
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$filename = $_FILES['gambar']['name'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Format gambar hanya JPG, PNG, GIF']);
exit;
}
// Buat folder uploads jika belum ada
if (!is_dir(__DIR__ . '/../uploads')) {
mkdir(__DIR__ . '/../uploads', 0755, true);
}
// Generate nama file unik
$gambar = 'uploads/' . time() . '_' . basename($filename);
if (!move_uploaded_file($_FILES['gambar']['tmp_name'], __DIR__ . '/../' . $gambar)) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Gagal upload gambar']);
exit;
}
}
$stmt = $conn->prepare("INSERT INTO jalan_rusak (latitude, longitude, jenis_kerusakan, deskripsi, severity, gambar) VALUES (?, ?, ?, ?, ?, ?)");
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
exit;
}
$stmt->bind_param("ddssss", $latitude, $longitude, $jenis_kerusakan, $deskripsi, $severity, $gambar);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Laporan jalan rusak berhasil disimpan', 'id' => $stmt->insert_id]);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json');
include 'db_config.php';
$sql = 'SELECT id, kategori, pelapor_nama, kontak_pelapor, deskripsi, status_tindak_lanjut, tindak_lanjut, foto, latitude, longitude, created_at FROM aduan_warga ORDER BY created_at DESC';
$result = $conn->query($sql);
if (!$result) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
$data = [];
while ($row = $result->fetch_assoc()) {
$row['latitude'] = (float)$row['latitude'];
$row['longitude'] = (float)$row['longitude'];
$data[] = $row;
}
echo json_encode($data);
$conn->close();
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json');
include 'db_config.php';
$sql = 'SELECT id, jenis_target, target_nama, petugas_nama, status_verifikasi, hasil_temuan, tindak_lanjut, foto, latitude, longitude, created_at FROM verifikasi_lapangan ORDER BY created_at DESC';
$result = $conn->query($sql);
if (!$result) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
$data = [];
while ($row = $result->fetch_assoc()) {
$row['latitude'] = (float)$row['latitude'];
$row['longitude'] = (float)$row['longitude'];
$data[] = $row;
}
echo json_encode($data);
$conn->close();
?>
+17
View File
@@ -0,0 +1,17 @@
// Simple loader to make Leaflet resources available globally to pages
(function(){
if(window._leafletLoaderRan) return; window._leafletLoaderRan = true;
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(link);
var script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.defer = false;
script.onload = function(){
console.log('Leaflet loaded');
var ev = document.createEvent('Event'); ev.initEvent('leaflet:loaded', true, true); window.dispatchEvent(ev);
};
document.head.appendChild(script);
})();
+748 -18
View File
@@ -155,6 +155,12 @@
<button class="icon-btn" id="modeRoadDamageBtn" title="Lapor jalan rusak">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 12h12M12 6v12M2 12h20M12 2v20"/><circle cx="12" cy="12" r="10"/></svg>
</button>
<button class="icon-btn" id="modeVerifikasiBtn" title="Verifikasi lapangan">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"/><path d="M4 4h16v16H4z"/></svg>
</button>
<button class="icon-btn" id="modeAduanWargaBtn" title="Aduan warga">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 8h10"/><path d="M7 12h6"/><path d="M20 16.5A2.5 2.5 0 0 1 17.5 19H8l-4 3V6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5z"/></svg>
</button>
<button class="icon-btn" id="modeChoroplethBtn" title="Lihat peta choropleth kecamatan">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6l6-2 6 2 6-2v14l-6 2-6-2-6 2z"/><path d="M9 4v14"/><path d="M15 6v14"/></svg>
</button>
@@ -251,6 +257,12 @@
<button class="icon-btn" id="miniModePendudukMiskinBtn" title="Input penduduk miskin">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 12a4 4 0 1 0-4-4 4 4 0 0 0 4 4z"/><path d="M4 21a8 8 0 0 1 16 0"/><path d="M18 8h4"/><path d="M20 6v4"/></svg>
</button>
<button class="icon-btn" id="miniModeVerifikasiBtn" title="Verifikasi lapangan">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"/><path d="M4 4h16v16H4z"/></svg>
</button>
<button class="icon-btn" id="miniModeAduanWargaBtn" title="Aduan warga">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 8h10"/><path d="M7 12h6"/><path d="M20 16.5A2.5 2.5 0 0 1 17.5 19H8l-4 3V6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5z"/></svg>
</button>
<button class="icon-btn" id="toggleRegistryDrawerBtn" title="Buka registry & filter">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="7" y1="12" x2="17" y2="12"/><line x1="10" y1="18" x2="14" y2="18"/></svg>
</button>
@@ -401,6 +413,14 @@
<input id="layerToggle-pendudukMiskinLayer" type="checkbox" class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
<span>Penduduk Miskin</span>
</label>
<label class="flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<input id="layerToggle-verifikasiLapanganLayer" type="checkbox" class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
<span>Verifikasi Lapangan</span>
</label>
<label class="flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<input id="layerToggle-aduanWargaLayer" type="checkbox" class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
<span>Aduan Warga</span>
</label>
<label class="flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
<input id="layerToggle-roadNetworkLayer" type="checkbox" class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
<span>Jalan Eksternal</span>
@@ -644,6 +664,128 @@
</div>
</div>
<div id="verifikasiLapanganModal" class="fixed inset-0 z-[10005] hidden items-center justify-center bg-slate-900/50 px-4">
<div class="w-full max-w-xl rounded-2xl bg-white p-5 shadow-2xl">
<div class="flex items-start justify-between gap-4 border-b pb-3">
<div>
<h3 class="text-lg font-bold text-slate-800">Verifikasi Lapangan</h3>
<p class="text-sm text-slate-500">Catat hasil cek lapangan untuk data kemiskinan atau infrastruktur.</p>
</div>
<button id="closeVerifikasiLapanganModalBtn" class="rounded-lg bg-slate-100 px-3 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Tutup</button>
</div>
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<input id="verifikasiLapanganId" type="hidden">
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Target Data</label>
<select id="verifikasiJenisTarget" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-emerald-500">
<option value="penduduk_miskin">Penduduk Miskin</option>
<option value="jalan_rusak">Jalan Rusak</option>
<option value="rumah_ibadah">Rumah Ibadah</option>
<option value="lainnya">Lainnya</option>
</select>
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Nama Target</label>
<input id="verifikasiTargetNama" type="text" placeholder="Contoh: KK A / Jalan RW 03" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-emerald-500">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Nama Petugas</label>
<input id="verifikasiPetugasNama" type="text" placeholder="Nama surveyor" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-emerald-500">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Status</label>
<select id="verifikasiStatus" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-emerald-500">
<option value="menunggu">Menunggu</option>
<option value="valid">Valid</option>
<option value="perlu_tindak_lanjut">Perlu Tindak Lanjut</option>
</select>
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Hasil Temuan</label>
<textarea id="verifikasiHasilTemuan" rows="3" placeholder="Ringkasan hasil cek lapangan" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-emerald-500"></textarea>
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Tindak Lanjut</label>
<textarea id="verifikasiTindakLanjut" rows="2" placeholder="Catatan tindak lanjut" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-emerald-500"></textarea>
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Foto (Opsional)</label>
<input id="verifikasiFoto" type="file" accept="image/*" class="w-full rounded-lg border border-slate-300 p-2 text-sm">
</div>
<div class="sm:col-span-2 text-[10px] text-slate-500 bg-slate-50 p-2 rounded">
📍 Koordinat: <span id="verifikasiCoords">-</span>
</div>
</div>
<div class="mt-5 flex flex-wrap gap-2 border-t pt-4">
<button id="saveVerifikasiLapanganBtn" class="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-bold text-white hover:bg-emerald-700">Simpan Verifikasi</button>
<button id="cancelVerifikasiLapanganBtn" class="rounded-lg bg-slate-100 px-4 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Batal</button>
</div>
</div>
</div>
<div id="aduanWargaModal" class="fixed inset-0 z-[10006] hidden items-center justify-center bg-slate-900/50 px-4">
<div class="w-full max-w-xl rounded-2xl bg-white p-5 shadow-2xl">
<div class="flex items-start justify-between gap-4 border-b pb-3">
<div>
<h3 class="text-lg font-bold text-slate-800">Aduan Warga</h3>
<p class="text-sm text-slate-500">Terima aduan berbasis lokasi untuk memprioritaskan respon lapangan.</p>
</div>
<button id="closeAduanWargaModalBtn" class="rounded-lg bg-slate-100 px-3 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Tutup</button>
</div>
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<input id="aduanWargaId" type="hidden">
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Kategori</label>
<select id="aduanKategori" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-rose-500">
<option value="">Pilih kategori</option>
<option value="jalan rusak">Jalan Rusak</option>
<option value="banjir">Banjir</option>
<option value="rumah tidak layak">Rumah Tidak Layak</option>
<option value="air bersih">Air Bersih</option>
<option value="sanitasi">Sanitasi</option>
<option value="akses bantuan">Akses Bantuan</option>
<option value="lainnya">Lainnya</option>
</select>
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Nama Pelapor</label>
<input id="aduanPelaporNama" type="text" placeholder="Nama warga" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-rose-500">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Kontak</label>
<input id="aduanKontakPelapor" type="text" placeholder="Nomor HP / WA" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-rose-500">
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Status</label>
<select id="aduanStatus" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-rose-500">
<option value="baru">Baru</option>
<option value="diproses">Diproses</option>
<option value="selesai">Selesai</option>
</select>
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Deskripsi Aduan</label>
<textarea id="aduanDeskripsi" rows="3" placeholder="Jelaskan kondisi yang diadukan" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-rose-500"></textarea>
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Tindak Lanjut</label>
<textarea id="aduanTindakLanjut" rows="2" placeholder="Catatan respon / tindak lanjut" class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-rose-500"></textarea>
</div>
<div class="sm:col-span-2">
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Foto (Opsional)</label>
<input id="aduanFoto" type="file" accept="image/*" class="w-full rounded-lg border border-slate-300 p-2 text-sm">
</div>
<div class="sm:col-span-2 text-[10px] text-slate-500 bg-slate-50 p-2 rounded">
📍 Koordinat: <span id="aduanCoords">-</span>
</div>
</div>
<div class="mt-5 flex flex-wrap gap-2 border-t pt-4">
<button id="saveAduanWargaBtn" class="rounded-lg bg-rose-600 px-4 py-2 text-sm font-bold text-white hover:bg-rose-700">Simpan Aduan</button>
<button id="cancelAduanWargaBtn" class="rounded-lg bg-slate-100 px-4 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Batal</button>
</div>
</div>
</div>
<select id="featureType" class="hidden">
<option value="polyline">polyline</option>
<option value="polygon">polygon</option>
@@ -687,12 +829,20 @@
let utilityControl = null;
let rumahIbadahLayer = L.featureGroup().addTo(map);
let pendudukMiskinLayer = L.featureGroup().addTo(map);
let verifikasiLapanganLayer = L.featureGroup().addTo(map);
let aduanWargaLayer = L.featureGroup().addTo(map);
let rumahIbadahLayerMap = {};
let pendudukMiskinLayerMap = {};
let verifikasiLapanganLayerMap = {};
let aduanWargaLayerMap = {};
let rumahIbadahRecords = [];
let pendudukMiskinRecords = [];
let verifikasiLapanganRecords = [];
let aduanWargaRecords = [];
let pendingRumahIbadahCoords = null;
let pendingPendudukMiskinCoords = null;
let pendingVerifikasiCoords = null;
let pendingAduanCoords = null;
let editingRumahIbadahId = null;
let selectedRumahIbadahId = null;
let selectedBantuanRumahIbadahId = null;
@@ -1151,7 +1301,29 @@
longitude: Number(record.longitude)
}));
return [...pointRecords, ...spatialRecords, ...roadDamageRegistryRecords, ...rumahIbadahRegistryRecords, ...pendudukMiskinRegistryRecords].sort((left, right) => {
const verifikasiRegistryRecords = verifikasiLapanganRecords.map(record => ({
id: Number(record.id),
type: 'verifikasiLapangan',
title: record.target_nama || 'Verifikasi lapangan',
subtitle: record.petugas_nama || '-',
status: record.status_verifikasi || 'menunggu',
created_at: record.created_at || null,
latitude: Number(record.latitude),
longitude: Number(record.longitude)
}));
const aduanRegistryRecords = aduanWargaRecords.map(record => ({
id: Number(record.id),
type: 'aduanWarga',
title: record.kategori || 'Aduan warga',
subtitle: record.pelapor_nama || '-',
status: record.status_tindak_lanjut || 'baru',
created_at: record.created_at || null,
latitude: Number(record.latitude),
longitude: Number(record.longitude)
}));
return [...pointRecords, ...spatialRecords, ...roadDamageRegistryRecords, ...rumahIbadahRegistryRecords, ...pendudukMiskinRegistryRecords, ...verifikasiRegistryRecords, ...aduanRegistryRecords].sort((left, right) => {
const leftDate = parseDateOnly(left.created_at);
const rightDate = parseDateOnly(right.created_at);
@@ -1169,6 +1341,8 @@
if (type === 'roadDamage') return 'Jalan Rusak';
if (type === 'rumahIbadah') return 'Rumah Ibadah';
if (type === 'pendudukMiskin') return 'Penduduk Miskin';
if (type === 'verifikasiLapangan') return 'Verifikasi Lapangan';
if (type === 'aduanWarga') return 'Aduan Warga';
return 'Data';
}
@@ -1179,6 +1353,8 @@
if (type === 'roadDamage') return 'bg-red-100 text-red-700';
if (type === 'rumahIbadah') return 'bg-teal-100 text-teal-700';
if (type === 'pendudukMiskin') return 'bg-rose-100 text-rose-700';
if (type === 'verifikasiLapangan') return 'bg-emerald-100 text-emerald-700';
if (type === 'aduanWarga') return 'bg-fuchsia-100 text-fuchsia-700';
return 'bg-slate-100 text-slate-700';
}
@@ -1228,6 +1404,22 @@
return;
}
if (type === 'verifikasiLapangan') {
const marker = verifikasiLapanganLayerMap[id];
if (!marker) return;
map.setView(marker.getLatLng(), Math.max(map.getZoom(), 16));
marker.openPopup();
return;
}
if (type === 'aduanWarga') {
const marker = aduanWargaLayerMap[id];
if (!marker) return;
map.setView(marker.getLatLng(), Math.max(map.getZoom(), 16));
marker.openPopup();
return;
}
const layer = featureLayerMap[id];
if (!layer) return;
if (layer.getBounds) {
@@ -1267,7 +1459,11 @@
? `PIC: ${record.subtitle}`
: record.type === 'pendudukMiskin'
? `Anggota: ${record.subtitle}`
: `Kategori: ${record.subtitle}`;
: record.type === 'verifikasiLapangan'
? `Petugas: ${record.subtitle}`
: record.type === 'aduanWarga'
? `Pelapor: ${record.subtitle}`
: `Kategori: ${record.subtitle}`;
const statusLabel = record.type === 'point'
? `24 Jam: ${record.status}`
: record.type === 'roadDamage'
@@ -1276,7 +1472,11 @@
? `Radius: ${record.status}`
: record.type === 'pendudukMiskin'
? `Rumah Ibadah: ${record.status}`
: `Status: ${record.status}`;
: record.type === 'verifikasiLapangan'
? `Status Verifikasi: ${record.status}`
: record.type === 'aduanWarga'
? `Status Tindak Lanjut: ${record.status}`
: `Status: ${record.status}`;
return `
<button type="button" onclick="focusRegistryItem('${record.type}', ${record.id})" class="registry-item w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-left shadow-sm">
@@ -1342,6 +1542,8 @@
roadDamageLayer: document.getElementById('layerToggle-roadDamageLayer'),
rumahIbadahLayer: document.getElementById('layerToggle-rumahIbadahLayer'),
pendudukMiskinLayer: document.getElementById('layerToggle-pendudukMiskinLayer'),
verifikasiLapanganLayer: document.getElementById('layerToggle-verifikasiLapanganLayer'),
aduanWargaLayer: document.getElementById('layerToggle-aduanWargaLayer'),
roadNetworkLayer: document.getElementById('layerToggle-roadNetworkLayer')
}
};
@@ -1356,6 +1558,8 @@
{ key: 'roadDamageLayer', layer: roadDamageLayer, label: 'Jalan Rusak' },
{ key: 'rumahIbadahLayer', layer: rumahIbadahLayer, label: 'Rumah Ibadah' },
{ key: 'pendudukMiskinLayer', layer: pendudukMiskinLayer, label: 'Penduduk Miskin' },
{ key: 'verifikasiLapanganLayer', layer: verifikasiLapanganLayer, label: 'Verifikasi Lapangan' },
{ key: 'aduanWargaLayer', layer: aduanWargaLayer, label: 'Aduan Warga' },
{ key: 'roadNetworkLayer', layer: roadNetworkLayer, label: 'Jalan Eksternal' }
];
}
@@ -1503,6 +1707,11 @@
const ui = getBantuanUi();
if (!ui.panel || !ui.list || !ui.count || !ui.rumahFilter || !ui.statusFilter || !ui.search) return;
const existingSummary = ui.panel.querySelector('[data-bantuan-summary="1"]');
if (existingSummary) {
existingSummary.remove();
}
const previousValue = ui.rumahFilter.value;
const options = ['<option value="">Pilih rumah ibadah...</option>'].concat(
rumahIbadahRecords.map(record => `<option value="${Number(record.id)}">${record.nama} (radius ${Number(record.radius_m || 0).toLocaleString('id-ID')} m)</option>`)
@@ -1545,8 +1754,34 @@
return true;
});
const statusCounts = residents.reduce((accumulator, item) => {
const status = normalizeAidStatus(item.bantuan_status);
accumulator[status] = (accumulator[status] || 0) + 1;
return accumulator;
}, { belum: 0, proses: 0, sudah: 0 });
ui.count.innerText = String(residents.length);
const summaryWrapper = document.createElement('div');
summaryWrapper.setAttribute('data-bantuan-summary', '1');
summaryWrapper.className = 'mt-3 grid grid-cols-3 gap-2 text-[11px] text-slate-600';
summaryWrapper.innerHTML = `
<div class="rounded-lg bg-rose-50 px-2 py-2 text-center">
<div class="font-black text-rose-700">${statusCounts.belum || 0}</div>
<div>Belum</div>
</div>
<div class="rounded-lg bg-amber-50 px-2 py-2 text-center">
<div class="font-black text-amber-700">${statusCounts.proses || 0}</div>
<div>Diproses</div>
</div>
<div class="rounded-lg bg-emerald-50 px-2 py-2 text-center">
<div class="font-black text-emerald-700">${statusCounts.sudah || 0}</div>
<div>Sudah</div>
</div>
`;
ui.list.parentNode.insertBefore(summaryWrapper, ui.list);
if (!residents.length) {
ui.list.innerHTML = `
<div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-sm text-slate-500">
@@ -1681,11 +1916,15 @@
pendudukMiskin: document.getElementById('modePendudukMiskinBtn'),
choropleth: document.getElementById('modeChoroplethBtn'),
roadDamage: document.getElementById('leafletRoadDamageBtn') || document.getElementById('modeRoadDamageBtn'),
verifikasiLapangan: document.getElementById('leafletVerifikasiBtn') || document.getElementById('modeVerifikasiBtn'),
aduanWarga: document.getElementById('leafletAduanWargaBtn') || document.getElementById('modeAduanWargaBtn'),
miniPoint: document.getElementById('miniModePointBtn'),
miniPolyline: document.getElementById('miniModePolylineBtn'),
miniPolygon: document.getElementById('miniModePolygonBtn'),
miniRumahIbadah: document.getElementById('miniModeRumahIbadahBtn'),
miniPendudukMiskin: document.getElementById('miniModePendudukMiskinBtn')
miniPendudukMiskin: document.getElementById('miniModePendudukMiskinBtn'),
miniVerifikasiLapangan: document.getElementById('miniModeVerifikasiBtn'),
miniAduanWarga: document.getElementById('miniModeAduanWargaBtn')
};
Object.values(modeButtons).forEach(button => {
@@ -1711,6 +1950,14 @@
if (modeButtons.pendudukMiskin) modeButtons.pendudukMiskin.classList.add('active');
if (modeButtons.miniPendudukMiskin) modeButtons.miniPendudukMiskin.classList.add('active');
}
if (mode === 'verifikasiLapangan') {
if (modeButtons.verifikasiLapangan) modeButtons.verifikasiLapangan.classList.add('active');
if (modeButtons.miniVerifikasiLapangan) modeButtons.miniVerifikasiLapangan.classList.add('active');
}
if (mode === 'aduanWarga') {
if (modeButtons.aduanWarga) modeButtons.aduanWarga.classList.add('active');
if (modeButtons.miniAduanWarga) modeButtons.miniAduanWarga.classList.add('active');
}
if (modeButtons[mode]) {
modeButtons[mode].classList.add('active');
}
@@ -1737,14 +1984,16 @@
return;
}
if (mode === 'roadDamage') {
if (mode === 'roadDamage' || mode === 'verifikasiLapangan' || mode === 'aduanWarga') {
spatialControls.classList.add('hidden');
runActionBtn.innerText = 'Klik Peta';
runActionBtn.disabled = true;
clearDraftBtn.disabled = true;
updateDrawControl(null);
document.getElementById('modeMeasure').value = '';
loadRoadDamage(true);
if (mode === 'roadDamage') loadRoadDamage(true);
if (mode === 'verifikasiLapangan') loadVerifikasiLapangan(true);
if (mode === 'aduanWarga') loadAduanWarga(true);
return;
}
@@ -2376,6 +2625,10 @@
<button onclick="deletePendudukMiskin(${record.id})" class="rounded-lg bg-red-50 px-3 py-2 text-xs font-bold text-red-700 hover:bg-red-100">Hapus</button>
<button onclick="focusRegistryItem('rumahIbadah', ${record.rumah_ibadah_id || 0})" class="rounded-lg bg-emerald-50 px-3 py-2 text-xs font-bold text-emerald-700 hover:bg-emerald-100">Lihat Rumah Ibadah</button>
</div>
<div class="mt-2 grid grid-cols-2 gap-2">
<button onclick="openVerifikasiFromPenduduk(${record.id})" class="rounded-lg bg-emerald-50 px-3 py-2 text-xs font-bold text-emerald-700 hover:bg-emerald-100">Verifikasi</button>
<button onclick="setDataMode('aduanWarga'); openAduanWargaModal(${Number(record.latitude)}, ${Number(record.longitude)})" class="rounded-lg bg-fuchsia-50 px-3 py-2 text-xs font-bold text-fuchsia-700 hover:bg-fuchsia-100">Buat Aduan</button>
</div>
</div>
`;
}
@@ -2459,6 +2712,329 @@
showMessage(`Menampilkan ${withinRadius.length} lokasi miskin terdekat`, 'bg-emerald-600 text-white');
}
function getAidStatusColor(status) {
const normalized = normalizeAidStatus(status);
if (normalized === 'sudah') return '#10b981';
if (normalized === 'proses') return '#f59e0b';
return '#dc2626';
}
function createPendudukMiskinIcon(status) {
const color = getAidStatusColor(status);
return L.divIcon({
className: 'custom-marker',
html: `<svg width="32" height="40" viewBox="0 0 24 24" fill="${color}" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="8" r="4"/><path d="M4 22a8 8 0 0 1 16 0"/></svg>`,
iconSize: [32, 40],
iconAnchor: [16, 40],
popupAnchor: [0, -40]
});
}
function getVerifikasiStatusBadge(status) {
const value = String(status || 'menunggu').toLowerCase().trim();
if (value === 'valid') return { label: 'Valid', className: 'bg-emerald-100 text-emerald-700' };
if (value === 'perlu_tindak_lanjut') return { label: 'Perlu Tindak Lanjut', className: 'bg-amber-100 text-amber-700' };
return { label: 'Menunggu', className: 'bg-slate-100 text-slate-700' };
}
function getVerifikasiIcon(status) {
const value = String(status || 'menunggu').toLowerCase().trim();
const color = value === 'valid' ? '#10b981' : value === 'perlu_tindak_lanjut' ? '#f59e0b' : '#64748b';
return L.divIcon({
className: 'custom-marker',
html: `<svg width="32" height="40" viewBox="0 0 24 24" fill="${color}" xmlns="http://www.w3.org/2000/svg"><path d="M12 0 3 4v6c0 6.2 4.2 11.4 9 14 4.8-2.6 9-7.8 9-14V4z"/><path d="M9.5 11.5 11 13l3.5-4" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
iconSize: [32, 40],
iconAnchor: [16, 40],
popupAnchor: [0, -40]
});
}
function getAduanStatusBadge(status) {
const value = String(status || 'baru').toLowerCase().trim();
if (value === 'selesai') return { label: 'Selesai', className: 'bg-emerald-100 text-emerald-700' };
if (value === 'diproses') return { label: 'Diproses', className: 'bg-amber-100 text-amber-700' };
return { label: 'Baru', className: 'bg-rose-100 text-rose-700' };
}
function getAduanIcon(status) {
const value = String(status || 'baru').toLowerCase().trim();
const color = value === 'selesai' ? '#10b981' : value === 'diproses' ? '#f59e0b' : '#e11d48';
return L.divIcon({
className: 'custom-marker',
html: `<svg width="32" height="40" viewBox="0 0 24 24" fill="${color}" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c-5 0-9 3.6-9 8 0 3.1 2.1 5.8 5.2 7l-.7 4 4.5-2.4c.3 0 .6.1 1 .1 5 0 9-3.6 9-8s-4-8.7-9-8.7z"/><circle cx="9" cy="10" r="1" fill="white"/><circle cx="12" cy="10" r="1" fill="white"/><circle cx="15" cy="10" r="1" fill="white"/></svg>`,
iconSize: [32, 40],
iconAnchor: [16, 40],
popupAnchor: [0, -40]
});
}
function buildVerifikasiLapanganPopup(record) {
const badge = getVerifikasiStatusBadge(record.status_verifikasi);
const fotoHtml = record.foto ? `<img src="${record.foto}" alt="Foto verifikasi" class="mt-2 w-full rounded-lg max-h-48 object-cover">` : '';
return `
<div class="w-full p-4">
<h3 class="text-lg font-bold text-slate-800">Verifikasi Lapangan</h3>
<p class="mt-1 text-sm text-slate-600">Target: ${record.target_nama || '-'} (${record.jenis_target || '-'})</p>
<p class="mt-1 text-sm text-slate-600">Petugas: ${record.petugas_nama || '-'}</p>
<div class="mt-2">
<span class="registry-chip ${badge.className}">${badge.label}</span>
</div>
${record.hasil_temuan ? `<p class="mt-2 text-sm text-slate-600"><strong>Temuan:</strong> ${record.hasil_temuan}</p>` : ''}
${record.tindak_lanjut ? `<p class="mt-1 text-sm text-slate-600"><strong>Tindak lanjut:</strong> ${record.tindak_lanjut}</p>` : ''}
<p class="mt-1 text-xs text-slate-500">📍 ${Number(record.latitude).toFixed(5)}, ${Number(record.longitude).toFixed(5)}</p>
${fotoHtml}
<div class="mt-3 grid grid-cols-2 gap-2 border-t pt-3">
<button onclick="openVerifikasiLapanganEditor(${record.id})" class="rounded-lg bg-indigo-50 px-3 py-2 text-xs font-bold text-indigo-700 hover:bg-indigo-100">Edit</button>
<button onclick="deleteVerifikasiLapangan(${record.id})" class="rounded-lg bg-red-50 px-3 py-2 text-xs font-bold text-red-700 hover:bg-red-100">Hapus</button>
</div>
</div>
`;
}
function openVerifikasiLapanganModal(lat, lng, record = null) {
pendingVerifikasiCoords = { lat, lng };
document.getElementById('verifikasiLapanganId').value = record ? String(record.id) : '';
document.getElementById('verifikasiJenisTarget').value = record ? (record.jenis_target || 'penduduk_miskin') : 'penduduk_miskin';
document.getElementById('verifikasiTargetNama').value = record ? (record.target_nama || '') : '';
document.getElementById('verifikasiPetugasNama').value = record ? (record.petugas_nama || '') : '';
document.getElementById('verifikasiStatus').value = record ? (record.status_verifikasi || 'menunggu') : 'menunggu';
document.getElementById('verifikasiHasilTemuan').value = record ? (record.hasil_temuan || '') : '';
document.getElementById('verifikasiTindakLanjut').value = record ? (record.tindak_lanjut || '') : '';
document.getElementById('verifikasiCoords').innerText = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
document.getElementById('verifikasiFoto').value = '';
const modal = document.getElementById('verifikasiLapanganModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
suppressAutoReload = true;
}
function closeVerifikasiLapanganModal() {
const modal = document.getElementById('verifikasiLapanganModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
pendingVerifikasiCoords = null;
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)') || !!editingFeatureId;
}
function saveVerifikasiLapangan() {
if (!pendingVerifikasiCoords) {
showMessage('Koordinat verifikasi tidak ditemukan', 'bg-red-600 text-white');
return;
}
const id = document.getElementById('verifikasiLapanganId').value;
const formData = new FormData();
formData.append('jenis_target', document.getElementById('verifikasiJenisTarget').value);
formData.append('target_nama', document.getElementById('verifikasiTargetNama').value.trim());
formData.append('petugas_nama', document.getElementById('verifikasiPetugasNama').value.trim());
formData.append('status_verifikasi', document.getElementById('verifikasiStatus').value);
formData.append('hasil_temuan', document.getElementById('verifikasiHasilTemuan').value.trim());
formData.append('tindak_lanjut', document.getElementById('verifikasiTindakLanjut').value.trim());
formData.append('latitude', pendingVerifikasiCoords.lat);
formData.append('longitude', pendingVerifikasiCoords.lng);
const fileInput = document.getElementById('verifikasiFoto');
if (fileInput.files && fileInput.files[0]) {
formData.append('foto', fileInput.files[0]);
}
const endpoint = id ? 'update_verifikasi_lapangan.php' : 'save_verifikasi_lapangan.php';
if (id) {
formData.append('id', id);
}
fetch(endpoint, { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, endpoint))
.then(data => {
if (data.status === 'success') {
showMessage('✅ Verifikasi lapangan berhasil disimpan', 'bg-green-600 text-white');
closeVerifikasiLapanganModal();
loadVerifikasiLapangan(true);
} else {
throw new Error(data.message || 'Gagal menyimpan verifikasi');
}
})
.catch(err => showMessage(err.message || 'Gagal menyimpan verifikasi', 'bg-red-600 text-white'));
}
function deleteVerifikasiLapangan(id) {
if (!confirm('Hapus verifikasi lapangan ini?')) return;
const formData = new FormData();
formData.append('id', id);
fetch('delete_verifikasi_lapangan.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'delete_verifikasi_lapangan.php'))
.then(data => {
if (data.status === 'success') {
showMessage('🗑️ Verifikasi lapangan berhasil dihapus', 'bg-orange-600 text-white');
loadVerifikasiLapangan(true);
} else {
throw new Error(data.message || 'Gagal menghapus verifikasi');
}
})
.catch(err => showMessage(err.message || 'Gagal menghapus verifikasi', 'bg-red-600 text-white'));
}
function openVerifikasiLapanganEditor(id) {
const record = verifikasiLapanganRecords.find(item => Number(item.id) === Number(id));
if (!record) {
showMessage('Data verifikasi tidak ditemukan', 'bg-red-600 text-white');
return;
}
openVerifikasiLapanganModal(Number(record.latitude), Number(record.longitude), record);
}
function openVerifikasiFromPenduduk(id) {
const record = pendudukMiskinRecords.find(item => Number(item.id) === Number(id));
if (!record) {
showMessage('Data penduduk miskin tidak ditemukan', 'bg-red-600 text-white');
return;
}
openVerifikasiLapanganModal(Number(record.latitude), Number(record.longitude), {
jenis_target: 'penduduk_miskin',
target_nama: record.nama_ketua_kk || '',
petugas_nama: '',
status_verifikasi: 'menunggu',
hasil_temuan: '',
tindak_lanjut: ''
});
}
function buildAduanWargaPopup(record) {
const badge = getAduanStatusBadge(record.status_tindak_lanjut);
const fotoHtml = record.foto ? `<img src="${record.foto}" alt="Foto aduan" class="mt-2 w-full rounded-lg max-h-48 object-cover">` : '';
return `
<div class="w-full p-4">
<h3 class="text-lg font-bold text-slate-800">Aduan Warga</h3>
<p class="mt-1 text-sm text-slate-600">Kategori: ${record.kategori || '-'}</p>
<p class="mt-1 text-sm text-slate-600">Pelapor: ${record.pelapor_nama || '-'}</p>
<p class="mt-1 text-sm text-slate-600">Kontak: ${record.kontak_pelapor || '-'}</p>
<div class="mt-2">
<span class="registry-chip ${badge.className}">${badge.label}</span>
</div>
<p class="mt-2 text-sm text-slate-600"><strong>Deskripsi:</strong> ${record.deskripsi || '-'}</p>
${record.tindak_lanjut ? `<p class="mt-1 text-sm text-slate-600"><strong>Tindak lanjut:</strong> ${record.tindak_lanjut}</p>` : ''}
<p class="mt-1 text-xs text-slate-500">📍 ${Number(record.latitude).toFixed(5)}, ${Number(record.longitude).toFixed(5)}</p>
${fotoHtml}
<div class="mt-3 grid grid-cols-2 gap-2 border-t pt-3">
<button onclick="openAduanWargaEditor(${record.id})" class="rounded-lg bg-indigo-50 px-3 py-2 text-xs font-bold text-indigo-700 hover:bg-indigo-100">Edit</button>
<button onclick="deleteAduanWarga(${record.id})" class="rounded-lg bg-red-50 px-3 py-2 text-xs font-bold text-red-700 hover:bg-red-100">Hapus</button>
</div>
</div>
`;
}
function openAduanWargaModal(lat, lng, record = null) {
pendingAduanCoords = { lat, lng };
document.getElementById('aduanWargaId').value = record ? String(record.id) : '';
document.getElementById('aduanKategori').value = record ? (record.kategori || '') : '';
document.getElementById('aduanPelaporNama').value = record ? (record.pelapor_nama || '') : '';
document.getElementById('aduanKontakPelapor').value = record ? (record.kontak_pelapor || '') : '';
document.getElementById('aduanStatus').value = record ? (record.status_tindak_lanjut || 'baru') : 'baru';
document.getElementById('aduanDeskripsi').value = record ? (record.deskripsi || '') : '';
document.getElementById('aduanTindakLanjut').value = record ? (record.tindak_lanjut || '') : '';
document.getElementById('aduanCoords').innerText = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
document.getElementById('aduanFoto').value = '';
const modal = document.getElementById('aduanWargaModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
suppressAutoReload = true;
}
function closeAduanWargaModal() {
const modal = document.getElementById('aduanWargaModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
pendingAduanCoords = null;
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)') || !!editingFeatureId;
}
function saveAduanWarga() {
if (!pendingAduanCoords) {
showMessage('Koordinat aduan tidak ditemukan', 'bg-red-600 text-white');
return;
}
const id = document.getElementById('aduanWargaId').value;
const kategori = document.getElementById('aduanKategori').value.trim();
const pelaporNama = document.getElementById('aduanPelaporNama').value.trim();
const deskripsi = document.getElementById('aduanDeskripsi').value.trim();
if (!kategori || !pelaporNama || !deskripsi) {
showMessage('Kategori, nama pelapor, dan deskripsi wajib diisi', 'bg-red-600 text-white');
return;
}
const formData = new FormData();
formData.append('kategori', kategori);
formData.append('pelapor_nama', pelaporNama);
formData.append('kontak_pelapor', document.getElementById('aduanKontakPelapor').value.trim());
formData.append('deskripsi', deskripsi);
formData.append('status_tindak_lanjut', document.getElementById('aduanStatus').value);
formData.append('tindak_lanjut', document.getElementById('aduanTindakLanjut').value.trim());
formData.append('latitude', pendingAduanCoords.lat);
formData.append('longitude', pendingAduanCoords.lng);
const fileInput = document.getElementById('aduanFoto');
if (fileInput.files && fileInput.files[0]) {
formData.append('foto', fileInput.files[0]);
}
const endpoint = id ? 'update_aduan_warga.php' : 'save_aduan_warga.php';
if (id) {
formData.append('id', id);
}
fetch(endpoint, { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, endpoint))
.then(data => {
if (data.status === 'success') {
showMessage('✅ Aduan warga berhasil disimpan', 'bg-green-600 text-white');
closeAduanWargaModal();
loadAduanWarga(true);
} else {
throw new Error(data.message || 'Gagal menyimpan aduan');
}
})
.catch(err => showMessage(err.message || 'Gagal menyimpan aduan', 'bg-red-600 text-white'));
}
function deleteAduanWarga(id) {
if (!confirm('Hapus aduan warga ini?')) return;
const formData = new FormData();
formData.append('id', id);
fetch('delete_aduan_warga.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'delete_aduan_warga.php'))
.then(data => {
if (data.status === 'success') {
showMessage('🗑️ Aduan warga berhasil dihapus', 'bg-orange-600 text-white');
loadAduanWarga(true);
} else {
throw new Error(data.message || 'Gagal menghapus aduan');
}
})
.catch(err => showMessage(err.message || 'Gagal menghapus aduan', 'bg-red-600 text-white'));
}
function openAduanWargaEditor(id) {
const record = aduanWargaRecords.find(item => Number(item.id) === Number(id));
if (!record) {
showMessage('Data aduan tidak ditemukan', 'bg-red-600 text-white');
return;
}
openAduanWargaModal(Number(record.latitude), Number(record.longitude), record);
}
function saveRumahIbadah() {
if (!pendingRumahIbadahCoords) {
showMessage('Koordinat rumah ibadah tidak ditemukan', 'bg-red-600 text-white');
@@ -2608,16 +3184,6 @@
});
}
function createPendudukMiskinIcon() {
return L.divIcon({
className: 'custom-marker',
html: `<svg width="32" height="40" viewBox="0 0 24 24" fill="#b91c1c" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="8" r="4"/><path d="M4 22a8 8 0 0 1 16 0"/></svg>`,
iconSize: [32, 40],
iconAnchor: [16, 40],
popupAnchor: [0, -40]
});
}
function clearRumahIbadahLayers() {
rumahIbadahLayer.clearLayers();
rumahIbadahLayerMap = {};
@@ -2700,7 +3266,7 @@
data.forEach(record => {
const marker = L.marker([Number(record.latitude), Number(record.longitude)], {
icon: createPendudukMiskinIcon()
icon: createPendudukMiskinIcon(record.bantuan_status)
}).addTo(pendudukMiskinLayer);
marker.bindPopup(buildPendudukMiskinPopup(record));
@@ -2718,6 +3284,86 @@
.catch(err => console.error('Gagal memuat penduduk miskin:', err));
}
function getVerifikasiLayerPopup(record) {
return buildVerifikasiLapanganPopup(record);
}
function getAduanLayerPopup(record) {
return buildAduanWargaPopup(record);
}
function clearVerifikasiLapanganLayers() {
verifikasiLapanganLayer.clearLayers();
verifikasiLapanganLayerMap = {};
}
function clearAduanWargaLayers() {
aduanWargaLayer.clearLayers();
aduanWargaLayerMap = {};
}
function loadVerifikasiLapangan(force = false) {
if (!force && (document.hidden || suppressAutoReload)) return;
fetch('get_verifikasi_lapangan.php')
.then(res => {
if (!res.ok) throw new Error('HTTP Error: ' + res.status);
return res.text();
})
.then(text => {
const data = JSON.parse(text);
if (!Array.isArray(data)) {
throw new Error('Format data verifikasi tidak valid');
}
verifikasiLapanganRecords = data;
clearVerifikasiLapanganLayers();
data.forEach(record => {
const marker = L.marker([Number(record.latitude), Number(record.longitude)], {
icon: getVerifikasiIcon(record.status_verifikasi)
}).addTo(verifikasiLapanganLayer);
marker.bindPopup(getVerifikasiLayerPopup(record));
verifikasiLapanganLayerMap[record.id] = marker;
});
renderInputRegistry();
})
.catch(err => console.error('Gagal memuat verifikasi lapangan:', err));
}
function loadAduanWarga(force = false) {
if (!force && (document.hidden || suppressAutoReload)) return;
fetch('get_aduan_warga.php')
.then(res => {
if (!res.ok) throw new Error('HTTP Error: ' + res.status);
return res.text();
})
.then(text => {
const data = JSON.parse(text);
if (!Array.isArray(data)) {
throw new Error('Format data aduan tidak valid');
}
aduanWargaRecords = data;
clearAduanWargaLayers();
data.forEach(record => {
const marker = L.marker([Number(record.latitude), Number(record.longitude)], {
icon: getAduanIcon(record.status_tindak_lanjut)
}).addTo(aduanWargaLayer);
marker.bindPopup(getAduanLayerPopup(record));
aduanWargaLayerMap[record.id] = marker;
});
renderInputRegistry();
})
.catch(err => console.error('Gagal memuat aduan warga:', err));
}
function convertCircleToPolygon(circleLayer, vertices = 48) {
const center = circleLayer.getLatLng();
const radius = Number(circleLayer.getRadius() || 0);
@@ -3396,7 +4042,9 @@
'Polygon': polygonLayerGroup,
'Jalan Rusak': roadDamageLayer,
'Rumah Ibadah': rumahIbadahLayer,
'Penduduk Miskin': pendudukMiskinLayer
'Penduduk Miskin': pendudukMiskinLayer,
'Verifikasi Lapangan': verifikasiLapanganLayer,
'Aduan Warga': aduanWargaLayer
}, {
collapsed: true,
position: 'topleft'
@@ -3415,6 +4063,8 @@
pendudukMiskinBtn.classList.toggle('active', activeMode === 'pendudukMiskin');
choroplethBtn.classList.toggle('active', activeMode === 'choropleth');
roadDamageBtn.classList.toggle('active', activeMode === 'roadDamage');
verifikasiBtn.classList.toggle('active', activeMode === 'verifikasiLapangan');
aduanWargaBtn.classList.toggle('active', activeMode === 'aduanWarga');
}
const registryBtn = L.DomUtil.create('a', '', container);
@@ -3459,6 +4109,18 @@
roadDamageBtn.title = 'Lapor Jalan Rusak';
roadDamageBtn.innerHTML = 'Jr';
const verifikasiBtn = L.DomUtil.create('a', '', container);
verifikasiBtn.href = '#';
verifikasiBtn.id = 'leafletVerifikasiBtn';
verifikasiBtn.title = 'Verifikasi Lapangan';
verifikasiBtn.innerHTML = 'Vf';
const aduanWargaBtn = L.DomUtil.create('a', '', container);
aduanWargaBtn.href = '#';
aduanWargaBtn.id = 'leafletAduanWargaBtn';
aduanWargaBtn.title = 'Aduan Warga';
aduanWargaBtn.innerHTML = 'Ad';
L.DomEvent.disableClickPropagation(container);
L.DomEvent.on(registryBtn, 'click', function(event) {
@@ -3508,6 +4170,22 @@
showMessage('Mode laporan jalan rusak aktif. Klik peta untuk menandai lokasi.', 'bg-red-600 text-white');
});
L.DomEvent.on(verifikasiBtn, 'click', function(event) {
L.DomEvent.stop(event);
setDataMode('verifikasiLapangan');
toggleInputRegistry(false);
setUtilityModeActive('verifikasiLapangan');
showMessage('Mode verifikasi lapangan aktif. Klik peta untuk mencatat hasil cek.', 'bg-emerald-600 text-white');
});
L.DomEvent.on(aduanWargaBtn, 'click', function(event) {
L.DomEvent.stop(event);
setDataMode('aduanWarga');
toggleInputRegistry(false);
setUtilityModeActive('aduanWarga');
showMessage('Mode aduan warga aktif. Klik peta untuk mencatat laporan.', 'bg-fuchsia-600 text-white');
});
return container;
};
@@ -3740,6 +4418,16 @@
return;
}
if (appMode === 'verifikasiLapangan') {
openVerifikasiLapanganModal(e.latlng.lat, e.latlng.lng);
return;
}
if (appMode === 'aduanWarga') {
openAduanWargaModal(e.latlng.lat, e.latlng.lng);
return;
}
if (appMode === 'rumahIbadah') {
openRumahIbadahModal(e.latlng.lat, e.latlng.lng);
return;
@@ -3788,6 +4476,8 @@
loadFeatures();
loadRumahIbadah();
loadPendudukMiskin();
loadVerifikasiLapangan();
loadAduanWarga();
}, REFRESH_INTERVAL_MS);
}
@@ -3797,6 +4487,8 @@
loadFeatures(true);
loadRumahIbadah(true);
loadPendudukMiskin(true);
loadVerifikasiLapangan(true);
loadAduanWarga(true);
}
});
@@ -3841,6 +4533,8 @@
loadRumahIbadah(true);
loadPendudukMiskin(true);
loadRoadDamage(true);
loadVerifikasiLapangan(true);
loadAduanWarga(true);
loadExternalRoadNetwork(true);
startAutoRefresh();
};
@@ -3859,6 +4553,14 @@
document.getElementById('modePendudukMiskinBtn').addEventListener('click', () => setDataMode('pendudukMiskin'));
document.getElementById('modeChoroplethBtn').addEventListener('click', () => setDataMode('choropleth'));
document.getElementById('modeRoadDamageBtn').addEventListener('click', () => setDataMode('roadDamage'));
const modeVerifikasiBtn = document.getElementById('modeVerifikasiBtn');
if (modeVerifikasiBtn) {
modeVerifikasiBtn.addEventListener('click', () => setDataMode('verifikasiLapangan'));
}
const modeAduanWargaBtn = document.getElementById('modeAduanWargaBtn');
if (modeAduanWargaBtn) {
modeAduanWargaBtn.addEventListener('click', () => setDataMode('aduanWarga'));
}
const miniModePointBtn = document.getElementById('miniModePointBtn');
if (miniModePointBtn) {
miniModePointBtn.addEventListener('click', () => {
@@ -3894,6 +4596,20 @@
showMessage('Klik peta untuk menambah penduduk miskin', 'bg-indigo-600 text-white');
});
}
const miniModeVerifikasiBtn = document.getElementById('miniModeVerifikasiBtn');
if (miniModeVerifikasiBtn) {
miniModeVerifikasiBtn.addEventListener('click', () => {
setDataMode('verifikasiLapangan');
showMessage('Klik peta untuk verifikasi lapangan', 'bg-emerald-600 text-white');
});
}
const miniModeAduanWargaBtn = document.getElementById('miniModeAduanWargaBtn');
if (miniModeAduanWargaBtn) {
miniModeAduanWargaBtn.addEventListener('click', () => {
setDataMode('aduanWarga');
showMessage('Klik peta untuk membuat aduan warga', 'bg-fuchsia-600 text-white');
});
}
const toggleRegistryDrawerBtn = document.getElementById('toggleRegistryDrawerBtn');
if (toggleRegistryDrawerBtn) {
toggleRegistryDrawerBtn.addEventListener('click', () => toggleInputRegistry());
@@ -4033,6 +4749,20 @@
if (event.target.id === 'pendudukMiskinModal') closePendudukMiskinModal();
});
document.getElementById('saveVerifikasiLapanganBtn').addEventListener('click', saveVerifikasiLapangan);
document.getElementById('cancelVerifikasiLapanganBtn').addEventListener('click', closeVerifikasiLapanganModal);
document.getElementById('closeVerifikasiLapanganModalBtn').addEventListener('click', closeVerifikasiLapanganModal);
document.getElementById('verifikasiLapanganModal').addEventListener('click', (event) => {
if (event.target.id === 'verifikasiLapanganModal') closeVerifikasiLapanganModal();
});
document.getElementById('saveAduanWargaBtn').addEventListener('click', saveAduanWarga);
document.getElementById('cancelAduanWargaBtn').addEventListener('click', closeAduanWargaModal);
document.getElementById('closeAduanWargaModalBtn').addEventListener('click', closeAduanWargaModal);
document.getElementById('aduanWargaModal').addEventListener('click', (event) => {
if (event.target.id === 'aduanWargaModal') closeAduanWargaModal();
});
// Road Damage Modal Event Listeners
document.getElementById('saveRoadDamageBtn').addEventListener('click', saveRoadDamage);
document.getElementById('cancelRoadDamageBtn').addEventListener('click', closeRoadDamageModal);
+3
View File
@@ -0,0 +1,3 @@
<?php
header('Content-Type: application/json');
echo json_encode(['success'=>true,'msg'=>'API placeholder untuk layer groups']);
@@ -0,0 +1,36 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'ID fitur tidak valid']);
$conn->close();
exit;
}
$stmt = $conn->prepare("DELETE FROM spatial_features WHERE id = ?");
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$stmt->bind_param("i", $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Fitur berhasil dihapus']);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
?>
+29
View File
@@ -0,0 +1,29 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
$sql = "SELECT id, nama_objek, kategori, status_objek, type, geometry_data, nilai_ukur, created_at
FROM spatial_features
ORDER BY created_at DESC";
$result = $conn->query($sql);
$features = [];
if (!$result) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
if ($result && $result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$row['geometry_data'] = json_decode($row['geometry_data']);
$row['nilai_ukur'] = (float)$row['nilai_ukur'];
$features[] = $row;
}
}
echo json_encode($features);
$conn->close();
?>
+36
View File
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Layer Groups & Choropleth</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>html,body,#map{height:100%;margin:0}</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([-0.03,109.34],11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Layer group for SPBU points
const spbuLayer = L.layerGroup();
const spbuMarkers = [L.marker([-0.03,109.34]).bindPopup('SPBU A'), L.marker([-0.04,109.36]).bindPopup('SPBU B')];
spbuMarkers.forEach(m=>spbuLayer.addLayer(m));
// Choropleth layer (load from ../data/pontianak-kecamatan.geojson if available)
let choropleth = L.layerGroup();
fetch('../data/pontianak-kecamatan.geojson').then(r=>r.json()).then(j=>{
function getColor(d){return d>10000?'#800026':d>5000?'#BD0026':d>2000?'#E31A1C':d>1000?'#FC4E2A':'#FD8D3C';}
choropleth = L.geoJSON(j,{style: f=>({fillColor:getColor(f.properties.luas||0),weight:1,fillOpacity:0.7,color:'#333'})}).addTo(map);
controlLayers.addOverlay(choropleth,'Choropleth');
}).catch(()=>{ /* ignore missing file */ });
const base = {};
const overlays = {'SPBU Points': spbuLayer};
const controlLayers = L.control.layers(base, overlays, {collapsed:false}).addTo(map);
spbuLayer.addTo(map);
</script>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
<?php
header('Content-Type: application/json');
ini_set('display_errors', '0');
ini_set('html_errors', '0');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = trim($_POST['nama'] ?? '');
$kategori = trim($_POST['kategori'] ?? '');
if ($kategori === 'Parsil Tanah') {
$kategori = 'Tanah';
}
$status = trim($_POST['status'] ?? '');
$type = trim($_POST['type'] ?? '');
$geometry = $_POST['geometry'] ?? '';
$nilai = (float)($_POST['nilai'] ?? 0);
if ($nama === '' || $kategori === '' || $status === '' || $type === '' || $geometry === '') {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Data fitur tidak lengkap']);
$conn->close();
exit;
}
$stmt = $conn->prepare("INSERT INTO spatial_features (nama_objek, kategori, status_objek, type, geometry_data, nilai_ukur) VALUES (?, ?, ?, ?, ?, ?)");
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$stmt->bind_param("sssssd", $nama, $kategori, $status, $type, $geometry, $nilai);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Data berhasil disimpan']);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>
@@ -0,0 +1,45 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$nama = trim($_POST['nama'] ?? '');
$kategori = trim($_POST['kategori'] ?? '');
if ($kategori === 'Parsil Tanah') {
$kategori = 'Tanah';
}
$status = trim($_POST['status'] ?? '');
$type = trim($_POST['type'] ?? '');
$geometry = $_POST['geometry'] ?? '';
$nilai = (float)($_POST['nilai'] ?? 0);
if ($id <= 0 || $nama === '' || $kategori === '' || $status === '' || $type === '' || $geometry === '') {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Data fitur tidak lengkap']);
$conn->close();
exit;
}
$stmt = $conn->prepare("UPDATE spatial_features SET nama_objek = ?, kategori = ?, status_objek = ?, type = ?, geometry_data = ?, nilai_ukur = ? WHERE id = ?");
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$stmt->bind_param("sssssdi", $nama, $kategori, $status, $type, $geometry, $nilai, $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Fitur berhasil diperbarui']);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
?>
+29
View File
@@ -0,0 +1,29 @@
# Rancangan Sistem Geospasial untuk Kontribusi Pengentasan Kemiskinan
Ringkasan singkat: rancangan ini menggabungkan data titik (SPBU, fasilitas), jaringan jalan (polyline), parsel tanah (polygon dengan luas otomatis), dan peta tematik (choropleth). Tujuan: menyediakan basis data spasial untuk analisis kebutuhan bantuan, akses layanan, dan prioritas intervensi.
Komponen:
- Data: sampel sensus, kemiskinan, infrastruktur, peta batas administrasi, penggunaan lahan, parsel tanah.
- Backend: API PHP ringan untuk CRUD GeoJSON + penyimpanan file atau database spatial (PostGIS direkomendasikan).
- Frontend: Leaflet + plugins (Draw, Turf.js) untuk editing, pengukuran, dan visualisasi layer groups.
- Analitik: agregasi statistik per kelurahan/kecamatan untuk choropleth (tingkat kemiskinan, akses fasilitas).
Alur kerja singkat:
1. Kumpulkan & validasi data atribut (nama, id, status ekonomis, luas parsel).
2. Masukkan ke sistem: titik/line/polygon.
3. Hitung atribut turunan: luas parsel (m²), distance-to-nearest-facility, road-condition index.
4. Hasilkan peta tematik & laporan untuk prioritisasi intervensi.
Rekomendasi teknis singkat:
- Gunakan PostGIS untuk query spasial yang efisien.
- Simpan snapshot GeoJSON untuk interoperabilitas.
- Bangun API otentikasi dan logging perubahan (audit trail) untuk kebijakan bantuan.
Catatan: folder lain di repo ini berisi contoh starter untuk tiap fitur (uji_spbu, fitur_jalan_parsel, layer_groups_choropleth).
Global Leaflet loader:
- File: `global/leaflet-loader.js` — tambahkan tag berikut ke halaman HTML Anda untuk memuat Leaflet dari satu sumber global:
<script src="../global/leaflet-loader.js"></script>
Gunakan `rancangan_sistem/index.html` sebagai contoh integrasi: halaman akan memanggil `../get_rumah_ibadah.php` dan `../get_penduduk_miskin.php` dari root repository dan menambahkan data ke peta.
@@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
$conn->close();
exit;
}
$stmt = $conn->prepare("DELETE FROM penduduk_miskin WHERE id = ?");
$stmt->bind_param("i", $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Data penduduk miskin dihapus']);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
$conn->close();
exit;
}
$stmt = $conn->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
$stmt->bind_param("i", $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Rumah ibadah dihapus']);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
?>
+34
View File
@@ -0,0 +1,34 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
$sql = "SELECT id, nama_ketua_kk, jumlah_anggota, latitude, longitude, rumah_ibadah_id, rumah_ibadah_nama, jarak_ke_rumah_ibadah_m, bantuan_status, bantuan_catatan, bantuan_updated_at, created_at
FROM penduduk_miskin
ORDER BY created_at DESC";
$result = $conn->query($sql);
$data = [];
if (!$result) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$row['latitude'] = (float)$row['latitude'];
$row['longitude'] = (float)$row['longitude'];
$row['jumlah_anggota'] = (int)$row['jumlah_anggota'];
$row['rumah_ibadah_id'] = $row['rumah_ibadah_id'] !== null ? (int)$row['rumah_ibadah_id'] : null;
$row['jarak_ke_rumah_ibadah_m'] = $row['jarak_ke_rumah_ibadah_m'] !== null ? (float)$row['jarak_ke_rumah_ibadah_m'] : null;
$row['bantuan_status'] = $row['bantuan_status'] ?: 'belum';
$row['bantuan_catatan'] = $row['bantuan_catatan'] ?? '';
$data[] = $row;
}
}
echo json_encode($data);
$conn->close();
?>
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
$sql = "SELECT id, nama_ketua_kk, jumlah_anggota, latitude, longitude, rumah_ibadah_id, rumah_ibadah_nama, jarak_ke_rumah_ibadah_m, bantuan_status, bantuan_catatan, bantuan_updated_at, created_at FROM penduduk_miskin ORDER BY created_at DESC";
$result = $conn->query($sql);
$features = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$lat = isset($row['latitude']) ? (float)$row['latitude'] : null;
$lng = isset($row['longitude']) ? (float)$row['longitude'] : null;
$props = $row;
unset($props['latitude'], $props['longitude']);
$features[] = [
'type' => 'Feature',
'geometry' => [ 'type' => 'Point', 'coordinates' => [$lng, $lat] ],
'properties' => $props
];
}
}
echo json_encode([ 'type' => 'FeatureCollection', 'features' => $features ]);
$conn->close();
+30
View File
@@ -0,0 +1,30 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
$sql = "SELECT id, nama, alamat, pic, latitude, longitude, radius_m, created_at
FROM rumah_ibadah
ORDER BY created_at DESC";
$result = $conn->query($sql);
$data = [];
if (!$result) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$row['latitude'] = (float)$row['latitude'];
$row['longitude'] = (float)$row['longitude'];
$row['radius_m'] = (int)$row['radius_m'];
$data[] = $row;
}
}
echo json_encode($data);
$conn->close();
?>
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
$sql = "SELECT id, nama, alamat, pic, latitude, longitude, radius_m, created_at FROM rumah_ibadah ORDER BY created_at DESC";
$result = $conn->query($sql);
$features = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$lat = isset($row['latitude']) ? (float)$row['latitude'] : null;
$lng = isset($row['longitude']) ? (float)$row['longitude'] : null;
$props = $row;
unset($props['latitude'], $props['longitude']);
$features[] = [
'type' => 'Feature',
'geometry' => [ 'type' => 'Point', 'coordinates' => [$lng, $lat] ],
'properties' => $props
];
}
}
echo json_encode([ 'type' => 'FeatureCollection', 'features' => $features ]);
$conn->close();
+57
View File
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rancangan Sistem — Integrasi Rumah Ibadah & Penduduk Miskin</title>
<style>html,body,#map{height:100%;margin:0} .info{position:absolute;right:8px;top:8px;z-index:400;background:#fff;padding:6px}</style>
</head>
<body>
<div id="map"></div>
<div class="info">Data: Rumah Ibadah & Penduduk Miskin (dari API)</div>
<script src="../global/leaflet-loader.js"></script>
<script>
(function waitForLeaflet(){
if(window.L) return init();
window.addEventListener('leaflet:loaded', init);
setTimeout(function(){ if(window.L) init(); }, 1000);
function init(){
const map = L.map('map').setView([-0.03,109.34],12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:''}).addTo(map);
const overlays = {};
const control = L.control.layers(null, overlays, {collapsed:false}).addTo(map);
function addGeoData(data, title){
try{
if(!data) return;
if(data.type==='FeatureCollection' || data.features){
const layer = L.geoJSON(data, {onEachFeature:function(feature,layer){
const p = feature.properties || {};
layer.bindPopup(title+'<br>'+Object.keys(p).map(k=>k+': '+p[k]).join('<br>'));
}}).addTo(map);
overlays[title]=layer; control.addOverlay(layer,title);
} else if(Array.isArray(data)){
const lg = L.layerGroup();
data.forEach(it=>{
const lat = it.lat || it.latitude || it.y;
const lng = it.lng || it.lon || it.longitude || it.x;
if(lat && lng){
const m = L.marker([lat,lng]).bindPopup(title+'<br>'+(it.nama||it.name||''));
lg.addLayer(m);
}
});
lg.addTo(map); overlays[title]=lg; control.addOverlay(lg,title);
}
}catch(e){ console.error('addGeoData error',e); }
}
// load rumah ibadah and penduduk miskin as GeoJSON (endpoints in this folder)
fetch('get_rumah_ibadah_geojson.php').then(r=>r.json()).then(j=>addGeoData(j,'Rumah Ibadah')).catch(e=>console.error(e));
fetch('get_penduduk_miskin_geojson.php').then(r=>r.json()).then(j=>addGeoData(j,'Penduduk Miskin')).catch(e=>console.error(e));
}
})();
</script>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
function distanceMeters($lat1, $lng1, $lat2, $lng2) {
$earthRadius = 6371000;
$toRadians = function ($value) {
return $value * M_PI / 180;
};
$deltaLat = $toRadians($lat2 - $lat1);
$deltaLng = $toRadians($lng2 - $lng1);
$a = sin($deltaLat / 2) * sin($deltaLat / 2) + cos($toRadians($lat1)) * cos($toRadians($lat2)) * sin($deltaLng / 2) * sin($deltaLng / 2);
return 2 * $earthRadius * atan2(sqrt($a), sqrt(1 - $a));
}
function findNearestRumahIbadah($conn, $lat, $lng) {
$result = $conn->query("SELECT id, nama, latitude, longitude FROM rumah_ibadah ORDER BY created_at DESC");
if (!$result || $result->num_rows === 0) {
return null;
}
$nearest = null;
while ($row = $result->fetch_assoc()) {
$distance = distanceMeters($lat, $lng, (float)$row['latitude'], (float)$row['longitude']);
if ($nearest === null || $distance < $nearest['distance']) {
$nearest = [
'id' => (int)$row['id'],
'nama' => $row['nama'],
'distance' => $distance
];
}
}
return $nearest;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$namaKetuaKk = trim($_POST['nama_ketua_kk'] ?? '');
$jumlahAnggota = (int)($_POST['jumlah_anggota'] ?? 0);
$lat = $_POST['latitude'] ?? null;
$lng = $_POST['longitude'] ?? null;
$rumahIbadahId = $_POST['rumah_ibadah_id'] ?? null;
$rumahIbadahNama = trim($_POST['rumah_ibadah_nama'] ?? '');
$jarak = $_POST['jarak_ke_rumah_ibadah_m'] ?? null;
if ($namaKetuaKk === '' || $jumlahAnggota <= 0 || $lat === null || $lng === null) {
echo json_encode(['status' => 'error', 'message' => 'Data penduduk miskin tidak lengkap']);
$conn->close();
exit;
}
$lat = (float)$lat;
$lng = (float)$lng;
$nearest = findNearestRumahIbadah($conn, $lat, $lng);
if (!$nearest) {
echo json_encode(['status' => 'error', 'message' => 'Tambahkan rumah ibadah terlebih dahulu']);
$conn->close();
exit;
}
$jarak = $nearest['distance'];
$rumahIbadahId = $nearest['id'];
$rumahIbadahNama = $nearest['nama'];
$stmt = $conn->prepare("INSERT INTO penduduk_miskin (nama_ketua_kk, jumlah_anggota, latitude, longitude, rumah_ibadah_id, rumah_ibadah_nama, jarak_ke_rumah_ibadah_m) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("siddisd", $namaKetuaKk, $jumlahAnggota, $lat, $lng, $rumahIbadahId, $rumahIbadahNama, $jarak);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Penduduk miskin berhasil disimpan']);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = trim($_POST['nama'] ?? '');
$alamat = trim($_POST['alamat'] ?? '');
$pic = trim($_POST['pic'] ?? '');
$lat = $_POST['latitude'] ?? null;
$lng = $_POST['longitude'] ?? null;
$radius = $_POST['radius_m'] ?? 1000;
if ($nama === '' || $alamat === '' || $pic === '' || $lat === null || $lng === null) {
echo json_encode(['status' => 'error', 'message' => 'Data rumah ibadah tidak lengkap']);
$conn->close();
exit;
}
$lat = (float)$lat;
$lng = (float)$lng;
$radius = max(50, (int)$radius);
$stmt = $conn->prepare("INSERT INTO rumah_ibadah (nama, alamat, pic, latitude, longitude, radius_m) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->bind_param("sssddi", $nama, $alamat, $pic, $lat, $lng, $radius);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Rumah ibadah berhasil disimpan']);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
?>
@@ -0,0 +1,48 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Metode tidak diizinkan']);
$conn->close();
exit;
}
$id = (int)($_POST['id'] ?? 0);
$status = strtolower(trim($_POST['bantuan_status'] ?? 'belum'));
$catatan = trim($_POST['bantuan_catatan'] ?? '');
if ($id <= 0) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
$conn->close();
exit;
}
if (!in_array($status, ['belum', 'proses', 'sudah'], true)) {
$status = 'belum';
}
$stmt = $conn->prepare("UPDATE penduduk_miskin SET bantuan_status = ?, bantuan_catatan = ?, bantuan_updated_at = CURRENT_TIMESTAMP WHERE id = ?");
$stmt->bind_param("ssi", $status, $catatan, $id);
if ($stmt->execute()) {
$updatedAtResult = $conn->query("SELECT bantuan_updated_at FROM penduduk_miskin WHERE id = " . (int)$id . " LIMIT 1");
$updatedAt = null;
if ($updatedAtResult && $updatedAtResult->num_rows > 0) {
$row = $updatedAtResult->fetch_assoc();
$updatedAt = $row['bantuan_updated_at'] ?? null;
}
echo json_encode([
'status' => 'success',
'message' => 'Status bantuan berhasil diperbarui',
'bantuan_updated_at' => $updatedAt
]);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
?>
@@ -0,0 +1,82 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
function distanceMeters($lat1, $lng1, $lat2, $lng2) {
$earthRadius = 6371000;
$toRadians = function ($value) {
return $value * M_PI / 180;
};
$deltaLat = $toRadians($lat2 - $lat1);
$deltaLng = $toRadians($lng2 - $lng1);
$a = sin($deltaLat / 2) * sin($deltaLat / 2) + cos($toRadians($lat1)) * cos($toRadians($lat2)) * sin($deltaLng / 2) * sin($deltaLng / 2);
return 2 * $earthRadius * atan2(sqrt($a), sqrt(1 - $a));
}
function findNearestRumahIbadah($conn, $lat, $lng) {
$result = $conn->query("SELECT id, nama, latitude, longitude FROM rumah_ibadah ORDER BY created_at DESC");
if (!$result || $result->num_rows === 0) {
return null;
}
$nearest = null;
while ($row = $result->fetch_assoc()) {
$distance = distanceMeters($lat, $lng, (float)$row['latitude'], (float)$row['longitude']);
if ($nearest === null || $distance < $nearest['distance']) {
$nearest = [
'id' => (int)$row['id'],
'nama' => $row['nama'],
'distance' => $distance
];
}
}
return $nearest;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$namaKetuaKk = trim($_POST['nama_ketua_kk'] ?? '');
$jumlahAnggota = (int)($_POST['jumlah_anggota'] ?? 0);
$lat = $_POST['latitude'] ?? null;
$lng = $_POST['longitude'] ?? null;
$rumahIbadahId = $_POST['rumah_ibadah_id'] ?? null;
$rumahIbadahNama = trim($_POST['rumah_ibadah_nama'] ?? '');
$jarak = $_POST['jarak_ke_rumah_ibadah_m'] ?? null;
if ($id <= 0 || $namaKetuaKk === '' || $jumlahAnggota <= 0 || $lat === null || $lng === null) {
echo json_encode(['status' => 'error', 'message' => 'Data penduduk miskin tidak lengkap']);
$conn->close();
exit;
}
$lat = (float)$lat;
$lng = (float)$lng;
$nearest = findNearestRumahIbadah($conn, $lat, $lng);
if (!$nearest) {
echo json_encode(['status' => 'error', 'message' => 'Tambahkan rumah ibadah terlebih dahulu']);
$conn->close();
exit;
}
$jarak = $nearest['distance'];
$rumahIbadahId = $nearest['id'];
$rumahIbadahNama = $nearest['nama'];
$stmt = $conn->prepare("UPDATE penduduk_miskin SET nama_ketua_kk = ?, jumlah_anggota = ?, latitude = ?, longitude = ?, rumah_ibadah_id = ?, rumah_ibadah_nama = ?, jarak_ke_rumah_ibadah_m = ? WHERE id = ?");
$stmt->bind_param("siddisdi", $namaKetuaKk, $jumlahAnggota, $lat, $lng, $rumahIbadahId, $rumahIbadahNama, $jarak, $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Data penduduk miskin berhasil diperbarui']);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
header('Content-Type: application/json');
include __DIR__ . '/../db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$nama = trim($_POST['nama'] ?? '');
$alamat = trim($_POST['alamat'] ?? '');
$pic = trim($_POST['pic'] ?? '');
$lat = $_POST['latitude'] ?? null;
$lng = $_POST['longitude'] ?? null;
$radius = $_POST['radius_m'] ?? 1000;
if ($id <= 0 || $nama === '' || $alamat === '' || $pic === '' || $lat === null || $lng === null) {
echo json_encode(['status' => 'error', 'message' => 'Data rumah ibadah tidak lengkap']);
$conn->close();
exit;
}
$lat = (float)$lat;
$lng = (float)$lng;
$radius = max(50, (int)$radius);
$stmt = $conn->prepare("UPDATE rumah_ibadah SET nama = ?, alamat = ?, pic = ?, latitude = ?, longitude = ?, radius_m = ? WHERE id = ?");
$stmt->bind_param("sssddii", $nama, $alamat, $pic, $lat, $lng, $radius, $id);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Rumah ibadah berhasil diperbarui']);
} else {
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
?>
+75
View File
@@ -0,0 +1,75 @@
<?php
header('Content-Type: application/json');
include 'db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$kategori = trim($_POST['kategori'] ?? '');
$pelaporNama = trim($_POST['pelapor_nama'] ?? '');
$kontakPelapor = trim($_POST['kontak_pelapor'] ?? '');
$deskripsi = trim($_POST['deskripsi'] ?? '');
$statusTindakLanjut = trim($_POST['status_tindak_lanjut'] ?? 'baru');
$tindakLanjut = trim($_POST['tindak_lanjut'] ?? '');
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
if ($kategori === '' || $pelaporNama === '' || $deskripsi === '' || empty($latitude) || empty($longitude)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Kategori, nama pelapor, deskripsi, dan koordinat wajib diisi']);
$conn->close();
exit;
}
$foto = null;
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$filename = $_FILES['foto']['name'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Format foto hanya JPG, PNG, GIF, WEBP']);
$conn->close();
exit;
}
if (!is_dir('uploads')) {
mkdir('uploads', 0755, true);
}
$foto = 'uploads/' . time() . '_' . basename($filename);
if (!move_uploaded_file($_FILES['foto']['tmp_name'], $foto)) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Gagal upload foto']);
$conn->close();
exit;
}
}
$stmt = $conn->prepare('INSERT INTO aduan_warga (kategori, pelapor_nama, kontak_pelapor, deskripsi, status_tindak_lanjut, tindak_lanjut, foto, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$latitude = (float)$latitude;
$longitude = (float)$longitude;
$stmt->bind_param('sssssssdd', $kategori, $pelaporNama, $kontakPelapor, $deskripsi, $statusTindakLanjut, $tindakLanjut, $foto, $latitude, $longitude);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Aduan warga berhasil disimpan', 'id' => $stmt->insert_id]);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>
+75
View File
@@ -0,0 +1,75 @@
<?php
header('Content-Type: application/json');
include 'db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$jenisTarget = trim($_POST['jenis_target'] ?? 'penduduk_miskin');
$targetNama = trim($_POST['target_nama'] ?? '');
$petugasNama = trim($_POST['petugas_nama'] ?? '');
$statusVerifikasi = trim($_POST['status_verifikasi'] ?? 'menunggu');
$hasilTemuan = trim($_POST['hasil_temuan'] ?? '');
$tindakLanjut = trim($_POST['tindak_lanjut'] ?? '');
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
if ($petugasNama === '' || empty($latitude) || empty($longitude)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Nama petugas dan koordinat wajib diisi']);
$conn->close();
exit;
}
$foto = null;
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$filename = $_FILES['foto']['name'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Format foto hanya JPG, PNG, GIF, WEBP']);
$conn->close();
exit;
}
if (!is_dir('uploads')) {
mkdir('uploads', 0755, true);
}
$foto = 'uploads/' . time() . '_' . basename($filename);
if (!move_uploaded_file($_FILES['foto']['tmp_name'], $foto)) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Gagal upload foto']);
$conn->close();
exit;
}
}
$stmt = $conn->prepare('INSERT INTO verifikasi_lapangan (jenis_target, target_nama, petugas_nama, status_verifikasi, hasil_temuan, tindak_lanjut, foto, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$latitude = (float)$latitude;
$longitude = (float)$longitude;
$stmt->bind_param('sssssssdd', $jenisTarget, $targetNama, $petugasNama, $statusVerifikasi, $hasilTemuan, $tindakLanjut, $foto, $latitude, $longitude);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Verifikasi lapangan berhasil disimpan', 'id' => $stmt->insert_id]);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>
+12
View File
@@ -0,0 +1,12 @@
<?php
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if(!$input) { echo json_encode(['success'=>false,'msg'=>'no input']); exit; }
if($input['action']==='update_status'){
// contoh sederhana: simpan ke file log (uji saja)
$log = ['time'=>date('c'),'id'=>$input['id'],'status'=>$input['status']];
file_put_contents(__DIR__.'/spbu_status.log',json_encode($log)."\n",FILE_APPEND);
echo json_encode(['success'=>true,'log'=>$log]);
exit;
}
echo json_encode(['success'=>false,'msg'=>'unknown action']);
+44
View File
@@ -0,0 +1,44 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Uji CRUD SPBU — Open/Close</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>html,body,#map{height:100%;margin:0}</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([-0.03, 109.34], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:''}).addTo(map);
// contoh data SPBU (id, nama, coords, status)
const spbu = [
{id:1,name:'SPBU 1',lat:-0.03,lng:109.34,status:'open'},
{id:2,name:'SPBU 2',lat:-0.04,lng:109.36,status:'closed'}
];
function iconFor(status){
return L.divIcon({className:'spbu-icon',html:`<div style="background:${status==='open'?'green':'red'};width:14px;height:14px;border-radius:7px;border:2px solid #fff"></div>`});
}
spbu.forEach(s=>{
const m = L.marker([s.lat,s.lng],{icon:iconFor(s.status)}).addTo(map)
.bindPopup(`<b>${s.name}</b><br>Status: <span id=st-${s.id}>${s.status}</span><br><button onclick="toggle(${s.id})">Toggle</button>`);
window['marker_'+s.id]=m;
});
window.toggle = async function(id){
const s = spbu.find(x=>x.id===id);
s.status = s.status==='open'?'closed':'open';
await fetch('api.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'update_status',id, status:s.status})});
const m = window['marker_'+id];
m.setIcon(iconFor(s.status));
const el = document.getElementById('st-'+id);
if(el) el.textContent = s.status;
}
</script>
</body>
</html>
+84
View File
@@ -0,0 +1,84 @@
<?php
header('Content-Type: application/json');
include 'db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$kategori = trim($_POST['kategori'] ?? '');
$pelaporNama = trim($_POST['pelapor_nama'] ?? '');
$kontakPelapor = trim($_POST['kontak_pelapor'] ?? '');
$deskripsi = trim($_POST['deskripsi'] ?? '');
$statusTindakLanjut = trim($_POST['status_tindak_lanjut'] ?? 'baru');
$tindakLanjut = trim($_POST['tindak_lanjut'] ?? '');
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
if ($id <= 0 || $kategori === '' || $pelaporNama === '' || $deskripsi === '' || empty($latitude) || empty($longitude)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Data aduan tidak lengkap']);
$conn->close();
exit;
}
$fotoSql = '';
$foto = null;
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$filename = $_FILES['foto']['name'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Format foto hanya JPG, PNG, GIF, WEBP']);
$conn->close();
exit;
}
if (!is_dir('uploads')) {
mkdir('uploads', 0755, true);
}
$foto = 'uploads/' . time() . '_' . basename($filename);
if (!move_uploaded_file($_FILES['foto']['tmp_name'], $foto)) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Gagal upload foto']);
$conn->close();
exit;
}
$fotoSql = ', foto = ?';
}
$sql = 'UPDATE aduan_warga SET kategori = ?, pelapor_nama = ?, kontak_pelapor = ?, deskripsi = ?, status_tindak_lanjut = ?, tindak_lanjut = ?, latitude = ?, longitude = ?' . $fotoSql . ' WHERE id = ?';
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$latitude = (float)$latitude;
$longitude = (float)$longitude;
if ($foto !== null) {
$stmt->bind_param('sssssssddi', $kategori, $pelaporNama, $kontakPelapor, $deskripsi, $statusTindakLanjut, $tindakLanjut, $foto, $latitude, $longitude, $id);
} else {
$stmt->bind_param('ssssssddi', $kategori, $pelaporNama, $kontakPelapor, $deskripsi, $statusTindakLanjut, $tindakLanjut, $latitude, $longitude, $id);
}
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Aduan warga berhasil diperbarui']);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>
+84
View File
@@ -0,0 +1,84 @@
<?php
header('Content-Type: application/json');
include 'db_config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$jenisTarget = trim($_POST['jenis_target'] ?? 'penduduk_miskin');
$targetNama = trim($_POST['target_nama'] ?? '');
$petugasNama = trim($_POST['petugas_nama'] ?? '');
$statusVerifikasi = trim($_POST['status_verifikasi'] ?? 'menunggu');
$hasilTemuan = trim($_POST['hasil_temuan'] ?? '');
$tindakLanjut = trim($_POST['tindak_lanjut'] ?? '');
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
if ($id <= 0 || $petugasNama === '' || empty($latitude) || empty($longitude)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Data verifikasi tidak lengkap']);
$conn->close();
exit;
}
$fotoSql = '';
$foto = null;
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$filename = $_FILES['foto']['name'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Format foto hanya JPG, PNG, GIF, WEBP']);
$conn->close();
exit;
}
if (!is_dir('uploads')) {
mkdir('uploads', 0755, true);
}
$foto = 'uploads/' . time() . '_' . basename($filename);
if (!move_uploaded_file($_FILES['foto']['tmp_name'], $foto)) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Gagal upload foto']);
$conn->close();
exit;
}
$fotoSql = ', foto = ?';
}
$sql = 'UPDATE verifikasi_lapangan SET jenis_target = ?, target_nama = ?, petugas_nama = ?, status_verifikasi = ?, hasil_temuan = ?, tindak_lanjut = ?, latitude = ?, longitude = ?' . $fotoSql . ' WHERE id = ?';
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $conn->error]);
$conn->close();
exit;
}
$latitude = (float)$latitude;
$longitude = (float)$longitude;
if ($foto !== null) {
$stmt->bind_param('sssssssddi', $jenisTarget, $targetNama, $petugasNama, $statusVerifikasi, $hasilTemuan, $tindakLanjut, $foto, $latitude, $longitude, $id);
} else {
$stmt->bind_param('ssssssddi', $jenisTarget, $targetNama, $petugasNama, $statusVerifikasi, $hasilTemuan, $tindakLanjut, $latitude, $longitude, $id);
}
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Verifikasi lapangan berhasil diperbarui']);
} else {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
exit;
}
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
$conn->close();
?>