expria-frontend/src/features/simulations/state/SimulationFlowProvider.tsx
Hermann_Kitio 99617f117c style: prettier format
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 03:17:16 +03:00

150 lines
5.2 KiB
TypeScript

/**
* 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<SimulationStep>('idle')
const [production, setProduction] = useState<Production | null>(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 <SimulationFlowContext.Provider value={value}>{children}</SimulationFlowContext.Provider>
}