feat(simulations): useSimulation hook + TaskSelector + SimulationForm + SimulationPage + route (Sprint 3 étape 14)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-20 00:08:34 +03:00
parent b31e8666a5
commit 997f39bd33
7 changed files with 621 additions and 0 deletions

View file

@ -0,0 +1,110 @@
/**
* Page de simulation Expression Écrite.
*
* Orchestre les 3 étapes du flux : sélection de tâche saisie du texte rapport.
*
* 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.
*
* Nota : queryKey ['plan'] est dupliqué depuis features/dashboard/hooks/usePlan.
* TanStack Query partage le cache par clé FTD-17.
*/
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { getPlanStatus } from '@/entities/user/api'
import { Logo } from '@/shared/components/Logo'
import { ThemeToggle } from '@/shared/components/ThemeToggle'
import { Button } from '@/shared/components/ui/button'
import { useSimulation } from '../hooks/useSimulation'
import { TaskSelector } from '../components/TaskSelector'
import { SimulationForm } from '../components/SimulationForm'
function SimulationSkeleton() {
return (
<div className="space-y-4" aria-busy="true" aria-label="Chargement…">
<div className="h-6 w-40 animate-pulse rounded bg-canvas-2" />
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-24 animate-pulse rounded-lg bg-canvas-2" />
))}
</div>
</div>
)
}
export function SimulationPage() {
const navigate = useNavigate()
const {
data: planData,
isLoading: isPlanLoading,
isError: isPlanError,
refetch: refetchPlan,
} = useQuery({
queryKey: ['plan'],
queryFn: getPlanStatus,
staleTime: 5 * 60 * 1000,
})
const {
step,
production,
isCreating,
isCorrecting,
correctError,
selectTask,
submitText,
reset,
} = useSimulation()
useEffect(() => {
if (step === 'done' && production) {
navigate(`/rapport/${production.id}`)
}
}, [step, production, navigate])
return (
<div className="min-h-screen bg-canvas">
<header className="flex items-center justify-between border-b border-line bg-surface px-4 py-3">
<Logo size="sm" />
<ThemeToggle />
</header>
<main className="mx-auto max-w-2xl px-4 py-6">
{isPlanLoading && <SimulationSkeleton />}
{isPlanError && (
<div className="space-y-3 text-center">
<p className="text-sm text-danger">
Impossible de charger vos informations. Réessayez dans quelques instants.
</p>
<Button variant="outline" size="sm" onClick={() => refetchPlan()}>
Réessayer
</Button>
</div>
)}
{planData && step === 'idle' && (
<TaskSelector
plan={planData.plan}
simulationsUsed={planData.simulations_used}
isLoading={isCreating}
onSelect={selectTask}
/>
)}
{planData && (step === 'task-selected' || step === 'correcting') && production && (
<SimulationForm
tache={production.tache}
isSubmitting={isCorrecting}
error={correctError}
onSubmit={submitText}
onBack={reset}
/>
)}
</main>
</div>
)
}