150 lines
5.2 KiB
TypeScript
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>
|
|
}
|