Tambah ringkasan nilai per komponen, rincian tugas ala RTM, auto-save RPS

- Halaman Nilai: tabel "Nilai per Komponen" (rata-rata skor mentah tiap
  sesi, bisa diedit langsung) berdampingan dengan grid Sub-CPMK yang tetap
  presisi; kolom persentase ternilai di Hasil Ketercapaian.
- Komponen Tugas: sesi bisa menyasar subset Sub-CPMK (RPS tetap batasannya),
  lengkap dengan field ala RTM (bentuk tugas, deskripsi, metode, luaran,
  indikator/bobot, jadwal) dan tombol isi-otomatis per bentuk tugas.
- RPS: auto-save berkala sebagai draft, tombol ke atas, validasi total
  bobot komponen 100% saat ditandai selesai, komponen berbobot kosong
  disembunyikan dari halaman Nilai.
- Admin: tampilkan kelas yang tersedia dari data mahasiswa saat mengatur
  penugasan dosen per kelas.
- Perbaikan step="any" pada input nilai (mencegah validasi HTML5 menolak
  nilai desimal hasil roll-up).
- Tambah METODOLOGI_ASESMEN_KOMPONEN.md: dokumentasi metodologi Sub-CPMK
  vs komponen, termasuk skenario tugas menyasar sebagian Sub-CPMK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Power BI Dev
2026-07-23 23:28:46 +07:00
parent bdaacf1c8f
commit d5edbe2134
7 changed files with 668 additions and 67 deletions
+66 -5
View File
@@ -4,10 +4,12 @@
<div class="nav-baris">
<div>MK ini: <b>RPS</b> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
<div>
{% 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 %}
{% if updated_at %}<span style="color:var(--grey);font-size:13px"> · disimpan {{ updated_at }}{% if updated_by %} oleh {{ updated_by }}{% endif %}</span>{% endif %}
<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>
@@ -34,7 +36,7 @@
</div>
{% endif %}
<form method="post" action="/rps/{{ peta.kode }}">
<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>
@@ -213,6 +215,7 @@
<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>
@@ -224,6 +227,10 @@
</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 }};
@@ -283,5 +290,59 @@
}
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.addEventListener('input', jadwalkanAutosave);
}
</script>
{% endblock %}