feat: Dashboard placeholder avec preview Score Sérénité

Permet aux parents de visualiser une démo du Score Sérénité dès leur
première connexion, avant même que les données réelles soient disponibles.
Les autres rôles (enseignant, élève, admin) ont également leur dashboard
adapté avec des sections placeholder.

La landing page redirige automatiquement vers /dashboard si l'utilisateur
est déjà authentifié, offrant un accès direct au tableau de bord.
This commit is contained in:
2026-02-04 18:34:08 +01:00
parent d3c6773be5
commit b45ef735db
26 changed files with 3096 additions and 76 deletions

View File

@@ -0,0 +1,87 @@
import { describe, it, expect } from 'vitest';
import demoData from '$lib/data/demo-data.json';
describe('demo-data.json', () => {
describe('serenityScore', () => {
it('should have a value between 0 and 100', () => {
expect(demoData.serenityScore.value).toBeGreaterThanOrEqual(0);
expect(demoData.serenityScore.value).toBeLessThanOrEqual(100);
});
it('should have a valid emoji', () => {
expect(['💚', '🟡', '🔴']).toContain(demoData.serenityScore.emoji);
});
it('should have a valid trend', () => {
expect(['stable', 'up', 'down']).toContain(demoData.serenityScore.trend);
});
it('should have components with scores and labels', () => {
const { components } = demoData.serenityScore;
expect(components).toHaveProperty('notes');
expect(components).toHaveProperty('absences');
expect(components).toHaveProperty('devoirs');
for (const [, component] of Object.entries(components)) {
expect(component).toHaveProperty('score');
expect(component).toHaveProperty('label');
expect(typeof component.score).toBe('number');
expect(typeof component.label).toBe('string');
expect(component.score).toBeGreaterThanOrEqual(0);
expect(component.score).toBeLessThanOrEqual(100);
}
});
});
describe('schedule', () => {
it('should have today array with schedule items', () => {
expect(Array.isArray(demoData.schedule.today)).toBe(true);
expect(demoData.schedule.today.length).toBeGreaterThan(0);
});
it('should have valid schedule items with time, subject, and room', () => {
for (const item of demoData.schedule.today) {
expect(item).toHaveProperty('time');
expect(item).toHaveProperty('subject');
expect(item).toHaveProperty('room');
expect(typeof item.time).toBe('string');
expect(typeof item.subject).toBe('string');
expect(typeof item.room).toBe('string');
}
});
});
describe('grades', () => {
it('should have recent array with grade items', () => {
expect(Array.isArray(demoData.grades.recent)).toBe(true);
});
it('should have valid grade items', () => {
for (const item of demoData.grades.recent) {
expect(item).toHaveProperty('subject');
expect(item).toHaveProperty('value');
expect(item).toHaveProperty('max');
expect(item).toHaveProperty('date');
expect(typeof item.value).toBe('number');
expect(typeof item.max).toBe('number');
}
});
});
describe('homework', () => {
it('should have upcoming array with homework items', () => {
expect(Array.isArray(demoData.homework.upcoming)).toBe(true);
});
it('should have valid homework items', () => {
for (const item of demoData.homework.upcoming) {
expect(item).toHaveProperty('subject');
expect(item).toHaveProperty('title');
expect(item).toHaveProperty('dueDate');
expect(item).toHaveProperty('status');
expect(['pending', 'done', 'late']).toContain(item.status);
}
});
});
});

View File

@@ -0,0 +1,106 @@
import { describe, it, expect } from 'vitest';
import {
calculateSerenityScore,
getSerenityEmoji,
getSerenityLabel,
isValidScore,
getTrendIcon
} from '$lib/features/dashboard/serenity-score';
describe('serenity-score utilities', () => {
describe('calculateSerenityScore', () => {
it('should calculate score correctly with all 100s', () => {
const score = calculateSerenityScore({ notes: 100, absences: 100, devoirs: 100 });
expect(score).toBe(100);
});
it('should calculate score correctly with all 0s', () => {
const score = calculateSerenityScore({ notes: 0, absences: 0, devoirs: 0 });
expect(score).toBe(0);
});
it('should calculate score with weighted components', () => {
// Notes 90 × 0.4 = 36
// Absences 95 × 0.3 = 28.5
// Devoirs 70 × 0.3 = 21
// Total = 85.5, rounded to 86
const score = calculateSerenityScore({ notes: 90, absences: 95, devoirs: 70 });
expect(score).toBe(86);
});
it('should round the score to nearest integer', () => {
const score = calculateSerenityScore({ notes: 50, absences: 50, devoirs: 50 });
expect(score).toBe(50);
expect(Number.isInteger(score)).toBe(true);
});
});
describe('getSerenityEmoji', () => {
it('should return green emoji for scores >= 70', () => {
expect(getSerenityEmoji(70)).toBe('💚');
expect(getSerenityEmoji(85)).toBe('💚');
expect(getSerenityEmoji(100)).toBe('💚');
});
it('should return yellow emoji for scores 40-69', () => {
expect(getSerenityEmoji(40)).toBe('🟡');
expect(getSerenityEmoji(55)).toBe('🟡');
expect(getSerenityEmoji(69)).toBe('🟡');
});
it('should return red emoji for scores < 40', () => {
expect(getSerenityEmoji(0)).toBe('🔴');
expect(getSerenityEmoji(25)).toBe('🔴');
expect(getSerenityEmoji(39)).toBe('🔴');
});
});
describe('getSerenityLabel', () => {
it('should return Excellent for scores >= 70', () => {
expect(getSerenityLabel(70)).toBe('Excellent');
expect(getSerenityLabel(100)).toBe('Excellent');
});
it('should return A surveiller for scores 40-69', () => {
expect(getSerenityLabel(40)).toBe('A surveiller');
expect(getSerenityLabel(69)).toBe('A surveiller');
});
it('should return Attention requise for scores < 40', () => {
expect(getSerenityLabel(0)).toBe('Attention requise');
expect(getSerenityLabel(39)).toBe('Attention requise');
});
});
describe('isValidScore', () => {
it('should return true for valid scores', () => {
expect(isValidScore(0)).toBe(true);
expect(isValidScore(50)).toBe(true);
expect(isValidScore(100)).toBe(true);
});
it('should return false for scores below 0', () => {
expect(isValidScore(-1)).toBe(false);
expect(isValidScore(-100)).toBe(false);
});
it('should return false for scores above 100', () => {
expect(isValidScore(101)).toBe(false);
expect(isValidScore(200)).toBe(false);
});
});
describe('getTrendIcon', () => {
it('should return up arrow for up trend', () => {
expect(getTrendIcon('up')).toBe('↑');
});
it('should return down arrow for down trend', () => {
expect(getTrendIcon('down')).toBe('↓');
});
it('should return right arrow for stable trend', () => {
expect(getTrendIcon('stable')).toBe('→');
});
});
});