trading-platform-frontend-v2/src/modules/education/components/CourseProgressTracker.tsx
Adrian Flores Cortes cbb6637966 [OQI-002] feat: Add education progress and assessment components
- CourseProgressTracker: Comprehensive progress visualization
- LearningPathVisualizer: Visual roadmap for learning paths
- VideoProgressPlayer: Enhanced video player with bookmarks and controls
- AssessmentSummaryCard: Quiz results analysis and review

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:07:12 -06:00

311 lines
11 KiB
TypeScript

/**
* CourseProgressTracker Component
* Comprehensive progress visualization at the course level
* OQI-002: Modulo Educativo
*/
import React, { useMemo } from 'react';
import {
BookOpen,
CheckCircle,
Circle,
Clock,
Play,
Lock,
Trophy,
Target,
TrendingUp,
Download,
ChevronRight,
Zap,
Calendar,
} from 'lucide-react';
export interface ModuleProgress {
id: string;
title: string;
lessonsTotal: number;
lessonsCompleted: number;
quizScore?: number;
quizPassed?: boolean;
timeSpentMinutes: number;
estimatedMinutes: number;
status: 'locked' | 'available' | 'in_progress' | 'completed';
order: number;
}
export interface CourseProgressData {
courseId: string;
courseTitle: string;
totalLessons: number;
completedLessons: number;
totalQuizzes: number;
passedQuizzes: number;
overallProgress: number;
xpEarned: number;
xpTotal: number;
timeSpentMinutes: number;
estimatedTotalMinutes: number;
startedAt?: string;
lastAccessedAt?: string;
modules: ModuleProgress[];
}
interface CourseProgressTrackerProps {
progress: CourseProgressData;
onModuleClick?: (moduleId: string) => void;
onDownloadReport?: () => void;
compact?: boolean;
}
const CourseProgressTracker: React.FC<CourseProgressTrackerProps> = ({
progress,
onModuleClick,
onDownloadReport,
compact = false,
}) => {
const sortedModules = useMemo(() => {
return [...progress.modules].sort((a, b) => a.order - b.order);
}, [progress.modules]);
const completedModules = progress.modules.filter(m => m.status === 'completed').length;
const totalModules = progress.modules.length;
const formatTime = (minutes: number) => {
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('es-ES', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
};
const getStatusIcon = (status: ModuleProgress['status']) => {
switch (status) {
case 'completed':
return <CheckCircle className="w-5 h-5 text-green-400" />;
case 'in_progress':
return <Play className="w-5 h-5 text-blue-400" />;
case 'locked':
return <Lock className="w-5 h-5 text-gray-500" />;
default:
return <Circle className="w-5 h-5 text-gray-400" />;
}
};
const getStatusBg = (status: ModuleProgress['status']) => {
switch (status) {
case 'completed':
return 'bg-green-500/10 border-green-500/30';
case 'in_progress':
return 'bg-blue-500/10 border-blue-500/30';
case 'locked':
return 'bg-gray-800/50 border-gray-700 opacity-60';
default:
return 'bg-gray-800/50 border-gray-700';
}
};
const estimatedCompletion = useMemo(() => {
const remainingMinutes = progress.estimatedTotalMinutes - progress.timeSpentMinutes;
if (remainingMinutes <= 0) return 'Completable ahora';
const days = Math.ceil(remainingMinutes / 60 / 2); // Assuming 2 hours/day
return `~${days} ${days === 1 ? 'dia' : 'dias'} restantes`;
}, [progress]);
return (
<div className={`bg-gray-800/50 rounded-xl border border-gray-700 ${compact ? 'p-3' : 'p-4'}`}>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Target className="w-5 h-5 text-blue-400" />
</div>
<div>
<h3 className="font-semibold text-white">Progreso del Curso</h3>
<p className="text-xs text-gray-500">{progress.courseTitle}</p>
</div>
</div>
{onDownloadReport && (
<button
onClick={onDownloadReport}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
title="Descargar reporte"
>
<Download className="w-4 h-4" />
</button>
)}
</div>
{/* Overall Progress Bar */}
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-400">Progreso General</span>
<span className="text-lg font-bold text-white">{progress.overallProgress}%</span>
</div>
<div className="h-3 bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-600 to-green-500 rounded-full transition-all duration-500"
style={{ width: `${progress.overallProgress}%` }}
/>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-4 gap-3 mb-6">
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="flex items-center justify-center gap-1 mb-1">
<BookOpen className="w-4 h-4 text-blue-400" />
</div>
<div className="text-lg font-bold text-white">
{progress.completedLessons}/{progress.totalLessons}
</div>
<div className="text-xs text-gray-500">Lecciones</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="flex items-center justify-center gap-1 mb-1">
<Trophy className="w-4 h-4 text-yellow-400" />
</div>
<div className="text-lg font-bold text-white">
{progress.passedQuizzes}/{progress.totalQuizzes}
</div>
<div className="text-xs text-gray-500">Quizzes</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="flex items-center justify-center gap-1 mb-1">
<Clock className="w-4 h-4 text-purple-400" />
</div>
<div className="text-lg font-bold text-white">{formatTime(progress.timeSpentMinutes)}</div>
<div className="text-xs text-gray-500">Tiempo</div>
</div>
<div className="p-3 bg-gray-900/50 rounded-lg text-center">
<div className="flex items-center justify-center gap-1 mb-1">
<Zap className="w-4 h-4 text-green-400" />
</div>
<div className="text-lg font-bold text-white">{progress.xpEarned}</div>
<div className="text-xs text-gray-500">XP</div>
</div>
</div>
{/* Timeline Info */}
{(progress.startedAt || progress.lastAccessedAt) && (
<div className="flex items-center justify-between p-3 bg-gray-900/50 rounded-lg mb-4 text-xs">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-500" />
{progress.startedAt && (
<span className="text-gray-400">
Iniciado: <span className="text-white">{formatDate(progress.startedAt)}</span>
</span>
)}
</div>
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-gray-500" />
<span className="text-gray-400">{estimatedCompletion}</span>
</div>
</div>
)}
{/* Module List */}
<div className="space-y-2">
<div className="text-sm text-gray-400 mb-2">
Modulos ({completedModules}/{totalModules})
</div>
{sortedModules.map((module, index) => {
const moduleProgress =
module.lessonsTotal > 0
? Math.round((module.lessonsCompleted / module.lessonsTotal) * 100)
: 0;
return (
<div
key={module.id}
onClick={() => module.status !== 'locked' && onModuleClick?.(module.id)}
className={`p-3 rounded-lg border transition-all ${getStatusBg(module.status)} ${
module.status !== 'locked' ? 'cursor-pointer hover:border-gray-600' : ''
}`}
>
<div className="flex items-center gap-3">
{/* Status Icon */}
<div className="flex-shrink-0">{getStatusIcon(module.status)}</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span
className={`font-medium truncate ${
module.status === 'locked' ? 'text-gray-500' : 'text-white'
}`}
>
{index + 1}. {module.title}
</span>
{module.status !== 'locked' && (
<ChevronRight className="w-4 h-4 text-gray-500 flex-shrink-0" />
)}
</div>
{/* Progress Bar */}
{module.status !== 'locked' && (
<div className="mt-2">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-gray-500">
{module.lessonsCompleted}/{module.lessonsTotal} lecciones
</span>
<span className="text-gray-400">{moduleProgress}%</span>
</div>
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
module.status === 'completed' ? 'bg-green-500' : 'bg-blue-500'
}`}
style={{ width: `${moduleProgress}%` }}
/>
</div>
</div>
)}
{/* Quiz Badge */}
{module.quizScore !== undefined && (
<div className="flex items-center gap-2 mt-2">
<span
className={`px-2 py-0.5 rounded text-xs ${
module.quizPassed
? 'bg-green-500/20 text-green-400'
: 'bg-red-500/20 text-red-400'
}`}
>
Quiz: {module.quizScore}%
</span>
<span className="text-xs text-gray-500">
{formatTime(module.timeSpentMinutes)} / {formatTime(module.estimatedMinutes)}
</span>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
{/* Completion Message */}
{progress.overallProgress === 100 && (
<div className="mt-4 p-4 bg-gradient-to-r from-green-500/10 to-emerald-500/10 border border-green-500/30 rounded-lg text-center">
<Trophy className="w-8 h-8 text-yellow-400 mx-auto mb-2" />
<p className="text-green-400 font-medium">Curso Completado!</p>
<p className="text-xs text-gray-400 mt-1">Has ganado {progress.xpEarned} XP</p>
</div>
)}
</div>
);
};
export default CourseProgressTracker;