Compare commits
2
Commits
d6469a01dc
...
af369883f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af369883f3 | ||
|
|
b85c75bb31 |
@@ -156,7 +156,7 @@ def _migrasi_kelas_penugasan(conn: sqlite3.Connection) -> None:
|
||||
|
||||
def init_db() -> None:
|
||||
with get_conn() as conn:
|
||||
for tabel in ("rps", "portofolio", "nilai"):
|
||||
for tabel in ("rps", "portofolio", "nilai", "rtm"):
|
||||
conn.execute(
|
||||
f"""CREATE TABLE IF NOT EXISTS {tabel} (
|
||||
mk_kode TEXT NOT NULL,
|
||||
@@ -259,7 +259,7 @@ def simpan_pengaturan(d: dict) -> None:
|
||||
)
|
||||
|
||||
|
||||
_TABEL_DOK = ("rps", "portofolio", "nilai")
|
||||
_TABEL_DOK = ("rps", "portofolio", "nilai", "rtm")
|
||||
|
||||
|
||||
def simpan_dok(tabel: str, mk_kode: str, data: dict, status: str, updated_by: str,
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Generator Rancangan Tugas Mahasiswa (RTM) (.docx).
|
||||
Mengikuti format resmi `FORM RPS & RTM UNTAN (draft).docx`: tabel identitas
|
||||
(Mata Kuliah/Kode MK/Tugas ke/SKS/Dosen Pengampu/Semester) diikuti tabel
|
||||
RANCANGAN TUGAS MAHASISWA per tugas (Bentuk, Judul, Sub-CPMK, Deskripsi,
|
||||
Metode Pengerjaan, Bentuk & Format Luaran, Indikator/Kriteria/Bobot,
|
||||
Jadwal Pelaksanaan, Lain-lain, Daftar Rujukan). Dokumen ini berdiri sendiri,
|
||||
tak memuat data nilai/skor mahasiswa sama sekali.
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Cm, Pt, RGBColor
|
||||
|
||||
DARK_BLUE = "1F4E79"
|
||||
LIGHT_BLUE = "D6E4F0"
|
||||
WHITE = "FFFFFF"
|
||||
|
||||
FAKULTAS = os.environ.get("FAKULTAS", "FAKULTAS TEKNIK")
|
||||
PRODI = os.environ.get("PRODI", "PROGRAM STUDI INFORMATIKA")
|
||||
|
||||
|
||||
def _shd(cell, hex_color):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
el = OxmlElement("w:shd")
|
||||
el.set(qn("w:val"), "clear")
|
||||
el.set(qn("w:color"), "auto")
|
||||
el.set(qn("w:fill"), hex_color)
|
||||
tc_pr.append(el)
|
||||
|
||||
|
||||
def _ct(cell, text, bold=False, italic=False, size=10, color=None,
|
||||
align=WD_ALIGN_PARAGRAPH.LEFT):
|
||||
p = cell.paragraphs[0]
|
||||
p.alignment = align
|
||||
p.paragraph_format.space_before = Pt(2)
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
run = p.add_run()
|
||||
for i, line in enumerate(str(text or "").split("\n")):
|
||||
if i:
|
||||
run.add_break()
|
||||
run.add_text(line)
|
||||
run.bold = bold
|
||||
run.italic = italic
|
||||
run.font.name = "Times New Roman"
|
||||
run.font.size = Pt(size)
|
||||
if color:
|
||||
run.font.color.rgb = RGBColor.from_string(color)
|
||||
|
||||
|
||||
def _hdr(cell, text, size=10):
|
||||
_shd(cell, DARK_BLUE)
|
||||
_ct(cell, text, bold=True, color=WHITE, size=size, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
|
||||
|
||||
def _borders(table):
|
||||
tbl_pr = table._tbl.tblPr
|
||||
el = OxmlElement("w:tblBorders")
|
||||
for sisi in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
||||
b = OxmlElement(f"w:{sisi}")
|
||||
b.set(qn("w:val"), "single")
|
||||
b.set(qn("w:sz"), "4")
|
||||
b.set(qn("w:color"), "000000")
|
||||
el.append(b)
|
||||
tbl_pr.append(el)
|
||||
|
||||
|
||||
def build_rtm(d: dict) -> bytes:
|
||||
doc = Document()
|
||||
for sec in doc.sections:
|
||||
sec.top_margin = sec.bottom_margin = Cm(2.5)
|
||||
sec.left_margin = sec.right_margin = Cm(2.5)
|
||||
|
||||
def para(text, size=12, bold=False, align=WD_ALIGN_PARAGRAPH.LEFT, before=6, after=3):
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = align
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
r = p.add_run(text)
|
||||
r.bold = bold
|
||||
r.font.name = "Times New Roman"
|
||||
r.font.size = Pt(size)
|
||||
return p
|
||||
|
||||
para("RANCANGAN TUGAS MAHASISWA (RTM)", size=16, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER)
|
||||
para(PRODI, size=12, bold=True, align=WD_ALIGN_PARAGRAPH.CENTER, before=0)
|
||||
para(f"{FAKULTAS} — UNIVERSITAS TANJUNGPURA", size=11, align=WD_ALIGN_PARAGRAPH.CENTER, before=0)
|
||||
para(f"Tahun Ajaran {d.get('tahun', '')}", size=11, align=WD_ALIGN_PARAGRAPH.CENTER,
|
||||
before=0, after=10)
|
||||
|
||||
daftar_tugas = d.get("tugas", [])
|
||||
if not daftar_tugas:
|
||||
para("Belum ada Tugas yang diisi untuk RTM ini.", after=6)
|
||||
|
||||
for idx, t in enumerate(daftar_tugas, 1):
|
||||
if idx > 1:
|
||||
doc.add_page_break()
|
||||
|
||||
ti = doc.add_table(rows=3, cols=4)
|
||||
_borders(ti)
|
||||
baris = [
|
||||
("Mata Kuliah", d.get("nama", ""), "Kode MK", d.get("kode", "")),
|
||||
("Tugas ke", str(idx), "SKS", str(d.get("sks", "") or "—")),
|
||||
("Dosen Pengampu", d.get("pengampu", "") or "—", "Semester",
|
||||
str(d.get("semester", "") or "—")),
|
||||
]
|
||||
for ri, (l1, v1, l2, v2) in enumerate(baris):
|
||||
_shd(ti.cell(ri, 0), LIGHT_BLUE)
|
||||
_ct(ti.cell(ri, 0), l1, bold=True)
|
||||
_ct(ti.cell(ri, 1), v1)
|
||||
_shd(ti.cell(ri, 2), LIGHT_BLUE)
|
||||
_ct(ti.cell(ri, 2), l2, bold=True)
|
||||
_ct(ti.cell(ri, 3), v2)
|
||||
doc.add_paragraph()
|
||||
|
||||
subs_teks = ", ".join(t.get("subs") or []) or "—"
|
||||
jadwal = "; ".join(x for x in (
|
||||
f"Alokasi waktu: {t['alokasi_waktu']}" if t.get("alokasi_waktu") else "",
|
||||
f"Tanggal: {t['tanggal']}" if t.get("tanggal") else "",
|
||||
) if x) or "—"
|
||||
rows = [
|
||||
("BENTUK TUGAS", t.get("bentuk_tugas", "") or "—"),
|
||||
("JUDUL TUGAS", t.get("judul", "") or "—"),
|
||||
("SUB-CPMK YANG DICAPAI", subs_teks),
|
||||
("DESKRIPSI TUGAS", t.get("deskripsi", "") or "—"),
|
||||
("METODE PENGERJAAN TUGAS", t.get("metode_pengerjaan", "") or "—"),
|
||||
("BENTUK DAN FORMAT LUARAN", t.get("luaran", "") or "—"),
|
||||
("INDIKATOR, KRITERIA DAN BOBOT PENILAIAN", t.get("indikator_bobot", "") or "—"),
|
||||
("JADWAL PELAKSANAAN", jadwal),
|
||||
("LAIN-LAIN", t.get("lain_lain", "") or "—"),
|
||||
("DAFTAR RUJUKAN", t.get("rujukan", "") or "—"),
|
||||
]
|
||||
tt = doc.add_table(rows=1 + len(rows), cols=2)
|
||||
_borders(tt)
|
||||
_hdr(tt.cell(0, 0), "RANCANGAN TUGAS MAHASISWA")
|
||||
tt.cell(0, 0).merge(tt.cell(0, 1))
|
||||
for ri, (label, isi) in enumerate(rows, 1):
|
||||
_shd(tt.cell(ri, 0), LIGHT_BLUE)
|
||||
_ct(tt.cell(ri, 0), label, bold=True, size=9)
|
||||
_ct(tt.cell(ri, 1), isi, size=10)
|
||||
tt.rows[ri].cells[0].width = Cm(5.5)
|
||||
tt.rows[ri].cells[1].width = Cm(11)
|
||||
|
||||
buf = io.BytesIO()
|
||||
doc.save(buf)
|
||||
return buf.getvalue()
|
||||
+201
-1
@@ -13,6 +13,7 @@ from fastapi.templating import Jinja2Templates
|
||||
from . import auth, db, hitung, nilai_xlsx
|
||||
from .generator.portofolio_docx import build_portofolio
|
||||
from .generator.rps_untan import build_rps
|
||||
from .generator.rtm_docx import build_rtm
|
||||
|
||||
BASE = pathlib.Path(__file__).resolve().parent
|
||||
|
||||
@@ -358,6 +359,43 @@ def form_kosong() -> dict:
|
||||
}
|
||||
|
||||
|
||||
# ── RTM (Rancangan Tugas Mahasiswa) — dokumen sendiri, terpisah dari Nilai ──
|
||||
|
||||
RTM_TUGAS_KOSONG = {
|
||||
"judul": "", "subs": [], "bentuk_tugas": "", "deskripsi": "",
|
||||
"metode_pengerjaan": "", "luaran": "", "indikator_bobot": "",
|
||||
"alokasi_waktu": "", "tanggal": "", "lain_lain": "", "rujukan": "",
|
||||
}
|
||||
|
||||
|
||||
def _ambil_rtm(kode: str) -> tuple[list[dict], dict]:
|
||||
"""Daftar entri Tugas RTM yang sudah tersimpan (bisa kosong — halaman RTM
|
||||
menambah blok baru secara dinamis lewat JS, bukan slot tetap) — murni
|
||||
dokumentasi, tak menyentuh data Nilai."""
|
||||
tersimpan = db.ambil_dok("rtm", kode)
|
||||
tugas = [dict(RTM_TUGAS_KOSONG, **t) for t in ((tersimpan or {}).get("data", {}).get("tugas") or [])]
|
||||
meta = tersimpan or {"status": "", "updated_at": "", "updated_by": ""}
|
||||
return tugas, meta
|
||||
|
||||
|
||||
def _bahan_rtm(kode: str) -> dict | None:
|
||||
peta = db.peta_mk(kode)
|
||||
if not peta:
|
||||
return None
|
||||
rps = db.ambil_rps(kode)
|
||||
ident = ((rps or {}).get("data") or {}).get("identitas") or {}
|
||||
tersimpan = db.ambil_dok("rtm", kode)
|
||||
tugas = [t for t in ((tersimpan or {}).get("data", {}).get("tugas") or [])
|
||||
if t.get("judul") or t.get("bentuk_tugas") or t.get("deskripsi")]
|
||||
return {
|
||||
"tahun": db.tahun_aktif(),
|
||||
"nama": peta["nama"], "kode": peta["kode"],
|
||||
"sks": ident.get("sks", ""), "semester": ident.get("semester", ""),
|
||||
"pengampu": ident.get("pengampu", ""),
|
||||
"tugas": tugas,
|
||||
}
|
||||
|
||||
|
||||
def _terapkan_override_sub(peta: dict, rps_data: dict | None) -> dict:
|
||||
"""Salinan `peta` dengan uraian Sub-CPMK ditimpa override tersimpan di RPS
|
||||
MK ini (bila ada) — dipakai konsisten di form RPS, unduhan RPS, dan
|
||||
@@ -594,6 +632,82 @@ def unduh_rps(kode: str):
|
||||
)
|
||||
|
||||
|
||||
@app.post("/rps/{kode}/rtm")
|
||||
async def simpan_rtm(request: Request, kode: str):
|
||||
"""RTM (Rancangan Tugas Mahasiswa) — dokumen sendiri, sekadar dokumentasi
|
||||
rencana tugas per MK; tak ada kaitan dengan data Nilai/skor sama sekali."""
|
||||
peta = db.peta_mk(kode)
|
||||
if not peta:
|
||||
return RedirectResponse("/", status_code=303)
|
||||
if not boleh_rps(request.state.user, kode):
|
||||
return RedirectResponse(
|
||||
f"/rps/{kode}/rtm?pesan=Anda+tidak+memiliki+hak+edit+RPS+untuk+MK+ini.",
|
||||
status_code=303)
|
||||
form = await request.form()
|
||||
# blok bisa ditambah/dihapus bebas di JS (lihat form_rtm.html) sehingga
|
||||
# indeksnya boleh tidak berurutan — cari semua "judul_N" yang ada, bukan
|
||||
# mengandalkan hitungan 0,1,2,... berurutan tanpa celah.
|
||||
idxs = sorted({int(m.group(1)) for k in form.keys()
|
||||
for m in [re.fullmatch(r"judul_(\d+)", k)] if m})
|
||||
tugas = []
|
||||
for i in idxs:
|
||||
entri = {
|
||||
"judul": (form.get(f"judul_{i}") or "").strip(),
|
||||
"subs": form.getlist(f"subs_{i}"),
|
||||
"bentuk_tugas": (form.get(f"bentuk_tugas_{i}") or "").strip(),
|
||||
"deskripsi": (form.get(f"deskripsi_{i}") or "").strip(),
|
||||
"metode_pengerjaan": (form.get(f"metode_pengerjaan_{i}") or "").strip(),
|
||||
"luaran": (form.get(f"luaran_{i}") or "").strip(),
|
||||
"indikator_bobot": (form.get(f"indikator_bobot_{i}") or "").strip(),
|
||||
"alokasi_waktu": (form.get(f"alokasi_waktu_{i}") or "").strip(),
|
||||
"tanggal": (form.get(f"tanggal_{i}") or "").strip(),
|
||||
"lain_lain": (form.get(f"lain_lain_{i}") or "").strip(),
|
||||
"rujukan": (form.get(f"rujukan_{i}") or "").strip(),
|
||||
}
|
||||
if entri["judul"] or entri["bentuk_tugas"] or entri["deskripsi"]:
|
||||
tugas.append(entri)
|
||||
db.simpan_dok("rtm", kode, {"tugas": tugas}, "selesai", request.state.user["nama"])
|
||||
return RedirectResponse(
|
||||
f"/rps/{kode}/rtm?pesan=RTM+tersimpan+-+{len(tugas)}+tugas+terisi.",
|
||||
status_code=303)
|
||||
|
||||
|
||||
@app.get("/rps/{kode}/rtm", response_class=HTMLResponse)
|
||||
def form_rtm(request: Request, kode: str, pesan: str = ""):
|
||||
peta = db.peta_mk(kode)
|
||||
if not peta:
|
||||
return RedirectResponse("/", status_code=303)
|
||||
rps = db.ambil_rps(kode)
|
||||
if not rps or rps["status"] != "selesai":
|
||||
return RedirectResponse(
|
||||
f"/rps/{kode}?pesan=RTM+hanya+bisa+diisi+setelah+RPS+ditandai+selesai.",
|
||||
status_code=303)
|
||||
rtm_tugas, rtm_meta = _ambil_rtm(kode)
|
||||
return render(
|
||||
request, "form_rtm.html",
|
||||
{"peta": peta, "tahun": db.tahun_aktif(), "pesan": pesan,
|
||||
"boleh_edit": boleh_rps(request.state.user, kode),
|
||||
"rtm_tugas": rtm_tugas, "rtm_updated_at": rtm_meta.get("updated_at", ""),
|
||||
"rtm_updated_by": rtm_meta.get("updated_by", "")},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/rps/{kode}/rtm/unduh")
|
||||
def unduh_rtm(kode: str):
|
||||
bahan = _bahan_rtm(kode)
|
||||
if not bahan or not bahan["tugas"]:
|
||||
return RedirectResponse(
|
||||
f"/rps/{kode}/rtm?pesan=Belum+ada+Tugas+RTM+yang+diisi+untuk+diunduh.",
|
||||
status_code=303)
|
||||
isi = build_rtm(bahan)
|
||||
nama_file = re.sub(r"[^A-Za-z0-9_-]+", "_", f"RTM_{kode}_{bahan['nama']}") + ".docx"
|
||||
return Response(
|
||||
content=isi,
|
||||
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
headers={"Content-Disposition": f'attachment; filename="{nama_file}"'},
|
||||
)
|
||||
|
||||
|
||||
# ═════════════════════════ TAHAP 2: NILAI & PENGUKURAN ═════════════════════════
|
||||
|
||||
|
||||
@@ -703,6 +817,61 @@ def _tabel_komponen(grid: list[dict], komponen_daftar: list[dict],
|
||||
return out
|
||||
|
||||
|
||||
def _rincian_bobot(kode: str) -> tuple[list[dict], dict[str, list[dict]]]:
|
||||
"""Rincian pembagian bobot komponen (RPS bagian G) ke tiap Sub-CPMK —
|
||||
bobot_komponen ÷ jumlah Sub-CPMK komponen itu, dijumlah per Sub-CPMK bila
|
||||
diukur >1 komponen. Murni dari konfigurasi RPS (tak bergantung nilai
|
||||
mahasiswa) — dipakai untuk menjelaskan metodologi bobot di halaman nilai."""
|
||||
rps = db.ambil_rps(kode)
|
||||
per_komponen: list[dict] = []
|
||||
per_sub: dict[str, list[dict]] = {}
|
||||
if not rps:
|
||||
return per_komponen, per_sub
|
||||
for k in rps["data"].get("komponen", []):
|
||||
subs = k.get("subs") or []
|
||||
if not subs:
|
||||
continue
|
||||
try:
|
||||
bobot = float(k.get("bobot") or 0)
|
||||
except (TypeError, ValueError):
|
||||
bobot = 0
|
||||
if not bobot:
|
||||
continue
|
||||
poin = bobot / len(subs)
|
||||
per_komponen.append({"nama": k["nama"], "bobot": bobot,
|
||||
"n_subs": len(subs), "poin": poin})
|
||||
for s in subs:
|
||||
per_sub.setdefault(s, []).append({"nama": k["nama"], "poin": poin})
|
||||
return per_komponen, per_sub
|
||||
|
||||
|
||||
def _tabel_poin(grid: list[dict], bobot_sub_rincian: dict[str, list[dict]]) -> list[dict]:
|
||||
"""Nilai per mahasiswa dalam skala POIN (bukan 0-100) — tiap Sub-CPMK
|
||||
ditimbang sesuai kontribusi poinnya ke total 100 (lihat _rincian_bobot).
|
||||
Poin diperoleh = skor/100 × poin maks Sub-CPMK itu. Kolom Total adalah
|
||||
jumlah poin yang SUDAH didapat mahasiswa dari Sub-CPMK yang sudah
|
||||
ternilai — bukan nilai akhir yang direskalakan ke bobot yang ternilai
|
||||
saja, jadi Total ini bisa lebih kecil dari Nilai Akhir bila masih ada
|
||||
Sub-CPMK yang belum dinilai."""
|
||||
poin_maks = {s: sum(c["poin"] for c in kontribusi)
|
||||
for s, kontribusi in bobot_sub_rincian.items()}
|
||||
out = []
|
||||
for m in grid:
|
||||
skor = m.get("skor") or {}
|
||||
poin, total = {}, 0.0
|
||||
for s, maks in poin_maks.items():
|
||||
v = skor.get(s)
|
||||
if v is None:
|
||||
poin[s] = None
|
||||
else:
|
||||
dapat = v / 100 * maks
|
||||
poin[s] = {"dapat": dapat, "maks": maks}
|
||||
total += dapat
|
||||
out.append({"nim": m["nim"], "nama": m["nama"], "kelas": m.get("kelas"),
|
||||
"poin": poin, "total": total})
|
||||
return out
|
||||
|
||||
|
||||
def _konteks_nilai(request, kode, pesan="", peringatan=None):
|
||||
peta = db.peta_mk(kode)
|
||||
tersimpan = db.ambil_dok("nilai", kode)
|
||||
@@ -721,13 +890,44 @@ def _konteks_nilai(request, kode, pesan="", peringatan=None):
|
||||
grid = _grid_mahasiswa(tersimpan, terdaftar, kelas_dosen)
|
||||
komponen_daftar = _komponen_daftar(kode)
|
||||
sesi_semua = (tersimpan["data"].get("sesi") or {}) if tersimpan else {}
|
||||
bobot_komponen, bobot_sub_rincian = _rincian_bobot(kode)
|
||||
# Nilai Akhir dihitung hanya dari Sub-CPMK yang sudah ternilai (lihat
|
||||
# nilai_akhir_mahasiswa) — kalau belum semua Sub-CPMK MK ini ternilai,
|
||||
# angkanya cuma perkiraan sementara dan bisa berubah; ditandai bintang
|
||||
# di template supaya tak disalahsangka sudah final.
|
||||
total_subs = len(peta.get("subs") or [])
|
||||
tidak_lengkap = set()
|
||||
if total_subs:
|
||||
for m in grid:
|
||||
skor = m.get("skor") or {}
|
||||
n_terisi = sum(1 for s in peta["subs"] if skor.get(s["kode"]) is not None)
|
||||
if n_terisi < total_subs:
|
||||
tidak_lengkap.add(m["nim"])
|
||||
# Sub-CPMK yang skor tersimpannya beda dari hasil hitung ulang sesi —
|
||||
# berarti pernah "dikatrol" manual lewat grid Nilai per Mahasiswa. Nilai
|
||||
# itu akan tertimpa balik kalau sesi/komponen yang mengukur Sub-CPMK
|
||||
# tsb disimpan lagi (lihat aksi="komponen" di simpan_nilai), jadi
|
||||
# ditandai di template supaya dosen tak kaget kalau koreksinya hilang.
|
||||
dikatrol = set()
|
||||
if komponen_daftar:
|
||||
skor_hitung_sesi = hitung.skor_dari_sesi(komponen_daftar, sesi_semua)
|
||||
for m in grid:
|
||||
terhitung = skor_hitung_sesi.get(m["nim"]) or {}
|
||||
tersimpan_skor = m.get("skor") or {}
|
||||
for sub_kode, v_terhitung in terhitung.items():
|
||||
v_tersimpan = tersimpan_skor.get(sub_kode)
|
||||
if v_tersimpan is not None and abs(v_tersimpan - v_terhitung) > 0.05:
|
||||
dikatrol.add((m["nim"], sub_kode))
|
||||
return render(
|
||||
request, "form_nilai.html",
|
||||
{"peta": peta, "tersimpan": tersimpan, "hasil": hasil, "ambang": ambang,
|
||||
"akhir": akhir, "tahun": db.tahun_aktif(), "pesan": pesan,
|
||||
"grid": grid, "kelas_dosen": kelas_dosen,
|
||||
"grid": grid, "kelas_dosen": kelas_dosen, "tidak_lengkap": tidak_lengkap,
|
||||
"dikatrol": dikatrol,
|
||||
"komponen_daftar": komponen_daftar,
|
||||
"tabel_komponen": _tabel_komponen(grid, komponen_daftar, sesi_semua) if komponen_daftar else [],
|
||||
"bobot_komponen": bobot_komponen, "bobot_sub_rincian": bobot_sub_rincian,
|
||||
"tabel_poin": _tabel_poin(grid, bobot_sub_rincian) if bobot_sub_rincian else [],
|
||||
"peringatan": peringatan or []},
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block judul %}Nilai {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||
{% block isi %}
|
||||
<div class="nav-baris">
|
||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <b>Input Nilai</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/rps/{{ peta.kode }}/rtm">RTM</a> · <b>Input Nilai</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||
</div>
|
||||
|
||||
<h1>Input Nilai — {{ peta.nama }} <span style="color:var(--grey)">({{ peta.kode }})</span></h1>
|
||||
@@ -99,12 +99,15 @@
|
||||
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>
|
||||
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(r.nim)) if akhir.get(r.nim) is not none else '—' }}{% if r.nim in tidak_lengkap and akhir.get(r.nim) is not none %}<span title="Belum semua Sub-CPMK ternilai — angka ini sementara">*</span>{% endif %}</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if tidak_lengkap %}
|
||||
<p class="keterangan" style="margin-top:6px">* Sebagian Sub-CPMK mahasiswa ini belum ternilai — Nilai Akhir dihitung hanya dari yang sudah ternilai dan bisa berubah setelah sisanya diisi.</p>
|
||||
{% endif %}
|
||||
<div class="aksi-baris">
|
||||
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||
<span class="spacer"></span>
|
||||
@@ -144,9 +147,9 @@
|
||||
{% for s in peta.subs %}
|
||||
<td class="isian"><input type="number" name="skor_{{ i }}_{{ s.kode }}"
|
||||
min="0" max="100" step="any"
|
||||
value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}"></td>
|
||||
value="{{ '%g'|format(m.skor[s.kode]) if s.kode in m.skor else '' }}">{% if (m.nim, s.kode) in dikatrol %}<span title="Nilai ini dikoreksi manual (beda dari hasil hitung sesi/tugas komponen) — bisa tertimpa balik kalau sesi/komponen yang mengukur Sub-CPMK ini disimpan lagi">✎</span>{% endif %}</td>
|
||||
{% endfor %}
|
||||
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(m.nim)) if akhir.get(m.nim) is not none else '—' }}</b></td>
|
||||
<td class="tengah"><b>{{ '%.1f'|format(akhir.get(m.nim)) if akhir.get(m.nim) is not none else '—' }}{% if m.nim in tidak_lengkap and akhir.get(m.nim) is not none %}<span title="Belum semua Sub-CPMK ternilai — angka ini sementara">*</span>{% endif %}</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if per_mhs and hasil %}
|
||||
@@ -170,6 +173,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if tidak_lengkap %}
|
||||
<p class="keterangan" style="margin-top:6px">* Sebagian Sub-CPMK mahasiswa ini belum ternilai — Nilai Akhir dihitung hanya dari yang sudah ternilai dan bisa berubah setelah sisanya diisi.</p>
|
||||
{% endif %}
|
||||
{% if dikatrol %}
|
||||
<p class="keterangan" style="margin-top:6px">✎ = nilai sudah dikoreksi manual di sini (beda dari hasil hitung sesi/tugas komponennya) — hati-hati, koreksi ini akan tertimpa balik kalau sesi/komponen yang mengukur Sub-CPMK itu diisi/disimpan lagi.</p>
|
||||
{% endif %}
|
||||
<div class="aksi-baris">
|
||||
<span style="color:var(--grey);font-size:13px">Disimpan atas nama <b>{{ user.nama }}</b></span>
|
||||
<span class="spacer"></span>
|
||||
@@ -178,7 +187,111 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if tabel_poin %}
|
||||
{% set ada_kelas_p = tabel_poin|selectattr('kelas')|list|length > 0 %}
|
||||
{% set n_kiri_p = 4 if ada_kelas_p else 3 %}
|
||||
<h2 class="bagian">NILAI PER MAHASISWA — SKALA POIN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— nilai yang sama, ditampilkan sebagai poin yang didapat dari total 100 (lihat rincian bobot di bawah); hanya tampilan, bukan isian</span></h2>
|
||||
<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>
|
||||
{% if ada_kelas_p %}<th style="width:60px" class="tengah">Kelas</th>{% endif %}
|
||||
{% for s in peta.subs %}<th class="tengah" style="font-size:11px;min-width:70px">{{ s.kode }}</th>{% endfor %}
|
||||
<th class="tengah" style="width:90px">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in tabel_poin %}
|
||||
<tr>
|
||||
<td class="tengah">{{ loop.index }}</td>
|
||||
<td>{{ m.nim }}</td>
|
||||
<td>{{ m.nama }}</td>
|
||||
{% if ada_kelas_p %}<td class="tengah">{{ m.kelas or '—' }}</td>{% endif %}
|
||||
{% for s in peta.subs %}
|
||||
{% set p = m.poin.get(s.kode) %}
|
||||
<td class="tengah" style="font-size:12px">{% if p %}{{ '%.2f'|format(p.dapat) }}/{{ '%.2f'|format(p.maks) }}{% else %}—{% endif %}</td>
|
||||
{% endfor %}
|
||||
<td class="tengah"><b>{{ '%.2f'|format(m.total) }}</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if bobot_komponen %}
|
||||
<h2 class="bagian">RINCIAN BOBOT PENILAIAN <span style="font-weight:normal;font-style:italic;color:var(--grey)">— bobot komponen (RPS bagian G) dibagi rata ke Sub-CPMK-nya; murni dari RPS, tak bergantung nilai</span></h2>
|
||||
<div class="gulir">
|
||||
<table class="grid" style="max-width:820px">
|
||||
<thead><tr><th>Komponen</th><th class="tengah">Bobot</th><th class="tengah">Jumlah Sub-CPMK</th><th class="tengah">Poin per Sub-CPMK</th><th class="tengah">Verifikasi (Jumlah × Poin)</th></tr></thead>
|
||||
<tbody>
|
||||
{% for k in bobot_komponen %}
|
||||
<tr>
|
||||
<td>{{ k.nama }}</td>
|
||||
<td class="tengah">{{ '%g'|format(k.bobot) }}%</td>
|
||||
<td class="tengah">{{ k.n_subs }}</td>
|
||||
<td class="tengah">{{ '%.2f'|format(k.poin) }}%</td>
|
||||
<td class="tengah">{{ k.n_subs }} × {{ '%.2f'|format(k.poin) }}% = <b>{{ '%.2f'|format(k.n_subs * k.poin) }}%</b></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total">
|
||||
<td colspan="4" style="text-align:right"><b>Total</b></td>
|
||||
<td class="tengah"><b>{{ '%.2f'|format(bobot_komponen|sum(attribute='bobot')) }}%</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="keterangan" style="margin-top:10px">Dijumlah per Sub-CPMK (karena sebagian Sub-CPMK diukur lebih dari satu komponen):</p>
|
||||
<div class="gulir">
|
||||
<table class="grid">
|
||||
<thead><tr><th>Sub-CPMK</th><th class="tengah">Total Poin</th><th>Rincian</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in peta.subs %}
|
||||
{% set kontribusi = bobot_sub_rincian.get(s.kode) %}
|
||||
{% if kontribusi %}
|
||||
<tr>
|
||||
<td>{{ s.kode }}</td>
|
||||
<td class="tengah"><b>{{ '%.2f'|format(kontribusi|sum(attribute='poin')) }}</b></td>
|
||||
<td>{% for c in kontribusi %}{{ c.nama }} {{ '%.2f'|format(c.poin) }}{{ ' + ' if not loop.last }}{% endfor %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<tr class="total">
|
||||
<td style="text-align:right"><b>Total</b></td>
|
||||
<td class="tengah"><b>{{ '%.2f'|format(bobot_komponen|sum(attribute='bobot')) }}</b></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if hasil %}
|
||||
<h2 class="bagian">HASIL KETERCAPAIAN SUB-CPMK <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>Sub-CPMK</th><th class="tengah">Skor</th><th class="tengah">Maks</th><th class="tengah">Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in peta.subs %}
|
||||
{% set info = hasil.sub.get(s.kode) %}
|
||||
<tr>
|
||||
<td>{{ s.kode }}</td>
|
||||
<td class="tengah">{% if info and info.rata is not none %}<b>{{ '%.1f'|format(info.rata) }}</b>{% else %}—{% endif %}</td>
|
||||
<td class="tengah">100</td>
|
||||
<td class="tengah">
|
||||
{% if not info or info.rata is none %}<span class="stamp stamp-draft">BELUM TERUKUR</span>
|
||||
{% elif info.rata >= ambang %}<span class="stamp stamp-ok">TERCAPAI</span>
|
||||
{% else %}<span class="stamp stamp-no">DI BAWAH AMBANG</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block judul %}Nilai {{ komponen.nama }} — {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||
{% block isi %}
|
||||
<div class="nav-baris">
|
||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <b>{{ komponen.nama }}</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/rps/{{ peta.kode }}/rtm">RTM</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <b>{{ komponen.nama }}</b> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||
</div>
|
||||
|
||||
<h1>Isi Nilai — {{ komponen.nama }} <span style="color:var(--grey)">({{ peta.nama }})</span></h1>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block judul %}Portofolio {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||
{% block isi %}
|
||||
<div class="nav-baris">
|
||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <b>Portofolio</b></div>
|
||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <a href="/rps/{{ peta.kode }}/rtm">RTM</a> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <b>Portofolio</b></div>
|
||||
<div>
|
||||
{% if status == 'selesai' %}<span class="stamp stamp-ok">SELESAI</span>
|
||||
{% elif status == 'draft' %}<span class="stamp stamp-draft">DRAFT</span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block judul %}RPS {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||
{% block isi %}
|
||||
<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>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>
|
||||
@@ -342,7 +342,7 @@
|
||||
clearTimeout(timerAutosave);
|
||||
timerAutosave = setTimeout(simpanOtomatis, 3000);
|
||||
}
|
||||
document.addEventListener('input', jadwalkanAutosave);
|
||||
document.getElementById('form-rps').addEventListener('input', jadwalkanAutosave);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
{% extends "base.html" %}
|
||||
{% block judul %}RTM {{ peta.nama }} — Informatika UNTAN{% endblock %}
|
||||
{% block isi %}
|
||||
<div class="nav-baris">
|
||||
<div>MK ini: <a href="/rps/{{ peta.kode }}">RPS</a> · <b>RTM</b> · <a href="/nilai/{{ peta.kode }}">Input Nilai</a> · <a href="/portofolio/{{ peta.kode }}">Portofolio</a></div>
|
||||
</div>
|
||||
|
||||
<h1>Rencana Tugas Mandiri — {{ peta.nama }} <span style="color:var(--grey)">({{ peta.kode }})</span></h1>
|
||||
|
||||
<p class="keterangan">Dokumen RTM (Rancangan Tugas Mahasiswa) resmi sesuai format baku prodi —
|
||||
<b>berdiri sendiri, tak ada kaitan apa pun dengan Nilai</b> (tidak memuat skor/nilai mahasiswa
|
||||
sama sekali, murni dokumentasi rencana tugas MK ini). Tambah blok Tugas sesuai kebutuhan,
|
||||
isi, lalu unduh sebagai satu berkas .docx.</p>
|
||||
{% if rtm_updated_at %}<p class="keterangan" style="margin-top:-6px">Terakhir disimpan {{ rtm_updated_at }}{% if rtm_updated_by %} oleh {{ rtm_updated_by }}{% endif %}.</p>{% endif %}
|
||||
|
||||
{% 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>.</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/rps/{{ peta.kode }}/rtm" id="form-rtm">
|
||||
<fieldset {% if not boleh_edit %}disabled{% endif %} style="border:none;padding:0;margin:0">
|
||||
<div id="daftar-tugas-rtm"></div>
|
||||
<button type="button" class="tombol garis" onclick="tambahTugasRtm()">+ Tambah Tugas</button>
|
||||
</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 class="spacer"></span>
|
||||
<button class="tombol" type="submit">Simpan RTM</button>
|
||||
{% else %}
|
||||
<span class="spacer"></span>
|
||||
{% endif %}
|
||||
<a class="tombol garis" href="/rps/{{ peta.kode }}/rtm/unduh">Unduh RTM (.docx)</a>
|
||||
</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 id="tpl-tugas-rtm" type="text/template">
|
||||
<details style="margin-bottom:12px;border:1px solid var(--border);border-radius:6px;padding:10px 14px">
|
||||
<summary style="cursor:pointer;font-weight:bold"><span class="nomor-tugas-rtm">Tugas</span>
|
||||
<button type="button" class="tombol garis" style="padding:0 8px;font-size:11px;color:var(--red);border-color:var(--red);float:right"
|
||||
onclick="event.preventDefault(); hapusTugasRtm(this)">Hapus Tugas Ini</button>
|
||||
</summary>
|
||||
<div style="margin-top:12px">
|
||||
<label style="display:block;margin-bottom:10px">Judul Tugas
|
||||
<input type="text" name="judul___IDX__" style="display:block;margin-top:4px;max-width:480px">
|
||||
</label>
|
||||
<div style="margin-bottom:10px">
|
||||
<div>Sub Capaian Pembelajaran Mata Kuliah yang Dicapai <span style="font-weight:normal;color:var(--grey)">(opsional — untuk keterlacakan dokumen saja, tak memengaruhi nilai)</span></div>
|
||||
<div class="pilih-sub">
|
||||
{% for s in peta.subs %}
|
||||
<label><input type="checkbox" name="subs___IDX__" value="{{ s.kode }}">{{ 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___IDX__" id="bentuk_tugas___IDX__" onchange="toggleIsiOtomatisRtm('__IDX__')" style="max-width:280px">
|
||||
<option value="">— pilih —</option>
|
||||
{% for opsi in ["Diskusi", "PjBL", "CBL", "Esai", "Tes"] %}
|
||||
<option value="{{ opsi }}">{{ opsi }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button" class="tombol garis" id="tombol_isi_otomatis___IDX__"
|
||||
onclick="isiOtomatisRtm('__IDX__')" hidden>Isi Otomatis</button>
|
||||
</span>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Deskripsi Tugas
|
||||
<textarea name="deskripsi___IDX__" id="rtm_deskripsi___IDX__" rows="4" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Metode Pengerjaan Tugas
|
||||
<textarea name="metode_pengerjaan___IDX__" id="rtm_metode___IDX__" rows="3" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Bentuk dan Format Luaran
|
||||
<textarea name="luaran___IDX__" id="rtm_luaran___IDX__" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Indikator, Kriteria dan Bobot Penilaian
|
||||
<textarea name="indikator_bobot___IDX__" id="rtm_indikator___IDX__" rows="4" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Jadwal Pelaksanaan
|
||||
<span style="display:flex;gap:16px;margin-top:4px;flex-wrap:wrap">
|
||||
<span>Alokasi waktu<input type="text" name="alokasi_waktu___IDX__" id="rtm_alokasi___IDX__" placeholder="mis. 2 minggu" style="display:block;margin-top:2px;width:200px"></span>
|
||||
<span>Tanggal pelaksanaan<input type="text" name="tanggal___IDX__" placeholder="mis. minggu ke-5 s.d. 6" style="display:block;margin-top:2px;width:220px"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:10px">Lain-lain
|
||||
<textarea name="lain_lain___IDX__" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||
</label>
|
||||
<label style="display:block;margin-bottom:0">Daftar Rujukan
|
||||
<textarea name="rujukan___IDX__" rows="2" style="display:block;margin-top:4px;max-width:760px;width:100%"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const RTM_AWAL = {{ rtm_tugas | tojson | safe }};
|
||||
let rtmCounter = 0;
|
||||
|
||||
const BENTUK_TEMPLATE_RTM = {
|
||||
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 buatBlokTugasRtm(data) {
|
||||
data = data || {};
|
||||
const idx = rtmCounter++;
|
||||
const html = document.getElementById('tpl-tugas-rtm').innerHTML.split('__IDX__').join(idx);
|
||||
const bungkus = document.createElement('div');
|
||||
bungkus.innerHTML = html.trim();
|
||||
const det = bungkus.firstElementChild;
|
||||
det.querySelector(`[name="judul_${idx}"]`).value = data.judul || '';
|
||||
(data.subs || []).forEach(kode => {
|
||||
const cb = Array.from(det.querySelectorAll(`input[name="subs_${idx}"]`)).find(el => el.value === kode);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
det.querySelector(`#bentuk_tugas_${idx}`).value = data.bentuk_tugas || '';
|
||||
det.querySelector(`#tombol_isi_otomatis_${idx}`).hidden = !data.bentuk_tugas;
|
||||
det.querySelector(`[name="deskripsi_${idx}"]`).value = data.deskripsi || '';
|
||||
det.querySelector(`[name="metode_pengerjaan_${idx}"]`).value = data.metode_pengerjaan || '';
|
||||
det.querySelector(`[name="luaran_${idx}"]`).value = data.luaran || '';
|
||||
det.querySelector(`[name="indikator_bobot_${idx}"]`).value = data.indikator_bobot || '';
|
||||
det.querySelector(`[name="alokasi_waktu_${idx}"]`).value = data.alokasi_waktu || '';
|
||||
det.querySelector(`[name="tanggal_${idx}"]`).value = data.tanggal || '';
|
||||
det.querySelector(`[name="lain_lain_${idx}"]`).value = data.lain_lain || '';
|
||||
det.querySelector(`[name="rujukan_${idx}"]`).value = data.rujukan || '';
|
||||
if (data.judul || data.bentuk_tugas) det.open = true;
|
||||
document.getElementById('daftar-tugas-rtm').appendChild(det);
|
||||
renumberTugasRtm();
|
||||
return det;
|
||||
}
|
||||
|
||||
function tambahTugasRtm() {
|
||||
const det = buatBlokTugasRtm();
|
||||
det.open = true;
|
||||
det.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
function hapusTugasRtm(btn) {
|
||||
if (!confirm('Hapus blok Tugas ini?')) return;
|
||||
btn.closest('details').remove();
|
||||
renumberTugasRtm();
|
||||
}
|
||||
|
||||
function renumberTugasRtm() {
|
||||
document.querySelectorAll('#daftar-tugas-rtm > details').forEach((det, i) => {
|
||||
det.querySelector('.nomor-tugas-rtm').textContent = 'Tugas ' + (i + 1);
|
||||
});
|
||||
}
|
||||
|
||||
function isiOtomatisRtm(idx) {
|
||||
const bentuk = document.getElementById('bentuk_tugas_' + idx).value;
|
||||
const tmpl = BENTUK_TEMPLATE_RTM[bentuk];
|
||||
if (!tmpl) return;
|
||||
const checked = Array.from(document.querySelectorAll(`input[name="subs_${idx}"]: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_' + idx, tmpl.deskripsi);
|
||||
isi('rtm_metode_' + idx, tmpl.metode);
|
||||
isi('rtm_luaran_' + idx, tmpl.luaran);
|
||||
isi('rtm_indikator_' + idx, tmpl.indikator);
|
||||
isi('rtm_alokasi_' + idx, tmpl.jadwal);
|
||||
}
|
||||
function toggleIsiOtomatisRtm(idx) {
|
||||
const bentuk = document.getElementById('bentuk_tugas_' + idx).value;
|
||||
document.getElementById('tombol_isi_otomatis_' + idx).hidden = !bentuk;
|
||||
}
|
||||
|
||||
(RTM_AWAL.length ? RTM_AWAL : [{}]).forEach(buatBlokTugasRtm);
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user