/** * 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 { useEffect, useRef, useState, type ReactNode } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { useMutation } from '@tanstack/react-query' import { createSimulation, getSimulationState, updateSujet as updateSujetApi, } from '@/entities/production/api' import { correctEe } from '@/entities/report/api' import type { CreateSimulationPayload, Production, Tache } from '@/entities/production/types' import type { Report } from '@/entities/report/types' import type { ApiError } from '@/shared/types/api' import type { SujetData } from '@/entities/production/types' import { SimulationFlowContext, type FlowValue, type SimulationStep } from './simulationFlow' const TACHES_SANS_CATALOGUE: Tache[] = ['EO_T1'] const LS_SIMULATION_ID_KEY = 'expria_simulation_id' export function SimulationFlowProvider({ children }: { children: ReactNode }) { const [step, setStep] = useState('idle') const [production, setProduction] = useState(null) const navigate = useNavigate() const location = useLocation() const hydratedRef = useRef(false) // FTD-21 — restauration de session depuis localStorage au montage. // Si `rapport === null` → simulation en cours, on restaure le state et redirige // vers /simulation/ee. Sinon (rapport présent ou erreur/404) → on nettoie. useEffect(() => { if (hydratedRef.current) return hydratedRef.current = true const id = localStorage.getItem(LS_SIMULATION_ID_KEY) if (!id) return getSimulationState(id) .then((state) => { if (state.rapport !== null) { localStorage.removeItem(LS_SIMULATION_ID_KEY) return } setProduction({ id: state.simulation_id, tache: state.tache, mode: state.mode, created_at: state.created_at, sujet: state.sujet, contenu: state.contenu ?? undefined, sujet_id: state.sujet?.id, }) setStep('task-selected') if (!location.pathname.startsWith('/simulation/ee')) { navigate('/simulation/ee') } }) .catch(() => { localStorage.removeItem(LS_SIMULATION_ID_KEY) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const createMutation = useMutation({ mutationFn: createSimulation, onSuccess: (data) => { setProduction(data) const hasCatalogue = !TACHES_SANS_CATALOGUE.includes(data.tache) setStep(hasCatalogue ? 'choosing-subject' : 'task-selected') // Navigation initiale vers /sujets pour les tâches avec catalogue — // gérée ici (et non dans un useEffect sticky côté SimulationPage) pour // éviter la boucle infinie quand l'utilisateur revient depuis /sujets. if (hasCatalogue) { navigate('/sujets') } }, }) const correctMutation = useMutation({ mutationFn: correctEe, onMutate: () => setStep('correcting'), onSuccess: (_data, variables) => { setStep('done') localStorage.removeItem(LS_SIMULATION_ID_KEY) // Navigation vers le rapport déclenchée ici (plutôt que depuis un // useEffect sticky côté SimulationPage) — une seule fois par correction, // pas de redirection en boucle si l'utilisateur revient sur /simulation/ee. navigate(`/rapport/${variables.simulationId}`) }, onError: () => setStep('task-selected'), }) function selectTask(payload: CreateSimulationPayload): void { createMutation.mutate(payload) } function submitText(texte: string, nclcCible: 9 | 10 = 9): void { if (!production) return correctMutation.mutate({ simulationId: production.id, contenu: texte, tache: production.tache, nclc_cible: nclcCible, }) } function changeSubject(sujet: SujetData): void { // FTD-21 — persiste le changement côté backend (best-effort : l'UI ne bloque pas). if (production) { void updateSujetApi(production.id, sujet.id).catch(() => { // silencieux : le sujet reste localement, le resume ramènera l'ancien si échec }) } setProduction((p) => (p ? { ...p, sujet } : p)) } function reset(): void { setStep('idle') setProduction(null) localStorage.removeItem(LS_SIMULATION_ID_KEY) 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} }