feat(historique): refonte pixel-perfect avec stats + filtres + tendance 30j (Sprint 4.7)

Inclut le retrait du padding de AppLayout et le wrapper standardisé
(mx-auto w-full max-w-[1100px] px-5 py-6 lg:px-9 lg:py-9) ajouté sur
11 pages (Dashboard, Progression, 9 pages Simulation EE/EO/T1) pour
laisser chaque page gérer son max-width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-26 00:04:12 +03:00
parent d8bae9520c
commit 3ce91aaa7b
20 changed files with 1417 additions and 874 deletions

View file

@ -1,59 +1,65 @@
/**
* SimulationListItem Sprint 3.7.
* Item d'une ligne de la liste /historique réécrit Sprint 4.7 selon maquette.
*
* Carte item de la page /historique. Clic /rapport/:id (RapportPage gère le
* cas `rapport === null` en redirigeant vers /simulation/ee FTD-21).
* Layout flex : Date · Libellé · Badge NCLC · Score · Chevron.
* Couleur du badge NCLC selon seuil (cf. `nclcChipVariant`).
*
* Règle L : tokens Direction H exclusivement.
* Règle H : purement présentationnel aucune logique plan ici.
* Règle L : tokens DA Charcoal exclusivement.
* Règle H : purement présentationnel.
*/
import { ChevronRight } from 'lucide-react'
import { Link } from 'react-router-dom'
import { Badge } from '@/shared/ui/Badge'
import { formatTache } from '@/entities/production/lib'
import { formatRelativeDate } from '@/shared/lib/date'
import type { SimulationListItem as Item } from '@/entities/production/types'
import { formatShortDate, formatTaskLabel, nclcChipVariant } from '../lib/historique'
interface Props {
item: Item
isLast: boolean
}
export function SimulationListItem({ item }: Props) {
const CHIP_BASE =
'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide'
const CHIP_OK = 'bg-success-soft text-success border-success/30'
const CHIP_WARN = 'bg-warning-soft text-warning border-warning/30'
const CHIP_ERR = 'bg-danger-soft text-danger border-danger/30'
const CHIP_NEUTRAL = 'bg-surface text-ink-secondary border-border'
function NclcBadge({ nclc }: { nclc: number }) {
const variant = nclcChipVariant(nclc)
const cls = variant === 'ok' ? CHIP_OK : variant === 'warn' ? CHIP_WARN : CHIP_ERR
return <span className={`${CHIP_BASE} ${cls}`}>NCLC {nclc}</span>
}
export function SimulationListItem({ item, isLast }: Props) {
const hasScore = item.score !== null && item.nclc !== null
const isExam = item.mode === 'examen'
const borderClass = isLast ? '' : 'border-b border-border'
return (
<Link
to={`/rapport/${item.id}`}
className="block rounded-lg border border-border bg-surface p-4 shadow-card transition-colors duration-150 hover:border-brand hover:bg-surface-hover focus-visible:outline-none focus-visible:shadow-focus"
className={`flex items-center gap-[14px] px-4 py-[14px] transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:shadow-focus ${borderClass}`}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold text-ink-primary">
{formatTache(item.tache)}
</span>
{isExam && <Badge variant="nclc">Examen</Badge>}
{!hasScore && <Badge variant="neutral">En cours</Badge>}
</div>
<p className="text-xs text-ink-secondary">{formatRelativeDate(item.created_at)}</p>
</div>
<span className="w-[68px] shrink-0 text-[11.5px] tabular-nums text-ink-tertiary">
{formatShortDate(item.created_at)}
</span>
{hasScore ? (
<div className="shrink-0 text-right">
<p className="tabular-nums text-ink-primary">
<span className="text-xl font-bold">{item.score}</span>
<span className="text-sm font-medium text-ink-secondary">/20</span>
</p>
<p className="text-xs text-ink-secondary tabular-nums">
NCLC {item.nclc}
{item.nclc_cible ? ` / cible ${item.nclc_cible}` : ''}
</p>
</div>
) : (
<div className="shrink-0 text-right text-xs text-ink-secondary">Score à venir</div>
)}
</div>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-ink-primary">
{formatTaskLabel(item)}
</span>
{hasScore && item.nclc !== null ? (
<NclcBadge nclc={item.nclc} />
) : (
<span className={`${CHIP_BASE} ${CHIP_NEUTRAL}`}>En cours</span>
)}
<span className="min-w-[56px] text-right text-[16px] font-semibold tracking-[-0.02em] tabular-nums text-ink-primary">
{hasScore ? `${item.score}/20` : '—/20'}
</span>
<ChevronRight className="size-[14px] shrink-0 text-ink-tertiary" aria-hidden="true" />
</Link>
)
}