1172 lines
40 KiB
JavaScript
1172 lines
40 KiB
JavaScript
// ============================================
|
|
// 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<br><small>untuk tambah <b>' + label + '</b></small>';
|
|
}
|
|
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<br><small>Klik tiap titik, double-klik selesai</small>'
|
|
: '✏️ Gambar area<br><small>Klik tiap sudut, double-klik selesai</small>';
|
|
}
|
|
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 = `<span class="toast-icon">${icon}</span><span>${message}</span>`;
|
|
|
|
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 = '<tr><td colspan="5" style="text-align: center; color: var(--clr-muted); padding: 40px;">Tidak ada data</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.map(item => `
|
|
<tr>
|
|
<td><strong>${item.nama}</strong></td>
|
|
<td>${item.no_spbu}</td>
|
|
<td><span class="badge badge-${item.status === '24jam' ? 'success' : 'warning'}">${item.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'}</span></td>
|
|
<td>${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)}</td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="btn btn-sm" style="background: var(--clr-primary); color: white;" onclick="editSPBU(${item.id})">Edit</button>
|
|
<button class="btn btn-sm" style="background: var(--clr-danger); color: white;" onclick="deleteSPBU(${item.id})">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).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 = '<tr><td colspan="4" style="text-align: center; color: var(--clr-muted); padding: 40px;">Tidak ada data</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.map(item => `
|
|
<tr>
|
|
<td><strong>${item.nama_jalan}</strong></td>
|
|
<td><span class="badge badge-info">${item.status}</span></td>
|
|
<td>${item.panjang.toFixed(0)} m</td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="btn btn-sm" style="background: var(--clr-primary); color: white;" onclick="editJalan(${item.id})">Edit</button>
|
|
<button class="btn btn-sm" style="background: var(--clr-danger); color: white;" onclick="deleteJalan(${item.id})">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).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 = '<tr><td colspan="4" style="text-align: center; color: var(--clr-muted); padding: 40px;">Tidak ada data</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.map(item => `
|
|
<tr>
|
|
<td><strong>${item.nama_pemilik}</strong></td>
|
|
<td><span class="badge badge-info">${item.status}</span></td>
|
|
<td>${item.luas.toFixed(0)} m2</td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="btn btn-sm" style="background: var(--clr-primary); color: white;" onclick="editParsil(${item.id})">Edit</button>
|
|
<button class="btn btn-sm" style="background: var(--clr-danger); color: white;" onclick="deleteParsil(${item.id})">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).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 = '<tr><td colspan="4" style="text-align: center; color: var(--clr-muted); padding: 40px;">Tidak ada data</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.map(item => `
|
|
<tr>
|
|
<td><strong>${item.nama}</strong></td>
|
|
<td>${item.alamat.substring(0, 30)}...</td>
|
|
<td>${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)}</td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="btn btn-sm" style="background: var(--clr-primary); color: white;" onclick="editIbadah(${item.id})">Edit</button>
|
|
<button class="btn btn-sm" style="background: var(--clr-danger); color: white;" onclick="deleteIbadah(${item.id})">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).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 = '<tr><td colspan="4" style="text-align: center; color: var(--clr-muted); padding: 40px;">Tidak ada data</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.map(item => `
|
|
<tr>
|
|
<td><strong>${item.nama_kk}</strong></td>
|
|
<td>${item.keterangan || '-'}</td>
|
|
<td>${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)}</td>
|
|
<td>
|
|
<div class="table-actions">
|
|
<button class="btn btn-sm" style="background: var(--clr-primary); color: white;" onclick="editMiskin(${item.id})">Edit</button>
|
|
<button class="btn btn-sm" style="background: var(--clr-danger); color: white;" onclick="deleteMiskin(${item.id})">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).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: `<div class="marker-halo ${haloClass}"></div><div class="marker-icon-inner">⛽</div>`,
|
|
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
|
|
});
|
|
|
|
const marker = L.marker([item.latitude, item.longitude], { icon: customIcon })
|
|
.bindPopup(`<strong>${item.nama}</strong><br>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(`<strong>${item.nama_jalan}</strong><br>Status: ${item.status}<br>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(`<strong>${item.nama_pemilik}</strong><br>Status: ${item.status}<br>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: `<div class="marker-halo ibadah"></div><div class="marker-icon-inner">🕌</div>`,
|
|
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
|
|
});
|
|
const marker = L.marker([item.latitude, item.longitude], { icon: ibadahIcon })
|
|
.bindPopup(`<strong>${item.nama}</strong><br>${item.alamat}`);
|
|
marker.addTo(window.appState.layers.ibadah);
|
|
});
|
|
|
|
// Render Miskin
|
|
window.appState.data.miskin.forEach(item => {
|
|
const miskinIcon = L.divIcon({
|
|
className: 'custom-marker',
|
|
html: `<div class="marker-halo miskin"></div><div class="marker-icon-inner">🏠</div>`,
|
|
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
|
|
});
|
|
const marker = L.marker([item.latitude, item.longitude], { icon: miskinIcon })
|
|
.bindPopup(`<strong>${item.nama_kk}</strong><br>${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');
|
|
});
|