/**
* 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 (
)
}