From 6bfdf15db991255fde410ebe90d5bdede52341da Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Tue, 21 Apr 2026 02:48:48 +0300 Subject: [PATCH] =?UTF-8?q?feat(simulations):=20finaliser=20flux=20/sujets?= =?UTF-8?q?=20=E2=80=94=20SimulationForm=20+=20SujetDisplay=20+=20TaskSele?= =?UTF-8?q?ctor=20type=20prop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SimulationForm : bouton "Changer de sujet" → /sujets (étape 3 refonte) - SujetDisplay : redevient présentationnel (plus de dropdown) - TaskSelector : prop type 'EE' | 'EO' (EO_CARDS réservé usage futur — non routé) - SimulationPage : type='EE' hardcodé (EO restera ComingSoon jusqu'au Sprint EO) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../simulations/components/SimulationForm.tsx | 31 ++++--- .../simulations/components/SujetDisplay.tsx | 82 ++----------------- .../simulations/components/TaskSelector.tsx | 54 ++++++++---- .../simulations/pages/SimulationPage.tsx | 18 ++-- 4 files changed, 69 insertions(+), 116 deletions(-) diff --git a/src/features/simulations/components/SimulationForm.tsx b/src/features/simulations/components/SimulationForm.tsx index b2d0c07..d6fffeb 100644 --- a/src/features/simulations/components/SimulationForm.tsx +++ b/src/features/simulations/components/SimulationForm.tsx @@ -11,7 +11,7 @@ */ import { useEffect, useRef, useState, type FormEvent } from 'react' -import { Clock, Loader2 } from 'lucide-react' +import { Clock, Loader2, Shuffle } from 'lucide-react' import { z } from 'zod' import { Button } from '@/shared/components/ui/button' import { formatTache } from '@/entities/production/lib' @@ -49,20 +49,16 @@ function mapCorrectError(err: ApiError | null): string | null { interface Props { tache: Tache sujet: SujetData | null - sujets: SujetData[] - isLoadingSujets: boolean isSubmitting: boolean error: ApiError | null onSubmit: (texte: string) => void onBack: () => void - onChangeSujet: (sujet: SujetData) => void + onChangeSujet: () => void } export function SimulationForm({ tache, sujet, - sujets, - isLoadingSujets, isSubmitting, error, onSubmit, @@ -150,13 +146,22 @@ export function SimulationForm({

{formatTache(tache)}

- + + + {sujet && ( +
+ +
+ )} {apiError && (
void - disabled?: boolean -} - -function truncate(s: string, max: number): string { - if (s.length <= max) return s - return `${s.slice(0, max).trimEnd()}…` } function DocumentBlock({ titre, texte }: { titre: string | null; texte: string | null }) { @@ -42,67 +31,12 @@ function DocumentBlock({ titre, texte }: { titre: string | null; texte: string | ) } -export function SujetDisplay({ - sujet, - sujets, - isLoadingSujets, - onChangeSujet, - disabled = false, -}: Props) { +export function SujetDisplay({ sujet }: Props) { if (!sujet) return null - const hasCatalog = sujets.length > 0 - const canRandomize = hasCatalog && sujets.length > 1 - - function handleSelectChange(e: React.ChangeEvent) { - const next = sujets.find((s) => s.id === e.target.value) - if (next && next.id !== sujet?.id) onChangeSujet(next) - } - - function handleRandom() { - if (sujets.length === 0) return - const others = sujets.length > 1 ? sujets.filter((s) => s.id !== sujet?.id) : sujets - const pick = others[Math.floor(Math.random() * others.length)] - if (pick) onChangeSujet(pick) - } - return (
- {(hasCatalog || isLoadingSujets) && ( -
- - - -
- )} - {sujet.role && (
Rôle diff --git a/src/features/simulations/components/TaskSelector.tsx b/src/features/simulations/components/TaskSelector.tsx index 24c8adb..0387b46 100644 --- a/src/features/simulations/components/TaskSelector.tsx +++ b/src/features/simulations/components/TaskSelector.tsx @@ -1,14 +1,15 @@ /** - * Sélecteur de tâche pour lancer une simulation Expression Écrite. + * Sélecteur de tâche pour lancer une simulation. * - * Affiche les 3 tâches EE : T1, T2, T3 (sélectionnables si quota OK). - * Les tâches Expression Orale seront sur /simulation/eo (Sprint EO). + * Filtre les cartes selon `type` : + * - 'EE' → 3 tâches EE (T1/T2/T3) sélectionnables si quota OK + * - 'EO' → EO_T1 (Entretien) + EO_T3 (Point de vue) + EO_T2 Live verrouillé (Premium) * * Règle D : le quota est vérifié via canSimulate(), jamais if (plan === 'free'). * Règle H : aucune logique métier — uniquement appel de canSimulate() et affichage. */ -import { Loader2 } from 'lucide-react' +import { Lock, Loader2 } from 'lucide-react' import { canSimulate } from '@/entities/user/lib' import { cn } from '@/shared/lib/utils' import { Card } from '@/shared/ui/Card' @@ -16,7 +17,10 @@ import { Badge } from '@/shared/ui/Badge' import type { Plan } from '@/entities/user/lib' import type { CreateSimulationPayload, Tache } from '@/entities/production/types' +export type TaskKind = 'EE' | 'EO' + interface Props { + type: TaskKind plan: Plan simulationsUsed: number isLoading: boolean @@ -24,20 +28,29 @@ interface Props { } interface TaskCard { - tache: Tache + key: string + tache: Tache | null // null = carte verrouillée (EO_T2 Live) label: string sublabel: string + lockLabel?: string } -const TASK_CARDS: readonly TaskCard[] = [ - { tache: 'EE_T1', label: 'Expression Écrite', sublabel: 'Tâche 1' }, - { tache: 'EE_T2', label: 'Expression Écrite', sublabel: 'Tâche 2' }, - { tache: 'EE_T3', label: 'Expression Écrite', sublabel: 'Tâche 3' }, +const EE_CARDS: readonly TaskCard[] = [ + { key: 'EE_T1', tache: 'EE_T1', label: 'Expression Écrite', sublabel: 'Tâche 1' }, + { key: 'EE_T2', tache: 'EE_T2', label: 'Expression Écrite', sublabel: 'Tâche 2' }, + { key: 'EE_T3', tache: 'EE_T3', label: 'Expression Écrite', sublabel: 'Tâche 3' }, ] -export function TaskSelector({ plan, simulationsUsed, isLoading, onSelect }: Props) { +const EO_CARDS: readonly TaskCard[] = [ + { key: 'EO_T1', tache: 'EO_T1', label: 'Expression Orale', sublabel: 'Entretien' }, + { key: 'EO_T3', tache: 'EO_T3', label: 'Expression Orale', sublabel: 'Point de vue' }, + { key: 'EO_T2_LIVE', tache: null, label: 'Expression Orale', sublabel: 'Tâche 2 — Live', lockLabel: 'Exclusivité Premium' }, +] + +export function TaskSelector({ type, plan, simulationsUsed, isLoading, onSelect }: Props) { const simulationCheck = canSimulate(plan, simulationsUsed) const quotaBlocked = !simulationCheck.allowed + const cards = type === 'EE' ? EE_CARDS : EO_CARDS return (
@@ -62,31 +75,40 @@ export function TaskSelector({ plan, simulationsUsed, isLoading, onSelect }: Pro )}
- {TASK_CARDS.map((card) => { - const abbrev = card.tache.split('_')[0] + {cards.map((card) => { + const locked = card.tache === null || quotaBlocked + const abbrev = card.tache ? card.tache.split('_')[0] : 'EO' - if (quotaBlocked) { + if (locked) { return ( + {card.tache === null && ( + ) } return ( { - if (!isLoading) onSelect({ tache: card.tache, mode: 'entrainement' }) + if (!isLoading && card.tache) { + onSelect({ tache: card.tache, mode: 'entrainement' }) + } }} >
diff --git a/src/features/simulations/pages/SimulationPage.tsx b/src/features/simulations/pages/SimulationPage.tsx index c60fc9c..e9f6bff 100644 --- a/src/features/simulations/pages/SimulationPage.tsx +++ b/src/features/simulations/pages/SimulationPage.tsx @@ -1,8 +1,8 @@ /** * Page de simulation Expression Écrite. * - * Orchestre les 3 étapes du flux : sélection de tâche → saisie du texte → rapport. - * Le choix du sujet est désormais délégué à la page /sujets (refonte UX 2026-04-21). + * Orchestre les 3 étapes du flux : sélection de tâche → saisie → rapport. + * Le choix du sujet est délégué à la page /sujets (refonte UX 2026-04-21). * * Règle D : quotas et permissions passent par canSimulate() — jamais de plan === '...' * Règle H : aucune logique métier — tout est dans useSimulation() et les entités. @@ -17,7 +17,6 @@ import { useQuery } from '@tanstack/react-query' import { getPlanStatus } from '@/entities/user/api' import { Button } from '@/shared/ui/Button' import { useSimulation } from '../hooks/useSimulation' -import { useSujets } from '../hooks/useSujets' import { TaskSelector } from '../components/TaskSelector' import { SimulationForm } from '../components/SimulationForm' @@ -57,16 +56,10 @@ export function SimulationPage() { correctError, selectTask, submitText, - changeSubject, + goToSubjectPicker, reset, } = useSimulation() - // Catalogue passé à SimulationForm (dropdown hérité — refacto étape 3). - const { data: sujets, isLoading: isLoadingSujets } = useSujets( - production?.tache ?? 'EE_T1', - !!production, - ) - // Redirige vers /sujets dès que la création aboutit pour une tâche avec catalogue. useEffect(() => { if (step === 'choosing-subject' && production) { @@ -97,6 +90,7 @@ export function SimulationPage() { {planData && step === 'idle' && ( )}