refactor(simulations): supprimer SujetSelector + selectSujet orphelins (FTD-22)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-21 02:56:02 +03:00
parent 6bfdf15db9
commit 4712a3a16e
4 changed files with 40 additions and 170 deletions

View file

@ -339,6 +339,7 @@ Vient du pattern `c.json(result, result.status)` où `result` contient déjà `s
| FTD-11 | `@theme` Tailwind 4 non défini — palette et typographie absentes | 2026-04-18 | Résolu au Sprint 0.5 (design system). Palette Direction H complète (canvas/surface/ink/expria/deep/semantic) + typo Plus Jakarta Sans définis dans `src/index.css` via `@theme {}` et `.dark {}`. shadcn/ui remappé sur ces tokens. Règle L ajoutée dans `DEVELOPMENT_PRINCIPLES.md` pour garantir l'usage exclusif des tokens. |
| FTD-13 | Incompatibilité Vitest 3 / Vite 8 (conflit de types `Plugin<any>` entre le Vite 8 top-level avec Rolldown et le Vite 7 pinné de Vitest 3.2.4 ; `npm run build` cassé) | 2026-04-17 | Résolu par upgrade Vitest `3.2.4 → 4.1.4` (et `@vitest/coverage-v8` idem) à l'étape 12-bis du Sprint 0. Vitest 4.x supporte nativement Vite 8 Rolldown. Correctif complémentaire : script `typecheck` passé de `tsc --noEmit -p tsconfig.app.json` à `tsc -b --noEmit` pour couvrir aussi `tsconfig.node.json` (d'où `vite.config.ts`) et éviter qu'un bug similaire échappe à la CI. |
| FTD-16 | `VITE_MAINTENANCE_MODE` non lu dans le code — la variable d'env était dans `env.ts` mais jamais consommée | 2026-04-18 | Résolu au Sprint 1 étape 6. Ajout de `isMaintenanceMode` dans `src/shared/config/env.ts` et garde dans `src/app/main.tsx` : `isMaintenanceMode ? <MaintenancePage /> : <Providers />`. `MaintenancePage` est statique (aucun provider requis), tokens Direction H exclusivement. |
| FTD-22 | Code orphelin suite à la refonte UX `/sujets` (2026-04-21) — composant `SujetSelector` et helper `selectSujet` plus référencés après bascule dropdown → page dédiée | 2026-04-21 | Résolu **partiellement**. Supprimé : `src/features/simulations/components/SujetSelector.tsx` + helper `selectSujet` de `useSimulation.ts` (les tests `useSimulation.test.tsx` adaptés en utilisant `changeSubject` + `setStep('task-selected')` via `useSimulationFlow`). **Conservés intentionnellement** : le step `'choosing-subject'` (utilisé par `SimulationFlowProvider.selectTask` pour les tâches avec catalogue et par `SimulationPage` pour naviguer vers `/sujets`) et le helper `goToSubjectPicker` (bouton "Changer de sujet" dans `SimulationForm`). |
---
@ -355,3 +356,4 @@ Vient du pattern `c.json(result, result.status)` où `result` contient déjà `s
| 1.6 | 2026-04-20 | Ajout FTD-18 (SimulationForm shadcn Button — Sprint 0.5 bis D2) ; ajout FTD-19 (token --shadow-focus manquant — Sprint 0.5 bis D2) |
| 1.7 | 2026-04-20 | Ajout FTD-20 🔴 (GET /simulations/:id manquant backend — bloque RapportPage Sprint 3 étape 15) |
| 1.8 | 2026-04-20 | Ajout FTD-21 🔴 (persistance session simulation — prod + sujet perdus au refresh, session dédiée après G1-G5) |
| 1.9 | 2026-04-21 | FTD-22 résolu partiellement (nettoyage code orphelin refonte `/sujets``SujetSelector` + `selectSujet` supprimés ; `choosing-subject` + `goToSubjectPicker` conservés) |

View file

