first commit UasSIGWebgis

This commit is contained in:
raditarahman5-cloud
2026-06-09 18:19:10 +07:00
commit 6d0b24d2fe
25 changed files with 4616 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
<?php
session_start();
require_once '../api/db.php';
?>
<!DOCTYPE html>
<html>
<head>
<title>WebGIS Poverty Mapping - Dashboard Sosial & Warga</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"/>
<link rel="stylesheet" href="style.css"/>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body>
<button class="menu-toggle" onclick="toggleSidebar()" title="Buka Dashboard Eksekutif"><i class="fas fa-bars"></i></button>
<button onclick="window.location.href='../portal/index.php'" style="position: absolute; top: 15px; left: 65px; z-index: 1200; background: white; color: var(--primary); border: 2px solid var(--primary); padding: 10px 20px; border-radius: 30px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.1); font-size: 0.9rem; transition: all 0.3s ease; display: flex; align-items: center; gap: 8px;" onmouseover="this.style.background='var(--primary)'; this.style.color='white';" onmouseout="this.style.background='white'; this.style.color='var(--primary)';">
<i class="fas fa-arrow-left"></i> Kembali ke Dashboard
</button>
<button class="menu-toggle" style="left: auto; right: 20px; background: var(--primary);" onclick="toggleRightPanel()" title="Buka Form Input">
<i class="fas fa-pen"></i>
</button>
<div class="search-container">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" placeholder="Cari data GIS..." onkeyup="filterData()">
</div>
</div>
<div id="sidebar">
<div class="sidebar-header">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<h3><i class="fas fa-users"></i> Peta Sosial Warga</h3>
<small><i class="fas fa-hands-helping"></i> Dashboard Admin Sosial</small>
</div>
<div style="display: flex; gap: 8px;">
<a href="../portal/index.php" title="Kembali ke Portal Utama" style="color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onmouseover="this.style.background='rgba(59, 130, 246, 0.8)'" onmouseout="this.style.background='rgba(255,255,255,0.2)'">
<i class="fas fa-home"></i>
</a>
<a href="#" id="notifBell" title="Notifikasi Laporan Warga" style="position: relative; color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onclick="toggleSidebar()">
<i class="fas fa-bell"></i>
<span id="notifBadge" style="display: none; position: absolute; top: -5px; right: -5px; background: #ef4444; color: white; border-radius: 50%; padding: 2px 6px; font-size: 0.6rem; font-weight: bold; border: 2px solid #1e293b; animation: bounce 1s infinite;">0</span>
</a>
<a href="../portal/logout.php" title="Logout" style="color: #fff; background: rgba(255,255,255,0.2); padding: 8px 12px; border-radius: 8px; text-decoration: none; font-size: 0.9rem; transition: 0.3s;" onmouseover="this.style.background='rgba(239, 68, 68, 0.8)'" onmouseout="this.style.background='rgba(255,255,255,0.2)'">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
</div>
</div>
<div class="sidebar-content" id="sidebarContent">
<!-- Dirender secara dinamis oleh script.js -->
<small>Memuat data...</small>
</div>
</div>
<div id="map"></div>
<div class="panel-input" id="panelInput">
<div style="display:flex; justify-content:space-between; align-items:center; border-bottom: 2px solid var(--border); padding-bottom: 10px; margin-bottom: 15px;">
<h4 style="margin:0; border:none; padding:0; font-size:1.1rem; color:var(--dark);">Form Input & Laporan</h4>
<button onclick="toggleRightPanel()" style="background:none; border:none; cursor:pointer; font-size:1.2rem; color:#94a3b8; transition:0.2s;" onmouseover="this.style.color='#ef4444'" onmouseout="this.style.color='#94a3b8'"><i class="fas fa-times"></i></button>
</div>
<div class="radius-box">
<label>📡 Radius Jangkauan / Bantuan: <span id="radiusVal">500</span>m</label>
<input type="range" id="radiusRange" min="100" max="5000" value="500" step="100" oninput="changeRadius(this.value)">
</div>
<select id="jenis" onchange="toggleForm()">
<option value="pelaporan">🚨 Pelaporan Cepat (Masyarakat)</option>
<option value="warga">📍 Warga (Update Admin)</option>
<option value="masjid">🕌 Tempat Ibadah / Pusat Bantuan</option>
</select>
<!-- FORM PELAPORAN -->
<div id="form_pelaporan">
<p style="font-size:0.75rem; color:var(--danger); margin-top:0;">*Gunakan ini untuk lapor kasus gizi buruk, sakit, dll.</p>
<input type="text" id="nama_pelapor" placeholder="Nama Pelapor (Anda)">
<textarea id="isi_laporan" placeholder="Jelaskan kondisi darurat..."></textarea>
</div>
<!-- FORM WARGA -->
<div id="form_warga" class="hidden">
<input type="text" id="nama_kk" placeholder="Nama Kepala Keluarga">
<input type="number" id="jumlah_kk" placeholder="Jumlah Anggota">
<label style="font-size:0.75rem; color:#666;">Tanggal Lahir Kepala Keluarga:</label>
<input type="date" id="tanggal_lahir" title="Tanggal Lahir">
<select id="pendidikan">
<option value="">- Pilih Pendidikan Terakhir -</option>
<option value="Tidak Sekolah">Tidak Sekolah</option>
<option value="SD">SD Sederajat</option>
<option value="SMP">SMP Sederajat</option>
<option value="SMA">SMA Sederajat</option>
<option value="Perguruan Tinggi">Perguruan Tinggi</option>
</select>
<input type="text" id="riwayat_penyakit" placeholder="Riwayat Penyakit (misal: Stunting, TBC)">
<select id="status_bantuan">
<option value="Belum">Belum Menerima Bantuan</option>
<option value="Sudah">Sudah Menerima Bantuan</option>
</select>
<textarea id="histori_bantuan" placeholder="Histori Bantuan (misal: Sembako Jan 2026, PKH)"></textarea>
<textarea id="alamat_warga" placeholder="Alamat lengkap..."></textarea>
</div>
<!-- FORM MASJID -->
<div id="form_masjid" class="hidden">
<input type="text" id="nama_masjid" placeholder="Nama Tempat Ibadah">
<select id="jenis_masjid">
<option value="Masjid">Masjid</option>
<option value="Gereja">Gereja</option>
<option value="Vihara">Vihara / Klenteng</option>
<option value="Pura">Pura</option>
</select>
<input type="text" id="pic_masjid" placeholder="Nama Pengurus / PIC">
<textarea id="alamat_masjid" placeholder="Alamat lengkap..."></textarea>
</div>
<!-- END FORMS -->
<input type="hidden" id="lat"><input type="hidden" id="lng"><input type="hidden" id="edit_id" value="0">
<div style="display:flex; gap:10px;">
<button class="btn-save" id="btnSimpanUtama" onclick="simpanData()" style="flex:1;">Simpan Data</button>
<button class="btn-del" id="btnBatalEdit" onclick="batalEdit()" style="display:none; padding:14px; border-radius:10px; font-weight:700;">Batal</button>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script>
// Penanda untuk script.js agar tahu konteks dashboard
window.dashboardType = 'sosial';
</script>
<script src="script.js"></script>
</body>
</html>
+768
View File
@@ -0,0 +1,768 @@
var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Layer Groups yang Spesifik
var layerMasjid = L.layerGroup().addTo(map);
var layerMasjidRadius = L.layerGroup().addTo(map);
var layerPelaporan = L.layerGroup().addTo(map);
var layerWarga = L.layerGroup().addTo(map);
var layerSpbu24 = L.layerGroup().addTo(map);
var layerSpbuNon24 = L.layerGroup().addTo(map);
var layerLines = L.layerGroup().addTo(map);
var layerPolygons = L.layerGroup().addTo(map);
// Layers Control
var overlays = {};
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
overlays["🕌 Tempat Ibadah"] = layerMasjid;
overlays["📡 Radius Ibadah/Bantuan"] = layerMasjidRadius;
overlays["🚨 Pelaporan Darurat"] = layerPelaporan;
overlays["📍 Warga Rentan"] = layerWarga;
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
overlays["⛽ SPBU 24 Jam (Hijau)"] = layerSpbu24;
overlays["⛽ SPBU Terbatas (Merah)"] = layerSpbuNon24;
overlays["🛣️ Jalan (Polyline)"] = layerLines;
overlays["🟩 Kavling (Polygon)"] = layerPolygons;
}
L.control.layers(null, overlays, { collapsed: false, position: 'topright' }).addTo(map);
var drawnItems = new L.FeatureGroup().addTo(map);
var currentRadius = 500;
var cachedData = null;
var drawControl = new L.Control.Draw({
draw: {
marker:false, circle:false, circlemarker:false,
polyline: { shapeOptions: { color: 'blue', weight: 4 } },
polygon: { shapeOptions: { color: 'red' } },
rectangle: false
},
edit: { featureGroup: drawnItems }
});
map.addControl(drawControl);
// Locate Control
var locateBtn = L.control({position: 'topleft'});
locateBtn.onAdd = function (map) {
var div = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
div.style.backgroundColor = 'white';
div.style.width = '34px';
div.style.height = '34px';
div.style.cursor = 'pointer';
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.justifyContent = 'center';
div.innerHTML = '<i class="fas fa-crosshairs" style="font-size:1.2rem; color:#2563eb;"></i>';
div.title = "Lokasi Anda Saat Ini";
div.onclick = function(){ map.locate({setView: true, maxZoom: 16}); }
return div;
};
locateBtn.addTo(map);
map.on('locationfound', function(e) {
L.circleMarker(e.latlng, {radius: 8, fillColor: '#3b82f6', color: '#fff', weight: 3, fillOpacity: 1}).addTo(map).bindPopup("Lokasi Anda").openPopup();
});
// Event Draw
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var type = e.layerType;
drawnItems.addLayer(layer);
var panel = document.getElementById('panelInput');
if (panel) panel.classList.remove('collapsed');
if (type === 'polyline') {
document.getElementById('jenis').value = 'jalan';
toggleForm();
var coords = layer.getLatLngs();
document.getElementById('jalan_coords').value = JSON.stringify(coords);
// Calculate Length automatically
var length = 0;
for (var i = 0; i < coords.length - 1; i++) {
length += coords[i].distanceTo(coords[i + 1]);
}
document.getElementById('panjang_jalan').value = length.toFixed(2);
} else if (type === 'polygon') {
document.getElementById('jenis').value = 'parsil';
toggleForm();
var coords = layer.getLatLngs()[0];
document.getElementById('parsil_coords').value = JSON.stringify(coords);
}
});
map.on(L.Draw.Event.EDITED, function (e) {
var layers = e.layers;
layers.eachLayer(function (layer) {
if(layer.db_id && layer.db_tipe) {
var coords = (layer.db_tipe === 'parsil') ? layer.getLatLngs()[0] : layer.getLatLngs();
var fd = new FormData();
fd.append('action', 'update_koordinat');
fd.append('id', layer.db_id);
fd.append('tipe', layer.db_tipe);
fd.append('koordinat', JSON.stringify(coords));
if(layer.db_tipe === 'jalan') {
var length = 0;
for (var i = 0; i < coords.length - 1; i++) {
length += coords[i].distanceTo(coords[i + 1]);
}
fd.append('panjang_m', length.toFixed(2));
}
fetch('../api/api.php', { method: 'POST', body: fd });
}
});
Swal.fire({icon: 'success', title: 'Tersimpan', text: 'Perubahan bentuk berhasil disimpan!', timer: 1500, showConfirmButton: false});
});
map.on(L.Draw.Event.DELETED, function (e) {
var layers = e.layers;
layers.eachLayer(function (layer) {
if(layer.db_id && layer.db_tipe) {
var fd = new FormData();
fd.append('action', 'hapus_data');
fd.append('id', layer.db_id);
fd.append('tipe', layer.db_tipe);
fetch('../api/api.php', { method: 'POST', body: fd });
}
});
Swal.fire({icon: 'success', title: 'Terhapus', text: 'Data berhasil dihapus dari peta!', timer: 1500, showConfirmButton: false});
});
function toggleSidebar() { document.getElementById('sidebar').classList.toggle('active'); }
function toggleRightPanel() {
var panel = document.getElementById('panelInput');
if(panel) panel.classList.toggle('collapsed');
}
function filterData() {
var input = document.getElementById('searchInput').value.toLowerCase();
var cards = document.getElementsByClassName('item-data');
if(input.length > 0) document.getElementById('sidebar').classList.add('active');
for (var i = 0; i < cards.length; i++) {
cards[i].style.display = cards[i].getAttribute('data-nama').includes(input) ? "" : "none";
}
}
function changeRadius(v) {
currentRadius = parseInt(v);
document.getElementById('radiusVal').innerText = v;
if(cachedData) {
renderMap(cachedData);
renderSidebar(cachedData);
}
}
var prevReportCount = -1;
function loadData(isPolling = false) {
fetch('../api/api.php?data=all')
.then(r => r.json())
.then(data => {
cachedData = data;
// Logika Notifikasi Bubble
if (prevReportCount !== -1 && data.pelaporan.length > prevReportCount) {
var newCount = data.pelaporan.length - prevReportCount;
Swal.fire({
title: 'Laporan Baru!',
text: `Ada ${newCount} laporan warga baru yang masuk.`,
icon: 'info',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000
});
}
prevReportCount = data.pelaporan.length;
var badge = document.getElementById('notifBadge');
if(badge) {
if(data.pelaporan.length > 0) {
badge.style.display = 'block';
badge.innerText = data.pelaporan.length;
} else {
badge.style.display = 'none';
}
}
if (!isPolling) {
renderSidebar(data);
renderMap(data);
} else {
// If polling, maybe just update sidebar if it's open, or wait for manual refresh
// But for real-time map, we can re-render map as well (might cause flicker if doing heavy edits)
// We'll skip re-rendering map to avoid interrupting user edits, just update the bubble
renderSidebar(data);
renderMap(data);
}
});
}
// Auto-poll every 10 seconds
setInterval(() => {
// Only poll if not currently editing (edit_id is 0)
if(document.getElementById('edit_id') && document.getElementById('edit_id').value === "0") {
loadData(true);
}
}, 10000);
function renderSidebar(data) {
var html = `<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:20px">`;
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
html += `
<div style="background:#fee2e2; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#ef4444; font-size:1.5rem">${data.pelaporan.length}</b><br><small style="color:#991b1b; font-weight:600">Laporan</small>
</div>
<div style="background:#fef3c7; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#d97706; font-size:1.5rem">${data.warga.length}</b><br><small style="color:#92400e; font-weight:600">Warga Rentan</small>
</div>`;
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
html += `
<div style="background:#dcfce7; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#16a34a; font-size:1.5rem">${data.spbu.length}</b><br><small style="color:#166534; font-weight:600">SPBU Terdata</small>
</div>
<div style="background:#e0e7ff; padding:15px 10px; border-radius:10px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.05)">
<b style="color:#4f46e5; font-size:1.5rem">${data.jalan.length}</b><br><small style="color:#3730a3; font-weight:600">Jalur Jalan</small>
</div>`;
}
html += `</div>`;
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Pelaporan
html += `<div class="section-title" style="color:#ef4444">🚨 Laporan Darurat</div>`;
if(data.pelaporan.length === 0) html += `<small style="color:#666">Belum ada laporan</small>`;
data.pelaporan.forEach(p => {
html += `<div class="card item-data" data-nama="${p.nama_pelapor.toLowerCase()} ${p.isi_laporan.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${p.latitude}, ${p.longitude})" style="cursor:pointer">
<b>${p.nama_pelapor}</b><small><i class="far fa-clock"></i> ${p.waktu}</small><br><span>${p.isi_laporan.substring(0,40)}...</span>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('pelaporan', ${p.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${p.id}, 'pelaporan')">Hapus</button>
</div>
</div>
</div>`;
});
// Warga
html += `<div class="section-title">📍 Penduduk Miskin / Rentan</div>`;
data.warga.forEach(w => {
var pos = L.latLng(w.latitude, w.longitude);
var nearbyFacilities = data.masjid.map(m => {
return { nama: m.nama, distance: pos.distanceTo(L.latLng(m.latitude, m.longitude)) };
}).filter(f => f.distance <= currentRadius).sort((a,b) => a.distance - b.distance);
var facilityInfo = nearbyFacilities.length > 0 ? `<br><small style="color:#16a34a"><i class="fas fa-check-circle"></i> Jangkauan: ${nearbyFacilities[0].nama} (${Math.round(nearbyFacilities[0].distance)}m)</small>` : `<br><small style="color:#ef4444"><i class="fas fa-exclamation-triangle"></i> Jauh dari fasilitas / bantuan</small>`;
html += `<div class="card item-data" data-nama="${w.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${w.latitude}, ${w.longitude})" style="cursor:pointer">
<b>${w.nama.replace(/\s*\(.*?\)\s*/g, '')}</b>
${w.riwayat_penyakit ? '<span class="badge" style="background:#fef3c7; color:#d97706;"><i class="fas fa-heartbeat"></i> Medis</span>' : ''}
${facilityInfo}
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('warga', ${w.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${w.id}, 'warga')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// SPBU
html += `<div class="section-title">⛽ Stasiun Pengisian (SPBU)</div>`;
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
html += `<div class="card item-data" data-nama="${s.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${s.latitude}, ${s.longitude})" style="cursor:pointer">
<b>${s.nama}</b>
<small><i class="fas fa-phone"></i> ${s.no_hp}</small><br>
${is24 ? '<span class="badge" style="background:#dcfce7; color:#16a34a;">Buka 24 Jam</span>' : '<span class="badge" style="background:#fee2e2; color:#ef4444;">Tidak 24 Jam</span>'}
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('spbu', ${s.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${s.id}, 'spbu')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Tempat Ibadah
html += `<div class="section-title">🕌 Tempat Ibadah & Bantuan</div>`;
data.masjid.forEach(m => {
html += `<div class="card item-data" data-nama="${m.nama.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span onclick="fokus(${m.latitude}, ${m.longitude})" style="cursor:pointer"><b>${m.nama}</b></span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('masjid', ${m.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${m.id}, 'masjid')">Hapus</button>
</div>
</div>
</div>`;
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Jalan
html += `<div class="section-title">🛣️ Manajemen Jalan</div>`;
data.jalan.forEach(j => {
var color = '#ef4444'; // default red
if(j.status_jalan === 'Nasional') color = '#ef4444';
else if(j.status_jalan === 'Provinsi') color = '#eab308';
else if(j.status_jalan === 'Kabupaten') color = '#22c55e';
html += `<div class="card item-data" data-nama="${j.nama_jalan.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span>
<b>${j.nama_jalan}</b>
<small style="color:${color}; font-weight:bold;">${j.status_jalan}</small><br>
<small>${j.panjang_m} meter</small>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('jalan', ${j.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${j.id}, 'jalan')">Hapus</button>
</div>
</div>
</div>`;
});
// Parsil
html += `<div class="section-title">🟩 Manajemen Kavling / Parsil</div>`;
data.parsil.forEach(p => {
html += `<div class="card item-data" data-nama="${p.nama_pemilik.toLowerCase()}">
<div style="display:flex; justify-content:space-between">
<span>
<b>${p.nama_pemilik}</b><br>
<small>Status: <b>${p.status_hak}</b></small>
</span>
<div>
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-bottom:5px;" onclick="editData('parsil', ${p.id})">Edit</button><br>
<button class="btn-del" onclick="hapusData(${p.id}, 'parsil')">Hapus</button>
</div>
</div>
</div>`;
});
}
document.getElementById('sidebarContent').innerHTML = html;
}
function renderMap(data) {
layerMasjid.clearLayers();
layerMasjidRadius.clearLayers();
layerPelaporan.clearLayers();
layerWarga.clearLayers();
layerSpbu24.clearLayers();
layerSpbuNon24.clearLayers();
layerLines.clearLayers();
layerPolygons.clearLayers();
var lokMasjid = data.masjid.map(m => L.latLng(m.latitude, m.longitude));
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Render Masjid / Tempat Ibadah
data.masjid.forEach(m => {
var popup = `<b>${m.jenis || 'Tempat Ibadah'}:</b> ${m.nama}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('masjid', ${m.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${m.id}, 'masjid')">Hapus</button>
</div>`;
var faClass = 'fa-mosque';
if(m.jenis === 'Gereja') faClass = 'fa-church';
else if(m.jenis === 'Vihara') faClass = 'fa-vihara';
else if(m.jenis === 'Pura') faClass = 'fa-torii-gate';
// Buat custom marker bentuk map pin dengan icon sesuai jenis
var masjidIcon = L.divIcon({
html: `
<div style="
width: 34px;
height: 34px;
background-color: #2563eb;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas ${faClass}" style="
transform: rotate(45deg);
color: white;
font-size: 14px;
margin-top: 2px;
margin-left: -2px;
"></i>
</div>
`,
className: '', // Hilangkan background default Leaflet
iconSize: [34, 34],
iconAnchor: [17, 40], // Anchor ujung lancip di bawah
popupAnchor: [0, -40]
});
L.marker([m.latitude, m.longitude], { icon: masjidIcon }).addTo(layerMasjid).bindPopup(popup);
L.circle([m.latitude, m.longitude], {
radius: currentRadius, color: '#2563eb', fillOpacity: 0.1, weight: 1
}).addTo(layerMasjidRadius);
});
// Render Pelaporan
data.pelaporan.forEach(p => {
var popup = `<b>🚨 Laporan:</b> ${p.isi_laporan}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('pelaporan', ${p.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${p.id}, 'pelaporan')">Hapus</button>
</div>`;
var pelaporanIcon = L.divIcon({
html: `
<div style="
width: 32px;
height: 32px;
background-color: #f97316; /* Orange color */
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-exclamation" style="
transform: rotate(45deg);
color: white;
font-size: 16px;
font-weight: bold;
margin-top: 2px;
margin-left: -2px;
"></i>
</div>
`,
className: '',
iconSize: [32, 32],
iconAnchor: [16, 38],
popupAnchor: [0, -38]
});
L.marker([p.latitude, p.longitude], {
icon: pelaporanIcon
}).addTo(layerPelaporan).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Render SPBU
data.spbu.forEach(s => {
var is24 = s.kategori === '24jam';
var iconUrl = is24 ? 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png' : 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png';
var targetLayer = is24 ? layerSpbu24 : layerSpbuNon24;
var popup = `<b>⛽ ${s.nama}</b><br>Kategori: ${s.kategori === '24jam' ? 'Buka 24 Jam' : 'Tidak 24 Jam'}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('spbu', ${s.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${s.id}, 'spbu')">Hapus</button>
</div>`;
L.marker([s.latitude, s.longitude], {
icon: L.icon({ iconUrl: iconUrl, iconSize: [25, 41], iconAnchor: [12, 41] })
}).addTo(targetLayer).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'sosial' || window.dashboardType === 'admin') {
// Render Warga
data.warga.forEach(w => {
var pos = L.latLng(w.latitude, w.longitude);
var nearbyFacilities = data.masjid.map(m => {
return { nama: m.nama, distance: pos.distanceTo(L.latLng(m.latitude, m.longitude)) };
}).filter(f => f.distance <= currentRadius).sort((a,b) => a.distance - b.distance);
var isCovered = nearbyFacilities.length > 0;
var nearestName = isCovered ? nearbyFacilities[0].nama : '-';
var nearestDist = isCovered ? Math.round(nearbyFacilities[0].distance) + 'm' : '-';
// Buat elemen point berbentuk map pin (teardrop) dengan ikon rumah di dalamnya
var color = isCovered ? '#16a34a' : '#ef4444'; // Hijau aman, merah di luar
var houseIcon = L.divIcon({
html: `
<div style="
width: 30px;
height: 30px;
background-color: ${color};
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 2px solid white;
box-shadow: 2px 2px 5px rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
">
<div style="
width: 18px;
height: 18px;
background-color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-home" style="
transform: rotate(45deg);
color: ${color};
font-size: 10px;
"></i>
</div>
</div>
`,
className: '', // Kosongkan agar background default leaflet tidak muncul
iconSize: [30, 30],
iconAnchor: [15, 36], // Titik jangkar di ujung bawah pin
popupAnchor: [0, -36]
});
var isPenerima = (w.status_bantuan === 'Sudah') ? 'Ya' : 'Belum';
var txtPendidikan = w.pendidikan ? w.pendidikan : 'Tidak diketahui';
var txtPenyakit = (w.riwayat_penyakit && w.riwayat_penyakit.trim().length > 0) ? w.riwayat_penyakit : 'Tidak ada';
var popup = `
<div style="min-width: 200px;">
<b style="font-size: 1.1em; color: #1e293b;"><i class="fas fa-user-circle"></i> Warga: ${w.nama.replace(/\s*\(.*?\)\s*/g, '')}</b>
<hr style="margin: 5px 0; border: 0; border-top: 1px solid #cbd5e1;">
<table style="width: 100%; font-size: 0.85em; margin-bottom: 8px;">
<tr><td style="color: #64748b; padding-right: 10px;">Jangkauan</td><td>: <b>${isCovered ? '<span style="color:#16a34a">Aman (Dalam Radius)</span>' : '<span style="color:#ef4444">Di Luar</span>'}</b></td></tr>
${isCovered ? `<tr><td style="color: #64748b;">Fasilitas Trdkt</td><td>: <b>${nearestName}</b> (${nearestDist})</td></tr>` : ''}
<tr><td style="color: #64748b;">Tanggungan</td><td>: <b>${w.jumlah_kk} Jiwa</b></td></tr>
<tr><td style="color: #64748b;">Pendidikan</td><td>: ${txtPendidikan}</td></tr>
<tr><td style="color: #64748b;">Riwayat Sakit</td><td>: <span style="color: ${txtPenyakit !== 'Tidak ada' ? '#ef4444' : '#64748b'}">${txtPenyakit}</span></td></tr>
<tr><td style="color: #64748b;">Terima Bantuan</td><td>: <b>${isPenerima === 'Ya' ? '<span style="color:#16a34a">Sudah</span>' : '<span style="color:#ef4444">Belum</span>'}</b></td></tr>
</table>
<div style="font-size: 0.8em; color: #64748b; margin-bottom: 10px; background: #f8fafc; padding: 5px; border-radius: 4px; border: 1px solid #e2e8f0;">
<b>Histori:</b> ${w.histori_bantuan || '<i>Tidak ada riwayat bantuan</i>'}
</div>
<div style="text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('warga', ${w.id})">Edit Data</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${w.id}, 'warga')">Hapus</button>
</div>
</div>`;
L.marker(pos, { icon: houseIcon })
.addTo(layerWarga).bindPopup(popup);
});
}
if (!window.dashboardType || window.dashboardType === 'infra' || window.dashboardType === 'admin') {
// Render Jalan (Polyline)
data.jalan.forEach(j => {
var coords = JSON.parse(j.koordinat);
var color = '#ef4444'; // default red
if(j.status_jalan === 'Nasional') color = '#ef4444'; // Merah
else if(j.status_jalan === 'Provinsi') color = '#eab308'; // Kuning
else if(j.status_jalan === 'Kabupaten') color = '#22c55e'; // Hijau
var popup = `<b>🛣️ ${j.nama_jalan}</b><br>Status: ${j.status_jalan}<br>Panjang: ${j.panjang_m} meter<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('jalan', ${j.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${j.id}, 'jalan')">Hapus</button>
</div>`;
var polyline = L.polyline(coords, {color: color, weight: 6}).addTo(layerLines).bindPopup(popup);
polyline.db_id = j.id;
polyline.db_tipe = 'jalan';
drawnItems.addLayer(polyline);
});
// Render Parsil (Polygon)
data.parsil.forEach(p => {
var coords = JSON.parse(p.koordinat);
var popup = `<b>🟩 Kavling</b><br>Pemilik: ${p.nama_pemilik}<br>Status Hak: ${p.status_hak}<br>
<div style="margin-top:10px; text-align:center;">
<button class="btn-save" style="padding:4px 8px; font-size:0.75rem; width:auto; margin-right:5px;" onclick="editData('parsil', ${p.id})">Edit</button>
<button class="btn-del" style="padding:4px 8px; font-size:0.75rem; width:auto;" onclick="hapusData(${p.id}, 'parsil')">Hapus</button>
</div>`;
var polygon = L.polygon(coords, {color: '#2563eb', fillOpacity: 0.4}).addTo(layerPolygons).bindPopup(popup);
polygon.db_id = p.id;
polygon.db_tipe = 'parsil';
drawnItems.addLayer(polygon);
});
}
}
function toggleForm() {
var j = document.getElementById('jenis').value;
['pelaporan', 'warga', 'masjid', 'spbu', 'jalan', 'parsil'].forEach(f => {
var el = document.getElementById('form_' + f);
if (el) {
el.classList.toggle('hidden', j !== f);
}
});
}
map.on('click', e => {
document.getElementById('lat').value = e.latlng.lat;
document.getElementById('lng').value = e.latlng.lng;
L.popup().setLatLng(e.latlng).setContent("Dipilih: " + e.latlng.lat.toFixed(4) + ", " + e.latlng.lng.toFixed(4)).openOn(map);
});
function simpanData() {
var j = document.getElementById('jenis').value;
var fd = new FormData();
fd.append('action', 'simpan_' + j);
fd.append('edit_id', document.getElementById('edit_id').value);
var lat = document.getElementById('lat').value;
var lng = document.getElementById('lng').value;
if (j === 'jalan') {
fd.append('nama', document.getElementById('nama_jalan').value);
fd.append('status_jalan', document.getElementById('status_jalan').value);
fd.append('panjang', document.getElementById('panjang_jalan').value);
fd.append('koordinat', document.getElementById('jalan_coords').value);
} else if (j === 'parsil') {
fd.append('nama', document.getElementById('nama_pemilik').value);
fd.append('status_hak', document.getElementById('status_hak').value);
fd.append('koordinat', document.getElementById('parsil_coords').value);
} else {
if(j !== 'geometri' && (!lat || !lng) && document.getElementById('edit_id').value === "0") {
Swal.fire({icon: 'warning', title: 'Oops...', text: 'Silakan klik titik koordinat di peta terlebih dahulu!'});
return;
}
fd.append('lat', lat); fd.append('lng', lng);
if (j === 'pelaporan') {
fd.append('nama', document.getElementById('nama_pelapor').value);
fd.append('laporan', document.getElementById('isi_laporan').value);
} else if (j === 'warga') {
fd.append('nama', document.getElementById('nama_kk').value);
fd.append('jumlah_kk', document.getElementById('jumlah_kk').value);
fd.append('tanggal_lahir', document.getElementById('tanggal_lahir').value);
fd.append('pendidikan', document.getElementById('pendidikan').value);
fd.append('riwayat_penyakit', document.getElementById('riwayat_penyakit').value);
fd.append('status_bantuan', document.getElementById('status_bantuan').value);
fd.append('histori_bantuan', document.getElementById('histori_bantuan').value);
fd.append('alamat', document.getElementById('alamat_warga').value);
} else if (j === 'masjid') {
fd.append('nama', document.getElementById('nama_masjid').value);
fd.append('jenis', document.getElementById('jenis_masjid').value);
fd.append('pic', document.getElementById('pic_masjid').value);
fd.append('alamat', document.getElementById('alamat_masjid').value);
} else if (j === 'spbu') {
fd.append('nama', document.getElementById('nama_spbu').value);
fd.append('no_hp', document.getElementById('no_hp_spbu').value);
fd.append('kategori', document.getElementById('kategori_spbu').value);
}
}
Swal.fire({ title: 'Menyimpan...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
Swal.fire({icon: 'success', title: 'Berhasil', text: 'Data berhasil disimpan!', timer: 1500, showConfirmButton: false})
.then(() => location.reload());
}
});
}
function hapusData(id, tipe) {
Swal.fire({
title: 'Hapus Data?', text: "Data yang dihapus tidak dapat dikembalikan!", icon: 'warning',
showCancelButton: true, confirmButtonColor: '#ef4444', cancelButtonColor: '#94a3b8', confirmButtonText: 'Ya, Hapus!'
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({ title: 'Menghapus...', allowOutsideClick: false, didOpen: () => { Swal.showLoading() }});
var fd = new FormData();
fd.append('action', 'hapus_data');
fd.append('id', id);
fd.append('tipe', tipe);
fetch('../api/api.php', { method: 'POST', body: fd })
.then(res => res.json())
.then((res) => {
if (res.status === 'error') {
Swal.fire('Gagal!', res.message, 'error');
} else {
location.reload();
}
});
}
});
}
function fokus(lat, lng) { map.flyTo([lat, lng], 18); }
function batalEdit() {
document.getElementById('edit_id').value = "0";
document.getElementById('btnSimpanUtama').innerText = "Simpan Data";
document.getElementById('btnBatalEdit').style.display = "none";
document.getElementById('lat').value = "";
document.getElementById('lng').value = "";
}
function editData(tipe, id) {
var item = cachedData[tipe].find(x => x.id == id);
if(!item) return;
document.getElementById('edit_id').value = id;
document.getElementById('jenis').value = tipe;
toggleForm();
var panel = document.getElementById('panelInput');
if(panel) panel.classList.remove('collapsed');
if(tipe == 'warga') {
document.getElementById('nama_kk').value = item.nama;
document.getElementById('jumlah_kk').value = item.jumlah_kk;
document.getElementById('tanggal_lahir').value = item.tanggal_lahir || '';
document.getElementById('pendidikan').value = item.pendidikan || '';
document.getElementById('riwayat_penyakit').value = item.riwayat_penyakit || '';
document.getElementById('status_bantuan').value = item.status_bantuan || 'Belum';
document.getElementById('histori_bantuan').value = item.histori_bantuan || '';
document.getElementById('alamat_warga').value = item.alamat || '';
} else if (tipe == 'pelaporan') {
document.getElementById('nama_pelapor').value = item.nama_pelapor;
document.getElementById('isi_laporan').value = item.isi_laporan;
} else if (tipe == 'spbu') {
document.getElementById('nama_spbu').value = item.nama;
document.getElementById('no_hp_spbu').value = item.no_hp;
document.getElementById('kategori_spbu').value = item.kategori;
} else if (tipe == 'masjid') {
document.getElementById('nama_masjid').value = item.nama;
document.getElementById('pic_masjid').value = item.pic;
document.getElementById('alamat_masjid').value = item.alamat;
} else if (tipe == 'jalan') {
document.getElementById('nama_jalan').value = item.nama_jalan;
document.getElementById('status_jalan').value = item.status_jalan;
document.getElementById('panjang_jalan').value = item.panjang_m;
document.getElementById('jalan_coords').value = item.koordinat;
} else if (tipe == 'parsil') {
document.getElementById('nama_pemilik').value = item.nama_pemilik;
document.getElementById('status_hak').value = item.status_hak;
document.getElementById('parsil_coords').value = item.koordinat;
}
if(item.latitude && item.longitude) {
document.getElementById('lat').value = item.latitude;
document.getElementById('lng').value = item.longitude;
fokus(item.latitude, item.longitude);
}
document.getElementById('btnSimpanUtama').innerText = "Update Data";
document.getElementById('btnBatalEdit').style.display = "inline-block";
}
// INIT
loadData();
+149
View File
@@ -0,0 +1,149 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--dark: #0f172a;
--dark-grad: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
--success: #10b981;
--success-hover: #059669;
--danger: #ef4444;
--danger-hover: #dc2626;
--warning: #f59e0b;
--bg-light: #f8fafc;
--border: #e2e8f0;
}
body {
margin: 0; padding: 0;
font-family: 'Inter', sans-serif;
overflow: hidden; display: flex;
background: #e2e8f0;
}
#map { width: 100vw; height: 100vh; z-index: 1; }
/* Custom Sleek Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
/* SEARCH & MENU */
.search-container { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1100; width: 380px; }
.search-box {
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px);
border-radius: 50px; display: flex; align-items: center; padding: 5px 20px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.5);
transition: 0.3s ease;
}
.search-box:focus-within { box-shadow: 0 10px 30px rgba(37,99,235,0.2); border-color: var(--primary); }
.search-box i { color: #64748b; font-size: 1.1rem; }
.search-box input[type="text"] {
border: none !important; outline: none; padding: 12px 10px; margin: 0;
width: 100%; background: transparent !important; font-size: 0.95rem;
color: var(--dark); font-weight: 500; box-shadow: none !important;
}
.search-box input::placeholder { color: #94a3b8; }
.menu-toggle {
position: absolute; bottom: 30px; left: 20px; z-index: 1200;
background: var(--dark-grad); color: white; border: none;
width: 55px; height: 55px; border-radius: 50%; cursor: pointer;
box-shadow: 0 10px 25px rgba(15,23,42,0.4); font-size: 1.4rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.menu-toggle:hover { transform: scale(1.1) rotate(5deg); }
.menu-toggle:active { transform: scale(0.95); }
/* SIDEBAR */
#sidebar {
width: 360px; height: 100vh; background: white; border-right: 1px solid var(--border);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 1150; position: absolute; left: 0; transform: translateX(-100%);
display: flex; flex-direction: column; box-shadow: 5px 0 25px rgba(0,0,0,0.05);
}
#sidebar.active { transform: translateX(0); }
.sidebar-header { padding: 25px 20px; background: var(--dark-grad); color: white; border-bottom: 3px solid var(--primary); }
.sidebar-header h3 { margin: 0 0 5px 0; font-size: 1.4rem; font-weight: 700; letter-spacing: -0.5px; }
.sidebar-header small { color: #cbd5e1; font-weight: 500; font-size: 0.85rem;}
.sidebar-content { padding: 20px; overflow-y: auto; flex: 1; background: var(--bg-light); }
/* PANEL INPUT */
.panel-input {
position: absolute; top: 20px; right: 20px; width: 320px;
background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(10px);
border-radius: 16px; padding: 25px 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.12);
z-index: 1000; max-height: 85vh; overflow-y: auto; border: 1px solid rgba(255,255,255,0.6);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
transform-origin: top right;
}
.panel-input.collapsed {
transform: scale(0.9) translateX(150%);
opacity: 0;
pointer-events: none;
}
.panel-input h4 { color: var(--dark); font-size: 1.1rem; font-weight: 700; border-bottom: 2px solid var(--border); padding-bottom: 10px; margin-top: 0; }
/* RADIUS SLIDER */
.radius-box {
background: #eff6ff; padding: 15px; border-radius: 12px; margin-bottom: 20px;
border: 1px solid #bfdbfe; box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
}
.radius-box label { font-size: 0.85rem; font-weight: 700; color: var(--primary); display: block; margin-bottom: 8px;}
input[type=range] { width: 100%; cursor: pointer; accent-color: var(--primary); }
/* UI HELPERS */
input[type=text], input[type=number], input[type=date], select, textarea {
width: 100%; padding: 12px 15px; margin: 0 0 15px;
border: 1px solid #cbd5e1; border-radius: 8px; box-sizing: border-box;
transition: all 0.3s ease; font-family: 'Inter', sans-serif; font-size: 0.9rem;
background: #f8fafc; color: var(--dark);
}
input:focus, select:focus, textarea:focus {
border-color: var(--primary); outline: none; background: white;
box-shadow: 0 0 0 4px rgba(37,99,235,0.15); transform: translateY(-1px);
}
select { cursor: pointer; font-weight: 500; }
textarea { resize: vertical; min-height: 80px; }
/* CARDS IN SIDEBAR */
.card {
background: #ffffff; padding: 15px; border-radius: 12px; margin-bottom: 12px;
border: 1px solid var(--border); font-size: 0.85rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.02); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; overflow: hidden;
}
.card::before { content:''; position:absolute; left:0; top:0; width:4px; height:100%; background: var(--primary); opacity: 0; transition: 0.3s; }
.card:hover { transform: translateY(-3px); box-shadow: 0 10px 15px rgba(0,0,0,0.06); border-color: #cbd5e1; }
.card:hover::before { opacity: 1; }
.card b { color: var(--dark); display: block; font-size: 0.95rem; margin-bottom: 4px; font-weight: 700;}
.hidden { display: none !important; }
/* BUTTONS */
.btn-save {
background: var(--success); color: white; border: none; padding: 14px;
border-radius: 10px; font-weight: 700; cursor: pointer; width: 100%;
font-size: 1rem; transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.3); text-transform: uppercase; letter-spacing: 0.5px;
}
.btn-save:hover { background: var(--success-hover); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(16, 185, 129, 0.4); }
.btn-save:active { transform: translateY(0); }
.btn-del {
background: #fee2e2; color: var(--danger); border: none; padding: 5px 10px;
border-radius: 6px; font-size: 0.75rem; font-weight: 600; cursor: pointer; transition: 0.2s;
}
.btn-del:hover { background: var(--danger); color: white; }
.badge {
display: inline-block; padding: 4px 8px; border-radius: 6px;
font-size: 0.7rem; font-weight: 700; margin-top: 5px; text-transform: uppercase; letter-spacing: 0.5px;
}
.section-title {
font-size: 1rem; font-weight: 700; color: var(--dark);
margin: 20px 0 12px 0; border-bottom: 2px solid var(--border);
padding-bottom: 8px; display: flex; align-items: center; gap: 8px;
}