First commit

This commit is contained in:
2026-06-09 19:02:22 +07:00
commit c2c0774b3b
36 changed files with 10493 additions and 0 deletions
+474
View File
@@ -0,0 +1,474 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Dashboard Eksekutif Walikota WebGIS Poverty Mapping Kota Pontianak">
<title>Dashboard Walikota WebGIS Poverty Mapping</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #0a0e1a;
--bg-panel: #0f1422;
--bg-card: #141928;
--bg-card-hover: #1a2035;
--border: rgba(255,255,255,.07);
--accent-blue: #3b82f6;
--accent-green: #10b981;
--accent-orange: #f59e0b;
--accent-red: #ef4444;
--accent-purple: #8b5cf6;
--text: #e8edf5;
--text-sub: #8a96b0;
--text-muted: #4b5563;
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg-dark);
color: var(--text);
min-height: 100vh;
}
/* Header */
.header {
background: linear-gradient(135deg, #0f1422 0%, #0a0e1a 100%);
border-bottom: 1px solid var(--border);
padding: 16px 32px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header-left { display: flex; align-items: center; gap: 14px; }
.header-left h1 { font-size: 18px; font-weight: 800; }
.header-left span.tag {
font-size: 10px; font-weight: 700; padding: 3px 10px;
background: rgba(249,115,22,.15); border: 1px solid rgba(249,115,22,.3);
color: #fb923c; border-radius: 20px; text-transform: uppercase; letter-spacing: .5px;
}
.header-right { display: flex; align-items: center; gap: 14px; }
.user-info-hdr { text-align: right; }
.user-info-hdr .name { font-size: 13px; font-weight: 700; }
.user-info-hdr .role { font-size: 10px; color: #fb923c; font-weight: 600; }
.btn-back, .btn-logout-d {
padding: 7px 14px; border-radius: 8px; font-size: 11px; font-weight: 600;
cursor: pointer; border: none; font-family: 'Inter', sans-serif; transition: all .2s;
}
.btn-back {
background: rgba(59,130,246,.12); border: 1px solid rgba(59,130,246,.3); color: #60a5fa;
text-decoration: none;
}
.btn-back:hover { background: rgba(59,130,246,.22); }
.btn-logout-d {
background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.3); color: #f87171;
}
.btn-logout-d:hover { background: rgba(239,68,68,.22); }
/* Main Grid */
.dashboard {
max-width: 1400px;
margin: 0 auto;
padding: 24px 32px 48px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* KPI Cards Row */
.kpi-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
}
.kpi-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
position: relative;
overflow: hidden;
}
.kpi-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
}
.kpi-card:nth-child(1)::before { background: var(--accent-red); }
.kpi-card:nth-child(2)::before { background: var(--accent-orange); }
.kpi-card:nth-child(3)::before { background: var(--accent-green); }
.kpi-card:nth-child(4)::before { background: var(--accent-purple); }
.kpi-card:nth-child(5)::before { background: var(--accent-blue); }
.kpi-icon { font-size: 28px; margin-bottom: 10px; }
.kpi-value { font-size: 32px; font-weight: 800; line-height: 1; }
.kpi-label { font-size: 11px; color: var(--text-sub); margin-top: 6px; font-weight: 500; }
.kpi-sub { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
/* Progress Bar */
.progress-bar {
width: 100%; height: 6px; background: rgba(255,255,255,.06);
border-radius: 3px; margin-top: 12px; overflow: hidden;
}
.progress-fill {
height: 100%; border-radius: 3px; transition: width .6s ease;
}
/* Two column layout */
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
/* Sections */
.section {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.section-head {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.section-head h2 {
font-size: 14px; font-weight: 700;
}
.section-body {
padding: 16px 20px;
}
/* Kategori bars */
.cat-row {
display: flex; align-items: center; gap: 12px;
padding: 10px 0; border-bottom: 1px solid var(--border);
}
.cat-row:last-child { border-bottom: none; }
.cat-label { width: 120px; font-size: 12px; font-weight: 600; }
.cat-bar-wrap { flex: 1; }
.cat-bar {
height: 20px; border-radius: 4px; position: relative; overflow: hidden;
background: rgba(255,255,255,.04);
}
.cat-fill {
height: 100%; border-radius: 4px; transition: width .6s; display: flex;
align-items: center; padding-left: 8px; font-size: 10px; font-weight: 700;
}
.cat-count { width: 50px; text-align: right; font-size: 13px; font-weight: 700; }
/* Laporan list */
.laporan-item {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 12px;
}
.laporan-item:last-child { border-bottom: none; }
.laporan-info { flex: 1; }
.laporan-nama { font-weight: 600; margin-bottom: 2px; }
.laporan-desc { color: var(--text-sub); font-size: 11px; }
.status-badge {
font-size: 10px; font-weight: 700; padding: 3px 8px;
border-radius: 4px; white-space: nowrap;
}
/* Map */
#mini-map {
width: 100%;
height: 350px;
border-radius: 0 0 var(--radius) var(--radius);
}
/* Penyakit Table */
.simple-table { width: 100%; border-collapse: collapse; }
.simple-table th, .simple-table td {
padding: 8px 12px; text-align: left; font-size: 12px;
border-bottom: 1px solid var(--border);
}
.simple-table th {
color: var(--text-muted); font-weight: 600; text-transform: uppercase;
font-size: 10px; letter-spacing: .5px;
}
/* Export buttons */
.btn-export-d {
padding: 6px 14px; border-radius: 6px; font-size: 11px; font-weight: 600;
cursor: pointer; border: none; font-family: 'Inter', sans-serif;
background: rgba(59,130,246,.12); border: 1px solid rgba(59,130,246,.3);
color: #60a5fa; transition: all .2s;
}
.btn-export-d:hover { background: rgba(59,130,246,.22); }
/* Loading */
.loading { text-align: center; padding: 30px; color: var(--text-muted); font-size: 12px; }
/* Scrollbar */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.section-body-scroll { max-height: 320px; overflow-y: auto; }
@media (max-width: 1024px) {
.kpi-row { grid-template-columns: repeat(3, 1fr); }
.two-col { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.kpi-row { grid-template-columns: repeat(2, 1fr); }
.dashboard { padding: 16px; }
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<div class="header-left">
<span style="font-size:24px">👑</span>
<h1>Dashboard Eksekutif</h1>
<span class="tag">Walikota Pontianak</span>
</div>
<div class="header-right">
<div class="user-info-hdr">
<div class="name" id="hdr-name"></div>
<div class="role">👑 Walikota</div>
</div>
<a href="index.html" class="btn-back">🗺️ Lihat Peta</a>
<button class="btn-logout-d" onclick="logout()">🚪 Keluar</button>
</div>
</div>
<!-- Dashboard Content -->
<div class="dashboard">
<!-- KPI Row -->
<div class="kpi-row">
<div class="kpi-card">
<div class="kpi-icon">🏚️</div>
<div class="kpi-value" id="kpi-total" style="color:var(--accent-red)"></div>
<div class="kpi-label">Total Warga Miskin</div>
<div class="kpi-sub" id="kpi-total-sub">Memuat...</div>
</div>
<div class="kpi-card">
<div class="kpi-icon"></div>
<div class="kpi-value" id="kpi-sudah" style="color:var(--accent-green)"></div>
<div class="kpi-label">Sudah Dibantu</div>
<div class="progress-bar"><div class="progress-fill" id="prog-sudah" style="width:0%; background:var(--accent-green)"></div></div>
<div class="kpi-sub" id="kpi-sudah-sub">0%</div>
</div>
<div class="kpi-card">
<div class="kpi-icon"></div>
<div class="kpi-value" id="kpi-belum" style="color:var(--accent-orange)"></div>
<div class="kpi-label">Belum Dibantu</div>
<div class="kpi-sub" id="kpi-belum-sub">Memuat...</div>
</div>
<div class="kpi-card">
<div class="kpi-icon">🕌</div>
<div class="kpi-value" id="kpi-ibadah" style="color:var(--accent-purple)"></div>
<div class="kpi-label">Rumah Ibadah Aktif</div>
<div class="kpi-sub" id="kpi-ibadah-sub">Memuat...</div>
</div>
<div class="kpi-card">
<div class="kpi-icon">🎁</div>
<div class="kpi-value" id="kpi-bantuan" style="color:var(--accent-blue)"></div>
<div class="kpi-label">Total Bantuan Tercatat</div>
<div class="kpi-sub">Seluruh riwayat</div>
</div>
</div>
<!-- Distribusi Kategori + Laporan Terbaru -->
<div class="two-col">
<!-- Distribusi Kategori -->
<div class="section">
<div class="section-head">
<h2>📊 Distribusi Kategori Kemiskinan</h2>
<button class="btn-export-d" onclick="exportCSV()">📥 Export CSV</button>
</div>
<div class="section-body" id="distribusi-body">
<div class="loading">⏳ Memuat data...</div>
</div>
</div>
<!-- Laporan Terbaru -->
<div class="section">
<div class="section-head">
<h2>📢 Laporan Masyarakat Terbaru</h2>
</div>
<div class="section-body section-body-scroll" id="laporan-body">
<div class="loading">⏳ Memuat data...</div>
</div>
</div>
</div>
<!-- Peta Heatmap + Riwayat Penyakit -->
<div class="two-col">
<!-- Peta Heatmap -->
<div class="section">
<div class="section-head">
<h2>🔥 Heatmap Distribusi Kemiskinan</h2>
</div>
<div id="mini-map"></div>
</div>
<!-- Penyakit Terbanyak -->
<div class="section">
<div class="section-head">
<h2>🏥 Riwayat Penyakit Terbanyak</h2>
</div>
<div class="section-body" id="penyakit-body">
<div class="loading">⏳ Memuat data...</div>
</div>
</div>
</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>
// ── Auth Check ──
const currentUser = JSON.parse(sessionStorage.getItem('webgis_user') || 'null');
if (!currentUser || currentUser.role !== 'walikota') {
// Bukan walikota → redirect
window.location.replace('login.html');
}
document.getElementById('hdr-name').textContent = currentUser.nama || currentUser.username || 'Walikota';
function logout() {
if (!confirm('Yakin ingin keluar?')) return;
sessionStorage.removeItem('webgis_user');
window.location.replace('login.html?logout=1');
}
const API = 'api_dashboard.php';
// ── Load Stats ──
fetch(`${API}?action=stats`).then(r => r.json()).then(stats => {
// KPI
document.getElementById('kpi-total').textContent = stats.total_warga;
document.getElementById('kpi-total-sub').textContent = `Data terverifikasi`;
document.getElementById('kpi-sudah').textContent = stats.total_sudah;
document.getElementById('kpi-sudah-sub').textContent = `${stats.persen_sudah}% dari total`;
document.getElementById('prog-sudah').style.width = `${stats.persen_sudah}%`;
document.getElementById('kpi-belum').textContent = stats.total_belum;
document.getElementById('kpi-belum-sub').textContent = `Perlu perhatian`;
document.getElementById('kpi-ibadah').textContent = stats.total_ibadah;
const ibadahTypes = stats.rumah_ibadah.map(i => `${i.jenis}: ${i.total}`).join(', ');
document.getElementById('kpi-ibadah-sub').textContent = ibadahTypes || 'Belum ada data';
document.getElementById('kpi-bantuan').textContent = stats.total_bantuan;
// Distribusi Kategori
const distBody = document.getElementById('distribusi-body');
const cats = [
{ label: '🚨 Sangat Miskin', color: '#ef4444', key: 'Sangat Miskin' },
{ label: '⚠️ Miskin', color: '#f97316', key: 'Miskin' },
{ label: '💡 Hampir Miskin', color: '#eab308', key: 'Hampir Miskin' }
];
let distHTML = '';
cats.forEach(c => {
const found = stats.kategori.find(k => k.kategori === c.key);
const total = found ? found.total : 0;
const sudah = found ? found.sudah_dibantu : 0;
const belum = found ? found.belum_dibantu : 0;
const pct = stats.total_warga > 0 ? ((total / stats.total_warga) * 100).toFixed(1) : 0;
distHTML += `<div class="cat-row">
<div class="cat-label" style="color:${c.color}">${c.label}</div>
<div class="cat-bar-wrap">
<div class="cat-bar">
<div class="cat-fill" style="width:${pct}%; background:${c.color}; color:#fff; min-width:40px">${pct}%</div>
</div>
<div style="font-size:10px; color:var(--text-muted); margin-top:3px">Sudah: ${sudah} | Belum: ${belum}</div>
</div>
<div class="cat-count" style="color:${c.color}">${total}</div>
</div>`;
});
distBody.innerHTML = distHTML;
// Penyakit
const pBody = document.getElementById('penyakit-body');
if (stats.penyakit_terbanyak.length === 0) {
pBody.innerHTML = '<div class="loading">Belum ada data riwayat penyakit</div>';
} else {
let tbl = '<table class="simple-table"><thead><tr><th>Penyakit</th><th>Jumlah Warga</th></tr></thead><tbody>';
stats.penyakit_terbanyak.forEach((p, i) => {
tbl += `<tr><td style="font-weight:600">${p.riwayat_penyakit}</td><td style="color:var(--accent-red);font-weight:700">${p.total}</td></tr>`;
});
tbl += '</tbody></table>';
pBody.innerHTML = tbl;
}
}).catch(err => {
console.error('Gagal memuat statistik:', err);
});
// ── Load Laporan Terbaru ──
fetch(`${API}?action=laporan_terbaru&limit=15`).then(r => r.json()).then(data => {
const body = document.getElementById('laporan-body');
if (!data.length) {
body.innerHTML = '<div class="loading">Belum ada laporan masuk</div>';
return;
}
let html = '';
const statusColors = { 'Pending': '#eab308', 'Diproses': '#3b82f6', 'Diverifikasi': '#10b981', 'Ditolak': '#ef4444', 'Selesai': '#10b981' };
data.forEach(d => {
const sc = statusColors[d.status] || '#888';
html += `<div class="laporan-item">
<div class="laporan-info">
<div class="laporan-nama">${d.nama_pelapor}</div>
<div class="laporan-desc">${d.deskripsi.substring(0, 60)}${d.deskripsi.length > 60 ? '...' : ''}</div>
</div>
<span class="status-badge" style="background:${sc}22;color:${sc};border:1px solid ${sc}">${d.status}</span>
</div>`;
});
body.innerHTML = html;
});
// ── Mini Map + Heatmap ──
const miniMap = L.map('mini-map', { zoomControl: false }).setView([-0.0553, 109.3495], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OSM', maxZoom: 18
}).addTo(miniMap);
fetch(`${API}?action=heatmap`).then(r => r.json()).then(points => {
if (points.length) {
L.heatLayer(points, {
radius: 25, blur: 18, maxZoom: 15,
gradient: { 0.2: '#2563eb', 0.4: '#f59e0b', 0.6: '#f97316', 0.8: '#ef4444', 1: '#dc2626' }
}).addTo(miniMap);
}
});
// ── Export CSV ──
function exportCSV() {
fetch(`${API}?action=kemiskinan`).then(r => r.json()).then(data => {
if (!data.length) { alert('Tidak ada data'); return; }
const headers = ['ID','Nama KK','NIK','Kategori','Status Bantuan','Jumlah KK','Alamat','Pendidikan','Riwayat Penyakit','Lat','Lng'];
const rows = data.map(d => [
d.id, d.nama_kk, d.nik || '', d.kategori || '', d.status_bantuan || 'Belum',
d.jumlah_kk, '"' + (d.alamat || '').replace(/"/g, '""') + '"',
d.pendidikan || '', d.riwayat_penyakit || '',
d.latitude, d.longitude
]);
let csv = '\uFEFF' + headers.join(',') + '\n';
rows.forEach(r => csv += r.join(',') + '\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `laporan_kemiskinan_walikota_${new Date().toISOString().slice(0,10)}.csv`;
a.click();
URL.revokeObjectURL(url);
});
}
</script>
</body>
</html>