475 lines
20 KiB
HTML
475 lines
20 KiB
HTML
<!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>
|