Initial commit

This commit is contained in:
muthianura
2026-06-11 18:10:22 +07:00
commit f104007f77
30 changed files with 7141 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
<?php
include 'db2.php';
require_once __DIR__ . '/includes/stats.php';
function toGeoJsonLine($pts) {
$coords = array_map(fn($p) => [(float)$p[1], (float)$p[0]], $pts);
return json_encode(['type' => 'LineString', 'coordinates' => $coords]);
}
function toGeoJsonPoly($pts) {
$coords = array_map(fn($p) => [(float)$p[1], (float)$p[0]], $pts);
if (count($coords) && $coords[0] !== $coords[count($coords)-1]) $coords[] = $coords[0];
return json_encode(['type' => 'Polygon', 'coordinates' => [$coords]]);
}
function toGeoJsonPoint($lat, $lng) {
return json_encode(['type' => 'Point', 'coordinates' => [(float)$lng, (float)$lat]]);
}
$action = $_GET['action'] ?? $_POST['action'] ?? null;
if (!$action) return;
header('Content-Type: application/json; charset=utf-8');
$a = $action;
if ($a === 'search') {
$q = '%' . $conn->real_escape_string($_GET['q'] ?? '') . '%';
$tipe = $_GET['tipe'] ?? 'semua';
$sf = $_GET['status_filter'] ?? '';
$pmin = (float)($_GET['panjang_min'] ?? 0);
$pmax = (float)($_GET['panjang_max'] ?? 0);
$rusak_periode = $_GET['rusak_periode'] ?? 'semua';
$out = [];
if ($tipe === 'semua' || $tipe === 'jalan') {
$w = "nama_jalan LIKE '$q'";
if ($sf) $w .= " AND status_jalan='" . $conn->real_escape_string($sf) . "'";
if ($pmin > 0) $w .= " AND panjang_meter>=$pmin";
if ($pmax > 0) $w .= " AND panjang_meter<=$pmax";
$rj = $conn->query("SELECT id,nama_jalan,status_jalan,panjang_meter,koordinat FROM jalan WHERE $w LIMIT 20");
while ($row = $rj->fetch_assoc()) { $row['type'] = 'jalan'; $out[] = $row; }
}
if ($tipe === 'semua' || $tipe === 'parsil') {
$w = "(nama_parsil LIKE '$q' OR nama_pemilik LIKE '$q')";
if ($sf) $w .= " AND status_kepemilikan='" . $conn->real_escape_string($sf) . "'";
$rp = $conn->query("SELECT id,nama_parsil,nama_pemilik,nomor_sertifikat,status_kepemilikan,luas_m2,koordinat FROM parsil WHERE $w LIMIT 20");
while ($row = $rp->fetch_assoc()) { $row['type'] = 'parsil'; $out[] = $row; }
}
if ($tipe === 'semua' || $tipe === 'rusak') {
$wR = "nama LIKE '$q'";
if ($rusak_periode === '1bulan') $wR .= " AND tanggal >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)";
elseif ($rusak_periode === '3tahun') $wR .= " AND tanggal >= DATE_SUB(CURDATE(), INTERVAL 3 YEAR)";
$rr = $conn->query("SELECT id,nama,tanggal,latitude,longitude,keterangan,koordinat FROM jalan_rusak WHERE $wR ORDER BY tanggal DESC LIMIT 20");
while ($row = $rr->fetch_assoc()) { $row['type'] = 'rusak'; $out[] = $row; }
}
echo json_encode($out); exit;
}
if ($a === 'save_jalan' && isset($_GET['nama'], $_GET['status'], $_GET['ukuran'], $_GET['koordinat'])) {
$nama = $conn->real_escape_string($_GET['nama']);
$status = $conn->real_escape_string($_GET['status']);
$panjang = (float)$_GET['ukuran'];
$raw = json_decode($_GET['koordinat'], true);
$geojson = $conn->real_escape_string(toGeoJsonLine($raw));
$r = $conn->query("INSERT INTO jalan(nama_jalan,status_jalan,panjang_meter,koordinat) VALUES('$nama','$status','$panjang','$geojson')");
echo json_encode(array_merge(['ok' => (bool)$r, 'id' => $conn->insert_id, 'msg' => $conn->error], getStats($conn))); exit;
}
if ($a === 'save_parsil' && isset($_GET['nama'], $_GET['status'], $_GET['ukuran'], $_GET['koordinat'])) {
$nama = $conn->real_escape_string($_GET['nama']);
$pemilik = $conn->real_escape_string($_GET['pemilik'] ?? '');
$nosert = $conn->real_escape_string($_GET['nosert'] ?? '');
$ket = $conn->real_escape_string($_GET['keterangan'] ?? '');
$status = $conn->real_escape_string($_GET['status']);
$luas = (float)$_GET['ukuran'];
$raw = json_decode($_GET['koordinat'], true);
$geojson = $conn->real_escape_string(toGeoJsonPoly($raw));
$r = $conn->query("INSERT INTO parsil(nama_parsil,nama_pemilik,nomor_sertifikat,keterangan,status_kepemilikan,luas_m2,koordinat) VALUES('$nama','$pemilik','$nosert','$ket','$status','$luas','$geojson')");
echo json_encode(array_merge(['ok' => (bool)$r, 'id' => $conn->insert_id, 'msg' => $conn->error], getStats($conn))); exit;
}
if ($a === 'save_rusak') {
$pelapor = $conn->real_escape_string($_POST['pelapor'] ?? $_GET['pelapor'] ?? '') ?: 'Anonym';
$nama_jalan = $conn->real_escape_string($_POST['nama_jalan'] ?? $_GET['nama_jalan'] ?? '');
$tanggal = $conn->real_escape_string($_POST['tanggal'] ?? $_GET['tanggal'] ?? date('Y-m-d'));
$lat = (float)($_POST['lat'] ?? $_GET['lat'] ?? 0);
$lng = (float)($_POST['lng'] ?? $_GET['lng'] ?? 0);
$ket = $conn->real_escape_string($_POST['keterangan'] ?? $_GET['keterangan'] ?? '');
$foto = $conn->real_escape_string($_POST['foto'] ?? $_GET['foto'] ?? '');
$namaFull = $conn->real_escape_string($pelapor . ($nama_jalan ? ' | Jalan: ' . $nama_jalan : ''));
$geojson = $conn->real_escape_string(toGeoJsonPoint($lat, $lng));
$r = $conn->query("INSERT INTO jalan_rusak(nama,tanggal,latitude,longitude,keterangan,foto,koordinat) VALUES('$namaFull','$tanggal','$lat','$lng','$ket','$foto','$geojson')");
echo json_encode(array_merge(['ok' => (bool)$r, 'id' => $conn->insert_id, 'msg' => $conn->error, 'nama' => $namaFull], getStats($conn))); exit;
}
if ($a === 'delete_jalan' && isset($_GET['id'])) {
$conn->query("DELETE FROM jalan WHERE id=" . (int)$_GET['id']);
echo json_encode(getStats($conn)); exit;
}
if ($a === 'delete_parsil' && isset($_GET['id'])) {
$conn->query("DELETE FROM parsil WHERE id=" . (int)$_GET['id']);
echo json_encode(getStats($conn)); exit;
}
if ($a === 'delete_rusak' && isset($_GET['id'])) {
$conn->query("DELETE FROM jalan_rusak WHERE id=" . (int)$_GET['id']);
echo json_encode(getStats($conn)); exit;
}
if ($a === 'edit_jalan' && isset($_GET['id'], $_GET['nama'], $_GET['status'])) {
$id = (int)$_GET['id'];
$n = $conn->real_escape_string($_GET['nama']);
$s = $conn->real_escape_string($_GET['status']);
$coords = $_GET['koordinat'] ?? '';
$pjg = 0;
if ($coords) {
$g = json_decode($coords, true);
$pts = [];
if (isset($g['type']) && $g['type'] === 'LineString') foreach ($g['coordinates'] as $c) $pts[] = ['lat' => $c[1], 'lng' => $c[0]];
else foreach (json_decode($coords, true) as $p) $pts[] = ['lat' => $p[0], 'lng' => $p[1]];
for ($i = 0; $i < count($pts) - 1; $i++) {
$la1 = deg2rad($pts[$i]['lat']); $lo1 = deg2rad($pts[$i]['lng']);
$la2 = deg2rad($pts[$i+1]['lat']); $lo2 = deg2rad($pts[$i+1]['lng']);
$d = 2 * 6371000 * asin(sqrt(sin(($la2-$la1)/2)**2 + cos($la1)*cos($la2)*sin(($lo2-$lo1)/2)**2));
$pjg += $d;
}
$ce = $conn->real_escape_string($coords);
$conn->query("UPDATE jalan SET nama_jalan='$n',status_jalan='$s',panjang_meter='$pjg',koordinat='$ce' WHERE id=$id");
} else {
$conn->query("UPDATE jalan SET nama_jalan='$n',status_jalan='$s' WHERE id=$id");
$pjg = $conn->query("SELECT panjang_meter FROM jalan WHERE id=$id")->fetch_assoc()['panjang_meter'];
}
echo json_encode(array_merge(getStats($conn), ['panjang' => round($pjg, 1)])); exit;
}
if ($a === 'edit_parsil' && isset($_GET['id'], $_GET['nama'], $_GET['status'])) {
$id = (int)$_GET['id'];
$n = $conn->real_escape_string($_GET['nama']);
$s = $conn->real_escape_string($_GET['status']);
$pemilik = $conn->real_escape_string($_GET['pemilik'] ?? '');
$nosert = $conn->real_escape_string($_GET['nosert'] ?? '');
$ket = $conn->real_escape_string($_GET['keterangan'] ?? '');
$coords = $_GET['koordinat'] ?? '';
$luas = 0;
if ($coords) {
$g = json_decode($coords, true);
$pts = isset($g['type']) && $g['type'] === 'Polygon'
? array_map(fn($c) => ['lat' => $c[1], 'lng' => $c[0]], $g['coordinates'][0])
: array_map(fn($p) => ['lat' => $p[0], 'lng' => $p[1]], json_decode($coords, true));
$R = 6371000; $n2 = count($pts); $area = 0;
for ($i = 0; $i < $n2; $i++) {
$j = ($i + 1) % $n2;
$xi = $pts[$i]['lng'] * M_PI/180 * $R * cos($pts[$i]['lat'] * M_PI/180);
$yi = $pts[$i]['lat'] * M_PI/180 * $R;
$xj = $pts[$j]['lng'] * M_PI/180 * $R * cos($pts[$j]['lat'] * M_PI/180);
$yj = $pts[$j]['lat'] * M_PI/180 * $R;
$area += ($xi * $yj - $xj * $yi);
}
$luas = abs($area / 2);
$ce = $conn->real_escape_string($coords);
$conn->query("UPDATE parsil SET nama_parsil='$n',status_kepemilikan='$s',nama_pemilik='$pemilik',nomor_sertifikat='$nosert',keterangan='$ket',luas_m2='$luas',koordinat='$ce' WHERE id=$id");
} else {
$conn->query("UPDATE parsil SET nama_parsil='$n',status_kepemilikan='$s',nama_pemilik='$pemilik',nomor_sertifikat='$nosert',keterangan='$ket' WHERE id=$id");
$luas = $conn->query("SELECT luas_m2 FROM parsil WHERE id=$id")->fetch_assoc()['luas_m2'];
}
echo json_encode(array_merge(getStats($conn), ['luas' => round($luas, 1)])); exit;
}
if ($a === 'edit_rusak' && isset($_GET['id'])) {
$id = (int)$_GET['id'];
$n = $conn->real_escape_string($_POST['nama'] ?? $_GET['nama'] ?? '');
$tgl = $conn->real_escape_string($_POST['tanggal'] ?? $_GET['tanggal'] ?? '');
$ket = $conn->real_escape_string($_POST['keterangan'] ?? $_GET['keterangan'] ?? '');
$foto = $conn->real_escape_string($_POST['foto'] ?? $_GET['foto'] ?? '');
$lat = isset($_POST['latitude']) ? (float)$_POST['latitude'] : (isset($_GET['latitude']) ? (float)$_GET['latitude'] : null);
$lng = isset($_POST['longitude']) ? (float)$_POST['longitude'] : (isset($_GET['longitude']) ? (float)$_GET['longitude'] : null);
if ($lat !== null && $lng !== null) {
$geojson = $conn->real_escape_string(toGeoJsonPoint($lat, $lng));
$conn->query("UPDATE jalan_rusak SET nama='$n',tanggal='$tgl',keterangan='$ket',foto='$foto',latitude='$lat',longitude='$lng',koordinat='$geojson' WHERE id=$id");
} else {
$conn->query("UPDATE jalan_rusak SET nama='$n',tanggal='$tgl',keterangan='$ket',foto='$foto' WHERE id=$id");
}
echo json_encode(array_merge(getStats($conn), ['ok' => true])); exit;
}
echo json_encode(['error' => 'unknown']); exit;
+407
View File
@@ -0,0 +1,407 @@
/* Input mode: draw jalan / kavling / rusak on map */
var drawMode = null, drawing = false, points = [], previewLayer = null, tempMarkers = [];
var pendingKoord = null, pendingUkuran = 0, currentMode = null;
var pendingRusakLatLng = null, fotoBase64 = '';
function setMode(m) {
if (drawing) cancelDraw();
drawMode = m;
currentMode = m;
var bj = document.getElementById('btn-jalan');
var bp = document.getElementById('btn-parsil');
var br = document.getElementById('btn-rusak');
if (bj) bj.className = m === 'jalan' ? 'active-jalan' : '';
if (bp) bp.className = m === 'parsil' ? 'active-parsil' : '';
if (br) br.className = m === 'rusak' ? 'active-rusak' : '';
var actions = document.getElementById('draw-actions');
var hint = document.getElementById('hint');
if (m === 'rusak') {
drawing = false;
points = [];
if (hint) hint.textContent = 'Klik titik di peta untuk menandai jalan rusak';
map.getContainer().style.cursor = 'crosshair';
if (actions) actions.style.display = 'flex';
['undo-btn', 'finish-btn'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.style.display = 'none';
});
var cancel = document.getElementById('cancel-btn');
if (cancel) cancel.style.display = 'block';
return;
}
drawing = true;
points = [];
if (actions) actions.style.display = 'flex';
['undo-btn', 'finish-btn', 'cancel-btn'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.style.display = 'block';
});
if (hint) {
hint.textContent = m === 'jalan'
? 'Klik peta untuk menambah titik jalan — minimal 2 titik'
: 'Klik peta untuk menggambar area kavling — minimal 3 titik';
}
map.getContainer().style.cursor = 'crosshair';
}
map.on('click', function (e) {
if (currentMode === 'rusak') {
pendingRusakLatLng = e.latlng;
openRusakModal(e.latlng);
return;
}
if (!drawing) return;
points.push(e.latlng);
var dot = L.circleMarker(e.latlng, {
radius: 5,
color: drawMode === 'jalan' ? '#2563eb' : '#7c3aed',
fillColor: '#fff',
fillOpacity: 1,
weight: 2.5
}).addTo(map);
tempMarkers.push(dot);
redrawPreview();
});
function redrawPreview() {
if (previewLayer) map.removeLayer(previewLayer);
if (points.length < 2) return;
var opts = { color: drawMode === 'jalan' ? '#2563eb' : '#7c3aed', weight: 2.5, dashArray: '7,5', opacity: 0.85 };
previewLayer = drawMode === 'jalan'
? L.polyline(points, opts).addTo(map)
: L.polygon(points, Object.assign({}, opts, { fillOpacity: 0.1 })).addTo(map);
}
function undoLast() {
if (!points.length) return;
points.pop();
var last = tempMarkers.pop();
if (last) map.removeLayer(last);
redrawPreview();
}
function cancelDraw() {
drawing = false;
points = [];
if (previewLayer) { map.removeLayer(previewLayer); previewLayer = null; }
tempMarkers.forEach(function (m) { map.removeLayer(m); });
tempMarkers = [];
map.getContainer().style.cursor = '';
var actions = document.getElementById('draw-actions');
if (actions) actions.style.display = 'none';
['btn-jalan', 'btn-parsil', 'btn-rusak'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.className = '';
});
var hint = document.getElementById('hint');
if (hint) hint.textContent = 'Pilih mode input di atas';
currentMode = null;
drawMode = null;
pendingRusakLatLng = null;
}
function finishShape() {
if (drawMode === 'jalan' && points.length < 2) { showToast('Minimal 2 titik untuk jalan!', 'error'); return; }
if (drawMode === 'parsil' && points.length < 3) { showToast('Minimal 3 titik untuk kavling!', 'error'); return; }
drawing = false;
map.getContainer().style.cursor = '';
var ukuran = 0;
if (drawMode === 'jalan') {
for (var i = 0; i < points.length - 1; i++) ukuran += points[i].distanceTo(points[i + 1]);
ukuran = Math.round(ukuran * 10) / 10;
document.getElementById('inp-ukuran').value = ukuran.toFixed(1) + ' meter';
document.getElementById('form-title').textContent = 'Simpan Data Jalan';
document.getElementById('form-sub').textContent = 'Lengkapi informasi ruas jalan';
document.getElementById('form-icon').textContent = '🛣';
document.getElementById('form-icon').className = 'modal-icon jalan';
document.getElementById('label-nama').textContent = 'Nama Jalan';
document.getElementById('label-status').textContent = 'Status Jalan';
document.getElementById('label-ukuran').innerHTML = 'Panjang Jalan <span class="readonly-tag">Otomatis</span>';
document.getElementById('inp-nama').placeholder = 'Contoh: Jl. Ahmad Yani';
document.getElementById('inp-status').innerHTML =
'<option value="Jalan Nasional">Jalan Nasional</option>' +
'<option value="Jalan Provinsi">Jalan Provinsi</option>' +
'<option value="Jalan Kabupaten">Jalan Kabupaten</option>';
document.getElementById('parsil-extra-fields').style.display = 'none';
} else {
ukuran = calcArea(points);
ukuran = Math.round(ukuran * 10) / 10;
document.getElementById('inp-ukuran').value = ukuran.toFixed(1) + ' m²';
document.getElementById('form-title').textContent = 'Simpan Data Kavling';
document.getElementById('form-sub').textContent = 'Lengkapi informasi kavling tanah';
document.getElementById('form-icon').textContent = '🏘';
document.getElementById('form-icon').className = 'modal-icon parsil';
document.getElementById('label-nama').textContent = 'Nama / Nomor Parsil';
document.getElementById('label-status').textContent = 'Status Kepemilikan';
document.getElementById('label-ukuran').innerHTML = 'Luas Area <span class="readonly-tag">Otomatis</span>';
document.getElementById('inp-nama').placeholder = 'Contoh: Parsil A-01';
document.getElementById('inp-status').innerHTML =
'<option value="SHM">SHM — Sertifikat Hak Milik</option>' +
'<option value="HGB">HGB — Hak Guna Bangunan</option>' +
'<option value="HGU">HGU — Hak Guna Usaha</option>' +
'<option value="HP">HP — Hak Pakai</option>';
document.getElementById('parsil-extra-fields').style.display = 'block';
document.getElementById('inp-pemilik').value = '';
document.getElementById('inp-nosert').value = '';
document.getElementById('inp-keterangan').value = '';
}
pendingKoord = points.map(function (p) { return [p.lat, p.lng]; });
pendingUkuran = ukuran;
document.getElementById('inp-nama').value = '';
document.getElementById('form-modal').classList.add('active');
setTimeout(function () { document.getElementById('inp-nama').focus(); }, 100);
}
function calcArea(latlngs) {
var R = 6371000, n = latlngs.length, area = 0;
if (n < 3) return 0;
for (var i = 0; i < n; i++) {
var j = (i + 1) % n;
var xi = latlngs[i].lng * Math.PI / 180 * R * Math.cos(latlngs[i].lat * Math.PI / 180);
var yi = latlngs[i].lat * Math.PI / 180 * R;
var xj = latlngs[j].lng * Math.PI / 180 * R * Math.cos(latlngs[j].lat * Math.PI / 180);
var yj = latlngs[j].lat * Math.PI / 180 * R;
area += xi * yj - xj * yi;
}
return Math.abs(area / 2);
}
function saveData() {
var nama = document.getElementById('inp-nama').value.trim();
var status = document.getElementById('inp-status').value;
if (!nama) { showToast('Nama tidak boleh kosong!', 'error'); return; }
var btn = document.querySelector('#form-modal .btn-primary');
btn.textContent = 'Menyimpan…';
btn.disabled = true;
var params = {
action: currentMode === 'jalan' ? 'save_jalan' : 'save_parsil',
nama: nama,
status: status,
ukuran: pendingUkuran,
koordinat: JSON.stringify(pendingKoord)
};
if (currentMode === 'parsil') {
params.pemilik = document.getElementById('inp-pemilik').value.trim();
params.nosert = document.getElementById('inp-nosert').value.trim();
params.keterangan = document.getElementById('inp-keterangan').value.trim();
}
fetch('api.php?' + new URLSearchParams(params))
.then(function (r) { return r.json(); })
.then(function (res) {
btn.textContent = 'Simpan Data';
btn.disabled = false;
if (res.ok) {
showToast('Data berhasil disimpan!', 'success');
closeForm();
var id = res.id;
var ll = pendingKoord.map(function (p) { return [p[0], p[1]]; });
if (currentMode === 'jalan') {
var layer = L.polyline(ll, { color: WARNA_JALAN[status] || '#999', weight: 5, opacity: 0.9 }).addTo(map);
layer.bindPopup(makeJalanPopup(id, nama, status, pendingUkuran.toFixed(1)), { maxWidth: 280 });
layer.on('click', function () { lnavHighlightFromMap('jalan', id); });
jalanLayers[id] = layer;
jalanData[id] = { id: id, nama: nama, status: status, pjg: pendingUkuran.toFixed(1), ll: ll };
} else {
var opt = WARNA_PARSIL[status] || { color: '#999', fillColor: '#999', fillOpacity: 0.2, weight: 2 };
var layer2 = L.polygon(ll, opt).addTo(map);
layer2.bindPopup(makeParsilPopup(id, nama, params.pemilik || '', params.nosert || '', params.keterangan || '', status, pendingUkuran.toFixed(1)), { maxWidth: 280 });
layer2.on('click', function () { lnavHighlightFromMap('parsil', id); });
parsilLayers[id] = layer2;
parsilData[id] = { id: id, nama: nama, pemilik: params.pemilik || '', nosert: params.nosert || '', ket: params.keterangan || '', status: status, luas: pendingUkuran.toFixed(1), ll: ll };
}
if (res.tj !== undefined) updateStats(res);
cancelDraw();
} else {
showToast('Gagal: ' + (res.msg || 'unknown'), 'error');
}
})
.catch(function () {
btn.textContent = 'Simpan Data';
btn.disabled = false;
showToast('Koneksi gagal!', 'error');
});
}
function closeForm() {
document.getElementById('form-modal').classList.remove('active');
}
document.getElementById('form-modal').addEventListener('click', function (e) {
if (e.target === this) closeForm();
});
function openRusakModal(latlng) {
var d = new Date();
var today = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
document.getElementById('rusak-tanggal').value = today;
document.getElementById('rusak-koordinat').value = latlng.lat.toFixed(6) + ', ' + latlng.lng.toFixed(6);
document.getElementById('rusak-sub').textContent = latlng.lat.toFixed(5) + ', ' + latlng.lng.toFixed(5);
document.getElementById('rusak-pelapor').value = '';
document.getElementById('rusak-nama-jalan').value = 'Mencari nama jalan…';
document.getElementById('rusak-ket').value = '';
clearFoto();
fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latlng.lat + '&lon=' + latlng.lng + '&zoom=17&accept-language=id')
.then(function (r) { return r.json(); })
.then(function (geo) {
var road = geo.address && (geo.address.road || geo.address.residential || geo.address.path || geo.address.pedestrian || geo.address.footway || '');
document.getElementById('rusak-nama-jalan').value = road || '(Jalan tidak teridentifikasi)';
})
.catch(function () {
document.getElementById('rusak-nama-jalan').value = '(Gagal deteksi nama jalan)';
});
var badge = document.getElementById('gps-badge');
badge.style.display = 'inline-flex';
badge.className = 'gps-badge loading';
badge.textContent = 'Mengambil GPS…';
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (pos) {
badge.className = 'gps-badge';
badge.textContent = 'GPS: ±' + Math.round(pos.coords.accuracy) + 'm akurasi';
}, function () {
badge.className = 'gps-badge';
badge.textContent = 'Koordinat dari klik peta';
}, { timeout: 5000 });
} else {
badge.className = 'gps-badge';
badge.textContent = 'Koordinat dari klik peta';
}
document.getElementById('rusak-modal').classList.add('active');
setTimeout(function () { document.getElementById('rusak-pelapor').focus(); }, 100);
}
function closeRusakModal() {
document.getElementById('rusak-modal').classList.remove('active');
fotoBase64 = '';
pendingRusakLatLng = null;
}
document.getElementById('rusak-modal').addEventListener('click', function (e) {
if (e.target === this) closeRusakModal();
});
function handleFoto(input) {
var file = input.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) { showToast('Foto maks 5 MB!', 'error'); return; }
EXIF.getData(file, function() {
var lat = EXIF.getTag(this, "GPSLatitude");
var lon = EXIF.getTag(this, "GPSLongitude");
var latRef = EXIF.getTag(this, "GPSLatitudeRef") || "N";
var lonRef = EXIF.getTag(this, "GPSLongitudeRef") || "E";
if (lat && lon) {
function parseDms(dms) {
if (!dms || dms.length < 3) return null;
var d = (typeof dms[0] === 'object' && dms[0].denominator) ? dms[0].numerator / dms[0].denominator : Number(dms[0]);
var m = (typeof dms[1] === 'object' && dms[1].denominator) ? dms[1].numerator / dms[1].denominator : Number(dms[1]);
var s = (typeof dms[2] === 'object' && dms[2].denominator) ? dms[2].numerator / dms[2].denominator : Number(dms[2]);
return d + (m / 60) + (s / 3600);
}
var latDec = parseDms(lat);
var lonDec = parseDms(lon);
if (latDec !== null && lonDec !== null && !isNaN(latDec) && !isNaN(lonDec)) {
if (latRef === "S") latDec = -latDec;
if (lonRef === "W") lonDec = -lonDec;
pendingRusakLatLng = {lat: latDec, lng: lonDec};
document.getElementById('rusak-koordinat').value = latDec.toFixed(6) + ', ' + lonDec.toFixed(6);
showToast('Geotagging terdeteksi!', 'success');
var badge = document.getElementById('gps-badge');
if(badge) {
badge.style.display = 'inline-flex';
badge.className = 'gps-badge';
badge.textContent = 'GPS dari Foto';
}
fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latDec + '&lon=' + lonDec + '&zoom=17&accept-language=id')
.then(function (r) { return r.json(); })
.then(function (geo) {
var road = geo.address && (geo.address.road || geo.address.residential || geo.address.path || geo.address.pedestrian || geo.address.footway || '');
document.getElementById('rusak-nama-jalan').value = road || geo.display_name || '(Jalan tidak teridentifikasi)';
});
}
}
});
var reader = new FileReader();
reader.onload = function (e) {
fotoBase64 = e.target.result;
var preview = document.getElementById('photo-preview');
preview.src = fotoBase64;
preview.style.display = 'block';
document.getElementById('foto-clear-btn').style.display = 'flex';
};
reader.readAsDataURL(file);
}
function clearFoto() {
fotoBase64 = '';
document.getElementById('photo-preview').style.display = 'none';
document.getElementById('photo-preview').src = '';
document.getElementById('foto-clear-btn').style.display = 'none';
document.getElementById('foto-file').value = '';
document.getElementById('foto-cam').value = '';
}
function saveRusak() {
if (!pendingRusakLatLng) { showToast('Pilih titik di peta dulu!', 'error'); return; }
var pelapor = document.getElementById('rusak-pelapor').value.trim() || 'Anonym';
var namaJalan = document.getElementById('rusak-nama-jalan').value.trim();
var tanggal = document.getElementById('rusak-tanggal').value;
var ket = document.getElementById('rusak-ket').value.trim();
var lat = pendingRusakLatLng.lat;
var lng = pendingRusakLatLng.lng;
var btn = document.getElementById('rusak-save-btn');
btn.textContent = 'Menyimpan…';
btn.disabled = true;
var formData = new FormData();
formData.append('action', 'save_rusak');
formData.append('pelapor', pelapor);
formData.append('nama_jalan', namaJalan);
formData.append('tanggal', tanggal);
formData.append('lat', lat);
formData.append('lng', lng);
formData.append('keterangan', ket);
formData.append('foto', fotoBase64);
fetch('api.php', { method: 'POST', body: formData })
.then(function (r) { return r.json(); })
.then(function (res) {
btn.textContent = 'Simpan Titik';
btn.disabled = false;
if (res.ok) {
showToast('Titik jalan rusak disimpan!', 'success');
closeRusakModal();
var id = res.id;
var nama = res.nama || pelapor;
var layer = makeRusakLayer(id, nama, tanggal, lat, lng, ket, fotoBase64, 'recent', false);
rusakLayers[id] = layer;
rusakData[id] = { id: id, nama: nama, tgl: tanggal, lat: lat, lng: lng, ket: ket, foto: fotoBase64, age: 'recent', recurring: false };
if (res.tr !== undefined) updateStats(res);
cancelDraw();
} else {
showToast('Gagal: ' + (res.msg || 'unknown'), 'error');
}
})
.catch(function () {
btn.textContent = 'Simpan Titik';
btn.disabled = false;
showToast('Koneksi gagal!', 'error');
});
}
+11
View File
@@ -0,0 +1,11 @@
/* SIG Jalan Tanah — shared layout tokens (main styles in index.php) */
:root {
--bg: #f1f5f9;
--white: #fff;
--border: #e2e8f0;
--text: #0f172a;
--text2: #475569;
--text3: #94a3b8;
--blue: #1d4ed8;
--sidebar-w: 320px;
}
+7
View File
@@ -0,0 +1,7 @@
<?php
$conn = new mysqli("localhost", "root", "", "webgis2");
if ($conn->connect_error) {
die("Koneksi gagal: " . $conn->connect_error);
}
$conn->set_charset("utf8");
?>
+115
View File
@@ -0,0 +1,115 @@
<style>
#confirm-overlay {
display: none; position: fixed; inset: 0; z-index: 99999;
background: rgba(15,23,42,.4); backdrop-filter: blur(2px);
align-items: center; justify-content: center; padding: 20px;
}
#confirm-overlay.show { display: flex; }
#confirm-overlay.confirm-top {
align-items: flex-start; justify-content: center;
padding-top: 72px; background: transparent; backdrop-filter: none;
pointer-events: none;
}
#confirm-box {
background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
width: 100%; max-width: 400px;
box-shadow: 0 16px 48px rgba(15,23,42,.14);
animation: confirmIn .18s ease; overflow: hidden;
}
#confirm-overlay.confirm-top #confirm-box {
pointer-events: all; max-width: 460px;
box-shadow: 0 8px 28px rgba(15,23,42,.12);
}
@keyframes confirmIn { from { opacity:0; transform: translateY(-6px); } to { opacity:1; transform: none; } }
#confirm-box .cf-head {
display: flex; gap: 14px; padding: 20px 22px 0; align-items: flex-start;
}
#confirm-box .cf-icon {
width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
#confirm-box .cf-icon.warn { background: #fffbeb; border: 1px solid #fde68a; }
#confirm-box .cf-icon.danger { background: #fef2f2; border: 1px solid #fecaca; }
#confirm-box .cf-icon.info { background: #eff6ff; border: 1px solid #bfdbfe; }
#confirm-box .cf-icon.move { background: #eff6ff; border: 1px solid #bfdbfe; }
#confirm-box .cf-text { flex: 1; min-width: 0; }
#confirm-box .cf-title { font-size: 15px; font-weight: 700; color: #0f172a; margin-bottom: 4px; }
#confirm-box .cf-msg { font-size: 13px; color: #64748b; line-height: 1.55; }
#confirm-box .cf-actions {
display: flex; gap: 8px; padding: 18px 22px 20px; justify-content: flex-end;
}
#confirm-box .cf-btn {
padding: 9px 16px; border-radius: 8px; font-size: 13px;
font-weight: 600; cursor: pointer; border: 1px solid transparent;
font-family: inherit; transition: background .15s;
}
#confirm-box .cf-cancel { background: #fff; color: #475569; border-color: #e2e8f0; }
#confirm-box .cf-cancel:hover { background: #f8fafc; }
#confirm-box .cf-ok { background: #1d4ed8; color: #fff; }
#confirm-box .cf-ok:hover { background: #1e40af; }
#confirm-box .cf-ok.danger { background: #dc2626; }
#confirm-box .cf-ok.danger:hover { background: #b91c1c; }
</style>
<div id="confirm-overlay" role="dialog" aria-modal="true">
<div id="confirm-box">
<div class="cf-head">
<div class="cf-icon warn" id="cf-icon"></div>
<div class="cf-text">
<div class="cf-title" id="cf-title">Konfirmasi</div>
<div class="cf-msg" id="cf-msg"></div>
</div>
</div>
<div class="cf-actions">
<button type="button" class="cf-btn cf-cancel" id="cf-cancel">Batal</button>
<button type="button" class="cf-btn cf-ok" id="cf-ok">Lanjutkan</button>
</div>
</div>
</div>
<script>
(function(){
var overlay = document.getElementById('confirm-overlay');
var iconEl = document.getElementById('cf-icon');
var titleEl = document.getElementById('cf-title');
var msgEl = document.getElementById('cf-msg');
var btnOk = document.getElementById('cf-ok');
var btnCancel = document.getElementById('cf-cancel');
var resolver = null;
var icons = {
warn: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="2" stroke-linecap="round"><path d="M12 9v4m0 4h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>',
danger: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg>',
move: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round"><path d="M12 2v20M2 12h20"/><circle cx="12" cy="12" r="3"/></svg>'
};
function closeConfirm(result) {
overlay.classList.remove('show', 'confirm-top');
document.body.style.overflow = '';
if (resolver) { var r = resolver; resolver = null; r(result); }
}
btnCancel.addEventListener('click', function(){ closeConfirm(false); });
btnOk.addEventListener('click', function(){ closeConfirm(true); });
overlay.addEventListener('click', function(e){ if (e.target === overlay && !overlay.classList.contains('confirm-top')) closeConfirm(false); });
document.addEventListener('keydown', function(e){
if (!overlay.classList.contains('show')) return;
if (e.key === 'Escape') closeConfirm(false);
});
window.showConfirm = function(opts) {
opts = opts || {};
var danger = !!opts.danger;
var kind = danger ? 'danger' : (opts.icon || 'warn');
iconEl.className = 'cf-icon ' + kind;
iconEl.innerHTML = icons[kind] || icons.warn;
titleEl.textContent = opts.title || 'Konfirmasi';
msgEl.innerHTML = opts.message || 'Apakah Anda yakin?';
btnOk.textContent = opts.confirmText || (danger ? 'Ya, Hapus' : 'Lanjutkan');
btnCancel.textContent = opts.cancelText || 'Batal';
btnOk.classList.toggle('danger', danger);
overlay.classList.toggle('confirm-top', opts.position === 'top');
if (opts.position !== 'top') document.body.style.overflow = 'hidden';
overlay.classList.add('show');
return new Promise(function(resolve){ resolver = resolve; });
};
})();
</script>
+14
View File
@@ -0,0 +1,14 @@
<?php
function getStats($conn) {
$tj = $conn->query("SELECT COUNT(*) c FROM jalan")->fetch_assoc()['c'];
$nas = $conn->query("SELECT COUNT(*) c FROM jalan WHERE status_jalan='Jalan Nasional'")->fetch_assoc()['c'];
$pro = $conn->query("SELECT COUNT(*) c FROM jalan WHERE status_jalan='Jalan Provinsi'")->fetch_assoc()['c'];
$kab = $conn->query("SELECT COUNT(*) c FROM jalan WHERE status_jalan='Jalan Kabupaten'")->fetch_assoc()['c'];
$tp = $conn->query("SELECT COUNT(*) c FROM parsil")->fetch_assoc()['c'];
$shm = $conn->query("SELECT COUNT(*) c FROM parsil WHERE status_kepemilikan='SHM'")->fetch_assoc()['c'];
$hgb = $conn->query("SELECT COUNT(*) c FROM parsil WHERE status_kepemilikan='HGB'")->fetch_assoc()['c'];
$hgu = $conn->query("SELECT COUNT(*) c FROM parsil WHERE status_kepemilikan='HGU'")->fetch_assoc()['c'];
$hp = $conn->query("SELECT COUNT(*) c FROM parsil WHERE status_kepemilikan='HP'")->fetch_assoc()['c'];
$tr = $conn->query("SELECT COUNT(*) c FROM jalan_rusak")->fetch_assoc()['c'];
return compact('tj','nas','pro','kab','tp','shm','hgb','hgu','hp','tr');
}
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
<?php
// config.php - Konfigurasi Database
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'gis_penduduk_miskin');
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
$conn->set_charset('utf8mb4');
if ($conn->connect_error) {
http_response_code(500);
die(json_encode(['error' => 'Database connection failed: ' . $conn->connect_error]));
}
+300
View File
@@ -0,0 +1,300 @@
<?php
session_start();
if(!isset($_SESSION['user'])) { header('Location: login.php'); exit; }
if($_SESSION['role']!=='superadmin') { header('Location: index.php'); exit; }
require_once 'config.php';
$total_penduduk = $conn->query("SELECT COUNT(*) c FROM penduduk_miskin")->fetch_assoc()['c'];
$total_anggota = $conn->query("SELECT SUM(jumlah_anggota) c FROM penduduk_miskin")->fetch_assoc()['c'] ?? 0;
$total_ibadah = $conn->query("SELECT COUNT(*) c FROM rumah_ibadah")->fetch_assoc()['c'];
$terdaftar = $conn->query("SELECT COUNT(*) c FROM penduduk_miskin WHERE rumah_ibadah_id IS NOT NULL")->fetch_assoc()['c'];
$belum = $total_penduduk - $terdaftar;
$per_ibadah = $conn->query("SELECT ri.nama, COUNT(pm.id) AS jumlah FROM rumah_ibadah ri LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id=ri.id GROUP BY ri.id ORDER BY jumlah DESC");
$jenis_ibadah= $conn->query("SELECT jenis, COUNT(*) c FROM rumah_ibadah GROUP BY jenis ORDER BY c DESC");
$jenis_colors= ['Masjid'=>'blue','Gereja'=>'violet','Pura'=>'amber','Vihara'=>'green','Kelenteng'=>'cyan','Lainnya'=>'gray'];
$rows_ibadah=[]; $max=1;
while($r=$per_ibadah->fetch_assoc()){ $rows_ibadah[]=$r; if($r['jumlah']>$max) $max=$r['jumlah']; }
$rows_jenis=[];
while($r=$jenis_ibadah->fetch_assoc()) $rows_jenis[]=$r;
// Pagination logic for latest assistance distributions
$limit = 15;
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$offset = ($page - 1) * $limit;
// Count total records
$total_bantuan = $conn->query("SELECT COUNT(*) c FROM riwayat_bantuan")->fetch_assoc()['c'];
$total_pages = ceil($total_bantuan / $limit);
$recent_bantuan = $conn->query("
SELECT rb.*, pm.nama_kk, ri.nama as nama_ibadah, ri2.nama as ibadah_terdaftar
FROM riwayat_bantuan rb
JOIN penduduk_miskin pm ON rb.penduduk_id = pm.id
LEFT JOIN rumah_ibadah ri ON rb.rumah_ibadah_id = ri.id
LEFT JOIN rumah_ibadah ri2 ON pm.rumah_ibadah_id = ri2.id
ORDER BY rb.tanggal_pemberian DESC, rb.id DESC
LIMIT $limit OFFSET $offset
");
$current = basename($_SERVER['PHP_SELF']);
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dashboard — Pemetaan Kemiskinan Terpadu</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#f0f4f8;--white:#fff;--border:#e2e8f0;--text:#1e293b;--text2:#64748b;--text3:#94a3b8;--blue:#2563eb;--blue-lt:#dbeafe;--green:#16a34a;--green-lt:#dcfce7;--red:#dc2626;--red-lt:#fee2e2;--amber:#d97706;--amber-lt:#fef3c7;--violet:#7c3aed;--violet-lt:#ede9fe;--cyan:#0891b2;--cyan-lt:#cffafe;--sidebar:240px;--radius:12px;--shadow:0 1px 3px rgba(0,0,0,.08),0 4px 16px rgba(0,0,0,.06)}
*{box-sizing:border-box;margin:0;padding:0}body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex}
#sidebar{width:var(--sidebar);min-width:var(--sidebar);background:var(--white);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;left:0;height:100vh;box-shadow:2px 0 8px rgba(0,0,0,.05);z-index:100}
.sb-brand{padding:20px 16px 16px;border-bottom:1px solid var(--border)}.sb-brand-logo{width:40px;height:40px;border-radius:10px;background:linear-gradient(135deg,#7c3aed,#2563eb);display:flex;align-items:center;justify-content:center;font-size:18px;margin-bottom:10px}.sb-brand h1{font-size:13px;font-weight:700;color:var(--text);line-height:1.3}.sb-brand p{font-size:10px;color:var(--text3);margin-top:2px}
.sb-nav{flex:1;overflow-y:auto;padding:10px 8px}.sb-lbl{font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.8px;padding:10px 8px 4px}
.sb-item{display:flex;align-items:center;gap:9px;padding:9px 10px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;color:var(--text2);transition:all .15s;text-decoration:none;margin-bottom:2px;border:1px solid transparent}.sb-item:hover{background:var(--bg);color:var(--text)}.sb-item.active{background:var(--violet-lt);color:var(--violet);border-color:#ddd6fe;font-weight:600}.sb-item .ico{font-size:15px;width:20px;text-align:center}
.sb-footer{padding:10px;border-top:1px solid var(--border)}
.btn-logout{width:100%;padding:8px;background:var(--red-lt);border:1px solid #fca5a5;border-radius:8px;font-size:12px;color:var(--red);cursor:pointer;font-family:inherit;font-weight:600;display:flex;align-items:center;justify-content:center;gap:6px;text-decoration:none;transition:all .15s}.btn-logout:hover{background:var(--red);color:#fff}
#main{margin-left:var(--sidebar);flex:1;display:flex;flex-direction:column;min-height:100vh}
.topbar{
height:56px;background:var(--white);border-bottom:1px solid var(--border);
display:flex;align-items:center;padding:0 28px;position:sticky;top:0;z-index:50;
}
.topbar h2{font-size:16px;font-weight:700;flex:1}
.topbar span{font-size:12px;color:var(--text3)}
.content{padding:28px;flex:1}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px;margin-bottom:24px}
.stat-card{
background:var(--white);border:1px solid var(--border);border-radius:12px;
padding:18px 18px 16px;
}
.stat-label{font-size:11px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px}
.stat-n{font-size:30px;font-weight:700;font-family:'DM Mono',monospace;line-height:1}
.stat-n.blue{color:var(--blue)}.stat-n.green{color:var(--green)}.stat-n.violet{color:var(--violet)}
.stat-n.amber{color:var(--amber)}.stat-n.red{color:var(--red)}
.stat-sub{font-size:12px;color:var(--text2);margin-top:6px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
@media(max-width:900px){.grid2{grid-template-columns:1fr}}
.card{background:var(--white);border:1px solid var(--border);border-radius:12px;overflow:hidden}
.card-head{padding:16px 18px;border-bottom:1px solid var(--border)}
.card-head h3{font-size:14px;font-weight:700}
.card-head p{font-size:12px;color:var(--text3);margin-top:3px}
.card-body{padding:16px 18px}
.bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px;font-size:12px}
.bar-lbl{width:130px;flex-shrink:0;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar-track{flex:1;background:var(--bg);border-radius:4px;height:7px;overflow:hidden}
.bar-fill{height:100%;border-radius:4px;background:var(--blue)}
.bar-val{width:24px;text-align:right;font-weight:700;font-family:'DM Mono',monospace;font-size:12px;flex-shrink:0}
.jenis-row{display:flex;align-items:center;justify-content:space-between;padding:9px 0;border-bottom:1px solid var(--border)}
.jenis-row:last-child{border-bottom:none}
.badge{display:inline-flex;padding:3px 10px;border-radius:6px;font-size:11px;font-weight:600}
.badge-blue{background:var(--blue-lt);color:var(--blue)}
.badge-green{background:var(--green-lt);color:var(--green)}
.badge-violet{background:var(--violet-lt);color:var(--violet)}
.badge-amber{background:var(--amber-lt);color:var(--amber)}
.badge-cyan{background:var(--cyan-lt);color:var(--cyan)}
.badge-gray{background:var(--bg);color:var(--text2)}
.jenis-count{font-size:13px;font-weight:600;font-family:'DM Mono',monospace}
.shortcut-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
@media(max-width:700px){.shortcut-grid{grid-template-columns:1fr}}
.shortcut{
background:var(--white);border:1px solid var(--border);border-radius:12px;
padding:16px 18px;text-decoration:none;display:flex;align-items:flex-start;gap:12px;
transition:border-color .15s,box-shadow .15s;
}
.shortcut:hover{border-color:#93c5fd;box-shadow:0 4px 14px rgba(29,78,216,.08)}
.shortcut-icon{
width:36px;height:36px;border-radius:9px;background:var(--bg);
display:flex;align-items:center;justify-content:center;flex-shrink:0;
border:1px solid var(--border);
}
.shortcut-icon svg{width:18px;height:18px;color:var(--blue)}
.shortcut-lbl{font-size:13px;font-weight:700;color:var(--text)}
.shortcut-sub{font-size:11px;color:var(--text3);margin-top:3px}
.pagination { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 16px; border-top: 1px solid var(--border); }
.pagination a {
padding: 6px 12px; border: 1px solid var(--border); border-radius: 6px;
background: var(--white); color: var(--text2); font-size: 12px; font-weight: 600;
text-decoration: none; transition: all .15s;
}
.pagination a:hover { border-color: var(--blue); color: var(--blue); background: var(--blue-lt); }
.pagination span { font-size: 12px; color: var(--text3); font-weight: 500; }
</style>
</head>
<body>
<aside id="sidebar">
<div class="sb-brand"><div class="sb-brand-logo"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg></div><h1>Kemiskinan Terpadu</h1><p>Panel Superadmin</p></div>
<nav class="sb-nav">
<div class="sb-lbl">Menu</div>
<a href="dashboard.php" class="sb-item <?= $current==='dashboard.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></span> Dashboard</a>
<a href="data_penduduk.php" class="sb-item <?= $current==='data_penduduk.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></svg></span> Data Penduduk</a>
<a href="data_ibadah.php" class="sb-item <?= $current==='data_ibadah.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg></span> Rumah Ibadah</a>
<div class="sb-lbl">Navigasi</div>
<a href="index.php?readonly=1" class="sb-item" target="_blank"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg></span> Lihat Peta GIS</a>
</nav>
<div class="sb-footer">
<?php include __DIR__.'/includes/user-profile.php'; ?>
<a href="logout.php" class="btn-logout" data-confirm-logout>Keluar</a>
</div>
</aside>
<?php include __DIR__.'/includes/confirm-modal.php'; ?>
<main id="main">
<div class="topbar">
<h2>Dashboard</h2>
<span>Diperbarui <?= date('d M Y, H:i') ?> WIB</span>
</div>
<div class="content">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">KK Penduduk Miskin</div>
<div class="stat-n blue"><?= $total_penduduk ?></div>
<div class="stat-sub">Kepala keluarga terdata</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Jiwa</div>
<div class="stat-n green"><?= $total_anggota ?></div>
<div class="stat-sub">Anggota keluarga</div>
</div>
<div class="stat-card">
<div class="stat-label">Rumah Ibadah</div>
<div class="stat-n violet"><?= $total_ibadah ?></div>
<div class="stat-sub">Titik ibadah aktif</div>
</div>
<div class="stat-card">
<div class="stat-label">Terdaftar Radius</div>
<div class="stat-n amber"><?= $terdaftar ?></div>
<div class="stat-sub">KK dalam cakupan</div>
</div>
<div class="stat-card">
<div class="stat-label">Belum Terdaftar</div>
<div class="stat-n red"><?= $belum ?></div>
<div class="stat-sub">Di luar radius ibadah</div>
</div>
</div>
<div class="grid2">
<div class="card">
<div class="card-head">
<h3>Binaan per Rumah Ibadah</h3>
<p>Jumlah KK dalam radius tiap tempat ibadah</p>
</div>
<div class="card-body">
<?php foreach($rows_ibadah as $r): $pct=$max>0?round($r['jumlah']/$max*100):0; ?>
<div class="bar-row">
<div class="bar-lbl" title="<?= htmlspecialchars($r['nama']) ?>"><?= htmlspecialchars($r['nama']) ?></div>
<div class="bar-track"><div class="bar-fill" style="width:<?= $pct ?>%"></div></div>
<div class="bar-val"><?= $r['jumlah'] ?></div>
</div>
<?php endforeach ?>
</div>
</div>
<div class="card">
<div class="card-head">
<h3>Sebaran Jenis Rumah Ibadah</h3>
<p>Komposisi berdasarkan jenis bangunan</p>
</div>
<div class="card-body">
<?php foreach($rows_jenis as $r): $col=$jenis_colors[$r['jenis']]??'gray'; ?>
<div class="jenis-row">
<span class="badge badge-<?= $col ?>"><?= htmlspecialchars($r['jenis']) ?></span>
<span class="jenis-count"><?= $r['c'] ?> tempat</span>
</div>
<?php endforeach ?>
</div>
</div>
</div>
<div class="shortcut-grid" style="margin-bottom: 24px">
<a href="data_penduduk.php" class="shortcut">
<div class="shortcut-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
</div>
<div>
<div class="shortcut-lbl">Data Penduduk</div>
<div class="shortcut-sub"><?= $total_penduduk ?> KK · <?= $total_anggota ?> jiwa</div>
</div>
</a>
<a href="data_ibadah.php" class="shortcut">
<div class="shortcut-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M5 21V7l7-4 7 4v14"/></svg>
</div>
<div>
<div class="shortcut-lbl">Rumah Ibadah</div>
<div class="shortcut-sub"><?= $total_ibadah ?> tempat ibadah</div>
</div>
</a>
</div>
<!-- Card Log Pembagian Bantuan Terbaru -->
<div class="card" style="margin-bottom: 24px">
<div class="card-head">
<h3>Log Pembagian Bantuan Terbaru</h3>
<p>Aktivitas penyaluran bantuan sosial kepada keluarga binaan</p>
</div>
<div class="card-body" style="padding: 0; overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
<thead>
<tr style="background: #f8fafc; border-bottom: 1px solid var(--border)">
<th style="padding: 12px 16px; font-weight: 600; color: var(--text2)">Nama Kepala Keluarga</th>
<th style="padding: 12px 16px; font-weight: 600; color: var(--text2)">Lokasi Tempat Ibadah</th>
<th style="padding: 12px 16px; font-weight: 600; color: var(--text2)">Jenis Bantuan</th>
<th style="padding: 12px 16px; font-weight: 600; color: var(--text2)">Tanggal Diberikan</th>
<th style="padding: 12px 16px; font-weight: 600; color: var(--text2)">Keterangan</th>
</tr>
</thead>
<tbody>
<?php if ($recent_bantuan->num_rows > 0): ?>
<?php while($rb = $recent_bantuan->fetch_assoc()): ?>
<tr style="border-bottom: 1px solid #f1f5f9">
<td style="padding: 12px 16px; font-weight: 600">
<?= htmlspecialchars($rb['nama_kk']) ?>
<?php if ($rb['nama_ibadah']): ?>
<br><span style="font-size: 11px; color: var(--blue); font-weight: normal;"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:3px;margin-top:-2px"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg><?= htmlspecialchars($rb['nama_ibadah']) ?></span>
<?php endif; ?>
</td>
<td style="padding: 12px 16px;">
<?php if ($rb['ibadah_terdaftar']): ?>
<span style="font-weight: 600; display: inline-flex; align-items: center; gap: 4px; color: var(--blue)">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg>
<?= htmlspecialchars($rb['ibadah_terdaftar']) ?>
</span>
<?php else: ?>
<span style="color: var(--red); font-weight: 500;">❌ Belum Terdaftar</span>
<?php endif; ?>
</td>
<td style="padding: 12px 16px;"><span class="badge badge-green" style="font-weight:normal"><?= htmlspecialchars($rb['jenis_bantuan']) ?></span></td>
<td style="padding: 12px 16px; font-family: 'DM Mono', monospace; color: var(--text2)"><?= date('d/m/Y', strtotime($rb['tanggal_pemberian'])) ?></td>
<td style="padding: 12px 16px; color: var(--text2)"><?= htmlspecialchars($rb['keterangan'] ?? '—') ?></td>
</tr>
<?php endwhile; ?>
<?php else: ?>
<tr>
<td colspan="5" style="padding: 24px; text-align: center; color: var(--text3)">Belum ada riwayat pembagian bantuan.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php if ($page > 1): ?>
<a href="?page=<?= $page - 1 ?>"> Seb</a>
<?php endif; ?>
<span>Halaman <?= $page ?> dari <?= $total_pages ?></span>
<?php if ($page < $total_pages): ?>
<a href="?page=<?= $page + 1 ?>">Sel </a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
</main>
</body></html>
+175
View File
@@ -0,0 +1,175 @@
<?php
session_start();
if(!isset($_SESSION['user'])) { header('Location: login.php'); exit; }
if($_SESSION['role']!=='superadmin') { header('Location: index.php'); exit; }
require_once 'config.php';
$per_page = 15;
$page = max(1, intval($_GET['page'] ?? 1));
$cari = trim($_GET['cari'] ?? '');
$offset = ($page - 1) * $per_page;
$where = '';
if($cari !== '') {
$c = $conn->real_escape_string($cari);
$where = "WHERE ri.nama LIKE '%$c%' OR ri.alamat LIKE '%$c%' OR ri.pic LIKE '%$c%' OR ri.jenis LIKE '%$c%'";
}
$total_rows = $conn->query("SELECT COUNT(*) c FROM rumah_ibadah ri $where")->fetch_assoc()['c'];
$total_pages = max(1, ceil($total_rows / $per_page));
$page = min($page, $total_pages);
$offset = ($page - 1) * $per_page;
$data = $conn->query("
SELECT ri.*, COUNT(pm.id) AS jumlah_binaan, SUM(pm.jumlah_anggota) AS total_jiwa
FROM rumah_ibadah ri
LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id = ri.id
$where
GROUP BY ri.id ORDER BY ri.nama ASC
LIMIT $per_page OFFSET $offset
");
$all = $conn->query("SELECT ri.nama,ri.jenis,ri.alamat,ri.pic,ri.no_wa,ri.radius,COUNT(pm.id) AS jumlah_binaan,SUM(pm.jumlah_anggota) AS total_jiwa FROM rumah_ibadah ri LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id=ri.id GROUP BY ri.id ORDER BY ri.nama");
$all_rows=[];
while($r=$all->fetch_assoc()) $all_rows[]=$r;
$jenis_colors=['Masjid'=>'blue','Gereja'=>'violet','Pura'=>'amber','Vihara'=>'green','Kelenteng'=>'cyan','Lainnya'=>'gray'];
$current = basename($_SERVER['PHP_SELF']);
function pag_url($p,$cari){
$q=$cari!==''?'&cari='.urlencode($cari):'';
return '?page='.$p.$q;
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Rumah Ibadah — Pemetaan Kemiskinan Terpadu</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#f0f4f8;--white:#fff;--border:#e2e8f0;--text:#1e293b;--text2:#64748b;--text3:#94a3b8;--blue:#2563eb;--blue-lt:#dbeafe;--green:#16a34a;--green-lt:#dcfce7;--red:#dc2626;--red-lt:#fee2e2;--amber:#d97706;--amber-lt:#fef3c7;--violet:#7c3aed;--violet-lt:#ede9fe;--cyan:#0891b2;--cyan-lt:#cffafe;--sidebar:240px;--radius:12px;--shadow:0 1px 3px rgba(0,0,0,.08),0 4px 16px rgba(0,0,0,.06)}
*{box-sizing:border-box;margin:0;padding:0}body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex}
#sidebar{width:var(--sidebar);min-width:var(--sidebar);background:var(--white);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;left:0;height:100vh;box-shadow:2px 0 8px rgba(0,0,0,.05);z-index:100}
.sb-brand{padding:20px 16px 16px;border-bottom:1px solid var(--border)}.sb-brand-logo{width:40px;height:40px;border-radius:10px;background:linear-gradient(135deg,#7c3aed,#2563eb);display:flex;align-items:center;justify-content:center;font-size:18px;margin-bottom:10px}.sb-brand h1{font-size:13px;font-weight:700;color:var(--text);line-height:1.3}.sb-brand p{font-size:10px;color:var(--text3);margin-top:2px}
.sb-nav{flex:1;overflow-y:auto;padding:10px 8px}.sb-lbl{font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.8px;padding:10px 8px 4px}
.sb-item{display:flex;align-items:center;gap:9px;padding:9px 10px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;color:var(--text2);transition:all .15s;text-decoration:none;margin-bottom:2px;border:1px solid transparent}.sb-item:hover{background:var(--bg);color:var(--text)}.sb-item.active{background:var(--violet-lt);color:var(--violet);border-color:#ddd6fe;font-weight:600}.sb-item .ico{font-size:15px;width:20px;text-align:center}
.sb-footer{padding:10px;border-top:1px solid var(--border)}
.btn-logout{width:100%;padding:8px;background:var(--red-lt);border:1px solid #fca5a5;border-radius:8px;font-size:12px;color:var(--red);cursor:pointer;font-family:inherit;font-weight:600;display:flex;align-items:center;justify-content:center;gap:6px;text-decoration:none;transition:all .15s}.btn-logout:hover{background:var(--red);color:#fff}
#main{margin-left:var(--sidebar);flex:1;display:flex;flex-direction:column;min-height:100vh}
.topbar{height:56px;background:var(--white);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 24px;gap:12px;position:sticky;top:0;z-index:50}.topbar h2{font-size:15px;font-weight:700;flex:1}.topbar span{font-size:12px;color:var(--text3)}
.content{padding:24px;flex:1}
.page-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
.page-title h2{font-size:17px;font-weight:800}.page-title p{font-size:12px;color:var(--text3);margin-top:2px}
.btn-dl{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;background:var(--green);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .15s}.btn-dl:hover{background:#15803d}
.tbl-wrap{background:var(--white);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow)}
.tbl-toolbar{padding:12px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}
.tbl-toolbar form{display:flex;gap:8px;flex:1}.tbl-toolbar input{flex:1;padding:7px 10px;border:1px solid var(--border);border-radius:8px;font-family:inherit;font-size:12.5px;color:var(--text);background:var(--bg);outline:none}.tbl-toolbar input:focus{border-color:var(--blue);background:#fff}
.tbl-toolbar button{padding:7px 14px;background:var(--blue);color:#fff;border:none;border-radius:8px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit}
.tbl-info{font-size:12px;color:var(--text3);white-space:nowrap}
table{width:100%;border-collapse:collapse;font-size:12.5px}
thead th{padding:10px 14px;text-align:left;font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;background:var(--bg);border-bottom:1px solid var(--border);white-space:nowrap}
tbody td{padding:10px 14px;border-bottom:1px solid var(--border);vertical-align:middle}
tbody tr:last-child td{border-bottom:none}tbody tr:hover{background:#fafbfc}
.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:700}
.badge-blue{background:var(--blue-lt);color:var(--blue)}.badge-green{background:var(--green-lt);color:var(--green)}.badge-red{background:var(--red-lt);color:var(--red)}.badge-amber{background:var(--amber-lt);color:var(--amber)}.badge-violet{background:var(--violet-lt);color:var(--violet)}.badge-cyan{background:var(--cyan-lt);color:var(--cyan)}.badge-gray{background:var(--bg);color:var(--text2);border:1px solid var(--border)}
.pagination{display:flex;align-items:center;gap:5px;padding:12px 14px;border-top:1px solid var(--border);flex-wrap:wrap}
.pag-btn{padding:5px 11px;border:1px solid var(--border);border-radius:7px;background:var(--white);font-size:12px;font-weight:600;cursor:pointer;color:var(--text2);text-decoration:none;transition:all .15s;display:inline-flex;align-items:center}
.pag-btn:hover{border-color:var(--blue);color:var(--blue)}.pag-btn.active{background:var(--blue);color:#fff;border-color:var(--blue)}.pag-btn.disabled{opacity:.4;pointer-events:none}
.pag-info{font-size:12px;color:var(--text3);margin-left:auto}
</style>
</head>
<body>
<aside id="sidebar">
<div class="sb-brand"><div class="sb-brand-logo"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg></div><h1>Kemiskinan Terpadu</h1><p>Panel Superadmin</p></div>
<nav class="sb-nav">
<div class="sb-lbl">Menu</div>
<a href="dashboard.php" class="sb-item <?= $current==='dashboard.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></span> Dashboard</a>
<a href="data_penduduk.php" class="sb-item <?= $current==='data_penduduk.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></svg></span> Data Penduduk</a>
<a href="data_ibadah.php" class="sb-item <?= $current==='data_ibadah.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg></span> Rumah Ibadah</a>
<div class="sb-lbl">Navigasi</div>
<a href="index.php?readonly=1" class="sb-item" target="_blank"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg></span> Lihat Peta GIS</a>
</nav>
<div class="sb-footer">
<?php include __DIR__.'/includes/user-profile.php'; ?>
<a href="logout.php" class="btn-logout" data-confirm-logout>Keluar</a>
</div>
</aside>
<?php include __DIR__.'/includes/confirm-modal.php'; ?>
<main id="main">
<div class="topbar"><h2>Rumah Ibadah</h2><span>Total <?= $total_rows ?> tempat ibadah</span></div>
<div class="content">
<div class="page-head">
<div class="page-title">
<h2>Daftar Rumah Ibadah</h2>
<p>Halaman <?= $page ?> dari <?= $total_pages ?> · <?= $total_rows ?> data total</p>
</div>
<button class="btn-dl" onclick="downloadCSV()"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> Download Excel</button>
</div>
<div class="tbl-wrap">
<div class="tbl-toolbar">
<form method="GET" action="">
<div style="position:relative;flex:1;display:flex;align-items:center;">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="position:absolute;left:10px;color:var(--text3)"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" name="cari" value="<?= htmlspecialchars($cari) ?>" placeholder="Cari nama, jenis, PIC, alamat..." style="padding-left:32px;width:100%;">
</div>
<button type="submit">Cari</button>
<?php if($cari): ?><a href="data_ibadah.php" style="padding:7px 12px;border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text2);text-decoration:none">✕ Reset</a><?php endif ?>
</form>
<span class="tbl-info"><?= $per_page*(($page-1)+1)-($per_page-1) ?><?= min($per_page*$page,$total_rows) ?> dari <?= $total_rows ?></span>
</div>
<table>
<thead>
<tr><th>#</th><th>Nama</th><th>Jenis</th><th>Alamat</th><th>PIC</th><th>No. WA</th><th>Radius</th><th>Binaan (KK)</th><th>Total Jiwa</th></tr>
</thead>
<tbody>
<?php $no=($page-1)*$per_page+1; while($r=$data->fetch_assoc()):
$col=$jenis_colors[$r['jenis']]??'gray'; ?>
<tr>
<td style="color:var(--text3);font-family:'DM Mono',monospace"><?= $no++ ?></td>
<td><b><?= htmlspecialchars($r['nama']) ?></b></td>
<td><span class="badge badge-<?= $col ?>"><?= $r['jenis'] ?></span></td>
<td style="color:var(--text2);max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="<?= htmlspecialchars($r['alamat']??'') ?>"><?= htmlspecialchars($r['alamat']??'—') ?></td>
<td><?= htmlspecialchars($r['pic']??'—') ?></td>
<td style="font-family:'DM Mono',monospace;font-size:11.5px"><?= htmlspecialchars($r['no_wa']??'—') ?></td>
<td style="font-family:'DM Mono',monospace"><?= number_format($r['radius']) ?> m</td>
<td style="text-align:center"><span class="badge <?= $r['jumlah_binaan']>0?'badge-green':'badge-gray' ?>"><?= $r['jumlah_binaan'] ?> KK</span></td>
<td style="text-align:center;font-family:'DM Mono',monospace"><?= $r['total_jiwa'] ?? 0 ?></td>
</tr>
<?php endwhile ?>
</tbody>
</table>
<div class="pagination">
<a href="<?= pag_url(1,$cari) ?>" class="pag-btn <?= $page<=1?'disabled':'' ?>">«</a>
<a href="<?= pag_url(max(1,$page-1),$cari) ?>" class="pag-btn <?= $page<=1?'disabled':'' ?>"></a>
<?php $from=max(1,$page-2);$to=min($total_pages,$page+2);
for($i=$from;$i<=$to;$i++): ?>
<a href="<?= pag_url($i,$cari) ?>" class="pag-btn <?= $i===$page?'active':'' ?>"><?= $i ?></a>
<?php endfor ?>
<a href="<?= pag_url(min($total_pages,$page+1),$cari) ?>" class="pag-btn <?= $page>=$total_pages?'disabled':'' ?>"></a>
<a href="<?= pag_url($total_pages,$cari) ?>" class="pag-btn <?= $page>=$total_pages?'disabled':'' ?>">»</a>
<span class="pag-info">Halaman <?= $page ?> / <?= $total_pages ?></span>
</div>
</div>
</div>
</main>
<script>
const allData = <?= json_encode($all_rows) ?>;
function downloadCSV(){
if(!allData.length) return;
const keys=['Nama','Jenis','Alamat','PIC','No WA','Radius (m)','Jumlah Binaan (KK)','Total Jiwa'];
const map =['nama','jenis','alamat','pic','no_wa','radius','jumlah_binaan','total_jiwa'];
const rows=[keys,...allData.map(r=>map.map(k=>'"'+(r[k]??'')+'"'))];
const csv='\uFEFF'+rows.map(r=>r.join(',')).join('\n');
const a=document.createElement('a');
a.href='data:text/csv;charset=utf-8,'+encodeURIComponent(csv);
a.download='rumah_ibadah_<?= date('Ymd') ?>.csv';
a.click();
}
</script>
</body></html>
+207
View File
@@ -0,0 +1,207 @@
<?php
session_start();
if(!isset($_SESSION['user'])) { header('Location: login.php'); exit; }
if($_SESSION['role']!=='superadmin') { header('Location: index.php'); exit; }
require_once 'config.php';
$per_page = 15;
$page = max(1, intval($_GET['page'] ?? 1));
$cari = trim($_GET['cari'] ?? '');
$offset = ($page - 1) * $per_page;
$where = '';
if($cari !== '') {
$c = $conn->real_escape_string($cari);
$where = "WHERE pm.nama_kk LIKE '%$c%' OR pm.alamat LIKE '%$c%' OR ri.nama LIKE '%$c%'";
}
$total_rows = $conn->query("SELECT COUNT(*) c FROM penduduk_miskin pm LEFT JOIN rumah_ibadah ri ON ri.id=pm.rumah_ibadah_id $where")->fetch_assoc()['c'];
$total_pages= max(1, ceil($total_rows / $per_page));
$page = min($page, $total_pages);
$offset = ($page - 1) * $per_page;
$data = $conn->query("
SELECT pm.*, ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah,
(SELECT GROUP_CONCAT(CONCAT(rb.jenis_bantuan, ' (', DATE_FORMAT(rb.tanggal_pemberian, '%d/%m/%Y'), ')') ORDER BY rb.tanggal_pemberian DESC SEPARATOR ', ')
FROM riwayat_bantuan rb WHERE rb.penduduk_id = pm.id) AS riwayat_bantuan_list
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON ri.id = pm.rumah_ibadah_id
$where
ORDER BY pm.id ASC
LIMIT $per_page OFFSET $offset
");
// Untuk download CSV — ambil semua tanpa limit
$all = $conn->query("
SELECT pm.nama_kk, pm.jumlah_anggota, pm.alamat,
(SELECT GROUP_CONCAT(CONCAT(rb.jenis_bantuan, ' (', DATE_FORMAT(rb.tanggal_pemberian, '%d/%m/%Y'), ')') ORDER BY rb.tanggal_pemberian DESC SEPARATOR '; ')
FROM riwayat_bantuan rb WHERE rb.penduduk_id = pm.id) AS riwayat_bantuan_list,
ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah, pm.jarak_ke_ibadah,
IF(pm.rumah_ibadah_id IS NOT NULL,'Terdaftar','Belum Terdaftar') AS status, pm.lat, pm.lng
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON ri.id=pm.rumah_ibadah_id
ORDER BY pm.id ASC
");
$all_rows = [];
while($r=$all->fetch_assoc()) $all_rows[]=$r;
$jenis_colors=['Masjid'=>'blue','Gereja'=>'violet','Pura'=>'amber','Vihara'=>'green','Kelenteng'=>'cyan','Lainnya'=>'gray'];
$current = basename($_SERVER['PHP_SELF']);
function pag_url($p, $cari) {
$q = $cari !== '' ? '&cari='.urlencode($cari) : '';
return '?page='.$p.$q;
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Data Penduduk — Pemetaan Kemiskinan Terpadu</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#f0f4f8;--white:#fff;--border:#e2e8f0;--text:#1e293b;--text2:#64748b;--text3:#94a3b8;--blue:#2563eb;--blue-lt:#dbeafe;--green:#16a34a;--green-lt:#dcfce7;--red:#dc2626;--red-lt:#fee2e2;--amber:#d97706;--amber-lt:#fef3c7;--violet:#7c3aed;--violet-lt:#ede9fe;--cyan:#0891b2;--cyan-lt:#cffafe;--sidebar:240px;--radius:12px;--shadow:0 1px 3px rgba(0,0,0,.08),0 4px 16px rgba(0,0,0,.06)}
*{box-sizing:border-box;margin:0;padding:0}body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex}
#sidebar{width:var(--sidebar);min-width:var(--sidebar);background:var(--white);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;left:0;height:100vh;box-shadow:2px 0 8px rgba(0,0,0,.05);z-index:100}
.sb-brand{padding:20px 16px 16px;border-bottom:1px solid var(--border)}.sb-brand-logo{width:40px;height:40px;border-radius:10px;background:linear-gradient(135deg,#7c3aed,#2563eb);display:flex;align-items:center;justify-content:center;font-size:18px;margin-bottom:10px}.sb-brand h1{font-size:13px;font-weight:700;color:var(--text);line-height:1.3}.sb-brand p{font-size:10px;color:var(--text3);margin-top:2px}
.sb-nav{flex:1;overflow-y:auto;padding:10px 8px}.sb-lbl{font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.8px;padding:10px 8px 4px}
.sb-item{display:flex;align-items:center;gap:9px;padding:9px 10px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;color:var(--text2);transition:all .15s;text-decoration:none;margin-bottom:2px;border:1px solid transparent}.sb-item:hover{background:var(--bg);color:var(--text)}.sb-item.active{background:var(--violet-lt);color:var(--violet);border-color:#ddd6fe;font-weight:600}.sb-item .ico{font-size:15px;width:20px;text-align:center}
.sb-footer{padding:10px;border-top:1px solid var(--border)}
.btn-logout{width:100%;padding:8px;background:var(--red-lt);border:1px solid #fca5a5;border-radius:8px;font-size:12px;color:var(--red);cursor:pointer;font-family:inherit;font-weight:600;display:flex;align-items:center;justify-content:center;gap:6px;text-decoration:none;transition:all .15s}.btn-logout:hover{background:var(--red);color:#fff}
#main{margin-left:var(--sidebar);flex:1;display:flex;flex-direction:column;min-height:100vh}
.topbar{height:56px;background:var(--white);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 24px;gap:12px;position:sticky;top:0;z-index:50}.topbar h2{font-size:15px;font-weight:700;flex:1}.topbar span{font-size:12px;color:var(--text3)}
.content{padding:24px;flex:1}
.page-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
.page-title h2{font-size:17px;font-weight:800}.page-title p{font-size:12px;color:var(--text3);margin-top:2px}
.btn-dl{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;background:var(--green);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .15s}.btn-dl:hover{background:#15803d}
.tbl-wrap{background:var(--white);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow)}
.tbl-toolbar{padding:12px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}
.tbl-toolbar form{display:flex;gap:8px;flex:1}
.tbl-toolbar input{flex:1;padding:7px 10px;border:1px solid var(--border);border-radius:8px;font-family:inherit;font-size:12.5px;color:var(--text);background:var(--bg);outline:none}.tbl-toolbar input:focus{border-color:var(--blue);background:#fff}
.tbl-toolbar button{padding:7px 14px;background:var(--blue);color:#fff;border:none;border-radius:8px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit}
.tbl-info{font-size:12px;color:var(--text3);white-space:nowrap}
table{width:100%;border-collapse:collapse;font-size:12.5px}
thead th{padding:10px 14px;text-align:left;font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;background:var(--bg);border-bottom:1px solid var(--border);white-space:nowrap}
tbody td{padding:10px 14px;border-bottom:1px solid var(--border);vertical-align:middle}
tbody tr:last-child td{border-bottom:none}tbody tr:hover{background:#fafbfc}
.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:700}
.badge-blue{background:var(--blue-lt);color:var(--blue)}.badge-green{background:var(--green-lt);color:var(--green)}.badge-red{background:var(--red-lt);color:var(--red)}.badge-amber{background:var(--amber-lt);color:var(--amber)}.badge-violet{background:var(--violet-lt);color:var(--violet)}.badge-cyan{background:var(--cyan-lt);color:var(--cyan)}.badge-gray{background:var(--bg);color:var(--text2);border:1px solid var(--border)}
.pagination{display:flex;align-items:center;gap:5px;padding:12px 14px;border-top:1px solid var(--border);flex-wrap:wrap}
.pag-btn{padding:5px 11px;border:1px solid var(--border);border-radius:7px;background:var(--white);font-size:12px;font-weight:600;cursor:pointer;color:var(--text2);text-decoration:none;transition:all .15s;font-family:inherit;display:inline-flex;align-items:center}
.pag-btn:hover{border-color:var(--blue);color:var(--blue)}.pag-btn.active{background:var(--blue);color:#fff;border-color:var(--blue)}.pag-btn.disabled{opacity:.4;pointer-events:none}
.pag-info{font-size:12px;color:var(--text3);margin-left:auto}
</style>
</head>
<body>
<aside id="sidebar">
<div class="sb-brand"><div class="sb-brand-logo"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg></div><h1>Kemiskinan Terpadu</h1><p>Panel Superadmin</p></div>
<nav class="sb-nav">
<div class="sb-lbl">Menu</div>
<a href="dashboard.php" class="sb-item <?= $current==='dashboard.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></span> Dashboard</a>
<a href="data_penduduk.php" class="sb-item <?= $current==='data_penduduk.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></svg></span> Data Penduduk</a>
<a href="data_ibadah.php" class="sb-item <?= $current==='data_ibadah.php'?'active':'' ?>"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg></span> Rumah Ibadah</a>
<div class="sb-lbl">Navigasi</div>
<a href="index.php?readonly=1" class="sb-item" target="_blank"><span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg></span> Lihat Peta GIS</a>
</nav>
<div class="sb-footer">
<?php include __DIR__.'/includes/user-profile.php'; ?>
<a href="logout.php" class="btn-logout" data-confirm-logout>Keluar</a>
</div>
</aside>
<?php include __DIR__.'/includes/confirm-modal.php'; ?>
<main id="main">
<div class="topbar"><h2>Data Penduduk Miskin</h2><span>Total <?= $total_rows ?> KK ditemukan</span></div>
<div class="content">
<div class="page-head">
<div class="page-title">
<h2>Daftar Penduduk Miskin</h2>
<p>Menampilkan halaman <?= $page ?> dari <?= $total_pages ?> · <?= $total_rows ?> data total</p>
</div>
<button class="btn-dl" onclick="downloadCSV()"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> Download Excel</button>
</div>
<div class="tbl-wrap">
<div class="tbl-toolbar">
<form method="GET" action="">
<div style="position:relative;flex:1;display:flex;align-items:center;">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="position:absolute;left:10px;color:var(--text3)"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" name="cari" value="<?= htmlspecialchars($cari) ?>" placeholder="Cari nama KK, alamat, rumah ibadah..." style="padding-left:32px;width:100%;">
</div>
<button type="submit">Cari</button>
<?php if($cari): ?><a href="data_penduduk.php" style="padding:7px 12px;border:1px solid var(--border);border-radius:8px;font-size:12px;color:var(--text2);text-decoration:none">✕ Reset</a><?php endif ?>
</form>
<span class="tbl-info"><?= $per_page*(($page-1)+1)-($per_page-1) ?><?= min($per_page*$page,$total_rows) ?> dari <?= $total_rows ?></span>
</div>
<table>
<thead>
<tr>
<th>#</th><th>Nama KK</th><th>Anggota</th><th>Alamat</th>
<th>Riwayat Bantuan</th>
<th>Rumah Ibadah Terdekat</th><th>Jarak</th><th>Status</th><th>Foto</th>
</tr>
</thead>
<tbody>
<?php $no=($page-1)*$per_page+1; while($r=$data->fetch_assoc()): ?>
<tr>
<td style="color:var(--text3);font-family:'DM Mono',monospace"><?= $no++ ?></td>
<td><b><?= htmlspecialchars($r['nama_kk']) ?></b></td>
<td style="font-family:'DM Mono',monospace;text-align:center"><?= $r['jumlah_anggota'] ?></td>
<td style="color:var(--text2);max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="<?= htmlspecialchars($r['alamat']??'') ?>"><?= htmlspecialchars($r['alamat']??'—') ?></td>
<td>
<?php if($r['riwayat_bantuan_list']): ?>
<?php
$items = explode(', ', $r['riwayat_bantuan_list']);
foreach($items as $idx => $item):
if ($idx < 2): ?>
<span class="badge badge-green" style="margin:2px; font-weight:normal"><?= htmlspecialchars($item) ?></span>
<?php else: ?>
<span class="badge badge-gray" style="margin:2px; font-weight:normal" title="<?= htmlspecialchars($r['riwayat_bantuan_list']) ?>">+<?= count($items) - 2 ?> lainnya</span>
<?php break;
endif;
endforeach; ?>
<?php else: ?>
<span style="color:var(--text3)">Belum ada bantuan</span>
<?php endif; ?>
</td>
<td><?php if($r['nama_ibadah']): $col=$jenis_colors[$r['jenis_ibadah']]??'gray'; ?><span class="badge badge-<?= $col ?>"><?= htmlspecialchars($r['nama_ibadah']) ?></span><?php else: ?><span style="color:var(--text3)">—</span><?php endif ?></td>
<td style="font-family:'DM Mono',monospace;color:var(--text2)"><?= $r['jarak_ke_ibadah'] ? number_format($r['jarak_ke_ibadah'],0).' m' : '—' ?></td>
<td><?= $r['rumah_ibadah_id'] ? '<span class="badge badge-green"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="3" style="margin-right:4px"><polyline points="20 6 9 17 4 12"></polyline></svg>Terdaftar</span>' : '<span class="badge badge-red"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="3" style="margin-right:4px"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>Belum</span>' ?></td>
<td><?php if($r['foto_rumah']): ?><a href="<?= htmlspecialchars($r['foto_rumah']) ?>" target="_blank" style="display:flex;align-items:center;color:var(--blue);text-decoration:none" title="Lihat foto"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></a><?php else: ?><span style="color:var(--text3)">—</span><?php endif ?></td>
</tr>
<?php endwhile ?>
</tbody>
</table>
<div class="pagination">
<a href="<?= pag_url(1,$cari) ?>" class="pag-btn <?= $page<=1?'disabled':'' ?>">«</a>
<a href="<?= pag_url(max(1,$page-1),$cari) ?>" class="pag-btn <?= $page<=1?'disabled':'' ?>"></a>
<?php
$from=max(1,$page-2); $to=min($total_pages,$page+2);
for($i=$from;$i<=$to;$i++): ?>
<a href="<?= pag_url($i,$cari) ?>" class="pag-btn <?= $i===$page?'active':'' ?>"><?= $i ?></a>
<?php endfor ?>
<a href="<?= pag_url(min($total_pages,$page+1),$cari) ?>" class="pag-btn <?= $page>=$total_pages?'disabled':'' ?>"></a>
<a href="<?= pag_url($total_pages,$cari) ?>" class="pag-btn <?= $page>=$total_pages?'disabled':'' ?>">»</a>
<span class="pag-info">Halaman <?= $page ?> / <?= $total_pages ?></span>
</div>
</div>
</div>
</main>
<script>
const allData = <?= json_encode($all_rows) ?>;
function downloadCSV(){
if(!allData.length) return;
const keys=['Nama KK','Jumlah Anggota','Alamat','Riwayat Penerimaan Bantuan','Rumah Ibadah','Jenis Ibadah','Jarak (m)','Status','Lat','Lng'];
const map =['nama_kk','jumlah_anggota','alamat','riwayat_bantuan_list','nama_ibadah','jenis_ibadah','jarak_ke_ibadah','status','lat','lng'];
const rows=[keys,...allData.map(r=>map.map(k=>'"'+(r[k]??'')+'"'))];
const csv='\uFEFF'+rows.map(r=>r.join(',')).join('\n');
const a=document.createElement('a');
a.href='data:text/csv;charset=utf-8,'+encodeURIComponent(csv);
a.download='penduduk_miskin_<?= date('Ymd') ?>.csv';
a.click();
}
</script>
</body></html>
+63
View File
@@ -0,0 +1,63 @@
-- ============================================
-- DATABASE: gis_penduduk_miskin
-- GIS Web App - Rumah Ibadah & Penduduk Miskin
-- ============================================
CREATE DATABASE IF NOT EXISTS gis_penduduk_miskin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE gis_penduduk_miskin;
-- Tabel Rumah Ibadah
CREATE TABLE IF NOT EXISTS rumah_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(200) NOT NULL,
jenis ENUM('Masjid','Gereja','Pura','Vihara','Kelenteng','Lainnya') DEFAULT 'Masjid',
alamat TEXT,
pic VARCHAR(150) COMMENT 'Person In Charge',
no_wa VARCHAR(20),
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
radius INT DEFAULT 500 COMMENT 'Radius dalam meter',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Tabel Penduduk Miskin
CREATE TABLE IF NOT EXISTS penduduk_miskin (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_kk VARCHAR(200) NOT NULL COMMENT 'Nama Kepala Keluarga',
jumlah_anggota INT NOT NULL DEFAULT 1 COMMENT 'Jumlah Anggota KK',
alamat TEXT,
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
rumah_ibadah_id INT NULL COMMENT 'Rumah ibadah terdekat (FK)',
jarak_ke_ibadah DOUBLE NULL COMMENT 'Jarak dalam meter ke rumah ibadah terdekat',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
);
-- Index untuk performa query geospasial
CREATE INDEX idx_rumah_ibadah_coords ON rumah_ibadah(lat, lng);
CREATE INDEX idx_penduduk_coords ON penduduk_miskin(lat, lng);
CREATE INDEX idx_penduduk_ibadah ON penduduk_miskin(rumah_ibadah_id);
-- =====================
-- DATA CONTOH (Pontianak)
-- =====================
INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, lat, lng, radius) VALUES
('Masjid Raya Mujahidin', 'Masjid', 'Jl. Ahmad Yani, Pontianak Selatan', 'H. Abdurrahman', '081234567890', -0.0397, 109.3319, 500),
('Masjid Jami Pontianak', 'Masjid', 'Jl. Tanjungpura, Pontianak Kota', 'H. Mukhlis', '081234567891', -0.0263, 109.3413, 500),
('GBI Pontianak', 'Gereja', 'Jl. Gajahmada, Pontianak Kota', 'Pdt. Stevanus', '081234567892', -0.0280, 109.3350, 500),
('Vihara Bodhisattva Karaniya Metta', 'Vihara', 'Jl. Khatulistiwa, Pontianak Utara', 'Bhikkhu Santoso', '081234567893', -0.0120, 109.3250, 500);
INSERT INTO penduduk_miskin (nama_kk, jumlah_anggota, alamat, lat, lng) VALUES
('Budi Santoso', 4, 'Jl. Ahmad Yani Gg. 3, Pontianak', -0.0410, 109.3330),
('Siti Rahayu', 3, 'Jl. Ahmad Yani Gg. 5, Pontianak', -0.0380, 109.3310),
('Muhammad Amin', 5, 'Jl. Tanjungpura No. 12, Pontianak', -0.0255, 109.3420),
('Dewi Lestari', 2, 'Jl. Gajahmada No. 7, Pontianak', -0.0290, 109.3360),
('Hendra Kusuma', 6, 'Jl. Khatulistiwa Gg. 2, Pontianak', -0.0130, 109.3260),
('Yanti Wulandari', 3, 'Jl. Reformasi, Pontianak Utara', -0.0100, 109.3240),
('Agus Prasetyo', 4, 'Jl. Sultan Hamid, Pontianak Barat', -0.0450, 109.3200);
-- Update rumah_ibadah_id dan jarak untuk data contoh (akan dihitung via PHP)
+127
View File
@@ -0,0 +1,127 @@
<style>
#confirm-overlay {
display: none; position: fixed; inset: 0; z-index: 99999;
background: rgba(15,23,42,.4); backdrop-filter: blur(2px);
align-items: center; justify-content: center; padding: 20px;
}
#confirm-overlay.show { display: flex; }
#confirm-overlay.confirm-top {
align-items: flex-start; justify-content: center;
padding-top: 72px; background: transparent; backdrop-filter: none;
pointer-events: none;
}
#confirm-box {
background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
width: 100%; max-width: 400px;
box-shadow: 0 16px 48px rgba(15,23,42,.14);
animation: confirmIn .18s ease; overflow: hidden;
}
#confirm-overlay.confirm-top #confirm-box {
pointer-events: all; max-width: 460px;
box-shadow: 0 8px 28px rgba(15,23,42,.12);
}
@keyframes confirmIn { from { opacity:0; transform: translateY(-6px); } to { opacity:1; transform: none; } }
#confirm-box .cf-head {
display: flex; gap: 14px; padding: 20px 22px 0; align-items: flex-start;
}
#confirm-box .cf-icon {
width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
#confirm-box .cf-icon.warn { background: #fffbeb; border: 1px solid #fde68a; }
#confirm-box .cf-icon.danger { background: #fef2f2; border: 1px solid #fecaca; }
#confirm-box .cf-icon.info { background: #eff6ff; border: 1px solid #bfdbfe; }
#confirm-box .cf-text { flex: 1; min-width: 0; }
#confirm-box .cf-title { font-size: 15px; font-weight: 700; color: #0f172a; margin-bottom: 4px; }
#confirm-box .cf-msg { font-size: 13px; color: #64748b; line-height: 1.55; }
#confirm-box .cf-actions {
display: flex; gap: 8px; padding: 18px 22px 20px; justify-content: flex-end;
}
#confirm-box .cf-btn {
padding: 9px 16px; border-radius: 8px; font-size: 13px;
font-weight: 600; cursor: pointer; border: 1px solid transparent;
font-family: inherit; transition: background .15s;
}
#confirm-box .cf-cancel { background: #fff; color: #475569; border-color: #e2e8f0; }
#confirm-box .cf-cancel:hover { background: #f8fafc; }
#confirm-box .cf-ok { background: #1d4ed8; color: #fff; }
#confirm-box .cf-ok:hover { background: #1e40af; }
#confirm-box .cf-ok.danger { background: #dc2626; }
#confirm-box .cf-ok.danger:hover { background: #b91c1c; }
</style>
<div id="confirm-overlay" role="dialog" aria-modal="true">
<div id="confirm-box">
<div class="cf-head">
<div class="cf-icon warn" id="cf-icon"></div>
<div class="cf-text">
<div class="cf-title" id="cf-title">Konfirmasi</div>
<div class="cf-msg" id="cf-msg"></div>
</div>
</div>
<div class="cf-actions">
<button type="button" class="cf-btn cf-cancel" id="cf-cancel">Batal</button>
<button type="button" class="cf-btn cf-ok" id="cf-ok">Lanjutkan</button>
</div>
</div>
</div>
<script>
(function(){
var overlay = document.getElementById('confirm-overlay');
var iconEl = document.getElementById('cf-icon');
var titleEl = document.getElementById('cf-title');
var msgEl = document.getElementById('cf-msg');
var btnOk = document.getElementById('cf-ok');
var btnCancel = document.getElementById('cf-cancel');
var resolver = null;
var icons = {
warn: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="2" stroke-linecap="round"><path d="M12 9v4m0 4h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>',
danger: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg>',
info: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg>',
move: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round"><path d="M12 2v20M2 12h20"/><circle cx="12" cy="12" r="3"/></svg>'
};
function closeConfirm(result) {
overlay.classList.remove('show', 'confirm-top');
document.body.style.overflow = '';
if (resolver) { var r = resolver; resolver = null; r(result); }
}
btnCancel.addEventListener('click', function(){ closeConfirm(false); });
btnOk.addEventListener('click', function(){ closeConfirm(true); });
overlay.addEventListener('click', function(e){ if (e.target === overlay && !overlay.classList.contains('confirm-top')) closeConfirm(false); });
document.addEventListener('keydown', function(e){
if (!overlay.classList.contains('show')) return;
if (e.key === 'Escape') closeConfirm(false);
});
window.showConfirm = function(opts) {
opts = opts || {};
var danger = !!opts.danger;
var kind = danger ? 'danger' : (opts.icon || 'warn');
iconEl.className = 'cf-icon ' + (kind === 'move' || kind === 'info' ? kind : (danger ? 'danger' : 'warn'));
iconEl.innerHTML = icons[kind] || icons.warn;
titleEl.textContent = opts.title || 'Konfirmasi';
msgEl.innerHTML = opts.message || 'Apakah Anda yakin?';
btnOk.textContent = opts.confirmText || (danger ? 'Ya, Hapus' : 'Lanjutkan');
btnCancel.textContent = opts.cancelText || 'Batal';
btnOk.classList.toggle('danger', danger);
overlay.classList.toggle('confirm-top', opts.position === 'top');
if (opts.position !== 'top') document.body.style.overflow = 'hidden';
overlay.classList.add('show');
return new Promise(function(resolve){ resolver = resolve; });
};
document.querySelectorAll('[data-confirm-logout]').forEach(function(el){
el.addEventListener('click', function(e){
e.preventDefault();
showConfirm({
title: 'Keluar dari akun',
message: 'Sesi Anda akan diakhiri. Lanjutkan keluar?',
confirmText: 'Ya, Keluar',
danger: true
}).then(function(ok){ if (ok) window.location = el.getAttribute('href'); });
});
});
})();
</script>
+46
View File
@@ -0,0 +1,46 @@
<?php
$nama = htmlspecialchars($_SESSION['nama'] ?? 'Pengguna');
$role = $_SESSION['role'] ?? '';
$roleLabel = $role === 'superadmin' ? 'Super Admin' : (in_array($role, ['admin','user'], true) ? 'Admin' : ($role ? ucfirst($role) : 'User'));
$parts = preg_split('/\s+/', trim($_SESSION['nama'] ?? 'U'));
$initials = '';
foreach (array_slice($parts, 0, 2) as $p) { $initials .= mb_strtoupper(mb_substr($p, 0, 1)); }
if ($initials === '') $initials = 'U';
$avatarClass = $role === 'superadmin' ? 'avatar-admin' : 'avatar-user';
?>
<style>
.sb-account {
display: flex; align-items: center; gap: 10px;
padding: 10px; margin-bottom: 8px;
background: var(--bg, #f0f4f8); border-radius: 10px;
border: 1px solid var(--border, #e2e8f0);
}
.sb-avatar {
width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; color: #fff;
letter-spacing: .3px;
}
.sb-avatar.avatar-admin { background: linear-gradient(135deg, #7c3aed, #2563eb); }
.sb-avatar.avatar-user { background: linear-gradient(135deg, #0891b2, #2563eb); }
.sb-account-info { flex: 1; min-width: 0; text-align: left; }
.sb-account-name {
font-size: 13px; font-weight: 700; color: var(--text, #1e293b);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
line-height: 1.3;
}
.sb-account-role {
display: inline-block; margin-top: 3px;
font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 20px;
text-transform: capitalize;
}
.sb-account-role.role-admin { background: #ede9fe; color: #7c3aed; }
.sb-account-role.role-user { background: #dbeafe; color: #2563eb; }
</style>
<div class="sb-account">
<div class="sb-avatar <?= $avatarClass ?>"><?= $initials ?></div>
<div class="sb-account-info">
<div class="sb-account-name"><?= $nama ?></div>
<span class="sb-account-role <?= $role === 'superadmin' ? 'role-admin' : 'role-user' ?>"><?= htmlspecialchars($roleLabel) ?></span>
</div>
</div>
File diff suppressed because it is too large Load Diff
+200
View File
@@ -0,0 +1,200 @@
<?php
session_start();
if (isset($_SESSION['user'])) {
header('Location: ' . ($_SESSION['role'] === 'superadmin' ? 'dashboard.php' : 'index.php'));
exit;
}
$err = '';
$selectedRole = $_POST['role'] ?? 'admin';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require 'config.php';
$u = $conn->real_escape_string($_POST['username'] ?? '');
$role = $_POST['role'] ?? '';
if ($role === 'admin') {
$row = $conn->query("SELECT * FROM users WHERE username='$u' AND role IN ('admin','user')")->fetch_assoc();
} else {
$roleEsc = $conn->real_escape_string($role);
$row = $conn->query("SELECT * FROM users WHERE username='$u' AND role='$roleEsc'")->fetch_assoc();
}
if ($row && password_verify($_POST['password'] ?? '', $row['password'])) {
session_regenerate_id(true);
$_SESSION['user'] = $row['id'];
$_SESSION['nama'] = $row['nama'];
$_SESSION['role'] = $row['role'];
header('Location: ' . ($row['role'] === 'superadmin' ? 'dashboard.php' : 'index.php'));
exit;
}
$err = 'Kredensial tidak valid untuk peran yang dipilih.';
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Masuk — Poverty Map</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--text: #0f172a; --text2: #64748b; --border: #e2e8f0;
--navy: #08184fff; --blue: #08184fff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
display: flex; flex-direction: column; font-family: 'DM Sans', sans-serif;
background: #fff; color: var(--text); overflow: hidden;
}
.login-header {
flex-shrink: 0; height: 56px; display: flex; align-items: center;
padding: 0 28px; border-bottom: 1px solid var(--border); background: #fff;
}
.login-header a {
font-size: 13px; font-weight: 600; color: var(--text2); text-decoration: none;
display: inline-flex; align-items: center; gap: 6px;
}
.login-header a:hover { color: var(--text); }
.login-header span {
margin-left: auto; font-size: 12px; color: var(--text2);
}
.login-body { flex: 1; display: flex; min-height: 0; }
.panel-left {
flex: 1; display: flex; flex-direction: column; justify-content: center;
padding: 40px 56px; background: var(--navy); color: #fff; overflow-y: auto;
}
.pl-label {
font-size: 11px; font-weight: 700; letter-spacing: .08em;
text-transform: uppercase; color: rgba(255,255,255,.5); margin-bottom: 16px;
}
.pl-title { font-size: clamp(26px, 3vw, 36px); font-weight: 700; line-height: 1.25; margin-bottom: 14px; max-width: 400px; }
.pl-desc { font-size: 15px; line-height: 1.65; color: rgba(255,255,255,.7); max-width: 380px; margin-bottom: 32px; }
.pl-list { list-style: none; display: flex; flex-direction: column; gap: 10px; }
.pl-list li {
font-size: 14px; color: rgba(255,255,255,.75); padding-left: 18px; position: relative;
}
.pl-list li::before {
content: ''; position: absolute; left: 0; top: 8px;
width: 5px; height: 5px; border-radius: 50%; background: rgba(255,255,255,.35);
}
.panel-right {
flex: 1; display: flex; align-items: center; justify-content: center;
padding: 40px 48px; background: #f8fafc; overflow-y: auto;
}
.login-form { width: 100%; max-width: 380px; }
.lf-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
.lf-sub { font-size: 14px; color: var(--text2); margin-bottom: 28px; }
.role-picker { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 22px; }
.role-card { cursor: pointer; }
.role-card input { position: absolute; opacity: 0; pointer-events: none; }
.role-card-inner {
display: block; padding: 14px; border: 1px solid var(--border);
border-radius: 10px; background: #fff; transition: border-color .15s, box-shadow .15s;
}
.role-card input:checked + .role-card-inner {
border-color: var(--blue); box-shadow: 0 0 0 3px rgba(30,58,138,.08);
}
.role-card strong { display: block; font-size: 13px; font-weight: 700; margin-bottom: 2px; }
.role-card small { font-size: 11px; color: var(--text2); line-height: 1.4; }
.field-label {
display: block; font-size: 12px; font-weight: 600;
color: var(--text2); margin-bottom: 6px;
}
.field-input {
width: 100%; padding: 11px 13px; border: 1px solid var(--border);
border-radius: 8px; font-size: 14px; font-family: inherit;
margin-bottom: 14px; background: #fff; transition: border-color .15s, box-shadow .15s;
}
.field-input:focus {
outline: none; border-color: var(--blue);
box-shadow: 0 0 0 3px rgba(30,58,138,.08);
}
.btn-submit {
width: 100%; padding: 12px; background: var(--blue); color: #fff;
border: none; border-radius: 8px; font-size: 14px; font-weight: 600;
cursor: pointer; font-family: inherit; margin-top: 6px;
}
.btn-submit:hover { background: #1e3a8a; }
.err {
background: #fef2f2; color: #b91c1c; padding: 11px 13px;
border-radius: 8px; font-size: 13px; margin-bottom: 16px; border: 1px solid #fecaca;
}
.lf-foot { margin-top: 22px; font-size: 12px; color: var(--text2); text-align: center; }
@media (max-width: 860px) {
body { overflow: auto; height: auto; min-height: 100vh; }
.login-body { flex-direction: column; }
.panel-left { padding: 32px 28px; }
.panel-right { padding: 32px 24px 48px; }
.pl-list { display: none; }
}
</style>
</head>
<body>
<header class="login-header">
<a href="../../index.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Portal WebGIS
</a>
<span>Poverty Map · Pontianak</span>
</header>
<div class="login-body">
<div class="panel-left">
<div class="pl-label">Sistem Informasi Geografis</div>
<h1 class="pl-title">Poverty Map</h1>
<p class="pl-desc">Pemetaan rumah ibadah dan penduduk miskin untuk perencanaan program binaan berbasis lokasi.</p>
<ul class="pl-list">
<li>Peta interaktif rumah ibadah dan kepala keluarga</li>
<li>Analisis radius cakupan binaan</li>
<li>Manajemen data untuk super admin</li>
</ul>
</div>
<div class="panel-right">
<div class="login-form">
<h2 class="lf-title">Masuk ke sistem</h2>
<p class="lf-sub">Pilih peran, lalu masukkan username dan password.</p>
<?php if ($err): ?><div class="err"><?= htmlspecialchars($err) ?></div><?php endif ?>
<form method="POST" autocomplete="on">
<span class="field-label">Peran akses</span>
<div class="role-picker">
<label class="role-card">
<input type="radio" name="role" value="superadmin" <?= $selectedRole === 'superadmin' ? 'checked' : '' ?> required>
<span class="role-card-inner">
<strong>Super Admin</strong>
<small>Dashboard dan kelola data</small>
</span>
</label>
<label class="role-card">
<input type="radio" name="role" value="admin" <?= $selectedRole === 'admin' ? 'checked' : '' ?>>
<span class="role-card-inner">
<strong>Admin</strong>
<small>Peta input dan edit lapangan</small>
</span>
</label>
</div>
<label class="field-label" for="username">Username</label>
<input class="field-input" type="text" id="username" name="username" required placeholder="Username" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>">
<label class="field-label" for="password">Password</label>
<input class="field-input" type="password" id="password" name="password" required placeholder="Password">
<button type="submit" class="btn-submit">Masuk</button>
</form>
<p class="lf-foot">Dinas Sosial · Kabupaten/Kota Pontianak</p>
</div>
</div>
</div>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header('Location: login.php');
exit;
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+47
View File
@@ -0,0 +1,47 @@
<?php
include 'db.php';
if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])) {
$id = (int) $_GET['id'];
$conn->query("DELETE FROM tempat WHERE id = $id");
header('Location: index.php');
exit;
}
if (isset($_GET['action']) && $_GET['action'] === 'edit' &&
isset($_GET['id'], $_GET['nama'], $_GET['nomor'], $_GET['status'])) {
$id = (int) $_GET['id'];
$nama = $conn->real_escape_string($_GET['nama']);
$nomor = $conn->real_escape_string($_GET['nomor']);
$status = $conn->real_escape_string($_GET['status']);
$conn->query("UPDATE tempat SET nama_tempat='$nama', nomor_tempat='$nomor', status='$status' WHERE id=$id");
header('Location: index.php');
exit;
}
if (isset($_GET['action']) && $_GET['action'] === 'update_pos' &&
isset($_GET['id'], $_GET['lat'], $_GET['lng'])) {
$id = (int) $_GET['id'];
$lat = (float) $_GET['lat'];
$lng = (float) $_GET['lng'];
$result = $conn->query("UPDATE tempat SET latitude='$lat', longitude='$lng' WHERE id=$id");
header('Content-Type: text/plain');
echo ($result) ? 'ok' : 'error: ' . $conn->error;
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) {
$nama = $conn->real_escape_string($_POST['nama']);
$nomor = $conn->real_escape_string($_POST['nomor']);
$status = $conn->real_escape_string($_POST['status']);
$lat = (float) $_POST['lat'];
$lng = (float) $_POST['lng'];
$sql = "INSERT INTO tempat (nama_tempat, nomor_tempat, status, latitude, longitude)
VALUES ('$nama','$nomor','$status','$lat','$lng')";
if ($conn->query($sql)) {
header('Location: index.php?saved=1');
exit;
}
header('Location: tambah.php?err=' . urlencode($conn->error));
exit;
}
+40
View File
@@ -0,0 +1,40 @@
function initBaseLayers(map) {
var baseMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap', maxZoom: 19
});
var baseSat = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '&copy; Esri', maxZoom: 19
});
baseMap.addTo(map);
document.getElementById('base-map').addEventListener('click', function() {
if (map.hasLayer(baseSat)) map.removeLayer(baseSat);
if (!map.hasLayer(baseMap)) baseMap.addTo(map);
this.classList.add('active');
document.getElementById('base-sat').classList.remove('active');
});
document.getElementById('base-sat').addEventListener('click', function() {
if (map.hasLayer(baseMap)) map.removeLayer(baseMap);
if (!map.hasLayer(baseSat)) baseSat.addTo(map);
this.classList.add('active');
document.getElementById('base-map').classList.remove('active');
});
}
function makeSpbuIcon(color, size) {
size = size || 18;
var h = size / 2;
return L.divIcon({
className: '',
html: '<div style="width:'+size+'px;height:'+size+'px;border-radius:50%;background:'+color+';border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.35);"></div>',
iconSize: [size, size], iconAnchor: [h, h], popupAnchor: [0, -12]
});
}
function showToast(msg) {
var el = document.getElementById('toast');
if (!el) return;
el.textContent = msg;
el.classList.add('show');
setTimeout(function() { el.classList.remove('show'); }, 3000);
}
+234
View File
@@ -0,0 +1,234 @@
:root {
--bg: #f1f5f9; --white: #fff; --border: #e2e8f0;
--text: #0f172a; --text2: #475569; --text3: #94a3b8;
--blue: #1d4ed8; --blue-lt: #eff6ff;
--green: #15803d; --green-lt: #f0fdf4;
--red: #b91c1c; --red-lt: #fef2f2;
--sidebar-w: 320px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; font-family: 'DM Sans', sans-serif; color: var(--text); background: var(--bg); overflow: hidden; }
body { display: flex; }
#sidebar {
width: var(--sidebar-w); min-width: var(--sidebar-w);
background: var(--white); border-right: 1px solid var(--border);
display: flex; flex-direction: column; z-index: 200;
}
.sb-head {
padding: 18px 16px 14px; border-bottom: 1px solid var(--border);
background: var(--white); color: var(--text);
}
.sb-head-top { display: flex; align-items: center; gap: 11px; margin-bottom: 12px; }
.sb-logo {
width: 38px; height: 38px; border-radius: 10px;
background: linear-gradient(135deg, var(--blue), #60a5fa);
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
box-shadow: 0 2px 8px rgba(29,78,216,.25);
}
.sb-logo svg { width: 18px; height: 18px; fill: #fff; }
.sb-title h1 { font-size: 14px; font-weight: 700; color: var(--text); }
.sb-title p { font-size: 10.5px; color: var(--text3); margin-top: 2px; }
.sb-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.sb-stat {
background: var(--bg); border: 1px solid var(--border);
border-radius: 8px; padding: 8px 6px; text-align: center;
}
.sb-stat b { display: block; font-size: 16px; font-weight: 800; font-family: 'DM Mono', monospace; line-height: 1; }
.sb-stat span { font-size: 8.5px; color: var(--text3); margin-top: 4px; display: block; text-transform: uppercase; letter-spacing: .3px; font-weight: 600; }
.sb-stat.total b { color: var(--blue); }
.sb-stat.open b { color: var(--green); }
.sb-stat.closed b{ color: var(--red); }
.sb-section-lbl {
font-size: 9.5px; font-weight: 700; color: var(--text3);
text-transform: uppercase; letter-spacing: .8px; padding: 10px 8px 4px;
}
.sb-item {
display: flex; align-items: center; gap: 9px; padding: 8px 10px;
border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500;
color: var(--text2); transition: all .15s; border: 1px solid transparent; text-decoration: none;
}
.sb-item:hover { background: var(--bg); color: var(--text); }
.sb-item.active { background: var(--blue-lt); color: var(--blue); border-color: #bfdbfe; font-weight: 600; }
.sb-item .ico { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.sb-body { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
.sb-search { padding: 12px; border-bottom: 1px solid var(--border); }
.sb-search input {
width: 100%; padding: 9px 12px 9px 34px;
border: 1px solid var(--border); border-radius: 8px; font-size: 13px;
outline: none; font-family: inherit; background: var(--bg);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.099zM6.5 12a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11z'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: 11px center;
}
.sb-search input:focus { border-color: var(--blue); background-color: #fff; }
.sb-filter { display: flex; padding: 0 12px 10px; gap: 5px; }
.sb-filter button {
flex: 1; padding: 6px 4px; font-size: 11px; font-weight: 600;
border: 1px solid var(--border); border-radius: 6px; background: var(--white);
color: var(--text2); cursor: pointer; font-family: inherit; transition: all .15s;
}
.sb-filter button.active { background: var(--blue); color: #fff; border-color: var(--blue); }
.sb-list-head {
padding: 0 14px 8px; display: flex; justify-content: space-between; align-items: center;
font-size: 11px; color: var(--text3); font-weight: 600; text-transform: uppercase; letter-spacing: .4px;
}
.sb-list { flex: 1; overflow-y: auto; }
.sb-list::-webkit-scrollbar { width: 4px; }
.sb-list::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
.spbu-item {
padding: 11px 14px; border-bottom: 1px solid #f1f5f9; cursor: pointer;
display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: start;
transition: background .12s;
}
.spbu-item:hover { background: #f8fafc; }
.spbu-item.active { background: var(--blue-lt); border-left: 3px solid var(--blue); padding-left: 11px; }
.spbu-dot {
width: 10px; height: 10px; border-radius: 50%; margin-top: 4px;
border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.1);
}
.spbu-nama { font-size: 13px; font-weight: 600; line-height: 1.3; }
.spbu-meta { font-size: 11px; color: var(--text3); margin-top: 3px; font-family: 'DM Mono', monospace; }
.spbu-badge { font-size: 10px; font-weight: 700; padding: 3px 7px; border-radius: 5px; white-space: nowrap; }
.badge-green { background: var(--green-lt); color: var(--green); }
.badge-red { background: var(--red-lt); color: var(--red); }
.no-result { padding: 32px 16px; text-align: center; color: var(--text3); font-size: 13px; }
.sb-foot {
padding: 10px 14px; border-top: 1px solid var(--border);
font-size: 11px; color: var(--text3); display: flex; justify-content: space-between;
}
.sb-foot b { color: var(--text2); }
/* Add form in sidebar */
.form-panel { padding: 16px; flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 0; }
.form-panel h3 { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
.form-panel .form-desc { font-size: 12px; color: var(--text2); line-height: 1.5; margin-bottom: 16px; }
.form-group { margin-bottom: 14px; }
.form-group label {
display: block; font-size: 11px; font-weight: 700; color: var(--text3);
text-transform: uppercase; letter-spacing: .4px; margin-bottom: 5px;
}
.form-group input, .form-group select {
width: 100%; padding: 10px 12px; border: 1px solid var(--border);
border-radius: 8px; font-size: 14px; font-family: inherit; outline: none; background: #fff;
transition: border-color .15s, box-shadow .15s;
}
.form-group input:focus, .form-group select:focus {
border-color: var(--blue); box-shadow: 0 0 0 3px rgba(29,78,216,.1);
}
.form-group input[readonly] { background: var(--bg); color: var(--text2); font-family: 'DM Mono', monospace; font-size: 12px; }
.coord-box {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
}
.coord-box input { text-align: center; }
.form-alert {
padding: 10px 12px; border-radius: 8px; font-size: 12px; margin-bottom: 14px;
background: var(--red-lt); color: var(--red); border: 1px solid #fecaca;
}
.form-hint {
font-size: 11px; color: var(--text3); padding: 10px 12px;
background: var(--bg); border-radius: 8px; border: 1px solid var(--border);
line-height: 1.5; margin-top: auto;
}
.form-hint.active { border-color: #93c5fd; background: var(--blue-lt); color: var(--blue); }
.form-actions { display: flex; gap: 8px; margin-top: 16px; }
.btn-primary {
flex: 1; padding: 11px; background: var(--blue); color: #fff; border: none;
border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit;
}
.btn-primary:hover { background: #1e40af; }
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
.btn-secondary {
padding: 11px 14px; background: #f1f5f9; color: var(--text2); border: 1px solid var(--border);
border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit;
}
#map-area { flex: 1; position: relative; min-width: 0; display: flex; flex-direction: column; height: 100%; }
#map { flex: 1; min-height: 0; width: 100%; position: relative; }
#map-controls {
position: absolute; top: 14px; right: 14px; z-index: 800;
background: var(--white); border: 1px solid var(--border);
border-radius: 10px; box-shadow: 0 2px 12px rgba(0,0,0,.1);
width: 190px; overflow: hidden; font-size: 12px;
}
.mc-section { padding: 10px 12px; border-bottom: 1px solid var(--border); }
.mc-section:last-child { border-bottom: none; }
.mc-label {
font-size: 10px; font-weight: 700; color: var(--text3);
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px;
}
.mc-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; cursor: pointer; color: var(--text2); }
.mc-row:last-child { margin-bottom: 0; }
.mc-row input { accent-color: var(--blue); }
.mc-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.mc-base { display: flex; gap: 5px; }
.mc-base-btn {
flex: 1; padding: 7px 4px; border: 1px solid var(--border); border-radius: 6px;
background: var(--white); font-size: 11px; font-weight: 600; color: var(--text2);
cursor: pointer; font-family: inherit; transition: all .15s;
}
.mc-base-btn.active { background: var(--blue); color: #fff; border-color: var(--blue); }
#map-hint {
position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%);
z-index: 800; background: rgba(15,23,42,.88); color: #fff;
padding: 8px 16px; border-radius: 8px; font-size: 12px; pointer-events: none;
}
#toast {
position: fixed; bottom: 20px; right: 20px; z-index: 9000;
background: #0f172a; color: #fff; padding: 11px 16px; border-radius: 8px;
font-size: 13px; display: none; box-shadow: 0 4px 16px rgba(0,0,0,.2);
}
#toast.show { display: block; }
#edit-modal {
display: none; position: fixed; inset: 0; z-index: 9999;
background: rgba(15,23,42,.4); align-items: center; justify-content: center; padding: 20px;
}
#edit-modal.active { display: flex; }
#edit-box {
background: #fff; border-radius: 12px; padding: 24px;
width: 100%; max-width: 380px; box-shadow: 0 16px 48px rgba(0,0,0,.18);
}
#edit-box h3 { font-size: 16px; font-weight: 700; margin-bottom: 16px; }
#edit-box label {
display: block; font-size: 11px; font-weight: 700; color: var(--text3);
text-transform: uppercase; letter-spacing: .4px; margin: 12px 0 4px;
}
#edit-box input, #edit-box select {
width: 100%; padding: 9px 12px; border: 1px solid var(--border);
border-radius: 8px; font-size: 14px; font-family: inherit; outline: none;
}
#edit-box input:focus, #edit-box select:focus { border-color: var(--blue); }
.edit-btns { display: flex; gap: 8px; margin-top: 20px; }
.edit-btns button {
flex: 1; padding: 10px; border-radius: 8px; font-size: 13px;
font-weight: 600; cursor: pointer; font-family: inherit; border: none;
}
.btn-save { background: var(--blue); color: #fff; }
.btn-cancel { background: #f1f5f9; color: var(--text2); }
.popup-wrap { font-family: 'DM Sans', sans-serif; min-width: 190px; }
.popup-title { font-size: 14px; font-weight: 700; margin-bottom: 6px; }
.popup-row { font-size: 12px; color: var(--text2); margin-bottom: 3px; }
.popup-coord { font-size: 11px; color: var(--text3); font-family: 'DM Mono', monospace; margin: 4px 0 8px; }
.popup-badge { display: inline-block; padding: 2px 8px; border-radius: 5px; font-size: 10px; font-weight: 700; }
.popup-actions { display: flex; gap: 6px; margin-top: 10px; }
.btn-edit, .btn-delete {
flex: 1; border: none; padding: 7px; border-radius: 6px;
font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit;
}
.btn-edit { background: var(--blue); color: #fff; }
.btn-delete { background: var(--red); color: #fff; }
.btn-yes, .btn-no {
border: none; padding: 7px 16px; border-radius: 6px;
font-size: 12px; font-weight: 600; cursor: pointer; margin: 3px; font-family: inherit;
}
.btn-yes { background: var(--green); color: #fff; }
.btn-no { background: #e2e8f0; color: var(--text2); }
+7
View File
@@ -0,0 +1,7 @@
<?php
$conn = new mysqli("localhost", "root", "", "webgis");
if ($conn->connect_error) {
die("Koneksi gagal: " . $conn->connect_error);
}
?>
+115
View File
@@ -0,0 +1,115 @@
<style>
#confirm-overlay {
display: none; position: fixed; inset: 0; z-index: 99999;
background: rgba(15,23,42,.4); backdrop-filter: blur(2px);
align-items: center; justify-content: center; padding: 20px;
}
#confirm-overlay.show { display: flex; }
#confirm-overlay.confirm-top {
align-items: flex-start; justify-content: center;
padding-top: 72px; background: transparent; backdrop-filter: none;
pointer-events: none;
}
#confirm-box {
background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
width: 100%; max-width: 400px;
box-shadow: 0 16px 48px rgba(15,23,42,.14);
animation: confirmIn .18s ease; overflow: hidden;
}
#confirm-overlay.confirm-top #confirm-box {
pointer-events: all; max-width: 460px;
box-shadow: 0 8px 28px rgba(15,23,42,.12);
}
@keyframes confirmIn { from { opacity:0; transform: translateY(-6px); } to { opacity:1; transform: none; } }
#confirm-box .cf-head {
display: flex; gap: 14px; padding: 20px 22px 0; align-items: flex-start;
}
#confirm-box .cf-icon {
width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
#confirm-box .cf-icon.warn { background: #fffbeb; border: 1px solid #fde68a; }
#confirm-box .cf-icon.danger { background: #fef2f2; border: 1px solid #fecaca; }
#confirm-box .cf-icon.info { background: #eff6ff; border: 1px solid #bfdbfe; }
#confirm-box .cf-icon.move { background: #eff6ff; border: 1px solid #bfdbfe; }
#confirm-box .cf-text { flex: 1; min-width: 0; }
#confirm-box .cf-title { font-size: 15px; font-weight: 700; color: #0f172a; margin-bottom: 4px; }
#confirm-box .cf-msg { font-size: 13px; color: #64748b; line-height: 1.55; }
#confirm-box .cf-actions {
display: flex; gap: 8px; padding: 18px 22px 20px; justify-content: flex-end;
}
#confirm-box .cf-btn {
padding: 9px 16px; border-radius: 8px; font-size: 13px;
font-weight: 600; cursor: pointer; border: 1px solid transparent;
font-family: inherit; transition: background .15s;
}
#confirm-box .cf-cancel { background: #fff; color: #475569; border-color: #e2e8f0; }
#confirm-box .cf-cancel:hover { background: #f8fafc; }
#confirm-box .cf-ok { background: #1d4ed8; color: #fff; }
#confirm-box .cf-ok:hover { background: #1e40af; }
#confirm-box .cf-ok.danger { background: #dc2626; }
#confirm-box .cf-ok.danger:hover { background: #b91c1c; }
</style>
<div id="confirm-overlay" role="dialog" aria-modal="true">
<div id="confirm-box">
<div class="cf-head">
<div class="cf-icon warn" id="cf-icon"></div>
<div class="cf-text">
<div class="cf-title" id="cf-title">Konfirmasi</div>
<div class="cf-msg" id="cf-msg"></div>
</div>
</div>
<div class="cf-actions">
<button type="button" class="cf-btn cf-cancel" id="cf-cancel">Batal</button>
<button type="button" class="cf-btn cf-ok" id="cf-ok">Lanjutkan</button>
</div>
</div>
</div>
<script>
(function(){
var overlay = document.getElementById('confirm-overlay');
var iconEl = document.getElementById('cf-icon');
var titleEl = document.getElementById('cf-title');
var msgEl = document.getElementById('cf-msg');
var btnOk = document.getElementById('cf-ok');
var btnCancel = document.getElementById('cf-cancel');
var resolver = null;
var icons = {
warn: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="2" stroke-linecap="round"><path d="M12 9v4m0 4h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>',
danger: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></svg>',
move: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round"><path d="M12 2v20M2 12h20"/><circle cx="12" cy="12" r="3"/></svg>'
};
function closeConfirm(result) {
overlay.classList.remove('show', 'confirm-top');
document.body.style.overflow = '';
if (resolver) { var r = resolver; resolver = null; r(result); }
}
btnCancel.addEventListener('click', function(){ closeConfirm(false); });
btnOk.addEventListener('click', function(){ closeConfirm(true); });
overlay.addEventListener('click', function(e){ if (e.target === overlay && !overlay.classList.contains('confirm-top')) closeConfirm(false); });
document.addEventListener('keydown', function(e){
if (!overlay.classList.contains('show')) return;
if (e.key === 'Escape') closeConfirm(false);
});
window.showConfirm = function(opts) {
opts = opts || {};
var danger = !!opts.danger;
var kind = danger ? 'danger' : (opts.icon || 'warn');
iconEl.className = 'cf-icon ' + kind;
iconEl.innerHTML = icons[kind] || icons.warn;
titleEl.textContent = opts.title || 'Konfirmasi';
msgEl.innerHTML = opts.message || 'Apakah Anda yakin?';
btnOk.textContent = opts.confirmText || (danger ? 'Ya, Hapus' : 'Lanjutkan');
btnCancel.textContent = opts.cancelText || 'Batal';
btnOk.classList.toggle('danger', danger);
overlay.classList.toggle('confirm-top', opts.position === 'top');
if (opts.position !== 'top') document.body.style.overflow = 'hidden';
overlay.classList.add('show');
return new Promise(function(resolve){ resolver = resolve; });
};
})();
</script>
+14
View File
@@ -0,0 +1,14 @@
<div id="map-controls" style="top: 70px;">
<div class="mc-section">
<div class="mc-label">Layer SPBU</div>
<label class="mc-row"><input type="checkbox" id="ly-24" checked><span class="mc-dot" style="background:#22c55e"></span> Buka 24 Jam</label>
<label class="mc-row"><input type="checkbox" id="ly-tdk" checked><span class="mc-dot" style="background:#ef4444"></span> Tidak 24 Jam</label>
</div>
<div class="mc-section">
<div class="mc-label">Tampilan Peta</div>
<div class="mc-base">
<button class="mc-base-btn active" id="base-map" type="button">Peta</button>
<button class="mc-base-btn" id="base-sat" type="button">Satelit</button>
</div>
</div>
</div>
+44
View File
@@ -0,0 +1,44 @@
<?php
/** @var array $stats @var string $activePage 'index'|'tambah' */
$activePage = $activePage ?? 'index';
?>
<aside id="sidebar">
<div class="sb-head">
<a href="index.php" style="display:flex; align-items:center; gap:10px; text-decoration:none; color:inherit">
<div class="sb-logo" style="width:38px; height:38px; border-radius:10px; background:linear-gradient(135deg, #1d4ed8, #60a5fa); display:flex; align-items:center; justify-content:center; flex-shrink:0; box-shadow:0 2px 8px rgba(29,78,216,.25)">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2" style="flex-shrink:0"><path d="M6 3h12l-1 7H7L6 3zm0 9h12v9H6v-9zm3 2v5h6v-5H9z" fill="currentColor"/></svg>
</div>
<div class="sb-title"><h1>Geografis SPBU</h1><p>Sistem Informasi Geografis</p></div>
</a>
<!-- 2x2 Stats Grid -->
<div class="sb-stats" style="display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px">
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#1d4ed8" id="s-total"><?= $stats['total'] ?></div>
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Total SPBU</div>
</div>
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#16a34a" id="s-jam24"><?= $stats['jam24'] ?></div>
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">24 Jam</div>
</div>
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#dc2626" id="s-tidak24"><?= $stats['tidak24'] ?></div>
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Tdk 24 Jam</div>
</div>
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#ca8a04" id="s-pct"><?= $stats['pct24'] ?>%</div>
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Ratio 24h</div>
</div>
</div>
</div>
<!-- Navigation Sections -->
<div class="sb-nav-container" style="padding:8px; display:flex; flex-direction:column; gap:2px; border-bottom:1px solid #e2e8f0">
<div class="sb-section-lbl" style="font-size:9.5px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:.8px; padding:10px 8px 4px;">Aksi Admin</div>
<a href="tambah.php" class="sb-item <?= $activePage==='tambah'?'active':'' ?>" style="display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px; cursor:pointer; font-size:13px; font-weight:500; color:#475569; text-decoration:none; transition:all .15s; border:1px solid transparent;">
<span class="ico" style="font-size:16px; width:20px; text-align:center; flex-shrink:0; display:flex; align-items:center; justify-content:center">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</span>
<span>Tambah SPBU Baru</span>
</a>
</div>
+8
View File
@@ -0,0 +1,8 @@
<?php
function spbu_stats($conn) {
$total = (int) $conn->query("SELECT COUNT(*) c FROM tempat")->fetch_assoc()['c'];
$jam24 = (int) $conn->query("SELECT COUNT(*) c FROM tempat WHERE status='24 Jam'")->fetch_assoc()['c'];
$tidak24 = $total - $jam24;
$pct24 = $total > 0 ? round($jam24 / $total * 100) : 0;
return compact('total', 'jam24', 'tidak24', 'pct24');
}
+283
View File
@@ -0,0 +1,283 @@
<?php
if (isset($_GET['action'])) { include 'api.php'; exit; }
include 'db.php';
require_once 'includes/stats.php';
$stats = spbu_stats($conn);
$activePage = 'index';
$saved = isset($_GET['saved']);
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sistem Informasi Geografis SPBU — Pontianak</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="stylesheet" href="assets/spbu.css?v=1.2"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</head>
<body>
<?php include 'includes/sidebar.php'; ?>
<div class="sb-body">
<div class="sb-search"><input type="text" id="search-input" placeholder="Cari nama atau nomor SPBU..."></div>
<div class="sb-filter">
<button class="active" data-filter="semua">Semua (<?= $stats['total'] ?>)</button>
<button data-filter="24 Jam">24 Jam (<?= $stats['jam24'] ?>)</button>
<button data-filter="Tidak 24 Jam">Lainnya (<?= $stats['tidak24'] ?>)</button>
</div>
<div class="sb-list-head">
<span>Daftar SPBU</span>
<span id="list-count"><?= $stats['total'] ?> lokasi</span>
</div>
<div class="sb-list" id="spbu-list"></div>
</div>
<div class="sb-foot" style="flex-direction:column; gap:10px">
<div style="display:flex; justify-content:space-between; width:100%">
<span>Operasional 24 jam: <b><?= $stats['pct24'] ?>%</b></span>
<span><?= date('d M Y') ?></span>
</div>
<a href="../index.php" class="btn-portal" style="display:flex; align-items:center; justify-content:center; gap:6px; padding:10px; background:#eff6ff; color:#1d4ed8; border:1px solid #bfdbfe; border-radius:8px; font-weight:600; text-decoration:none; font-size:12px; transition:all 0.15s; width:100%">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Kembali ke Portal
</a>
</div>
</aside>
<div id="map-area">
<!-- Topbar -->
<div id="topbar" style="position:relative; z-index:1000; height:56px; background:#fff; border-bottom:1px solid #e2e8f0; display:flex; align-items:center; justify-content:space-between; padding:0 16px; flex-shrink:0">
<div style="display:flex; align-items:center; gap:8px">
<button type="button" onclick="document.getElementById('sidebar').style.display=document.getElementById('sidebar').style.display==='none'?'flex':'none';setTimeout(function(){map.invalidateSize()},300)" style="background:none;border:none;cursor:pointer;color:#64748b;padding:4px;display:flex;align-items:center"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button>
<div style="font-size:15px; font-weight:700; color:#1e293b">Peta GIS <span style="font-size:13px; font-weight:500; color:#64748b; margin-left:4px">Kanalisasi SPBU</span></div>
</div>
<div style="display:flex; gap:8px">
<a href="tambah.php" style="display:flex; align-items:center; gap:6px; padding:8px 14px; background:#1d4ed8; color:#fff; border-radius:8px; font-size:12px; font-weight:600; text-decoration:none; box-shadow:0 2px 4px rgba(29,78,216,0.15)">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Tambah Baru
</a>
</div>
</div>
<div id="map"></div>
<?php include 'includes/map-controls.php'; ?>
</div>
<div id="edit-modal">
<div id="edit-box">
<h3>Edit Data SPBU</h3>
<input type="hidden" id="edit-id">
<label>Nama SPBU</label>
<input type="text" id="edit-nama">
<label>Nomor Depot</label>
<input type="text" id="edit-nomor">
<label>Jam Operasional</label>
<select id="edit-status">
<option value="24 Jam">24 Jam</option>
<option value="Tidak 24 Jam">Tidak 24 Jam</option>
</select>
<div class="edit-btns">
<button class="btn-cancel" type="button" onclick="closeModal()">Batal</button>
<button class="btn-save" type="button" onclick="submitEdit()">Simpan</button>
</div>
</div>
</div>
<div id="toast"></div>
<?php include 'includes/confirm-modal.php'; ?>
<script src="assets/map-base.js"></script>
<script>
var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.control.zoom({ position: 'bottomright' }).addTo(map);
initBaseLayers(map);
<?php if ($saved): ?>showToast('Data SPBU berhasil disimpan.');<?php endif ?>
var spbuData = [];
var layer24Jam = L.layerGroup().addTo(map);
var layerTidak24 = L.layerGroup().addTo(map);
document.getElementById('ly-24').addEventListener('change', function() {
this.checked ? map.addLayer(layer24Jam) : map.removeLayer(layer24Jam);
});
document.getElementById('ly-tdk').addEventListener('change', function() {
this.checked ? map.addLayer(layerTidak24) : map.removeLayer(layerTidak24);
});
async function deleteData(id) {
var ok = await showConfirm({
title: 'Hapus Data SPBU',
message: 'Data ini akan dihapus permanen dari database.',
confirmText: 'Ya, Hapus', danger: true
});
if (ok) window.location.href = 'index.php?action=delete&id=' + id;
}
function openEdit(id, nama, nomor, status) {
document.getElementById('edit-id').value = id;
document.getElementById('edit-nama').value = nama;
document.getElementById('edit-nomor').value = nomor;
document.getElementById('edit-status').value = status;
document.getElementById('edit-modal').classList.add('active');
}
function closeModal() { document.getElementById('edit-modal').classList.remove('active'); }
function submitEdit() {
var id = document.getElementById('edit-id').value;
var nama = document.getElementById('edit-nama').value.trim();
var nomor = document.getElementById('edit-nomor').value.trim();
var status = document.getElementById('edit-status').value;
if (!nama || !nomor) { showToast('Nama dan nomor wajib diisi.'); return; }
window.location.href = 'index.php?action=edit&id=' + id
+ '&nama=' + encodeURIComponent(nama)
+ '&nomor=' + encodeURIComponent(nomor)
+ '&status=' + encodeURIComponent(status);
}
document.getElementById('edit-modal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
function flyToSpbu(id) {
var s = spbuData.find(function(x) { return x.id === id; });
if (!s) return;
document.querySelectorAll('.spbu-item').forEach(function(el) { el.classList.remove('active'); });
var el = document.querySelector('.spbu-item[data-id="' + id + '"]');
if (el) el.classList.add('active');
map.flyTo([s.lat, s.lng], 17, { duration: 1 });
setTimeout(function() { s.marker.openPopup(); }, 1100);
}
<?php
$data = $conn->query("SELECT * FROM tempat ORDER BY nama_tempat ASC");
while ($row = $data->fetch_assoc()):
$id = $row['id']; $lat = $row['latitude']; $lng = $row['longitude'];
$nama = addslashes($row['nama_tempat']); $nomor = addslashes($row['nomor_tempat']);
$status = addslashes($row['status']);
$color = ($row['status'] === '24 Jam') ? '#22c55e' : '#ef4444';
$badge = ($row['status'] === '24 Jam') ? 'badge-green' : 'badge-red';
$layerVar = ($row['status'] === '24 Jam') ? 'layer24Jam' : 'layerTidak24';
$latFmt = number_format($lat, 5); $lngFmt = number_format($lng, 5);
?>
(function() {
var savedLat = <?= $lat ?>, savedLng = <?= $lng ?>, markerId = <?= $id ?>, color = '<?= $color ?>';
var marker = L.marker([savedLat, savedLng], { icon: makeSpbuIcon(color), draggable: true }).addTo(<?= $layerVar ?>);
spbuData.push({ id: markerId, lat: savedLat, lng: savedLng, nama: '<?= $nama ?>', nomor: '<?= $nomor ?>', status: '<?= $status ?>', color: color, badge: '<?= $badge ?>', marker: marker });
function showInfoPopup() {
marker.unbindPopup();
marker.bindPopup(
'<div class="popup-wrap"><div class="popup-title"><?= $nama ?></div>' +
'<div class="popup-row">Depot No. <?= $nomor ?></div>' +
'<div class="popup-coord"><?= $latFmt ?>, <?= $lngFmt ?></div>' +
'<span class="popup-badge <?= $badge ?>"><?= $status ?></span>' +
'<div class="popup-actions">' +
'<button class="btn-edit" onclick="openEdit(<?= $id ?>,\'<?= $nama ?>\',\'<?= $nomor ?>\',\'<?= $status ?>\')">Edit</button>' +
'<button class="btn-delete" onclick="deleteData(<?= $id ?>)">Hapus</button></div></div>'
);
}
showInfoPopup();
var dragStartLat, dragStartLng;
marker.on('dragstart', function() {
dragStartLat = marker.getLatLng().lat; dragStartLng = marker.getLatLng().lng;
marker.closePopup(); marker.unbindPopup();
});
marker.on('dragend', function() {
var np = marker.getLatLng();
var msgBase = '<div style="text-align:left;line-height:1.6">' +
'<b>' + '<?= $nama ?>' + '</b><br>' +
'<span style="color:var(--text3);font-size:12px">Koordinat: ' + np.lat.toFixed(6) + ', ' + np.lng.toFixed(6) + '</span><br>';
fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat='+np.lat+'&lon='+np.lng+'&accept-language=id&zoom=18')
.then(r => r.json())
.then(d => {
var alamat = d.display_name || 'Alamat tidak ditemukan';
showConfirm({
title: 'Simpan posisi baru?',
message: msgBase + '<span style="font-size:13px;color:var(--text2)"><b>Alamat Baru:</b> ' + alamat + '</span></div>',
confirmText: 'Ya, Simpan',
icon: 'move',
position: 'top'
}).then(function(ok) {
if (ok) {
fetch('index.php?action=update_pos&id=' + markerId + '&lat=' + np.lat + '&lng=' + np.lng)
.then(function(r) { return r.text(); }).then(function(t) {
if (t.trim() === 'ok') {
savedLat = np.lat; savedLng = np.lng;
var s = spbuData.find(function(x) { return x.id === markerId; });
if (s) { s.lat = savedLat; s.lng = savedLng; }
showToast('Posisi diperbarui.');
} else {
showToast('Gagal menyimpan.');
marker.setLatLng([dragStartLat, dragStartLng]);
}
showInfoPopup();
});
} else {
marker.setLatLng([dragStartLat, dragStartLng]);
showInfoPopup();
}
});
})
.catch(() => {
showConfirm({
title: 'Simpan posisi baru?',
message: msgBase + '<span style="font-size:13px;color:var(--text2)"><b>Alamat Baru:</b> Gagal memuat alamat</span></div>',
confirmText: 'Ya, Simpan',
icon: 'move',
position: 'top'
}).then(function(ok) {
if (ok) {
fetch('index.php?action=update_pos&id=' + markerId + '&lat=' + np.lat + '&lng=' + np.lng)
.then(function(r) { return r.text(); }).then(function(t) {
if (t.trim() === 'ok') {
savedLat = np.lat; savedLng = np.lng;
var s = spbuData.find(function(x) { return x.id === markerId; });
if (s) { s.lat = savedLat; s.lng = savedLng; }
showToast('Posisi diperbarui.');
} else {
showToast('Gagal menyimpan.');
marker.setLatLng([dragStartLat, dragStartLng]);
}
showInfoPopup();
});
} else {
marker.setLatLng([dragStartLat, dragStartLng]);
showInfoPopup();
}
});
});
});
marker.on('click', function() {
document.querySelectorAll('.spbu-item').forEach(function(el) { el.classList.remove('active'); });
var el = document.querySelector('.spbu-item[data-id="' + markerId + '"]');
if (el) { el.classList.add('active'); el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
});
})();
<?php endwhile; ?>
var currentFilter = 'semua', currentSearch = '';
function renderList() {
var list = document.getElementById('spbu-list');
var kw = currentSearch.toLowerCase();
var filtered = spbuData.filter(function(s) {
var f = (currentFilter === 'semua') || (s.status === currentFilter);
var q = s.nama.toLowerCase().indexOf(kw) !== -1 || s.nomor.toLowerCase().indexOf(kw) !== -1;
return f && q;
});
document.getElementById('list-count').textContent = filtered.length + ' lokasi';
if (!filtered.length) { list.innerHTML = '<div class="no-result">Tidak ada SPBU ditemukan</div>'; return; }
list.innerHTML = filtered.map(function(s) {
return '<div class="spbu-item" data-id="' + s.id + '" onclick="flyToSpbu(' + s.id + ')">' +
'<div class="spbu-dot" style="background:' + s.color + '"></div>' +
'<div><div class="spbu-nama">' + s.nama + '</div>' +
'<div class="spbu-meta">No. ' + s.nomor + ' · ' + s.lat.toFixed(5) + ', ' + s.lng.toFixed(5) + '</div></div>' +
'<span class="spbu-badge ' + s.badge + '">' + (s.status === '24 Jam' ? '24 Jam' : 'Tdk 24 Jam') + '</span></div>';
}).join('');
}
document.getElementById('search-input').addEventListener('input', function() { currentSearch = this.value; renderList(); });
document.querySelectorAll('.sb-filter button').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('.sb-filter button').forEach(function(b) { b.classList.remove('active'); });
this.classList.add('active'); currentFilter = this.dataset.filter; renderList();
});
});
renderList();
</script>
</body>
</html>
+149
View File
@@ -0,0 +1,149 @@
<?php
include 'db.php';
require_once 'includes/stats.php';
$stats = spbu_stats($conn);
$activePage = 'tambah';
$err = isset($_GET['err']) ? $_GET['err'] : '';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Tambah SPBU — Sistem Informasi Geografis SPBU</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="stylesheet" href="assets/spbu.css?v=1.2"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</head>
<body>
<?php include 'includes/sidebar.php'; ?>
<div class="sb-body">
<form class="form-panel" id="form-spbu" method="POST" action="api.php">
<h3>Input Data SPBU</h3>
<p class="form-desc">Klik titik pada peta untuk menentukan koordinat, lalu lengkapi formulir di bawah.</p>
<?php if ($err): ?><div class="form-alert"><?= htmlspecialchars($err) ?></div><?php endif ?>
<div class="form-group">
<label for="nama">Nama SPBU</label>
<input type="text" id="nama" name="nama" placeholder="Contoh: SPBU Kota Baru" required>
</div>
<div class="form-group">
<label for="nomor">Nomor Depot</label>
<input type="text" id="nomor" name="nomor" placeholder="Contoh: 34.123.45" required>
</div>
<div class="form-group">
<label for="status">Jam Operasional</label>
<select id="status" name="status">
<option value="24 Jam">Buka 24 Jam</option>
<option value="Tidak 24 Jam">Tidak 24 Jam</option>
</select>
</div>
<div class="form-group">
<label>Koordinat Lokasi</label>
<div class="coord-box">
<input type="text" id="lat-display" placeholder="Latitude" readonly>
<input type="text" id="lng-display" placeholder="Longitude" readonly>
</div>
<input type="hidden" name="lat" id="lat" required>
<input type="hidden" name="lng" id="lng" required>
</div>
<div class="form-actions">
<button type="button" class="btn-secondary" onclick="resetForm()">Reset</button>
<button type="submit" name="save" class="btn-primary" id="btn-save" disabled>Simpan Data</button>
</div>
<div class="form-hint" id="form-hint">Belum ada lokasi dipilih — klik peta di sebelah kanan</div>
</form>
</div>
<div class="sb-foot" style="flex-direction:column; gap:10px">
<div style="display:flex; justify-content:space-between; width:100%">
<span>Operasional 24 jam: <b><?= $stats['pct24'] ?>%</b></span>
<span><?= date('d M Y') ?></span>
</div>
<a href="../index.php" class="btn-portal" style="display:flex; align-items:center; justify-content:center; gap:6px; padding:10px; background:#eff6ff; color:#1d4ed8; border:1px solid #bfdbfe; border-radius:8px; font-weight:600; text-decoration:none; font-size:12px; transition:all 0.15s; width:100%">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Kembali ke Portal
</a>
</div>
</aside>
<div id="map-area">
<!-- Topbar -->
<div id="topbar" style="position:relative; z-index:1000; height:56px; background:#fff; border-bottom:1px solid #e2e8f0; display:flex; align-items:center; justify-content:space-between; padding:0 16px; flex-shrink:0">
<div style="display:flex; align-items:center; gap:8px">
<div style="font-size:15px; font-weight:700; color:#1e293b">Tambah Baru <span style="font-size:13px; font-weight:500; color:#64748b; margin-left:4px">Lokasi Depot SPBU</span></div>
</div>
<div style="display:flex; gap:8px">
<a href="index.php" style="display:flex; align-items:center; gap:6px; padding:8px 14px; border:1px solid #cbd5e1; color:#475569; border-radius:8px; font-size:12px; font-weight:600; text-decoration:none; background:#fff">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
Lihat Peta
</a>
</div>
</div>
<div id="map"></div>
<?php include 'includes/map-controls.php'; ?>
<div id="map-hint" style="display:block">Klik peta untuk menandai lokasi SPBU</div>
</div>
<div id="toast"></div>
<script src="assets/map-base.js"></script>
<script>
var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.control.zoom({ position: 'bottomright' }).addTo(map);
initBaseLayers(map);
var newMarker = null;
var newIcon = makeSpbuIcon('#3b82f6', 22);
var hintEl = document.getElementById('form-hint');
var btnSave = document.getElementById('btn-save');
<?php
$existing = $conn->query("SELECT * FROM tempat");
while ($row = $existing->fetch_assoc()):
$nama = addslashes($row['nama_tempat']); $nomor = addslashes($row['nomor_tempat']);
$status = addslashes($row['status']);
$color = ($row['status'] === '24 Jam') ? '#22c55e' : '#ef4444';
$badge = ($row['status'] === '24 Jam') ? 'badge-green' : 'badge-red';
?>
L.marker([<?= $row['latitude'] ?>, <?= $row['longitude'] ?>], { icon: makeSpbuIcon('<?= $color ?>') })
.addTo(map).bindPopup(
'<div class="popup-wrap"><div class="popup-title"><?= $nama ?></div>' +
'<div class="popup-row">Depot No. <?= $nomor ?></div>' +
'<span class="popup-badge <?= $badge ?>"><?= $status ?></span></div>'
);
<?php endwhile; ?>
function setCoords(lat, lng) {
document.getElementById('lat').value = lat.toFixed(6);
document.getElementById('lng').value = lng.toFixed(6);
document.getElementById('lat-display').value = lat.toFixed(6);
document.getElementById('lng-display').value = lng.toFixed(6);
btnSave.disabled = false;
hintEl.textContent = 'Lokasi terpilih — lengkapi data lalu simpan';
hintEl.classList.add('active');
}
function resetForm() {
document.getElementById('form-spbu').reset();
document.getElementById('lat').value = '';
document.getElementById('lng').value = '';
document.getElementById('lat-display').value = '';
document.getElementById('lng-display').value = '';
btnSave.disabled = true;
hintEl.textContent = 'Belum ada lokasi dipilih — klik peta di sebelah kanan';
hintEl.classList.remove('active');
if (newMarker) { map.removeLayer(newMarker); newMarker = null; }
}
map.on('click', function(e) {
if (newMarker) map.removeLayer(newMarker);
newMarker = L.marker(e.latlng, { icon: newIcon }).addTo(map);
setCoords(e.latlng.lat, e.latlng.lng);
});
</script>
</body>
</html>
+202
View File
@@ -0,0 +1,202 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WebGIS Pontianak Portal</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #eff6ff; --white: #ffffff; --border: #bfdbfe;
--text: #1e293b; --text2: #475569; --text3: #94a3b8;
--blue-primary: #1d4ed8; --blue-light: #dbeafe; --blue-hover: #1e40af;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', sans-serif; color: var(--text);
background: var(--bg); min-height: 100vh;
display: flex; flex-direction: column;
}
.navbar {
background: var(--white);
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
box-shadow: 0 1px 3px rgba(0,0,0,.05);
}
.nav-brand {
display: flex; align-items: center; gap: 10px;
font-weight: 700; font-size: 18px; color: var(--blue-primary);
}
.hero {
background: linear-gradient(135deg, var(--white) 0%, var(--blue-light) 100%);
padding: 80px 24px; text-align: center;
border-bottom: 1px solid var(--border);
}
.hero-badge {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 600;
letter-spacing: .05em; text-transform: uppercase;
color: var(--blue-primary); background: rgba(37,99,235,.1);
padding: 6px 14px; border-radius: 20px;
margin-bottom: 20px;
}
.hero h1 { font-size: clamp(32px, 5vw, 48px); font-weight: 800; line-height: 1.2; margin-bottom: 16px; color: #1e293b; }
.hero p {
font-size: 16px; color: var(--text2); max-width: 600px;
margin: 0 auto; line-height: 1.7;
}
.main { max-width: 1140px; margin: 60px auto; padding: 0 24px; flex: 1; width: 100%; }
.projects {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px;
}
@media (max-width: 900px) { .projects { grid-template-columns: 1fr; } }
.project-card {
background: var(--white); border: 1px solid var(--border);
border-radius: 16px; overflow: hidden; text-decoration: none; color: inherit;
display: flex; flex-direction: column;
transition: all .25s ease;
box-shadow: 0 4px 6px -1px rgba(0,0,0,.05), 0 2px 4px -1px rgba(0,0,0,.03);
}
.project-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0,0,0,.1), 0 10px 10px -5px rgba(0,0,0,.04);
border-color: #bfdbfe;
}
.pc-head {
padding: 30px 24px 20px; border-bottom: 1px solid var(--border);
display: flex; flex-direction: column; align-items: center; text-align: center;
}
.pc-icon {
width: 56px; height: 56px; border-radius: 14px;
display: flex; align-items: center; justify-content: center;
margin-bottom: 18px;
background: var(--blue-light); color: var(--blue-primary);
}
.pc-icon svg { width: 28px; height: 28px; }
.pc-head h2 { font-size: 18px; font-weight: 700; margin-bottom: 8px; color: #1e293b; }
.pc-head p { font-size: 14px; color: var(--text2); line-height: 1.6; }
.pc-body { padding: 20px 24px; flex: 1; background: #fafafa; }
.pc-features { list-style: none; font-size: 13.5px; color: var(--text2); line-height: 1.8; }
.pc-features li { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.pc-features li svg { width: 16px; height: 16px; color: var(--blue-primary); flex-shrink: 0; }
.pc-foot {
padding: 16px 24px; border-top: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
background: var(--white);
}
.pc-link {
font-size: 14px; font-weight: 600; color: var(--blue-primary);
display: flex; align-items: center; gap: 4px;
}
.pc-tag {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: .05em; padding: 4px 10px; border-radius: 6px;
background: var(--blue-light); color: var(--blue-primary);
}
.site-foot {
text-align: center; padding: 32px 24px; font-size: 14px; color: var(--text3);
border-top: 1px solid var(--border); background: var(--white);
}
</style>
</head>
<body>
<nav class="navbar">
<div class="nav-brand">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="24" height="24">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
WebGIS Pontianak
</div>
</nav>
<header class="hero">
<div class="hero-badge">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
Sistem Informasi Geografis
</div>
<h1>Portal Terpadu WebGIS</h1>
<p>Pusat akses untuk aplikasi pemetaan wilayah Pontianak, Kalimantan Barat. Pilih modul di bawah untuk membuka project yang sesuai dengan kebutuhan Anda.</p>
</header>
<main class="main">
<div class="projects">
<a class="project-card" href="SIG_spbu/index.php">
<div class="pc-head">
<div class="pc-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 22v-8p2 2 0 0 1 4 0h10a2 2 0 0 1 2 2v4"/><path d="M14 22V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v18"/><path d="M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 2 2h0a2 2 0 0 0 2-2V9.83a2 2 0 0 0-.59-1.42L18 5"/></svg>
</div>
<h2>Sistem Informasi Geografis SPBU</h2>
<p>Pemetaan lokasi Stasiun Pengisian Bahan Bakar Umum beserta status operasional 24 jam.</p>
</div>
<div class="pc-body">
<ul class="pc-features">
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Peta interaktif titik SPBU</li>
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Filter status buka 24 jam</li>
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Tambah & kelola data depot</li>
</ul>
</div>
<div class="pc-foot">
<span class="pc-link">Buka aplikasi <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></span>
</div>
</a>
<a class="project-card" href="SIG_miskin/gis/login.php">
<div class="pc-head">
<div class="pc-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
</div>
<h2>Pemetaan Kemiskinan Terpadu</h2>
<p>SIG penduduk miskin dan rumah ibadah untuk perencanaan program binaan.</p>
</div>
<div class="pc-body">
<ul class="pc-features">
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Peta rumah ibadah & KK miskin</li>
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Analisis radius binaan</li>
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Dashboard super admin</li>
</ul>
</div>
<div class="pc-foot">
<span class="pc-link">Masuk ke aplikasi <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></span>
</div>
</a>
<a class="project-card" href="SIG2_jalantanah/index.php">
<div class="pc-head">
<div class="pc-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
</div>
<h2>Sistem Informasi Infrastruktur Jalan</h2>
<p>Pemetaan jalan tanah, kavling tanah, dan titik kerusakan infrastruktur wilayah.</p>
</div>
<div class="pc-body">
<ul class="pc-features">
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Klasifikasi jalan nasionalkab</li>
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Data kavling & status sertifikat</li>
<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Input titik jalan rusak</li>
</ul>
</div>
<div class="pc-foot">
<span class="pc-link">Buka aplikasi <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg></span>
</div>
</a>
</div>
</main>
<footer class="site-foot">WebGIS Pontianak · Kalimantan Barat · <?= date('Y') ?></footer>
</body>
</html>