9be6bd03e3
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>
193 lines
12 KiB
HTML
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 & penyumbang terlemah.
|
|
Tabel rinci: <a href="/rekap">Rekap</a>.</p>
|
|
<div class="legenda">
|
|
<span><i class="chip" style="background:var(--blue)"></i> tercapai (≥ 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 & 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 & 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 %}
|