feat(landing-page):penambahan landing page utama pdemisah antar fitur
This commit is contained in:
@@ -0,0 +1,651 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sistem GIS — Parsil Tanah</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<!-- Leaflet CSS & Draw CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Outfit', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body { height: 100%; font-family: 'Outfit', sans-serif; overflow: hidden; }
|
||||
#map {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.custom-popup .leaflet-popup-content-wrapper {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
padding: 4px;
|
||||
}
|
||||
.custom-popup .leaflet-popup-content {
|
||||
margin: 12px;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800">
|
||||
|
||||
<!-- App Shell -->
|
||||
<div class="relative w-full h-screen flex overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="w-[380px] h-full bg-white border-r border-slate-200/80 flex flex-col z-10 shadow-lg relative">
|
||||
<!-- Header -->
|
||||
<div class="p-6 border-b border-slate-100 flex flex-col gap-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="../index.html" class="flex items-center justify-center w-8 h-8 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
</a>
|
||||
<div class="flex items-center gap-1.5 text-xs font-semibold text-indigo-600 bg-indigo-50 px-2.5 py-1 rounded-full uppercase tracking-wider">
|
||||
<span>Informatika UNTAN</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold text-slate-800 mt-2">Parsil Tanah</h1>
|
||||
<p class="text-xs text-slate-500">Kelola dan ukur luas parsel tanah (polygon) secara realtime.</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="px-6 py-4 bg-slate-50 border-b border-slate-100 flex items-center justify-between">
|
||||
<span class="text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Parsil</span>
|
||||
<span id="parselCount" class="text-base font-black text-indigo-600">0</span>
|
||||
</div>
|
||||
|
||||
<!-- List & Search -->
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
<div class="p-4 border-b border-slate-100">
|
||||
<div class="relative">
|
||||
<input id="searchQuery" type="text" placeholder="Cari nama parsil atau status..." class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-10 pr-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<svg class="absolute left-3.5 top-3.5 text-slate-400" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="parselList" class="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
<!-- Loading Placeholder -->
|
||||
<div class="text-center py-8 text-slate-400 text-sm">
|
||||
<svg class="animate-spin h-6 w-6 mx-auto mb-2 text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
Memuat data parsel...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions Panel -->
|
||||
<div class="p-4 bg-indigo-50/50 border-t border-indigo-100/50 text-xs text-indigo-800 flex gap-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0 mt-0.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||
<div>
|
||||
<span class="font-bold">Tips:</span> Gunakan tombol gambar di peta (sisi kanan) untuk menggambar batas parsel tanah baru, lalu isi nama pemilik & status sertifikat.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Container -->
|
||||
<div class="flex-1 h-full relative">
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Toast notification -->
|
||||
<div id="toast" class="absolute top-4 right-4 z-[2000] hidden items-center gap-3 px-4 py-3 bg-slate-900/90 text-white rounded-xl shadow-xl backdrop-blur-md transition transform scale-95 opacity-0">
|
||||
<span id="toastMessage" class="text-sm font-medium">Notifikasi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Input Parsil Baru -->
|
||||
<div id="newParselModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Simpan Parsil Tanah</h3>
|
||||
<button onclick="closeNewParselModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Pemilik / Objek</label>
|
||||
<input id="newParselName" type="text" placeholder="Contoh: Tanah Kavling Pak Budi" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Status Sertifikat</label>
|
||||
<select id="newParselStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<option value="SHM">SHM (Hak Milik)</option>
|
||||
<option value="HGB">HGB (Guna Bangunan)</option>
|
||||
<option value="HGU">HGU (Guna Usaha)</option>
|
||||
<option value="HP">HP (Hak Pakai)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Luas Tanah</label>
|
||||
<input id="newParselArea" type="text" readonly class="w-full bg-slate-100 border border-slate-200 rounded-xl px-4 py-2.5 text-sm text-slate-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-3">
|
||||
<button onclick="saveNewParsel()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg shadow-indigo-600/10 hover:shadow-indigo-600/20 transition">Simpan Parsil</button>
|
||||
<button onclick="closeNewParselModal()" class="flex-1 bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Edit Parsil -->
|
||||
<div id="editParselModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
||||
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
||||
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<h3 class="font-bold text-slate-800">Edit Atribut Parsil</h3>
|
||||
<button onclick="closeEditParselModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<input type="hidden" id="editParselId">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Pemilik / Objek</label>
|
||||
<input id="editParselName" type="text" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Status Sertifikat</label>
|
||||
<select id="editParselStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
||||
<option value="SHM">SHM (Hak Milik)</option>
|
||||
<option value="HGB">HGB (Guna Bangunan)</option>
|
||||
<option value="HGU">HGU (Guna Usaha)</option>
|
||||
<option value="HP">HP (Hak Pakai)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Luas Tanah</label>
|
||||
<input id="editParselArea" type="text" readonly class="w-full bg-slate-100 border border-slate-200 rounded-xl px-4 py-2.5 text-sm text-slate-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-2">
|
||||
<button onclick="updateParsilAttr()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan</button>
|
||||
<button onclick="deleteParsilDirect()" class="bg-red-50 hover:bg-red-100 text-red-600 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Hapus</button>
|
||||
<button onclick="closeEditParselModal()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet & Turf JS Libraries -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Init Map Pontianak
|
||||
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
||||
|
||||
// Custom Zoom Control at Top-Right
|
||||
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||
|
||||
// Tile Layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Draw Layer & Control
|
||||
const drawnItems = new L.FeatureGroup().addTo(map);
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polygon: {
|
||||
allowIntersection: false,
|
||||
showArea: true,
|
||||
shapeOptions: {
|
||||
color: '#6366f1',
|
||||
fillColor: '#818cf8',
|
||||
fillOpacity: 0.35,
|
||||
weight: 3
|
||||
}
|
||||
},
|
||||
polyline: false,
|
||||
circle: false,
|
||||
marker: false,
|
||||
circlemarker: false,
|
||||
rectangle: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnItems,
|
||||
remove: true
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
let records = [];
|
||||
let pendingLayer = null;
|
||||
let pendingArea = 0;
|
||||
|
||||
// Colors based on Status Sertifikat
|
||||
const STATUS_COLORS = {
|
||||
'SHM': '#10b981', // green
|
||||
'HGB': '#3b82f6', // blue
|
||||
'HGU': '#8b5cf6', // purple
|
||||
'HP': '#64748b' // slate
|
||||
};
|
||||
|
||||
// Load data from DB
|
||||
async function loadParsil() {
|
||||
try {
|
||||
const response = await fetch('api/get_features.php');
|
||||
if (!response.ok) throw new Error('HTTP Error');
|
||||
records = await response.json();
|
||||
renderParsilList(records);
|
||||
displayParsilOnMap(records);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Gagal memuat data parsel.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Render List in Sidebar
|
||||
function renderParsilList(items) {
|
||||
const list = document.getElementById('parselList');
|
||||
document.getElementById('parselCount').textContent = items.length;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = `
|
||||
<div class="text-center py-12 text-slate-400 text-sm border-2 border-dashed border-slate-200 rounded-2xl">
|
||||
<svg class="h-8 w-8 mx-auto mb-2 text-slate-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
|
||||
Belum ada data parsel tanah.
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(item => {
|
||||
const color = STATUS_COLORS[item.status_objek] || '#4f46e5';
|
||||
return `
|
||||
<div onclick="focusParsil(${item.id})" class="p-4 bg-white border border-slate-200 rounded-2xl hover:border-indigo-500 hover:shadow-md cursor-pointer transition flex flex-col gap-2.5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="font-bold text-slate-800 text-sm leading-tight line-clamp-1">${escapeHtml(item.nama_objek)}</span>
|
||||
<span class="text-[9px] font-extrabold uppercase tracking-wider text-white px-2 py-0.5 rounded-full" style="background-color: ${color}">
|
||||
${item.status_objek}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-slate-500">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M21 12H3"/><path d="M12 3v18"/></svg>
|
||||
Luas: <strong>${Math.round(item.nilai_ukur).toLocaleString('id-ID')} m²</strong>
|
||||
</span>
|
||||
<span class="text-[10px] text-slate-400">${formatDate(item.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Helper to parse geometry coordinates from GeoJSON or raw format
|
||||
function parseGeometryToCoords(geom, type) {
|
||||
if (!geom) return null;
|
||||
if (typeof geom === 'object' && geom.type && geom.coordinates) {
|
||||
if (geom.type === 'Polygon' || geom.type === 'MultiPolygon') {
|
||||
const rings = geom.coordinates;
|
||||
return rings.map(ring => ring.map(coord => [coord[1], coord[0]]));
|
||||
} else if (geom.type === 'LineString' || geom.type === 'MultiLineString') {
|
||||
const coords = geom.coordinates;
|
||||
if (geom.type === 'LineString') {
|
||||
return coords.map(coord => [coord[1], coord[0]]);
|
||||
} else {
|
||||
return coords.map(line => line.map(coord => [coord[1], coord[0]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return geom;
|
||||
}
|
||||
|
||||
// Display Markers/Polygons on Map
|
||||
let mapLayers = {};
|
||||
function displayParsilOnMap(items) {
|
||||
drawnItems.clearLayers();
|
||||
mapLayers = {};
|
||||
|
||||
items.forEach(item => {
|
||||
if (!item.geometry_data) return;
|
||||
|
||||
const coords = parseGeometryToCoords(item.geometry_data, 'polygon');
|
||||
if (!coords) return;
|
||||
|
||||
const color = STATUS_COLORS[item.status_objek] || '#4f46e5';
|
||||
const layer = L.polygon(coords, {
|
||||
color: color,
|
||||
fillColor: color,
|
||||
fillOpacity: 0.2,
|
||||
weight: 2.5
|
||||
}).addTo(drawnItems);
|
||||
|
||||
layer.on('click', (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
openEditModal(item);
|
||||
});
|
||||
|
||||
// bind simple tooltip
|
||||
layer.bindTooltip(`<b>${item.nama_objek}</b><br>Luas: ${Math.round(item.nilai_ukur).toLocaleString('id-ID')} m²`, {
|
||||
sticky: true,
|
||||
className: 'text-xs rounded-lg border-slate-100 font-sans'
|
||||
});
|
||||
|
||||
mapLayers[item.id] = layer;
|
||||
});
|
||||
}
|
||||
|
||||
// Focus & Zoom to parcel
|
||||
function focusParsil(id) {
|
||||
const layer = mapLayers[id];
|
||||
if (layer) {
|
||||
map.fitBounds(layer.getBounds(), { padding: [40, 40], maxZoom: 18 });
|
||||
}
|
||||
}
|
||||
|
||||
// Leaflet Draw Event Handler
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
const layer = e.layer;
|
||||
pendingLayer = layer;
|
||||
|
||||
const geo = layer.toGeoJSON();
|
||||
pendingArea = turf.area(geo); // calculate area using turf in m2
|
||||
|
||||
// open form modal
|
||||
document.getElementById('newParselName').value = '';
|
||||
document.getElementById('newParselStatus').value = 'SHM';
|
||||
document.getElementById('newParselArea').value = `${Math.round(pendingArea).toLocaleString('id-ID')} m²`;
|
||||
|
||||
openNewParselModal();
|
||||
});
|
||||
|
||||
// Edit polygon geometry directly inside map edit event
|
||||
map.on(L.Draw.Event.EDITED, function (e) {
|
||||
const layers = e.layers;
|
||||
layers.eachLayer(async function (layer) {
|
||||
// Find DB ID for this layer
|
||||
let dbId = null;
|
||||
for (let id in mapLayers) {
|
||||
if (mapLayers[id] === layer) {
|
||||
dbId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dbId) {
|
||||
const geo = layer.toGeoJSON();
|
||||
const area = turf.area(geo);
|
||||
const geomString = JSON.stringify(geo.geometry);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', dbId);
|
||||
formData.append('geometry', geomString);
|
||||
formData.append('nilai', area);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/update_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Geometri parsel berhasil diperbarui.', 'success');
|
||||
loadParsil();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('Gagal menyimpan perubahan geometri.', 'error');
|
||||
loadParsil();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete geometry via Leaflet Draw Delete tool
|
||||
map.on(L.Draw.Event.DELETED, function (e) {
|
||||
const layers = e.layers;
|
||||
layers.eachLayer(async function (layer) {
|
||||
let dbId = null;
|
||||
for (let id in mapLayers) {
|
||||
if (mapLayers[id] === layer) {
|
||||
dbId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dbId) {
|
||||
const formData = new FormData();
|
||||
formData.append('id', dbId);
|
||||
try {
|
||||
const res = await fetch('api/delete_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Parsil berhasil dihapus.', 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
setTimeout(loadParsil, 500);
|
||||
});
|
||||
|
||||
// Save new parsel to DB
|
||||
async function saveNewParsel() {
|
||||
const name = document.getElementById('newParselName').value.trim();
|
||||
const status = document.getElementById('newParselStatus').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('Masukkan nama pemilik atau objek parsel.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const geo = pendingLayer.toGeoJSON();
|
||||
const geomString = JSON.stringify(geo.geometry);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('nama', name);
|
||||
formData.append('status', status);
|
||||
formData.append('type', 'polygon');
|
||||
formData.append('geometry', geomString);
|
||||
formData.append('nilai', pendingArea);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/save_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Parsil berhasil disimpan!', 'success');
|
||||
closeNewParselModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menyimpan parsel.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Open edit attribute modal
|
||||
function openEditModal(record) {
|
||||
document.getElementById('editParselId').value = record.id;
|
||||
document.getElementById('editParselName').value = record.nama_objek;
|
||||
document.getElementById('editParselStatus').value = record.status_objek;
|
||||
document.getElementById('editParselArea').value = `${Math.round(record.nilai_ukur).toLocaleString('id-ID')} m²`;
|
||||
|
||||
openEditParselModal();
|
||||
}
|
||||
|
||||
// Save edited attributes
|
||||
async function updateParsilAttr() {
|
||||
const id = document.getElementById('editParselId').value;
|
||||
const name = document.getElementById('editParselName').value.trim();
|
||||
const status = document.getElementById('editParselStatus').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('Nama tidak boleh kosong.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('nama', name);
|
||||
formData.append('status', status);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/update_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Atribut parsel berhasil disimpan.', 'success');
|
||||
closeEditParselModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal memperbarui atribut.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete parsel direct from modal
|
||||
async function deleteParsilDirect() {
|
||||
const id = document.getElementById('editParselId').value;
|
||||
if (!confirm('Hapus parsel tanah ini?')) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/delete_feature.php', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
showToast('Parsil berhasil dihapus.', 'success');
|
||||
closeEditParselModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Gagal menghapus parsel.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Search Filter
|
||||
document.getElementById('searchQuery').addEventListener('input', function (e) {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = records.filter(rec =>
|
||||
rec.nama_objek.toLowerCase().includes(query) ||
|
||||
rec.status_objek.toLowerCase().includes(query)
|
||||
);
|
||||
renderParsilList(filtered);
|
||||
});
|
||||
|
||||
// Modals management helpers
|
||||
function openNewParselModal() {
|
||||
const modal = document.getElementById('newParselModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeNewParselModal() {
|
||||
const modal = document.getElementById('newParselModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
if (pendingLayer) {
|
||||
map.removeLayer(pendingLayer);
|
||||
pendingLayer = null;
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function openEditParselModal() {
|
||||
const modal = document.getElementById('editParselModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
||||
modal.children[0].classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeEditParselModal() {
|
||||
const modal = document.getElementById('editParselModal');
|
||||
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
||||
modal.children[0].classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// Toast Helper
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast');
|
||||
const msg = document.getElementById('toastMessage');
|
||||
msg.textContent = message;
|
||||
|
||||
toast.classList.remove('hidden', 'bg-red-500', 'bg-emerald-500', 'bg-indigo-500');
|
||||
toast.classList.add('flex');
|
||||
|
||||
if (type === 'error') {
|
||||
toast.classList.add('bg-red-500');
|
||||
} else if (type === 'success') {
|
||||
toast.classList.add('bg-emerald-500');
|
||||
} else {
|
||||
toast.classList.add('bg-indigo-500');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('scale-95', 'opacity-0');
|
||||
toast.classList.add('scale-100', 'opacity-100');
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('scale-100', 'opacity-100');
|
||||
toast.classList.add('scale-95', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hidden');
|
||||
}, 150);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// General helpers
|
||||
function escapeHtml(str) {
|
||||
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const d = new Date(dateString.replace(' ', 'T'));
|
||||
return d.toLocaleDateString('id-ID', { day: 'numeric', month: 'short' });
|
||||
}
|
||||
|
||||
// Startup load
|
||||
loadParsil();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user