diff --git a/src/features/simulations/components/SujetCard.tsx b/src/features/simulations/components/SujetCard.tsx new file mode 100644 index 0000000..54471f5 --- /dev/null +++ b/src/features/simulations/components/SujetCard.tsx @@ -0,0 +1,31 @@ +/** + * Carte cliquable pour un sujet dans la grille /sujets. + * + * Rendu : consigne tronquée sur 3 lignes (line-clamp-3) + badge rôle si présent. + * Règle H : purement présentationnel — l'action vient du parent. + * Règle L : tokens Direction H via la primitive Card (variant interactive). + */ + +import { Badge } from '@/shared/ui/Badge' +import { Card } from '@/shared/ui/Card' +import type { SujetData } from '@/entities/production/types' + +interface Props { + sujet: SujetData + onSelect: (sujet: SujetData) => void +} + +export function SujetCard({ sujet, onSelect }: Props) { + return ( + onSelect(sujet)}> +
+ {sujet.role && ( +
+ {sujet.role} +
+ )} +

{sujet.consigne}

+
+
+ ) +} diff --git a/src/features/simulations/pages/SujetsPage.tsx b/src/features/simulations/pages/SujetsPage.tsx new file mode 100644 index 0000000..15a5b38 --- /dev/null +++ b/src/features/simulations/pages/SujetsPage.tsx @@ -0,0 +1,130 @@ +/** + * Page /sujets — sélection d'un sujet en cartes pour une production en cours. + * + * Flux : + * 1. /simulation/ee → selectTask (POST /simulations) → navigate('/sujets') + * 2. Ici : liste les sujets de la tâche en cours, permet choix manuel ou aléatoire + * 3. Sélection → changeSubject + navigate('/simulation/ee') (SimulationForm visible) + * + * MVP : refresh direct sur /sujets → redirect vers /simulation/ee (pas de state). + * Règle D : aucun contrôle de plan/quota ici — déjà fait à la création de la simulation. + * Règle H : aucune logique métier — délègue au provider + useSujets. + * Règle L : tokens Direction H exclusivement. + */ + +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Shuffle } from 'lucide-react' +import { Button } from '@/shared/ui/Button' +import { formatTache } from '@/entities/production/lib' +import type { SujetData } from '@/entities/production/types' +import { useSimulationFlow } from '../state/SimulationFlowProvider' +import { useSujets } from '../hooks/useSujets' +import { SujetCard } from '../components/SujetCard' + +function SujetsSkeleton() { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ ) +} + +export function SujetsPage() { + const navigate = useNavigate() + const { production, changeSubject, setStep } = useSimulationFlow() + + // MVP : pas de production en contexte (refresh direct) → retour à /simulation/ee + useEffect(() => { + if (!production) navigate('/simulation/ee', { replace: true }) + }, [production, navigate]) + + const { data: sujets, isLoading, isError, refetch } = useSujets( + production?.tache ?? 'EE_T1', + !!production, + ) + + if (!production) return null + + function handleSelect(sujet: SujetData) { + changeSubject(sujet) + setStep('task-selected') + navigate('/simulation/ee') + } + + function handleRandom() { + if (!sujets || sujets.length === 0) return + const pool = production?.sujet + ? sujets.filter((s) => s.id !== production.sujet?.id) + : sujets + const list = pool.length > 0 ? pool : sujets + const pick = list[Math.floor(Math.random() * list.length)] + if (pick) handleSelect(pick) + } + + const hasSujets = (sujets?.length ?? 0) > 0 + + return ( +
+
+ +

+ Choisir un sujet — {formatTache(production.tache)} +

+
+ +
+

+ {isLoading + ? 'Chargement des sujets…' + : hasSujets + ? `${sujets!.length} sujet${sujets!.length > 1 ? 's' : ''} disponible${sujets!.length > 1 ? 's' : ''}.` + : 'Aucun sujet disponible pour cette tâche.'} +

+ +
+ + {isError && ( +
+ Impossible de charger les sujets.{' '} + +
+ )} + + {isLoading ? ( + + ) : hasSujets ? ( +
+ {sujets!.map((sujet) => ( + + ))} +
+ ) : null} +
+ ) +} diff --git a/src/features/simulations/state/SimulationFlowProvider.tsx b/src/features/simulations/state/SimulationFlowProvider.tsx new file mode 100644 index 0000000..c091eab --- /dev/null +++ b/src/features/simulations/state/SimulationFlowProvider.tsx @@ -0,0 +1,117 @@ +/** + * Provider de flux simulation — partage l'état entre /simulation/ee et /sujets. + * + * Hérite de la state machine de useSimulation mais déplace la source de vérité + * hors du hook pour qu'elle survive aux navigations React Router (Option A — cf. + * plan de refonte UX "page /sujets avec cartes"). + * + * Règle H : aucune logique métier — les mutations s'appuient sur entities/. + */ + +import { createContext, useContext, useState, type ReactNode } from 'react' +import { useMutation } from '@tanstack/react-query' +import { createSimulation } from '@/entities/production/api' +import { correctEe } from '@/entities/report/api' +import type { + CreateSimulationPayload, + Production, + SujetData, + Tache, +} from '@/entities/production/types' +import type { Report } from '@/entities/report/types' +import type { ApiError } from '@/shared/types/api' + +export type SimulationStep = + | 'idle' + | 'choosing-subject' + | 'task-selected' + | 'correcting' + | 'done' + +const TACHES_SANS_CATALOGUE: Tache[] = ['EO_T1'] + +interface FlowValue { + step: SimulationStep + production: Production | null + sujet: SujetData | null + report: Report | null + isCreating: boolean + isCorrecting: boolean + createError: ApiError | null + correctError: ApiError | null + selectTask: (payload: CreateSimulationPayload) => void + submitText: (texte: string) => void + changeSubject: (sujet: SujetData) => void + setStep: (step: SimulationStep) => void + reset: () => void +} + +const SimulationFlowContext = createContext(null) + +export function SimulationFlowProvider({ children }: { children: ReactNode }) { + const [step, setStep] = useState('idle') + const [production, setProduction] = useState(null) + + const createMutation = useMutation({ + mutationFn: createSimulation, + onSuccess: (data) => { + setProduction(data) + setStep(TACHES_SANS_CATALOGUE.includes(data.tache) ? 'task-selected' : 'choosing-subject') + }, + }) + + const correctMutation = useMutation({ + mutationFn: correctEe, + onMutate: () => setStep('correcting'), + onSuccess: () => setStep('done'), + onError: () => setStep('task-selected'), + }) + + function selectTask(payload: CreateSimulationPayload): void { + createMutation.mutate(payload) + } + + function submitText(texte: string): void { + if (!production) return + correctMutation.mutate({ simulationId: production.id, contenu: texte, tache: production.tache }) + } + + function changeSubject(sujet: SujetData): void { + setProduction((p) => (p ? { ...p, sujet } : p)) + } + + function reset(): void { + setStep('idle') + setProduction(null) + createMutation.reset() + correctMutation.reset() + } + + const value: FlowValue = { + step, + production, + sujet: production?.sujet ?? null, + report: (correctMutation.data ?? null) as Report | null, + isCreating: createMutation.isPending, + isCorrecting: correctMutation.isPending, + createError: createMutation.error as ApiError | null, + correctError: correctMutation.error as ApiError | null, + selectTask, + submitText, + changeSubject, + setStep, + reset, + } + + return ( + {children} + ) +} + +export function useSimulationFlow(): FlowValue { + const ctx = useContext(SimulationFlowContext) + if (!ctx) { + throw new Error('useSimulationFlow doit être utilisé dans un .') + } + return ctx +}