2026-03-25 09:39:51 +07:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { onMount } from 'svelte';
|
2026-04-22 12:57:54 +07:00
|
|
|
import { auth, authIsTeacher } from '$stores/auth';
|
2026-03-25 09:39:51 +07:00
|
|
|
|
|
|
|
|
interface LessonHeader {
|
|
|
|
|
filename: string;
|
|
|
|
|
title: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface StudentProgress {
|
|
|
|
|
nama_siswa: string;
|
|
|
|
|
completed_count: number;
|
|
|
|
|
[key: string]: string | number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let students = $state<StudentProgress[]>([]);
|
|
|
|
|
let lessons = $state<LessonHeader[]>([]);
|
|
|
|
|
let loading = $state(true);
|
|
|
|
|
|
|
|
|
|
onMount(async () => {
|
2026-04-22 12:57:54 +07:00
|
|
|
if (!$authIsTeacher) {
|
|
|
|
|
loading = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 09:39:51 +07:00
|
|
|
try {
|
2026-04-22 12:57:54 +07:00
|
|
|
const res = await fetch(`/api/progress-report.json?token=${encodeURIComponent(auth.token)}`);
|
2026-03-25 09:39:51 +07:00
|
|
|
const data = await res.json();
|
|
|
|
|
students = data.students ?? [];
|
|
|
|
|
lessons = data.lessons ?? [];
|
|
|
|
|
} catch {
|
|
|
|
|
// API not available
|
|
|
|
|
} finally {
|
|
|
|
|
loading = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const totalLessons = $derived(lessons.length);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<svelte:head>
|
|
|
|
|
<title>Laporan Progress - Elemes LMS</title>
|
|
|
|
|
</svelte:head>
|
|
|
|
|
|
|
|
|
|
<h1>Laporan Progress Siswa</h1>
|
|
|
|
|
|
2026-04-22 12:57:54 +07:00
|
|
|
{#if !$authIsTeacher}
|
|
|
|
|
<p class="empty">Anda tidak memiliki akses ke halaman ini.</p>
|
|
|
|
|
{:else if loading}
|
2026-03-25 09:39:51 +07:00
|
|
|
<p class="loading">Memuat data...</p>
|
|
|
|
|
{:else if students.length === 0}
|
|
|
|
|
<p class="empty">Belum ada data siswa.</p>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="summary-bar">
|
|
|
|
|
<span><strong>{students.length}</strong> siswa</span>
|
|
|
|
|
<span><strong>{totalLessons}</strong> pelajaran</span>
|
2026-04-22 12:57:54 +07:00
|
|
|
<a href="/api/progress-report/export-csv?token={encodeURIComponent(auth.token)}" class="btn btn-secondary btn-sm">
|
2026-03-25 09:39:51 +07:00
|
|
|
Export CSV
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-wrapper">
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th class="sticky-col">Nama Siswa</th>
|
|
|
|
|
{#each lessons as lesson}
|
|
|
|
|
<th title={lesson.filename}>{lesson.title}</th>
|
|
|
|
|
{/each}
|
|
|
|
|
<th>Selesai</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{#each students as student}
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="sticky-col student-name">{student.nama_siswa}</td>
|
|
|
|
|
{#each lessons as lesson}
|
|
|
|
|
{@const key = lesson.filename.replace('.md', '')}
|
|
|
|
|
{@const status = student[key]}
|
|
|
|
|
<td class="status-cell">
|
|
|
|
|
{#if status === 'completed'}
|
|
|
|
|
<span class="badge done">✓</span>
|
|
|
|
|
{:else}
|
|
|
|
|
<span class="badge empty">—</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</td>
|
|
|
|
|
{/each}
|
|
|
|
|
<td class="completion-count">
|
|
|
|
|
{student.completed_count}/{totalLessons}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
h1 {
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
.loading, .empty {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 3rem;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summary-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
.summary-bar .btn-sm {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
padding: 0.3rem 0.75rem;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-wrapper {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
}
|
|
|
|
|
table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
}
|
|
|
|
|
th, td {
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
|
text-align: center;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
th {
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0;
|
|
|
|
|
}
|
|
|
|
|
.sticky-col {
|
|
|
|
|
position: sticky;
|
|
|
|
|
left: 0;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
z-index: 1;
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
th.sticky-col {
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
z-index: 2;
|
|
|
|
|
}
|
|
|
|
|
.student-name {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
min-width: 150px;
|
|
|
|
|
}
|
|
|
|
|
.status-cell {
|
|
|
|
|
min-width: 40px;
|
|
|
|
|
}
|
|
|
|
|
.badge.done {
|
|
|
|
|
color: var(--color-success);
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
.badge.empty {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
}
|
|
|
|
|
.completion-count {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
min-width: 60px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|