@ -1,153 +0,0 @@
/**
* Écran de choix du sujet avant de démarrer la rédaction.
*
* Deux modes :
* - `choice` : propose "Sujet aléatoire" ou "Choisir dans la liste"
* - `list` : affiche les sujets actifs renvoyés par GET /sujets
*
* Règle H : aucune logique métier le composant reçoit tache et handlers,
* et consomme `useSujets` pour la lecture du catalogue.
* Règle L : tokens Direction H uniquement (primitives Card / Button).
*
* Pour EO_T1, cet écran n'est pas rendu (flux direct SimulationForm)
* cf. SimulationPage.
*/
import { useState } from 'react'
import { ArrowLeft, Loader2, Shuffle, List } from 'lucide-react'
import { Button } from '@/shared/ui/Button'
import { Card } from '@/shared/ui/Card'
import type { SujetData, Tache } from '@/entities/production/types'
import { useSujets } from '../hooks/useSujets'
interface Props {
tache: Tache
currentSujetId: string | null
onSelect: (sujet: SujetData) => void
onRandom: (sujets: SujetData[]) => void
}
type ViewMode = 'choice' | 'list'
export function SujetSelector({ tache, currentSujetId, onSelect, onRandom }: Props) {
const [mode, setMode] = useState<ViewMode>('choice')
const { data: sujets, isLoading, isError, refetch } = useSujets(tache)
if (mode === 'choice') {
return (
<div className="space-y-3">
<div>
<h2 className="text-lg font-semibold text-ink-1">Choisir un sujet</h2>
<p className="mt-1 text-sm text-ink-4">
Lancez-vous avec un sujet aléatoire ou parcourez le catalogue.
</p>
</div>
<Card
variant="interactive"
onClick={() => onRandom(sujets ?? [])}
>
<div className="flex items-start gap-3 p-4 text-left">
<Shuffle className="mt-0.5 size-5 shrink-0 text-expria" aria-hidden="true" />
<div>
<div className="text-sm font-medium text-ink-1">Sujet aléatoire</div>
<p className="mt-0.5 text-xs text-ink-4">
On tire un sujet au hasard dans le catalogue.
</p>
</div>
</div>
</Card>
<Card
variant="interactive"
onClick={() => setMode('list')}
>
<div className="flex items-start gap-3 p-4 text-left">
<List className="mt-0.5 size-5 shrink-0 text-expria" aria-hidden="true" />
<div>
<div className="text-sm font-medium text-ink-1">Choisir dans la liste</div>
<p className="mt-0.5 text-xs text-ink-4">
Parcourir tous les sujets disponibles pour cette tâche.
</p>
</div>
</div>
</Card>
</div>
)
}
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
icon={<ArrowLeft className="size-4" aria-hidden="true" />}
onClick={() => setMode('choice')}
>
Retour
</Button>
<h2 className="text-lg font-semibold text-ink-1">Liste des sujets</h2>
</div>
{isLoading && (
<div
role="status"
aria-live="polite"
className="flex items-center gap-2 text-sm text-ink-4"
>
<Loader2 className="size-4 animate-spin" aria-hidden="true" />
Chargement des sujets
</div>
)}
{isError && (
<div className="space-y-2">
<p role="alert" className="text-sm text-danger">
Impossible de charger la liste. Réessayez dans quelques instants.
</p>
<Button variant="secondary" size="sm" onClick={() => refetch()}>
Réessayer
</Button>
</div>
)}
{sujets && sujets.length === 0 && (
<p className="text-sm text-ink-4">
Aucun sujet disponible pour cette tâche pour le moment.
</p>
)}
{sujets && sujets.length > 0 && (
<ul className="space-y-2">
{sujets.map((sujet) => {
const isCurrent = sujet.id === currentSujetId
return (
<li key={sujet.id}>
<Card
variant="interactive"
onClick={() => onSelect(sujet)}
>
<div
aria-current={isCurrent ? 'true' : undefined}
className={`p-4 text-left ${isCurrent ? 'ring-2 ring-expria/40' : ''}`}
>
{sujet.role && (
<div className="mb-1 text-xs font-semibold uppercase tracking-wide text-ink-4">
Rôle : {sujet.role}
</div>
)}
<p className="line-clamp-2 text-sm text-ink-1">{sujet.consigne}</p>
{isCurrent && (
<div className="mt-2 text-xs font-medium text-expria">Sujet actuel</div>
)}
</div>
</Card>
</li>
)
})}
</ul>
)}
</div>
)
}

