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:
@@ -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 & 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 %}
|
||||
|
||||
Reference in New Issue
Block a user