feat: Permettre aux élèves de consulter leur emploi du temps

Les élèves n'avaient aucun moyen de voir leur emploi du temps
depuis l'application. Cette fonctionnalité ajoute une page dédiée
avec deux modes de visualisation (jour et semaine), la navigation
temporelle, et le détail des cours au tap.

Le backend résout l'EDT de l'élève en chaînant : affectation classe →
créneaux récurrents + exceptions + calendrier scolaire → enrichissement
des noms (matières/enseignants). Le frontend utilise un cache offline
(Workbox NetworkFirst) pour rester consultable hors connexion.
This commit is contained in:
2026-03-05 16:21:37 +01:00
parent ae640e91ac
commit 36ceefb625
30 changed files with 3526 additions and 30 deletions

View File

@@ -1,7 +1,12 @@
<script lang="ts">
import type { DemoData } from '$types';
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
import { fetchDaySchedule, fetchNextClass } from '$lib/features/schedule/api/schedule';
import { recordSync } from '$lib/features/schedule/stores/scheduleCache';
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';
import { getActiveRole } from '$features/roles/roleContext.svelte';
let {
demoData,
@@ -14,6 +19,47 @@
hasRealData?: boolean;
isMinor?: boolean;
} = $props();
let isEleve = $derived(getActiveRole() === 'ROLE_ELEVE');
// Schedule widget state (AC1: "0 tap" — visible dès le dashboard)
let scheduleSlots = $state<ScheduleSlot[]>([]);
let scheduleNextSlotId = $state<string | null>(null);
let scheduleLoading = $state(false);
let scheduleError = $state<string | null>(null);
function formatLocalDate(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
async function loadTodaySchedule() {
scheduleLoading = true;
scheduleError = null;
try {
const today = formatLocalDate(new Date());
scheduleSlots = await fetchDaySchedule(today);
recordSync();
try {
const next = await fetchNextClass();
scheduleNextSlotId = next?.slotId ?? null;
} catch {
scheduleNextSlotId = null;
}
} catch (e) {
scheduleError = e instanceof Error ? e.message : 'Erreur de chargement';
} finally {
scheduleLoading = false;
}
}
if (isEleve) {
loadTodaySchedule();
}
</script>
<div class="dashboard-student">
@@ -45,11 +91,18 @@
<!-- EDT Section -->
<DashboardSection
title="Mon emploi du temps"
subtitle={hasRealData ? "Aujourd'hui" : undefined}
isPlaceholder={!hasRealData}
subtitle={isEleve ? "Aujourd'hui" : (hasRealData ? "Aujourd'hui" : undefined)}
isPlaceholder={!isEleve && !hasRealData}
placeholderMessage={isMinor ? "Ton emploi du temps sera bientôt disponible" : "Votre emploi du temps sera bientôt disponible"}
>
{#if hasRealData}
{#if isEleve}
<ScheduleWidget
slots={scheduleSlots}
nextSlotId={scheduleNextSlotId}
isLoading={scheduleLoading}
error={scheduleError}
/>
{:else if hasRealData}
{#if isLoading}
<SkeletonList items={4} message="Chargement de l'emploi du temps..." />
{:else}