L'élève avait accès à ses compétences mais pas à ses notes numériques. Cette fonctionnalité lui donne une vue complète de sa progression scolaire avec moyennes par matière, détail par évaluation, statistiques de classe, et un mode "découverte" pour révéler ses notes à son rythme (FR14, FR15). Les notes ne sont visibles qu'après publication par l'enseignant, ce qui garantit que l'élève les découvre avant ses parents (délai 24h story 6.7).
529 lines
12 KiB
Svelte
529 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { untrack } from 'svelte';
|
|
import { page } from '$app/state';
|
|
import { goto } from '$app/navigation';
|
|
import { isAuthenticated, refreshToken, logout } from '$lib/auth/auth.svelte';
|
|
import RoleSwitcher from '$lib/components/organisms/RoleSwitcher/RoleSwitcher.svelte';
|
|
import { fetchRoles, resetRoleContext, getActiveRole } from '$features/roles/roleContext.svelte';
|
|
import { fetchBranding, resetBranding, getLogoUrl } from '$features/branding/brandingStore.svelte';
|
|
|
|
let { children } = $props();
|
|
let isLoggingOut = $state(false);
|
|
let mobileMenuOpen = $state(false);
|
|
let logoUrl = $derived(getLogoUrl());
|
|
let pathname = $derived(page.url.pathname);
|
|
let isEleve = $derived(getActiveRole() === 'ROLE_ELEVE');
|
|
let isParent = $derived(getActiveRole() === 'ROLE_PARENT');
|
|
let isProf = $derived(getActiveRole() === 'ROLE_PROF');
|
|
|
|
// Load user roles on mount for multi-role context switching (FR5)
|
|
// Guard: only fetch if authenticated (or refresh succeeds), otherwise stay in demo mode
|
|
$effect(() => {
|
|
untrack(async () => {
|
|
if (!isAuthenticated()) {
|
|
const refreshed = await refreshToken();
|
|
if (!refreshed) return;
|
|
}
|
|
fetchRoles();
|
|
fetchBranding();
|
|
});
|
|
});
|
|
|
|
// Close menu on route change
|
|
$effect(() => {
|
|
void page.url.pathname;
|
|
mobileMenuOpen = false;
|
|
});
|
|
|
|
// Lock body scroll when mobile menu is open
|
|
$effect(() => {
|
|
document.body.style.overflow = mobileMenuOpen ? 'hidden' : '';
|
|
return () => {
|
|
document.body.style.overflow = '';
|
|
};
|
|
});
|
|
|
|
async function handleLogout() {
|
|
isLoggingOut = true;
|
|
try {
|
|
resetRoleContext();
|
|
resetBranding();
|
|
await logout();
|
|
} finally {
|
|
isLoggingOut = false;
|
|
}
|
|
}
|
|
|
|
function goHome() {
|
|
goto('/dashboard');
|
|
}
|
|
|
|
function goSettings() {
|
|
goto('/settings');
|
|
}
|
|
|
|
function toggleMobileMenu() {
|
|
mobileMenuOpen = !mobileMenuOpen;
|
|
}
|
|
|
|
function closeMobileMenu() {
|
|
mobileMenuOpen = false;
|
|
}
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape' && mobileMenuOpen) {
|
|
closeMobileMenu();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={handleKeydown} />
|
|
|
|
<div class="dashboard-layout">
|
|
<header class="dashboard-header">
|
|
<div class="header-content">
|
|
<button class="logo-button" onclick={goHome}>
|
|
{#if logoUrl}
|
|
<img src={logoUrl} alt="Logo de l'établissement" class="header-logo" />
|
|
{/if}
|
|
<span class="logo-text">Classeo</span>
|
|
</button>
|
|
|
|
<button
|
|
class="hamburger-button"
|
|
onclick={toggleMobileMenu}
|
|
aria-expanded={mobileMenuOpen}
|
|
aria-label="Ouvrir le menu de navigation"
|
|
>
|
|
<span class="hamburger-line"></span>
|
|
<span class="hamburger-line"></span>
|
|
<span class="hamburger-line"></span>
|
|
</button>
|
|
|
|
<nav class="desktop-nav">
|
|
<RoleSwitcher />
|
|
<a href="/dashboard" class="nav-link" class:active={pathname === '/dashboard'}>Tableau de bord</a>
|
|
{#if isProf}
|
|
<a href="/dashboard/teacher/homework" class="nav-link" class:active={pathname === '/dashboard/teacher/homework'}>Devoirs</a>
|
|
<a href="/dashboard/teacher/evaluations" class="nav-link" class:active={pathname === '/dashboard/teacher/evaluations'}>Évaluations</a>
|
|
{/if}
|
|
{#if isEleve}
|
|
<a href="/dashboard/schedule" class="nav-link" class:active={pathname === '/dashboard/schedule'}>Mon EDT</a>
|
|
<a href="/dashboard/student-grades" class="nav-link" class:active={pathname === '/dashboard/student-grades'}>Mes notes</a>
|
|
<a href="/dashboard/student-competencies" class="nav-link" class:active={pathname === '/dashboard/student-competencies'}>Compétences</a>
|
|
{/if}
|
|
{#if isParent}
|
|
<a href="/dashboard/parent-schedule" class="nav-link" class:active={pathname === '/dashboard/parent-schedule'}>EDT enfants</a>
|
|
<a href="/dashboard/parent-homework" class="nav-link" class:active={pathname === '/dashboard/parent-homework'}>Devoirs</a>
|
|
{/if}
|
|
<button class="nav-button" onclick={goSettings}>Paramètres</button>
|
|
<button class="logout-button" onclick={handleLogout} disabled={isLoggingOut}>
|
|
{#if isLoggingOut}
|
|
<span class="spinner"></span>
|
|
Déconnexion...
|
|
{:else}
|
|
Déconnexion
|
|
{/if}
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
{#if mobileMenuOpen}
|
|
<div
|
|
class="mobile-overlay"
|
|
onclick={closeMobileMenu}
|
|
onkeydown={(e) => e.key === 'Enter' && closeMobileMenu()}
|
|
role="presentation"
|
|
></div>
|
|
<div class="mobile-drawer" role="dialog" aria-modal="true" aria-label="Menu de navigation">
|
|
<div class="mobile-drawer-header">
|
|
{#if logoUrl}
|
|
<img src={logoUrl} alt="Logo de l'établissement" class="header-logo" />
|
|
{/if}
|
|
<span class="logo-text">Classeo</span>
|
|
<button class="mobile-close" onclick={closeMobileMenu} aria-label="Fermer le menu">
|
|
×
|
|
</button>
|
|
</div>
|
|
<div class="mobile-drawer-body">
|
|
<div class="mobile-role-switcher">
|
|
<RoleSwitcher />
|
|
</div>
|
|
<a href="/dashboard" class="mobile-nav-link" class:active={pathname === '/dashboard'}>
|
|
Tableau de bord
|
|
</a>
|
|
{#if isProf}
|
|
<a href="/dashboard/teacher/homework" class="mobile-nav-link" class:active={pathname === '/dashboard/teacher/homework'}>
|
|
Devoirs
|
|
</a>
|
|
<a href="/dashboard/teacher/evaluations" class="mobile-nav-link" class:active={pathname === '/dashboard/teacher/evaluations'}>
|
|
Évaluations
|
|
</a>
|
|
{/if}
|
|
{#if isEleve}
|
|
<a href="/dashboard/schedule" class="mobile-nav-link" class:active={pathname === '/dashboard/schedule'}>
|
|
Mon emploi du temps
|
|
</a>
|
|
<a href="/dashboard/student-grades" class="mobile-nav-link" class:active={pathname === '/dashboard/student-grades'}>
|
|
Mes notes
|
|
</a>
|
|
<a href="/dashboard/student-competencies" class="mobile-nav-link" class:active={pathname === '/dashboard/student-competencies'}>
|
|
Compétences
|
|
</a>
|
|
{/if}
|
|
{#if isParent}
|
|
<a href="/dashboard/parent-schedule" class="mobile-nav-link" class:active={pathname === '/dashboard/parent-schedule'}>
|
|
EDT enfants
|
|
</a>
|
|
<a href="/dashboard/parent-homework" class="mobile-nav-link" class:active={pathname === '/dashboard/parent-homework'}>
|
|
Devoirs
|
|
</a>
|
|
{/if}
|
|
<button class="mobile-nav-link" onclick={goSettings}>Paramètres</button>
|
|
</div>
|
|
<div class="mobile-drawer-footer">
|
|
<button
|
|
class="mobile-nav-link mobile-logout"
|
|
onclick={handleLogout}
|
|
disabled={isLoggingOut}
|
|
>
|
|
{#if isLoggingOut}
|
|
Déconnexion...
|
|
{:else}
|
|
Déconnexion
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<main class="dashboard-main">
|
|
<div class="main-content">
|
|
{@render children()}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<style>
|
|
.dashboard-layout {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--surface-primary, #f8fafc);
|
|
}
|
|
|
|
.dashboard-header {
|
|
background: var(--surface-elevated, #fff);
|
|
border-bottom: 1px solid var(--border-subtle, #e2e8f0);
|
|
padding: 0 1rem;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
.header-content {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
height: 56px;
|
|
}
|
|
|
|
.logo-button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 0.5rem 0;
|
|
}
|
|
|
|
.header-logo {
|
|
height: 32px;
|
|
width: auto;
|
|
object-fit: contain;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: var(--accent-primary, #0ea5e9);
|
|
}
|
|
|
|
/* Hamburger — visible on mobile */
|
|
.hamburger-button {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 5px;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 8px;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.hamburger-button:hover {
|
|
background: var(--surface-primary, #f8fafc);
|
|
}
|
|
|
|
.hamburger-line {
|
|
display: block;
|
|
width: 20px;
|
|
height: 2px;
|
|
background: var(--text-secondary, #64748b);
|
|
border-radius: 1px;
|
|
}
|
|
|
|
/* Desktop nav — hidden on mobile */
|
|
.desktop-nav {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.nav-link {
|
|
padding: 0.5rem 1rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--text-secondary, #64748b);
|
|
text-decoration: none;
|
|
border-radius: 0.5rem;
|
|
transition: all 0.2s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.nav-link:hover {
|
|
color: var(--text-primary, #1f2937);
|
|
background: var(--surface-primary, #f8fafc);
|
|
}
|
|
|
|
.nav-link.active {
|
|
color: var(--accent-primary, #0ea5e9);
|
|
background: var(--accent-primary-light, #e0f2fe);
|
|
}
|
|
|
|
.nav-button {
|
|
padding: 0.5rem 1rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--text-secondary, #64748b);
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.nav-button:hover {
|
|
color: var(--text-primary, #1f2937);
|
|
background: var(--surface-primary, #f8fafc);
|
|
}
|
|
|
|
.logout-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--text-secondary, #64748b);
|
|
background: transparent;
|
|
border: 1px solid var(--border-subtle, #e2e8f0);
|
|
border-radius: 0.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.logout-button:hover:not(:disabled) {
|
|
color: var(--color-alert, #ef4444);
|
|
border-color: var(--color-alert, #ef4444);
|
|
}
|
|
|
|
.logout-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Mobile overlay */
|
|
.mobile-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.4);
|
|
z-index: 200;
|
|
animation: fadeIn 0.2s ease-out;
|
|
}
|
|
|
|
/* Mobile drawer */
|
|
.mobile-drawer {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: min(300px, 85vw);
|
|
background: var(--surface-elevated, #fff);
|
|
z-index: 201;
|
|
display: flex;
|
|
flex-direction: column;
|
|
animation: slideInLeft 0.25s ease-out;
|
|
}
|
|
|
|
.mobile-drawer-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1rem 1.25rem;
|
|
border-bottom: 1px solid var(--border-subtle, #e2e8f0);
|
|
}
|
|
|
|
.mobile-close {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 36px;
|
|
height: 36px;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 1.5rem;
|
|
color: var(--text-secondary, #64748b);
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.mobile-close:hover {
|
|
background: var(--surface-primary, #f8fafc);
|
|
color: var(--text-primary, #1f2937);
|
|
}
|
|
|
|
.mobile-drawer-body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0.75rem 0;
|
|
}
|
|
|
|
.mobile-role-switcher {
|
|
padding: 0.5rem 1.25rem 0.75rem;
|
|
border-bottom: 1px solid var(--border-subtle, #e2e8f0);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.mobile-nav-link {
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
padding: 0.75rem 1.25rem;
|
|
font-size: 0.9375rem;
|
|
font-weight: 500;
|
|
color: var(--text-secondary, #64748b);
|
|
text-decoration: none;
|
|
border: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
border-left: 3px solid transparent;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.mobile-nav-link:hover {
|
|
background: var(--surface-primary, #f8fafc);
|
|
color: var(--text-primary, #1f2937);
|
|
}
|
|
|
|
.mobile-nav-link.active {
|
|
color: var(--accent-primary, #0ea5e9);
|
|
border-left-color: var(--accent-primary, #0ea5e9);
|
|
background: var(--accent-primary-light, #e0f2fe);
|
|
}
|
|
|
|
.mobile-logout {
|
|
color: var(--color-alert, #ef4444);
|
|
}
|
|
|
|
.mobile-logout:hover {
|
|
background: #fef2f2;
|
|
}
|
|
|
|
.mobile-drawer-footer {
|
|
border-top: 1px solid var(--border-subtle, #e2e8f0);
|
|
padding: 0.5rem 0;
|
|
}
|
|
|
|
.spinner {
|
|
width: 14px;
|
|
height: 14px;
|
|
border: 2px solid var(--border-subtle, #e2e8f0);
|
|
border-top-color: var(--text-secondary, #64748b);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
.dashboard-main {
|
|
flex: 1;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.main-content {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideInLeft {
|
|
from {
|
|
transform: translateX(-100%);
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.header-content {
|
|
height: 64px;
|
|
}
|
|
|
|
.hamburger-button {
|
|
display: none;
|
|
}
|
|
|
|
.desktop-nav {
|
|
display: flex;
|
|
}
|
|
|
|
.mobile-overlay,
|
|
.mobile-drawer {
|
|
display: none;
|
|
}
|
|
|
|
.dashboard-header {
|
|
padding: 0 1.5rem;
|
|
}
|
|
|
|
.dashboard-main {
|
|
padding: 1.5rem;
|
|
}
|
|
}
|
|
</style>
|