diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 562cdf9..a098a44 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -67,6 +67,10 @@ Edit Content + (showDropdown = false)}> + + Kelas + (showDropdown = false)}> Progress diff --git a/frontend/src/routes/teacher/classroom/+page.svelte b/frontend/src/routes/teacher/classroom/+page.svelte new file mode 100644 index 0000000..444dac8 --- /dev/null +++ b/frontend/src/routes/teacher/classroom/+page.svelte @@ -0,0 +1,480 @@ + + + + Manajemen Kelas - Elemes LMS + + +
+
+

Manajemen Kelas & Siswa

+

Kelola akun siswa, impor/ekspor data kelas, dan pantau ringkasan siswa.

+
+
+ + Lihat Perkembangan Siswa → + +
+
+ +{#if loading} +

Memuat data kelas...

+{:else if !$authIsTeacher} +

Anda tidak memiliki akses ke halaman ini.

+{:else} +
+ {studentRows.length} siswa terdaftar + + {#if selectionCount > 0} + + {selectionCount} siswa dipilih + + {/if} + +
+ {#if exportError} + {exportError} + {/if} + + + + + + + + +
+
+ + {#if studentRows.length === 0} +
+

Belum ada data siswa

+

Gunakan tombol Import CSV untuk mengimpor daftar siswa atau Tambah Siswa untuk memasukkan siswa satu per satu.

+
+ + +
+
+ {:else} +
+ + + + + + + + + + + {#each students as student} + {@const isChecked = selectedIds.includes(student.id as string)} + {@const isTeacher = student.role === 'teacher'} + + + + + + + {/each} + +
+ + Nama SiswaPeranMateri Diselesaikan
+ {#if isTeacher} + — + {:else} + studentSelection.toggle(student.id as string)} + aria-label={`Pilih ${student.nama_siswa}`} + /> + {/if} + + {student.nama_siswa} + + {#if isTeacher} + Guru + {:else} + Siswa + {/if} + + {student.completed_count} materi +
+
+ {/if} + + (importOpen = false)} + onImported={handleImported} + /> + (addOpen = false)} + onAdded={handleAdded} + /> + ({ id: s.id as string, nama_siswa: s.nama_siswa }))} + onClose={() => (deleteOpen = false)} + onDeleted={handleDeleted} + /> +{/if} + + diff --git a/frontend/src/routes/teacher/progress/+page.svelte b/frontend/src/routes/teacher/progress/+page.svelte index 64ed021..e31ed77 100644 --- a/frontend/src/routes/teacher/progress/+page.svelte +++ b/frontend/src/routes/teacher/progress/+page.svelte @@ -1,15 +1,11 @@ - Laporan Progress - Elemes LMS + Perkembangan Siswa - Elemes LMS -

Laporan Progress Siswa

+
+
+

Laporan Perkembangan Siswa

+

Pantau kemajuan belajar siswa per sub-bab dan materi secara terstruktur.

+
+ +
{#if loading} -

Memuat data...

+

Memuat data progres...

{:else if !$authIsTeacher}

Anda tidak memiliki akses ke halaman ini.

{:else} -
- {studentRows.length} siswa - {totalLessons} pelajaran +
+
+ {treeGroups.length} Sub-Bab + {totalLessonsCount} Materi + {studentRows.length} Siswa +
- {#if selectionCount > 0} - - {selectionCount} siswa dipilih - - {/if} - -
- {#if exportError} - {exportError} + - - - - - - - - - - Export Laporan -
- {#if studentRows.length === 0} -

Belum ada data siswa. Gunakan Import CSV untuk menambah siswa pertama.

+ {#if treeGroups.length === 0} +
+ {#if searchQuery} +

Tidak ada hasil yang cocok

+

Tidak ditemukan sub-bab, materi, atau siswa dengan kata kunci "{searchQuery}".

+ + {:else} +

Belum ada materi pelajaran

+

Materi pelajaran belum terdeteksi di dalam direktori konten.

+ {/if} +
{:else} -
- - - - - - {#each lessons as lesson} - - {/each} - - - - - {#each students as student} - {@const isChecked = selectedIds.includes(student.id as string)} - {@const isTeacher = student.role === 'teacher'} - - - - {#each lessons as lesson} - {@const key = lesson.filename.replace('.md', '')} - {@const status = student[key]} - {@const composite = student[key + '_composite']} - {@const exercisePassed = student[key + '_exercise_passed']} - {@const quizScore = student[key + '_quiz_score']} - {@const violation = student[key + '_has_violation']} - {@const violationReason = student[key + '_termination_reason']} - {@const violationTime = student[key + '_attempt_finished_at']} - {@const evalScore = student[key + '_eval']} - {@const diagScore = student[key + '_diag']} - {@const diagUnmastered = parseUnmastered(student[key + '_diag_unmastered'])} - + + {#if isLessonOpen} +
+ {#if studentCount === 0} +
+ + Belum ada siswa yang menyelesaikan materi ini. +
+ {:else} + {#each item.completedStudents as student (student.id)} + {@const isTeacher = student.role === 'teacher'} +
+ +
+ 👤 + + {student.nama_siswa} + {#if isTeacher} + Guru + {/if} + + +
+ {#if student.composite !== null} + + Skor: {student.composite}% + + {/if} + + {#if student.exercise_passed || student.status === 'completed'} + ✓ Latihan + {/if} + + {#if student.quiz_score} + Kuis: {student.quiz_score} + {/if} + + {#if student.has_violation} + + ⚠ Pelanggaran + + {/if} + + {#if student.eval_score} + Eval: {student.eval_score} + {/if} + + {#if student.diag_score} + Diag: {student.diag_score} + {#if student.diag_unmastered.length > 0} + + {student.diag_unmastered.length} belum dikuasai + + {/if} + {/if} +
+ + +
+
+ {/each} + {/if} +
+ {/if} + {/each} - - - {/each} - -
- - Nama Siswa{lesson.title}Selesai
- {#if isTeacher} - — - {:else} - studentSelection.toggle(student.id as string)} - aria-label={`Pilih ${student.nama_siswa}`} - /> - {/if} - - {student.nama_siswa} - {#if isTeacher} - Guru - {/if} - -
- {#if composite !== null && composite !== undefined} - {composite} - {#if exercisePassed} - ✓ - {/if} - {#if quizScore} - {quizScore} - {/if} - {:else if status === 'completed'} - ✓ - {:else if status && status !== 'not_started'} - {status} - {:else} - — - {/if} +
+ {#each treeGroups as group (group.folder)} + {@const isSubBabOpen = openSubBabs[group.folder] ?? true} +
+ + - {#if evalScore} - Eval: {evalScore} - {/if} - {#if diagScore} - Diag: {diagScore} - {#if diagUnmastered.length > 0} - - {diagUnmastered.length} belum dikuasai + + {#if isSubBabOpen} +
+ {#each group.lessons as item (item.key)} + {@const isLessonOpen = openLessons[item.key] ?? (searchQuery !== '' && item.completedStudents.length > 0)} + {@const studentCount = item.completedStudents.length} +
+ - {#if (composite !== null && composite !== undefined) || (status && status !== 'not_started')} - - {/if} -
-
- {student.completed_count}/{totalLessons} -
+
+ {/if} +
+ {/each} {/if} - - (importOpen = false)} - onImported={handleImported} - /> - (addOpen = false)} - onAdded={handleAdded} - /> - ({ id: s.id as string, nama_siswa: s.nama_siswa }))} - onClose={() => (deleteOpen = false)} - onDeleted={handleDeleted} - /> {/if} diff --git a/routes/progress.py b/routes/progress.py index 03e0ef0..7320fd6 100644 --- a/routes/progress.py +++ b/routes/progress.py @@ -13,7 +13,13 @@ from services.token_service import ( reset_student_progress, get_all_students_progress, ) -from services.lesson_service import get_lessons_with_learning_objectives +from services.lesson_service import ( + get_lessons_with_learning_objectives, + find_lesson_file, + find_sub_home_for_lesson, + get_sub_home_data, + find_all_sub_home_folders, +) progress_bp = Blueprint('progress', __name__) @@ -85,9 +91,31 @@ def api_progress_report(): get_lessons_with_learning_objectives, ) + # Petakan sub-bab dari sub-home.md yang terdaftar + sub_home_folders = find_all_sub_home_folders() + sub_bab_map = {} + for folder in sub_home_folders: + data = get_sub_home_data(folder) + if data and data.get('title'): + sub_bab_map[folder] = data['title'] + + def _lesson_to_dict(lesson): + d = {'filename': lesson['filename'], 'title': lesson['title']} + file_path = lesson.get('path') or find_lesson_file(lesson['filename']) + if file_path: + _sub_home_path, folder_name = find_sub_home_for_lesson(file_path) + if folder_name and folder_name in sub_bab_map: + d['sub_bab'] = folder_name + d['sub_bab_title'] = sub_bab_map[folder_name] + return d + resp = jsonify({ 'students': all_students_progress, - 'lessons': [{'filename': l['filename'], 'title': l['title']} for l in ordered_lessons], + 'lessons': [_lesson_to_dict(l) for l in ordered_lessons], + 'sub_bab_list': [ + {'folder': f, 'title': sub_bab_map[f]} + for f in sub_home_folders if f in sub_bab_map + ], }) resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' return resp diff --git a/services/tests/test_progress_routes.py b/services/tests/test_progress_routes.py index 5763b79..97b0259 100644 --- a/services/tests/test_progress_routes.py +++ b/services/tests/test_progress_routes.py @@ -84,6 +84,7 @@ def test_teacher_can_access_report(client): # Guru + 2 siswa harus muncul sebagai row assert len(data["students"]) == 3 assert data["lessons"] + assert "sub_bab_list" in data def test_teacher_appears_in_progress_report(client):