Files
OBE-Mapping/app/templates/form_rps.html
T
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

355 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block judul %}RPS {{ peta.nama }} — Informatika UNTAN{% endblock %}
{% block isi %}
<div class="nav-baris">
<div>MK ini: <b>RPS</b> · <a href="/rps/{{ peta.kode }}/rtm">RTM</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
<div>
<span id="status-stamp">
{% if status == 'selesai' %}<span class="stamp stamp-ok">SELESAI</span>
{% elif status == 'draft' %}<span class="stamp stamp-draft">DRAFT</span>
{% else %}<span class="stamp stamp-no">BELUM</span>{% endif %}
</span>
<span id="status-waktu" style="color:var(--grey);font-size:13px">{% if updated_at %} · disimpan {{ updated_at }}{% if updated_by %} oleh {{ updated_by }}{% endif %}{% endif %}</span>
</div>
</div>
<h1>Rencana Pembelajaran Semester — {{ peta.nama }} <span style="color:var(--grey)">({{ peta.kode }})</span></h1>
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
{% if not boleh_edit %}
<div class="info">Anda tidak memiliki hak edit RPS untuk MK ini — form terbuka
<b>hanya untuk dibaca</b>. Ini karena Anda bukan pengampu MK ini, atau peran Anda
<b>Dosen MK</b> (hanya mengelola nilai &amp; portofolio). Hubungi kaprodi bila ini
perlu diubah.</div>
{% endif %}
{% if tahun_lain and boleh_edit %}
<div class="info">
MK ini punya RPS pada tahun ajaran lain. Salin sebagai titik awal (status draft, bisa diedit):
{% for t in tahun_lain %}
<form method="post" action="/rps/{{ peta.kode }}/salin" style="display:inline">
<input type="hidden" name="dari" value="{{ t }}">
<button class="tombol garis" type="submit" style="padding:3px 10px;font-size:13px">Salin dari {{ t }}</button>
</form>
{% endfor %}
</div>
{% endif %}
<form method="post" action="/rps/{{ peta.kode }}" id="form-rps">
<fieldset {% if not boleh_edit %}disabled{% endif %} style="border:none;padding:0;margin:0">
<h2 class="bagian">A. IDENTITAS MATA KULIAH</h2>
<table class="grid">
<tr><td class="label">Nama Mata Kuliah</td><td>{{ peta.nama }}</td></tr>
<tr><td class="label">Kode Mata Kuliah</td><td>{{ peta.kode }}</td></tr>
<tr><td class="label">Bobot SKS</td>
<td class="isian" style="display:flex;gap:8px;align-items:center;border:none">
<input type="number" name="sks" min="0" max="8" style="width:70px" value="{{ f.identitas.sks }}"> SKS
&nbsp;(Teori: <input type="number" name="sks_teori" min="0" max="8" style="width:70px" value="{{ f.identitas.sks_teori }}">
| Praktikum: <input type="number" name="sks_praktikum" min="0" max="8" style="width:70px" value="{{ f.identitas.sks_praktikum }}">)
</td></tr>
<tr><td class="label">Semester</td><td class="isian"><input type="text" name="semester" value="{{ f.identitas.semester }}" placeholder="mis. 3 (Ganjil)"></td></tr>
<tr><td class="label">Rumpun MK</td><td class="isian"><input type="text" name="rumpun" value="{{ f.identitas.rumpun }}" placeholder="mis. MK Inti Keilmuan"></td></tr>
<tr><td class="label">MK Prasyarat</td><td class="isian"><input type="text" name="prasyarat" value="{{ f.identitas.prasyarat }}" placeholder="kosongkan bila tidak ada"></td></tr>
<tr><td class="label">Dosen Pengampu</td><td class="isian"><input type="text" name="pengampu" value="{{ f.identitas.pengampu }}" placeholder="Nama Dosen 1 | Nama Dosen 2"></td></tr>
<tr><td class="label">Dosen Pengembang RPS</td><td class="isian"><input type="text" name="pengembang" value="{{ f.identitas.pengembang }}" placeholder="bila kosong: diisi nama dosen pengampu"></td></tr>
<tr><td class="label">Koordinator MK</td><td class="isian"><input type="text" name="koordinator" value="{{ f.identitas.koordinator }}"></td></tr>
<tr><td class="label">Ketua Program Studi</td><td class="isian"><input type="text" name="kaprodi" value="{{ f.identitas.kaprodi }}"></td></tr>
<tr><td class="label">Tgl Penyusunan</td><td class="isian"><input type="text" name="tgl_penyusunan" value="{{ f.identitas.tgl_penyusunan }}" placeholder="mis. 13 Juli 2026"></td></tr>
<tr><td class="label">Kode Dokumen</td><td class="isian"><input type="text" name="kode_dokumen" value="{{ f.identitas.kode_dokumen }}" placeholder="diisi bagian penjaminan mutu, boleh kosong"></td></tr>
</table>
<h2 class="bagian">B. CPL YANG DIBEBANKAN PADA MK <span style="font-weight:normal;font-style:italic;color:var(--grey)">— otomatis dari pemetaan kurikulum</span></h2>
<table class="grid">
<thead><tr><th style="width:90px">Kode</th><th>Deskripsi CPL</th></tr></thead>
<tbody>
{% for c in peta.cpls %}
<tr><td class="tengah">{{ c.kode }}</td><td>{{ c.desk }}</td></tr>
{% endfor %}
</tbody>
</table>
<h2 class="bagian">C. CPMK <span style="font-weight:normal;font-style:italic;color:var(--grey)">— otomatis dari pemetaan kurikulum</span></h2>
<table class="grid">
<thead><tr><th style="width:110px">Kode</th><th>Deskripsi CPMK</th><th style="width:130px">CPL Didukung</th></tr></thead>
<tbody>
{% for c in peta.cpmks %}
<tr><td class="tengah">{{ c.kode }}</td><td>{{ c.desk }}</td><td class="tengah">{{ c.cpl|join(', ') }}</td></tr>
{% endfor %}
</tbody>
</table>
<h2 class="bagian">D. SUB-CPMK <span style="font-weight:normal;font-style:italic;color:var(--grey)">— kode &amp; kaitan CPMK otomatis dari pemetaan kurikulum ({{ peta.subs|length }} butir); redaksi kalimat boleh disesuaikan bila perlu</span></h2>
<div class="gulir" style="max-height:320px;overflow-y:auto">
<table class="grid">
<thead><tr><th style="width:110px">Kode</th><th>Uraian Sub-CPMK</th><th style="width:110px">CPMK</th></tr></thead>
<tbody>
{% for s in peta.subs %}
<tr><td class="tengah">{{ s.kode }}</td>
<td class="isian"><textarea name="sub_uraian_{{ s.kode }}" rows="2">{{ s.uraian }}</textarea></td>
<td class="tengah">{{ s.cpmk }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<h2 class="bagian">E. DESKRIPSI SINGKAT MATA KULIAH</h2>
<textarea name="deskripsi" rows="4" placeholder="Topik utama, pendekatan pembelajaran, relevansi dengan profil lulusan dan CPL. Maksimal ± 100 kata.">{{ f.deskripsi }}</textarea>
<h2 class="bagian">E2. BAHAN KAJIAN — MATERI PEMBELAJARAN</h2>
<textarea name="bahan_kajian" rows="4" placeholder="Materi/bahan kajian MK secara berurutan (sesuai format RPS UNTAN).">{{ f.bahan_kajian }}</textarea>
<h2 class="bagian">E3. MEDIA PEMBELAJARAN</h2>
<textarea name="media" rows="2" placeholder="mis. LMS (e-learning UNTAN), LCD proyektor, perangkat lunak pendukung.">{{ f.media }}</textarea>
<h2 class="bagian">F. RENCANA MINGGUAN <span id="total-bobot-f" style="font-weight:normal">— total bobot: <b>0</b>%</span></h2>
<p class="keterangan">Minggu 8 = UTS, minggu 16 = UAS. Total bobot seluruh minggu harus 100 %.
{% if boleh_edit %}Pilih Sub-CPMK lalu klik <b>Isi Otomatis</b> untuk mengisi kolom kosong dengan teks
bantu (termasuk usulan Bobot — UTS 20%, UAS 30%, minggu biasa 3,5% — sesuaikan lagi agar totalnya
pas 100%).{% endif %}</p>
{% if boleh_edit %}
<details style="margin:0 0 10px">
<summary style="cursor:pointer;color:var(--blue);font-size:13px">Lihat contoh teks bantu per kolom</summary>
<div class="info" style="margin-top:6px;line-height:1.7">
<b>Indikator</b> — turunan Sub-CPMK, mis. "Mahasiswa mampu menjelaskan …"; "Ketepatan dan kelengkapan dalam …"<br>
<b>Kriteria &amp; Teknik</b> — mis. "Rubrik holistik; tes tertulis" / "Ketepatan jawaban; unjuk kerja" / "Kelengkapan tugas; observasi"<br>
<b>Tatap Muka</b> — mis. "Ceramah, diskusi, tanya jawab [TM: 2x50 menit]" / "Studi kasus &amp; presentasi kelompok [TM: 2x50 menit]"<br>
<b>Daring</b> — mis. "Membaca modul &amp; kuis di LMS [BT+BM: 2x60 menit]" / "Diskusi forum LMS [BT+BM: 2x60 menit]"<br>
<b>Kegiatan Pembelajaran</b> — mis. "Mahasiswa menyimak paparan dosen, berdiskusi, dan mengerjakan latihan terkait materi minggu ini."<br>
<b>Materi [Pustaka]</b> — rujuk nomor pustaka di Bagian H, mis. "Bab 3 (Pustaka Utama #1)"
</div>
</details>
{% endif %}
<div class="gulir">
<table class="grid" id="tabel-f">
<thead>
<tr>
<th class="tengah" style="width:52px" rowspan="2">Mg</th>
<th style="min-width:190px" rowspan="2">Sub-CPMK</th>
<th colspan="2">Penilaian</th>
<th colspan="2">Bentuk/Metode Pembelajaran [Estimasi Waktu]</th>
<th style="min-width:140px" rowspan="2">Kegiatan Pembelajaran</th>
<th style="min-width:150px" rowspan="2">Materi [Pustaka]</th>
<th class="tengah" style="width:70px" rowspan="2">Bobot (%)</th>
</tr>
<tr>
<th class="sub" style="min-width:150px">Indikator</th>
<th class="sub" style="min-width:140px">Kriteria &amp; Teknik</th>
<th class="sub" style="min-width:130px">Tatap Muka</th>
<th class="sub" style="min-width:120px">Daring</th>
</tr>
</thead>
<tbody>
{% for m in f.minggu %}
<tr class="{% if m.ke == 8 %}uts{% elif m.ke == 16 %}uas{% endif %}">
<td class="tengah">{{ m.ke }}</td>
<td class="isian">
<select name="m{{ m.ke }}_sub" {% if boleh_edit %}onchange="document.getElementById('btn-isi-m{{ m.ke }}').style.display = this.value ? '' : 'none'"{% endif %}>
<option value="">— pilih —</option>
{% for s in peta.subs %}
<option value="{{ s.kode }}" {% if m.sub == s.kode %}selected{% endif %}>{{ s.kode }} — {{ s.uraian }}</option>
<option disabled>──────────────────────</option>
{% endfor %}
<option value="__uts__" {% if m.sub == '__uts__' %}selected{% endif %}>Sub-CPMK minggu 17 (UTS)</option>
<option value="__uas__" {% if m.sub == '__uas__' %}selected{% endif %}>Sub-CPMK minggu 915 (UAS)</option>
</select>
{% if boleh_edit %}
<button type="button" id="btn-isi-m{{ m.ke }}" class="tombol garis"
style="padding:1px 6px;font-size:10px;margin-top:4px;{% if not m.sub %}display:none{% endif %}"
onclick="isiOtomatis({{ m.ke }})" title="Isi kolom kosong dengan teks bantu dari Sub-CPMK terpilih">Isi Otomatis</button>
{% endif %}
</td>
<td class="isian"><textarea name="m{{ m.ke }}_indikator" rows="2">{{ m.indikator }}</textarea></td>
<td class="isian"><textarea name="m{{ m.ke }}_kriteria" rows="2">{{ m.kriteria }}</textarea></td>
<td class="isian"><textarea name="m{{ m.ke }}_metode" rows="2">{{ m.metode }}</textarea></td>
<td class="isian"><textarea name="m{{ m.ke }}_daring" rows="2">{{ m.daring }}</textarea></td>
<td class="isian"><textarea name="m{{ m.ke }}_kegiatan" rows="2">{{ m.kegiatan }}</textarea></td>
<td class="isian"><textarea name="m{{ m.ke }}_materi" rows="2">{{ m.materi }}</textarea></td>
<td class="isian"><input type="number" class="bobot-f" name="m{{ m.ke }}_bobot" min="0" max="100" step="0.5" value="{{ m.bobot }}"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<h2 class="bagian">G. REKAP BOBOT PENILAIAN <span id="total-bobot-g" style="font-weight:normal">— total: <b>0</b>%</span></h2>
<p class="keterangan">Enam komponen teknik asesmen baku prodi. Bobot boleh dikosongkan untuk
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
satu komponen agar CPMK/CPL-nya terukur.</p>
{% if komponen_tanpa_sub %}
<div class="peringatan">Bobot sudah terisi tapi <b>belum ada Sub-CPMK dicentang</b> untuk:
{{ komponen_tanpa_sub|join(', ') }}. Komponen begini ikut dihitung ke total 100% di bawah,
tapi <b>tidak akan muncul</b> di halaman Input Nilai — centang minimal satu Sub-CPMK di
tiap baris sebelum menyimpan.</div>
{% endif %}
<div class="gulir">
<table class="grid">
<thead>
<tr><th style="width:190px">Komponen</th><th>Sub-CPMK yang Dinilai</th><th class="tengah" style="width:90px">Bobot (%)</th></tr>
</thead>
<tbody>
{% for k in f.komponen %}
{% set ki = loop.index %}
<tr>
<td>{{ k.nama }}</td>
<td>
<div class="pilih-sub">
{% for s in peta.subs %}
<label><input type="checkbox" class="sub-k" name="k{{ ki }}_subs" value="{{ s.kode }}" {% if s.kode in k.subs %}checked{% endif %}>{{ s.kode }} — {{ s.uraian }}</label>
{% endfor %}
</div>
</td>
<td class="isian"><input type="number" class="bobot-g" name="k{{ ki }}_bobot" min="0" max="100" step="0.5" value="{{ k.bobot }}"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="info" id="cek-cakupan" hidden></div>
<h2 class="bagian">H. PUSTAKA UTAMA</h2>
<p class="keterangan">Satu referensi per baris, format APA.</p>
<textarea name="referensi" rows="5" placeholder="Penulis. (Tahun). Judul. Penerbit / Jurnal, Vol(No), Hal. https://doi.org/...">{{ f.referensi_teks }}</textarea>
<h2 class="bagian">H2. PUSTAKA PENDUKUNG</h2>
<textarea name="referensi_pendukung" rows="3" placeholder="Satu referensi per baris; boleh kosong.">{{ f.referensi_pendukung_teks }}</textarea>
</fieldset>
<div class="aksi-baris">
{% if boleh_edit %}
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
<span id="autosave-status" style="color:var(--grey);font-size:13px;margin-left:10px"></span>
<span class="spacer"></span>
<button class="tombol garis" type="submit" name="aksi" value="draft">Simpan Draft</button>
<button class="tombol selesai" type="submit" name="aksi" value="selesai">Simpan &amp; Tandai Selesai</button>
{% else %}
<span class="spacer"></span>
{% endif %}
{% if status %}<a class="tombol" href="/rps/{{ peta.kode }}/unduh">Unduh .docx</a>
<a class="tombol garis" href="/rps/{{ peta.kode }}/unduh.pdf">PDF</a>{% endif %}
</div>
</form>
<div style="text-align:center;margin-top:16px">
<button type="button" class="tombol garis" onclick="window.scrollTo({top:0, behavior:'smooth'})">▲ Ke Atas</button>
</div>
<script>
const SUB_URAIAN = {{ sub_uraian | tojson | safe }};
function isiOtomatis(mg) {
const sel = document.querySelector(`select[name="m${mg}_sub"]`);
const nilai = sel ? sel.value : '';
const isi = (nama, teks) => {
const el = document.querySelector(`[name="m${mg}_${nama}"]`);
if (el && !el.value.trim()) el.value = teks;
};
if (SUB_URAIAN[nilai]) {
const uraian = SUB_URAIAN[nilai];
isi('indikator', 'Mahasiswa ' + uraian.charAt(0).toLowerCase() + uraian.slice(1));
} else if (nilai === '__uts__') {
isi('indikator', 'Mahasiswa menguasai capaian Sub-CPMK minggu 17.');
} else if (nilai === '__uas__') {
isi('indikator', 'Mahasiswa menguasai capaian Sub-CPMK minggu 915.');
} else {
return; // belum pilih Sub-CPMK — tak ada dasar untuk mengisi
}
isi('kriteria', 'Ketepatan dan kelengkapan jawaban/tugas sesuai rubrik penilaian.');
isi('metode', 'Ceramah, diskusi, dan tanya jawab [TM: 2x50 menit]');
isi('daring', 'Membaca materi & berdiskusi di LMS [BT+BM: 2x60 menit]');
isi('kegiatan', 'Mahasiswa menyimak penjelasan dosen, berdiskusi, dan mengerjakan latihan terkait materi minggu ini.');
// bobot: usulan awal (UTS/UAS lazim lebih besar) — total tetap harus disesuaikan ke 100%
isi('bobot', mg == 8 ? '20' : mg == 16 ? '30' : '3.5');
segarkan();
}
function hitungTotal(kelas, idLabel) {
let t = 0;
document.querySelectorAll(kelas).forEach(el => { t += parseFloat(el.value) || 0; });
const label = document.querySelector(idLabel);
label.querySelector('b').textContent = (Math.round(t * 100) / 100);
label.classList.toggle('salah', Math.abs(t - 100) > 0.01);
}
function cekCakupan() {
const dinilai = new Set();
document.querySelectorAll('.sub-k:checked').forEach(cb => dinilai.add(cb.value));
const semua = [{% for s in peta.subs %}'{{ s.kode }}'{% if not loop.last %},{% endif %}{% endfor %}];
const hilang = semua.filter(k => !dinilai.has(k));
const kotak = document.getElementById('cek-cakupan');
if (hilang.length === 0) {
kotak.hidden = false;
kotak.className = 'info';
kotak.textContent = 'Semua ' + semua.length + ' Sub-CPMK sudah tercakup komponen penilaian — CPMK dan CPL mata kuliah ini akan terukur penuh.';
} else {
kotak.hidden = false;
kotak.className = 'peringatan';
kotak.textContent = 'Perhatian: ' + hilang.length + ' Sub-CPMK belum dinilai komponen mana pun (ketercapaian CPMK/CPL-nya tidak akan terukur): ' + hilang.join(', ');
}
}
function segarkan() {
hitungTotal('.bobot-f', '#total-bobot-f');
hitungTotal('.bobot-g', '#total-bobot-g');
cekCakupan();
}
document.addEventListener('input', segarkan);
segarkan();
// Auto-save berkala (selalu sebagai draft — tak pernah menandai "selesai"
// otomatis) supaya isian tak hilang kalau sinyal/listrik tiba-tiba putus.
const BOLEH_EDIT = {{ boleh_edit | tojson | safe }};
const NAMA_USER = {{ user.nama | tojson | safe }};
if (BOLEH_EDIT) {
let timerAutosave = null;
let sedangMenyimpan = false;
let perluSimpanLagi = false;
function waktuSekarang() {
const d = new Date();
const p = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
async function simpanOtomatis() {
if (sedangMenyimpan) { perluSimpanLagi = true; return; }
sedangMenyimpan = true;
const status = document.getElementById('autosave-status');
if (status) status.textContent = 'Menyimpan otomatis…';
try {
const form = document.getElementById('form-rps');
const data = new FormData(form);
data.set('aksi', 'draft');
const res = await fetch(form.action, { method: 'POST', body: data });
const jam = new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
if (status) {
status.textContent = res.ok
? `Tersimpan otomatis ${jam}`
: 'Auto-save gagal — klik Simpan Draft manual.';
}
if (res.ok) {
// label stempel "DRAFT · disimpan ... oleh ..." di atas halaman
// dirender sekali saat page-load — perbarui juga di sini supaya
// tak kelihatan basi walau auto-save baru saja jalan di belakang layar.
const stamp = document.getElementById('status-stamp');
const waktu = document.getElementById('status-waktu');
if (stamp) stamp.innerHTML = '<span class="stamp stamp-draft">DRAFT</span>';
if (waktu) waktu.textContent = ` · disimpan ${waktuSekarang()} oleh ${NAMA_USER}`;
}
} catch (e) {
if (status) status.textContent = 'Auto-save gagal (offline?) — klik Simpan Draft manual.';
} finally {
sedangMenyimpan = false;
if (perluSimpanLagi) { perluSimpanLagi = false; jadwalkanAutosave(); }
}
}
function jadwalkanAutosave() {
clearTimeout(timerAutosave);
timerAutosave = setTimeout(simpanOtomatis, 3000);
}
document.getElementById('form-rps').addEventListener('input', jadwalkanAutosave);
}
</script>
{% endblock %}