View file

@ -3,7 +3,7 @@
*
* Transitions couvertes :
* idle choosing-subject (selectTask success, tâche avec catalogue)
* choosing-subject task-selected (selectSujet)
* choosing-subject task-selected (changeSubject + setStep depuis /sujets)
* task-selected correcting (submitText déclenché)
* correcting done (correctEe success)
* correcting task-selected (correctEe error)
@ -17,7 +17,7 @@ import { MemoryRouter } from 'react-router-dom'
import React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useSimulation } from '../useSimulation'
import { SimulationFlowProvider } from '../../state/SimulationFlowProvider'
import { SimulationFlowProvider, useSimulationFlow } from '../../state/SimulationFlowProvider'
import { createSimulation } from '@/entities/production/api'
import { correctEe } from '@/entities/report/api'
import type { Production } from '@/entities/production/types'
@ -142,11 +142,21 @@ describe('useSimulation — submitText', () => {
let resolveCorrect!: (r: Report) => void
mockCorrectEe.mockImplementation(() => new Promise(r => { resolveCorrect = r }))
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
const { result } = renderHook(
() => {
const sim = useSimulation()
const { setStep } = useSimulationFlow()
return { ...sim, setStep }
},
{ wrapper: createWrapper() },
)
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
await waitFor(() => expect(result.current.step).toBe('choosing-subject'))
act(() => result.current.selectSujet(mockSujet))
act(() => {
result.current.changeSubject(mockSujet)
result.current.setStep('task-selected')
})
await waitFor(() => expect(result.current.step).toBe('task-selected'))
act(() => result.current.submitText('Mon texte de production.'))
@ -161,11 +171,21 @@ describe('useSimulation — submitText', () => {
mockCreateSimulation.mockResolvedValue(mockProduction)
mockCorrectEe.mockRejectedValue({ code: 'SIMULATION_NOT_FOUND', message: 'Not found' })
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
const { result } = renderHook(
() => {
const sim = useSimulation()
const { setStep } = useSimulationFlow()
return { ...sim, setStep }
},
{ wrapper: createWrapper() },
)
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
await waitFor(() => expect(result.current.step).toBe('choosing-subject'))
act(() => result.current.selectSujet(mockSujet))
act(() => {
result.current.changeSubject(mockSujet)
result.current.setStep('task-selected')
})
await waitFor(() => expect(result.current.step).toBe('task-selected'))
act(() => result.current.submitText('Mon texte.')
@ -188,11 +208,21 @@ describe('useSimulation — reset', () => {
it('reset depuis task-selected remet step à idle et production à null', async () => {
mockCreateSimulation.mockResolvedValue(mockProduction)
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
const { result } = renderHook(
() => {
const sim = useSimulation()
const { setStep } = useSimulationFlow()
return { ...sim, setStep }
},
{ wrapper: createWrapper() },
)
act(() => result.current.selectTask({ tache: 'EE_T1', mode: 'entrainement' }))
await waitFor(() => expect(result.current.step).toBe('choosing-subject'))
act(() => result.current.selectSujet(mockSujet))
act(() => {
result.current.changeSubject(mockSujet)
result.current.setStep('task-selected')
})
await waitFor(() => expect(result.current.step).toBe('task-selected'))
act(() => result.current.reset())

View file

@ -11,19 +11,11 @@
import { useNavigate } from 'react-router-dom'
import { useSimulationFlow } from '../state/SimulationFlowProvider'
import type { SujetData } from '@/entities/production/types'
export function useSimulation() {
const navigate = useNavigate()
const flow = useSimulationFlow()
/** Sélectionne un sujet puis passe à la saisie (utilisé depuis /sujets). */
function selectSujet(sujet: SujetData): void {
flow.changeSubject(sujet)
flow.setStep('task-selected')
navigate('/simulation/ee')
}
/** Retour à /sujets depuis SimulationForm (bouton "Changer de sujet"). */
function goToSubjectPicker(): void {
flow.setStep('choosing-subject')
@ -42,7 +34,6 @@ export function useSimulation() {
selectTask: flow.selectTask,
submitText: flow.submitText,
changeSubject: flow.changeSubject,
selectSujet,
goToSubjectPicker,
reset: flow.reset,
}