feat(rapport/eo): support 5 critères × /4 — Phonologie (Sprint 4.8 frontend)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ce91aaa7b
commit
04019f8348
11 changed files with 273 additions and 10 deletions
|
|
@ -29,7 +29,27 @@ Chaque entrée suit ce format :
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [Unreleased] — 2026-04-25 — Sprint 4.6 — UI EO (waveform + timeline)
|
## [Unreleased] — 2026-04-26 — Sprint 4.8 — Phonologie EO (frontend)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `src/entities/report/__tests__/getMaxScorePerCritere.test.ts` — 7 tests (détection maxScore + mapping libellés EO).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `src/entities/report/lib.ts` — nouveau helper `getMaxScorePerCritere(rapport): 4 | 5` (détection sur criteres.length === 5). `CRITERE_NOM_TO_CODE` étendu avec les 4 libellés EO Sprint 4.8.
|
||||||
|
- `src/features/simulations/components/rapport/CritereCard.tsx` — nouvelle prop `maxScore` : affiche `X/4` (EO Sprint 4.8) ou `X/5` (EE, EO legacy).
|
||||||
|
- `src/features/simulations/pages/RapportPage.tsx` — calcul maxScore propagé aux CritereCard.
|
||||||
|
- `src/entities/report/types.ts` — commentaire Critere.score clarifié.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Rétrocompatibilité : rapports EO legacy (4 critères × /5) et EE (4 × /5) inchangés.
|
||||||
|
- Tests : 191 → 198 verts (+7).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [Unreleased] — 2026-04-26 — Sprint 4.6 — UI EO (waveform + timeline)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
|
||||||
148
docs/DEPLOYMENT.md
Normal file
148
docs/DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
# DEPLOYMENT.md — Expria V2
|
||||||
|
> Version 1.0 — Rédigé avant lancement
|
||||||
|
> Procédure officielle de bascule V1 → V2 sur expria.app
|
||||||
|
> À lire intégralement avant toute action de déploiement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Architecture cible
|
||||||
|
|
||||||
|
| Composant | V1 (actuel) | V2 (cible) |
|
||||||
|
|---|---|---|
|
||||||
|
| Frontend | Next.js sur Render | React/Vite sur Cloudflare Pages |
|
||||||
|
| Backend | Next.js API routes sur Render | Hono.js sur Render (déjà live) |
|
||||||
|
| DNS | Vercel | Vercel (inchangé) |
|
||||||
|
| Domaine | expria.app | expria.app (inchangé) |
|
||||||
|
| Auth | Supabase | Supabase (inchangé) |
|
||||||
|
| Paiement | Stripe | Stripe (inchangé) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Prérequis — ne pas lancer la bascule sans cocher tout
|
||||||
|
|
||||||
|
### Code
|
||||||
|
- [ ] Tous les tests Vitest backend passent (0 échec)
|
||||||
|
- [ ] Tous les tests Vitest frontend passent (0 échec)
|
||||||
|
- [ ] npm run typecheck frontend → 0 erreur
|
||||||
|
- [ ] Smoke test complet (Groupe Z du GOLDEN_DATASET.md) validé en local
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
- [ ] Backend V2 stable sur api.expria.app depuis au moins 48h sans erreur critique Sentry
|
||||||
|
- [ ] Sentry configuré et actif sur le frontend V2
|
||||||
|
- [ ] Variables d'environnement Cloudflare Pages configurées :
|
||||||
|
- VITE_API_URL=https://api.expria.app
|
||||||
|
- VITE_SUPABASE_URL=...
|
||||||
|
- VITE_SUPABASE_ANON_KEY=...
|
||||||
|
- VITE_ENABLE_T2_LIVE=false
|
||||||
|
- [ ] CNAME Cloudflare Pages créé et testé sur une URL de preview
|
||||||
|
|
||||||
|
### Stripe
|
||||||
|
- [ ] Webhooks Stripe pointent vers api.expria.app (backend V2)
|
||||||
|
- [ ] Test de paiement réel effectué sur l'URL de preview Cloudflare Pages
|
||||||
|
|
||||||
|
### Rollback DNS — valeurs de référence (ne pas supprimer)
|
||||||
|
- @ → A → 216.24.57.1 (frontend V1 Render)
|
||||||
|
- www → CNAME → expria.onrender.com (frontend V1 Render)
|
||||||
|
- api → CNAME → expria-backend.onrender.com (backend V2 — inchangé)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Procédure de bascule — dans l'ordre exact
|
||||||
|
|
||||||
|
### Étape 1 — Mettre V1 en mode maintenance (2 min)
|
||||||
|
Dans Render, sur le service frontend V1 :
|
||||||
|
- Modifier la variable d'environnement `MAINTENANCE_MODE=true`
|
||||||
|
- Redéployer le service V1
|
||||||
|
- Vérifier que expria.app affiche la page de maintenance
|
||||||
|
|
||||||
|
> ⚠️ À partir de ce moment, expria.app est inaccessible pour les utilisateurs.
|
||||||
|
> Faire cette étape à une heure creuse (nuit, week-end).
|
||||||
|
|
||||||
|
### Étape 2 — Configurer le CNAME dans Vercel (5 min)
|
||||||
|
Dans le dashboard Vercel → Domains → expria.app :
|
||||||
|
- Supprimer ou modifier l'enregistrement A/CNAME actuel qui pointe vers Render
|
||||||
|
- Ajouter un CNAME : `expria.app` → `<projet>.pages.dev` (URL Cloudflare Pages)
|
||||||
|
- Sauvegarder
|
||||||
|
|
||||||
|
### Étape 3 — Configurer le domaine dans Cloudflare Pages (5 min)
|
||||||
|
Dans Cloudflare Pages → projet expria-frontend → Custom domains :
|
||||||
|
- Ajouter `expria.app`
|
||||||
|
- Cloudflare Pages vérifie le CNAME automatiquement
|
||||||
|
- Attendre la validation (peut prendre 1-5 min)
|
||||||
|
|
||||||
|
### Étape 4 — Vérifier la propagation DNS (5-15 min)
|
||||||
|
Vérifier sur https://dnschecker.org que `expria.app` pointe vers Cloudflare Pages.
|
||||||
|
Ne pas continuer avant que la propagation soit visible depuis au moins 3 régions.
|
||||||
|
|
||||||
|
### Étape 5 — Smoke test en production (15 min)
|
||||||
|
Rejouer le Groupe Z du GOLDEN_DATASET.md sur expria.app :
|
||||||
|
- [ ] Z1 — Inscription + première simulation Free
|
||||||
|
- [ ] Z2 — Blocage quota Free
|
||||||
|
- [ ] Z3 — Simulation Standard complète
|
||||||
|
- [ ] Z4 — Mode examen bloqué en Standard
|
||||||
|
- [ ] Z5 — T2 live Premium
|
||||||
|
- [ ] Z6 — Mode examen EE complet
|
||||||
|
- [ ] Z7 — Paiement Free → Standard
|
||||||
|
- [ ] Z8 — Prorata Standard → Premium
|
||||||
|
- [ ] Z9 — Déconnexion + accès protégé
|
||||||
|
- [ ] Z10 — Responsive mobile Home + Login
|
||||||
|
|
||||||
|
### Étape 6 — Vérifier Sentry (5 min)
|
||||||
|
- Ouvrir le dashboard Sentry projet expria-frontend
|
||||||
|
- Vérifier qu'aucune erreur critique n'apparaît dans les 5 premières minutes
|
||||||
|
- Si erreur critique → déclencher le rollback immédiatement
|
||||||
|
|
||||||
|
### Étape 7 — Déclarer la bascule réussie
|
||||||
|
- Noter la date et l'heure dans ce fichier (section 6)
|
||||||
|
- Désactiver MAINTENANCE_MODE sur V1 (optionnel — V1 reste sur Render comme fallback 30 jours)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Rollback — si quelque chose casse
|
||||||
|
|
||||||
|
**Objectif : revenir sur V1 en moins de 10 minutes.**
|
||||||
|
|
||||||
|
### Étape 1 — Désactiver V2 (2 min)
|
||||||
|
Dans Cloudflare Pages → projet expria-frontend :
|
||||||
|
- Désactiver le domaine personnalisé expria.app
|
||||||
|
|
||||||
|
### Étape 2 — Remettre V1 en ligne (3 min)
|
||||||
|
Dans Vercel → Domains → expria.app :
|
||||||
|
- Remettre le CNAME/A record vers Render (valeur originale)
|
||||||
|
Dans Render → service frontend V1 :
|
||||||
|
- Modifier `MAINTENANCE_MODE=false`
|
||||||
|
- Redéployer
|
||||||
|
|
||||||
|
### Étape 3 — Vérifier (5 min)
|
||||||
|
- Vérifier que expria.app affiche à nouveau V1
|
||||||
|
- Vérifier que la connexion et une simulation fonctionnent
|
||||||
|
|
||||||
|
### Étape 4 — Diagnostiquer
|
||||||
|
- Ouvrir Sentry V2 — identifier l'erreur critique
|
||||||
|
- Ne pas retenter la bascule avant d'avoir corrigé et rejoué le Groupe Z complet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Post-bascule — checks 24h après
|
||||||
|
|
||||||
|
- [ ] Sentry : aucune erreur critique nouvelle
|
||||||
|
- [ ] Stripe : webhooks reçus et traités correctement
|
||||||
|
- [ ] Supabase : aucune erreur d'auth dans les logs
|
||||||
|
- [ ] Au moins 1 simulation complète effectuée par un vrai utilisateur
|
||||||
|
- [ ] V1 sur Render toujours en ligne comme fallback (désactiver après 30 jours)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Historique des déploiements
|
||||||
|
|
||||||
|
| Date | Version | Résultat | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| — | — | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Historique du document
|
||||||
|
|
||||||
|
| Version | Date | Changements |
|
||||||
|
|---|---|---|
|
||||||
|
| 1.0 | 2026-04-19 | Création initiale |
|
||||||
63
src/entities/report/__tests__/getMaxScorePerCritere.test.ts
Normal file
63
src/entities/report/__tests__/getMaxScorePerCritere.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { getMaxScorePerCritere, critereCodeFromNom } from '../lib'
|
||||||
|
import type { Critere } from '../types'
|
||||||
|
|
||||||
|
function critere(nom: string, score: number): Critere {
|
||||||
|
return { nom, score, commentaire: '', exemple: '', suggestion: '', astuce: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('getMaxScorePerCritere — Sprint 4.8', () => {
|
||||||
|
it('5 critères (EO Sprint 4.8) → maxScore = 4', () => {
|
||||||
|
const rapport = {
|
||||||
|
criteres: [
|
||||||
|
critere('Adéquation à la tâche', 3),
|
||||||
|
critere('Cohérence et cohésion', 3),
|
||||||
|
critere('Étendue et maîtrise du lexique', 2),
|
||||||
|
critere('Maîtrise morphosyntaxique', 3),
|
||||||
|
critere('Phonologie', 3),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(getMaxScorePerCritere(rapport)).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('4 critères (EE / EO legacy) → maxScore = 5', () => {
|
||||||
|
const rapport = {
|
||||||
|
criteres: [
|
||||||
|
critere('Adéquation à la tâche et au registre', 4),
|
||||||
|
critere('Cohérence et cohésion du discours', 3),
|
||||||
|
critere('Compétence lexicale', 3),
|
||||||
|
critere('Compétence grammaticale', 4),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(getMaxScorePerCritere(rapport)).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('0 critère (cas limite) → maxScore = 5 (défaut sécurité)', () => {
|
||||||
|
expect(getMaxScorePerCritere({ criteres: [] })).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('6+ critères (cas hypothétique) → maxScore = 5 (défaut sécurité)', () => {
|
||||||
|
const rapport = {
|
||||||
|
criteres: Array.from({ length: 6 }, (_, i) => critere(`c${i}`, 0)),
|
||||||
|
}
|
||||||
|
expect(getMaxScorePerCritere(rapport)).toBe(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('critereCodeFromNom — extension Sprint 4.8 EO', () => {
|
||||||
|
it('mappe les libellés EO Sprint 4.8 vers les codes taxonomie', () => {
|
||||||
|
expect(critereCodeFromNom('Adéquation à la tâche')).toBe('adequation_tache')
|
||||||
|
expect(critereCodeFromNom('Cohérence et cohésion')).toBe('coherence_cohesion')
|
||||||
|
expect(critereCodeFromNom('Étendue et maîtrise du lexique')).toBe('competence_lexicale')
|
||||||
|
expect(critereCodeFromNom('Maîtrise morphosyntaxique')).toBe('competence_grammaticale')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Phonologie n'a pas de code taxonomie → null", () => {
|
||||||
|
expect(critereCodeFromNom('Phonologie')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('libellés EE legacy toujours mappés (rétrocompat)', () => {
|
||||||
|
expect(critereCodeFromNom('Adéquation à la tâche et au registre')).toBe('adequation_tache')
|
||||||
|
expect(critereCodeFromNom('Compétence lexicale')).toBe('competence_lexicale')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { hasAccess } from '@/entities/user/lib'
|
import { hasAccess } from '@/entities/user/lib'
|
||||||
import type { Plan } from '@/entities/user/lib'
|
import type { Plan } from '@/entities/user/lib'
|
||||||
import type { BlurableSection, Critere, ErreurCode, CritereCode } from './types'
|
import type { BlurableSection, Critere, ErreurCode, CritereCode, Report } from './types'
|
||||||
|
|
||||||
const SECTION_FEATURE: Record<BlurableSection, 'detailed_report' | 'tips'> = {
|
const SECTION_FEATURE: Record<BlurableSection, 'detailed_report' | 'tips'> = {
|
||||||
criteres: 'detailed_report',
|
criteres: 'detailed_report',
|
||||||
|
|
@ -61,16 +61,40 @@ export function groupErreursByCritere(
|
||||||
* pour rattacher `erreurs_codes` à la bonne carte critère côté UI.
|
* pour rattacher `erreurs_codes` à la bonne carte critère côté UI.
|
||||||
*/
|
*/
|
||||||
const CRITERE_NOM_TO_CODE: Record<string, CritereCode> = {
|
const CRITERE_NOM_TO_CODE: Record<string, CritereCode> = {
|
||||||
|
// Libellés EE (CRITERE_LABELS backend)
|
||||||
'Adéquation à la tâche et au registre': 'adequation_tache',
|
'Adéquation à la tâche et au registre': 'adequation_tache',
|
||||||
'Cohérence et cohésion du discours': 'coherence_cohesion',
|
'Cohérence et cohésion du discours': 'coherence_cohesion',
|
||||||
'Compétence lexicale': 'competence_lexicale',
|
'Compétence lexicale': 'competence_lexicale',
|
||||||
'Compétence grammaticale': 'competence_grammaticale',
|
'Compétence grammaticale': 'competence_grammaticale',
|
||||||
|
// Libellés EO Sprint 4.8 (CRITERE_LABELS_EO backend) — mappés vers les
|
||||||
|
// mêmes codes taxonomie pour rattacher les `erreurs_codes`. La 5e dimension
|
||||||
|
// « Phonologie » n'a pas de CritereCode (aucune erreur taxonomie associée
|
||||||
|
// côté backend) : `critereCodeFromNom('Phonologie')` retourne donc `null`.
|
||||||
|
'Adéquation à la tâche': 'adequation_tache',
|
||||||
|
'Cohérence et cohésion': 'coherence_cohesion',
|
||||||
|
'Étendue et maîtrise du lexique': 'competence_lexicale',
|
||||||
|
'Maîtrise morphosyntaxique': 'competence_grammaticale',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function critereCodeFromNom(nom: string): CritereCode | null {
|
export function critereCodeFromNom(nom: string): CritereCode | null {
|
||||||
return CRITERE_NOM_TO_CODE[nom] ?? null
|
return CRITERE_NOM_TO_CODE[nom] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprint 4.8 — Détecte le score maximum par critère selon le format du rapport.
|
||||||
|
*
|
||||||
|
* - Rapports EO Sprint 4.8 : 5 critères × /4 (4 textuels DeepSeek + Phonologie Gemini).
|
||||||
|
* - Rapports EE et EO legacy : 4 critères × /5.
|
||||||
|
*
|
||||||
|
* Détection sur la donnée elle-même (pas sur la tâche) pour rester rétrocompatible
|
||||||
|
* avec les rapports EO en base d'avant Sprint 4.8.
|
||||||
|
*
|
||||||
|
* Défaut sécurité : tout autre nombre de critères → 5.
|
||||||
|
*/
|
||||||
|
export function getMaxScorePerCritere(rapport: Pick<Report, 'criteres'>): 4 | 5 {
|
||||||
|
return rapport.criteres.length === 5 ? 4 : 5
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcule l'écart en points /20 entre le score obtenu et l'objectif NCLC cible.
|
* Calcule l'écart en points /20 entre le score obtenu et l'objectif NCLC cible.
|
||||||
* Barème TCF Canada (cf. Prompt_maître.md §Barème) : NCLC 9 → 14/20, NCLC 10 → 16/20.
|
* Barème TCF Canada (cf. Prompt_maître.md §Barème) : NCLC 9 → 14/20, NCLC 10 → 16/20.
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export interface ErreurCode {
|
||||||
|
|
||||||
export interface Critere {
|
export interface Critere {
|
||||||
nom: string
|
nom: string
|
||||||
score: number // 0-5
|
score: number // 0-4 (EO Sprint 4.8 — 5 critères) | 0-5 (EE, EO legacy 4 critères)
|
||||||
commentaire: string
|
commentaire: string
|
||||||
exemple: string
|
exemple: string
|
||||||
suggestion: string
|
suggestion: string
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,17 @@ import type { Critere, ErreurCode } from '@/entities/report/types'
|
||||||
interface Props {
|
interface Props {
|
||||||
critere: Critere
|
critere: Critere
|
||||||
erreursCodes: ErreurCode[]
|
erreursCodes: ErreurCode[]
|
||||||
|
/** Sprint 4.8 — échelle par critère : 4 (EO 5 critères) ou 5 (EE / EO legacy 4 critères). */
|
||||||
|
maxScore?: 4 | 5
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CritereCard({ critere, erreursCodes }: Props) {
|
export function CritereCard({ critere, erreursCodes, maxScore = 5 }: Props) {
|
||||||
return (
|
return (
|
||||||
<Card variant="default" className="space-y-3 p-4">
|
<Card variant="default" className="space-y-3 p-4">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<h3 className="text-sm font-semibold text-ink-primary">{critere.nom}</h3>
|
<h3 className="text-sm font-semibold text-ink-primary">{critere.nom}</h3>
|
||||||
<Badge variant="nclc" className="shrink-0 tabular-nums">
|
<Badge variant="nclc" className="shrink-0 tabular-nums">
|
||||||
{critere.score}/5
|
{critere.score}/{maxScore}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,12 @@ class FakeMediaRecorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
static isTypeSupported(_t: string): boolean {
|
static isTypeSupported(_t: string): boolean {
|
||||||
|
void _t
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
start(_timeslice?: number) {
|
start(_timeslice?: number) {
|
||||||
|
void _timeslice
|
||||||
this.state = 'recording'
|
this.state = 'recording'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,6 @@ export function useDeepgramLive(): UseDeepgramLiveResult {
|
||||||
// Conserver l'ancienne connexion ; nouvelle tentative à la prochaine échéance.
|
// Conserver l'ancienne connexion ; nouvelle tentative à la prochaine échéance.
|
||||||
// FTD-XX : durcir avec une retry policy plus fine en Sprint 4c-2.
|
// FTD-XX : durcir avec une retry policy plus fine en Sprint 4c-2.
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [openConnection])
|
}, [openConnection])
|
||||||
|
|
||||||
const connect = useCallback(async () => {
|
const connect = useCallback(async () => {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,12 @@ import { useEffect } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { Lock } from 'lucide-react'
|
import { Lock } from 'lucide-react'
|
||||||
import { usePlan } from '@/features/dashboard/hooks/usePlan'
|
import { usePlan } from '@/features/dashboard/hooks/usePlan'
|
||||||
import { isSectionVisible, groupErreursByCritere, critereCodeFromNom } from '@/entities/report/lib'
|
import {
|
||||||
|
isSectionVisible,
|
||||||
|
groupErreursByCritere,
|
||||||
|
critereCodeFromNom,
|
||||||
|
getMaxScorePerCritere,
|
||||||
|
} from '@/entities/report/lib'
|
||||||
import type { Report } from '@/entities/report/types'
|
import type { Report } from '@/entities/report/types'
|
||||||
import { useRapport } from '../hooks/useRapport'
|
import { useRapport } from '../hooks/useRapport'
|
||||||
import { useSimulation } from '../hooks/useSimulation'
|
import { useSimulation } from '../hooks/useSimulation'
|
||||||
|
|
@ -93,6 +98,7 @@ function RapportSkeleton() {
|
||||||
|
|
||||||
function CriteresSection({ rapport }: { rapport: Report }) {
|
function CriteresSection({ rapport }: { rapport: Report }) {
|
||||||
const grouped = groupErreursByCritere(rapport.erreurs_codes)
|
const grouped = groupErreursByCritere(rapport.erreurs_codes)
|
||||||
|
const maxScore = getMaxScorePerCritere(rapport)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section aria-label="Détail par critère">
|
<section aria-label="Détail par critère">
|
||||||
|
|
@ -101,7 +107,7 @@ function CriteresSection({ rapport }: { rapport: Report }) {
|
||||||
{rapport.criteres.map((c) => {
|
{rapport.criteres.map((c) => {
|
||||||
const code = critereCodeFromNom(c.nom)
|
const code = critereCodeFromNom(c.nom)
|
||||||
const codes = code ? grouped[code] : []
|
const codes = code ? grouped[code] : []
|
||||||
return <CritereCard key={c.nom} critere={c} erreursCodes={codes} />
|
return <CritereCard key={c.nom} critere={c} erreursCodes={codes} maxScore={maxScore} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
* - onSuccess : setPresentationT1 + navigate vers /simulation/eo/t1/presentation
|
* - onSuccess : setPresentationT1 + navigate vers /simulation/eo/t1/presentation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
import { render, screen, cleanup, waitFor } from '@testing-library/react'
|
import { render, screen, cleanup, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
@import 'tailwindcss';
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
/* Dark = défaut. `.light` sur <html> active le mode clair (override sur --color-*). */
|
/* Dark = défaut. `.light` sur <html> active le mode clair (override sur --color-*). */
|
||||||
@custom-variant light (&:where(.light, .light *));
|
@custom-variant light (&:where(.light, .light *));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue