first commit

This commit is contained in:
2026-09-16 10:38:01 +07:00
commit c20afe6543
10 changed files with 3307 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
// ═══════════════════════════════════════════════════════════════════
// 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);
}