141 lines
4.9 KiB
TypeScript
141 lines
4.9 KiB
TypeScript
/**
|
|
* 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/simulationFlow'
|
|
import { useSujets } from '../hooks/useSujets'
|
|
import { SujetCard } from '../components/SujetCard'
|
|
|
|
function SujetsSkeleton() {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<div key={i} className="h-32 animate-pulse rounded-lg bg-canvas-2" />
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function SujetsPage() {
|
|
const navigate = useNavigate()
|
|
const { step, production, changeSubject, setStep, reset } = useSimulationFlow()
|
|
|
|
// Redirige vers /simulation/ee si :
|
|
// - production absente (refresh direct sur /sujets sans contexte)
|
|
// - step === 'idle' (état initial, pas de simulation en cours)
|
|
// - step === 'done' (simulation corrigée — /sujets ne doit pas patcher
|
|
// une simulation dont le rapport est déjà persisté — cf. FTD-23)
|
|
const shouldRedirect = !production || step === 'idle' || step === 'done'
|
|
useEffect(() => {
|
|
if (shouldRedirect) navigate('/simulation/ee', { replace: true })
|
|
}, [shouldRedirect, navigate])
|
|
|
|
const { data: sujets, isLoading, isError, refetch } = useSujets(
|
|
production?.tache ?? 'EE_T1',
|
|
!!production && !shouldRedirect,
|
|
)
|
|
|
|
if (shouldRedirect || !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 (
|
|
<main className="mx-auto max-w-4xl px-4 py-6">
|
|
<div className="mb-4 flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
// « Retour » = annuler la simulation en cours et revenir au
|
|
// TaskSelector. reset() doit être appelé AVANT navigate pour que
|
|
// step retombe à 'idle' sans repasser par 'choosing-subject'.
|
|
reset()
|
|
navigate('/simulation/ee')
|
|
}}
|
|
className="text-sm text-ink-4 underline-offset-4 hover:text-ink-2 hover:underline"
|
|
>
|
|
← Retour
|
|
</button>
|
|
<h2 className="flex-1 text-lg font-semibold text-ink-1">
|
|
Choisir un sujet — {formatTache(production.tache)}
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
<p className="text-sm text-ink-3">
|
|
{isLoading
|
|
? 'Chargement des sujets…'
|
|
: hasSujets
|
|
? `${sujets!.length} sujet${sujets!.length > 1 ? 's' : ''} disponible${sujets!.length > 1 ? 's' : ''}.`
|
|
: 'Aucun sujet disponible pour cette tâche.'}
|
|
</p>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
icon={<Shuffle className="size-4" aria-hidden="true" />}
|
|
onClick={handleRandom}
|
|
disabled={!hasSujets}
|
|
>
|
|
Sujet aléatoire
|
|
</Button>
|
|
</div>
|
|
|
|
{isError && (
|
|
<div
|
|
role="alert"
|
|
className="mb-4 rounded-md border border-danger/40 bg-danger-bg px-3 py-2 text-sm text-danger"
|
|
>
|
|
Impossible de charger les sujets.{' '}
|
|
<button
|
|
type="button"
|
|
onClick={() => refetch()}
|
|
className="underline underline-offset-2"
|
|
>
|
|
Réessayer
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<SujetsSkeleton />
|
|
) : hasSujets ? (
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
{sujets!.map((sujet) => (
|
|
<SujetCard key={sujet.id} sujet={sujet} onSelect={handleSelect} />
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</main>
|
|
)
|
|
}
|