fitur penanda jalan rusak
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
// Script untuk membuat tabel jalan_rusak jika belum ada
|
||||||
|
include 'db_config.php';
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS jalan_rusak (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
latitude DECIMAL(10, 8) NOT NULL,
|
||||||
|
longitude DECIMAL(11, 8) NOT NULL,
|
||||||
|
jenis_kerusakan VARCHAR(100) NOT NULL COMMENT 'Jenis: lubang, retak, ambles, dll',
|
||||||
|
deskripsi TEXT,
|
||||||
|
severity VARCHAR(50) DEFAULT 'sedang' COMMENT 'tertarik: ringan, sedang, berat',
|
||||||
|
gambar VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_coords (latitude, longitude)
|
||||||
|
)";
|
||||||
|
|
||||||
|
if ($conn->query($sql) === TRUE) {
|
||||||
|
echo json_encode(['status' => 'success', 'message' => 'Tabel jalan_rusak berhasil dibuat atau sudah ada']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
?>
|
||||||
@@ -6998,7 +6998,8 @@
|
|||||||
"Id": 0,
|
"Id": 0,
|
||||||
"Plan_Area": 113026328.1,
|
"Plan_Area": 113026328.1,
|
||||||
"Ket": "Pontianak Timur",
|
"Ket": "Pontianak Timur",
|
||||||
"name": "Pontianak Timur"
|
"name": "Pontianak Timur",
|
||||||
|
"population": 121500
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -7854,7 +7855,8 @@
|
|||||||
"Id": 0,
|
"Id": 0,
|
||||||
"Plan_Area": 113026328.1,
|
"Plan_Area": 113026328.1,
|
||||||
"Ket": "Pontianak Selatan",
|
"Ket": "Pontianak Selatan",
|
||||||
"name": "Pontianak Selatan"
|
"name": "Pontianak Selatan",
|
||||||
|
"population": 133600
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -14352,7 +14354,8 @@
|
|||||||
"Id": 0,
|
"Id": 0,
|
||||||
"Plan_Area": 113026328.1,
|
"Plan_Area": 113026328.1,
|
||||||
"Ket": "Pontianak Utara",
|
"Ket": "Pontianak Utara",
|
||||||
"name": "Pontianak Utara"
|
"name": "Pontianak Utara",
|
||||||
|
"population": 129200
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -17096,7 +17099,8 @@
|
|||||||
"Id": 0,
|
"Id": 0,
|
||||||
"Plan_Area": 113026328.1,
|
"Plan_Area": 113026328.1,
|
||||||
"Ket": "Pontianak Barat",
|
"Ket": "Pontianak Barat",
|
||||||
"name": "Pontianak Barat"
|
"name": "Pontianak Barat",
|
||||||
|
"population": 148800
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -18288,7 +18292,8 @@
|
|||||||
"Id": 0,
|
"Id": 0,
|
||||||
"Plan_Area": 113026328.1,
|
"Plan_Area": 113026328.1,
|
||||||
"Ket": "Pontianak Kota",
|
"Ket": "Pontianak Kota",
|
||||||
"name": "Pontianak Kota"
|
"name": "Pontianak Kota",
|
||||||
|
"population": 98500
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -19400,7 +19405,8 @@
|
|||||||
"Id": 0,
|
"Id": 0,
|
||||||
"Plan_Area": 113026328.1,
|
"Plan_Area": 113026328.1,
|
||||||
"Ket": "Pontianak Tenggara",
|
"Ket": "Pontianak Tenggara",
|
||||||
"name": "Pontianak Tenggara"
|
"name": "Pontianak Tenggara",
|
||||||
|
"population": 52900
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '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($row['gambar'])) {
|
||||||
|
unlink($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();
|
||||||
|
?>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '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();
|
||||||
|
?>
|
||||||
+297
-2
@@ -83,6 +83,9 @@
|
|||||||
<button class="icon-btn" id="modePolygonBtn" title="Tambah polygon parsil">
|
<button class="icon-btn" id="modePolygonBtn" title="Tambah polygon parsil">
|
||||||
<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 2l8 5v10l-8 5-8-5V7z"/></svg>
|
<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 2l8 5v10l-8 5-8-5V7z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<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="modeChoroplethBtn" title="Lihat peta choropleth kecamatan">
|
<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>
|
<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>
|
</button>
|
||||||
@@ -265,6 +268,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Lapor Jalan Rusak -->
|
||||||
|
<div id="roadDamageModal" class="fixed inset-0 z-[10002] hidden items-center justify-center bg-slate-900/50 px-4">
|
||||||
|
<div class="w-full max-w-lg 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">Lapor Jalan Rusak</h3>
|
||||||
|
<p class="text-sm text-slate-500">Dokumentasikan kondisi jalan dengan foto untuk membantu perbaikan.</p>
|
||||||
|
</div>
|
||||||
|
<button id="closeRoadDamageModalBtn" 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">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Jenis Kerusakan</label>
|
||||||
|
<select id="roadDamageType" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-red-500">
|
||||||
|
<option value="">Pilih jenis kerusakan</option>
|
||||||
|
<option value="lubang">Lubang</option>
|
||||||
|
<option value="retak">Retak</option>
|
||||||
|
<option value="ambles">Ambles</option>
|
||||||
|
<option value="bergelombang">Bergelombang</option>
|
||||||
|
<option value="lainnya">Lainnya</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Tingkat Kerusakan</label>
|
||||||
|
<select id="roadDamageSeverity" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-red-500">
|
||||||
|
<option value="ringan">Ringan</option>
|
||||||
|
<option value="sedang" selected>Sedang</option>
|
||||||
|
<option value="berat">Berat</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Deskripsi</label>
|
||||||
|
<textarea id="roadDamageDesc" placeholder="Jelaskan kondisi jalan, lokasi spesifik, dll..." class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-red-500" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Foto Kondisi (Opsional)</label>
|
||||||
|
<input id="roadDamageImage" type="file" accept="image/*" class="w-full rounded-lg border border-slate-300 p-2 text-sm">
|
||||||
|
<div id="imagePreview" class="mt-2 hidden">
|
||||||
|
<img id="previewImg" src="" alt="Preview" class="max-h-40 rounded-lg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-slate-500 bg-slate-50 p-2 rounded">
|
||||||
|
📍 Koordinat: <span id="roadDamageCoords">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 flex flex-wrap gap-2 border-t pt-4">
|
||||||
|
<button id="saveRoadDamageBtn" class="rounded-lg bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700">Simpan Laporan</button>
|
||||||
|
<button id="cancelRoadDamageBtn" 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">
|
<select id="featureType" class="hidden">
|
||||||
<option value="polyline">polyline</option>
|
<option value="polyline">polyline</option>
|
||||||
<option value="polygon">polygon</option>
|
<option value="polygon">polygon</option>
|
||||||
@@ -299,6 +354,9 @@
|
|||||||
let featureRecords = [];
|
let featureRecords = [];
|
||||||
let editingFeatureId = null;
|
let editingFeatureId = null;
|
||||||
let choroplethLayer = null;
|
let choroplethLayer = null;
|
||||||
|
let roadDamageLayer = L.featureGroup().addTo(map);
|
||||||
|
let roadDamageRecords = [];
|
||||||
|
let pendingRoadDamageCoords = null;
|
||||||
|
|
||||||
const FEATURE_STATUS_OPTIONS = {
|
const FEATURE_STATUS_OPTIONS = {
|
||||||
polyline: [
|
polyline: [
|
||||||
@@ -316,6 +374,40 @@
|
|||||||
|
|
||||||
const CHOROPLETH_GEOJSON_PATH = 'data/pontianak-kecamatan.geojson';
|
const CHOROPLETH_GEOJSON_PATH = 'data/pontianak-kecamatan.geojson';
|
||||||
let choroplethGeoJsonData = null;
|
let choroplethGeoJsonData = null;
|
||||||
|
const DISTRICT_POPULATION_MAP = {
|
||||||
|
'pontianak barat': 148800,
|
||||||
|
'pontianak kota': 98500,
|
||||||
|
'pontianak selatan': 133600,
|
||||||
|
'pontianak tenggara': 52900,
|
||||||
|
'pontianak timur': 121500,
|
||||||
|
'pontianak utara': 129200
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeDistrictName(name) {
|
||||||
|
return String(name || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function enrichFeaturePopulation(feature) {
|
||||||
|
const properties = feature.properties || {};
|
||||||
|
const districtName = properties.name || properties.NAME || properties.Ket || '';
|
||||||
|
const districtKey = normalizeDistrictName(districtName);
|
||||||
|
const mappedPopulation = DISTRICT_POPULATION_MAP[districtKey];
|
||||||
|
|
||||||
|
if (mappedPopulation == null) {
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...feature,
|
||||||
|
properties: {
|
||||||
|
...properties,
|
||||||
|
population: Number(mappedPopulation)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getPopulationFill(population) {
|
function getPopulationFill(population) {
|
||||||
if (population > 120000) return '#ef4444';
|
if (population > 120000) return '#ef4444';
|
||||||
@@ -418,7 +510,9 @@
|
|||||||
function sanitizeChoroplethGeoJson(data) {
|
function sanitizeChoroplethGeoJson(data) {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
features: data.features.map(sanitizeChoroplethFeature)
|
features: data.features
|
||||||
|
.map(sanitizeChoroplethFeature)
|
||||||
|
.map(enrichFeaturePopulation)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,7 +859,8 @@
|
|||||||
point: document.getElementById('modePointBtn'),
|
point: document.getElementById('modePointBtn'),
|
||||||
polyline: document.getElementById('modePolylineBtn'),
|
polyline: document.getElementById('modePolylineBtn'),
|
||||||
polygon: document.getElementById('modePolygonBtn'),
|
polygon: document.getElementById('modePolygonBtn'),
|
||||||
choropleth: document.getElementById('modeChoroplethBtn')
|
choropleth: document.getElementById('modeChoroplethBtn'),
|
||||||
|
roadDamage: document.getElementById('modeRoadDamageBtn')
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.values(modeButtons).forEach(button => button.classList.remove('active'));
|
Object.values(modeButtons).forEach(button => button.classList.remove('active'));
|
||||||
@@ -797,6 +892,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode === 'roadDamage') {
|
||||||
|
hint.innerText = 'Mode laporan jalan rusak aktif: klik peta untuk menandai lokasi kerusakan.';
|
||||||
|
spatialControls.classList.add('hidden');
|
||||||
|
runActionBtn.innerText = 'Klik Peta';
|
||||||
|
runActionBtn.disabled = true;
|
||||||
|
clearDraftBtn.disabled = true;
|
||||||
|
updateDrawControl(null);
|
||||||
|
document.getElementById('modeMeasure').value = '';
|
||||||
|
loadRoadDamage(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
runActionBtn.disabled = false;
|
runActionBtn.disabled = false;
|
||||||
clearDraftBtn.disabled = false;
|
clearDraftBtn.disabled = false;
|
||||||
|
|
||||||
@@ -818,6 +925,163 @@
|
|||||||
setSpatialType(mode);
|
setSpatialType(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRoadDamageModal(lat, lng) {
|
||||||
|
pendingRoadDamageCoords = { lat, lng };
|
||||||
|
document.getElementById('roadDamageCoords').innerText = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
|
||||||
|
document.getElementById('roadDamageType').value = '';
|
||||||
|
document.getElementById('roadDamageSeverity').value = 'sedang';
|
||||||
|
document.getElementById('roadDamageDesc').value = '';
|
||||||
|
document.getElementById('roadDamageImage').value = '';
|
||||||
|
document.getElementById('imagePreview').classList.add('hidden');
|
||||||
|
const modal = document.getElementById('roadDamageModal');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
suppressAutoReload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRoadDamageModal() {
|
||||||
|
const modal = document.getElementById('roadDamageModal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
pendingRoadDamageCoords = null;
|
||||||
|
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)') || !!editingFeatureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRoadDamage() {
|
||||||
|
if (!pendingRoadDamageCoords) {
|
||||||
|
showMessage('Koordinat tidak ditemukan', 'bg-red-600 text-white');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jenis = document.getElementById('roadDamageType').value;
|
||||||
|
if (!jenis) {
|
||||||
|
showMessage('Pilih jenis kerusakan terlebih dahulu', 'bg-red-600 text-white');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const severity = document.getElementById('roadDamageSeverity').value;
|
||||||
|
const deskripsi = document.getElementById('roadDamageDesc').value;
|
||||||
|
const fileInput = document.getElementById('roadDamageImage');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('latitude', pendingRoadDamageCoords.lat);
|
||||||
|
formData.append('longitude', pendingRoadDamageCoords.lng);
|
||||||
|
formData.append('jenis_kerusakan', jenis);
|
||||||
|
formData.append('severity', severity);
|
||||||
|
formData.append('deskripsi', deskripsi);
|
||||||
|
|
||||||
|
if (fileInput.files && fileInput.files[0]) {
|
||||||
|
formData.append('gambar', fileInput.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('save_jalan_rusak.php', { method: 'POST', body: formData })
|
||||||
|
.then(res => parseJsonResponse(res, 'save_jalan_rusak.php'))
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage('✅ Laporan jalan rusak berhasil disimpan', 'bg-green-600 text-white');
|
||||||
|
closeRoadDamageModal();
|
||||||
|
loadRoadDamage(true);
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Gagal menyimpan laporan');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => showMessage(err.message || 'Gagal menyimpan laporan', 'bg-red-600 text-white'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoadDamageIcon(severity) {
|
||||||
|
const colorMap = {
|
||||||
|
'ringan': '#fbbf24',
|
||||||
|
'sedang': '#f97316',
|
||||||
|
'berat': '#dc2626'
|
||||||
|
};
|
||||||
|
const color = colorMap[severity] || '#f97316';
|
||||||
|
|
||||||
|
return L.divIcon({
|
||||||
|
className: "custom-marker",
|
||||||
|
html: `<svg width="32" height="40" viewBox="0 0 32 40" fill="${color}" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16 0C8.27 0 2 6.27 2 14c0 8 14 26 14 26s14-18 14-26c0-7.73-6.27-14-14-14z"/>
|
||||||
|
<circle cx="16" cy="14" r="5" fill="white"/>
|
||||||
|
<text x="16" y="16" text-anchor="middle" dy=".3em" font-size="10" font-weight="bold" fill="${color}">!</text>
|
||||||
|
</svg>`,
|
||||||
|
iconSize: [32, 40],
|
||||||
|
iconAnchor: [16, 40],
|
||||||
|
popupAnchor: [0, -40]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoadDamagePopup(record) {
|
||||||
|
const imageHtml = record.gambar ? `<img src="${record.gambar}" alt="Foto jalan rusak" class="mt-2 w-full rounded-lg max-h-48 object-cover">` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="w-full p-3">
|
||||||
|
<h4 class="font-bold text-slate-800">Laporan Jalan Rusak</h4>
|
||||||
|
<p class="mt-1 text-sm text-slate-600"><strong>Jenis:</strong> ${record.jenis_kerusakan}</p>
|
||||||
|
<p class="text-sm text-slate-600"><strong>Tingkat:</strong>
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-bold ${
|
||||||
|
record.severity === 'ringan' ? 'bg-yellow-100 text-yellow-700' :
|
||||||
|
record.severity === 'sedang' ? 'bg-orange-100 text-orange-700' :
|
||||||
|
'bg-red-100 text-red-700'
|
||||||
|
}">${record.severity}</span>
|
||||||
|
</p>
|
||||||
|
${record.deskripsi ? `<p class="mt-1 text-sm text-slate-600"><strong>Deskripsi:</strong> ${record.deskripsi}</p>` : ''}
|
||||||
|
<p class="mt-1 text-xs text-slate-500">📍 ${record.latitude.toFixed(5)}, ${record.longitude.toFixed(5)}</p>
|
||||||
|
${imageHtml}
|
||||||
|
<div class="mt-3 flex gap-2 border-t pt-2">
|
||||||
|
<button onclick="deleteRoadDamage(${record.id})" class="flex-1 rounded bg-red-50 px-2 py-1 text-xs font-bold text-red-700 hover:bg-red-100">Hapus</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRoadDamage(force = false) {
|
||||||
|
if (!force && (document.hidden || suppressAutoReload)) return;
|
||||||
|
|
||||||
|
fetch('get_jalan_rusak.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 tidak valid');
|
||||||
|
}
|
||||||
|
|
||||||
|
roadDamageRecords = data;
|
||||||
|
roadDamageLayer.clearLayers();
|
||||||
|
|
||||||
|
data.forEach(record => {
|
||||||
|
const marker = L.marker([record.latitude, record.longitude], {
|
||||||
|
icon: createRoadDamageIcon(record.severity)
|
||||||
|
}).addTo(roadDamageLayer);
|
||||||
|
|
||||||
|
marker.bindPopup(getRoadDamagePopup(record));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Gagal memuat data jalan rusak:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRoadDamage(id) {
|
||||||
|
if (!confirm('Hapus laporan jalan rusak ini?')) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('id', id);
|
||||||
|
|
||||||
|
fetch('delete_jalan_rusak.php', { method: 'POST', body: formData })
|
||||||
|
.then(res => parseJsonResponse(res, 'delete_jalan_rusak.php'))
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage('🗑️ Laporan berhasil dihapus', 'bg-orange-600 text-white');
|
||||||
|
loadRoadDamage(true);
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Gagal menghapus laporan');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => showMessage(err.message || 'Gagal menghapus laporan', 'bg-red-600 text-white'));
|
||||||
|
}
|
||||||
|
|
||||||
function updateFeatureMeasurePreview() {
|
function updateFeatureMeasurePreview() {
|
||||||
const type = document.getElementById('featureType').value;
|
const type = document.getElementById('featureType').value;
|
||||||
const layer = pendingShape;
|
const layer = pendingShape;
|
||||||
@@ -1646,6 +1910,12 @@
|
|||||||
// Klik Peta untuk Input Baru
|
// Klik Peta untuk Input Baru
|
||||||
map.on('click', function(e) {
|
map.on('click', function(e) {
|
||||||
if (isFeatureDrawing) return;
|
if (isFeatureDrawing) return;
|
||||||
|
|
||||||
|
if (appMode === 'roadDamage') {
|
||||||
|
openRoadDamageModal(e.latlng.lat, e.latlng.lng);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (appMode !== 'point') return;
|
if (appMode !== 'point') return;
|
||||||
|
|
||||||
const lat = e.latlng.lat;
|
const lat = e.latlng.lat;
|
||||||
@@ -1754,6 +2024,7 @@
|
|||||||
document.getElementById('modePolylineBtn').addEventListener('click', () => setDataMode('polyline'));
|
document.getElementById('modePolylineBtn').addEventListener('click', () => setDataMode('polyline'));
|
||||||
document.getElementById('modePolygonBtn').addEventListener('click', () => setDataMode('polygon'));
|
document.getElementById('modePolygonBtn').addEventListener('click', () => setDataMode('polygon'));
|
||||||
document.getElementById('modeChoroplethBtn').addEventListener('click', () => setDataMode('choropleth'));
|
document.getElementById('modeChoroplethBtn').addEventListener('click', () => setDataMode('choropleth'));
|
||||||
|
document.getElementById('modeRoadDamageBtn').addEventListener('click', () => setDataMode('roadDamage'));
|
||||||
document.getElementById('drawStatus').addEventListener('change', () => {
|
document.getElementById('drawStatus').addEventListener('change', () => {
|
||||||
const type = document.getElementById('featureType').value;
|
const type = document.getElementById('featureType').value;
|
||||||
document.getElementById('featureStatus').value = document.getElementById('drawStatus').value;
|
document.getElementById('featureStatus').value = document.getElementById('drawStatus').value;
|
||||||
@@ -1831,6 +2102,30 @@
|
|||||||
document.getElementById('newFeatureModal').addEventListener('click', (event) => {
|
document.getElementById('newFeatureModal').addEventListener('click', (event) => {
|
||||||
if (event.target.id === 'newFeatureModal') closeNewFeatureModal(true);
|
if (event.target.id === 'newFeatureModal') closeNewFeatureModal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Road Damage Modal Event Listeners
|
||||||
|
document.getElementById('saveRoadDamageBtn').addEventListener('click', saveRoadDamage);
|
||||||
|
document.getElementById('cancelRoadDamageBtn').addEventListener('click', closeRoadDamageModal);
|
||||||
|
document.getElementById('closeRoadDamageModalBtn').addEventListener('click', closeRoadDamageModal);
|
||||||
|
document.getElementById('roadDamageImage').addEventListener('change', (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
const preview = document.getElementById('imagePreview');
|
||||||
|
const previewImg = document.getElementById('previewImg');
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
previewImg.src = e.target.result;
|
||||||
|
preview.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
preview.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('roadDamageModal').addEventListener('click', (event) => {
|
||||||
|
if (event.target.id === 'roadDamageModal') closeRoadDamageModal();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
include '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('uploads')) {
|
||||||
|
mkdir('uploads', 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate nama file unik
|
||||||
|
$gambar = 'uploads/' . time() . '_' . basename($filename);
|
||||||
|
|
||||||
|
if (!move_uploaded_file($_FILES['gambar']['tmp_name'], $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();
|
||||||
|
?>
|
||||||
Reference in New Issue
Block a user