feat: Afficher la couleur des matières dans l'emploi du temps élève et parent
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

L'admin pouvait attribuer une couleur à chaque matière, mais cette
couleur n'était utilisée que dans la vue admin de l'emploi du temps.
Les APIs élève et parent ne renvoyaient pas cette information, ce qui
donnait un affichage générique (gris/bleu) pour tous les créneaux.

L'API renvoie désormais subjectColor dans chaque créneau, et les vues
jour/semaine/widget/détails affichent la bordure colorée correspondante.
Le marqueur "Prochain cours" conserve sa priorité visuelle via une
surcharge CSS variable.
This commit is contained in:
2026-03-09 11:20:50 +01:00
parent bda63bd98c
commit 81e97c4f3b
27 changed files with 188 additions and 70 deletions

View File

@@ -14,7 +14,7 @@
let {
onChildSelected
}: {
onChildSelected?: (childId: string) => void;
onChildSelected?: (childId: string | null) => void;
} = $props();
let children = $state<Child[]>([]);
@@ -40,8 +40,9 @@
const data = await response.json();
children = data['hydra:member'] ?? data['member'] ?? (Array.isArray(data) ? data : []);
// Auto-select only when there's exactly 1 child
const first = children[0];
if (first && !selectedChildId) {
if (first && !selectedChildId && children.length === 1) {
selectedChildId = first.studentId;
onChildSelected?.(first.studentId);
}
@@ -52,7 +53,7 @@
}
}
function selectChild(childId: string) {
function selectChild(childId: string | null) {
selectedChildId = childId;
onChildSelected?.(childId);
}
@@ -75,6 +76,13 @@
<div class="child-selector">
<span class="child-selector-label">Enfant :</span>
<div class="child-selector-buttons">
<button
class="child-button"
class:selected={selectedChildId === null}
onclick={() => selectChild(null)}
>
Tous
</button>
{#each children as child (child.id)}
<button
class="child-button"

View File

@@ -2,7 +2,7 @@
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 { recordSync } from '$lib/features/schedule/stores/scheduleCache.svelte';
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';

View File

@@ -2,7 +2,7 @@
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 { recordSync } from '$lib/features/schedule/stores/scheduleCache.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';

View File

@@ -2,7 +2,7 @@
import type { ChildScheduleSummary } from '$lib/features/schedule/api/parentSchedule';
import { fetchChildrenScheduleSummary } from '$lib/features/schedule/api/parentSchedule';
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
import { isOffline, getLastSyncDate } from '$lib/features/schedule/stores/scheduleCache';
import { isOffline, getLastSyncDate, recordSync } from '$lib/features/schedule/stores/scheduleCache.svelte';
import { untrack } from 'svelte';
let {
@@ -43,6 +43,7 @@
try {
children = await fetchChildrenScheduleSummary();
recordSync();
} catch (e) {
error = e instanceof Error ? e.message : 'Erreur de chargement';
} finally {

View File

@@ -2,7 +2,7 @@
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
import { fetchChildDaySchedule, fetchChildWeekSchedule } from '$lib/features/schedule/api/parentSchedule';
import { untrack } from 'svelte';
import { recordSync, isOffline, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache';
import { recordSync, isOffline, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache.svelte';
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
import ChildSelector from '$lib/components/organisms/ChildSelector/ChildSelector.svelte';
import MultiChildSummary from '$lib/components/organisms/ParentSchedule/MultiChildSummary.svelte';
@@ -88,13 +88,15 @@
};
});
function handleChildSelected(childId: string) {
function handleChildSelected(childId: string | null) {
selectedChildId = childId;
untrack(() => {
loadSchedule();
// Prefetch for offline support
prefetchScheduleDays((date) => fetchChildDaySchedule(childId, date));
});
if (childId) {
untrack(() => {
loadSchedule();
// Prefetch for offline support
prefetchScheduleDays((date) => fetchChildDaySchedule(childId, date));
});
}
}
function navigateDay(offset: number) {

View File

@@ -44,7 +44,11 @@
{:else}
<ul class="slot-list">
{#each slots as slot (slot.slotId + slot.date)}
<li class="slot-item" class:next={slot.slotId === nextSlotId}>
<li
class="slot-item"
class:next={slot.slotId === nextSlotId}
style:--slot-color={slot.subjectColor ?? '#e5e7eb'}
>
<button
class="slot-button"
onclick={() => onSlotClick(slot)}
@@ -123,12 +127,12 @@
.slot-item {
border-radius: 0.75rem;
overflow: hidden;
border-left: 4px solid #e5e7eb;
border-left: 4px solid var(--slot-color, #e5e7eb);
transition: border-color 0.2s;
}
.slot-item.next {
border-left-color: #3b82f6;
--slot-color: #3b82f6;
}
.slot-button {

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { ScheduleSlot } from '$lib/features/schedule/api/schedule';
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
import { isOffline, getLastSyncDate } from '$lib/features/schedule/stores/scheduleCache';
import { isOffline, getLastSyncDate } from '$lib/features/schedule/stores/scheduleCache.svelte';
let {
slots = [],
@@ -61,6 +61,7 @@
class="slot-item"
class:next={slot.slotId === nextSlotId}
data-testid="schedule-slot"
style:--slot-color={slot.subjectColor ?? 'transparent'}
>
<div class="slot-time">
<span class="time-start">{slot.startTime}</span>
@@ -144,12 +145,12 @@
background: #f9fafb;
border-radius: 0.5rem;
align-items: center;
border-left: 3px solid transparent;
border-left: 3px solid var(--slot-color, transparent);
position: relative;
}
.slot-item.next {
border-left-color: #3b82f6;
--slot-color: #3b82f6;
background: #eff6ff;
}

View File

@@ -71,7 +71,7 @@
aria-label="Fermer">&times;</button
>
<h2 class="subject-name">{slot.subjectName}</h2>
<h2 class="subject-name" class:has-color={slot.subjectColor} style:--slot-color={slot.subjectColor}>{slot.subjectName}</h2>
<div class="detail-grid">
<div class="detail-row">
@@ -149,6 +149,11 @@
padding-right: 2rem;
}
.subject-name.has-color {
border-left: 4px solid var(--slot-color);
padding-left: 0.75rem;
}
.detail-grid {
display: flex;
flex-direction: column;

View File

@@ -3,7 +3,7 @@
import { fetchDaySchedule, fetchWeekSchedule, fetchNextClass } from '$lib/features/schedule/api/schedule';
import { formatSyncDate } from '$lib/features/schedule/formatSyncDate';
import { untrack } from 'svelte';
import { recordSync, isOffline, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache';
import { recordSync, isOffline, getLastSyncDate, prefetchScheduleDays } from '$lib/features/schedule/stores/scheduleCache.svelte';
import DayView from './DayView.svelte';
import WeekView from './WeekView.svelte';
import SlotDetails from './SlotDetails.svelte';

View File

@@ -64,6 +64,7 @@
class:modified={slot.isModified}
onclick={() => onSlotClick(slot)}
data-testid="week-slot"
style:--slot-color={slot.subjectColor ?? '#e5e7eb'}
>
<span class="slot-time-mobile">{slot.startTime} - {slot.endTime}</span>
<span class="slot-subject-mobile">{slot.subjectName}</span>
@@ -97,6 +98,7 @@
class:modified={slot.isModified}
onclick={() => onSlotClick(slot)}
data-testid="week-slot"
style:--slot-color={slot.subjectColor ?? '#e5e7eb'}
>
<span class="week-slot-time">{slot.startTime}</span>
<span class="week-slot-subject">{slot.subjectName}</span>
@@ -182,6 +184,7 @@
background: white;
border: none;
border-top: 1px solid #f3f4f6;
border-left: 4px solid var(--slot-color, #e5e7eb);
cursor: pointer;
text-align: left;
font-size: 0.875rem;
@@ -283,6 +286,7 @@
padding: 0.5rem;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-left: 4px solid var(--slot-color, #bfdbfe);
border-radius: 0.375rem;
cursor: pointer;
text-align: left;

View File

@@ -9,6 +9,7 @@ export interface ScheduleSlot {
endTime: string;
subjectId: string;
subjectName: string;
subjectColor: string | null;
teacherId: string;
teacherName: string;
room: string | null;

View File

@@ -2,6 +2,10 @@ import { browser } from '$app/environment';
const LAST_SYNC_KEY = 'classeo:schedule:lastSync';
let lastSyncValue = $state<string | null>(
browser ? localStorage.getItem(LAST_SYNC_KEY) : null
);
/**
* Vérifie si le navigateur est actuellement hors ligne.
*/
@@ -15,15 +19,16 @@ export function isOffline(): boolean {
*/
export function recordSync(): void {
if (!browser) return;
localStorage.setItem(LAST_SYNC_KEY, new Date().toISOString());
const now = new Date().toISOString();
localStorage.setItem(LAST_SYNC_KEY, now);
lastSyncValue = now;
}
/**
* Récupère la date de dernière synchronisation de l'EDT.
* Récupère la date de dernière synchronisation de l'EDT (réactif via $state).
*/
export function getLastSyncDate(): string | null {
if (!browser) return null;
return localStorage.getItem(LAST_SYNC_KEY);
return lastSyncValue;
}
/**

View File

@@ -49,7 +49,7 @@
// Demo child name for personalized messages
let childName = $state('Emma');
function handleChildSelected(childId: string) {
function handleChildSelected(childId: string | null) {
selectedChildId = childId;
}