/** * StatCards — trois cartes synthétiques affichées sur le Dashboard. * * - Simulations restantes (barre de progression pour Free, "Illimitées" ailleurs) * - NCLC estimé (dernière simulation) * - Dernier score (+ delta vs précédent) * * Règle H : aucune logique métier de gating ici — le parent décide du rendu * global via hasAccess. Ce composant ne fait que formater les * valeurs déjà fournies. * Règle L : tokens du design system exclusivement. */ import { Card } from '@/shared/ui/Card' import { formatRelativeDate } from '@/shared/lib/date' import { isEcrit } from '@/entities/production/lib' import type { SimulationListItem } from '@/entities/production/types' import type { Plan } from '@/entities/user/lib' interface StatCardsProps { plan: Plan simulationsUsed: number /** null = illimité (Standard/Premium), number = reste (Free). */ simulationsRemaining: number | null /** Liste des dernières simulations (index 0 = la plus récente). */ recentSimulations: readonly SimulationListItem[] } function formatNclc(n: number): string { return n.toLocaleString('fr-FR', { maximumFractionDigits: 1 }) } function formatScore(value: number): string { return value.toLocaleString('fr-FR', { maximumFractionDigits: 1 }) } function StatShell({ label, children }: { label: string; children: React.ReactNode }) { return (

{label}

{children}
) } function SimulationsRestantesCard({ plan, simulationsUsed, simulationsRemaining, }: { plan: Plan simulationsUsed: number simulationsRemaining: number | null }) { if (simulationsRemaining === null) { return (

Illimitées

{simulationsUsed} effectuée{simulationsUsed > 1 ? 's' : ''}

) } const total = simulationsUsed + simulationsRemaining const pct = total > 0 ? (simulationsUsed / total) * 100 : 0 return (

{simulationsRemaining} /{total}

{plan === 'free' && (

Renouvellement offert à l'upgrade

)} ) } function NclcCard({ lastSim }: { lastSim: SimulationListItem | null }) { if (!lastSim || lastSim.nclc === null) { return (

Démarrez une simulation pour estimer votre niveau.

) } const nclc = lastSim.nclc const inTarget = nclc >= 7 return (

{formatNclc(nclc)}

{inTarget ? 'Dans la cible CLB 7+' : 'Visez la cible CLB 7+'}
) } function DernierScoreCard({ recentSimulations, }: { recentSimulations: readonly SimulationListItem[] }) { const lastWithScore = recentSimulations.find((s) => s.score !== null) ?? null if (!lastWithScore || lastWithScore.score === null) { return (

Aucun score enregistré.

) } // Précédente simulation avec score, pour calculer le delta. const previous = recentSimulations.filter((s) => s.id !== lastWithScore.id && s.score !== null).at(0) ?? null const delta = previous && previous.score !== null ? lastWithScore.score - previous.score : null const type = isEcrit(lastWithScore.tache) ? 'Écrit' : 'Oral' const relative = formatRelativeDate(lastWithScore.created_at) return (

{formatScore(lastWithScore.score)} /20

{type} {relative} {delta !== null && delta !== 0 && ( 0 ? 'font-semibold text-success' : 'font-semibold text-warning'}> {delta > 0 ? '+' : ''} {formatScore(delta)} vs précédent )}
) } export function StatCards({ plan, simulationsUsed, simulationsRemaining, recentSimulations, }: StatCardsProps) { const lastSim = recentSimulations.at(0) ?? null return (
) }