10 Commits

Author SHA1 Message Date
Power BI Dev e31f8e57dd Merge branch 'fix/rps-docx-baris-salah-sasaran' into main 2026-08-09 21:56:12 +07:00
Power BI Dev 374bdb5565 Perbaiki isi RPS unduhan yang nyasar ke baris salah (Peta CPL, Deskripsi, dst)
build_rps() mengisi blok naratif (Peta CPL-CPMK, Deskripsi Singkat MK,
Bahan Kajian, Pustaka, Media, Dosen Pengampu, MK Prasyarat) lewat
t.cell(18, 2) dst -- indeks baris tetap sesuai template asli (39 baris).
Tapi baris-baris itu dieksekusi SETELAH daftar CPL/CPMK/Sub-CPMK
digandakan & baris contohnya dibuang -- jumlah baris yang disisipkan
beda-beda tiap MK (tergantung berapa CPL/CPMK/Sub-CPMK-nya), jadi
t.cell(18, 2) di tabel yang sudah berubah itu tidak lagi menunjuk baris
"Peta CPL-CPMK" yang dimaksud. Teks jadi nyasar ke sel/baris lain,
termasuk kolom sempit -- terlihat sebagai puluhan halaman teks terpotong
huruf demi huruf.

Pola yang benar sudah dipakai untuk tabel mingguan (trs[31], trs[33],
dst -- referensi elemen XML baris asli yang dibekukan di awal, tak
ikut bergeser). Terapkan pola sama (helper cell()) untuk blok naratif
yang kelewat.

