Files
OBE-Mapping/app/templates/pantau.html
T
Power BI Dev 9be6bd03e3 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>
2026-07-18 12:05:28 +07:00

193 lines
12 KiB
HTML

{% extends "base.html" %}
{% block judul %}Dasbor Monitoring — Informatika UNTAN{% endblock %}
{% block isi %}
<h1>Dasbor Monitoring Prodi — {{ hasil.tahun }}</h1>
<div class="kpi-baris">
<div><b>{{ hasil.n_mk_setor }}/{{ n_mk }}</b> <span>MK setor nilai</span></div>
<div><b>{{ cpl_urut|selectattr('tercapai')|list|length }}/{{ cpl_urut|length }}</b> <span>CPL tercapai</span></div>
<div><b>{{ cpmk_urut|rejectattr('skor', 'none')|list|length }}/{{ cpmk_urut|length }}</b> <span>CPMK terukur</span></div>
<div><b>{% if rata_cpl is not none %}{{ '%.1f'|format(rata_cpl) }}{% else %}—{% endif %}</b> <span>rata-rata skor CPL</span></div>
<div><b>{{ hasil.ambang|round(0)|int }}</b> <span>ambang ketercapaian</span></div>
</div>
{% if hasil.n_mk_setor == 0 %}
<div class="info">Belum ada MK yang menyetor nilai pada {{ hasil.tahun }} —
grafik ketercapaian akan terisi begitu nilai pertama masuk. Pantau dulu
kelengkapan proses dokumen dan daftar tindak lanjut di bawah.</div>
{% endif %}
{# ── A. Ketercapaian CPL (bar vertikal, 10 CPL) ─────────────────────── #}
<h2 class="bagian">KETERCAPAIAN CPL — SEMESTER BERJALAN</h2>
<p class="keterangan">Skor gabungan tiap CPL vs ambang {{ hasil.ambang|round(0)|int }}.
Arahkan kursor ke batang untuk rincian kelengkapan bukti &amp; penyumbang terlemah.
Tabel rinci: <a href="/rekap">Rekap</a>.</p>
<div class="legenda">
<span><i class="chip" style="background:var(--blue)"></i> tercapai (&ge; ambang)</span>
<span><i class="chip" style="background:var(--red)"></i> di bawah ambang</span>
<span><i class="chip chip-kosong"></i> belum terukur</span>
<span><i class="chip-garis"></i> garis ambang</span>
</div>
<div class="gulir">
<svg viewBox="0 0 760 252" class="grafik" role="img" aria-label="Grafik batang skor tiap CPL dibandingkan ambang">
<defs><clipPath id="plot-cpl"><rect x="46" y="10" width="706" height="206"/></clipPath></defs>
{% for g in [0, 25, 50, 75, 100] %}
<line x1="46" y1="{{ 216 - g * 2 }}" x2="752" y2="{{ 216 - g * 2 }}" stroke="{{ '#bbb' if g == 0 else '#e4e4e4' }}" stroke-width="1"/>
<text x="40" y="{{ 220 - g * 2 }}" class="sumbu" text-anchor="end">{{ g }}</text>
{% endfor %}
<line x1="46" y1="{{ '%.1f'|format(216 - hasil.ambang * 2) }}" x2="752" y2="{{ '%.1f'|format(216 - hasil.ambang * 2) }}"
stroke="var(--red)" stroke-width="1.5" stroke-dasharray="6 4"/>
{% set lebar_slot = 706 / cpl_urut|length %}
{% for c in cpl_urut %}
{% set x_slot = 46 + loop.index0 * lebar_slot %}
{% set x_bar = x_slot + (lebar_slot - 30) / 2 %}
<g class="bar">
<title>{{ c.kode }} — {% if c.skor is not none %}skor {{ '%.2f'|format(c.skor) }} ({{ 'tercapai' if c.tercapai else 'di bawah ambang' }})
bukti: {{ c.n_mk_setor }}/{{ c.n_mk_pemikul }} titik ukur{% if c.ada_agregat %} (ada bukti agregat){% endif %}
{% if c.mk_terlemah %}terlemah: {{ c.cpmk_terlemah }} — {{ nama_mk.get(c.mk_terlemah[0], c.mk_terlemah[0]) }} ({{ '%.1f'|format(c.mk_terlemah[1]) }}){% endif %}{% else %}belum terukur — belum ada MK pemikulnya yang setor nilai{% endif %}</title>
<rect x="{{ '%.1f'|format(x_slot) }}" y="10" width="{{ '%.1f'|format(lebar_slot) }}" height="206" fill="transparent"/>
{% if c.skor is not none %}
<rect clip-path="url(#plot-cpl)" x="{{ '%.1f'|format(x_bar) }}" y="{{ '%.1f'|format(216 - c.skor * 2) }}"
width="30" height="{{ '%.1f'|format(c.skor * 2 + 6) }}" rx="4"
fill="{{ 'var(--blue)' if c.tercapai else 'var(--red)' }}"/>
<text x="{{ '%.1f'|format(x_bar + 15) }}" y="{{ '%.1f'|format(211 - c.skor * 2) }}" class="nilai-bar" text-anchor="middle">{{ '%.1f'|format(c.skor) }}</text>
{% else %}
<rect x="{{ '%.1f'|format(x_bar) }}" y="204" width="30" height="12" rx="4"
fill="none" stroke="var(--grey)" stroke-dasharray="3 3"/>
{% endif %}
<text x="{{ '%.1f'|format(x_slot + lebar_slot / 2) }}" y="232" class="sumbu{{ ' pudar' if c.skor is none }}" text-anchor="middle">{{ c.kode }}</text>
<text x="{{ '%.1f'|format(x_slot + lebar_slot / 2) }}" y="246" class="sumbu pudar" text-anchor="middle">{{ c.n_mk_setor }}/{{ c.n_mk_pemikul }} bukti</text>
</g>
{% endfor %}
</svg>
</div>
{# ── B. Kelengkapan proses dokumen (stacked bar per tahap) ──────────── #}
<h2 class="bagian">KELENGKAPAN PROSES DOKUMEN ({{ n_mk }} MK)</h2>
<p class="keterangan">Alur per MK: RPS → Nilai → Portofolio. Batang menunjukkan berapa MK yang sudah menuntaskan tiap tahap.</p>
<div class="legenda">
<span><i class="chip" style="background:var(--blue)"></i> selesai</span>
<span><i class="chip" style="background:var(--blue-fill);border:1px solid #9db8cf"></i> draft</span>
<span><i class="chip chip-kosong"></i> belum</span>
</div>
{% for p in proses %}
<div class="proses-baris">
<span class="proses-label">{{ p.label }}</span>
<span class="proses-jalur">
{% if p.selesai %}<span class="seg" style="width:{{ '%.2f'|format(100 * p.selesai / n_mk) }}%;background:var(--blue);color:#fff" title="{{ p.selesai }} MK selesai {{ p.label }}">{{ p.selesai }}</span>{% endif %}
{% if p.draft %}<span class="seg" style="width:{{ '%.2f'|format(100 * p.draft / n_mk) }}%;background:var(--blue-fill)" title="{{ p.draft }} MK masih draft">{{ p.draft }}</span>{% endif %}
{% if p.belum %}<span class="seg seg-kosong" style="width:{{ '%.2f'|format(100 * p.belum / n_mk) }}%" title="{{ p.belum }} MK belum mulai">{{ p.belum }}</span>{% endif %}
</span>
</div>
{% endfor %}
{# ── C. Ketercapaian CPMK (bar horizontal, tampil bila ada data) ────── #}
{% set cpmk_ternilai = cpmk_urut|rejectattr('skor', 'none')|list %}
{% if cpmk_ternilai %}
<h2 class="bagian">KETERCAPAIAN CPMK — SEMESTER BERJALAN</h2>
<p class="keterangan">Hanya CPMK yang sudah terukur ({{ cpmk_ternilai|length }} dari {{ cpmk_urut|length }}).
Garis putus-putus = ambang. Arahkan kursor untuk melihat MK pengukur terlemah.</p>
{% set tinggi = cpmk_ternilai|length * 22 + 34 %}
<div class="gulir">
<svg viewBox="0 0 760 {{ tinggi }}" class="grafik" role="img" aria-label="Grafik batang horizontal skor tiap CPMK">
<defs><clipPath id="plot-cpmk"><rect x="100" y="0" width="646" height="{{ tinggi - 30 }}"/></clipPath></defs>
{% for g in [0, 25, 50, 75, 100] %}
<line x1="{{ 100 + g * 6.46 }}" y1="0" x2="{{ 100 + g * 6.46 }}" y2="{{ tinggi - 30 }}" stroke="{{ '#bbb' if g == 0 else '#e4e4e4' }}" stroke-width="1"/>
<text x="{{ 100 + g * 6.46 }}" y="{{ tinggi - 16 }}" class="sumbu" text-anchor="middle">{{ g }}</text>
{% endfor %}
<line x1="{{ '%.1f'|format(100 + hasil.ambang * 6.46) }}" y1="0" x2="{{ '%.1f'|format(100 + hasil.ambang * 6.46) }}" y2="{{ tinggi - 30 }}"
stroke="var(--red)" stroke-width="1.5" stroke-dasharray="6 4"/>
{% for c in cpmk_ternilai %}
{% set y = loop.index0 * 22 %}
<g class="bar">
<title>{{ c.kode }} (mendukung {{ c.cpl|join(', ') }}) — skor {{ '%.2f'|format(c.skor) }}
setor: {{ c.n_setor }}/{{ c.n_pemikul }} MK pemikul{% if c.ada_agregat %} (ada bukti agregat){% endif %}
{% if c.mk_terlemah %}terlemah: {{ nama_mk.get(c.mk_terlemah[0], c.mk_terlemah[0]) }} ({{ '%.1f'|format(c.mk_terlemah[1]) }}){% endif %}</title>
<rect x="0" y="{{ y }}" width="760" height="22" fill="transparent"/>
<text x="94" y="{{ y + 15 }}" class="sumbu" text-anchor="end">{{ c.kode }}</text>
<rect clip-path="url(#plot-cpmk)" x="94" y="{{ y + 4 }}" width="{{ '%.1f'|format(c.skor * 6.46 + 6) }}" height="14" rx="4"
fill="{{ 'var(--blue)' if c.skor >= hasil.ambang else 'var(--red)' }}"/>
<text x="{{ '%.1f'|format(104 + c.skor * 6.46) }}" y="{{ y + 15 }}" class="nilai-bar">{{ '%.1f'|format(c.skor) }}</text>
</g>
{% endfor %}
</svg>
</div>
{% endif %}
{# ── D. Tren lintas semester (sparkline per CPL, bila >1 tahun) ─────── #}
{% if tren %}
<h2 class="bagian">TREN SKOR CPL LINTAS TAHUN AJARAN</h2>
<p class="keterangan">Garis kecil per CPL; titik terakhir = {{ hasil.tahun }}. Rincian angka: <a href="/rekap/riwayat">Riwayat Lintas Semester</a>.</p>
<div class="tren-kisi">
{% for kode, titik in tren.items() %}
{% set ada = titik|selectattr(1, 'ne', none)|list %}
<div class="tren-kartu" title="{{ kode }}: {% for t, s in titik %}{{ t }} = {{ '%.1f'|format(s) if s is not none else '—' }}{{ ' · ' if not loop.last }}{% endfor %}">
<b>{{ kode }}</b>
<svg viewBox="0 0 150 46" class="tren-svg" role="img" aria-label="Tren {{ kode }}">
{% set lebar_x = 130 / (titik|length - 1) if titik|length > 1 else 0 %}
{# skala vertikal mengikuti rentang data (± 2 poin) agar tren terlihat #}
{% set nilai2 = ada|map(attribute=1)|list %}
{% set lo = [nilai2|min - 2, 0]|max %}
{% set span = [nilai2|max + 2 - lo, 4]|max %}
{% if lo <= hasil.ambang <= lo + span %}
<line x1="6" y1="{{ '%.1f'|format(42 - (hasil.ambang - lo) * 36 / span) }}" x2="136" y2="{{ '%.1f'|format(42 - (hasil.ambang - lo) * 36 / span) }}" stroke="var(--red)" stroke-width="1" stroke-dasharray="3 3"/>
{% endif %}
<polyline fill="none" stroke="var(--blue)" stroke-width="2" stroke-linejoin="round"
points="{% for t, s in titik %}{% if s is not none %}{{ '%.1f'|format(6 + loop.index0 * lebar_x) }},{{ '%.1f'|format(42 - (s - lo) * 36 / span) }} {% endif %}{% endfor %}"/>
{% for t, s in titik %}{% if s is not none %}
<circle cx="{{ '%.1f'|format(6 + loop.index0 * lebar_x) }}" cy="{{ '%.1f'|format(42 - (s - lo) * 36 / span) }}" r="3" fill="var(--blue)" stroke="#fff" stroke-width="1.5"/>
{% endif %}{% endfor %}
</svg>
<span class="tren-akhir">{% if ada %}{{ '%.1f'|format(ada[-1][1]) }}{% else %}—{% endif %}</span>
</div>
{% endfor %}
</div>
{% endif %}
{# ── E. Perlu perhatian ─────────────────────────────────────────────── #}
<h2 class="bagian">PERLU PERHATIAN</h2>
<div class="perhatian-kisi">
<div>
<p class="keterangan" style="margin-top:0">CPL di bawah ambang &amp; penyumbang terlemahnya — mulai perbaikan dari sini.</p>
{% set cpl_rendah = cpl_urut|rejectattr('skor', 'none')|rejectattr('tercapai')|list %}
{% if cpl_rendah %}
<table class="grid">
<thead><tr><th style="width:70px">CPL</th><th class="tengah" style="width:64px">Skor</th><th>CPMK &amp; MK Terlemah</th></tr></thead>
<tbody>
{% for c in cpl_rendah %}
<tr>
<td class="tengah"><b>{{ c.kode }}</b></td>
<td class="tengah">{{ '%.2f'|format(c.skor) }}</td>
<td>{% if c.mk_terlemah %}{{ c.cpmk_terlemah }} — <a href="/nilai/{{ c.mk_terlemah[0] }}">{{ nama_mk.get(c.mk_terlemah[0], c.mk_terlemah[0]) }}</a> ({{ '%.1f'|format(c.mk_terlemah[1]) }}){% else %}—{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="info">Tidak ada CPL terukur yang berada di bawah ambang. ✓</div>
{% endif %}
</div>
<div>
<p class="keterangan" style="margin-top:0">MK belum setor nilai ({{ belum_nilai|length }}) — hubungi pengampunya.</p>
{% if belum_nilai %}
<div class="gulir" style="max-height:320px;overflow-y:auto">
<table class="grid">
<thead><tr><th style="width:100px">Kode</th><th>Mata Kuliah</th><th style="width:180px">Pengampu</th></tr></thead>
<tbody>
{% for m in belum_nilai %}
<tr>
<td>{{ m.kode }}</td>
<td><a href="/nilai/{{ m.kode }}">{{ m.nama }}</a></td>
<td style="font-size:13px">{{ m.pengampu|join(', ') or '— belum ditugaskan' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="info">Semua MK sudah menyetor nilai. ✓</div>
{% endif %}
</div>
</div>
{% endblock %}