Harden Project 01 public endpoints

This commit is contained in:
Andrie
2026-06-11 23:15:42 +07:00
parent a90748d9c1
commit 6f4488c1b8
39 changed files with 1183 additions and 612 deletions
+47 -6
View File
@@ -60,7 +60,33 @@
}
function isClassicDrawingActive() {
return typeof window.currentMode !== 'undefined' && Number(window.currentMode) !== 0;
const modeActive = typeof window.currentMode !== 'undefined' && Number(window.currentMode) !== 0;
const toolActive = window.activeTool === 'jalan' || window.activeTool === 'parsil' || window.activeTool === 'point';
const drawHint = document.getElementById('drawHint');
const hintVisible = !!drawHint && drawHint.classList.contains('visible');
return modeActive || toolActive || hintVisible;
}
function stopLayerEvent(e) {
if (e.originalEvent) {
L.DomEvent.stop(e.originalEvent);
} else {
L.DomEvent.stopPropagation(e);
}
}
function forwardDrawingEventToMap(type, e) {
const active = isClassicDrawingActive();
console.log("choropleth.js forwardDrawingEventToMap: type =", type, ", isClassicDrawingActive =", active);
if (!active) return false;
stopLayerEvent(e);
map.fire(type, {
latlng: e.latlng,
layerPoint: e.layerPoint || map.latLngToLayerPoint(e.latlng),
containerPoint: e.containerPoint || map.latLngToContainerPoint(e.latlng),
originalEvent: e.originalEvent,
});
return true;
}
function setLayerHitTesting(leafletLayer, enabled) {
@@ -125,12 +151,15 @@
lyr.resetStyle(layer);
});
layer.on('click', function (e) {
if (isClassicDrawingActive()) return;
if (forwardDrawingEventToMap('click', e)) return;
L.popup({ closeButton: true, className: 'choro-popup-wrap' })
.setLatLng(e.latlng)
.setContent(buildPopup(true))
.openOn(map);
L.DomEvent.stopPropagation(e);
stopLayerEvent(e);
});
layer.on('dblclick', function (e) {
if (forwardDrawingEventToMap('dblclick', e)) return;
});
},
});
@@ -189,9 +218,8 @@
const eyeIcon = isVisible ? 'eye' : 'eye-off';
const palDots = (PALETTES[m.palette] || PALETTES.blue)
.map(c => `<span class="pal-dot" style="background:${c}"></span>`).join('');
const safeNama = m.nama.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
const delBtn = window._IS_ADMIN
? `<button class="layer-del" onclick="window._choro.deleteLayer(${m.id},'${safeNama}')" title="Hapus"><i data-lucide="trash-2"></i></button>`
? `<button class="layer-del" data-layer-delete="${m.id}" title="Hapus"><i data-lucide="trash-2"></i></button>`
: '';
return `
<div class="layer-item ${isVisible ? '' : 'is-hidden'}" id="li${m.id}">
@@ -209,6 +237,14 @@
</div>`;
}).join('');
list.querySelectorAll('[data-layer-delete]').forEach(btn => {
btn.addEventListener('click', () => {
const id = Number(btn.getAttribute('data-layer-delete'));
const meta = allMeta.find(item => item.id === id);
if (meta) window._choro.deleteLayer(id, meta.nama);
});
});
if (typeof lucide !== 'undefined') lucide.createIcons();
}
@@ -255,7 +291,10 @@
meta.is_visible = newVisible;
if (rendered) {
rendered.meta.is_visible = newVisible;
if (newVisible) rendered.leafletLayer.addTo(map);
if (newVisible) {
rendered.leafletLayer.addTo(map);
setLayerHitTesting(rendered.leafletLayer, !isClassicDrawingActive());
}
else map.removeLayer(rendered.leafletLayer);
}
@@ -266,6 +305,7 @@
if (window._IS_ADMIN) {
const fd = new FormData();
fd.append('id', id);
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/choropleth/toggle.php', { method: 'POST', body: fd }).catch(() => {});
}
},
@@ -275,6 +315,7 @@
if (!ok) return;
const fd = new FormData();
fd.append('id', id);
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/choropleth/hapus.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(j => {
+35 -19
View File
@@ -55,14 +55,18 @@
return `
<div class="info-popup">
<div class="info-popup-header">
<div class="info-popup-icon" style="background:${color}22;">🛣️</div>
<div class="info-popup-icon" style="background:${color}22;">
<i data-lucide="route" style="color:${color};width:18px;height:18px;"></i>
</div>
<div>
<div class="info-popup-name">${escapeHTML(row.nama_jalan)}</div>
<div class="info-popup-id">#${row.id}</div>
</div>
</div>
<div class="info-row">
<div class="info-row-icon">🏷️</div>
<div class="info-row-icon">
<i data-lucide="tag" style="width:14px;height:14px;color:var(--text-m)"></i>
</div>
<div>
<div class="info-row-label">Status Jalan</div>
<div class="info-row-value">
@@ -74,13 +78,15 @@
</div>
</div>
<div class="info-row">
<div class="info-row-icon">📏</div>
<div class="info-row-icon">
<i data-lucide="milestone" style="width:14px;height:14px;color:var(--text-m)"></i>
</div>
<div>
<div class="info-row-label">Panjang Jalan</div>
<div class="info-row-value" style="font-family:var(--font-mono);">${panjang}</div>
</div>
</div>
<button class="btn-hapus" onclick="window._jalanHapus(${row.id}, this)">🗑 Hapus Jalan</button>
${window._IS_ADMIN ? `<button class="btn-hapus" onclick="window._jalanHapus(${row.id}, this)"><i data-lucide="trash-2" style="width:13px;height:13px;margin-right:4px;"></i> Hapus Jalan</button>` : ''}
</div>
`;
}
@@ -94,7 +100,7 @@
.then(r => r.json())
.then(j => {
if (j.status !== 'success') throw new Error(j.message);
updateCount(j.total, '🛣️');
updateCount(j.total, 'Jalan');
j.data.forEach(addPolylineToMap);
refreshJalanList(j.data);
})
@@ -120,10 +126,11 @@
showDeleteConfirm('Yakin ingin menghapus data jalan ini?').then(confirmed => {
if (!confirmed) return;
btnEl.disabled = true;
btnEl.textContent = ' Menghapus...';
btnEl.innerHTML = '<span class="btn-spinner" style="margin-right: 6px; display: inline-block; vertical-align: middle;"></span> Menghapus...';
const fd = new FormData();
fd.append('id', id);
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/jalan/hapus.php', { method: 'POST', body: fd })
.then(r => r.json())
@@ -131,7 +138,7 @@
if (j.status === 'success') {
map.closePopup();
if (allLayers[id]) { layerGroup.removeLayer(allLayers[id]); delete allLayers[id]; }
updateCount(Object.keys(allLayers).length, '🛣️');
updateCount(Object.keys(allLayers).length, 'Jalan');
const COLORS = { 'Nasional': '#ef4444', 'Provinsi': '#f97316', 'Kabupaten': '#eab308' };
const remaining = Object.values(allLayers).map(l => ({
id: l._jalanId, name: l._jalanNama,
@@ -146,7 +153,8 @@
.catch(err => {
showToast(err.message || 'Gagal menghapus.', 'error');
btnEl.disabled = false;
btnEl.textContent = '🗑 Hapus Jalan';
btnEl.innerHTML = '<i data-lucide="trash-2" style="width:13px;height:13px;margin-right:4px;"></i> Hapus Jalan';
if (typeof lucide !== 'undefined') lucide.createIcons();
});
});
};
@@ -180,12 +188,15 @@
drawingMarkers.forEach(m => map.removeLayer(m));
drawingMarkers = [];
if (save && drawingPoints.length >= 2) {
const shouldSave = save && drawingPoints.length >= 2;
if (shouldSave) {
showFormJalan(drawingPoints.slice());
}
drawingPoints = [];
if (typeof window._resetActiveTool === 'function') window._resetActiveTool();
if (!shouldSave) {
if (typeof window._resetActiveTool === 'function') window._resetActiveTool();
}
}
function finishDrawing() {
@@ -303,7 +314,7 @@
.setContent(`
<div class="form-popup">
<div class="form-popup-header">
<div class="form-popup-icon blue">🛣️</div>
<div class="form-popup-icon blue"><i data-lucide="route" style="width:18px;height:18px;"></i></div>
<div>
<div class="form-popup-title">Tambah Data Jalan</div>
<div class="form-popup-coords">${points.length} titik · ${totalMeter} m</div>
@@ -327,7 +338,7 @@
</div>
<input type="hidden" id="fj_geojson" value='${escapeHTML(geojson)}'>
<input type="hidden" id="fj_panjang" value="${totalMeter}">
<button class="btn-save" id="btnSimpanJalan" onclick="window._jalanSimpan()">💾 Simpan Jalan</button>
<button class="btn-save" id="btnSimpanJalan" onclick="window._jalanSimpan()"><i data-lucide="save" style="width:14px;height:14px;margin-right:4px;"></i> Simpan Jalan</button>
<div class="form-status" id="jalanStatus"></div>
</div>
`)
@@ -335,9 +346,11 @@
setTimeout(() => { const el = document.getElementById('fj_nama'); if (el) el.focus(); }, 200);
// Kalau popup ditutup tanpa simpan: hapus preview
// Reset active tool when popup is closed (either by saving or clicking close/outside)
map.once('popupclose', () => {
// preview sudah di-clear di stopDrawing
if (typeof window._resetActiveTool === 'function') {
window._resetActiveTool();
}
});
}
@@ -351,20 +364,22 @@
const btn = document.getElementById('btnSimpanJalan');
if (!nama) {
statusEl.textContent = ' Nama jalan wajib diisi.';
statusEl.innerHTML = '<i data-lucide="alert-triangle" style="width:14px;height:14px;margin-right:4px;display:inline-block;vertical-align:middle;"></i> Nama jalan wajib diisi.';
statusEl.className = 'form-status error';
if (typeof lucide !== 'undefined') lucide.createIcons();
document.getElementById('fj_nama').focus();
return;
}
btn.disabled = true;
btn.textContent = ' Menyimpan...';
btn.innerHTML = '<span class="btn-spinner" style="margin-right: 6px; display: inline-block; vertical-align: middle;"></span> Menyimpan...';
const fd = new FormData();
fd.append('nama_jalan', nama);
fd.append('status_jalan', status);
fd.append('geojson', geojson);
fd.append('panjang_meter', panjang);
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/jalan/simpan.php', { method: 'POST', body: fd })
.then(r => r.json())
@@ -372,7 +387,7 @@
if (j.status === 'success') {
map.closePopup();
addPolylineToMap(j.data);
updateCount(Object.keys(allLayers).length, '🛣️');
updateCount(Object.keys(allLayers).length, 'Jalan');
const COLORS = { 'Nasional': '#ef4444', 'Provinsi': '#f97316', 'Kabupaten': '#eab308' };
const allItems = Object.values(allLayers).map(l => ({
id: l._jalanId, name: l._jalanNama,
@@ -385,10 +400,11 @@
} else throw new Error(j.message);
})
.catch(err => {
statusEl.textContent = '✕ ' + err.message;
statusEl.innerHTML = '<i data-lucide="x-circle" style="width:14px;height:14px;margin-right:4px;display:inline-block;vertical-align:middle;"></i> ' + escapeHTML(err.message);
statusEl.className = 'form-status error';
btn.disabled = false;
btn.textContent = '💾 Simpan Jalan';
btn.innerHTML = '<i data-lucide="save" style="width:14px;height:14px;margin-right:4px;"></i> Simpan Jalan';
if (typeof lucide !== 'undefined') lucide.createIcons();
});
};
+36 -19
View File
@@ -74,14 +74,18 @@
return `
<div class="info-popup">
<div class="info-popup-header">
<div class="info-popup-icon" style="background:${color}22;">🏘️</div>
<div class="info-popup-icon" style="background:${color}22;">
<i data-lucide="pentagon" style="color:${color};width:18px;height:18px;"></i>
</div>
<div>
<div class="info-popup-name">${escapeHTML(row.nama_parsil)}</div>
<div class="info-popup-id">#${row.id}</div>
</div>
</div>
<div class="info-row">
<div class="info-row-icon">📜</div>
<div class="info-row-icon">
<i data-lucide="file-text" style="width:14px;height:14px;color:var(--text-m)"></i>
</div>
<div>
<div class="info-row-label">Status Kepemilikan</div>
<div class="info-row-value">
@@ -93,13 +97,15 @@
</div>
</div>
<div class="info-row">
<div class="info-row-icon">📐</div>
<div class="info-row-icon">
<i data-lucide="maximize-2" style="width:14px;height:14px;color:var(--text-m)"></i>
</div>
<div>
<div class="info-row-label">Luas Tanah</div>
<div class="info-row-value" style="font-family:var(--font-mono);">${luas}</div>
</div>
</div>
<button class="btn-hapus" onclick="window._parsilHapus(${row.id}, this)">🗑 Hapus Parsil</button>
${window._IS_ADMIN ? `<button class="btn-hapus" onclick="window._parsilHapus(${row.id}, this)"><i data-lucide="trash-2" style="width:13px;height:13px;margin-right:4px;"></i> Hapus Parsil</button>` : ''}
</div>
`;
}
@@ -113,7 +119,7 @@
.then(r => r.json())
.then(j => {
if (j.status !== 'success') throw new Error(j.message);
updateCount(j.total, '🏘️');
updateCount(j.total, 'Parsil');
j.data.forEach(addPolygonToMap);
refreshParsilList(j.data);
})
@@ -138,10 +144,11 @@
showDeleteConfirm('Yakin ingin menghapus data parsil tanah ini?').then(confirmed => {
if (!confirmed) return;
btnEl.disabled = true;
btnEl.textContent = ' Menghapus...';
btnEl.innerHTML = '<span class="btn-spinner" style="margin-right: 6px; display: inline-block; vertical-align: middle;"></span> Menghapus...';
const fd = new FormData();
fd.append('id', id);
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/parsil/hapus.php', { method: 'POST', body: fd })
.then(r => r.json())
@@ -149,7 +156,7 @@
if (j.status === 'success') {
map.closePopup();
if (allLayers[id]) { layerGroup.removeLayer(allLayers[id]); delete allLayers[id]; }
updateCount(Object.keys(allLayers).length, '🏘️');
updateCount(Object.keys(allLayers).length, 'Parsil');
const remaining = Object.values(allLayers).map(l => ({
id: l._parsilId, name: l._parsilNama,
badge: l._parsilStatus,
@@ -163,7 +170,8 @@
.catch(err => {
showToast(err.message || 'Gagal menghapus.', 'error');
btnEl.disabled = false;
btnEl.textContent = '🗑 Hapus Parsil';
btnEl.innerHTML = '<i data-lucide="trash-2" style="width:13px;height:13px;margin-right:4px;"></i> Hapus Parsil';
if (typeof lucide !== 'undefined') lucide.createIcons();
});
});
};
@@ -197,14 +205,17 @@
drawingMarkers.forEach(m => map.removeLayer(m));
drawingMarkers = [];
if (save && drawingPoints.length >= 3) {
const shouldSave = save && drawingPoints.length >= 3;
if (shouldSave) {
showFormParsil(drawingPoints.slice());
} else if (save && drawingPoints.length < 3) {
showToast('Minimal 3 titik untuk membuat parsil (Polygon)', 'error');
}
drawingPoints = [];
if (typeof window._resetActiveTool === 'function') window._resetActiveTool();
if (!shouldSave) {
if (typeof window._resetActiveTool === 'function') window._resetActiveTool();
}
}
function finishDrawing() {
@@ -217,6 +228,7 @@
}
function onMapClick(e) {
console.log("parsil.js onMapClick: currentMode =", (typeof currentMode !== "undefined" ? currentMode : "undefined"), ", currentSubMode =", (typeof currentSubMode !== "undefined" ? currentSubMode : "undefined"), ", isDrawing =", isDrawing);
if (currentMode !== 2 || currentSubMode !== 'parsil') return;
if (!isDrawing) return;
@@ -330,7 +342,7 @@
.setContent(`
<div class="form-popup">
<div class="form-popup-header">
<div class="form-popup-icon amber">🏘️</div>
<div class="form-popup-icon amber"><i data-lucide="pentagon" style="width:18px;height:18px;"></i></div>
<div>
<div class="form-popup-title">Tambah Data Parsil (Kavling)</div>
<div class="form-popup-coords">${points.length} titik · Luas : ${luasStr} m²</div>
@@ -355,7 +367,7 @@
</div>
<input type="hidden" id="fp_geojson" value='${escapeHTML(geojson)}'>
<input type="hidden" id="fp_luas" value="${luasArea}">
<button class="btn-save" id="btnSimpanParsil" style="background:var(--c-warn);color:#111" onclick="window._parsilSimpan()">💾 Simpan Parsil</button>
<button class="btn-save" id="btnSimpanParsil" onclick="window._parsilSimpan()"><i data-lucide="save" style="width:14px;height:14px;margin-right:4px;"></i> Simpan Parsil</button>
<div class="form-status" id="parsilStatus"></div>
</div>
`)
@@ -363,9 +375,11 @@
setTimeout(() => { const el = document.getElementById('fp_nama'); if (el) el.focus(); }, 200);
// Kalau popup ditutup tanpa simpan: hapus preview
// Reset active tool when popup is closed (either by saving or clicking close/outside)
map.once('popupclose', () => {
// preview sudah di-clear di stopDrawing
if (typeof window._resetActiveTool === 'function') {
window._resetActiveTool();
}
});
}
@@ -379,20 +393,22 @@
const btn = document.getElementById('btnSimpanParsil');
if (!nama) {
statusEl.textContent = ' Nama parsil wajib diisi.';
statusEl.innerHTML = '<i data-lucide="alert-triangle" style="width:14px;height:14px;margin-right:4px;display:inline-block;vertical-align:middle;"></i> Nama parsil wajib diisi.';
statusEl.className = 'form-status error';
if (typeof lucide !== 'undefined') lucide.createIcons();
document.getElementById('fp_nama').focus();
return;
}
btn.disabled = true;
btn.textContent = ' Menyimpan...';
btn.innerHTML = '<span class="btn-spinner" style="margin-right: 6px; display: inline-block; vertical-align: middle;"></span> Menyimpan...';
const fd = new FormData();
fd.append('nama_parsil', nama);
fd.append('status_kepemilikan', status);
fd.append('geojson', geojson);
fd.append('luas_m2', luas);
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/parsil/simpan.php', { method: 'POST', body: fd })
.then(r => r.json())
@@ -400,7 +416,7 @@
if (j.status === 'success') {
map.closePopup();
addPolygonToMap(j.data);
updateCount(Object.keys(allLayers).length, '🏘️');
updateCount(Object.keys(allLayers).length, 'Parsil');
const allItems = Object.values(allLayers).map(l => ({
id: l._parsilId, name: l._parsilNama,
badge: l._parsilStatus,
@@ -412,10 +428,11 @@
} else throw new Error(j.message);
})
.catch(err => {
statusEl.textContent = '✕ ' + err.message;
statusEl.innerHTML = '<i data-lucide="x-circle" style="width:14px;height:14px;margin-right:4px;display:inline-block;vertical-align:middle;"></i> ' + escapeHTML(err.message);
statusEl.className = 'form-status error';
btn.disabled = false;
btn.textContent = '💾 Simpan Parsil';
btn.innerHTML = '<i data-lucide="save" style="width:14px;height:14px;margin-right:4px;"></i> Simpan Parsil';
if (typeof lucide !== 'undefined') lucide.createIcons();
});
};
+47 -37
View File
@@ -33,7 +33,7 @@
const isOpen = Number(poi.buka_24jam) === 1;
const marker = L.marker(
[poi.latitude, poi.longitude],
{ icon: isOpen ? iconDefault : iconClosed, draggable: true, pane: 'pointPane' }
{ icon: isOpen ? iconDefault : iconClosed, draggable: window._IS_ADMIN, pane: 'pointPane' }
);
marker._poiId = poi.id;
@@ -41,61 +41,66 @@
marker._poiBuka = Number(poi.buka_24jam) === 1;
allMarkers[poi.id] = marker;
// Drag to update position
marker.on('dragend', function (e) {
const { lat, lng } = e.target.getLatLng();
const fd = new FormData();
fd.append('id', poi.id);
fd.append('latitude', lat.toFixed(8));
fd.append('longitude', lng.toFixed(8));
if (window._IS_ADMIN) {
// Drag to update position
marker.on('dragend', function (e) {
const { lat, lng } = e.target.getLatLng();
const fd = new FormData();
fd.append('id', poi.id);
fd.append('latitude', lat.toFixed(8));
fd.append('longitude', lng.toFixed(8));
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/point/update_posisi.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(j => {
if (j.status === 'success') showToast(`Posisi "${escapeHTML(poi.nama_tempat)}" diperbarui!`);
else throw new Error(j.message);
})
.catch(err => showToast(err.message || 'Gagal update posisi.', 'error'));
});
fetch('api/point/update_posisi.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(j => {
if (j.status === 'success') showToast(`Posisi "${escapeHTML(poi.nama_tempat)}" diperbarui!`);
else throw new Error(j.message);
})
.catch(err => showToast(err.message || 'Gagal update posisi.', 'error'));
});
}
// Popup info
const noWa = poi.no_wa ? poi.no_wa.replace(/\D/g, '') : '';
const waBtn = noWa
? `<a class="btn-wa" href="https://wa.me/${noWa}" target="_blank" rel="noopener"
style="display:flex;align-items:center;justify-content:center;gap:8px;width:100%;margin-top:14px;padding:9px;background:#25d366;color:#fff;border:none;border-radius:7px;font-family:var(--font-body);font-size:13px;font-weight:700;text-decoration:none;">
💬 Chat WhatsApp
<i data-lucide="message-square" style="width:14px;height:14px;margin-right:4px;"></i> Chat WhatsApp
</a>`
: `<div style="font-size:12px;color:var(--c-muted);text-align:center;padding:8px 0;margin-top:8px;">Tidak ada nomor WA</div>`;
const buka24Badge = isOpen
? `<span class="status-badge-inline" style="background:rgba(46,160,67,.2);color:#3fb950;border:1px solid rgba(46,160,67,.35);">🟢 Buka 24 Jam</span>`
: `<span class="status-badge-inline" style="background:rgba(248,81,73,.12);color:#f85149;border:1px solid rgba(248,81,73,.3);">🔴 Tidak 24 Jam</span>`;
? `<span class="status-badge-inline" style="background:rgba(46,160,67,.2);color:#3fb950;border:1px solid rgba(46,160,67,.35);display:inline-flex;align-items:center;gap:4px;"><i data-lucide="check" style="width:12px;height:12px;"></i> Buka 24 Jam</span>`
: `<span class="status-badge-inline" style="background:rgba(248,81,73,.12);color:#f85149;border:1px solid rgba(248,81,73,.3);display:inline-flex;align-items:center;gap:4px;"><i data-lucide="x" style="width:12px;height:12px;"></i> Tidak 24 Jam</span>`;
const deleteBtn = window._IS_ADMIN ? `<button class="btn-hapus" onclick="window._pointHapus(${poi.id}, this)"><i data-lucide="trash-2" style="width:14px;height:14px;margin-right:4px;"></i> Hapus Lokasi</button>` : '';
marker.bindPopup(`
<div class="info-popup">
<div class="info-popup-header">
<div class="info-popup-icon">📍</div>
<div class="info-popup-icon"><i data-lucide="map-pin" style="width:18px;height:18px;"></i></div>
<div>
<div class="info-popup-name">${escapeHTML(poi.nama_tempat)}</div>
<div class="info-popup-id">#${poi.id}</div>
</div>
</div>
<div class="info-row">
<div class="info-row-icon">📞</div>
<div class="info-row-icon"><i data-lucide="phone" style="width:14px;height:14px;"></i></div>
<div>
<div class="info-row-label">No. WhatsApp</div>
<div class="info-row-value">${poi.no_wa ? escapeHTML(poi.no_wa) : '—'}</div>
</div>
</div>
<div class="info-row">
<div class="info-row-icon">🕐</div>
<div class="info-row-icon"><i data-lucide="clock" style="width:14px;height:14px;"></i></div>
<div>
<div class="info-row-label">Jam Operasional</div>
<div class="info-row-value">${buka24Badge}</div>
</div>
</div>
<div class="info-row">
<div class="info-row-icon">🌐</div>
<div class="info-row-icon"><i data-lucide="globe" style="width:14px;height:14px;"></i></div>
<div>
<div class="info-row-label">Koordinat</div>
<div class="info-row-value" style="font-family:var(--font-mono);font-size:11px;">
@@ -104,7 +109,7 @@
</div>
</div>
${waBtn}
<button class="btn-hapus" onclick="window._pointHapus(${poi.id}, this)">🗑 Hapus Lokasi</button>
${deleteBtn}
</div>
`, { maxWidth: 300 });
@@ -120,7 +125,7 @@
.then(r => r.json())
.then(j => {
if (j.status !== 'success') throw new Error(j.message);
updateCount(j.total, '📍');
updateCount(j.total, 'Point');
j.data.forEach(addMarkerToMap);
refreshPointList(j.data);
})
@@ -148,10 +153,11 @@
showDeleteConfirm('Yakin ingin menghapus lokasi ini?').then(confirmed => {
if (!confirmed) return;
btnEl.disabled = true;
btnEl.textContent = ' Menghapus...';
btnEl.innerHTML = '<span class="btn-spinner" style="margin-right: 6px; display: inline-block; vertical-align: middle;"></span> Menghapus...';
const fd = new FormData();
fd.append('id', id);
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/point/hapus.php', { method: 'POST', body: fd })
.then(r => r.json())
@@ -159,7 +165,7 @@
if (j.status === 'success') {
map.closePopup();
if (allMarkers[id]) { layerGroup.removeLayer(allMarkers[id]); delete allMarkers[id]; }
updateCount(Math.max(0, Object.keys(allMarkers).length), '📍');
updateCount(Math.max(0, Object.keys(allMarkers).length), 'Point');
// rebuild list dari allMarkers (ambil nama dari marker)
const remaining = Object.keys(allMarkers).map(k => ({
id: k,
@@ -175,7 +181,8 @@
.catch(err => {
showToast(err.message || 'Gagal menghapus.', 'error');
btnEl.disabled = false;
btnEl.textContent = '🗑 Hapus Lokasi';
btnEl.innerHTML = '<i data-lucide="trash-2" style="width:14px;height:14px;margin-right:4px;"></i> Hapus Lokasi';
if (typeof lucide !== 'undefined') lucide.createIcons();
});
});
};
@@ -195,7 +202,7 @@
.setContent(`
<div class="form-popup">
<div class="form-popup-header">
<div class="form-popup-icon"></div>
<div class="form-popup-icon"><i data-lucide="plus" style="width:18px;height:18px;"></i></div>
<div>
<div class="form-popup-title">Tambah Lokasi Baru</div>
<div class="form-popup-coords">${lat}, ${lng}</div>
@@ -212,13 +219,13 @@
<div class="form-group">
<label>Buka 24 Jam?</label>
<select id="f_buka24">
<option value="0">Tidak</option>
<option value="1">Ya</option>
<option value="0">Tidak</option>
<option value="1">Ya</option>
</select>
</div>
<input type="hidden" id="f_lat" value="${lat}">
<input type="hidden" id="f_lng" value="${lng}">
<button class="btn-save" id="btnSimpanPoi" onclick="window._pointSimpan()">💾 Simpan Lokasi</button>
<button class="btn-save" id="btnSimpanPoi" onclick="window._pointSimpan()"><i data-lucide="save" style="width:14px;height:14px;margin-right:4px;"></i> Simpan Lokasi</button>
<div class="form-status" id="poiStatus"></div>
</div>
`)
@@ -238,14 +245,15 @@
const btn = document.getElementById('btnSimpanPoi');
if (!nama) {
status.textContent = ' Nama tempat wajib diisi.';
status.innerHTML = '<i data-lucide="alert-triangle" style="width:14px;height:14px;margin-right:4px;display:inline-block;vertical-align:middle;"></i> Nama tempat wajib diisi.';
status.className = 'form-status error';
if (typeof lucide !== 'undefined') lucide.createIcons();
document.getElementById('f_nama').focus();
return;
}
btn.disabled = true;
btn.textContent = ' Menyimpan...';
btn.innerHTML = '<span class="btn-spinner" style="margin-right: 6px; display: inline-block; vertical-align: middle;"></span> Menyimpan...';
const fd = new FormData();
fd.append('nama_tempat', nama);
@@ -253,6 +261,7 @@
fd.append('buka_24jam', buka24);
fd.append('latitude', lat);
fd.append('longitude', lng);
fd.append('csrf_token', window._CSRF_TOKEN || '');
fetch('api/point/simpan.php', { method: 'POST', body: fd })
.then(r => r.json())
@@ -261,7 +270,7 @@
map.closePopup();
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
addMarkerToMap(j.data);
updateCount(Object.keys(allMarkers).length, '📍');
updateCount(Object.keys(allMarkers).length, 'Point');
// Tambahkan item baru ke list
const allItems = Object.values(allMarkers).map(m => ({
id: m._poiId,
@@ -275,10 +284,11 @@
} else throw new Error(j.message);
})
.catch(err => {
status.textContent = '✕ ' + err.message;
status.innerHTML = '<i data-lucide="x-circle" style="width:14px;height:14px;margin-right:4px;display:inline-block;vertical-align:middle;"></i> ' + escapeHTML(err.message);
status.className = 'form-status error';
btn.disabled = false;
btn.textContent = '💾 Simpan Lokasi';
btn.innerHTML = '<i data-lucide="save" style="width:14px;height:14px;margin-right:4px;"></i> Simpan Lokasi';
if (typeof lucide !== 'undefined') lucide.createIcons();
});
};