Diverifikasi: label & isi sekarang sejajar benar (baris "Peta CPL - CP
MK" berisi peta CPMK->CPL, bukan lagi nama dosen pengampu), dokumen
turun dari puluhan halaman rusak jadi 2 halaman rapi untuk bagian ini.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 21:42:01 +07:00
Power BI Dev de65ca99e6 Merge branch 'ubah/teks-peringatan-subcpmk-komponen' into main 2026-08-09 20:58:45 +07:00
Power BI Dev adf2918e85 Ubah teks peringatan Sub-CPMK-komponen: fokus ke "belum ditautkan"
Framing lama ("RPS sudah punya bobot untuk X, tapi belum ada Sub-CPMK
dicentang") terkesan seperti pengisian sengaja yang cuma kurang
langkah terakhir. Padahal akar masalahnya sering kali bobot bawaan
aplikasi (KOMPONEN_DEFAULT) yang belum pernah disentuh dosen sama
sekali — reframing ke "Sub-CPMK belum ditautkan ke komponen penilaian"
lebih jujur ke kondisi sebenarnya.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 20:56:18 +07:00
Power BI Dev 3c9482ee23 Merge branch 'fitur/peringatan-komponen-tanpa-subcpmk' into main 2026-08-09 20:33:38 +07:00
Power BI Dev e5d45cdcdd Peringatan kalau bobot komponen RPS terisi tapi Sub-CPMK-nya belum dicentang
Kasus nyata: RPS bagian G bobotnya sudah lengkap (total 100%), tapi kolom
"Sub-CPMK yang Dinilai" tak satu pun dicentang di tiap komponen. Komponen
begini lolos validasi total bobot, tapi _komponen_daftar() menyaringnya
habis (butuh subs DAN bobot) -- dosen jadi bingung kenapa halaman Nilai
sama sekali tak punya section "ISI PER KOMPONEN", padahal RPS-nya sudah
diisi.

- _komponen_tanpa_sub(): fungsi baru, deteksi komponen dengan bobot > 0
  tapi subs kosong
- Halaman RPS: peringatan di atas tabel bagian G, sebut nama komponen
  yang bermasalah
- Halaman Nilai: peringatan serupa + link "Buka RPS" -- muncul di titik
  gejalanya, bukan cuma di sumbernya

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 20:29:15 +07:00
Power BI Dev a3efa9fef3 Merge branch 'fitur/konfirmasi-hapus-nilai' into main 2026-08-09 19:49:12 +07:00
Power BI Dev 6375bc88f4 Tambah konfirmasi password & auto-arsip sebelum hapus nilai
Tombol "Mulai Ulang (hapus semua nilai)" di halaman Input Nilai
sebelumnya cuma dijaga confirm() browser biasa — gampang ke-klik tanpa
sadar, apalagi datanya sekarang ratusan mahasiswa per MK.

- Dialog konfirmasi kini menyebut jumlah mahasiswa yang akan hilang
- Password akun yang login harus dimasukkan ulang & diverifikasi
  server-side (auth.verifikasi_password) sebelum aksi hapus dieksekusi
- Isi nilai lama diarsipkan otomatis ke tabel nilai_arsip_hapus sebelum
  dihapus (mk_kode, tahun, data JSON, kapan, oleh siapa) — jaring
  pengaman, ikut ke-backup lewat /cadangan.db yang sudah ada; belum ada
  UI pemulihan (di luar cakupan saat ini, cukup query manual bila perlu)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 18:51:09 +07:00
Power BI Dev efc14dc7a7 Merge branch 'fitur/filter-kelas-input-nilai' into main 2026-08-09 18:36:25 +07:00
Power BI Dev 9c0fe8baa0 Tambah filter kelas di halaman Input Nilai
- /nilai/<kode>: dropdown "Tampilkan kelas" menyaring tabel Nilai per
  Komponen & Nilai per Mahasiswa sekaligus (tanpa reload); baris
  ringkasan (rata-rata/% lulus MK) ikut disembunyikan saat filter aktif
  karena angkanya dihitung dari seluruh MK, bukan per kelas
- /nilai/<kode>/komponen/<i>: filter serupa + kolom Kelas ditambahkan
  di tabel Nilai Mahasiswa (halaman paling sering dipakai dosen isi
  nilai per sesi/tugas)
- Muncul otomatis hanya bila MK punya >1 kelas

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 18:35:43 +07:00
6 changed files with 194 additions and 21 deletions
+25 -2
View File
@@ -185,6 +185,16 @@ def init_db() -> None:
updated_by TEXT NOT NULL updated_by TEXT NOT NULL
)""" )"""
) )
conn.execute(
"""CREATE TABLE IF NOT EXISTS nilai_arsip_hapus (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mk_kode TEXT NOT NULL,
tahun TEXT NOT NULL,
data TEXT NOT NULL,
dihapus_at TEXT NOT NULL,
dihapus_oleh TEXT NOT NULL
)"""
)
conn.execute( conn.execute(
"""CREATE TABLE IF NOT EXISTS pengaturan ( """CREATE TABLE IF NOT EXISTS pengaturan (
kunci TEXT PRIMARY KEY, kunci TEXT PRIMARY KEY,
@@ -292,6 +302,19 @@ def simpan_dok(tabel: str, mk_kode: str, data: dict, status: str, updated_by: st
) )
def arsipkan_nilai_hapus(mk_kode: str, tahun: str, data: dict, dihapus_oleh: str) -> None:
"""Simpan salinan nilai sebelum dihapus (jaring pengaman) — tak ada UI
pemulihan otomatis, tapi baris ini tetap ikut ke-backup lewat /cadangan.db
dan bisa diambil manual bila suatu saat perlu dipulihkan."""
now = datetime.now(WIB).strftime("%Y-%m-%d %H:%M")
with get_conn() as conn:
conn.execute(
"""INSERT INTO nilai_arsip_hapus (mk_kode, tahun, data, dihapus_at, dihapus_oleh)
VALUES (?,?,?,?,?)""",
(mk_kode, tahun, json.dumps(data, ensure_ascii=False), now, dihapus_oleh),
)
def hapus_dok(tabel: str, mk_kode: str, tahun: str | None = None) -> None: def hapus_dok(tabel: str, mk_kode: str, tahun: str | None = None) -> None:
assert tabel in _TABEL_DOK assert tabel in _TABEL_DOK
tahun = tahun or tahun_aktif() tahun = tahun or tahun_aktif()
@@ -362,11 +385,11 @@ def dump_semua() -> dict:
"""Seluruh isi database sebagai dict (untuk cadangan JSON).""" """Seluruh isi database sebagai dict (untuk cadangan JSON)."""
out = {} out = {}
with get_conn() as conn: with get_conn() as conn:
for tabel in (*_TABEL_DOK, "rps_arsip", "pengaturan", for tabel in (*_TABEL_DOK, "rps_arsip", "nilai_arsip_hapus", "pengaturan",
"pengguna", "penugasan", "mahasiswa"): "pengguna", "penugasan", "mahasiswa"):
rows = conn.execute(f"SELECT * FROM {tabel}").fetchall() rows = conn.execute(f"SELECT * FROM {tabel}").fetchall()
out[tabel] = [dict(r) for r in rows] out[tabel] = [dict(r) for r in rows]
for tabel in _TABEL_DOK + ("rps_arsip",): for tabel in _TABEL_DOK + ("rps_arsip", "nilai_arsip_hapus"):
for r in out.get(tabel, []): for r in out.get(tabel, []):
r["data"] = json.loads(r["data"]) r["data"] = json.loads(r["data"])
return out return out
+15 -8
View File
@@ -130,6 +130,13 @@ def build_rps(d: dict) -> bytes:
trs = [r._tr for r in t.rows] # indeks baris template asli trs = [r._tr for r in t.rows] # indeks baris template asli
def cell(idx, col):
"""Ambil sel dari baris ASLI template lewat trs[idx] (referensi XML
beku), bukan t.cell(idx, col) yang ikut bergeser tiap kali baris
CPL/CPMK/Sub-CPMK digandakan/dibuang di atasnya — pakai t.cell()
langsung untuk baris di bawah blok itu akan salah sasaran."""
return _Row(trs[idx], t).cells[col]
def isi_daftar(proto_idx, hapus_idx, items, kolom_kode=2, kolom_isi=5): def isi_daftar(proto_idx, hapus_idx, items, kolom_kode=2, kolom_isi=5):
"""Isi blok daftar (CPL/CPMK/Sub-CPMK): prototipe digandakan per item.""" """Isi blok daftar (CPL/CPMK/Sub-CPMK): prototipe digandakan per item."""
proto = trs[proto_idx] proto = trs[proto_idx]
@@ -153,17 +160,17 @@ def build_rps(d: dict) -> bytes:
f"{c['kode']}{', '.join(c.get('cpl', [])) or '-'}" f"{c['kode']}{', '.join(c.get('cpl', [])) or '-'}"
for c in d.get("cpmk", []) for c in d.get("cpmk", [])
) )
_set_text(t.cell(18, 2), peta_txt) _set_text(cell(18, 2), peta_txt)
_set_text(t.cell(19, 2), d.get("deskripsi", "")) _set_text(cell(19, 2), d.get("deskripsi", ""))
_set_text(t.cell(20, 2), d.get("bahan_kajian", "")) _set_text(cell(20, 2), d.get("bahan_kajian", ""))
utama = d.get("referensi", []) utama = d.get("referensi", [])
pendukung = d.get("referensi_pendukung", []) pendukung = d.get("referensi_pendukung", [])
_set_text(t.cell(22, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(utama, 1))) _set_text(cell(22, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(utama, 1)))
_set_text(t.cell(24, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(pendukung, 1))) _set_text(cell(24, 2), "\n".join(f"{i}. {r}" for i, r in enumerate(pendukung, 1)))
_set_text(t.cell(25, 2), d.get("media", "")) _set_text(cell(25, 2), d.get("media", ""))
_set_text(t.cell(26, 2), ident.get("pengampu", "")) _set_text(cell(26, 2), ident.get("pengampu", ""))
_set_text(t.cell(27, 2), ident.get("prasyarat") or "") _set_text(cell(27, 2), ident.get("prasyarat") or "")
# ── tabel mingguan ── # ── tabel mingguan ──
proto_norm, proto_uts, proto_uas = trs[31], trs[33], trs[37] proto_norm, proto_uts, proto_uas = trs[31], trs[33], trs[37]
+29 -1
View File
@@ -327,6 +327,19 @@ def _normalisasi_komponen(tersimpan: list[dict]) -> list[dict]:
return out return out
def _komponen_tanpa_sub(rps_data: dict) -> list[str]:
"""Nama komponen RPS bagian G yang bobotnya sudah diisi tapi belum ada
Sub-CPMK dicentang. Komponen begini masih ikut dihitung ke total 100% di
bagian G (jadi mudah lolos tanpa disadari), tapi disaring habis oleh
_komponen_daftar() — tidak akan pernah muncul di halaman Nilai."""
out = []
for k in _normalisasi_komponen(rps_data.get("komponen", [])):
bobot_terisi = str(k.get("bobot") or "").strip() not in ("", "0", "0.0")
if bobot_terisi and not k.get("subs"):
out.append(k["nama"])
return out
def minggu_default() -> list[dict]: def minggu_default() -> list[dict]:
rows = [] rows = []
for w in range(1, 17): for w in range(1, 17):
@@ -456,6 +469,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
f.setdefault("referensi_teks", "\n".join(f.get("referensi", []))) f.setdefault("referensi_teks", "\n".join(f.get("referensi", [])))
f["komponen"] = _normalisasi_komponen(f.get("komponen", [])) f["komponen"] = _normalisasi_komponen(f.get("komponen", []))
ctx_status = tersimpan ctx_status = tersimpan
komponen_tanpa_sub = _komponen_tanpa_sub(f)
else: else:
f = form_kosong() f = form_kosong()
# prefill SKS & semester dari data kurikulum (sheet '14. Susunan MK') # prefill SKS & semester dari data kurikulum (sheet '14. Susunan MK')
@@ -465,6 +479,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
if info.get("semester"): if info.get("semester"):
f["identitas"]["semester"] = str(info["semester"]) f["identitas"]["semester"] = str(info["semester"])
ctx_status = {"status": "", "updated_at": "", "updated_by": ""} ctx_status = {"status": "", "updated_at": "", "updated_by": ""}
komponen_tanpa_sub = [] # form belum pernah disimpan — bobot masih bawaan default
peta = _terapkan_override_sub(peta, f) peta = _terapkan_override_sub(peta, f)
return render( return render(
request, "form_rps.html", request, "form_rps.html",
@@ -473,6 +488,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
"tahun": db.tahun_aktif(), "pesan": pesan, "tahun": db.tahun_aktif(), "pesan": pesan,
"boleh_edit": boleh_rps(request.state.user, kode), "boleh_edit": boleh_rps(request.state.user, kode),
"tahun_lain": db.tahun_rps_lain(kode) if not tersimpan else [], "tahun_lain": db.tahun_rps_lain(kode) if not tersimpan else [],
"komponen_tanpa_sub": komponen_tanpa_sub,
"sub_uraian": {s["kode"]: s["uraian"] for s in peta["subs"]}}, "sub_uraian": {s["kode"]: s["uraian"] for s in peta["subs"]}},
) )
@@ -946,6 +962,8 @@ def _konteks_nilai(request, kode, pesan="", peringatan=None):
terdaftar = db.mahasiswa_mk(kode) terdaftar = db.mahasiswa_mk(kode)
grid = _grid_mahasiswa(tersimpan, terdaftar, kelas_dosen) grid = _grid_mahasiswa(tersimpan, terdaftar, kelas_dosen)
komponen_daftar = _komponen_daftar(kode) komponen_daftar = _komponen_daftar(kode)
rps_cek = db.ambil_rps(kode)
komponen_tanpa_sub = _komponen_tanpa_sub(rps_cek["data"]) if rps_cek else []
sesi_semua = (tersimpan["data"].get("sesi") or {}) if tersimpan else {} sesi_semua = (tersimpan["data"].get("sesi") or {}) if tersimpan else {}
bobot_komponen, bobot_sub_rincian = _rincian_bobot(kode) bobot_komponen, bobot_sub_rincian = _rincian_bobot(kode)
# Nilai Akhir dihitung hanya dari Sub-CPMK yang sudah ternilai (lihat # Nilai Akhir dihitung hanya dari Sub-CPMK yang sudah ternilai (lihat
@@ -983,6 +1001,7 @@ def _konteks_nilai(request, kode, pesan="", peringatan=None):
"grid": grid, "kelas_dosen": kelas_dosen, "tidak_lengkap": tidak_lengkap, "grid": grid, "kelas_dosen": kelas_dosen, "tidak_lengkap": tidak_lengkap,
"dikatrol": dikatrol, "dikatrol": dikatrol,
"komponen_daftar": komponen_daftar, "komponen_daftar": komponen_daftar,
"komponen_tanpa_sub": komponen_tanpa_sub,
"tabel_komponen": _tabel_komponen(grid, komponen_daftar, sesi_semua) if komponen_daftar else [], "tabel_komponen": _tabel_komponen(grid, komponen_daftar, sesi_semua) if komponen_daftar else [],
"bobot_komponen": bobot_komponen, "bobot_sub_rincian": bobot_sub_rincian, "bobot_komponen": bobot_komponen, "bobot_sub_rincian": bobot_sub_rincian,
"tabel_poin": _tabel_poin(grid, bobot_sub_rincian) if bobot_sub_rincian else [], "tabel_poin": _tabel_poin(grid, bobot_sub_rincian) if bobot_sub_rincian else [],
@@ -1165,10 +1184,19 @@ async def simpan_nilai(request: Request, kode: str):
return _konteks_nilai(request, kode, peringatan=[ return _konteks_nilai(request, kode, peringatan=[
"Anda hanya diampukan sebagian kelas MK ini — menghapus seluruh nilai " "Anda hanya diampukan sebagian kelas MK ini — menghapus seluruh nilai "
"hanya bisa dilakukan kaprodi atau dosen yang mengampu seluruh kelas."]) "hanya bisa dilakukan kaprodi atau dosen yang mengampu seluruh kelas."])
akun = db.pengguna_by_id(request.state.user["id"])
password = (form.get("password") or "").strip()
if not akun or not auth.verifikasi_password(password, akun["pw_hash"]):
return _konteks_nilai(request, kode, peringatan=[
"Password salah — nilai TIDAK jadi dihapus."])
lama = db.ambil_dok("nilai", kode)
if lama:
db.arsipkan_nilai_hapus(kode, db.tahun_aktif(), lama["data"], updated_by)
db.hapus_dok("nilai", kode) db.hapus_dok("nilai", kode)
return _konteks_nilai( return _konteks_nilai(
request, kode, request, kode,
pesan="Seluruh nilai tersimpan MK ini dihapus — silakan mulai dari awal.") pesan="Seluruh nilai tersimpan MK ini dihapus (salinan sebelumnya "
"tersimpan otomatis sebagai arsip) — silakan mulai dari awal.")
if aksi == "komponen": if aksi == "komponen":
kelas_dosen = _kelas_dosen(request.state.user, kode) kelas_dosen = _kelas_dosen(request.state.user, kode)
+79 -9
View File
@@ -17,6 +17,13 @@
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %} {% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
{% for w in peringatan %}<div class="peringatan">{{ w }}</div>{% endfor %} {% for w in peringatan %}<div class="peringatan">{{ w }}</div>{% endfor %}
{% if komponen_tanpa_sub %}
<div class="peringatan"><b>Sub-CPMK belum ditautkan ke komponen penilaian</b> untuk
{{ komponen_tanpa_sub|join(', ') }} — segera isi di RPS bagian G. Selama belum ditautkan,
komponen itu {% if not komponen_daftar %}tidak muncul sama sekali{% else %}belum ikut muncul{% endif %}
di "Isi per Komponen" di bawah. <a href="/rps/{{ peta.kode }}">Buka RPS &rarr;</a> lalu centang Sub-CPMK-nya.</div>
{% endif %}
{% if komponen_daftar %} {% if komponen_daftar %}
<h2 class="bagian">ISI PER KOMPONEN</h2> <h2 class="bagian">ISI PER KOMPONEN</h2>
<table class="grid" style="max-width:760px"> <table class="grid" style="max-width:760px">
@@ -54,15 +61,21 @@
{% endif %} {% endif %}
{% if tersimpan %} {% if tersimpan %}
{% set n_mhs = tersimpan.data.mahasiswa|length if tersimpan.data.mode == 'per_mahasiswa' else none %}
<div class="info"> <div class="info">
Data tersimpan: mode 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> <b>{% if tersimpan.data.mode == 'per_mahasiswa' %}PER MAHASISWA ({{ n_mhs }} mahasiswa){% else %}BUKTI AGREGAT (rata-rata kelas){% endif %}</b>
— terakhir disimpan {{ tersimpan.updated_at }}{% if tersimpan.updated_by %} oleh {{ tersimpan.updated_by }}{% endif %}. — terakhir disimpan {{ tersimpan.updated_at }}{% if tersimpan.updated_by %} oleh {{ tersimpan.updated_by }}{% endif %}.
{% if riwayat_ringkas %}<br>Ringkasan pengisian — team teaching: {% if riwayat_ringkas %}<br>Ringkasan pengisian — team teaching:
{% for k in riwayat_ringkas %}{{ k.komponen }}: {% for d in k.per_dosen %}{{ d.n }}&times; {{ d.nama }}{{ ', ' if not loop.last }}{% endfor %}{{ ' · ' if not loop.last }}{% endfor %}.{% endif %} {% for k in riwayat_ringkas %}{{ k.komponen }}: {% for d in k.per_dosen %}{{ d.n }}&times; {{ d.nama }}{{ ', ' if not loop.last }}{% endfor %}{{ ' · ' if not loop.last }}{% endfor %}.{% endif %}
<form method="post" action="/nilai/{{ peta.kode }}" style="display:inline;margin-left:8px" <form method="post" action="/nilai/{{ peta.kode }}" style="display:flex;align-items:center;gap:8px;margin-top:8px;flex-wrap:wrap"
onsubmit="return confirm('Hapus SELURUH nilai tersimpan MK ini untuk {{ tahun }}? Tindakan ini tidak bisa dibatalkan.')"> onsubmit="return confirm('Hapus SELURUH nilai{% if n_mhs %} {{ n_mhs }} mahasiswa{% endif %} tersimpan MK ini untuk {{ tahun }}?\n\nSalinan sebelumnya akan diarsipkan otomatis, tapi tidak ada tombol pulihkan di aplikasi — hubungi kaprodi/admin bila perlu dikembalikan.\n\nTindakan ini tidak bisa dibatalkan sendiri. Lanjutkan?')">
<button class="tombol garis" type="submit" name="aksi" value="hapus" <input type="hidden" name="aksi" value="hapus">
<label style="font-size:13px;color:var(--grey)">Konfirmasi password Anda:
<input type="password" name="password" required autocomplete="current-password"
style="display:inline-block;width:auto;padding:3px 6px;font-size:13px;margin-left:4px">
</label>
<button class="tombol garis" type="submit"
style="color:var(--red);border-color:var(--red);padding:2px 10px;font-size:13px">Mulai Ulang (hapus semua nilai)</button> style="color:var(--red);border-color:var(--red);padding:2px 10px;font-size:13px">Mulai Ulang (hapus semua nilai)</button>
</form> </form>
</div> </div>
@@ -89,10 +102,25 @@
</details> </details>
{% endif %} {% endif %}
{% set ada_kelas = grid and grid|selectattr('kelas')|list|length > 0 %}
{% if grid and tabel_komponen and komponen_daftar %} {% if grid and tabel_komponen and komponen_daftar %}
<h2 class="bagian">NILAI PER KOMPONEN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan komponen</span></h2> <h2 class="bagian">NILAI PER KOMPONEN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan komponen</span></h2>
<p class="keterangan">Angka = rata-rata skor mentah tiap sesi/tugas komponen itu yang diikuti mahasiswa (bukan lewat Sub-CPMK). Ketik untuk menimpa — <p class="keterangan">Angka = rata-rata skor mentah tiap sesi/tugas komponen itu yang diikuti mahasiswa (bukan lewat Sub-CPMK). Ketik untuk menimpa —
ditulis rata ke semua Sub-CPMK komponen itu; rincian per Sub-CPMK tetap presisi di grid bawah.</p> ditulis rata ke semua Sub-CPMK komponen itu; rincian per Sub-CPMK tetap presisi di grid bawah.</p>
{% if ada_kelas %}
{% set semua_kelas = grid|map(attribute='kelas')|select|unique|sort|list %}
<div class="filter-baris">
<label for="filter-kelas">Tampilkan kelas:</label>
<select id="filter-kelas" style="width:auto">
<option value="">Semua</option>
{% for k in semua_kelas %}
<option value="{{ k }}">Kelas {{ k }}</option>
{% endfor %}
</select>
<span id="filter-kelas-info" class="keterangan" style="margin:0"></span>
</div>
{% endif %}
<form method="post" action="/nilai/{{ peta.kode }}"> <form method="post" action="/nilai/{{ peta.kode }}">
<input type="hidden" name="aksi" value="komponen"> <input type="hidden" name="aksi" value="komponen">
<div class="gulir"> <div class="gulir">
@@ -109,7 +137,7 @@
<tbody> <tbody>
{% for r in tabel_komponen %} {% for r in tabel_komponen %}
{% set ri = loop.index0 %} {% set ri = loop.index0 %}
<tr> <tr data-kelas="{{ r.kelas or '' }}">
<td class="tengah">{{ loop.index }} <td class="tengah">{{ loop.index }}
<input type="hidden" name="nim_{{ ri }}" value="{{ r.nim }}"> <input type="hidden" name="nim_{{ ri }}" value="{{ r.nim }}">
<input type="hidden" name="nama_{{ ri }}" value="{{ r.nama }}"></td> <input type="hidden" name="nama_{{ ri }}" value="{{ r.nama }}"></td>
@@ -140,9 +168,21 @@
{% if grid %} {% if grid %}
{% set per_mhs = tersimpan and tersimpan.data.mode == 'per_mahasiswa' %} {% 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)">— nilai mahasiswa berdasarkan Sub-CPMK</span></h2> <h2 class="bagian">NILAI PER MAHASISWA <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan Sub-CPMK</span></h2>
{% if ada_kelas and not (tabel_komponen and komponen_daftar) %}
{% set semua_kelas = grid|map(attribute='kelas')|select|unique|sort|list %}
<div class="filter-baris">
<label for="filter-kelas">Tampilkan kelas:</label>
<select id="filter-kelas" style="width:auto">
<option value="">Semua</option>
{% for k in semua_kelas %}
<option value="{{ k }}">Kelas {{ k }}</option>
{% endfor %}
</select>
<span id="filter-kelas-info" class="keterangan" style="margin:0"></span>
</div>
{% endif %}
<form method="post" action="/nilai/{{ peta.kode }}"> <form method="post" action="/nilai/{{ peta.kode }}">
<div class="gulir"> <div class="gulir">
{% set ada_kelas = grid|selectattr('kelas')|list|length > 0 %}
{% set n_kiri = 4 if ada_kelas else 3 %} {% set n_kiri = 4 if ada_kelas else 3 %}
<table class="grid"> <table class="grid">
<thead> <thead>
@@ -158,7 +198,7 @@
<tbody> <tbody>
{% for m in grid %} {% for m in grid %}
{% set i = loop.index0 %} {% set i = loop.index0 %}
<tr> <tr data-kelas="{{ m.kelas or '' }}">
<td class="tengah">{{ loop.index }} <td class="tengah">{{ loop.index }}
<input type="hidden" name="nim_{{ i }}" value="{{ m.nim }}"> <input type="hidden" name="nim_{{ i }}" value="{{ m.nim }}">
<input type="hidden" name="nama_{{ i }}" value="{{ m.nama }}"></td> <input type="hidden" name="nama_{{ i }}" value="{{ m.nama }}"></td>
@@ -174,7 +214,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
{% if per_mhs and hasil %} {% if per_mhs and hasil %}
<tr class="total"> <tr class="total" data-kelas="__total__" title="Dihitung dari seluruh mahasiswa MK ini, tidak per kelas">
<td colspan="{{ n_kiri }}" style="text-align:right">Rata-rata kelas (tersimpan)</td> <td colspan="{{ n_kiri }}" style="text-align:right">Rata-rata kelas (tersimpan)</td>
{% for s in peta.subs %} {% for s in peta.subs %}
{% set info = hasil.sub.get(s.kode) %} {% set info = hasil.sub.get(s.kode) %}
@@ -182,7 +222,7 @@
{% endfor %} {% endfor %}
<td class="tengah">{{ '%.1f'|format(akhir.values()|sum / akhir|length) if akhir else '—' }}</td> <td class="tengah">{{ '%.1f'|format(akhir.values()|sum / akhir|length) if akhir else '—' }}</td>
</tr> </tr>
<tr class="total"> <tr class="total" data-kelas="__total__" title="Dihitung dari seluruh mahasiswa MK ini, tidak per kelas">
<td colspan="{{ n_kiri }}" style="text-align:right">% lulus (≥ {{ ambang|round(0)|int }})</td> <td colspan="{{ n_kiri }}" style="text-align:right">% lulus (≥ {{ ambang|round(0)|int }})</td>
{% for s in peta.subs %} {% for s in peta.subs %}
{% set info = hasil.sub.get(s.kode) %} {% set info = hasil.sub.get(s.kode) %}
@@ -383,4 +423,34 @@
{% endif %} {% endif %}
</details> </details>
{% endif %} {% endif %}
{% if ada_kelas %}
<script>
(function () {
var pilih = document.getElementById('filter-kelas');
var info = document.getElementById('filter-kelas-info');
var baris = document.querySelectorAll('table.grid tbody tr[data-kelas]');
function perbarui() {
var v = pilih.value, n = 0, total = 0;
baris.forEach(function (tr) {
var kelas = tr.dataset.kelas;
if (kelas === '__total__') {
// baris rata-rata/% lulus: dihitung dari seluruh MK, cuma masuk akal saat "Semua"
tr.style.display = v === '' ? '' : 'none';
return;
}
total++;
var tampil = v === '' || kelas === v;
tr.style.display = tampil ? '' : 'none';
if (tampil) n++;
});
info.textContent = v === '' ? '' : n + ' dari ' + total + ' mahasiswa ditampilkan';
}
pilih.addEventListener('change', perbarui);
perbarui();
})();
</script>
{% endif %}
{% endblock %} {% endblock %}
+40 -1
View File
@@ -96,6 +96,20 @@
</label> </label>
{% endif %} {% endif %}
<h2 class="bagian">NILAI MAHASISWA</h2> <h2 class="bagian">NILAI MAHASISWA</h2>
{% set ada_kelas = grid|selectattr('kelas')|list|length > 0 %}
{% if ada_kelas %}
{% set semua_kelas = grid|map(attribute='kelas')|select|unique|sort|list %}
<div class="filter-baris">
<label for="filter-kelas">Tampilkan kelas:</label>
<select id="filter-kelas" style="width:auto">
<option value="">Semua</option>
{% for k in semua_kelas %}
<option value="{{ k }}">Kelas {{ k }}</option>
{% endfor %}
</select>
<span id="filter-kelas-info" class="keterangan" style="margin:0"></span>
</div>
{% endif %}
<div class="gulir" style="max-height:480px;overflow-y:auto"> <div class="gulir" style="max-height:480px;overflow-y:auto">
<table class="grid"> <table class="grid">
<thead> <thead>
@@ -103,17 +117,19 @@
<th style="width:44px" class="tengah">No.</th> <th style="width:44px" class="tengah">No.</th>
<th style="width:112px">NIM</th> <th style="width:112px">NIM</th>
<th style="min-width:150px">Nama</th> <th style="min-width:150px">Nama</th>
{% if ada_kelas %}<th style="width:60px" class="tengah">Kelas</th>{% endif %}
<th class="tengah" style="width:140px">Nilai (0100)</th> <th class="tengah" style="width:140px">Nilai (0100)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for m in grid %} {% for m in grid %}
<tr> <tr data-kelas="{{ m.kelas or '' }}">
<td class="tengah">{{ loop.index }} <td class="tengah">{{ loop.index }}
<input type="hidden" name="nim_{{ loop.index0 }}" value="{{ m.nim }}"> <input type="hidden" name="nim_{{ loop.index0 }}" value="{{ m.nim }}">
<input type="hidden" name="nama_{{ loop.index0 }}" value="{{ m.nama }}"></td> <input type="hidden" name="nama_{{ loop.index0 }}" value="{{ m.nama }}"></td>
<td>{{ m.nim }}</td> <td>{{ m.nim }}</td>
<td>{{ m.nama }}</td> <td>{{ m.nama }}</td>
{% if ada_kelas %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
<td class="isian"><input type="number" name="nilai_{{ loop.index0 }}" <td class="isian"><input type="number" name="nilai_{{ loop.index0 }}"
min="0" max="100" step="any" min="0" max="100" step="any"
value="{{ '%g'|format(m.nilai_awal) if m.nilai_awal is not none else '' }}"></td> value="{{ '%g'|format(m.nilai_awal) if m.nilai_awal is not none else '' }}"></td>
@@ -129,6 +145,29 @@
</div> </div>
</form> </form>
{% if ada_kelas %}
<script>
(function () {
var pilih = document.getElementById('filter-kelas');
var info = document.getElementById('filter-kelas-info');
var baris = document.querySelectorAll('table.grid tbody tr[data-kelas]');
function perbarui() {
var v = pilih.value, n = 0;
baris.forEach(function (tr) {
var tampil = v === '' || tr.dataset.kelas === v;
tr.style.display = tampil ? '' : 'none';
if (tampil) n++;
});
info.textContent = v === '' ? '' : n + ' dari ' + baris.length + ' mahasiswa ditampilkan';
}
pilih.addEventListener('change', perbarui);
perbarui();
})();
</script>
{% endif %}
{% if komponen.tugas %} {% if komponen.tugas %}
<script> <script>
const BENTUK_TEMPLATE = { const BENTUK_TEMPLATE = {
+6
View File
@@ -178,6 +178,12 @@
komponen yang tidak dipakai MK ini (opsional), tapi total seluruh komponen yang diisi harus komponen yang tidak dipakai MK ini (opsional), tapi total seluruh komponen yang diisi harus
tepat 100%. Centang Sub-CPMK yang dinilai tiap komponen; setiap Sub-CPMK harus tercakup minimal tepat 100%. Centang Sub-CPMK yang dinilai tiap komponen; setiap Sub-CPMK harus tercakup minimal
satu komponen agar CPMK/CPL-nya terukur.</p> satu komponen agar CPMK/CPL-nya terukur.</p>
{% if komponen_tanpa_sub %}
<div class="peringatan"><b>Sub-CPMK belum ditautkan ke komponen penilaian</b> — segera centang
minimal satu Sub-CPMK untuk: {{ komponen_tanpa_sub|join(', ') }}. Tanpa tautan ini, komponen
tersebut <b>tidak akan muncul</b> di halaman Input Nilai meski bobotnya sudah dihitung ke
total 100% di bawah.</div>
{% endif %}
<div class="gulir"> <div class="gulir">
<table class="grid"> <table class="grid">
<thead> <thead>