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:
@@ -48,6 +48,11 @@
|
||||
{% if mk.kode in p.mk %}checked{% endif %}> {{ mk.kode }} — {{ mk.nama }}</label>
|
||||
<input type="text" name="kelas_{{ mk.kode }}" placeholder="kelas (kosong = semua)"
|
||||
value="{{ p.kelas_map.get(mk.kode, '') }}" style="width:150px;margin:0 0 4px 22px;font-size:12px">
|
||||
{% if mhs.get(mk.kode) and mhs[mk.kode].kelas %}
|
||||
<span style="color:var(--grey);font-size:11px">kelas tersedia (dari data mahasiswa): {{ mhs[mk.kode].kelas|join(', ') }}</span>
|
||||
{% else %}
|
||||
<span style="color:var(--grey);font-size:11px;font-style:italic">belum ada data mahasiswa/kelas untuk MK ini — impor dulu di bagian C</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p class="cari-kosong" style="display:none;color:var(--grey);font-size:13px;margin:4px 0">Tidak ada MK yang cocok.</p>
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
<h1>Input Nilai — {{ peta.nama }} <span style="color:var(--grey)">({{ peta.kode }})</span></h1>
|
||||
|
||||
<p class="keterangan">Daftar mahasiswa mengikuti impor kaprodi. Apa pun cara
|
||||
inputnya, pengukuran memakai <b>satu metodologi yang sama</b>: skor per Sub-CPMK
|
||||
digulung ke CPMK lalu CPL (rincian di halaman <a href="/metodologi">Metodologi</a>).</p>
|
||||
<p class="keterangan">Skor per Sub-CPMK digulung ke CPMK lalu CPL (rincian di halaman <a href="/metodologi">Metodologi</a>).</p>
|
||||
|
||||
{% if kelas_dosen is not none %}
|
||||
<div class="info">Anda hanya mengampu kelas <b>{{ kelas_dosen|join(', ') }}</b> pada MK ini —
|
||||
@@ -20,13 +18,7 @@
|
||||
{% for w in peringatan %}<div class="peringatan">{{ w }}</div>{% endfor %}
|
||||
|
||||
{% if komponen_daftar %}
|
||||
<h2 class="bagian">ISI PER KOMPONEN PENILAIAN <span style="font-weight:normal;font-style:italic;color:var(--grey)">(cara cepat — nilai per mahasiswa digabung ke Sub-CPMK yang diukur komponen itu)</span></h2>
|
||||
<p class="keterangan">
|
||||
Komponen <b>berulang</b> (Kuis, Tugas, Presentasi, Tes Lisan) boleh diisi berkali-kali
|
||||
sepanjang semester — tiap sesi dirata-rata otomatis. Komponen <b>sekali-jalan</b>
|
||||
(UTS, UAS) hanya satu sesi; mengisi lagi memperbarui sesi yang sama, bukan menambah.
|
||||
Bila satu instrumen (mis. UTS dengan beberapa kelompok soal) perlu nilai berbeda
|
||||
antar Sub-CPMK, isi lewat grid per Sub-CPMK di bawah.</p>
|
||||
<h2 class="bagian">ISI PER KOMPONEN</h2>
|
||||
<table class="grid" style="max-width:760px">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -76,14 +68,54 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% 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>
|
||||
<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>
|
||||
<form method="post" action="/nilai/{{ peta.kode }}">
|
||||
<input type="hidden" name="aksi" value="komponen">
|
||||
<div class="gulir" style="max-height:480px;overflow-y:auto">
|
||||
<table class="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:44px" class="tengah">No.</th>
|
||||
<th style="width:112px">NIM</th>
|
||||
<th style="min-width:150px">Nama</th>
|
||||
{% for k in komponen_daftar %}<th class="tengah" style="font-size:11px;min-width:90px">{{ k.nama }}</th>{% endfor %}
|
||||
<th class="tengah" style="width:80px">Nilai Akhir</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in tabel_komponen %}
|
||||
{% set ri = loop.index0 %}
|
||||
<tr>
|
||||
<td class="tengah">{{ loop.index }}
|
||||
<input type="hidden" name="nim_{{ ri }}" value="{{ r.nim }}">
|
||||
<input type="hidden" name="nama_{{ ri }}" value="{{ r.nama }}"></td>
|
||||
<td>{{ r.nim }}</td>
|
||||
<td>{{ r.nama }}</td>
|
||||
{% for k in komponen_daftar %}
|
||||
<td class="isian"><input type="number" name="komp_{{ ri }}_{{ k.i }}"
|
||||
min="0" max="100" step="any"
|
||||
value="{{ '%g'|format(r.nilai[k.i]) if r.nilai[k.i] is not none else '' }}"></td>
|
||||
{% endfor %}
|
||||
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(r.nim)) if akhir.get(r.nim) is not none else '—' }}</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="aksi-baris">
|
||||
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||
<span class="spacer"></span>
|
||||
<button class="tombol" type="submit">Simpan Nilai Komponen</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if grid %}
|
||||
{% 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)">(ketik langsung di sel, 0–100; kolom Nilai Akhir & baris rata-rata dihitung dari data tersimpan)</span></h2>
|
||||
<p class="keterangan">
|
||||
Daftar mahasiswa dari impor kaprodi. Kosongkan sel yang belum dinilai;
|
||||
<b>Simpan Nilai</b> menyimpan persis isi tabel. Nilai Akhir = tertimbang bobot
|
||||
komponen RPS; angkanya diperbarui setelah disimpan.
|
||||
</p>
|
||||
<h2 class="bagian">NILAI PER MAHASISWA <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai mahasiswa berdasarkan Sub-CPMK</span></h2>
|
||||
<form method="post" action="/nilai/{{ peta.kode }}">
|
||||
<div class="gulir" style="max-height:480px;overflow-y:auto">
|
||||
{% set ada_kelas = grid|selectattr('kelas')|list|length > 0 %}
|
||||
@@ -111,7 +143,7 @@
|
||||
{% if ada_kelas %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
|
||||
{% for s in peta.subs %}
|
||||
<td class="isian"><input type="number" name="skor_{{ i }}_{{ s.kode }}"
|
||||
min="0" max="100" step="0.01"
|
||||
min="0" max="100" step="any"
|
||||
value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}"></td>
|
||||
{% endfor %}
|
||||
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(m.nim)) if akhir.get(m.nim) is not none else '—' }}</b></td>
|
||||
@@ -147,16 +179,17 @@
|
||||
{% endif %}
|
||||
|
||||
{% if hasil %}
|
||||
<h2 class="bagian">HASIL KETERCAPAIAN MK INI <span style="font-weight:normal;font-style:italic;color:var(--grey)">(ambang {{ ambang|round(0)|int }}; roll-up bagi rata)</span></h2>
|
||||
<h2 class="bagian">HASIL KETERCAPAIAN MK <span style="font-weight:normal;font-style:italic;color:var(--grey)">(ambang {{ ambang|round(0)|int }})</span></h2>
|
||||
<div class="gulir">
|
||||
<table class="grid">
|
||||
<thead><tr><th>CPMK</th><th class="tengah">Skor</th><th class="tengah">Sub Ternilai</th><th>CPL Didukung</th></tr></thead>
|
||||
<thead><tr><th>CPMK</th><th class="tengah">Skor</th><th class="tengah">Sub Ternilai</th><th class="tengah">% Ternilai</th><th>CPL Didukung</th></tr></thead>
|
||||
<tbody>
|
||||
{% for kode_c, v in hasil.cpmk.items() %}
|
||||
<tr>
|
||||
<td>{{ kode_c }}</td>
|
||||
<td class="tengah">{% if v.skor is not none %}<b>{{ '%.2f'|format(v.skor) }}</b>{% else %}—{% endif %}</td>
|
||||
<td class="tengah">{{ v.n_ternilai }}/{{ v.n_sub }}</td>
|
||||
<td class="tengah">{{ '%.0f'|format(100 * v.n_ternilai / v.n_sub) ~ '%' if v.n_sub else '—' }}</td>
|
||||
<td>{{ v.cpl|join(', ') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -165,12 +198,13 @@
|
||||
</div>
|
||||
<div class="gulir" style="margin-top:10px">
|
||||
<table class="grid">
|
||||
<thead><tr><th>CPL (dalam MK ini)</th><th class="tengah">Skor</th><th class="tengah">Status</th></tr></thead>
|
||||
<thead><tr><th>CPL</th><th class="tengah">Skor</th><th class="tengah">% CPMK Ternilai</th><th class="tengah">Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for kode_c, v in hasil.cpl.items() %}
|
||||
<tr>
|
||||
<td>{{ kode_c }}</td>
|
||||
<td class="tengah">{% if v.skor is not none %}<b>{{ '%.2f'|format(v.skor) }}</b>{% else %}—{% endif %}</td>
|
||||
<td class="tengah">{{ '%.0f'|format(100 * v.n_cpmk_ternilai / v.n_cpmk) ~ '%' if v.n_cpmk else '—' }}</td>
|
||||
<td class="tengah">
|
||||
{% if v.skor is none %}<span class="stamp stamp-draft">BELUM TERUKUR</span>
|
||||
{% elif v.skor >= ambang %}<span class="stamp stamp-ok">TERCAPAI</span>
|
||||
|
||||
@@ -28,12 +28,13 @@
|
||||
|
||||
{% if komponen.ulang and daftar_sesi %}
|
||||
<h2 class="bagian">SESI TERSIMPAN</h2>
|
||||
<table class="grid" style="max-width:560px">
|
||||
<thead><tr><th>Sesi</th><th class="tengah" style="width:120px">Mahasiswa Terisi</th><th style="width:60px"></th></tr></thead>
|
||||
<table class="grid" style="max-width:{{ '900px' if komponen.tugas else '560px' }}">
|
||||
<thead><tr><th>Sesi</th>{% if komponen.tugas %}<th>Sub-CPMK</th><th style="width:130px">Jadwal</th>{% endif %}<th class="tengah" style="width:120px">Mahasiswa Terisi</th><th style="width:60px"></th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in daftar_sesi %}
|
||||
<tr{% if loop.index0 == edit_id %} style="background:var(--blue-fill)"{% endif %}>
|
||||
<td>{{ s.label }}</td>
|
||||
<td>{{ s.label }}{% if komponen.tugas and s.deskripsi %}<br><span style="font-size:12px;color:var(--grey);font-style:italic">{{ s.deskripsi }}</span>{% endif %}</td>
|
||||
{% if komponen.tugas %}<td>{{ (s.subs or komponen.subs)|join(', ') }}</td><td>{{ s.jadwal or '—' }}</td>{% endif %}
|
||||
<td class="tengah">{{ (s.nilai or {})|length }}</td>
|
||||
<td><a href="/nilai/{{ peta.kode }}/komponen/{{ i }}?sesi_id={{ loop.index0 }}">Edit</a></td>
|
||||
</tr>
|
||||
@@ -46,12 +47,55 @@
|
||||
<form method="post" action="/nilai/{{ peta.kode }}/komponen/{{ i }}">
|
||||
<input type="hidden" name="edit_id" value="{{ edit_id }}">
|
||||
{% if komponen.ulang %}
|
||||
<label style="display:block;margin-bottom:10px">Nama sesi
|
||||
<input type="text" name="label" value="{{ label_awal }}" style="display:block;margin-top:4px;max-width:280px">
|
||||
<label style="display:block;margin-bottom:10px">{{ 'Judul Tugas' if komponen.tugas else 'Nama sesi' }}
|
||||
<input type="text" name="label" value="{{ label_awal }}" style="display:block;margin-top:4px;max-width:480px">
|
||||
</label>
|
||||
{% else %}
|
||||
<input type="hidden" name="label" value="{{ label_awal }}">
|
||||
{% endif %}
|
||||
{% if komponen.tugas %}
|
||||
<div style="margin-bottom:10px">
|
||||
<div>Sub Capaian Pembelajaran Mata Kuliah <span style="font-weight:normal;color:var(--grey)">(bawaan semua tercentang, sesuai yang ditentukan RPS untuk komponen ini — kurangi bila tugas lain akan menyasar sisanya; bobot komponen ini tetap {{ komponen.bobot }}%)</span></div>
|
||||
<div class="pilih-sub">
|
||||
{% for s in subs_pilihan %}
|
||||
<label><input type="checkbox" name="subs" value="{{ s.kode }}" {% if s.kode in subs_terpilih %}checked{% endif %}>{{ s.kode }} — {{ s.uraian }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<label style="display:block;margin-bottom:10px">Bentuk Tugas
|
||||
<span style="display:flex;align-items:center;gap:8px;margin-top:4px">
|
||||
<select name="bentuk_tugas" id="bentuk_tugas" onchange="toggleIsiOtomatis()" style="max-width:280px">
|
||||
<option value="">— pilih —</option>
|
||||
{% for opsi in ["Diskusi", "PjBL", "CBL", "Esai", "Tes"] %}
|
||||
<option value="{{ opsi }}" {% if rtm.bentuk_tugas == opsi %}selected{% endif %}>{{ opsi }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" class="tombol garis" id="tombol_isi_otomatis" onclick="isiOtomatisTugas()" {% if not rtm.bentuk_tugas %}hidden{% endif %}>Isi Otomatis</button>
|
||||
</span>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Deskripsi Tugas <span style="font-weight:normal;color:var(--grey)">(opsional — soal cerita, esai, kasus, atau proyek yang harus diselesaikan mahasiswa)</span>
|
||||
<textarea name="deskripsi" id="rtm_deskripsi" rows="4" style="display:block;margin-top:4px;max-width:760px;width:100%">{{ rtm.deskripsi }}</textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Metode Pengerjaan Tugas <span style="font-weight:normal;color:var(--grey)">(opsional — langkah-langkah pengerjaan)</span>
|
||||
<textarea name="metode_pengerjaan" id="rtm_metode" rows="3" style="display:block;margin-top:4px;max-width:760px;width:100%">{{ rtm.metode_pengerjaan }}</textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Bentuk dan Format Luaran <span style="font-weight:normal;color:var(--grey)">(opsional)</span>
|
||||
<textarea name="luaran" id="rtm_luaran" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%">{{ rtm.luaran }}</textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Indikator, Kriteria dan Bobot Penilaian <span style="font-weight:normal;color:var(--grey)">(opsional — dokumentasi rubrik; bobot komponen di aplikasi tetap {{ komponen.bobot }}%, tak dihitung dari sini)</span>
|
||||
<textarea name="indikator_bobot" id="rtm_indikator" rows="4" style="display:block;margin-top:4px;max-width:760px;width:100%">{{ rtm.indikator_bobot }}</textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Jadwal Pelaksanaan <span style="font-weight:normal;color:var(--grey)">(opsional)</span>
|
||||
<input type="text" name="jadwal" id="rtm_jadwal" value="{{ rtm.jadwal }}" style="display:block;margin-top:4px;max-width:480px">
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Lain-lain <span style="font-weight:normal;color:var(--grey)">(opsional)</span>
|
||||
<textarea name="lain_lain" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%">{{ rtm.lain_lain }}</textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Daftar Rujukan <span style="font-weight:normal;color:var(--grey)">(opsional)</span>
|
||||
<textarea name="rujukan" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%">{{ rtm.rujukan }}</textarea>
|
||||
</label>
|
||||
{% endif %}
|
||||
<h2 class="bagian">NILAI MAHASISWA</h2>
|
||||
<div class="gulir" style="max-height:480px;overflow-y:auto">
|
||||
<table class="grid">
|
||||
<thead>
|
||||
@@ -71,7 +115,7 @@
|
||||
<td>{{ m.nim }}</td>
|
||||
<td>{{ m.nama }}</td>
|
||||
<td class="isian"><input type="number" name="nilai_{{ loop.index0 }}"
|
||||
min="0" max="100" step="0.01"
|
||||
min="0" max="100" step="any"
|
||||
value="{{ '%g'|format(m.nilai_awal) if m.nilai_awal is not none else '' }}"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -84,4 +128,67 @@
|
||||
<button class="tombol" type="submit">Simpan Sesi Ini</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if komponen.tugas %}
|
||||
<script>
|
||||
const BENTUK_TEMPLATE = {
|
||||
Diskusi: {
|
||||
deskripsi: 'Diskusikan {SUBCPMK} melalui studi kasus/topik terkait materi ini. Kelompok kecil (3-5 orang) menganalisis permasalahan dan menyusun rekomendasi.',
|
||||
metode: '1) Baca materi & bahan diskusi;\n2) Diskusi kelompok;\n3) Susun poin-poin kesimpulan;\n4) Presentasikan hasil diskusi.',
|
||||
luaran: 'Notulen/rangkuman diskusi kelompok dalam bentuk dokumen tertulis (maks. 3 halaman).',
|
||||
indikator: 'Ketepatan analisis (30%)\nKeaktifan berdiskusi (30%)\nKejelasan kesimpulan (20%)\nKerja sama kelompok (20%)\nBobot: 100%',
|
||||
jadwal: '1 minggu',
|
||||
},
|
||||
PjBL: {
|
||||
deskripsi: 'Kerjakan proyek yang menghasilkan solusi nyata terkait {SUBCPMK}, mulai dari perencanaan hingga penyerahan hasil akhir.',
|
||||
metode: '1) Identifikasi masalah;\n2) Rencanakan solusi;\n3) Kerjakan proyek secara bertahap;\n4) Uji & perbaiki;\n5) Serahkan hasil akhir beserta laporan.',
|
||||
luaran: 'Produk/prototipe beserta laporan tertulis yang menjelaskan proses pengerjaan.',
|
||||
indikator: 'Perencanaan (20%)\nProses pengerjaan (30%)\nKualitas hasil akhir (30%)\nLaporan (20%)\nBobot: 100%',
|
||||
jadwal: '2-3 minggu',
|
||||
},
|
||||
CBL: {
|
||||
deskripsi: 'Analisis studi kasus terkait {SUBCPMK}; identifikasi permasalahan dan usulkan solusi berbasis konsep yang dipelajari.',
|
||||
metode: '1) Baca & pahami kasus;\n2) Identifikasi masalah inti;\n3) Terapkan konsep untuk analisis;\n4) Rumuskan solusi;\n5) Tulis laporan.',
|
||||
luaran: 'Laporan analisis kasus tertulis, memuat identifikasi masalah, analisis, dan solusi yang diusulkan.',
|
||||
indikator: 'Ketepatan identifikasi masalah (25%)\nKedalaman analisis (35%)\nKetepatan solusi (25%)\nSistematika laporan (15%)\nBobot: 100%',
|
||||
jadwal: '1-2 minggu',
|
||||
},
|
||||
Esai: {
|
||||
deskripsi: 'Tulis esai mengenai {SUBCPMK} sepanjang 800-1200 kata, disertai argumen dan referensi yang relevan.',
|
||||
metode: '1) Tentukan tesis/argumen utama;\n2) Kumpulkan referensi pendukung;\n3) Susun kerangka esai;\n4) Tulis draf;\n5) Revisi & finalisasi.',
|
||||
luaran: 'Esai tertulis (800-1200 kata) dalam format dokumen (PDF/Word).',
|
||||
indikator: 'Ketepatan argumen (30%)\nKedalaman analisis (30%)\nSistematika penulisan (20%)\nPenggunaan referensi (20%)\nBobot: 100%',
|
||||
jadwal: '1 minggu',
|
||||
},
|
||||
Tes: {
|
||||
deskripsi: 'Kerjakan soal-soal tes tertulis terkait {SUBCPMK} sesuai instruksi yang diberikan.',
|
||||
metode: '1) Baca instruksi & soal dengan teliti;\n2) Kerjakan sesuai alokasi waktu;\n3) Periksa kembali jawaban sebelum dikumpulkan.',
|
||||
luaran: 'Lembar jawaban tes (tertulis/daring).',
|
||||
indikator: 'Ketepatan jawaban per butir soal, dijumlah sesuai bobot masing-masing.\nBobot: 100%',
|
||||
jadwal: '1x pertemuan (sesuai jadwal tes)',
|
||||
},
|
||||
};
|
||||
|
||||
function isiOtomatisTugas() {
|
||||
const bentuk = document.getElementById('bentuk_tugas').value;
|
||||
const tmpl = BENTUK_TEMPLATE[bentuk];
|
||||
if (!tmpl) return;
|
||||
const checked = Array.from(document.querySelectorAll('.pilih-sub input[name="subs"]:checked')).map(cb => cb.value);
|
||||
const subcpmk = checked.length ? checked.join(', ') : 'Sub-CPMK terkait';
|
||||
const isi = (id, teks) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el && !el.value.trim()) el.value = teks.replace('{SUBCPMK}', subcpmk);
|
||||
};
|
||||
isi('rtm_deskripsi', tmpl.deskripsi);
|
||||
isi('rtm_metode', tmpl.metode);
|
||||
isi('rtm_luaran', tmpl.luaran);
|
||||
isi('rtm_indikator', tmpl.indikator);
|
||||
isi('rtm_jadwal', tmpl.jadwal);
|
||||
}
|
||||
function toggleIsiOtomatis() {
|
||||
const bentuk = document.getElementById('bentuk_tugas').value;
|
||||
document.getElementById('tombol_isi_otomatis').hidden = !bentuk;
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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