# ESPECIFICACION TECNICA - FASE 5B ## Migracion de Funcionalidades Students a Monitoring (Medio Riesgo) **Fecha:** 2025-12-15 **Prioridad:** MEDIA **Riesgo:** MEDIO **Prerequisito:** FASE 5A completada **Tiempo estimado de implementacion:** 4-6 horas --- ## 1. OBJETIVO Migrar las funcionalidades UNICAS de `TeacherStudents` a `StudentMonitoringPanel` para que el usuario no pierda capacidades al eliminar la pagina de Estudiantes del sidebar. --- ## 2. FUNCIONALIDADES A MIGRAR ### 2.1 Funcionalidades de TeacherStudents que NO existen en Monitoring: | Funcionalidad | En Students | En Monitoring | Accion | |---------------|-------------|---------------|--------| | Filtro por rendimiento (Alto/Medio/Bajo) | SI | NO | MIGRAR | | Vista en tabla sorteable | SI | NO | AGREGAR COMO OPCION | | Ordenamiento multi-campo | SI | NO | MIGRAR | | Estadisticas por rendimiento | SI | NO | MIGRAR | | Vista consolidada todas las aulas | SI | NO | EVALUAR | ### 2.2 Funcionalidades que ya existen en Monitoring: | Funcionalidad | Estado | |---------------|--------| | Busqueda por nombre | OK | | Filtro por estado (Active/Inactive/Offline) | OK | | Vista en tarjetas | OK | | Detalle de estudiante (modal) | OK | | Auto-refresh | OK | | Notificaciones toast | OK | --- ## 3. ARCHIVOS A MODIFICAR ### 3.1 StudentMonitoringPanel.tsx **Ruta:** `/home/isem/workspace/projects/gamilit/apps/frontend/src/apps/teacher/components/monitoring/StudentMonitoringPanel.tsx` #### 3.1.1 CAMBIOS EN INTERFACE StudentFilter **CODIGO ACTUAL (types/index.ts o inline):** ```typescript interface StudentFilter { search?: string; status?: ('active' | 'inactive' | 'offline')[]; } ``` **CODIGO PROPUESTO:** ```typescript interface StudentFilter { search?: string; status?: ('active' | 'inactive' | 'offline' | 'in_exercise')[]; performanceLevel?: ('high' | 'medium' | 'low')[]; } ``` #### 3.1.2 AGREGAR ESTADO PARA VISTA **AGREGAR despues de useState existentes (linea ~20):** ```typescript const [viewMode, setViewMode] = useState<'cards' | 'table'>('cards'); const [sortField, setSortField] = useState<'name' | 'score' | 'completion' | 'activity'>('name'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); ``` #### 3.1.3 AGREGAR FUNCION calculatePerformanceLevel **AGREGAR antes de getStudentStatus (linea ~95):** ```typescript // Calculate performance level based on score const calculatePerformanceLevel = (student: StudentMonitoring): 'high' | 'medium' | 'low' => { const score = student.score_average || 0; if (score >= 80) return 'high'; if (score >= 60) return 'medium'; return 'low'; }; ``` #### 3.1.4 AGREGAR ESTADISTICAS DE RENDIMIENTO **AGREGAR despues de offlineCount (linea ~110):** ```typescript // Performance stats const highPerformanceCount = students.filter((s) => calculatePerformanceLevel(s) === 'high').length; const mediumPerformanceCount = students.filter((s) => calculatePerformanceLevel(s) === 'medium').length; const lowPerformanceCount = students.filter((s) => calculatePerformanceLevel(s) === 'low').length; ``` #### 3.1.5 AGREGAR FILTRO DE RENDIMIENTO **AGREGAR despues del handleStatusFilter (linea ~93):** ```typescript const handlePerformanceFilter = (level: 'high' | 'medium' | 'low') => { setFilters((prev) => { const currentLevels = prev.performanceLevel || []; const newLevels = currentLevels.includes(level) ? currentLevels.filter((l) => l !== level) : [...currentLevels, level]; return { ...prev, performanceLevel: newLevels.length > 0 ? newLevels : undefined, }; }); }; ``` #### 3.1.6 AGREGAR FUNCION DE ORDENAMIENTO **AGREGAR despues de handlePerformanceFilter:** ```typescript const handleSort = (field: typeof sortField) => { if (sortField === field) { setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc')); } else { setSortField(field); setSortDirection('asc'); } }; // Apply sorting to students const sortedStudents = useMemo(() => { return [...students].sort((a, b) => { let aValue: string | number; let bValue: string | number; switch (sortField) { case 'name': aValue = a.full_name.toLowerCase(); bValue = b.full_name.toLowerCase(); break; case 'score': aValue = a.score_average || 0; bValue = b.score_average || 0; break; case 'completion': aValue = a.progress_percentage || 0; bValue = b.progress_percentage || 0; break; case 'activity': aValue = a.last_activity ? new Date(a.last_activity).getTime() : 0; bValue = b.last_activity ? new Date(b.last_activity).getTime() : 0; break; default: return 0; } if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1; if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1; return 0; }); }, [students, sortField, sortDirection]); ``` #### 3.1.7 AGREGAR TARJETAS DE RENDIMIENTO AL GRID DE STATS **MODIFICAR el grid de stats (linea ~146-204) para agregar:** ```typescript {/* Performance Stats - Nueva fila */}
{highPerformanceCount}
{mediumPerformanceCount}
{lowPerformanceCount}
| handleSort('name')} > Nombre {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')} | Estado | handleSort('score')} > Puntuacion {sortField === 'score' && (sortDirection === 'asc' ? '↑' : '↓')} | handleSort('completion')} > Completitud {sortField === 'completion' && (sortDirection === 'asc' ? '↑' : '↓')} | Rendimiento | handleSort('activity')} > Ultima Actividad {sortField === 'activity' && (sortDirection === 'asc' ? '↑' : '↓')} |
|---|---|---|---|---|---|
|
{student.full_name} {student.email} |
{getStudentStatus(student) === 'active' && 'Activo'} {getStudentStatus(student) === 'in_exercise' && 'En ejercicio'} {getStudentStatus(student) === 'inactive' && 'Inactivo'} {getStudentStatus(student) === 'offline' && 'Offline'} | = 80 ? 'text-green-500' : (student.score_average || 0) >= 60 ? 'text-yellow-500' : 'text-red-500' }`}> {(student.score_average || 0).toFixed(1)}% |
= 70 ? 'bg-green-500' :
(student.progress_percentage || 0) >= 50 ? 'bg-yellow-500' :
'bg-red-500'
}`}
style={{ width: `${student.progress_percentage || 0}%` }}
/>
{(student.progress_percentage || 0).toFixed(0)}%
|
{calculatePerformanceLevel(student) === 'high' && 'Alto'} {calculatePerformanceLevel(student) === 'medium' && 'Medio'} {calculatePerformanceLevel(student) === 'low' && 'Bajo'} | {student.last_activity ? new Date(student.last_activity).toLocaleDateString('es-ES') : 'Sin actividad'} |