Files
classroomio/apps/dashboard/src/lib/features/course/components/analytics/student-table.svelte
T

211 lines
8.1 KiB
Svelte
Raw Normal View History

2025-09-18 01:00:49 +01:00
<script lang="ts">
import { goto } from '$app/navigation';
import { Badge } from '@cio/ui/base/badge';
import UserIcon from '@lucide/svelte/icons/user';
import * as Pagination from '@cio/ui/base/pagination';
2026-03-09 11:18:22 +01:00
import { courseApi } from '$features/course/api';
2025-09-18 01:00:49 +01:00
import { t } from '$lib/utils/functions/translations';
2026-03-09 11:18:22 +01:00
import EmptyState from './empty-state.svelte';
2025-12-02 15:27:41 +01:00
import * as Avatar from '@cio/ui/base/avatar';
2026-03-09 11:18:22 +01:00
import type { CourseAnalytics } from '$features/course/utils/types';
2025-12-02 15:27:41 +01:00
import { shortenName } from '$lib/utils/functions/string';
2025-11-01 15:49:29 +01:00
interface Props {
2026-03-09 11:18:22 +01:00
students?: CourseAnalytics['students'];
2025-11-01 15:49:29 +01:00
}
let { students = [] }: Props = $props();
2025-09-18 01:00:49 +01:00
// Pagination state
2025-11-01 15:49:29 +01:00
let currentPage = $state(1);
2025-09-18 01:00:49 +01:00
const pageSize = 15;
// Computed values
2025-11-01 15:49:29 +01:00
let totalPages = $derived(Math.ceil(students.length / pageSize));
let startIndex = $derived((currentPage - 1) * pageSize);
let endIndex = $derived(startIndex + pageSize);
let paginatedStudents = $derived(students.slice(startIndex, endIndex));
let startItem = $derived(startIndex + 1);
let endItem = $derived(Math.min(endIndex, students.length));
2025-09-18 01:00:49 +01:00
2026-03-09 11:18:22 +01:00
const GotoFullProfile = (student: CourseAnalytics['students'][number]) => {
2025-09-18 01:00:49 +01:00
if (!student) return;
2025-11-01 15:49:29 +01:00
2026-03-09 11:18:22 +01:00
goto(`/courses/${courseApi.course?.id}/people/${student.id}?back=/courses/${courseApi.course?.id}/analytics`);
2025-09-18 01:00:49 +01:00
};
// Reset to first page when students array changes
2025-11-01 15:49:29 +01:00
$effect(() => {
if (students.length > 0 && currentPage > totalPages) {
currentPage = 1;
}
});
2025-09-18 01:00:49 +01:00
</script>
{#if students.length === 0}
<EmptyState
title={$t('analytics.no_students_found')}
description={$t('analytics.no_students_description')}
2025-11-01 15:49:29 +01:00
icon={UserIcon}
2025-09-18 01:00:49 +01:00
/>
{:else}
<!-- Responsive Table -->
<div class="relative h-[300px] overflow-hidden">
<div class="h-full overflow-auto">
<table class="w-full min-w-[800px]">
<!-- Fixed Header -->
<thead class="sticky top-0 z-10 bg-white dark:bg-neutral-800">
2026-03-09 11:18:22 +01:00
<tr class="border-b border-gray-200">
2025-09-18 01:00:49 +01:00
<th
2026-03-09 11:18:22 +01:00
class="sticky top-0 left-0 z-20 min-w-[200px] bg-white px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:bg-neutral-800 dark:text-gray-300"
2025-09-18 01:00:49 +01:00
>
{$t('analytics.student_name')}
</th>
<th
2026-03-09 11:18:22 +01:00
class="min-w-[150px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
2025-09-18 01:00:49 +01:00
>
{$t('analytics.lessons_completed')}
</th>
<th
2026-03-09 11:18:22 +01:00
class="min-w-[140px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
2025-09-18 01:00:49 +01:00
>
{$t('analytics.exercises_submitted')}
</th>
<th
2026-03-09 11:18:22 +01:00
class="min-w-[120px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
2025-09-18 01:00:49 +01:00
>
{$t('analytics.average_grade')}
</th>
<th
2026-03-09 11:18:22 +01:00
class="min-w-[120px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
2025-09-18 01:00:49 +01:00
>
{$t('analytics.last_seen')}
</th>
<th
2026-03-09 11:18:22 +01:00
class="min-w-[120px] px-4 py-3 text-left text-sm font-medium whitespace-nowrap text-gray-500 dark:text-gray-300"
2025-09-18 01:00:49 +01:00
>
{$t('analytics.actions')}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-600">
{#each paginatedStudents as student}
<tr class="group h-[100px] border-b hover:bg-gray-50 dark:hover:bg-gray-700">
<!-- Fixed Name Column -->
<td
2026-03-09 11:18:22 +01:00
class="sticky left-0 z-10 min-w-[200px] bg-white px-4 py-3 whitespace-nowrap transition-colors group-hover:bg-gray-50 dark:bg-neutral-800 dark:group-hover:bg-gray-700"
2025-09-18 01:00:49 +01:00
>
<div class="flex items-center gap-3">
2025-12-02 15:27:41 +01:00
<Avatar.Root class="size-8">
<Avatar.Image
src={student.profile.avatar_url ? student.profile.avatar_url : '/logo-192.png'}
2026-03-09 11:18:22 +01:00
alt={student.profile.fullname ? student.profile.fullname : $t('analytics.student')}
2025-12-02 15:27:41 +01:00
/>
<Avatar.Fallback>{shortenName(student.profile.fullname) || 'S'}</Avatar.Fallback>
</Avatar.Root>
2025-09-18 01:00:49 +01:00
<div class="min-w-0 flex-1">
<p class="truncate font-medium text-gray-900 dark:text-white">
{student.profile.fullname}
</p>
<p class="truncate text-sm text-gray-500 dark:text-gray-400">
{student.profile.email}
</p>
</div>
</div>
</td>
<!-- Scrollable Content -->
2026-03-09 11:18:22 +01:00
<td class="min-w-[150px] px-4 py-3 whitespace-nowrap">
2025-09-18 01:00:49 +01:00
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{student.lessonsCompleted}/{student.totalLessons}
</span>
<div class="w-20 flex-shrink-0">
<div class="h-2 rounded-full bg-gray-200 dark:bg-neutral-700">
2025-11-01 15:49:29 +01:00
<div class="h-2 rounded-full bg-blue-500" style="width: {student.progressPercentage}%"></div>
2025-09-18 01:00:49 +01:00
</div>
</div>
</div>
</td>
2026-03-09 11:18:22 +01:00
<td class="min-w-[140px] px-4 py-3 whitespace-nowrap">
2025-09-18 01:00:49 +01:00
<span class="text-sm font-medium text-gray-900 dark:text-white">
{student.exercisesSubmitted} out of {student.totalExercises}
</span>
</td>
2026-03-09 11:18:22 +01:00
<td class="min-w-[120px] px-4 py-3 whitespace-nowrap">
<Badge
class={student.averageGrade >= 80
? 'bg-green-600'
: student.averageGrade >= 60
? 'bg-blue-600'
: 'bg-red-600'}
2025-09-18 01:00:49 +01:00
>
{student.averageGrade}%
</Badge>
2025-09-18 01:00:49 +01:00
</td>
2026-03-09 11:18:22 +01:00
<td class="min-w-[120px] px-4 py-3 whitespace-nowrap">
2025-09-18 01:00:49 +01:00
<span class="text-sm text-gray-500 dark:text-gray-400">
{student.lastSeen}
</span>
</td>
2026-03-09 11:18:22 +01:00
<td class="min-w-[120px] px-4 py-3 whitespace-nowrap">
2025-09-18 01:00:49 +01:00
<button
2026-03-09 11:18:22 +01:00
class="rounded bg-blue-600 px-3 py-1.5 text-sm font-medium whitespace-nowrap text-white transition-colors hover:bg-blue-700"
2025-11-01 15:49:29 +01:00
onclick={() => GotoFullProfile(student)}
2025-09-18 01:00:49 +01:00
>
{$t('analytics.view_details')}
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<!-- Pagination Controls -->
{#if students.length > pageSize}
2026-03-09 11:18:22 +01:00
<div class="mt-4 flex items-center justify-between border-t border-gray-200 px-4 py-3">
2025-09-18 01:00:49 +01:00
<div class="text-sm text-gray-700 dark:text-gray-300">
{$t('analytics.showing_students', {
start: startItem,
end: endItem,
total: students.length
})}
</div>
<Pagination.Root
count={students.length}
perPage={pageSize}
page={currentPage}
onPageChange={(page) => (currentPage = page)}
>
{#snippet children({ pages, currentPage: activePage })}
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item>
<Pagination.Ellipsis />
</Pagination.Item>
{:else}
<Pagination.Item>
<Pagination.Link {page} isActive={activePage === page.value}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
2025-09-18 01:00:49 +01:00
</div>
{/if}
{/if}