feat: Permettre à l'élève de consulter ses devoirs

L'élève n'avait aucun moyen de voir les devoirs assignés à sa classe.
Cette fonctionnalité ajoute la consultation complète : liste triée par
échéance, détail avec pièces jointes, filtrage par matière, et marquage
personnel « fait » en localStorage.

Le dashboard élève affiche désormais les devoirs à venir avec ouverture
du détail en modale, et un lien vers la page complète. L'accès API est
sécurisé par vérification de la classe de l'élève (pas d'IDOR) et
validation du chemin des pièces jointes (pas de path traversal).
This commit is contained in:
2026-03-22 17:01:32 +01:00
parent 14c7849179
commit 2e2328c6ca
20 changed files with 2442 additions and 12 deletions

View File

@@ -1,8 +1,12 @@
<script lang="ts">
import type { DemoData } from '$types';
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
import type { StudentHomework, StudentHomeworkDetail } from '$lib/features/homework/api/studentHomework';
import { fetchDaySchedule, fetchNextClass } from '$lib/features/schedule/api/schedule';
import { fetchStudentHomework, fetchHomeworkDetail } from '$lib/features/homework/api/studentHomework';
import HomeworkDetail from '$lib/components/organisms/StudentHomework/HomeworkDetail.svelte';
import { recordSync } from '$lib/features/schedule/stores/scheduleCache.svelte';
import { getHomeworkStatuses } from '$lib/features/homework/stores/homeworkStatus.svelte';
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
import ScheduleWidget from '$lib/components/organisms/StudentSchedule/ScheduleWidget.svelte';
@@ -28,6 +32,16 @@
let scheduleLoading = $state(false);
let scheduleError = $state<string | null>(null);
// Homework widget state
let studentHomeworks = $state<StudentHomework[]>([]);
let homeworkLoading = $state(false);
let hwStatuses = $derived(getHomeworkStatuses());
let pendingHomeworks = $derived(
studentHomeworks.filter(hw => !hwStatuses[hw.id]?.done).slice(0, 5)
);
function formatLocalDate(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
@@ -35,6 +49,11 @@
return `${y}-${m}-${day}`;
}
function formatShortDate(dateStr: string): string {
const date = new Date(dateStr + 'T00:00:00');
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
}
async function loadTodaySchedule() {
scheduleLoading = true;
scheduleError = null;
@@ -57,9 +76,47 @@
}
}
if (isEleve) {
loadTodaySchedule();
async function loadHomeworks() {
homeworkLoading = true;
try {
studentHomeworks = await fetchStudentHomework();
} catch {
// Silently fail on dashboard widget
} finally {
homeworkLoading = false;
}
}
// Homework detail modal
let selectedHomeworkDetail = $state<StudentHomeworkDetail | null>(null);
async function openHomeworkDetail(homeworkId: string) {
try {
selectedHomeworkDetail = await fetchHomeworkDetail(homeworkId);
} catch {
// Fallback: navigate to full page
window.location.href = '/dashboard/homework';
}
}
function closeHomeworkDetail() {
selectedHomeworkDetail = null;
}
function handleOverlayClick(e: MouseEvent) {
if (e.target === e.currentTarget) closeHomeworkDetail();
}
function handleModalKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') closeHomeworkDetail();
}
$effect(() => {
if (!isEleve) return;
void loadTodaySchedule();
void loadHomeworks();
});
</script>
<div class="dashboard-student">
@@ -148,11 +205,34 @@
<!-- Devoirs Section -->
<DashboardSection
title="Mes devoirs"
subtitle={hasRealData ? "À faire" : undefined}
isPlaceholder={!hasRealData}
subtitle={isEleve ? "À faire" : (hasRealData ? "À faire" : undefined)}
isPlaceholder={!isEleve && !hasRealData}
placeholderMessage={isMinor ? "Tes devoirs s'afficheront ici" : "Vos devoirs s'afficheront ici"}
>
{#if hasRealData}
{#if isEleve}
{#if homeworkLoading}
<SkeletonList items={3} message="Chargement des devoirs..." />
{:else if pendingHomeworks.length === 0}
<p class="empty-homework">Aucun devoir à faire</p>
{:else}
<ul class="homework-list">
{#each pendingHomeworks as homework}
<li>
<button class="homework-item" style:border-left-color={homework.subjectColor ?? '#3b82f6'} onclick={() => openHomeworkDetail(homework.id)}>
<div class="homework-header">
<span class="homework-subject" style:color={homework.subjectColor ?? '#3b82f6'}>{homework.subjectName}</span>
</div>
<span class="homework-title">{homework.title}</span>
<span class="homework-due">Pour le {formatShortDate(homework.dueDate)}</span>
</button>
</li>
{/each}
</ul>
<a href="/dashboard/homework" class="view-all-link">
Voir tous les devoirs →
</a>
{/if}
{:else if hasRealData}
{#if isLoading}
<SkeletonList items={3} message="Chargement des devoirs..." />
{:else}
@@ -178,6 +258,16 @@
</div>
</div>
{#if selectedHomeworkDetail}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div class="homework-modal-overlay" onclick={handleOverlayClick} onkeydown={handleModalKeydown} role="presentation">
<div class="homework-modal" role="dialog" aria-modal="true" aria-label="Détail du devoir">
<button class="homework-modal-close" onclick={closeHomeworkDetail} aria-label="Fermer">&times;</button>
<HomeworkDetail detail={selectedHomeworkDetail} onBack={closeHomeworkDetail} />
</div>
</div>
{/if}
<style>
.dashboard-student {
display: flex;
@@ -327,10 +417,21 @@
}
.homework-item {
display: block;
width: 100%;
padding: 0.75rem;
background: #f9fafb;
border: none;
border-radius: 0.5rem;
border-left: 3px solid #3b82f6;
text-align: left;
cursor: pointer;
transition: background 0.15s;
font: inherit;
}
button.homework-item:hover {
background: #f3f4f6;
}
.homework-item.done {
@@ -380,4 +481,67 @@
font-size: 0.875rem;
color: #6b7280;
}
.empty-homework {
margin: 0;
text-align: center;
padding: 1rem;
color: #6b7280;
font-size: 0.875rem;
}
.view-all-link {
display: block;
text-align: center;
margin-top: 0.75rem;
color: #3b82f6;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
}
.view-all-link:hover {
color: #2563eb;
}
/* Homework detail modal */
.homework-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
padding: 1rem;
}
.homework-modal {
position: relative;
background: white;
border-radius: 0.75rem;
padding: 1.5rem;
max-width: 40rem;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.homework-modal-close {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: none;
border: none;
font-size: 1.5rem;
color: #6b7280;
cursor: pointer;
line-height: 1;
padding: 0.25rem;
}
.homework-modal-close:hover {
color: #1f2937;
}
</style>