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
+189
View File
@@ -0,0 +1,189 @@
{% extends "base.html" %}
{% block judul %}Nilai {{ peta.nama }} — Informatika UNTAN{% endblock %}
{% block isi %}
<div class="nav-baris">
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <b>Input Nilai</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
</div>
<h1>Input Nilai — {{ peta.nama }} <span style="color:var(--grey)">({{ peta.kode }})</span></h1>
<p class="keterangan">Apa pun cara inputnya, pengukuran memakai <b>satu metodologi
yang sama</b>: skor per Sub-CPMK digulung ke CPMK lalu CPL
(rincian di halaman <a href="/metodologi">Metodologi</a>). Pilih <b>satu</b> cara
di bawah — menyimpan lewat cara mana pun menggantikan data nilai MK ini.</p>
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
{% for w in peringatan %}<div class="peringatan">{{ w }}</div>{% endfor %}
{% if tersimpan %}
<div class="info">
Data tersimpan: mode
<b>{% if tersimpan.data.mode == 'per_mahasiswa' %}PER MAHASISWA ({{ tersimpan.data.mahasiswa|length }} mahasiswa){% else %}BUKTI AGREGAT (rata-rata kelas){% endif %}</b>
— terakhir disimpan {{ tersimpan.updated_at }}{% if tersimpan.updated_by %} oleh {{ tersimpan.updated_by }}{% endif %}.
{% if tersimpan.data.sumber %}<br>Riwayat unggahan:
{{ tersimpan.data.sumber if tersimpan.data.sumber is string else tersimpan.data.sumber|join(' · ') }}.{% endif %}
<form method="post" action="/nilai/{{ peta.kode }}" style="display:inline;margin-left:8px"
onsubmit="return confirm('Hapus SELURUH nilai tersimpan MK ini untuk {{ tahun }}? Tindakan ini tidak bisa dibatalkan.')">
<button class="tombol garis" type="submit" name="aksi" value="hapus"
style="color:var(--red);border-color:var(--red);padding:2px 10px;font-size:13px">Mulai Ulang (hapus semua nilai)</button>
</form>
</div>
{% endif %}
{% if grid %}
{% set per_mhs = tersimpan and tersimpan.data.mode == 'per_mahasiswa' %}
<h2 class="bagian">NILAI PER MAHASISWA <span style="font-weight:normal;font-style:italic;color:var(--grey)">(ketik langsung di sel, 0100; kolom Nilai Akhir &amp; baris rata-rata dihitung dari data tersimpan)</span></h2>
<p class="keterangan">
Daftar mahasiswa dari impor kaprodi. Kosongkan sel yang belum dinilai;
<b>Simpan Nilai</b> menyimpan persis isi tabel. Nilai Akhir = tertimbang bobot
komponen RPS; angkanya diperbarui setelah disimpan.
</p>
<form method="post" action="/nilai/{{ peta.kode }}">
<div class="gulir" style="max-height:480px;overflow-y:auto">
{% set ada_kelas = grid|selectattr('kelas')|list|length > 0 %}
{% set n_kiri = 4 if ada_kelas else 3 %}
<table class="grid">
<thead>
<tr>
<th style="width:44px" class="tengah">No.</th>
<th style="width:112px">NPM</th>
<th style="min-width:150px">Nama</th>
{% if ada_kelas %}<th style="width:60px" class="tengah">Kelas</th>{% endif %}
{% for s in peta.subs %}<th class="tengah" style="font-size:11px;min-width:62px">{{ s.kode }}</th>{% endfor %}
<th class="tengah" style="width:80px">Nilai Akhir</th>
</tr>
</thead>
<tbody>
{% for m in grid %}
{% set i = loop.index0 %}
<tr>
<td class="tengah">{{ loop.index }}
<input type="hidden" name="npm_{{ i }}" value="{{ m.npm }}">
<input type="hidden" name="nama_{{ i }}" value="{{ m.nama }}"></td>
<td>{{ m.npm }}</td>
<td>{{ m.nama }}</td>
{% if ada_kelas %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
{% for s in peta.subs %}
<td class="isian"><input type="number" name="skor_{{ i }}_{{ s.kode }}"
min="0" max="100" step="0.01"
value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}"></td>
{% endfor %}
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(m.npm)) if akhir.get(m.npm) is not none else '—' }}</b></td>
</tr>
{% endfor %}
{% if per_mhs and hasil %}
<tr class="total">
<td colspan="{{ n_kiri }}" style="text-align:right">Rata-rata kelas (tersimpan)</td>
{% for s in peta.subs %}
{% set info = hasil.sub.get(s.kode) %}
<td class="tengah">{{ '%.1f'|format(info.rata) if info else '—' }}</td>
{% endfor %}
<td class="tengah">{{ '%.1f'|format(akhir.values()|sum / akhir|length) if akhir else '—' }}</td>
</tr>
<tr class="total">
<td colspan="{{ n_kiri }}" style="text-align:right">% lulus (≥ {{ ambang|round(0)|int }})</td>
{% for s in peta.subs %}
{% set info = hasil.sub.get(s.kode) %}
<td class="tengah" {% if info and info.lulus_pct is not none and info.rata < ambang %}style="color:var(--red)"{% endif %}>{{ '%.0f'|format(info.lulus_pct) ~ '%' if info and info.lulus_pct is not none else '—' }}</td>
{% endfor %}
<td></td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="aksi-baris">
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
<span class="spacer"></span>
<button class="tombol" type="submit" name="aksi" value="grid">Simpan Nilai</button>
</div>
</form>
{% endif %}
<details class="cara-alt"{% if not grid %} open{% endif %}>
<summary>{{ 'ALTERNATIF — ' if grid }}UNGGAH EXCEL PER MAHASISWA <span style="font-weight:normal;font-style:italic;color:var(--grey)">(cocok bila nilai sudah direkap di Excel, atau team teaching mengisi terpisah)</span></summary>
<p class="keterangan">
Unduh template berkolom Sub-CPMK MK ini, isi nilai 0100 per mahasiswa, lalu unggah kembali.
{% if terdaftar %}Template sudah berisi <b>{{ terdaftar|length }} mahasiswa</b> yang
mengontrak MK ini (dari daftar impor kaprodi).
{% else %}<b>Daftar mahasiswa MK ini belum diimpor kaprodi</b> — template kosong,
isi NPM &amp; nama secara manual.{% endif %}
<br><b>Team teaching:</b> tiap dosen cukup mengunggah bagiannya sendiri — kolom
Sub-CPMK yang diampunya (mis. sebelum/sesudah UTS) atau baris mahasiswa kelasnya.
Unggahan <b>digabung</b>: sel terisi memperbarui nilai, sel kosong tidak menghapus
nilai yang sudah diunggah dosen lain.
</p>
<div class="aksi-baris" style="border-top:none;margin-top:0;padding-top:0">
<a class="tombol garis" href="/nilai/{{ peta.kode }}/template.xlsx">Unduh Template Excel</a>
</div>
<form method="post" action="/nilai/{{ peta.kode }}" enctype="multipart/form-data">
<div class="aksi-baris" style="border-top:none">
<input type="file" name="berkas" accept=".xlsx" style="width:auto">
<button class="tombol" type="submit" name="aksi" value="unggah">Unggah &amp; Hitung</button>
</div>
</form>
</details>
<details class="cara-alt"{% if tersimpan and tersimpan.data.mode == 'agregat' %} open{% endif %}>
<summary>ALTERNATIF — KETIK RATA-RATA KELAS PER SUB-CPMK <span style="font-weight:normal;font-style:italic;color:var(--grey)">(hanya bila nilai per mahasiswa tidak tersedia; ditandai "bukti agregat" pada rekap)</span></summary>
<form method="post" action="/nilai/{{ peta.kode }}">
<div class="gulir" style="max-height:420px;overflow-y:auto">
<table class="grid">
<thead><tr><th style="width:120px">Sub-CPMK</th><th>Uraian</th><th class="tengah" style="width:130px">Rata-rata (0100)</th></tr></thead>
<tbody>
{% for s in peta.subs %}
<tr>
<td class="tengah">{{ s.kode }}</td>
<td>{{ s.uraian[:110] }}{% if s.uraian|length > 110 %}…{% endif %}</td>
<td class="isian"><input type="number" name="rata_{{ s.kode }}" min="0" max="100" step="0.01"
value="{% if tersimpan and tersimpan.data.mode == 'agregat' %}{{ tersimpan.data.agregat.get(s.kode, '') }}{% endif %}"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="aksi-baris">
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
<span class="spacer"></span>
<button class="tombol garis" type="submit" name="aksi" value="agregat">Simpan Rata-rata</button>
</div>
</form>
</details>
{% if hasil %}
<h2 class="bagian">HASIL KETERCAPAIAN MK INI <span style="font-weight:normal;font-style:italic;color:var(--grey)">(ambang {{ ambang|round(0)|int }}; roll-up bagi rata)</span></h2>
<div class="gulir">
<table class="grid">
<thead><tr><th>CPMK</th><th class="tengah">Skor</th><th class="tengah">Sub Ternilai</th><th>CPL Didukung</th></tr></thead>
<tbody>
{% for kode_c, v in hasil.cpmk.items() %}
<tr>
<td>{{ kode_c }}</td>
<td class="tengah">{% if v.skor is not none %}<b>{{ '%.2f'|format(v.skor) }}</b>{% else %}—{% endif %}</td>
<td class="tengah">{{ v.n_ternilai }}/{{ v.n_sub }}</td>
<td>{{ v.cpl|join(', ') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="gulir" style="margin-top:10px">
<table class="grid">
<thead><tr><th>CPL (dalam MK ini)</th><th class="tengah">Skor</th><th class="tengah">Status</th></tr></thead>
<tbody>
{% for kode_c, v in hasil.cpl.items() %}
<tr>
<td>{{ kode_c }}</td>
<td class="tengah">{% if v.skor is not none %}<b>{{ '%.2f'|format(v.skor) }}</b>{% else %}—{% endif %}</td>
<td class="tengah">
{% if v.skor is none %}<span class="stamp stamp-draft">BELUM TERUKUR</span>
{% elif v.skor >= ambang %}<span class="stamp stamp-ok">TERCAPAI</span>
{% else %}<span class="stamp stamp-no">DI BAWAH AMBANG</span>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}