247 lines
9.8 KiB
TypeScript
247 lines
9.8 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { createClient } from '@/lib/supabase/server'
|
|
import {
|
|
Monitor,
|
|
Users,
|
|
TrendingUp,
|
|
CheckCircle,
|
|
Zap,
|
|
ArrowRight,
|
|
FileText,
|
|
} from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { cn } from '@/lib/utils'
|
|
import NotificationBell from '@/components/dashboard/NotificationBell'
|
|
|
|
function KpiCard({
|
|
label,
|
|
value,
|
|
badge,
|
|
badgePositive,
|
|
icon: Icon,
|
|
}: {
|
|
label: string
|
|
value: string
|
|
badge: string
|
|
badgePositive: boolean
|
|
icon: React.ElementType
|
|
}) {
|
|
return (
|
|
<div className="card p-4 md:p-5 flex-1">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-sm text-text-secondary">{label}</span>
|
|
<div className="text-text-muted"><Icon size={18} /></div>
|
|
</div>
|
|
<div className="flex items-end gap-2">
|
|
<span className="text-3xl font-bold text-text-primary">{value}</span>
|
|
<span className={cn(
|
|
'text-xs font-medium px-2 py-0.5 rounded-full mb-1',
|
|
badgePositive ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'
|
|
)}>
|
|
{badge}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const statusConfig = {
|
|
active: { label: 'ACTIVE NOW', color: 'bg-green-500/10 text-green-400 border-green-500/20' },
|
|
scheduled: { label: 'PLANIFIÉE', color: 'bg-amber-500/10 text-amber-400 border-amber-500/20' },
|
|
completed: { label: 'TERMINÉE', color: 'bg-gray-500/10 text-gray-400 border-gray-500/20' },
|
|
} as const
|
|
|
|
function SessionCard({ session }: { session: any }) {
|
|
const statusKey: keyof typeof statusConfig = session.status
|
|
const status = statusConfig[statusKey] ?? statusConfig.completed
|
|
|
|
return (
|
|
<div className="card p-3 md:p-4 flex items-center gap-3 md:gap-4 hover:border-border-light transition-colors">
|
|
<div className="hidden sm:block w-16 h-14 bg-background-elevated rounded-lg flex-shrink-0 overflow-hidden">
|
|
<div className="w-full h-full bg-gradient-to-br from-primary/30 to-blue-700/20 flex items-center justify-center">
|
|
<FileText size={20} className="text-primary/60" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h3 className="font-semibold text-text-primary truncate">{session.title}</h3>
|
|
<span className={cn('text-xs px-2 py-0.5 rounded-full border font-medium flex-shrink-0', status.color)}>
|
|
{status.label}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-text-muted mb-2">Code: #{session.shortCode} · {session.createdAt}</p>
|
|
<div className="flex items-center gap-4 text-xs text-text-secondary">
|
|
<span className="flex items-center gap-1"><Users size={12} />{session.participants} Participants</span>
|
|
{session.status === 'active' && (
|
|
<div className="flex items-center gap-1.5 flex-1">
|
|
<div className="flex-1 h-1.5 bg-border rounded-full overflow-hidden">
|
|
<div className="h-full bg-primary rounded-full" style={{ width: `${session.completion}%` }} />
|
|
</div>
|
|
<span>{session.completion}% Complete</span>
|
|
</div>
|
|
)}
|
|
{session.status === 'completed' && session.avgScore && (
|
|
<span className="flex items-center gap-1 text-green-400">
|
|
<TrendingUp size={12} />{session.avgScore}% Avg Score
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2 flex-shrink-0">
|
|
{session.status === 'active' && (
|
|
<>
|
|
<Link href={`/dashboard/sessions/${session.id}/live`} className="btn-primary text-xs px-3 py-1.5">
|
|
<Monitor size={12} />Monitor
|
|
</Link>
|
|
<button className="btn-secondary text-xs px-3 py-1.5">Détails</button>
|
|
</>
|
|
)}
|
|
{session.status === 'completed' && (
|
|
<Link href={`/dashboard/sessions/${session.id}/live`} className="btn-secondary text-xs px-3 py-1.5">
|
|
Voir Rapport
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default async function DashboardPage() {
|
|
const supabase = await createClient()
|
|
const db = supabase as any
|
|
|
|
const { data: { user } } = await supabase.auth.getUser()
|
|
|
|
const { data: profile } = await db
|
|
.from('profiles')
|
|
.select('username')
|
|
.eq('id', user!.id)
|
|
.single()
|
|
|
|
const { data: sessions } = await db
|
|
.from('sessions')
|
|
.select('id, short_code, is_active, created_at, school_name, class_name, total_participants, quiz:quizzes(title)')
|
|
.eq('trainer_id', user!.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(5)
|
|
|
|
const { count: totalSessions } = await db
|
|
.from('sessions')
|
|
.select('id', { count: 'exact', head: true })
|
|
.eq('trainer_id', user!.id)
|
|
|
|
const activeSessionIds = (sessions ?? [])
|
|
.filter((s: any) => s.is_active)
|
|
.map((s: any) => s.id)
|
|
|
|
const { count: totalParticipants } = activeSessionIds.length > 0
|
|
? await db
|
|
.from('student_participations')
|
|
.select('id', { count: 'exact', head: true })
|
|
.in('session_id', activeSessionIds)
|
|
.eq('status', 'in_progress')
|
|
: { count: 0 }
|
|
|
|
const displayName = profile?.username ?? user?.email?.split('@')[0] ?? 'Formateur'
|
|
|
|
const mappedSessions = (sessions ?? []).map((s: any) => ({
|
|
id: s.id,
|
|
title: s.quiz?.title ?? 'Quiz sans titre',
|
|
shortCode: s.short_code,
|
|
createdAt: s.is_active
|
|
? `il y a ${Math.max(0, Math.floor((Date.now() - new Date(s.created_at).getTime()) / 3600000))}h`
|
|
: 'Terminé',
|
|
participants: s.total_participants,
|
|
avgTime: '15m',
|
|
completion: 85,
|
|
status: s.is_active ? 'active' as const : 'completed' as const,
|
|
}))
|
|
|
|
return (
|
|
<div className="p-4 md:p-8">
|
|
<div className="flex items-start justify-between mb-6 md:mb-8">
|
|
<div>
|
|
<h1 className="text-2xl md:text-3xl font-bold text-text-primary mb-1">Dashboard Overview</h1>
|
|
<p className="text-text-secondary text-sm md:text-base">Bienvenue, {displayName}. Voici ce qui se passe aujourd'hui.</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<NotificationBell />
|
|
<div className="w-9 h-9 bg-primary rounded-full flex items-center justify-center text-white font-semibold text-sm">
|
|
{displayName.slice(0, 2).toUpperCase()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4 mb-6 md:mb-8">
|
|
<KpiCard label="Total Sessions" value={String(totalSessions ?? 0)} badge="+2 cette semaine" badgePositive icon={Monitor} />
|
|
<KpiCard label="Étudiants en cours" value={String(totalParticipants ?? 0)} badge="sessions actives" badgePositive icon={Users} />
|
|
<KpiCard label="Score Moyen" value="78%" badge="+5%" badgePositive icon={TrendingUp} />
|
|
<KpiCard label="Taux de Complétion" value="92%" badge="-1%" badgePositive={false} icon={CheckCircle} />
|
|
</div>
|
|
|
|
<div className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-text-primary">Sessions Actives</h2>
|
|
<Link href="/dashboard/reports" className="text-sm text-primary hover:text-primary-light transition-colors flex items-center gap-1">
|
|
Voir tout <ArrowRight size={14} />
|
|
</Link>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{mappedSessions.length > 0 ? (
|
|
mappedSessions.map((session: any) => <SessionCard key={session.id} session={session} />)
|
|
) : (
|
|
<div className="card p-8 text-center">
|
|
<Monitor size={32} className="mx-auto text-text-muted mb-3" />
|
|
<p className="text-text-secondary mb-4">Aucune session créée pour l'instant</p>
|
|
<Link href="/dashboard/sessions/create" className="btn-primary inline-flex">
|
|
Créer une session
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
|
|
<div className="card p-6">
|
|
<h3 className="font-semibold text-text-primary mb-4">Notifications Récentes</h3>
|
|
<div className="space-y-4">
|
|
{[
|
|
{ icon: '🔧', title: 'Maintenance Système', desc: 'Prévue samedi à 22h. Sauvegardez votre travail.' },
|
|
{ icon: '✅', title: 'Session finalisée', desc: 'Les rapports sont disponibles pour JS Fundamentals.' },
|
|
{ icon: '👥', title: 'Nouvelles inscriptions', desc: '12 étudiants ont rejoint "React Basics Quiz".' },
|
|
].map((notif, i) => (
|
|
<div key={i} className="flex items-start gap-3">
|
|
<div className="w-8 h-8 bg-background-elevated rounded-lg flex items-center justify-center text-sm flex-shrink-0">
|
|
{notif.icon}
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-text-primary">{notif.title}</p>
|
|
<p className="text-xs text-text-muted">{notif.desc}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card p-6 bg-gradient-to-br from-primary to-blue-700 border-0">
|
|
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center mb-4">
|
|
<Zap size={20} className="text-white" />
|
|
</div>
|
|
<h3 className="font-bold text-white text-lg mb-2">Lancer un Quiz Rapide</h3>
|
|
<p className="text-blue-100 text-sm mb-6">
|
|
Créez instantanément un quiz pour un feedback immédiat de votre classe.
|
|
</p>
|
|
<Link
|
|
href="/dashboard/sessions/create"
|
|
className="inline-flex items-center gap-2 bg-white text-primary font-semibold px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors"
|
|
>
|
|
Démarrer
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|