// ============================================
// app.js — Main Application Logic
// ============================================
// ------- API Endpoints -------
const API = {
SPBU: {
READ: 'backend/read.php',
CREATE: 'backend/create.php',
UPDATE: 'backend/update.php',
DELETE: 'backend/delete.php'
},
JALAN: {
READ: 'backend/read_jalan.php',
CREATE: 'backend/create_jalan.php',
UPDATE: 'backend/update_jalan.php',
DELETE: 'backend/delete_jalan.php'
},
PARSIL: {
READ: 'backend/read_parsil.php',
CREATE: 'backend/create_parsil.php',
UPDATE: 'backend/update_parsil.php',
DELETE: 'backend/delete_parsil.php'
},
IBADAH: {
READ: 'backend/read_ibadah.php',
CREATE: 'backend/create_ibadah.php',
UPDATE: 'backend/update_ibadah.php',
DELETE: 'backend/delete_ibadah.php'
},
MISKIN: {
READ: 'backend/read_miskin.php',
CREATE: 'backend/create_miskin.php',
UPDATE: 'backend/update_miskin.php',
DELETE: 'backend/delete_miskin.php'
}
};
// ------- Global State -------
window.appState = {
data: {
spbu: [],
jalan: [],
parsil: [],
ibadah: [],
miskin: []
},
currentPanel: 'panel-map',
editingId: null,
editingType: null,
mapInstance: null,
layers: {}
};
// ============================================
// Map Toolbar — Global Functions (called via onclick in HTML)
// ============================================
window._mapClickMode = null;
window.mapToolActivatePoint = function(type, label) {
window._mapClickMode = type;
document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('map-add-' + type);
if (btn) btn.classList.add('active');
const status = document.getElementById('map-tool-status');
const cancel = document.getElementById('map-tool-cancel');
if (status) {
status.style.display = 'block';
status.innerHTML = '📍 Klik lokasi di peta
untuk tambah ' + label + '';
}
if (cancel) cancel.style.display = 'block';
const inst = window.appState.mapInstance;
if (inst) inst.getContainer().style.cursor = 'crosshair';
};
window.mapToolActivateDraw = function(type) {
const inst = window.appState.mapInstance;
document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('map-add-' + type);
if (btn) btn.classList.add('active');
const status = document.getElementById('map-tool-status');
const cancel = document.getElementById('map-tool-cancel');
if (inst && inst.pm && typeof inst.pm.enableDraw === 'function') {
inst.pm.enableDraw(type === 'jalan' ? 'Line' : 'Polygon');
if (status) {
status.style.display = 'block';
status.innerHTML = type === 'jalan'
? '✏️ Gambar garis
Klik tiap titik, double-klik selesai'
: '✏️ Gambar area
Klik tiap sudut, double-klik selesai';
}
if (cancel) cancel.style.display = 'block';
} else {
if (type === 'jalan') openAddJalanModal();
else openAddParsilModal();
}
};
window.mapToolCancel = function() {
window._mapClickMode = null;
document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active'));
const status = document.getElementById('map-tool-status');
const cancel = document.getElementById('map-tool-cancel');
if (status) status.style.display = 'none';
if (cancel) cancel.style.display = 'none';
const inst = window.appState.mapInstance;
if (inst) {
inst.getContainer().style.cursor = '';
if (inst.pm && typeof inst.pm.disableDraw === 'function') inst.pm.disableDraw();
}
};
// ============================================
// Utility Functions
// ============================================
function showToast(message, type = 'info', duration = 3000) {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const icon = {
success: '✓',
error: '✗',
info: 'i',
warning: '!'
}[type] || 'i';
toast.innerHTML = `${icon}${message}`;
container.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, duration);
}
function showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('show');
}
}
function hideModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('show');
}
}
function switchPanel(panelId) {
// Hide all panels
document.querySelectorAll('.content-panel').forEach(p => {
p.classList.remove('active');
});
// Deactivate all sidebar items
document.querySelectorAll('.sidebar-item').forEach(item => {
item.classList.remove('active');
});
// Show selected panel
const panel = document.getElementById(panelId);
if (panel) {
panel.classList.add('active');
// Mark sidebar item as active
const sidebarItem = document.querySelector(`[data-panel="${panelId}"]`);
if (sidebarItem) {
sidebarItem.classList.add('active');
}
}
// Update page title
const sidebarItem = document.querySelector(`[data-panel="${panelId}"]`);
if (sidebarItem) {
const title = sidebarItem.querySelector('.sidebar-item-text')?.textContent || 'Dashboard';
document.getElementById('page-title').textContent = title;
document.getElementById('breadcrumb-text').textContent = title;
}
window.appState.currentPanel = panelId;
}
// ============================================
// Data Loading & Management
// ============================================
async function loadAllData() {
try {
const [spbuRes, jalanRes, parsilRes, ibadahRes, miskinRes] = await Promise.all([
fetch(API.SPBU.READ),
fetch(API.JALAN.READ),
fetch(API.PARSIL.READ),
fetch(API.IBADAH.READ),
fetch(API.MISKIN.READ)
]);
const spbuData = await spbuRes.json();
const jalanData = await jalanRes.json();
const parsilData = await parsilRes.json();
const ibadahData = await ibadahRes.json();
const miskinData = await miskinRes.json();
window.appState.data.spbu = spbuData.success ? spbuData.data : [];
window.appState.data.jalan = jalanData.success ? jalanData.data : [];
window.appState.data.parsil = parsilData.success ? parsilData.data : [];
window.appState.data.ibadah = ibadahData.success ? ibadahData.data : [];
window.appState.data.miskin = miskinData.success ? miskinData.data : [];
updateBadges();
updateAnalytics();
renderTables();
renderMapLayers();
} catch (err) {
console.error('Error loading data:', err);
showToast('Gagal memuat data', 'error');
}
}
function updateBadges() {
document.getElementById('badge-spbu').textContent = window.appState.data.spbu.length;
document.getElementById('badge-jalan').textContent = window.appState.data.jalan.length;
document.getElementById('badge-parsil').textContent = window.appState.data.parsil.length;
document.getElementById('badge-ibadah').textContent = window.appState.data.ibadah.length;
document.getElementById('badge-miskin').textContent = window.appState.data.miskin.length;
const totalData = window.appState.data.spbu.length +
window.appState.data.jalan.length +
window.appState.data.parsil.length +
window.appState.data.ibadah.length +
window.appState.data.miskin.length;
document.getElementById('total-data-count').textContent = `${totalData} Data`;
}
function updateAnalytics() {
document.getElementById('stat-spbu').textContent = window.appState.data.spbu.length;
document.getElementById('stat-jalan').textContent = window.appState.data.jalan.length;
document.getElementById('stat-parsil').textContent = window.appState.data.parsil.length;
document.getElementById('stat-ibadah').textContent = window.appState.data.ibadah.length;
document.getElementById('stat-miskin').textContent = window.appState.data.miskin.length;
// Count SPBU 24 jam vs tidak
const spbu24 = window.appState.data.spbu.filter(s => s.status === '24jam').length;
const spbuTidak = window.appState.data.spbu.filter(s => s.status === 'tidak').length;
document.getElementById('stat-spbu-trend').textContent = `${spbu24} aktif 24jam`;
// Count jalan by type
const jalanNasional = window.appState.data.jalan.filter(j => j.status === 'Nasional').length;
document.getElementById('stat-jalan-trend').textContent = `${jalanNasional} Nasional`;
}
// ============================================
// Table Rendering
// ============================================
function renderTables() {
renderSPBUTable();
renderJalanTable();
renderParsilTable();
renderIbadahTable();
renderMiskinTable();
}
function renderSPBUTable(filter = 'all', search = '') {
const tbody = document.getElementById('table-spbu-body');
let data = window.appState.data.spbu;
if (filter !== 'all') {
data = data.filter(item => item.status === filter);
}
if (search) {
data = data.filter(item =>
item.nama.toLowerCase().includes(search.toLowerCase()) ||
item.no_spbu.toLowerCase().includes(search.toLowerCase())
);
}
if (data.length === 0) {
tbody.innerHTML = '
| Tidak ada data |
';
return;
}
tbody.innerHTML = data.map(item => `
| ${item.nama} |
${item.no_spbu} |
${item.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'} |
${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)} |
|
`).join('');
}
function renderJalanTable(filter = 'all', search = '') {
const tbody = document.getElementById('table-jalan-body');
let data = window.appState.data.jalan;
if (filter !== 'all') {
data = data.filter(item => item.status === filter);
}
if (search) {
data = data.filter(item => item.nama_jalan.toLowerCase().includes(search.toLowerCase()));
}
if (data.length === 0) {
tbody.innerHTML = '| Tidak ada data |
';
return;
}
tbody.innerHTML = data.map(item => `
| ${item.nama_jalan} |
${item.status} |
${item.panjang.toFixed(0)} m |
|
`).join('');
}
function renderParsilTable(filter = 'all', search = '') {
const tbody = document.getElementById('table-parsil-body');
let data = window.appState.data.parsil;
if (filter !== 'all') {
data = data.filter(item => item.status === filter);
}
if (search) {
data = data.filter(item => item.nama_pemilik.toLowerCase().includes(search.toLowerCase()));
}
if (data.length === 0) {
tbody.innerHTML = '| Tidak ada data |
';
return;
}
tbody.innerHTML = data.map(item => `
| ${item.nama_pemilik} |
${item.status} |
${item.luas.toFixed(0)} m2 |
|
`).join('');
}
function renderIbadahTable(search = '') {
const tbody = document.getElementById('table-ibadah-body');
let data = window.appState.data.ibadah;
if (search) {
data = data.filter(item =>
item.nama.toLowerCase().includes(search.toLowerCase()) ||
item.alamat.toLowerCase().includes(search.toLowerCase())
);
}
if (data.length === 0) {
tbody.innerHTML = '| Tidak ada data |
';
return;
}
tbody.innerHTML = data.map(item => `
| ${item.nama} |
${item.alamat.substring(0, 30)}... |
${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)} |
|
`).join('');
}
function renderMiskinTable(search = '') {
const tbody = document.getElementById('table-miskin-body');
let data = window.appState.data.miskin;
if (search) {
data = data.filter(item =>
item.nama_kk.toLowerCase().includes(search.toLowerCase())
);
}
if (data.length === 0) {
tbody.innerHTML = '| Tidak ada data |
';
return;
}
tbody.innerHTML = data.map(item => `
| ${item.nama_kk} |
${item.keterangan || '-'} |
${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)} |
|
`).join('');
}
// ============================================
// CRUD Operations - SPBU
// ============================================
function openAddSPBUModal() {
document.getElementById('form-spbu-id').value = '';
document.getElementById('form-spbu-nama').value = '';
document.getElementById('form-spbu-no').value = '';
document.getElementById('form-spbu-status').value = '';
document.getElementById('form-spbu-lat').value = '';
document.getElementById('form-spbu-lng').value = '';
document.getElementById('modal-spbu-title').textContent = 'Add SPBU';
window.appState.editingId = null;
window.appState.editingType = 'spbu';
showModal('modal-spbu');
}
async function editSPBU(id) {
const item = window.appState.data.spbu.find(s => s.id == id);
if (!item) return;
document.getElementById('form-spbu-id').value = item.id;
document.getElementById('form-spbu-nama').value = item.nama;
document.getElementById('form-spbu-no').value = item.no_spbu;
document.getElementById('form-spbu-status').value = item.status;
document.getElementById('form-spbu-lat').value = item.latitude;
document.getElementById('form-spbu-lng').value = item.longitude;
document.getElementById('modal-spbu-title').textContent = 'Edit SPBU';
window.appState.editingId = id;
window.appState.editingType = 'spbu';
showModal('modal-spbu');
}
async function saveSPBU() {
const id = document.getElementById('form-spbu-id').value;
const nama = document.getElementById('form-spbu-nama').value;
const no_spbu = document.getElementById('form-spbu-no').value;
const status = document.getElementById('form-spbu-status').value;
const latitude = parseFloat(document.getElementById('form-spbu-lat').value);
const longitude = parseFloat(document.getElementById('form-spbu-lng').value);
if (!nama || !no_spbu || !status || !latitude || !longitude) {
showToast('Silakan isi semua field', 'error');
return;
}
try {
const payload = { nama, no_spbu, status, latitude, longitude };
const endpoint = id ? API.SPBU.UPDATE : API.SPBU.CREATE;
if (id) payload.id = id;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.success) {
showToast(id ? 'SPBU updated successfully' : 'SPBU created successfully', 'success');
hideModal('modal-spbu');
loadAllData();
} else {
showToast(result.message || 'Error saving SPBU', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error saving SPBU', 'error');
}
}
async function deleteSPBU(id) {
if (!confirm('Are you sure you want to delete this SPBU?')) return;
try {
const res = await fetch(API.SPBU.DELETE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const result = await res.json();
if (result.success) {
showToast('SPBU deleted successfully', 'success');
loadAllData();
} else {
showToast(result.message || 'Error deleting SPBU', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error deleting SPBU', 'error');
}
}
// ============================================
// CRUD Operations - JALAN
// ============================================
function openAddJalanModal() {
document.getElementById('form-jalan-id').value = '';
document.getElementById('form-jalan-nama').value = '';
document.getElementById('form-jalan-status').value = '';
document.getElementById('form-jalan-panjang').value = '';
document.getElementById('form-jalan-geom').value = '';
document.getElementById('modal-jalan-title').textContent = 'Add Jalan';
showModal('modal-jalan');
}
async function editJalan(id) {
const item = window.appState.data.jalan.find(j => j.id == id);
if (!item) return;
document.getElementById('form-jalan-id').value = item.id;
document.getElementById('form-jalan-nama').value = item.nama_jalan;
document.getElementById('form-jalan-status').value = item.status;
document.getElementById('form-jalan-panjang').value = item.panjang;
document.getElementById('form-jalan-geom').value = item.geom;
document.getElementById('modal-jalan-title').textContent = 'Edit Jalan';
showModal('modal-jalan');
}
async function saveJalan() {
const id = document.getElementById('form-jalan-id').value;
const nama_jalan = document.getElementById('form-jalan-nama').value;
const status = document.getElementById('form-jalan-status').value;
const panjang = parseFloat(document.getElementById('form-jalan-panjang').value);
const geom = document.getElementById('form-jalan-geom').value;
if (!nama_jalan || !status || !panjang || !geom) {
showToast('Silakan isi semua field', 'error');
return;
}
try {
const payload = { nama_jalan, status, panjang, geom };
const endpoint = id ? API.JALAN.UPDATE : API.JALAN.CREATE;
if (id) payload.id = id;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.success) {
showToast(id ? 'Jalan updated successfully' : 'Jalan created successfully', 'success');
hideModal('modal-jalan');
loadAllData();
} else {
showToast(result.message || 'Error saving Jalan', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error saving Jalan', 'error');
}
}
async function deleteJalan(id) {
if (!confirm('Are you sure you want to delete this Jalan?')) return;
try {
const res = await fetch(API.JALAN.DELETE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const result = await res.json();
if (result.success) {
showToast('Jalan deleted successfully', 'success');
loadAllData();
} else {
showToast(result.message || 'Error deleting Jalan', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error deleting Jalan', 'error');
}
}
// ============================================
// CRUD Operations - PARSIL
// ============================================
function openAddParsilModal() {
document.getElementById('form-parsil-id').value = '';
document.getElementById('form-parsil-nama').value = '';
document.getElementById('form-parsil-status').value = '';
document.getElementById('form-parsil-luas').value = '';
document.getElementById('form-parsil-geom').value = '';
document.getElementById('modal-parsil-title').textContent = 'Add Parsil';
showModal('modal-parsil');
}
async function editParsil(id) {
const item = window.appState.data.parsil.find(p => p.id == id);
if (!item) return;
document.getElementById('form-parsil-id').value = item.id;
document.getElementById('form-parsil-nama').value = item.nama_pemilik;
document.getElementById('form-parsil-status').value = item.status;
document.getElementById('form-parsil-luas').value = item.luas;
document.getElementById('form-parsil-geom').value = item.geom;
document.getElementById('modal-parsil-title').textContent = 'Edit Parsil';
showModal('modal-parsil');
}
async function saveParsil() {
const id = document.getElementById('form-parsil-id').value;
const nama_pemilik = document.getElementById('form-parsil-nama').value;
const status = document.getElementById('form-parsil-status').value;
const luas = parseFloat(document.getElementById('form-parsil-luas').value);
const geom = document.getElementById('form-parsil-geom').value;
if (!nama_pemilik || !status || !luas || !geom) {
showToast('Silakan isi semua field', 'error');
return;
}
try {
const payload = { nama_pemilik, status, luas, geom };
const endpoint = id ? API.PARSIL.UPDATE : API.PARSIL.CREATE;
if (id) payload.id = id;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.success) {
showToast(id ? 'Parsil updated successfully' : 'Parsil created successfully', 'success');
hideModal('modal-parsil');
loadAllData();
} else {
showToast(result.message || 'Error saving Parsil', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error saving Parsil', 'error');
}
}
async function deleteParsil(id) {
if (!confirm('Are you sure you want to delete this Parsil?')) return;
try {
const res = await fetch(API.PARSIL.DELETE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const result = await res.json();
if (result.success) {
showToast('Parsil deleted successfully', 'success');
loadAllData();
} else {
showToast(result.message || 'Error deleting Parsil', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error deleting Parsil', 'error');
}
}
// ============================================
// CRUD Operations - IBADAH
// ============================================
function openAddIbadahModal() {
document.getElementById('form-ibadah-id').value = '';
document.getElementById('form-ibadah-nama').value = '';
document.getElementById('form-ibadah-alamat').value = '';
document.getElementById('form-ibadah-lat').value = '';
document.getElementById('form-ibadah-lng').value = '';
document.getElementById('modal-ibadah-title').textContent = 'Add Rumah Ibadah';
showModal('modal-ibadah');
}
async function editIbadah(id) {
const item = window.appState.data.ibadah.find(i => i.id == id);
if (!item) return;
document.getElementById('form-ibadah-id').value = item.id;
document.getElementById('form-ibadah-nama').value = item.nama;
document.getElementById('form-ibadah-alamat').value = item.alamat;
document.getElementById('form-ibadah-lat').value = item.latitude;
document.getElementById('form-ibadah-lng').value = item.longitude;
document.getElementById('modal-ibadah-title').textContent = 'Edit Rumah Ibadah';
showModal('modal-ibadah');
}
async function saveIbadah() {
const id = document.getElementById('form-ibadah-id').value;
const nama = document.getElementById('form-ibadah-nama').value;
const alamat = document.getElementById('form-ibadah-alamat').value;
const latitude = parseFloat(document.getElementById('form-ibadah-lat').value);
const longitude = parseFloat(document.getElementById('form-ibadah-lng').value);
if (!nama || !alamat || !latitude || !longitude) {
showToast('Silakan isi semua field', 'error');
return;
}
try {
const payload = { nama, alamat, latitude, longitude };
const endpoint = id ? API.IBADAH.UPDATE : API.IBADAH.CREATE;
if (id) payload.id = id;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.success) {
showToast(id ? 'Ibadah updated successfully' : 'Ibadah created successfully', 'success');
hideModal('modal-ibadah');
loadAllData();
} else {
showToast(result.message || 'Error saving Ibadah', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error saving Ibadah', 'error');
}
}
async function deleteIbadah(id) {
if (!confirm('Are you sure you want to delete this Rumah Ibadah?')) return;
try {
const res = await fetch(API.IBADAH.DELETE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const result = await res.json();
if (result.success) {
showToast('Ibadah deleted successfully', 'success');
loadAllData();
} else {
showToast(result.message || 'Error deleting Ibadah', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error deleting Ibadah', 'error');
}
}
// ============================================
// CRUD Operations - MISKIN
// ============================================
function openAddMiskinModal() {
document.getElementById('form-miskin-id').value = '';
document.getElementById('form-miskin-nama').value = '';
document.getElementById('form-miskin-ket').value = '';
document.getElementById('form-miskin-lat').value = '';
document.getElementById('form-miskin-lng').value = '';
document.getElementById('modal-miskin-title').textContent = 'Add Penduduk Miskin';
showModal('modal-miskin');
}
async function editMiskin(id) {
const item = window.appState.data.miskin.find(m => m.id == id);
if (!item) return;
document.getElementById('form-miskin-id').value = item.id;
document.getElementById('form-miskin-nama').value = item.nama_kk;
document.getElementById('form-miskin-ket').value = item.keterangan || '';
document.getElementById('form-miskin-lat').value = item.latitude;
document.getElementById('form-miskin-lng').value = item.longitude;
document.getElementById('modal-miskin-title').textContent = 'Edit Penduduk Miskin';
showModal('modal-miskin');
}
async function saveMiskin() {
const id = document.getElementById('form-miskin-id').value;
const nama_kk = document.getElementById('form-miskin-nama').value;
const keterangan = document.getElementById('form-miskin-ket').value;
const latitude = parseFloat(document.getElementById('form-miskin-lat').value);
const longitude = parseFloat(document.getElementById('form-miskin-lng').value);
if (!nama_kk || !latitude || !longitude) {
showToast('Silakan isi field yang diperlukan', 'error');
return;
}
try {
const payload = { nama_kk, keterangan, latitude, longitude };
const endpoint = id ? API.MISKIN.UPDATE : API.MISKIN.CREATE;
if (id) payload.id = id;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.success) {
showToast(id ? 'Miskin updated successfully' : 'Miskin created successfully', 'success');
hideModal('modal-miskin');
loadAllData();
} else {
showToast(result.message || 'Error saving Miskin', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error saving Miskin', 'error');
}
}
async function deleteMiskin(id) {
if (!confirm('Are you sure you want to delete this Penduduk Miskin?')) return;
try {
const res = await fetch(API.MISKIN.DELETE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const result = await res.json();
if (result.success) {
showToast('Miskin deleted successfully', 'success');
loadAllData();
} else {
showToast(result.message || 'Error deleting Miskin', 'error');
}
} catch (err) {
console.error('Error:', err);
showToast('Error deleting Miskin', 'error');
}
}
// ============================================
// Map Initialization & Rendering
// ============================================
function initMap() {
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
});
const map = L.map('map', {
center: [-0.0260, 109.3425],
zoom: 13,
zoomControl: true,
layers: [osm]
});
const baseMaps = {
"Standard (OSM)": osm,
"Dark": L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 })
};
L.control.layers(baseMaps).addTo(map);
// Initialize Geoman Controls (only if Geoman library is loaded)
if (map.pm && typeof map.pm.addControls === 'function') {
map.pm.addControls({
position: 'topleft',
drawCircle: false,
drawCircleMarker: false,
drawText: false,
editMode: false,
dragMode: false,
cutPolygon: false,
removalMode: false
});
}
// ALWAYS set mapInstance — regardless of whether Geoman loaded
window.appState.mapInstance = map;
window.appState.layers = {
spbu24: L.layerGroup().addTo(map),
spbuTidak: L.layerGroup().addTo(map),
jalan: L.layerGroup().addTo(map),
parsil: L.layerGroup().addTo(map),
ibadah: L.layerGroup().addTo(map),
miskin: L.layerGroup().addTo(map)
};
// Map click handler — capture coordinates for toolbar point mode
map.on('click', function(e) {
if (!window._mapClickMode) return;
const lat = e.latlng.lat.toFixed(7);
const lng = e.latlng.lng.toFixed(7);
const mode = window._mapClickMode;
window.mapToolCancel(); // reset mode
if (mode === 'spbu') {
openAddSPBUModal();
setTimeout(function() {
var el1 = document.getElementById('form-spbu-lat');
var el2 = document.getElementById('form-spbu-lng');
if (el1) el1.value = lat;
if (el2) el2.value = lng;
}, 80);
} else if (mode === 'ibadah') {
openAddIbadahModal();
setTimeout(function() {
var el1 = document.getElementById('form-ibadah-lat');
var el2 = document.getElementById('form-ibadah-lng');
if (el1) el1.value = lat;
if (el2) el2.value = lng;
}, 80);
} else if (mode === 'miskin') {
openAddMiskinModal();
setTimeout(function() {
var el1 = document.getElementById('form-miskin-lat');
var el2 = document.getElementById('form-miskin-lng');
if (el1) el1.value = lat;
if (el2) el2.value = lng;
}, 80);
}
});
// Geoman pm:create handler (for line/polygon)
if (map.pm) {
map.on('pm:create', function(e) {
var layer = e.layer;
var shape = e.shape;
window.mapToolCancel();
if (shape === 'Line' || shape === 'Polyline') {
openAddJalanModal();
var gj = layer.toGeoJSON();
var geomEl = document.getElementById('form-jalan-geom');
if (geomEl) geomEl.value = JSON.stringify(gj.geometry);
var panjEl = document.getElementById('form-jalan-panjang');
if (panjEl && window.turf) panjEl.value = Math.round(turf.length(gj, { units: 'kilometers' }) * 1000);
} else if (shape === 'Polygon' || shape === 'Rectangle') {
openAddParsilModal();
var gj = layer.toGeoJSON();
var geomEl = document.getElementById('form-parsil-geom');
if (geomEl) geomEl.value = JSON.stringify(gj.geometry);
var luasEl = document.getElementById('form-parsil-luas');
if (luasEl && window.turf) luasEl.value = Math.round(turf.area(gj));
}
map.removeLayer(layer);
});
}
return map;
}
function renderMapLayers() {
const map = window.appState.mapInstance;
if (!map) return;
// Clear layers
Object.values(window.appState.layers).forEach(layer => layer.clearLayers());
// Render SPBU
window.appState.data.spbu.forEach(item => {
const is24 = item.status === '24jam';
const haloClass = is24 ? 'spbu' : 'spbu-off';
const customIcon = L.divIcon({
className: 'custom-marker',
html: `⛽
`,
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
});
const marker = L.marker([item.latitude, item.longitude], { icon: customIcon })
.bindPopup(`${item.nama}
Status: ${is24 ? '24 Jam' : 'Tidak 24 Jam'}`);
const layer = item.status === '24jam' ? window.appState.layers.spbu24 : window.appState.layers.spbuTidak;
marker.addTo(layer);
});
// Render Jalan
window.appState.data.jalan.forEach(item => {
try {
const geom = JSON.parse(item.geom);
if (geom.coordinates) {
const colorMap = { 'Nasional': '#ef4444', 'Provinsi': '#eab308', 'Kabupaten': '#10b981' };
L.polyline(geom.coordinates, {
color: colorMap[item.status] || '#3b82f6',
weight: 3,
opacity: 0.7
}).bindPopup(`${item.nama_jalan}
Status: ${item.status}
Panjang: ${item.panjang.toFixed(0)}m`).addTo(window.appState.layers.jalan);
}
} catch (e) {}
});
// Render Parsil
window.appState.data.parsil.forEach(item => {
try {
const geom = JSON.parse(item.geom);
if (geom.coordinates) {
const colorMap = { 'SHM': '#3b82f6', 'HGB': '#8b5cf6', 'HGU': '#ec4899', 'HP': '#14b8a6' };
L.polygon(geom.coordinates, {
color: colorMap[item.status] || '#3b82f6',
weight: 2,
opacity: 0.6,
fillOpacity: 0.4
}).bindPopup(`${item.nama_pemilik}
Status: ${item.status}
Luas: ${item.luas.toFixed(0)}m2`).addTo(window.appState.layers.parsil);
}
} catch (e) {}
});
// Render Ibadah
window.appState.data.ibadah.forEach(item => {
const ibadahIcon = L.divIcon({
className: 'custom-marker',
html: `🕌
`,
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
});
const marker = L.marker([item.latitude, item.longitude], { icon: ibadahIcon })
.bindPopup(`${item.nama}
${item.alamat}`);
marker.addTo(window.appState.layers.ibadah);
});
// Render Miskin
window.appState.data.miskin.forEach(item => {
const miskinIcon = L.divIcon({
className: 'custom-marker',
html: `🏠
`,
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
});
const marker = L.marker([item.latitude, item.longitude], { icon: miskinIcon })
.bindPopup(`${item.nama_kk}
${item.keterangan || ''}`);
marker.addTo(window.appState.layers.miskin);
});
}
// ============================================
// Event Listeners & Initialization
// ============================================
document.addEventListener('DOMContentLoaded', () => {
// Initialize map
initMap();
// Sidebar navigation
document.querySelectorAll('[data-panel]').forEach(item => {
item.addEventListener('click', () => {
const panelId = item.dataset.panel;
switchPanel(panelId);
});
});
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.querySelector('.sidebar').classList.toggle('collapsed');
});
// Modal close buttons
document.querySelectorAll('[data-modal]').forEach(btn => {
btn.addEventListener('click', () => {
const modalId = btn.dataset.modal;
hideModal(modalId);
});
});
// Modal overlay close
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
hideModal(overlay.id);
}
});
});
// Add buttons
document.getElementById('btn-add-spbu').addEventListener('click', openAddSPBUModal);
document.getElementById('btn-add-jalan').addEventListener('click', openAddJalanModal);
document.getElementById('btn-add-parsil').addEventListener('click', openAddParsilModal);
document.getElementById('btn-add-ibadah').addEventListener('click', openAddIbadahModal);
document.getElementById('btn-add-miskin').addEventListener('click', openAddMiskinModal);
// Save buttons
document.getElementById('btn-save-spbu').addEventListener('click', saveSPBU);
document.getElementById('btn-save-jalan').addEventListener('click', saveJalan);
document.getElementById('btn-save-parsil').addEventListener('click', saveParsil);
document.getElementById('btn-save-ibadah').addEventListener('click', saveIbadah);
document.getElementById('btn-save-miskin').addEventListener('click', saveMiskin);
// Search and filter - SPBU
document.getElementById('search-spbu').addEventListener('input', (e) => {
renderSPBUTable('all', e.target.value);
});
document.querySelectorAll('#panel-spbu .filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#panel-spbu .filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const search = document.getElementById('search-spbu').value;
renderSPBUTable(btn.dataset.filter, search);
});
});
// Search and filter - JALAN
document.getElementById('search-jalan').addEventListener('input', (e) => {
renderJalanTable('all', e.target.value);
});
document.querySelectorAll('#panel-jalan .filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#panel-jalan .filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const search = document.getElementById('search-jalan').value;
renderJalanTable(btn.dataset.filter, search);
});
});
// Search and filter - PARSIL
document.getElementById('search-parsil').addEventListener('input', (e) => {
renderParsilTable('all', e.target.value);
});
document.querySelectorAll('#panel-parsil .filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#panel-parsil .filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const search = document.getElementById('search-parsil').value;
renderParsilTable(btn.dataset.filter, search);
});
});
// Search - IBADAH
document.getElementById('search-ibadah').addEventListener('input', (e) => {
renderIbadahTable(e.target.value);
});
// Search - MISKIN
document.getElementById('search-miskin').addEventListener('input', (e) => {
renderMiskinTable(e.target.value);
});
// Load initial data
loadAllData();
showToast('Welcome to WebGIS SPBU System', 'info');
});