248 lines
13 KiB
JavaScript
248 lines
13 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════
|
|
// PROGRAMS PAGE — Detail per program with stats and province breakdown
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
let _activeProgTab = 'mbg';
|
|
|
|
function renderProgramsPage() {
|
|
renderProgTabs();
|
|
renderProgBody(_activeProgTab);
|
|
}
|
|
|
|
function renderProgTabs() {
|
|
document.getElementById('prog-tabs').innerHTML = PROGRAMS.map(p => `
|
|
<button class="prog-tab ${p.id === _activeProgTab ? 'active' : ''}"
|
|
onclick="switchProgTab('${p.id}')">
|
|
${p.icon} ${p.shortName}
|
|
</button>
|
|
`).join('');
|
|
}
|
|
|
|
function switchProgTab(id) {
|
|
_activeProgTab = id;
|
|
renderProgTabs();
|
|
renderProgBody(id);
|
|
}
|
|
|
|
function renderProgBody(pid) {
|
|
const p = getProgramById(pid);
|
|
if (!p) return;
|
|
const pct = Math.round((p.capaian / p.target) * 100);
|
|
const budgetPct = Math.round((p.realisasi / p.budget) * 100);
|
|
const activeIssues = APP_STATE.issues.filter(i => i.programId === pid && i.status !== 'resolved' && i.status !== 'closed');
|
|
const critCount = activeIssues.filter(i => i.severity === 'critical').length;
|
|
const highCount = activeIssues.filter(i => i.severity === 'high').length;
|
|
|
|
// Location data for this program
|
|
const progLocs = APP_STATE.locs.filter(l => l.programs.includes(pid));
|
|
|
|
// Generate province stats
|
|
const provinceData = {};
|
|
progLocs.forEach(l => {
|
|
const prov = l.province;
|
|
if (!provinceData[prov]) provinceData[prov] = { locs: 0, issues: 0 };
|
|
provinceData[prov].locs++;
|
|
provinceData[prov].issues += l.issueCount;
|
|
});
|
|
|
|
const provinceRows = Object.entries(provinceData)
|
|
.sort((a,b) => b[1].issues - a[1].issues)
|
|
.slice(0, 10)
|
|
.map(([prov, data]) => {
|
|
const locList = progLocs.filter(l => l.province === prov);
|
|
let prog = 90; // default coverage
|
|
if (pid === 'mbg') prog = Math.round(locList.reduce((s,l) => s + (l.mbg?.coverage || 0), 0) / locList.length);
|
|
else if (pid === 'koperasi') prog = 75;
|
|
return `
|
|
<tr>
|
|
<td><span style="font-weight:500;">${prov}</span></td>
|
|
<td>${data.locs} titik</td>
|
|
<td>
|
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
<div class="progress-bar" style="width:80px;"><div class="progress-fill" style="width:${prog}%;background:${p.color};"></div></div>
|
|
<span style="font-size:12px;color:${p.color};font-weight:600;">${prog}%</span>
|
|
</div>
|
|
</td>
|
|
<td>${data.issues > 0 ? `<span style="color:var(--red);font-weight:600;">${data.issues} issue</span>` : '<span style="color:var(--green);">✓</span>'}</td>
|
|
<td>
|
|
<button class="btn btn-xs btn-secondary" onclick="filterIssuesByProvince('${prov}','${pid}')">Lihat Issue</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
document.getElementById('prog-bodies').innerHTML = `
|
|
<div class="prog-body active" style="flex:1;overflow:hidden;">
|
|
<div class="prog-scroll">
|
|
<!-- Header -->
|
|
<div class="card card-p mb-4" style="background:linear-gradient(135deg, ${p.color}15, transparent);border-color:${p.color}33;">
|
|
<div style="display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap;">
|
|
<div style="font-size:42px;line-height:1;">${p.icon}</div>
|
|
<div style="flex:1;min-width:200px;">
|
|
<div style="font-size:18px;font-weight:800;color:var(--text-1);margin-bottom:4px;">${p.name}</div>
|
|
<div style="font-size:12px;color:var(--text-3);margin-bottom:12px;">📋 ${p.ministry} · Mulai ${p.startDate}</div>
|
|
<div style="font-size:13px;color:var(--text-2);line-height:1.6;">${p.description}</div>
|
|
</div>
|
|
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;">
|
|
<div style="font-size:36px;font-weight:800;color:${p.color};">${pct}%</div>
|
|
<div style="font-size:11px;color:var(--text-3);">Capaian Target</div>
|
|
<span class="badge badge-${p.badge}">🟢 Aktif</span>
|
|
</div>
|
|
</div>
|
|
<div class="progress-bar progress-lg mt-3">
|
|
<div class="progress-fill" style="width:${pct}%;background:${p.color};"></div>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;margin-top:6px;">
|
|
<div style="font-size:11px;color:var(--text-3);">Realisasi: ${p.capaian.toLocaleString()} ${p.targetUnit}</div>
|
|
<div style="font-size:11px;color:var(--text-3);">Target: ${p.target.toLocaleString()} ${p.targetUnit}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- KPI Grid -->
|
|
<div class="prog-stat-grid mb-4">
|
|
${p.kpiLabel.map((lbl, i) => `
|
|
<div class="prog-stat-card">
|
|
<div class="prog-stat-val" style="color:${p.color};">${p.kpiValue[i]}</div>
|
|
<div class="prog-stat-lbl">${lbl}</div>
|
|
</div>
|
|
`).join('')}
|
|
<div class="prog-stat-card">
|
|
<div class="prog-stat-val">Rp ${p.realisasi.toLocaleString()}M</div>
|
|
<div class="prog-stat-lbl">Realisasi Anggaran</div>
|
|
</div>
|
|
<div class="prog-stat-card">
|
|
<div class="prog-stat-val">${budgetPct}%</div>
|
|
<div class="prog-stat-lbl">Serapan Anggaran</div>
|
|
</div>
|
|
<div class="prog-stat-card">
|
|
<div class="prog-stat-val" style="color:${activeIssues.length > 0 ? 'var(--red)' : 'var(--green)'};">${activeIssues.length}</div>
|
|
<div class="prog-stat-lbl">Issue Aktif</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Issues summary -->
|
|
${activeIssues.length > 0 ? `
|
|
<div class="card card-p mb-4" style="border-color:rgba(239,68,68,0.2);">
|
|
<div class="flex justify-between items-center mb-3">
|
|
<div style="font-size:13px;font-weight:600;color:var(--text-1);">⚠️ Issue Aktif (${activeIssues.length})</div>
|
|
<button class="btn btn-sm btn-secondary" onclick="filterIssuesByProgram('${pid}')">Lihat Semua</button>
|
|
</div>
|
|
${activeIssues.slice(0,3).map(i => `
|
|
<div class="alert-item ${i.severity}" onclick="openIssueDetail('${i.id}')">
|
|
<div class="flex gap-2 items-center mb-1">${getSevBadge(i.severity)} ${getStatBadge(i.status)}</div>
|
|
<div class="alert-title">${i.judul}</div>
|
|
<div class="alert-meta"><span>📍 ${i.daerah}, ${i.provinsi}</span></div>
|
|
</div>
|
|
`).join('')}
|
|
</div>` : ''}
|
|
|
|
<!-- Province Breakdown Table -->
|
|
<div class="card mb-4">
|
|
<div style="padding:14px 16px 10px;border-bottom:1px solid var(--border);font-size:13px;font-weight:600;color:var(--text-1);">
|
|
Distribusi per Provinsi (${progLocs.length} titik pemantauan)
|
|
</div>
|
|
<div class="table-wrap" style="border:none;border-radius:0;">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Provinsi</th>
|
|
<th>Titik Aktif</th>
|
|
<th>Capaian</th>
|
|
<th>Status Issue</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${provinceRows || '<tr><td colspan="5" style="text-align:center;padding:20px;color:var(--text-3);">Tidak ada data</td></tr>'}</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Location cards -->
|
|
<div style="font-size:13px;font-weight:600;color:var(--text-1);margin-bottom:12px;">
|
|
Lokasi Pemantauan (${progLocs.length} titik)
|
|
</div>
|
|
${progLocs.length === 0
|
|
? `<div class="empty-state card" style="padding:32px;"><div class="empty-icon">📍</div><div class="empty-title">Belum ada titik pemantauan</div><div class="empty-desc">Data lokasi untuk program ${p.shortName} belum tersedia</div></div>`
|
|
: `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;margin-bottom:20px;">
|
|
${progLocs.slice(0, 12).map(loc => {
|
|
const d = loc[pid];
|
|
const hasIssue = loc.issueCount > 0;
|
|
const relIssue = APP_STATE.issues.find(i => i.daerah === loc.name && i.programId === pid && i.status !== 'resolved');
|
|
const onclickAction = (hasIssue && relIssue)
|
|
? `openIssueDetail('${relIssue.id}')`
|
|
: `showToast('${loc.name.replace(/'/g,"\\'")}', '${loc.province.replace(/'/g,"\\'")} — kondisi normal', 'info')`;
|
|
return `
|
|
<div class="card card-p-sm" style="${hasIssue ? 'border-color:rgba(239,68,68,0.3);' : ''}cursor:pointer;" onclick="${onclickAction}">
|
|
<div class="flex justify-between items-center mb-1">
|
|
<span style="font-size:12px;font-weight:600;color:var(--text-1);">${loc.name}</span>
|
|
${hasIssue ? '<span style="color:var(--red);font-size:11px;">⚠ Issue</span>' : '<span style="color:var(--green);font-size:11px;">✓</span>'}
|
|
</div>
|
|
<div style="font-size:10px;color:var(--text-3);margin-bottom:6px;">${loc.province}</div>
|
|
${pid === 'mbg' && d ? `
|
|
<div class="stat-pair" style="padding:4px 0;">
|
|
<span class="stat-key text-xs">Porsi/Hari</span>
|
|
<span class="stat-val text-xs">${(d.meals||0).toLocaleString()}</span>
|
|
</div>
|
|
<div style="margin-top:4px;"><div class="progress-bar progress-sm"><div class="progress-fill" style="width:${d.coverage||0}%;background:${p.color};"></div></div></div>
|
|
` : ''}
|
|
${pid === 'koperasi' && d ? `
|
|
<div class="stat-pair" style="padding:4px 0;">
|
|
<span class="stat-key text-xs">Anggota</span>
|
|
<span class="stat-val text-xs">${(d.members||0).toLocaleString()}</span>
|
|
</div>
|
|
<span class="badge badge-${d.status === 'active' ? 'green' : d.status === 'inactive' ? 'red' : 'amber'}" style="font-size:10px;">${d.status}</span>
|
|
` : ''}
|
|
${pid === 'psn' && d ? `
|
|
<div class="stat-pair" style="padding:4px 0;">
|
|
<span class="stat-key text-xs" title="${d.projectName||''}">${(d.projectName||'—').slice(0,22)}…</span>
|
|
<span class="stat-val text-xs" style="color:${p.color};">${d.progress}%</span>
|
|
</div>
|
|
<div style="margin-top:4px;"><div class="progress-bar progress-sm"><div class="progress-fill" style="width:${d.progress}%;background:${p.color};"></div></div></div>
|
|
` : ''}
|
|
${pid === 'hilirisasi' && d ? `
|
|
<div class="stat-pair" style="padding:4px 0;">
|
|
<span class="stat-key text-xs">${d.commodity||'—'}</span>
|
|
<span class="stat-val text-xs" style="color:${p.color};">${d.progress}%</span>
|
|
</div>
|
|
<div style="font-size:10px;color:var(--text-3);margin-top:2px;">${d.stage||''}</div>
|
|
<div style="margin-top:4px;"><div class="progress-bar progress-sm"><div class="progress-fill" style="width:${d.progress}%;background:${p.color};"></div></div></div>
|
|
` : ''}
|
|
${pid === 'rumah' && d ? `
|
|
<div class="stat-pair" style="padding:4px 0;">
|
|
<span class="stat-key text-xs">Unit Selesai</span>
|
|
<span class="stat-val text-xs">${(d.completed||0).toLocaleString()}</span>
|
|
</div>
|
|
<div style="font-size:10px;color:var(--text-3);margin-top:2px;">Target: ${(d.units||0).toLocaleString()} unit</div>
|
|
<div style="margin-top:4px;"><div class="progress-bar progress-sm"><div class="progress-fill" style="width:${d.progress||0}%;background:${p.color};"></div></div></div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>`
|
|
}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function filterIssuesByProgram(pid) {
|
|
APP_STATE.issueFilter.program = pid;
|
|
navigateTo('issues');
|
|
setTimeout(() => {
|
|
const sel = document.getElementById('issues-filter-program');
|
|
if (sel) { sel.value = pid; applyIssueFilters(); }
|
|
}, 100);
|
|
}
|
|
|
|
function filterIssuesByProvince(prov, pid) {
|
|
APP_STATE.issueFilter.program = pid;
|
|
navigateTo('issues');
|
|
setTimeout(() => {
|
|
const sel = document.getElementById('issues-filter-program');
|
|
const search = document.getElementById('issues-search');
|
|
if (sel) sel.value = pid;
|
|
if (search) search.value = prov;
|
|
applyIssueFilters();
|
|
}, 100);
|
|
}
|