First commit / commit pertama

This commit is contained in:
Mr.Haruna
2026-06-13 11:24:58 +07:00
commit 522c4f7200
166 changed files with 13326 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pdo = Database::getConnection();
$pageTitle = 'Analisis Spasial';
$activeNav = 'analisis';
$extraHead = '<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">';
require_once __DIR__ . '/partials/header.php';
require_once __DIR__ . '/../partials/analisis_body.php';
require_once __DIR__ . '/partials/footer.php';
+191
View File
@@ -0,0 +1,191 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pdo = Database::getConnection();
$sql = "SELECT l.id, l.kategori, l.deskripsi, l.status, l.created_at,
u.nama_lengkap, u.username, ST_X(l.geometry) as lng, ST_Y(l.geometry) as lat
FROM laporan_warga l
JOIN users u ON l.user_id = u.id
ORDER BY l.id DESC";
$rows = $pdo->query($sql)->fetchAll();
$pageTitle = 'Laporan Warga';
$activeNav = 'laporan';
$extraHead = '<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">';
require_once __DIR__ . '/partials/header.php';
function badgeStatus($status) {
if($status === 'menunggu') return 'badge-warning';
if($status === 'diproses') return 'badge-info';
if($status === 'selesai') return 'badge-success';
if($status === 'ditolak') return 'badge-danger';
return 'badge-primary';
}
?>
<div class="page-header">
<div class="breadcrumb"><span>Admin</span><span class="sep">/</span><span>Laporan Warga</span></div>
<h1>📢 Validasi Laporan Warga</h1>
<p>Kelola dan perbarui status laporan kerusakan infrastruktur dari warga.</p>
</div>
<div class="page-toolbar">
<div class="search-box">
<i class="fas fa-search search-icon"></i>
<input type="text" id="searchInput" placeholder="Cari laporan..." oninput="filterTable('searchInput','lapTable')">
</div>
</div>
<div class="table-wrap">
<table class="data-table" id="lapTable">
<thead>
<tr>
<th>#</th>
<th>Pelapor</th>
<th>Kategori</th>
<th>Deskripsi</th>
<th>Status</th>
<th>Tanggal</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $i => $r): ?>
<tr>
<td style="color:var(--text-muted)"><?= $i+1 ?></td>
<td><strong><?= htmlspecialchars($r['nama_lengkap'] ?: $r['username']) ?></strong></td>
<td><?= htmlspecialchars($r['kategori']) ?></td>
<td style="color:var(--text-secondary); max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<?= htmlspecialchars($r['deskripsi']) ?>
</td>
<td><span class="badge <?= badgeStatus($r['status']) ?>"><?= ucfirst($r['status']) ?></span></td>
<td style="color:var(--text-muted); font-size:0.8rem;"><?= date('d M Y', strtotime($r['created_at'])) ?></td>
<td>
<div class="actions">
<button class="btn btn-ghost btn-sm btn-icon" onclick='lihatLokasi(<?= json_encode($r) ?>)' title="Lihat Lokasi"><i class="fas fa-map-marker-alt" style="color: #3B82F6;"></i></button>
<button class="btn btn-ghost btn-sm btn-icon" onclick='editStatus(<?= json_encode($r) ?>)' title="Ubah Status"><i class="fas fa-edit"></i></button>
<button class="btn btn-danger btn-sm btn-icon" onclick="hapus(<?= $r['id'] ?>)" title="Hapus Laporan"><i class="fas fa-trash"></i></button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?>
<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text-muted);">Belum ada laporan dari warga.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Modal Ubah Status -->
<div class="modal-overlay" id="modalStatus">
<div class="modal">
<div class="modal-header">
<span class="modal-title">✏️ Perbarui Status Laporan</span>
<button class="modal-close" onclick="closeModal('modalStatus')">×</button>
</div>
<input type="hidden" id="editId">
<div class="form-group">
<label class="form-label">Ubah Status</label>
<select id="editStatusSelect" class="form-control">
<option value="menunggu">Menunggu</option>
<option value="diproses">Diproses</option>
<option value="selesai">Selesai</option>
<option value="ditolak">Ditolak</option>
</select>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('modalStatus')">Batal</button>
<button class="btn btn-primary" onclick="updateStatus()"><i class="fas fa-save"></i> Simpan</button>
</div>
</div>
</div>
<!-- Modal Lihat Peta -->
<div class="modal-overlay" id="modalMap">
<div class="modal">
<div class="modal-header">
<span class="modal-title">📍 Lokasi Laporan</span>
<button class="modal-close" onclick="closeModal('modalMap')">×</button>
</div>
<div style="padding-bottom: 12px; font-size: 0.9rem;">
<strong id="mapKategori"></strong><br>
<span style="color: var(--text-secondary);" id="mapDeskripsi"></span>
</div>
<div id="miniMapLihat" style="height:300px;border-radius:8px;border:1px solid #E5E7EB;"></div>
</div>
</div>
<?php $extraScript = <<<'JS'
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const API = APP_BASE + '/api/laporan.php';
function editStatus(r) {
document.getElementById('editId').value = r.id;
document.getElementById('editStatusSelect').value = r.status;
openModal('modalStatus');
}
async function updateStatus() {
const id = document.getElementById('editId').value;
const status = document.getElementById('editStatusSelect').value;
const res = await fetch(API + '?id=' + id, {
method:'PUT',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ status: status })
});
const d = await res.json();
if(d.status === 'success') {
showToast('Status diperbarui!');
setTimeout(() => location.reload(), 900);
} else {
showToast(d.message,'error');
}
}
async function hapus(id) {
if(!confirm('Hapus laporan secara permanen?')) return;
const res = await fetch(API + '?id=' + id, { method:'DELETE' });
const d = await res.json();
if(d.status === 'success') {
showToast('Laporan dihapus!');
setTimeout(() => location.reload(), 900);
} else {
showToast(d.message,'error');
}
}
let mapInstance = null, mapMarker = null;
const originalOpenModal = window.openModal;
window.openModal = function(id) {
originalOpenModal(id);
if(id === 'modalMap') {
setTimeout(() => {
if(mapInstance) mapInstance.invalidateSize();
}, 100);
}
};
function lihatLokasi(r) {
document.getElementById('mapKategori').innerText = r.kategori;
document.getElementById('mapDeskripsi').innerText = r.deskripsi;
openModal('modalMap');
setTimeout(() => {
if(!mapInstance) {
mapInstance = L.map('miniMapLihat');
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png').addTo(mapInstance);
}
if(mapMarker) mapInstance.removeLayer(mapMarker);
const latLng = [parseFloat(r.lat), parseFloat(r.lng)];
mapMarker = L.marker(latLng).addTo(mapInstance);
mapInstance.setView(latLng, 16);
}, 150);
}
</script>
JS; ?>
<?php require_once __DIR__ . '/partials/footer.php'; ?>
+324
View File
@@ -0,0 +1,324 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pdo = Database::getConnection();
$rows = $pdo->query("
SELECT w.*, wl.nama AS wilayah_nama,
(SELECT GROUP_CONCAT(jb.kode SEPARATOR ',')
FROM bantuan_sosial bs JOIN jenis_bantuan jb ON jb.id=bs.jenis_bantuan_id
WHERE bs.warga_id=w.id AND bs.is_aktif=1) AS bantuan_aktif,
(SELECT COUNT(*) FROM foto_rumah f WHERE f.warga_id=w.id) AS jumlah_foto
FROM warga_miskin w
LEFT JOIN wilayah wl ON wl.id=w.wilayah_id
ORDER BY w.id DESC
")->fetchAll();
$wilayahList = $pdo->query("SELECT id, nama FROM wilayah ORDER BY nama")->fetchAll();
$jenisList = $pdo->query("SELECT id, kode, nama FROM jenis_bantuan ORDER BY id")->fetchAll();
$pageTitle = 'Data Rumah Tangga Miskin'; $activeNav = 'warga';
$extraHead = '<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">';
require_once __DIR__ . '/partials/header.php';
function verifBadge($s) {
return match ($s) {
'terverifikasi' => '<span class="badge badge-success">✔ Terverifikasi</span>',
'perlu_tinjauan' => '<span class="badge badge-warning">⚠ Perlu Tinjauan</span>',
default => '<span class="badge" style="background:#F3F4F6;color:#6B7280;">○ Belum</span>',
};
}
?>
<div class="page-header">
<div class="breadcrumb"><span>Operator</span><span class="sep">/</span><span>Rumah Tangga Miskin</span></div>
<h1>👥 Data Rumah Tangga Miskin</h1>
<p>Kelola data lengkap rumah tangga, status bantuan, foto kondisi rumah, dan verifikasi (KF-12 s/d 18).</p>
</div>
<div class="page-toolbar" style="flex-wrap:wrap;gap:10px;">
<div class="search-box"><i class="fas fa-search search-icon"></i><input type="text" id="searchInput" placeholder="Cari nama / NIK..." oninput="filterTable('searchInput','wargaTable')"></div>
<select id="filterVerif" class="form-control" style="max-width:180px;" onchange="applyVerifFilter()">
<option value="">Semua status</option>
<option value="belum">Belum</option>
<option value="terverifikasi">Terverifikasi</option>
<option value="perlu_tinjauan">Perlu Tinjauan</option>
</select>
<button class="btn btn-ghost" onclick="bulkVerify()"><i class="fas fa-check-double"></i> Verifikasi Terpilih</button>
<a class="btn btn-ghost" href="<?= app_url('api/warga_export.php?format=csv') ?>"><i class="fas fa-file-csv"></i> Export CSV</a>
<a class="btn btn-ghost" href="<?= app_url('api/warga_export.php?format=print') ?>" target="_blank"><i class="fas fa-print"></i> Cetak/PDF</a>
<button class="btn btn-ghost" onclick="openModal('modalImport')"><i class="fas fa-file-import"></i> Import CSV</button>
<button class="btn btn-primary" onclick="openTambah()"><i class="fas fa-plus"></i> Tambah Data</button>
</div>
<div class="table-wrap">
<table class="data-table" id="wargaTable">
<thead><tr>
<th><input type="checkbox" id="checkAll" onclick="toggleAll(this)"></th>
<th>Nama KK</th><th>NIK</th><th>Kelurahan</th><th>Penghasilan</th>
<th>Jiwa</th><th>Bantuan</th><th>Foto</th><th>Verifikasi</th><th>Aksi</th>
</tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr data-verif="<?= $r['status_verifikasi'] ?>">
<td><input type="checkbox" class="rowCheck" value="<?= $r['id'] ?>"></td>
<td><strong><?= htmlspecialchars($r['nama_kk']) ?></strong></td>
<td style="font-size:0.82rem;color:var(--text-muted)"><?= htmlspecialchars($r['nik_kk'] ?: '-') ?></td>
<td style="font-size:0.85rem"><?= htmlspecialchars($r['wilayah_nama'] ?: '-') ?></td>
<td>Rp <?= number_format($r['penghasilan'],0,',','.') ?></td>
<td><?= $r['jumlah_jiwa'] ?: ($r['jumlah_tanggungan'] ?: '-') ?></td>
<td style="font-size:0.78rem"><?= $r['bantuan_aktif'] ? '<span class="badge badge-info">'.str_replace(',','</span> <span class="badge badge-info">',htmlspecialchars($r['bantuan_aktif'])).'</span>' : '<span style="color:#9CA3AF">—</span>' ?></td>
<td><?= $r['jumlah_foto'] > 0 ? '📷 '.$r['jumlah_foto'] : '<span style="color:#9CA3AF">—</span>' ?></td>
<td><?= verifBadge($r['status_verifikasi']) ?></td>
<td><div class="actions">
<button class="btn btn-ghost btn-sm btn-icon" title="Kelola (bantuan & foto)" onclick="openDetail(<?= $r['id'] ?>,'<?= addslashes($r['nama_kk']) ?>')"><i class="fas fa-folder-open"></i></button>
<button class="btn btn-ghost btn-sm btn-icon" title="Edit" onclick='editRow(<?= json_encode($r, JSON_HEX_APOS|JSON_HEX_QUOT) ?>)'><i class="fas fa-edit"></i></button>
<button class="btn btn-danger btn-sm btn-icon" title="Hapus" onclick="hapus(<?= $r['id'] ?>,'<?= addslashes($r['nama_kk']) ?>')"><i class="fas fa-trash"></i></button>
</div></td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="10" style="text-align:center;padding:40px;color:var(--text-muted);">Belum ada data.</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<!-- ===== Modal Tambah/Edit ===== -->
<div class="modal-overlay" id="modalForm"><div class="modal" style="max-width:680px;">
<div class="modal-header"><span class="modal-title" id="formTitle">Tambah Data</span><button class="modal-close" onclick="closeModal('modalForm')">×</button></div>
<div style="max-height:70vh;overflow-y:auto;padding-right:6px;">
<input type="hidden" id="fId">
<h4 style="margin:4px 0 8px;font-size:0.85rem;color:#2563EB;">Identitas</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Nama KK *</label><input id="fNama" class="form-control" required></div>
<div class="form-group"><label class="form-label">NIK KK</label><input id="fNik" class="form-control" maxlength="16" placeholder="16 digit"></div>
</div>
<div class="form-group"><label class="form-label">Alamat</label><input id="fAlamat" class="form-control"></div>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Kelurahan</label>
<select id="fWilayah" class="form-control"><option value="">- pilih -</option>
<?php foreach ($wilayahList as $w): ?><option value="<?= $w['id'] ?>"><?= htmlspecialchars($w['nama']) ?></option><?php endforeach; ?>
</select></div>
<div class="form-group"><label class="form-label">RT</label><input id="fRt" class="form-control"></div>
<div class="form-group"><label class="form-label">RW</label><input id="fRw" class="form-control"></div>
</div>
<h4 style="margin:10px 0 8px;font-size:0.85rem;color:#2563EB;">Demografi & Ekonomi</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Jumlah Jiwa</label><input id="fJiwa" type="number" min="0" class="form-control"></div>
<div class="form-group"><label class="form-label">Jumlah Tanggungan</label><input id="fTanggungan" type="number" min="0" class="form-control"></div>
<div class="form-group"><label class="form-label">Pekerjaan KK</label><input id="fPekerjaan" class="form-control"></div>
<div class="form-group"><label class="form-label">Penghasilan/bln (Rp)</label><input id="fPenghasilan" type="number" min="0" class="form-control"></div>
</div>
<h4 style="margin:10px 0 8px;font-size:0.85rem;color:#2563EB;">Kondisi Rumah</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Sumber Air</label>
<select id="fAir" class="form-control"><option value="">-</option><option>SUMUR_BOR</option><option>PDAM</option><option>SUNGAI</option><option>HUJAN</option><option>TIDAK_ADA</option></select></div>
<div class="form-group"><label class="form-label">Listrik</label>
<select id="fListrik" class="form-control"><option value="">-</option><option>PLN</option><option>NON_PLN</option><option>TIDAK_ADA</option></select></div>
<div class="form-group"><label class="form-label">Material Dinding</label>
<select id="fDinding" class="form-control"><option value="">-</option><option>PERMANEN</option><option>SEMI_PERMANEN</option><option>TIDAK_PERMANEN</option></select></div>
<div class="form-group"><label class="form-label">Status Kepemilikan</label>
<select id="fKepemilikan" class="form-control"><option value="">-</option><option>MILIK</option><option>SEWA</option><option>NUMPANG</option><option>LAINNYA</option></select></div>
</div>
<div class="form-group"><label class="form-label">Status Verifikasi</label>
<select id="fVerif" class="form-control"><option value="belum">Belum</option><option value="terverifikasi">Terverifikasi</option><option value="perlu_tinjauan">Perlu Tinjauan</option></select></div>
<h4 style="margin:10px 0 8px;font-size:0.85rem;color:#2563EB;">Lokasi *</h4>
<div id="miniMap" style="height:220px;border-radius:8px;border:1px solid #E5E7EB;margin-bottom:10px;z-index:1;"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<input id="fLat" class="form-control" placeholder="Latitude" readonly style="background:#F9FAFB;">
<input id="fLng" class="form-control" placeholder="Longitude" readonly style="background:#F9FAFB;">
</div>
</div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('modalForm')">Batal</button><button class="btn btn-primary" onclick="simpan()"><i class="fas fa-save"></i> Simpan</button></div>
</div></div>
<!-- ===== Modal Detail: Bantuan & Foto ===== -->
<div class="modal-overlay" id="modalDetail"><div class="modal" style="max-width:640px;">
<div class="modal-header"><span class="modal-title" id="detailTitle">Kelola</span><button class="modal-close" onclick="closeModal('modalDetail')">×</button></div>
<div style="max-height:70vh;overflow-y:auto;">
<h4 style="font-size:0.9rem;margin-bottom:8px;">💳 Status Bantuan</h4>
<div id="bantuanList" style="margin-bottom:10px;"></div>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:8px;align-items:end;background:#F9FAFB;padding:10px;border-radius:8px;">
<div><label class="form-label" style="font-size:0.75rem">Jenis</label>
<select id="bJenis" class="form-control">
<?php foreach ($jenisList as $j): ?><option value="<?= $j['id'] ?>"><?= htmlspecialchars($j['kode']) ?></option><?php endforeach; ?>
</select></div>
<div><label class="form-label" style="font-size:0.75rem">Mulai</label><input id="bMulai" type="date" class="form-control"></div>
<div><label class="form-label" style="font-size:0.75rem">Berakhir</label><input id="bBerakhir" type="date" class="form-control"></div>
<button class="btn btn-primary btn-sm" onclick="addBantuan()"><i class="fas fa-plus"></i></button>
</div>
<h4 style="font-size:0.9rem;margin:16px 0 8px;">📷 Foto Kondisi Rumah (maks 5)</h4>
<div id="fotoGallery" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;"></div>
<input type="file" id="fotoInput" accept="image/*" multiple style="font-size:0.85rem;">
<button class="btn btn-primary btn-sm" onclick="uploadFoto()"><i class="fas fa-upload"></i> Unggah</button>
</div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('modalDetail')">Tutup</button></div>
</div></div>
<!-- ===== Modal Import ===== -->
<div class="modal-overlay" id="modalImport"><div class="modal">
<div class="modal-header"><span class="modal-title">📥 Import CSV</span><button class="modal-close" onclick="closeModal('modalImport')">×</button></div>
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:10px;">Format kolom (Lampiran A): <code>nik_kk, nama_kk, kode_kelurahan, latitude, longitude, jumlah_jiwa, sumber_air, status_listrik, material_dinding, pekerjaan_kk</code></p>
<a href="<?= app_url('api/warga_export.php?format=template') ?>" class="btn btn-ghost btn-sm" style="margin-bottom:12px;"><i class="fas fa-download"></i> Unduh Template CSV</a>
<div class="form-group"><input type="file" id="importFile" accept=".csv" class="form-control"></div>
<div id="importResult" style="font-size:0.85rem;"></div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('modalImport')">Batal</button><button class="btn btn-primary" onclick="doImport()"><i class="fas fa-file-import"></i> Proses</button></div>
</div></div>
<?php $extraScript = <<<'JS'
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const API = APP_BASE + '/api/warga_miskin.php';
const API_BANTUAN = APP_BASE + '/api/bantuan.php';
const API_FOTO = APP_BASE + '/api/foto.php';
let miniMap=null, marker=null, currentDetailId=null;
function ensureMap(lat=-0.0500, lng=109.3450){
setTimeout(()=>{
if(!miniMap){
miniMap=L.map('miniMap').setView([lat,lng],13);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{subdomains:'abcd'}).addTo(miniMap);
miniMap.on('click',e=>setMarker(e.latlng.lat,e.latlng.lng));
} else { miniMap.invalidateSize(); miniMap.setView([lat,lng],13); }
},120);
}
function setMarker(lat,lng){
if(!marker) marker=L.marker([lat,lng]).addTo(miniMap); else marker.setLatLng([lat,lng]);
document.getElementById('fLat').value=lat.toFixed(6);
document.getElementById('fLng').value=lng.toFixed(6);
}
function clearForm(){
['fId','fNama','fNik','fAlamat','fRt','fRw','fJiwa','fTanggungan','fPekerjaan','fPenghasilan','fLat','fLng'].forEach(i=>document.getElementById(i).value='');
['fWilayah','fAir','fListrik','fDinding','fKepemilikan'].forEach(i=>document.getElementById(i).value='');
document.getElementById('fVerif').value='belum';
if(marker&&miniMap){miniMap.removeLayer(marker);marker=null;}
}
function openTambah(){ clearForm(); document.getElementById('formTitle').textContent='Tambah Data'; openModal('modalForm'); ensureMap(); }
function editRow(r){
clearForm();
document.getElementById('formTitle').textContent='Edit: '+r.nama_kk;
document.getElementById('fId').value=r.id;
document.getElementById('fNama').value=r.nama_kk||'';
document.getElementById('fNik').value=r.nik_kk||'';
document.getElementById('fAlamat').value=r.alamat||'';
document.getElementById('fWilayah').value=r.wilayah_id||'';
document.getElementById('fRt').value=r.rt||''; document.getElementById('fRw').value=r.rw||'';
document.getElementById('fJiwa').value=r.jumlah_jiwa||''; document.getElementById('fTanggungan').value=r.jumlah_tanggungan||'';
document.getElementById('fPekerjaan').value=r.pekerjaan_kk||''; document.getElementById('fPenghasilan').value=r.penghasilan||'';
document.getElementById('fAir').value=r.sumber_air||''; document.getElementById('fListrik').value=r.status_listrik||'';
document.getElementById('fDinding').value=r.material_dinding||''; document.getElementById('fKepemilikan').value=r.status_kepemilikan||'';
document.getElementById('fVerif').value=r.status_verifikasi||'belum';
openModal('modalForm');
ensureMap();
}
function payload(){
return {
nama_kk:document.getElementById('fNama').value.trim(),
nik_kk:document.getElementById('fNik').value.trim(),
alamat:document.getElementById('fAlamat').value.trim(),
wilayah_id:document.getElementById('fWilayah').value||null,
rt:document.getElementById('fRt').value.trim(), rw:document.getElementById('fRw').value.trim(),
jumlah_jiwa:document.getElementById('fJiwa').value||null,
jumlah_tanggungan:parseInt(document.getElementById('fTanggungan').value)||0,
pekerjaan_kk:document.getElementById('fPekerjaan').value.trim(),
penghasilan:parseFloat(document.getElementById('fPenghasilan').value)||0,
sumber_air:document.getElementById('fAir').value,
status_listrik:document.getElementById('fListrik').value,
material_dinding:document.getElementById('fDinding').value,
status_kepemilikan:document.getElementById('fKepemilikan').value,
status_verifikasi:document.getElementById('fVerif').value
};
}
async function simpan(){
const id=document.getElementById('fId').value;
const p=payload();
if(!p.nama_kk) return showToast('Nama KK wajib!','error');
const lat=parseFloat(document.getElementById('fLat').value), lng=parseFloat(document.getElementById('fLng').value);
if(id){
const res=await fetch(API+'?id='+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});
const d=await res.json();
if(d.status==='success'){ showToast('Diperbarui!'); setTimeout(()=>location.reload(),800);} else showToast(d.message,'error');
} else {
if(isNaN(lat)||isNaN(lng)) return showToast('Pilih lokasi di peta!','error');
p.geometry={type:'Point',coordinates:[lng,lat]};
const res=await fetch(API,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});
const d=await res.json();
if(d.status==='success'){ showToast('Data disimpan!'); setTimeout(()=>location.reload(),800);} else showToast(d.message,'error');
}
}
async function hapus(id,nama){ if(!confirm(`Hapus data "${nama}"?`))return;
const res=await fetch(API+'?id='+id,{method:'DELETE'}); const d=await res.json();
if(d.status==='success'){showToast('Dihapus!');setTimeout(()=>location.reload(),700);}else showToast(d.message,'error'); }
// ---- Verifikasi ----
function applyVerifFilter(){
const v=document.getElementById('filterVerif').value;
document.querySelectorAll('#wargaTable tbody tr').forEach(tr=>{
tr.style.display=(!v||tr.dataset.verif===v)?'':'none';
});
}
function toggleAll(c){ document.querySelectorAll('.rowCheck').forEach(x=>x.checked=c.checked); }
async function bulkVerify(){
const ids=[...document.querySelectorAll('.rowCheck:checked')].map(x=>x.value);
if(!ids.length) return showToast('Pilih minimal satu baris','error');
if(!confirm(`Tandai ${ids.length} data sebagai Terverifikasi?`))return;
for(const id of ids){ await fetch(API+'?id='+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status_verifikasi:'terverifikasi'})}); }
showToast('Verifikasi massal selesai!'); setTimeout(()=>location.reload(),800);
}
// ---- Detail: bantuan & foto ----
async function openDetail(id,nama){
currentDetailId=id;
document.getElementById('detailTitle').textContent='Kelola: '+nama;
openModal('modalDetail');
loadBantuan(); loadFoto();
}
async function loadBantuan(){
const res=await fetch(API_BANTUAN+'?warga_id='+currentDetailId); const d=await res.json();
const box=document.getElementById('bantuanList');
if(!d.data||!d.data.length){ box.innerHTML='<p style="color:#9CA3AF;font-size:0.85rem">Belum ada bantuan.</p>'; return; }
box.innerHTML=d.data.map(b=>`<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border:1px solid #E5E7EB;border-radius:8px;margin-bottom:6px;">
<div><strong>${b.kode}</strong> <span style="font-size:0.78rem;color:#6B7280">${b.nama}</span>
<div style="font-size:0.72rem;color:#9CA3AF">${b.tgl_mulai||'-'} s/d ${b.tgl_berakhir||'-'}</div></div>
<button class="btn btn-danger btn-sm btn-icon" onclick="delBantuan(${b.id})"><i class="fas fa-trash"></i></button></div>`).join('');
}
async function addBantuan(){
const body={warga_id:currentDetailId,jenis_bantuan_id:document.getElementById('bJenis').value,tgl_mulai:document.getElementById('bMulai').value,tgl_berakhir:document.getElementById('bBerakhir').value};
const res=await fetch(API_BANTUAN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d=await res.json(); if(d.status==='success'){showToast('Bantuan ditambah');loadBantuan();}else showToast(d.message,'error');
}
async function delBantuan(id){ const res=await fetch(API_BANTUAN+'?id='+id,{method:'DELETE'}); const d=await res.json(); if(d.status==='success'){showToast('Dihapus');loadBantuan();} }
async function loadFoto(){
const res=await fetch(API_FOTO+'?warga_id='+currentDetailId); const d=await res.json();
const g=document.getElementById('fotoGallery');
if(!d.data||!d.data.length){ g.innerHTML='<p style="color:#9CA3AF;font-size:0.85rem">Belum ada foto.</p>'; return; }
g.innerHTML=d.data.map(f=>`<div style="position:relative;"><img src="${f.url}" style="width:90px;height:90px;object-fit:cover;border-radius:8px;border:1px solid #E5E7EB;">
<button onclick="delFoto(${f.id})" style="position:absolute;top:-6px;right:-6px;background:#EF4444;color:#fff;border:none;border-radius:50%;width:22px;height:22px;cursor:pointer;">×</button></div>`).join('');
}
async function uploadFoto(){
const inp=document.getElementById('fotoInput'); if(!inp.files.length) return showToast('Pilih foto dulu','error');
const fd=new FormData(); fd.append('warga_id',currentDetailId);
for(const f of inp.files) fd.append('foto[]',f);
const res=await fetch(API_FOTO,{method:'POST',body:fd}); const d=await res.json();
if(d.status==='success'){showToast(d.message);inp.value='';loadFoto();}else showToast(d.message,'error');
}
async function delFoto(id){ const res=await fetch(API_FOTO+'?id='+id,{method:'DELETE'}); const d=await res.json(); if(d.status==='success'){showToast('Foto dihapus');loadFoto();} }
// ---- Import ----
async function doImport(){
const inp=document.getElementById('importFile'); if(!inp.files.length) return showToast('Pilih file CSV','error');
const fd=new FormData(); fd.append('file',inp.files[0]);
document.getElementById('importResult').innerHTML='Memproses...';
const res=await fetch(APP_BASE+'/api/warga_import.php',{method:'POST',body:fd}); const d=await res.json();
if(d.status==='success'){
document.getElementById('importResult').innerHTML=`<div style="color:#059669">✔ ${d.data.berhasil} baris diimpor.</div>`+
(d.data.gagal?`<div style="color:#DC2626">${d.data.gagal} gagal:<br>${d.data.errors.join('<br>')}</div>`:'');
setTimeout(()=>location.reload(),1800);
} else { document.getElementById('importResult').innerHTML=`<div style="color:#DC2626">${d.message}</div>`; }
}
</script>
JS; ?>
<?php require_once __DIR__ . '/partials/footer.php'; ?>
+171
View File
@@ -0,0 +1,171 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pdo = Database::getConnection();
// KPI kemiskinan
$stats = $pdo->query("SELECT
(SELECT COUNT(*) FROM warga_miskin) AS total_warga,
(SELECT COUNT(*) FROM warga_miskin WHERE status_verifikasi='terverifikasi') AS terverifikasi,
(SELECT COUNT(*) FROM warga_miskin WHERE status_verifikasi='belum') AS belum,
(SELECT COUNT(*) FROM warga_miskin WHERE status_verifikasi='perlu_tinjauan') AS perlu_tinjauan,
(SELECT COUNT(*) FROM wilayah) AS total_wilayah,
(SELECT COUNT(DISTINCT warga_id) FROM bantuan_sosial WHERE is_aktif=1) AS warga_dibantu,
(SELECT COUNT(*) FROM users) AS total_users
")->fetch();
$total = (int)$stats['total_warga'];
$dibantu = (int)$stats['warga_dibantu'];
$cakupan = $total > 0 ? round($dibantu / $total * 100, 1) : 0;
$belumDibantu = $total - $dibantu;
// Zona prioritas: kelurahan dengan % miskin > 5%
$zona = $pdo->query("
SELECT COUNT(*) AS cnt FROM (
SELECT w.id, (COUNT(wm.id) / NULLIF(w.jumlah_penduduk,0) * 100) AS persen
FROM wilayah w LEFT JOIN warga_miskin wm ON ST_Contains(w.geom, wm.geom)
GROUP BY w.id HAVING persen > 5
) sub
")->fetch();
$zonaPrioritas = (int)($zona['cnt'] ?? 0);
// Data terbaru
$recentWarga = $pdo->query("
SELECT w.nama_kk, w.penghasilan, w.status_verifikasi, wl.nama AS kelurahan, w.created_at
FROM warga_miskin w LEFT JOIN wilayah wl ON wl.id=w.wilayah_id
ORDER BY w.id DESC LIMIT 6
")->fetchAll();
// Rekap per kelurahan
$rekapWilayah = $pdo->query("
SELECT w.nama, w.jumlah_penduduk, COUNT(wm.id) AS miskin
FROM wilayah w LEFT JOIN warga_miskin wm ON ST_Contains(w.geom, wm.geom)
GROUP BY w.id ORDER BY miskin DESC
")->fetchAll();
$pageTitle = 'Dashboard';
$activeNav = 'dashboard';
require_once __DIR__ . '/partials/header.php';
function vBadge($s) {
return match ($s) {
'terverifikasi' => '<span class="badge badge-success">Terverifikasi</span>',
'perlu_tinjauan' => '<span class="badge badge-warning">Perlu Tinjauan</span>',
default => '<span class="badge" style="background:#F3F4F6;color:#6B7280;">Belum</span>',
};
}
?>
<!-- Quick access -->
<div class="card" style="background:#EFF6FF; border:1px solid #BFDBFE; margin-bottom:24px;">
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px;">
<div>
<h3 style="font-size:1.1rem; font-weight:700; margin-bottom:6px; color:#1E3A8A;">🗺️ Peta Pemetaan Kemiskinan</h3>
<p style="color:#3B82F6; font-size:0.88rem; max-width:520px;">
Visualisasikan sebaran kemiskinan (choropleth & heatmap), kelola data rumah tangga, dan analisis zona prioritas.
</p>
</div>
<a href="<?= app_url('admin/peta.php') ?>" class="btn btn-primary"><i class="fas fa-map-marked-alt"></i> Buka Peta</a>
</div>
</div>
<!-- KPI -->
<div class="stats-grid">
<div class="stat-card primary">
<div class="stat-icon"><i class="fas fa-house-user"></i></div>
<div class="stat-body">
<div class="stat-label">Total Rumah Tangga Miskin</div>
<div class="stat-value"><?= $total ?></div>
<div class="stat-change"><?= $stats['total_wilayah'] ?> kelurahan</div>
</div>
</div>
<div class="stat-card success">
<div class="stat-icon"><i class="fas fa-check-circle"></i></div>
<div class="stat-body">
<div class="stat-label">Terverifikasi</div>
<div class="stat-value"><?= $stats['terverifikasi'] ?></div>
<div class="stat-change"><?= $stats['belum'] ?> belum diverifikasi</div>
</div>
</div>
<div class="stat-card info">
<div class="stat-icon"><i class="fas fa-hand-holding-heart"></i></div>
<div class="stat-body">
<div class="stat-label">Cakupan Bantuan</div>
<div class="stat-value"><?= $cakupan ?>%</div>
<div class="stat-change"><?= $dibantu ?> dari <?= $total ?> RT</div>
</div>
</div>
<div class="stat-card warning">
<div class="stat-icon"><i class="fas fa-user-clock"></i></div>
<div class="stat-body">
<div class="stat-label">Belum Terima Bantuan</div>
<div class="stat-value"><?= $belumDibantu ?></div>
<div class="stat-change">kandidat prioritas</div>
</div>
</div>
<div class="stat-card danger">
<div class="stat-icon"><i class="fas fa-triangle-exclamation"></i></div>
<div class="stat-body">
<div class="stat-label">Zona Prioritas</div>
<div class="stat-value"><?= $zonaPrioritas ?></div>
<div class="stat-change">kelurahan &gt; 5% miskin</div>
</div>
</div>
<div class="stat-card violet">
<div class="stat-icon"><i class="fas fa-clipboard-check"></i></div>
<div class="stat-body">
<div class="stat-label">Perlu Tinjauan</div>
<div class="stat-value"><?= $stats['perlu_tinjauan'] ?></div>
<div class="stat-change">data menunggu review</div>
</div>
</div>
</div>
<!-- Content row -->
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:20px; margin-bottom:24px;">
<!-- Rekap per kelurahan -->
<div class="card">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
<h3 style="font-size:0.95rem; font-weight:700;">📊 Rekap per Kelurahan</h3>
</div>
<?php foreach ($rekapWilayah as $w): $p = $w['jumlah_penduduk'] > 0 ? round($w['miskin']/$w['jumlah_penduduk']*100,1) : 0; ?>
<div style="margin-bottom:10px;">
<div style="display:flex;justify-content:space-between;font-size:0.85rem;margin-bottom:3px;">
<span><?= htmlspecialchars($w['nama']) ?></span>
<strong><?= $w['miskin'] ?> RT (<?= $p ?>%)</strong>
</div>
<div style="height:7px;background:#F3F4F6;border-radius:4px;overflow:hidden;">
<div style="height:100%;width:<?= min($p*8,100) ?>%;background:<?= $p>10?'#EF4444':($p>5?'#FB923C':($p>2?'#FBBF24':'#10B981')) ?>;"></div>
</div>
</div>
<?php endforeach; ?>
<?php if (!$rekapWilayah): ?><p style="color:var(--text-muted);font-size:0.85rem;">Belum ada data wilayah.</p><?php endif; ?>
</div>
<!-- Data terbaru -->
<div class="card">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
<h3 style="font-size:0.95rem; font-weight:700;">👥 Rumah Tangga Terbaru</h3>
<a href="<?= app_url('admin/data_warga.php') ?>" class="btn btn-ghost btn-sm">Lihat Semua</a>
</div>
<?php if ($recentWarga): ?>
<div style="display:flex; flex-direction:column; gap:8px;">
<?php foreach ($recentWarga as $w): ?>
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; background:#F9FAFB; border:1px solid #E5E7EB; border-radius:8px;">
<div>
<div style="font-weight:600; font-size:0.88rem;"><?= htmlspecialchars($w['nama_kk']) ?></div>
<div style="font-size:0.75rem; color:var(--text-muted);"><?= htmlspecialchars($w['kelurahan'] ?: 'Tanpa kelurahan') ?> · Rp <?= number_format($w['penghasilan'],0,',','.') ?></div>
</div>
<?= vBadge($w['status_verifikasi']) ?>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p style="color:var(--text-muted); text-align:center; padding:20px 0; font-size:0.85rem;">Belum ada data warga</p>
<?php endif; ?>
</div>
</div>
<?php require_once __DIR__ . '/partials/footer.php'; ?>
+59
View File
@@ -0,0 +1,59 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pdo = Database::getConnection();
$rows = $pdo->query(
"SELECT id, username, aksi, tabel_target, record_id, keterangan, ip_address, created_at
FROM activity_log ORDER BY id DESC LIMIT 300"
)->fetchAll();
$pageTitle = 'Log Aktivitas';
$activeNav = 'log';
require_once __DIR__ . '/partials/header.php';
function aksiBadge(string $a): string {
return match ($a) {
'login' => '<span class="badge badge-info">login</span>',
'logout' => '<span class="badge" style="background:#F3F4F6;color:#6B7280;">logout</span>',
'create' => '<span class="badge badge-success">create</span>',
'update' => '<span class="badge badge-warning">update</span>',
'delete' => '<span class="badge badge-danger">delete</span>',
default => '<span class="badge">' . htmlspecialchars($a) . '</span>',
};
}
?>
<div class="page-header">
<div class="breadcrumb"><span>Operator</span><span class="sep">/</span><span>Log Aktivitas</span></div>
<h1>📋 Log Aktivitas Sistem</h1>
<p>Audit trail aktivitas pengguna terautentikasi (KF-04). Menampilkan 300 entri terbaru.</p>
</div>
<div class="page-toolbar">
<div class="search-box">
<i class="fas fa-search search-icon"></i>
<input type="text" id="searchInput" placeholder="Cari log..." oninput="filterTable('searchInput','logTable')">
</div>
</div>
<div class="table-wrap">
<table class="data-table" id="logTable">
<thead><tr>
<th>Waktu</th><th>User</th><th>Aksi</th><th>Tabel</th><th>Record</th><th>Keterangan</th><th>IP</th>
</tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td style="font-size:0.8rem;color:var(--text-muted);white-space:nowrap;"><?= date('d M Y H:i:s', strtotime($r['created_at'])) ?></td>
<td><strong><?= htmlspecialchars($r['username'] ?: '-') ?></strong></td>
<td><?= aksiBadge($r['aksi']) ?></td>
<td style="font-size:0.82rem;"><?= htmlspecialchars($r['tabel_target'] ?: '-') ?></td>
<td style="font-size:0.82rem;color:var(--text-muted);"><?= $r['record_id'] !== null ? (int)$r['record_id'] : '-' ?></td>
<td style="font-size:0.85rem;"><?= htmlspecialchars($r['keterangan'] ?: '-') ?></td>
<td style="font-size:0.78rem;color:var(--text-muted);"><?= htmlspecialchars($r['ip_address'] ?: '-') ?></td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text-muted);">Belum ada aktivitas tercatat.</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<?php require_once __DIR__ . '/partials/footer.php'; ?>
+54
View File
@@ -0,0 +1,54 @@
</div><!-- /admin-content -->
</main><!-- /admin-main -->
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<script>
// ── Clock ──
function updateClock() {
const now = new Date();
document.getElementById('clockText').textContent =
now.toLocaleTimeString('id-ID', { hour:'2-digit', minute:'2-digit', second:'2-digit' });
}
updateClock();
setInterval(updateClock, 1000);
// ── Toast utility ──
window.showToast = function(msg, type = 'success') {
const t = document.createElement('div');
t.className = `toast ${type}`;
const icons = { success: 'check-circle', error: 'exclamation-circle', info: 'info-circle' };
t.innerHTML = `<i class="fas fa-${icons[type]||'info-circle'}" style="margin-right:8px;"></i>${msg}`;
document.getElementById('toastContainer').appendChild(t);
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, 3000);
};
// ── Modal helpers ──
window.openModal = function(id) {
document.getElementById(id)?.classList.add('active');
document.body.style.overflow = 'hidden';
};
window.closeModal = function(id) {
document.getElementById(id)?.classList.remove('active');
document.body.style.overflow = '';
};
// Close modal on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', e => {
if (e.target === overlay) closeModal(overlay.id);
});
});
// ── Search filter ──
window.filterTable = function(inputId, tableId) {
const q = document.getElementById(inputId)?.value.toLowerCase() || '';
document.querySelectorAll(`#${tableId} tbody tr`).forEach(row => {
row.style.display = row.textContent.toLowerCase().includes(q) ? '' : 'none';
});
};
</script>
<?= $extraScript ?? '' ?>
</body>
</html>
+106
View File
@@ -0,0 +1,106 @@
<?php
/**
* Admin Layout — sidebar + topbar
* Include di awal setiap halaman admin setelah auth check.
* Variables expected from parent: $pageTitle, $activeNav
*/
$user = currentUser();
$initial = strtoupper(substr($user['nama_lengkap'] ?: $user['username'], 0, 1));
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($pageTitle ?? 'Panel Operator') ?> — WebGIS Kemiskinan</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="<?= app_url('assets/css/main.css') ?>?v=20260610-spbu-snackbar">
<link rel="stylesheet" href="<?= app_url('assets/css/admin.css') ?>">
<script>const APP_BASE = '<?= app_url() ?>';</script>
<?= $extraHead ?? '' ?>
</head>
<body class="admin-page <?= $bodyClass ?? '' ?>">
<!-- ══ SIDEBAR ══ -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">🗺️</div>
<div class="brand-text">
<span class="brand-name">WebGIS</span>
<span class="brand-sub">Panel Operator</span>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-section-title">Utama</div>
<a href="<?= app_url('admin/index.php') ?>"
class="nav-item <?= ($activeNav==='dashboard') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-chart-pie"></i></span> Dashboard
</a>
<a href="<?= app_url('admin/peta.php') ?>"
class="nav-item <?= ($activeNav==='peta') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-map-marked-alt"></i></span> Peta Interaktif
</a>
<div class="nav-section-title">Data Kemiskinan</div>
<a href="<?= app_url('admin/data_warga.php') ?>"
class="nav-item <?= ($activeNav==='warga') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-users"></i></span> Data Rumah Tangga
</a>
<a href="<?= app_url('admin/wilayah.php') ?>"
class="nav-item <?= ($activeNav==='wilayah') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-draw-polygon"></i></span> Kelola Wilayah
</a>
<a href="<?= app_url('admin/analisis.php') ?>"
class="nav-item <?= ($activeNav==='analisis') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-chart-line"></i></span> Analisis Spasial
</a>
<a href="<?= app_url('admin/pelaporan.php') ?>"
class="nav-item <?= ($activeNav==='pelaporan') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-file-pdf"></i></span> Laporan &amp; Cetak
</a>
<a href="<?= app_url('admin/data_laporan.php') ?>"
class="nav-item <?= ($activeNav==='laporan') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-bullhorn"></i></span> Laporan Warga
</a>
<div class="nav-section-title">Manajemen</div>
<a href="<?= app_url('admin/users.php') ?>"
class="nav-item <?= ($activeNav==='users') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-user-shield"></i></span> Kelola Users
</a>
<a href="<?= app_url('admin/log.php') ?>"
class="nav-item <?= ($activeNav==='log') ? 'active' : '' ?>">
<span class="nav-icon"><i class="fas fa-clipboard-list"></i></span> Log Aktivitas
</a>
</nav>
<div class="sidebar-footer">
<div class="user-info">
<div class="user-avatar"><?= $initial ?></div>
<div class="user-details">
<div class="user-name"><?= htmlspecialchars($user['nama_lengkap'] ?: $user['username']) ?></div>
<div class="user-role">Operator</div>
</div>
<a href="<?= app_url('logout.php') ?>" class="user-logout" title="Keluar">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
</div>
</aside>
<!-- ══ MAIN CONTENT ══ -->
<main class="admin-main">
<div class="topbar">
<span class="topbar-title"><?= htmlspecialchars($pageTitle ?? 'Dashboard') ?></span>
<div class="topbar-actions">
<div class="topbar-time" id="clock">
<i class="fas fa-clock"></i> <span id="clockText"></span>
</div>
<a href="<?= app_url('admin/peta.php') ?>" class="btn btn-primary btn-sm">
<i class="fas fa-map"></i> Buka Peta
</a>
</div>
</div>
<div class="admin-content">
+10
View File
@@ -0,0 +1,10 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pdo = Database::getConnection();
$pageTitle = 'Laporan & Cetak';
$activeNav = 'pelaporan';
require_once __DIR__ . '/partials/header.php';
require_once __DIR__ . '/../partials/laporan_body.php';
require_once __DIR__ . '/partials/footer.php';
+31
View File
@@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pageTitle = 'Peta Interaktif';
$activeNav = 'peta';
$extraHead = '
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
';
require_once __DIR__ . '/partials/header.php';
?>
<div class="page-header" style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:12px;">
<div>
<h1>🗺️ Peta Pemetaan Kemiskinan</h1>
<p>Sebaran rumah tangga miskin, choropleth tingkat kemiskinan, heatmap kepadatan, dan fasilitas publik.</p>
</div>
<a href="<?= app_url('admin/data_warga.php') ?>" class="btn btn-primary" style="white-space:nowrap;">
<i class="fas fa-plus"></i> Kelola Data Warga</a>
</div>
<div>
<div id="skpl-map" style="height:74vh;border-radius:12px;border:1px solid #E5E7EB;z-index:1;"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
<script src="<?= app_url('assets/js/skpl-layers.js') ?>?v=2"></script>
<script>const SKPL_ROLE = 'operator';</script>
<script src="<?= app_url('assets/js/skpl-map.js') ?>?v=1"></script>
<?php $extraScript = ''; ?>
<?php require_once __DIR__ . '/partials/footer.php'; ?>
+235
View File
@@ -0,0 +1,235 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pdo = Database::getConnection();
$rows = $pdo->query(
"SELECT id, username, email, role, is_active, wilayah_tugas, nama_lengkap, last_login, created_at
FROM users ORDER BY id ASC"
)->fetchAll();
$wilayahList = $pdo->query("SELECT nama FROM wilayah ORDER BY nama")->fetchAll();
$pageTitle = 'Kelola Users';
$activeNav = 'users';
require_once __DIR__ . '/partials/header.php';
// Helper badge per role
function roleBadge(string $role): string {
return match ($role) {
'operator' => '<span class="badge badge-danger">🛠️ Operator</span>',
'pimpinan' => '<span class="badge" style="background:#EDE9FE;color:#6D28D9;">📊 Pimpinan</span>',
'enumerator' => '<span class="badge badge-info">📋 Enumerator</span>',
'publik' => '<span class="badge" style="background:#F3F4F6;color:#6B7280;">🌐 Publik</span>',
default => htmlspecialchars($role),
};
}
?>
<div class="page-header">
<div class="breadcrumb"><span>Operator</span><span class="sep">/</span><span>Users</span></div>
<h1>👤 Manajemen User Sistem</h1>
<p>Kelola akun 4 peran: Publik, Enumerator, Operator, dan Pimpinan (KF-02 / KF-03).</p>
</div>
<div class="page-toolbar">
<div class="search-box">
<i class="fas fa-search search-icon"></i>
<input type="text" id="searchInput" placeholder="Cari user..." oninput="filterTable('searchInput','usersTable')">
</div>
<button class="btn btn-primary" onclick="openModal('modalTambah')">
<i class="fas fa-plus"></i> Tambah User
</button>
</div>
<div class="table-wrap">
<table class="data-table" id="usersTable">
<thead><tr>
<th>#</th><th>Username</th><th>Nama Lengkap</th><th>Email</th><th>Role</th>
<th>Wilayah</th><th>Status</th><th>Login Terakhir</th><th>Aksi</th>
</tr></thead>
<tbody>
<?php foreach ($rows as $i => $r): ?>
<tr>
<td style="color:var(--text-muted)"><?= $i+1 ?></td>
<td><strong><?= htmlspecialchars($r['username']) ?></strong></td>
<td><?= htmlspecialchars($r['nama_lengkap'] ?: '-') ?></td>
<td style="font-size:0.82rem;color:var(--text-muted)"><?= htmlspecialchars($r['email'] ?: '-') ?></td>
<td><?= roleBadge($r['role']) ?></td>
<td style="font-size:0.82rem"><?= htmlspecialchars($r['wilayah_tugas'] ?: '-') ?></td>
<td>
<?php if ((int)$r['is_active'] === 1): ?>
<span class="badge badge-success">Aktif</span>
<?php else: ?>
<span class="badge badge-danger">Nonaktif</span>
<?php endif; ?>
</td>
<td style="color:var(--text-muted); font-size:0.78rem;">
<?= $r['last_login'] ? date('d M Y H:i', strtotime($r['last_login'])) : 'Belum pernah' ?>
</td>
<td><div class="actions">
<button class="btn btn-ghost btn-sm btn-icon" onclick='editRow(<?= json_encode($r, JSON_HEX_APOS|JSON_HEX_QUOT) ?>)' title="Edit">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-danger btn-sm btn-icon" onclick="hapus(<?= $r['id'] ?>,'<?= addslashes($r['username']) ?>')" title="Hapus">
<i class="fas fa-trash"></i>
</button>
</div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Modal Tambah -->
<div class="modal-overlay" id="modalTambah">
<div class="modal">
<div class="modal-header">
<span class="modal-title">👤 Tambah User Baru</span>
<button class="modal-close" onclick="closeModal('modalTambah')">×</button>
</div>
<form id="formTambah">
<div class="form-group"><label class="form-label">Username *</label><input type="text" id="tUsername" class="form-control" placeholder="username unik" required></div>
<div class="form-group"><label class="form-label">Nama Lengkap</label><input type="text" id="tNama" class="form-control" placeholder="Nama lengkap"></div>
<div class="form-group"><label class="form-label">Email</label><input type="email" id="tEmail" class="form-control" placeholder="email@contoh.id"></div>
<div class="form-group"><label class="form-label">Role *</label>
<select id="tRole" class="form-control" onchange="toggleWilayah('t')">
<option value="enumerator">📋 Enumerator</option>
<option value="operator">🛠️ Operator</option>
<option value="pimpinan">📊 Pimpinan</option>
<option value="publik">🌐 Publik</option>
</select>
</div>
<div class="form-group" id="tWilayahGroup"><label class="form-label">Wilayah Tugas</label>
<select id="tWilayah" class="form-control"><option value="">- pilih kelurahan -</option>
<?php foreach ($wilayahList as $w): ?><option value="<?= htmlspecialchars($w['nama']) ?>"><?= htmlspecialchars($w['nama']) ?></option><?php endforeach; ?>
</select></div>
<div class="form-group"><label class="form-label">Password *</label><input type="password" id="tPassword" class="form-control" placeholder="Minimal 6 karakter" required minlength="6"></div>
</form>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('modalTambah')">Batal</button>
<button class="btn btn-primary" onclick="simpan()"><i class="fas fa-save"></i> Simpan</button>
</div>
</div>
</div>
<!-- Modal Edit -->
<div class="modal-overlay" id="modalEdit">
<div class="modal">
<div class="modal-header">
<span class="modal-title">✏️ Edit User</span>
<button class="modal-close" onclick="closeModal('modalEdit')">×</button>
</div>
<input type="hidden" id="editId">
<div class="form-group"><label class="form-label">Username *</label><input type="text" id="editUsername" class="form-control" required></div>
<div class="form-group"><label class="form-label">Nama Lengkap</label><input type="text" id="editNama" class="form-control"></div>
<div class="form-group"><label class="form-label">Email</label><input type="email" id="editEmail" class="form-control"></div>
<div class="form-group"><label class="form-label">Role *</label>
<select id="editRole" class="form-control" onchange="toggleWilayah('edit')">
<option value="enumerator">📋 Enumerator</option>
<option value="operator">🛠️ Operator</option>
<option value="pimpinan">📊 Pimpinan</option>
<option value="publik">🌐 Publik</option>
</select>
</div>
<div class="form-group" id="editWilayahGroup"><label class="form-label">Wilayah Tugas</label>
<select id="editWilayah" class="form-control"><option value="">- pilih kelurahan -</option>
<?php foreach ($wilayahList as $w): ?><option value="<?= htmlspecialchars($w['nama']) ?>"><?= htmlspecialchars($w['nama']) ?></option><?php endforeach; ?>
</select></div>
<div class="form-group"><label class="form-label">Status</label>
<select id="editActive" class="form-control">
<option value="1">Aktif</option>
<option value="0">Nonaktif</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Password Baru</label>
<input type="password" id="editPassword" class="form-control" placeholder="Kosongkan jika tidak diubah" minlength="6">
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('modalEdit')">Batal</button>
<button class="btn btn-primary" onclick="update()"><i class="fas fa-save"></i> Perbarui</button>
</div>
</div>
</div>
<?php $extraScript = <<<'JS'
<script>
const API = APP_BASE + '/api/users.php';
// Wilayah tugas hanya relevan untuk enumerator
function toggleWilayah(prefix) {
const role = document.getElementById(prefix + 'Role').value;
const group = document.getElementById(prefix + 'WilayahGroup');
group.style.display = (role === 'enumerator') ? 'block' : 'none';
}
function editRow(r) {
document.getElementById('editId').value = r.id;
document.getElementById('editUsername').value = r.username;
document.getElementById('editNama').value = r.nama_lengkap || '';
document.getElementById('editEmail').value = r.email || '';
document.getElementById('editRole').value = r.role;
document.getElementById('editWilayah').value = r.wilayah_tugas || '';
document.getElementById('editActive').value = String(r.is_active);
document.getElementById('editPassword').value = '';
toggleWilayah('edit');
openModal('modalEdit');
}
async function simpan() {
const u = document.getElementById('tUsername').value.trim();
const p = document.getElementById('tPassword').value;
if (!u) return showToast('Username wajib!', 'error');
if (p.length < 6) return showToast('Password minimal 6 karakter!', 'error');
const res = await fetch(API, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: u,
nama_lengkap: document.getElementById('tNama').value,
email: document.getElementById('tEmail').value,
role: document.getElementById('tRole').value,
wilayah_tugas: document.getElementById('tWilayah').value,
password: p
})
});
const d = await res.json();
if (d.status === 'success') { showToast('User berhasil ditambahkan!'); setTimeout(() => location.reload(), 900); }
else showToast(d.message || 'Gagal', 'error');
}
async function update() {
const id = document.getElementById('editId').value;
const u = document.getElementById('editUsername').value.trim();
const p = document.getElementById('editPassword').value;
if (!u) return showToast('Username wajib!', 'error');
if (p && p.length < 6) return showToast('Password minimal 6 karakter!', 'error');
const body = {
username: u,
nama_lengkap: document.getElementById('editNama').value,
email: document.getElementById('editEmail').value,
role: document.getElementById('editRole').value,
wilayah_tugas: document.getElementById('editWilayah').value,
is_active: document.getElementById('editActive').value
};
if (p) body.password = p;
const res = await fetch(API + '?id=' + id, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const d = await res.json();
if (d.status === 'success') { showToast('User diperbarui!'); setTimeout(() => location.reload(), 900); }
else showToast(d.message || 'Gagal', 'error');
}
async function hapus(id, username) {
if (id == 1) return showToast('Akun utama (ID=1) tidak dapat dihapus!', 'error');
if (!confirm(`Hapus user "${username}"?`)) return;
const res = await fetch(API + '?id=' + id, { method: 'DELETE' });
const d = await res.json();
if (d.status === 'success') { showToast('User dihapus!'); setTimeout(() => location.reload(), 900); }
else showToast(d.message || 'Gagal', 'error');
}
toggleWilayah('t');
</script>
JS; ?>
<?php require_once __DIR__ . '/partials/footer.php'; ?>
+160
View File
@@ -0,0 +1,160 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pageTitle = 'Kelola Wilayah';
$activeNav = 'wilayah';
$extraHead = '
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css">
';
require_once __DIR__ . '/partials/header.php';
?>
<div class="page-header" style="margin-bottom:12px;">
<div class="breadcrumb"><span>Operator</span><span class="sep">/</span><span>Kelola Wilayah</span></div>
<h1>🗺️ Kelola Batas Wilayah (Kelurahan)</h1>
<p>Gambar polygon batas kelurahan di peta, ubah bentuk/ukuran (geser titik), atur jumlah penduduk, atau hapus.</p>
</div>
<div style="display:grid;grid-template-columns:1.7fr 1fr;gap:16px;">
<div style="position:relative;">
<div id="wil-map" style="height:72vh;border-radius:12px;border:1px solid #E5E7EB;z-index:1;"></div>
<div style="position:absolute;bottom:12px;left:12px;z-index:500;background:#fff;padding:8px 12px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.15);font-size:0.78rem;max-width:260px;">
<b>Cara pakai:</b> klik ikon ⬠ (kiri atas peta) untuk gambar polygon baru. Ikon ✏️ untuk geser titik/ubah ukuran, 🗑️ untuk hapus. Klik polygon untuk edit atribut.
</div>
</div>
<div class="card">
<h3 id="formTitle" style="font-size:0.95rem;margin-bottom:10px;">Atribut Wilayah</h3>
<input type="hidden" id="wId">
<div class="form-group"><label class="form-label">Nama Kelurahan *</label><input id="wNama" class="form-control" placeholder="mis. Kel. Sungai Jawi"></div>
<div class="form-group"><label class="form-label">Kode Wilayah (BPS)</label><input id="wKode" class="form-control" placeholder="mis. 6171010005"></div>
<div class="form-group"><label class="form-label">Jenis</label>
<select id="wJenis" class="form-control"><option value="kelurahan">Kelurahan</option><option value="kecamatan">Kecamatan</option></select></div>
<div class="form-group"><label class="form-label">Jumlah Penduduk (KK)</label><input id="wPenduduk" type="number" min="0" class="form-control" placeholder="0"></div>
<div id="geomNote" style="font-size:0.78rem;color:#9CA3AF;margin-bottom:10px;">Belum ada polygon. Gambar di peta dulu untuk wilayah baru.</div>
<button class="btn btn-primary" style="width:100%;" onclick="simpanWilayah()"><i class="fas fa-save"></i> Simpan</button>
<button class="btn btn-ghost" style="width:100%;margin-top:6px;" onclick="resetForm()">Bersihkan Form</button>
<h3 style="font-size:0.95rem;margin:16px 0 8px;">Daftar Wilayah</h3>
<div id="wList" style="font-size:0.85rem;"></div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script>
const WAPI = APP_BASE + '/api/wilayah.php';
let map, drawn, pendingLayer = null;
const layerById = {};
function colorMiskin(p){ return p>=10?'#EF4444':p>=5?'#FB923C':p>=2?'#FBBF24':p>0?'#A3E635':'#2ECC71'; }
function initMap(){
map = L.map('wil-map').setView([-0.0500,109.3450],13);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{subdomains:'abcd',maxZoom:19}).addTo(map);
drawn = new L.FeatureGroup().addTo(map);
const drawControl = new L.Control.Draw({
edit: { featureGroup: drawn, remove: true },
draw: { polygon:{allowIntersection:false,showArea:true}, marker:false, polyline:false, circle:false, circlemarker:false, rectangle:false }
});
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, e => {
pendingLayer = e.layer;
drawn.addLayer(pendingLayer);
resetForm(); // mode wilayah baru
document.getElementById('formTitle').textContent = 'Wilayah Baru (polygon tergambar)';
document.getElementById('geomNote').innerHTML = '<span style="color:#059669">✔ Polygon siap. Isi atribut lalu Simpan.</span>';
});
map.on(L.Draw.Event.EDITED, async e => {
for (const layer of Object.values(e.layers._layers)) {
const id = layer.wilayahId; if(!id) continue;
const p = layer.props || {};
await fetch(`${WAPI}?id=${id}`, {method:'PUT',headers:{'Content-Type':'application/json'},
body:JSON.stringify({nama:p.nama,kode_wilayah:p.kode_wilayah,jenis:p.jenis,jumlah_penduduk:p.jumlah_penduduk,geometry:layer.toGeoJSON().geometry})});
}
showToast('Bentuk wilayah diperbarui'); loadWilayah();
});
map.on(L.Draw.Event.DELETED, async e => {
for (const layer of Object.values(e.layers._layers)) {
if (layer.wilayahId) await fetch(`${WAPI}?id=${layer.wilayahId}`, {method:'DELETE'});
}
showToast('Wilayah dihapus'); loadWilayah();
});
}
async function loadWilayah(){
Object.keys(layerById).forEach(k=>delete layerById[k]);
drawn.clearLayers(); pendingLayer=null;
const d = (await (await fetch(WAPI)).json()).data;
(d.features||[]).forEach(f=>{
const layer = L.geoJSON(f,{style:{color:'#374151',weight:1.5,fillColor:colorMiskin(f.properties.persentase_miskin),fillOpacity:.45}});
layer.eachLayer(l=>{
l.wilayahId = f.properties.id; l.props = f.properties;
l.bindTooltip(f.properties.nama,{sticky:true});
l.on('click',()=>editAttr(f.properties));
drawn.addLayer(l); layerById[f.properties.id]=l;
});
});
// daftar
document.getElementById('wList').innerHTML = (d.features||[]).map(f=>{const p=f.properties;
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid #F3F4F6;">
<div><b>${p.nama}</b><div style="font-size:0.72rem;color:#9CA3AF;">${p.jumlah_penduduk} KK · ${p.jumlah_miskin} miskin · ${p.persentase_miskin}%</div></div>
<div style="display:flex;gap:4px;">
<button class="btn btn-ghost btn-sm btn-icon" title="Edit atribut" onclick='editAttr(${JSON.stringify(p)})'><i class="fas fa-pen"></i></button>
<button class="btn btn-ghost btn-sm btn-icon" title="Zoom" onclick="zoomTo(${p.id})"><i class="fas fa-magnifying-glass"></i></button>
<button class="btn btn-danger btn-sm btn-icon" title="Hapus" onclick="hapusWilayah(${p.id},'${p.nama.replace(/'/g,'')}')"><i class="fas fa-trash"></i></button>
</div></div>`;}).join('') || '<p style="color:#9CA3AF">Belum ada wilayah.</p>';
}
function zoomTo(id){ const l=layerById[id]; if(l) map.fitBounds(l.getBounds(),{padding:[40,40]}); }
function editAttr(p){
document.getElementById('formTitle').textContent = 'Edit Atribut: '+p.nama;
document.getElementById('wId').value = p.id;
document.getElementById('wNama').value = p.nama||'';
document.getElementById('wKode').value = p.kode_wilayah||'';
document.getElementById('wJenis').value = p.jenis||'kelurahan';
document.getElementById('wPenduduk').value = p.jumlah_penduduk||0;
document.getElementById('geomNote').innerHTML = 'Mengubah atribut wilayah ini (bentuk diubah lewat tool ✏️ di peta).';
pendingLayer = null;
zoomTo(p.id);
}
function resetForm(){
document.getElementById('formTitle').textContent='Atribut Wilayah';
['wId','wNama','wKode','wPenduduk'].forEach(i=>document.getElementById(i).value='');
document.getElementById('wJenis').value='kelurahan';
document.getElementById('geomNote').innerHTML='Belum ada polygon. Gambar di peta dulu untuk wilayah baru.';
}
async function simpanWilayah(){
const nama=document.getElementById('wNama').value.trim();
if(!nama) return showToast('Nama kelurahan wajib','error');
const body={ nama, kode_wilayah:document.getElementById('wKode').value.trim(), jenis:document.getElementById('wJenis').value, jumlah_penduduk:parseInt(document.getElementById('wPenduduk').value)||0 };
const id=document.getElementById('wId').value;
if(pendingLayer && !id){
body.geometry = pendingLayer.toGeoJSON().geometry;
const r=await (await fetch(WAPI,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
if(r.status==='success'){ showToast('Wilayah baru disimpan'); loadWilayah(); resetForm(); } else showToast(r.message,'error');
} else if(id){
const r=await (await fetch(`${WAPI}?id=${id}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
if(r.status==='success'){ showToast('Atribut diperbarui'); loadWilayah(); } else showToast(r.message,'error');
} else {
showToast('Gambar polygon dulu di peta untuk wilayah baru','error');
}
}
async function hapusWilayah(id,nama){
if(!confirm(`Hapus wilayah "${nama}"?`))return;
const r=await (await fetch(`${WAPI}?id=${id}`,{method:'DELETE'})).json();
if(r.status==='success'){ showToast('Dihapus'); loadWilayah(); resetForm(); } else showToast(r.message,'error');
}
document.addEventListener('DOMContentLoaded',()=>{ initMap(); loadWilayah(); });
</script>
<?php $extraScript=''; ?>
<?php require_once __DIR__ . '/partials/footer.php'; ?>