Gabungkan aplikasi RPS & Portofolio OBE (dashboard jadi bagian aplikasi)

Dashboard pemetaan statis kini dilayani aplikasi di /dashboard
(app/static/dashboard.html). Fitur lengkap Tahap 1-4 + dasbor monitoring,
menu global, filter semester, input nilai langsung, penggabungan nilai
team teaching, tabel bisa urut. Dockerfile berubah dari nginx statis ke
uvicorn/FastAPI: healthcheck, proxy-headers, volume /data (SQLite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Power BI Dev
2026-07-18 12:05:28 +07:00
parent 3405dc21ff
commit 9be6bd03e3
42 changed files with 11366 additions and 53 deletions
+140
View File
@@ -0,0 +1,140 @@
{% extends "base.html" %}
{% block isi %}
<div class="kpi-baris" title="Persentase dihitung dari MK pada semester terpilih — hijau ≥ 75 %, kuning 4074 %, merah < 40 %">
<div><b id="kpi-mk">{{ daftar|length }}</b> <span>mata kuliah</span></div>
<div><b id="kpi-rps">{{ n_selesai }}</b> <span>RPS selesai</span> <span class="kpi-pct" id="pct-rps"></span></div>
<div><b id="kpi-draft">{{ n_draft }}</b> <span>RPS draft</span></div>
<div><b id="kpi-nilai">{{ st_nilai|length }}</b> <span>MK setor nilai</span> <span class="kpi-pct" id="pct-nilai"></span></div>
<div><b id="kpi-porto">{{ st_porto.values()|selectattr('status', 'equalto', 'selesai')|list|length }}</b> <span>portofolio selesai</span> <span class="kpi-pct" id="pct-porto"></span></div>
</div>
{% if user.role == 'dosen' %}
{% if hanya_saya %}
<div class="info" style="margin-top:0">Menampilkan <b>{{ daftar|length }} MK yang Anda ampu</b>.
RPS mata kuliah lain tetap terbuka baca — <a href="/?semua=1">tampilkan seluruh {{ n_total }} MK</a>.</div>
{% elif mk_saya %}
<div class="info" style="margin-top:0">Menampilkan seluruh {{ n_total }} MK —
<a href="/">kembali ke MK yang Anda ampu saja</a>.</div>
{% else %}
<div class="peringatan" style="margin-top:0">Belum ada MK yang ditugaskan kepada
Anda — hubungi kaprodi agar menetapkan penugasan di halaman Kelola Akun.
Sementara itu seluruh MK ditampilkan (hanya-baca).</div>
{% endif %}
{% endif %}
<div class="filter-baris">
<label for="filter-smt">Tampilkan semester:</label>
<select id="filter-smt" style="width:auto">
<option value="">Semua</option>
<option value="ganjil">Ganjil (1, 3, 5, 7)</option>
<option value="genap">Genap (2, 4, 6, 8)</option>
{% for s in smt.values()|select|unique|sort %}
<option value="{{ s }}">Semester {{ s }}</option>
{% endfor %}
</select>
<span id="filter-info" class="keterangan" style="margin:0"></span>
</div>
{% macro stempel(st) -%}
{% if st and st.status == 'selesai' %}<span class="stamp stamp-ok">SELESAI</span>
{% elif st %}<span class="stamp stamp-draft">DRAFT</span>
{% else %}<span class="stamp stamp-no">BELUM</span>{% endif %}
{%- endmacro %}
<div class="gulir">
<table class="grid">
<thead>
<tr>
<th style="width:56px">No.</th>
<th style="width:112px">Kode</th>
<th class="tengah" style="width:52px">Smt</th>
<th>Mata Kuliah</th>
<th style="width:150px">Pengampu</th>
<th style="width:120px">CPL Dipikul</th>
<th class="tengah" style="width:104px">RPS</th>
<th class="tengah" style="width:104px">Nilai</th>
<th class="tengah" style="width:104px">Portofolio</th>
<th style="width:170px">Pemutakhiran Terakhir</th>
</tr>
</thead>
<tbody>
{% for mk in daftar %}
{% set st = status.get(mk.kode) %}
{% set sn = st_nilai.get(mk.kode) %}
{% set sp = st_porto.get(mk.kode) %}
<tr data-smt="{{ smt.get(mk.kode) or '' }}"
data-rps="{{ st.status if st else '' }}"
data-nilai="{{ sn.status if sn else '' }}"
data-porto="{{ sp.status if sp else '' }}">
<td class="tengah">{{ mk.no }}</td>
<td>{{ mk.kode }}</td>
<td class="tengah">{{ smt.get(mk.kode) or '—' }}</td>
<td>{{ mk.nama }}<br>
<span style="font-size:12px;color:var(--grey)">{{ mk.n_cpmk }} CPMK · {{ mk.n_sub }} Sub-CPMK</span>
</td>
<td style="font-size:13px">{{ pengampu.get(mk.kode, [])|join(', ') or '—' }}</td>
<td>{{ mk.cpls|map('replace','CPL-','')|join(', ') }}</td>
<td class="tengah"><a href="/rps/{{ mk.kode }}" style="text-decoration:none">{{ stempel(st) }}</a></td>
<td class="tengah"><a href="/nilai/{{ mk.kode }}" style="text-decoration:none">{{ stempel(sn) }}</a></td>
<td class="tengah"><a href="/portofolio/{{ mk.kode }}" style="text-decoration:none">{{ stempel(sp) }}</a></td>
<td style="font-size:12.5px">
{% set terakhir = [st, sn, sp]|select|sort(attribute='updated_at')|last %}
{% if terakhir %}{{ terakhir.updated_at }}{% if terakhir.updated_by %} — {{ terakhir.updated_by }}{% endif %}{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function () {
var pilih = document.getElementById('filter-smt');
var info = document.getElementById('filter-info');
var baris = document.querySelectorAll('table.grid tbody tr');
function cocok(tr, v) {
var smt = tr.dataset.smt;
return v === '' ? true :
v === 'ganjil' ? smt !== '' && smt % 2 === 1 :
v === 'genap' ? smt !== '' && smt % 2 === 0 :
smt === v;
}
function isiPct(id, n, total) {
var el = document.getElementById(id);
if (!total) { el.textContent = ''; el.className = 'kpi-pct'; return; }
var p = Math.round(100 * n / total);
el.textContent = p + ' %';
el.className = 'kpi-pct ' +
(p >= 75 ? 'kpi-hijau' : p >= 40 ? 'kpi-kuning' : 'kpi-merah');
}
function perbarui() {
var v = pilih.value, n = 0, rSel = 0, rDraft = 0, nilai = 0, porto = 0;
baris.forEach(function (tr) {
var tampil = cocok(tr, v);
tr.style.display = tampil ? '' : 'none';
if (!tampil) return;
n++;
if (tr.dataset.rps === 'selesai') rSel++;
if (tr.dataset.rps === 'draft') rDraft++;
if (tr.dataset.nilai !== '') nilai++;
if (tr.dataset.porto === 'selesai') porto++;
});
document.getElementById('kpi-mk').textContent = n;
document.getElementById('kpi-rps').textContent = rSel;
document.getElementById('kpi-draft').textContent = rDraft;
document.getElementById('kpi-nilai').textContent = nilai;
document.getElementById('kpi-porto').textContent = porto;
isiPct('pct-rps', rSel, n);
isiPct('pct-nilai', nilai, n);
isiPct('pct-porto', porto, n);
info.textContent = v === '' ? '' : n + ' dari ' + baris.length + ' MK ditampilkan';
}
pilih.addEventListener('change', perbarui);
perbarui();
})();
</script>
<p class="keterangan" style="margin-top:10px">Klik stempel untuk membuka form terkait. Alur per MK: <b>RPS</b> (rencana &amp; bobot) → <b>Nilai</b> (unggah Excel / rata-rata) → <b>Portofolio</b> (refleksi &amp; dokumen akhir). Rekap prodi menggulung otomatis dari nilai yang masuk.</p>
{% endblock %}