131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
/**
|
|
* Page dashboard — affichage conditionnel selon le plan utilisateur.
|
|
*
|
|
* Toute logique de permission passe par hasAccess() et canSimulate()
|
|
* (Règles D et H — jamais de if plan === '...').
|
|
*/
|
|
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { Button } from '@/shared/ui/Button'
|
|
import { Badge } from '@/shared/ui/Badge'
|
|
import { hasAccess, canSimulate } from '@/entities/user/lib'
|
|
import type { Plan } from '@/entities/user/types'
|
|
import { useAuth } from '@/features/auth/hooks/useAuth'
|
|
import { usePlan } from '../hooks/usePlan'
|
|
import { PaywallBanner } from '../components/PaywallBanner'
|
|
import { PLAN_QUERY_KEY } from '../hooks/usePlan'
|
|
import { MonProfilPreparation } from '../components/MonProfilPreparation'
|
|
|
|
const PLAN_LABELS: Record<Plan, string> = {
|
|
free: 'Plan Découverte',
|
|
standard: 'Plan Standard',
|
|
premium: 'Plan Premium',
|
|
}
|
|
|
|
function getDisplayName(user: { user_metadata?: { full_name?: string }; email?: string } | null): string {
|
|
const fullName = user?.user_metadata?.full_name
|
|
if (fullName) return fullName.split(' ')[0]
|
|
const email = user?.email
|
|
if (email) return email.split('@')[0]
|
|
return 'vous'
|
|
}
|
|
|
|
function DashboardSkeleton() {
|
|
return (
|
|
<div className="space-y-6" aria-busy="true" aria-label="Chargement du tableau de bord">
|
|
<div className="h-8 w-48 animate-pulse rounded-md bg-canvas-2" />
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="h-24 animate-pulse rounded-lg bg-canvas-2" />
|
|
<div className="h-24 animate-pulse rounded-lg bg-canvas-2" />
|
|
</div>
|
|
<div className="h-9 animate-pulse rounded-md bg-canvas-2" />
|
|
<div className="h-16 animate-pulse rounded-lg bg-canvas-2" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function DashboardPage() {
|
|
const { user } = useAuth()
|
|
const { data, isLoading, isError } = usePlan()
|
|
const queryClient = useQueryClient()
|
|
const navigate = useNavigate()
|
|
|
|
const displayName = getDisplayName(user)
|
|
|
|
return (
|
|
<main className="mx-auto max-w-2xl px-4 py-6">
|
|
{isLoading && <DashboardSkeleton />}
|
|
|
|
{isError && (
|
|
<div className="space-y-3 text-center">
|
|
<p className="text-sm text-danger">
|
|
Impossible de charger votre tableau de bord. Réessayez dans quelques instants.
|
|
</p>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={() => queryClient.refetchQueries({ queryKey: PLAN_QUERY_KEY })}
|
|
>
|
|
Réessayer
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{data && (
|
|
<div className="space-y-6">
|
|
{/* Salutation */}
|
|
<section className="flex flex-wrap items-center gap-3">
|
|
<h1 className="text-2xl font-semibold text-ink-1">
|
|
Bonjour, {displayName}
|
|
</h1>
|
|
<Badge variant="plan" planValue={data.plan}>
|
|
{PLAN_LABELS[data.plan]}
|
|
</Badge>
|
|
</section>
|
|
|
|
{/* Bannière upgrade — plan Free uniquement */}
|
|
{!hasAccess(data.plan, 'dashboard') && <PaywallBanner />}
|
|
|
|
{/* Métriques */}
|
|
<section
|
|
className="grid grid-cols-2 gap-4"
|
|
aria-label="Métriques de préparation"
|
|
>
|
|
<div className="rounded-lg border border-line bg-surface p-4">
|
|
<p className="text-xs text-ink-4">Simulations restantes</p>
|
|
<p className="mt-1 text-2xl font-semibold text-ink-1">
|
|
{data.simulations_remaining === null
|
|
? 'Illimitées'
|
|
: data.simulations_remaining}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-lg border border-line bg-surface p-4">
|
|
<p className="text-xs text-ink-4">Niveau NCLC estimé</p>
|
|
<p className="mt-1 text-2xl font-semibold text-ink-1">—</p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* CTA Nouvelle simulation */}
|
|
<Button
|
|
variant="primary"
|
|
className="w-full"
|
|
disabled={!canSimulate(data.plan, data.simulations_used).allowed}
|
|
onClick={() => navigate('/simulation/ee')}
|
|
>
|
|
Nouvelle simulation
|
|
</Button>
|
|
|
|
{/* Dernières simulations */}
|
|
<section aria-label="Dernières simulations">
|
|
<h2 className="text-base font-semibold text-ink-1">Dernières simulations</h2>
|
|
<p className="mt-2 text-sm text-ink-4">Aucune simulation pour l'instant.</p>
|
|
</section>
|
|
|
|
{/* Mon profil de préparation — Premium uniquement (gate via hasAccess) */}
|
|
<MonProfilPreparation plan={data.plan} />
|
|
</div>
|
|
)}
|
|
</main>
|
|
)
|
|
}
|