feat: Permettre aux parents de consulter l'emploi du temps de leurs enfants
Some checks failed
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

Les parents avaient accès au lien "Emploi du temps" dans la navigation,
mais le dashboard n'affichait aucune donnée réelle : la section EDT
restait un placeholder vide ("L'emploi du temps sera disponible...").

Cette implémentation connecte le dashboard parent aux vrais endpoints API
(GET /api/me/children/{childId}/schedule/day|week/{date} et le résumé
multi-enfants), affiche le ScheduleWidget avec le prochain cours mis en
évidence (AC1), permet de cliquer sur chaque enfant dans le résumé pour
voir son EDT détaillé (AC2), et met en cache les endpoints parent dans le
Service Worker pour le mode offline (AC5).

Le handler backend est optimisé pour ne résoudre que l'enfant demandé
(via childId optionnel dans la query) au lieu de tous les enfants à chaque
appel, et les fonctions utilitaires dupliquées (formatSyncDate, timezone)
sont factorisées.
This commit is contained in:
2026-03-09 00:45:13 +01:00
parent 125d9d8806
commit bf753d1367
23 changed files with 2146 additions and 42 deletions

View File

@@ -1,11 +1,17 @@
<script lang="ts">
import type { DemoData } from '$types';
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
import { fetchChildDaySchedule } from '$lib/features/schedule/api/parentSchedule';
import { recordSync } from '$lib/features/schedule/stores/scheduleCache';
import SerenityScorePreview from '$lib/components/molecules/SerenityScore/SerenityScorePreview.svelte';
import SerenityScoreExplainer from '$lib/components/molecules/SerenityScore/SerenityScoreExplainer.svelte';
import DashboardSection from '$lib/components/molecules/DashboardSection.svelte';
import SkeletonCard from '$lib/components/atoms/Skeleton/SkeletonCard.svelte';
import SkeletonList from '$lib/components/atoms/Skeleton/SkeletonList.svelte';
import ScheduleWidget from '$lib/components/organisms/StudentSchedule/ScheduleWidget.svelte';
import MultiChildSummary from '$lib/components/organisms/ParentSchedule/MultiChildSummary.svelte';
import type { SerenityEmoji } from '$lib/features/dashboard/serenity-score';
import { getActiveRole } from '$features/roles/roleContext.svelte';
let {
demoData,
@@ -14,6 +20,7 @@
hasRealData = false,
serenityEnabled = false,
childName = '',
selectedChildId = null,
onToggleSerenity
}: {
demoData: DemoData;
@@ -22,9 +29,53 @@
hasRealData?: boolean;
serenityEnabled?: boolean;
childName?: string;
selectedChildId?: string | null;
onToggleSerenity?: (enabled: boolean) => void;
} = $props();
let isParent = $derived(getActiveRole() === 'ROLE_PARENT');
// Schedule widget state — mirrors DashboardStudent pattern
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 loadChildSchedule(childId: string) {
scheduleLoading = true;
scheduleError = null;
try {
const today = formatLocalDate(new Date());
scheduleSlots = await fetchChildDaySchedule(childId, today);
recordSync();
// Compute next slot
const now = new Date();
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
const next = scheduleSlots.find((s) => s.date === today && s.startTime > currentTime);
scheduleNextSlotId = next?.slotId ?? null;
} catch (e) {
scheduleError = e instanceof Error ? e.message : 'Erreur de chargement';
} finally {
scheduleLoading = false;
}
}
// Load schedule when selectedChildId changes
$effect(() => {
if (isParent && selectedChildId) {
loadChildSchedule(selectedChildId);
}
});
let showExplainer = $state(false);
const isDemo = $derived(!hasRealData);
@@ -74,11 +125,20 @@
<!-- EDT Section -->
<DashboardSection
title="Emploi du temps"
subtitle={hasRealData ? "Aujourd'hui" : undefined}
isPlaceholder={!hasRealData}
subtitle={isParent ? "Aujourd'hui" : (hasRealData ? "Aujourd'hui" : undefined)}
isPlaceholder={!isParent && !hasRealData}
placeholderMessage="L'emploi du temps sera disponible une fois les cours configurés"
>
{#if hasRealData}
{#if isParent && selectedChildId}
<ScheduleWidget
slots={scheduleSlots}
nextSlotId={scheduleNextSlotId}
isLoading={scheduleLoading}
error={scheduleError}
/>
{:else if isParent}
<MultiChildSummary />
{:else if hasRealData}
{#if isLoading}
<SkeletonList items={4} message={childName ? `Chargement de l'emploi du temps de ${childName}...` : "Chargement de l'emploi du temps..."} />
{:else}