Files
2026-06-09 19:02:22 +07:00

475 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>