Tambah peran Dosen MK/RPS+MK, komponen penilaian baku, dan bantu isi RPS

- Peran dosen dipecah jadi dosen_mk (nilai & portofolio saja, RPS baca-only)
  dan dosen_rps_mk (bisa edit RPS juga); migrasi otomatis dari skema lama.
- Rekap Bobot Penilaian RPS pakai 6 komponen teknik asesmen baku prodi;
  total wajib tepat 100%, komponen individual boleh dikosongkan.
- Dropdown & daftar Sub-CPMK tak lagi terpotong teksnya.
- Bagian F (Rencana Mingguan): tombol "Isi Otomatis" per baris (termasuk
  usulan Bobot) yang muncul setelah Sub-CPMK dipilih, plus panel contoh
  teks bantu per kolom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Power BI Dev
2026-07-21 08:45:56 +07:00
parent 9be6bd03e3
commit a45b08b00e
7 changed files with 175 additions and 45 deletions
+36 -1
View File
@@ -105,6 +105,39 @@ def get_conn() -> sqlite3.Connection:
return conn
def _migrasi_role_pengguna(conn: sqlite3.Connection) -> None:
"""Skema lama hanya kenal role 'dosen'; pecah jadi dosen_mk/dosen_rps_mk.
Akun 'dosen' lama dipetakan ke 'dosen_rps_mk' agar hak akses (termasuk
edit RPS) yang sudah mereka miliki tidak berkurang oleh migrasi ini.
"""
row = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='pengguna'"
).fetchone()
if not row or "dosen_rps_mk" in row["sql"]:
return
conn.execute("ALTER TABLE pengguna RENAME TO pengguna_lama")
conn.execute(
"""CREATE TABLE pengguna (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
nama TEXT NOT NULL,
role TEXT NOT NULL
CHECK (role IN ('kaprodi','dosen_mk','dosen_rps_mk')),
pw_hash TEXT NOT NULL,
dibuat TEXT NOT NULL
)"""
)
conn.execute(
"""INSERT INTO pengguna (id, username, nama, role, pw_hash, dibuat)
SELECT id, username, nama,
CASE WHEN role='dosen' THEN 'dosen_rps_mk' ELSE role END,
pw_hash, dibuat
FROM pengguna_lama"""
)
conn.execute("DROP TABLE pengguna_lama")
def init_db() -> None:
with get_conn() as conn:
for tabel in ("rps", "portofolio", "nilai"):
@@ -141,11 +174,13 @@ def init_db() -> None:
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
nama TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('kaprodi','dosen')),
role TEXT NOT NULL
CHECK (role IN ('kaprodi','dosen_mk','dosen_rps_mk')),
pw_hash TEXT NOT NULL,
dibuat TEXT NOT NULL
)"""
)
_migrasi_role_pengguna(conn)
conn.execute(
"""CREATE TABLE IF NOT EXISTS sesi (
token TEXT PRIMARY KEY,
+4 -1
View File
@@ -218,7 +218,10 @@ def build_rps(d: dict) -> bytes:
r.bold = True
r.font.size = Pt(11)
komponen = d.get("komponen", [])
komponen = [
k for k in d.get("komponen", [])
if k.get("subs") or str(k.get("bobot") or "").strip() not in ("", "0", "0.0")
]
tk = doc.add_table(rows=1 + max(len(komponen), 1), cols=4)
_grid_borders(tk)
for ci, judul in enumerate(
+61 -27
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
"""Aplikasi Penyusunan RPS & Portofolio — Prodi Informatika UNTAN."""
import json
import pathlib
import re
@@ -17,6 +18,9 @@ BASE = pathlib.Path(__file__).resolve().parent
app = FastAPI(title="RPS & Portofolio — Informatika UNTAN")
app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static")
templates = Jinja2Templates(directory=str(BASE / "templates"))
templates.env.filters["tojson"] = (
lambda v: json.dumps(v, ensure_ascii=False).replace("</", "<\\/")
)
db.init_db()
db.kurikulum()
@@ -46,9 +50,19 @@ def render(request: Request, nama: str, ctx: dict):
return templates.TemplateResponse(request, nama, ctx)
def boleh_kelola(user: dict, kode: str) -> bool:
"""Pengampu MK ini atau kaprodi — syarat edit & akses nilai."""
return user["role"] == "kaprodi" or kode in db.mk_diampu(user["id"])
def boleh_rps(user: dict, kode: str) -> bool:
"""Kaprodi, atau dosen RPS+MK yang ditugaskan ke MK ini — syarat edit RPS."""
if user["role"] == "kaprodi":
return True
return user["role"] == "dosen_rps_mk" and kode in db.mk_diampu(user["id"])
def boleh_mk(user: dict, kode: str) -> bool:
"""Kaprodi, atau dosen (MK / RPS+MK) yang ditugaskan ke MK ini —
syarat edit nilai & portofolio."""
if user["role"] == "kaprodi":
return True
return user["role"] in ("dosen_mk", "dosen_rps_mk") and kode in db.mk_diampu(user["id"])
def _masuk(user_id: int) -> RedirectResponse:
@@ -147,7 +161,8 @@ async def admin_buat_pengguna(request: Request):
form = await request.form()
nip = _rapikan_nip(form.get("nip") or "")
nama = (form.get("nama") or "").strip()
role = "kaprodi" if form.get("role") == "kaprodi" else "dosen"
role_in = form.get("role")
role = role_in if role_in in ("kaprodi", "dosen_mk", "dosen_rps_mk") else "dosen_mk"
if not nip or not nama:
return RedirectResponse(
"/admin?pesan=Nama+dan+NIP+wajib+diisi.", status_code=303)
@@ -243,15 +258,33 @@ async def admin_impor_mahasiswa(request: Request):
return RedirectResponse(f"/admin?pesan={pesan}", status_code=303)
# bobot default komponen — dari sheet `Asessement` draft kurikulum (Tabel 25)
# komponen penilaian tetap — teknik asesmen baku prodi; bobot boleh 0 (opsional,
# tak dipakai MK ybs) tapi total seluruh komponen tak boleh melebihi 100%.
KOMPONEN_DEFAULT = [
{"nama": "Kuis", "subs": [], "bobot": 15},
{"nama": "Ujian Tengah Semester (UTS)", "subs": [], "bobot": 20},
{"nama": "Ujian Akhir Semester (UAS)", "subs": [], "bobot": 30},
{"nama": "Tugas / Praktikum / Proyek", "subs": [], "bobot": 35},
{"nama": "Partisipasi (Kuis)", "subs": [], "bobot": 10},
{"nama": "Observasi (Praktik/Tugas)", "subs": [], "bobot": 20},
{"nama": "Unjuk Kerja (Presentasi)", "subs": [], "bobot": 10},
{"nama": "Tes Tulis (UTS)", "subs": [], "bobot": 25},
{"nama": "Tes Tulis (UAS)", "subs": [], "bobot": 25},
{"nama": "Tes Lisan (Tugas Kelompok)", "subs": [], "bobot": 10},
]
def _normalisasi_komponen(tersimpan: list[dict]) -> list[dict]:
"""Selaraskan komponen tersimpan ke struktur baku KOMPONEN_DEFAULT (cocokkan
berdasar nama; RPS lama dengan nama komponen berbeda kehilangan isian itu)."""
by_nama = {k.get("nama"): k for k in tersimpan}
out = []
for default in KOMPONEN_DEFAULT:
ada = by_nama.get(default["nama"])
out.append({
"nama": default["nama"],
"subs": ada.get("subs", []) if ada else [],
"bobot": ada.get("bobot", "") if ada else "",
})
return out
def minggu_default() -> list[dict]:
rows = []
for w in range(1, 17):
@@ -289,7 +322,7 @@ def beranda(request: Request, semua: int = 0):
n_total = len(daftar)
mk_saya = db.mk_diampu(request.state.user["id"])
# dosen: bawaan hanya MK yang diampu; ?semua=1 menampilkan seluruh MK
hanya_saya = (request.state.user["role"] == "dosen"
hanya_saya = (request.state.user["role"] != "kaprodi"
and bool(mk_saya) and not semua)
if hanya_saya:
daftar = [m for m in daftar if m["kode"] in mk_saya]
@@ -324,6 +357,7 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
if tersimpan:
f = tersimpan["data"]
f.setdefault("referensi_teks", "\n".join(f.get("referensi", [])))
f["komponen"] = _normalisasi_komponen(f.get("komponen", []))
ctx_status = tersimpan
else:
f = form_kosong()
@@ -339,17 +373,19 @@ def form_rps(request: Request, kode: str, pesan: str = ""):
{"peta": peta, "f": f, "status": ctx_status["status"],
"updated_at": ctx_status["updated_at"], "updated_by": ctx_status["updated_by"],
"tahun": db.tahun_aktif(), "pesan": pesan,
"boleh_edit": boleh_kelola(request.state.user, kode),
"tahun_lain": db.tahun_rps_lain(kode) if not tersimpan else []},
"boleh_edit": boleh_rps(request.state.user, kode),
"tahun_lain": db.tahun_rps_lain(kode) if not tersimpan else [],
"sub_uraian": {s["kode"]: s["uraian"] for s in peta["subs"]}},
)
@app.post("/rps/{kode}/salin")
async def salin_rps(request: Request, kode: str):
"""Duplikasi RPS dari tahun ajaran lain ke tahun aktif (status: draft)."""
if not boleh_kelola(request.state.user, kode):
if not boleh_rps(request.state.user, kode):
return RedirectResponse(
f"/rps/{kode}?pesan=Anda+bukan+pengampu+MK+ini.", status_code=303)
f"/rps/{kode}?pesan=Anda+tidak+memiliki+hak+edit+RPS+untuk+MK+ini.",
status_code=303)
form = await request.form()
dari = form.get("dari") or ""
sumber = db.ambil_rps(kode, tahun=dari) if dari else None
@@ -371,9 +407,10 @@ async def simpan_rps(request: Request, kode: str):
peta = db.peta_mk(kode)
if not peta:
return RedirectResponse("/", status_code=303)
if not boleh_kelola(request.state.user, kode):
if not boleh_rps(request.state.user, kode):
return RedirectResponse(
f"/rps/{kode}?pesan=Anda+bukan+pengampu+MK+ini.", status_code=303)
f"/rps/{kode}?pesan=Anda+tidak+memiliki+hak+edit+RPS+untuk+MK+ini.",
status_code=303)
form = await request.form()
identitas = {
@@ -396,12 +433,9 @@ async def simpan_rps(request: Request, kode: str):
"bobot": (form.get(f"m{w}_bobot") or "").strip(),
})
komponen = []
for i in range(1, len(KOMPONEN_DEFAULT) + 1):
nama = (form.get(f"k{i}_nama") or "").strip()
if not nama:
continue
for i, default in enumerate(KOMPONEN_DEFAULT, 1):
komponen.append({
"nama": nama,
"nama": default["nama"],
"subs": [s for s in form.getlist(f"k{i}_subs")],
"bobot": (form.get(f"k{i}_bobot") or "").strip(),
})
@@ -540,7 +574,7 @@ def _gabung_nilai(lama: dict, baru: list[dict], sumber: str, oleh: str) -> dict:
def form_nilai(request: Request, kode: str, pesan: str = ""):
if not db.peta_mk(kode):
return RedirectResponse("/", status_code=303)
if not boleh_kelola(request.state.user, kode):
if not boleh_mk(request.state.user, kode):
return RedirectResponse(
f"/rps/{kode}?pesan=Nilai+mahasiswa+hanya+dibuka+untuk+pengampu+MK+dan+kaprodi.",
status_code=303)
@@ -552,7 +586,7 @@ def template_nilai(request: Request, kode: str):
peta = db.peta_mk(kode)
if not peta:
return RedirectResponse("/", status_code=303)
if not boleh_kelola(request.state.user, kode):
if not boleh_mk(request.state.user, kode):
return RedirectResponse(f"/rps/{kode}", status_code=303)
isi = nilai_xlsx.buat_template(peta, db.tahun_aktif(), db.mahasiswa_mk(kode))
return Response(
@@ -568,7 +602,7 @@ async def simpan_nilai(request: Request, kode: str):
peta = db.peta_mk(kode)
if not peta:
return RedirectResponse("/", status_code=303)
if not boleh_kelola(request.state.user, kode):
if not boleh_mk(request.state.user, kode):
return RedirectResponse(
f"/rps/{kode}?pesan=Nilai+mahasiswa+hanya+dibuka+untuk+pengampu+MK+dan+kaprodi.",
status_code=303)
@@ -705,7 +739,7 @@ def form_porto(request: Request, kode: str, pesan: str = ""):
"status": tersimpan["status"] if tersimpan else "",
"updated_at": tersimpan["updated_at"] if tersimpan else "",
"updated_by": tersimpan["updated_by"] if tersimpan else "",
"boleh_edit": boleh_kelola(request.state.user, kode),
"boleh_edit": boleh_mk(request.state.user, kode),
"ada_rps": bool(rps), "ada_nilai": bool(nilai)},
)
@@ -714,7 +748,7 @@ def form_porto(request: Request, kode: str, pesan: str = ""):
async def simpan_porto(request: Request, kode: str):
if not db.peta_mk(kode):
return RedirectResponse("/", status_code=303)
if not boleh_kelola(request.state.user, kode):
if not boleh_mk(request.state.user, kode):
return RedirectResponse(
f"/portofolio/{kode}?pesan=Anda+bukan+pengampu+MK+ini.", status_code=303)
form = await request.form()
@@ -810,7 +844,7 @@ def _bahan_porto(kode: str) -> dict | None:
@app.get("/portofolio/{kode}/unduh")
def unduh_porto(request: Request, kode: str):
# dokumen memuat matriks nilai per mahasiswa — batasi pengampu + kaprodi
if not boleh_kelola(request.state.user, kode):
if not boleh_mk(request.state.user, kode):
return RedirectResponse(
f"/portofolio/{kode}?pesan=Unduhan+portofolio+memuat+nilai+mahasiswa+-+hanya+pengampu+dan+kaprodi.",
status_code=303)
+9 -6
View File
@@ -5,15 +5,17 @@
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
<h2 class="bagian">A. Akun Pengguna &amp; Penugasan Mata Kuliah</h2>
<p class="keterangan">Dosen hanya dapat mengedit RPS/portofolio dan melihat nilai
mahasiswa pada MK yang ditugaskan kepadanya. Kaprodi dapat mengakses semuanya.</p>
<p class="keterangan"><b>Dosen RPS + MK</b> dapat mengedit RPS, nilai, dan portofolio
pada MK yang ditugaskan. <b>Dosen MK</b> hanya dapat mengedit nilai &amp; portofolio
pada MK yang ditugaskan — halaman RPS terbuka hanya untuk dibaca. Kaprodi dapat
mengakses semuanya.</p>
<div class="gulir">
<table class="grid">
<thead>
<tr>
<th style="width:170px">Nama</th>
<th style="width:160px">NIP</th>
<th style="width:90px">Peran</th>
<th style="width:130px">Peran</th>
<th>MK yang Diampu</th>
<th style="width:340px">Tindakan</th>
</tr>
@@ -23,7 +25,7 @@
<tr>
<td>{{ p.nama }}</td>
<td>{{ p.username }}</td>
<td>{{ 'Kaprodi' if p.role == 'kaprodi' else 'Dosen' }}</td>
<td>{{ {'kaprodi': 'Kaprodi', 'dosen_rps_mk': 'Dosen RPS + MK', 'dosen_mk': 'Dosen MK'}[p.role] }}</td>
<td>
{% if p.role == 'kaprodi' %}
<i style="color:var(--grey)">semua MK</i>
@@ -71,8 +73,9 @@
<label>Nama lengkap <input type="text" name="nama" required style="width:230px"></label>
<label>NIP <input type="text" name="nip" required style="width:180px"></label>
<label>Peran
<select name="role" style="width:110px">
<option value="dosen" selected>Dosen</option>
<select name="role" style="width:150px">
<option value="dosen_mk" selected>Dosen MK</option>
<option value="dosen_rps_mk">Dosen RPS + MK</option>
<option value="kaprodi">Kaprodi</option>
</select>
</label>
+1 -1
View File
@@ -36,7 +36,7 @@
{% if user.role == 'kaprodi' %}{{ tautan('/admin', 'Kelola Akun & Mahasiswa') }}{% endif %}
</span>
<span class="menu-kanan">
<b>{{ user.nama }}</b> — {{ 'Kaprodi' if user.role == 'kaprodi' else 'Dosen' }}
<b>{{ user.nama }}</b> — {{ {'kaprodi': 'Kaprodi', 'dosen_rps_mk': 'Dosen RPS + MK', 'dosen_mk': 'Dosen MK'}[user.role] }}
· <a href="/keluar">Keluar</a>
</span>
</nav>
+1 -1
View File
@@ -8,7 +8,7 @@
<div><b id="kpi-porto">{{ st_porto.values()|selectattr('status', 'equalto', 'selesai')|list|length }}</b> <span>portofolio selesai</span> <span class="kpi-pct" id="pct-porto"></span></div>
</div>
{% if user.role == 'dosen' %}
{% if user.role != 'kaprodi' %}
{% if hanya_saya %}
<div class="info" style="margin-top:0">Menampilkan <b>{{ daftar|length }} MK yang Anda ampu</b>.
RPS mata kuliah lain tetap terbuka baca — <a href="/?semua=1">tampilkan seluruh {{ n_total }} MK</a>.</div>
+63 -8
View File
@@ -16,8 +16,10 @@
{% if pesan %}<div class="info">{{ pesan }}</div>{% endif %}
{% if not boleh_edit %}
<div class="info">Anda bukan pengampu MK ini — form terbuka <b>hanya untuk dibaca</b>.
Hubungi kaprodi bila penugasan MK Anda perlu diubah.</div>
<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 %}
@@ -97,7 +99,23 @@
<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 %.</p>
<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>
@@ -122,14 +140,20 @@
<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">
<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[:60] }}{% if s.uraian|length > 60 %}…{% endif %}</option>
<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>
@@ -145,7 +169,10 @@
</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">Centang Sub-CPMK yang dinilai tiap komponen. Setiap Sub-CPMK harus tercakup minimal satu komponen agar CPMK/CPL-nya terukur.</p>
<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>
<div class="gulir">
<table class="grid">
<thead>
@@ -155,11 +182,11 @@
{% for k in f.komponen %}
{% set ki = loop.index %}
<tr>
<td class="isian"><input type="text" name="k{{ ki }}_nama" value="{{ k.nama }}"></td>
<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[:70] }}{% if s.uraian|length > 70 %}…{% endif %}</label>
<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>
@@ -194,6 +221,34 @@
</form>
<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; });