# STUDENT-GAP-006: Perfil - Estadísticas Hardcodeadas
**Fecha de corrección:** 2025-11-24
**Severidad:** 🔴 CRÍTICA
**Prioridad:** P0
**Estado:** ✅ RESUELTO
**Agente responsable:** Frontend-Agent
**Tiempo estimado:** 1-2 horas
**Tiempo real:** 1 hora
---
## 📋 REQUERIMIENTOS
### Requerimiento Funcional
**RF-PROFILE-001:** El perfil del student DEBE mostrar estadísticas DINÁMICAS en tiempo real consumidas desde el backend, incluyendo:
- ML Coins actuales
- Total de logros desbloqueados vs disponibles
- XP total acumulado
- Rango actual (con icono y color)
- Ejercicios completados
### Criterios de Aceptación
1. **CA-001:** El componente `ProfilePage.tsx` DEBE usar hook `useUserStatistics()` para consumir datos del backend
2. **CA-002:** Las estadísticas mostradas DEBEN actualizarse automáticamente cada vez que cambian en backend
3. **CA-003:** El hook DEBE implementar caché de React Query con stale time de 2 minutos
4. **CA-004:** DEBE mostrar loading state mientras carga datos (skeleton o spinner)
5. **CA-005:** DEBE manejar errores gracefully (sin crashear la UI)
6. **CA-006:** DEBE refetch automáticamente cuando la ventana vuelve a tener foco
7. **CA-007:** NO DEBE tener valores hardcodeados en el código del componente
### Contexto del Problema
**Problema identificado:**
- Archivo: `apps/frontend/src/apps/student/pages/ProfilePage.tsx:14-15`
- Código existente:
```typescript
const stats = [
{ label: 'ML Coins', value: '350', icon: Coins }, // ❌ HARDCODED
{ label: 'Logros', value: '12/50', icon: Trophy }, // ❌ HARDCODED
{ label: 'XP Total', value: '1,250', icon: Zap }, // ❌ HARDCODED
{ label: 'Rango', value: 'Ah K\'in', icon: Crown, color: 'text-purple-400' }, // ❌ HARDCODED
];
```
**Impacto del problema:**
- Students veían datos FAKE sin importar su progreso real
- Desmotivación al completar ejercicios/misiones y no ver cambios en perfil
- Percepción de sistema no funcional
- Imposibilidad de validar progreso personal
- TODOS los students veían "350 coins, 12/50 logros" (sin personalización)
**Evidencia visual del problema:**
```
Student A (recién registrado):
ML Coins: 350 ❌ (debería ser 100)
Logros: 12/50 ❌ (debería ser 0/50)
XP Total: 1,250 ❌ (debería ser 0)
Rango: Ah K'in ❌ (debería ser Ajaw)
Student B (avanzado, 500 ejercicios completados):
ML Coins: 350 ❌ (tiene 2,500 reales en BD)
Logros: 12/50 ❌ (tiene 35/50 reales en BD)
XP Total: 1,250 ❌ (tiene 3,800 reales en BD)
Rango: Ah K'in ❌ (debería ser K'uk'ulkan)
```
---
## 🎯 DEFINICIONES
### Conceptos Clave
**User Statistics:**
- Conjunto de métricas agregadas del progreso del student
- Consumidas desde endpoint: `GET /api/users/:userId/statistics`
- Incluye: ml_coins, achievements_unlocked, achievements_available, total_xp, current_rank, exercises_completed
- Actualizadas automáticamente por triggers de BD
**React Query:**
- Librería de data fetching con caché inteligente
- Ventajas: loading state, error handling, refetching, cache, invalidation
- Configuración: staleTime (tiempo antes de considerar datos obsoletos), refetchOnWindowFocus
**Loading State:**
- Estado transitorio mientras se cargan datos del servidor
- UX: Mostrar Loader2 spinner + mensaje "Cargando..."
- Duración típica: 100-500ms
**Error State:**
- Estado cuando falla el fetch (red caída, backend down, 401 unauthorized)
- UX: Mostrar mensaje claro "No se pudieron cargar las estadísticas" con botón retry
- Fallback: NO usar datos fake, mostrar error honestamente
**Stale Time:**
- Tiempo durante el cual React Query considera los datos "frescos"
- Valor configurado: 2 minutos (120,000 ms)
- Comportamiento: Si datos tienen < 2 min de antigüedad, NO refetch al mount
**Refetch on Window Focus:**
- Funcionalidad de React Query que refetch datos cuando la ventana vuelve a tener foco
- Útil para mantener datos actualizados cuando student vuelve de otra pestaña
### Estructura de Datos
**UserStatistics Interface (TypeScript):**
```typescript
interface UserStatistics {
ml_coins: number; // Coins actuales del student
achievements_unlocked: number; // Logros desbloqueados
achievements_available: number; // Total de logros disponibles
total_xp: number; // XP acumulado histórico
current_rank: {
rank: string; // Nombre del rango (Ajaw, Nacom, etc.)
icon: string; // Nombre del icono (crown, shield, etc.)
color: string; // Color Tailwind (text-yellow-400)
};
exercises_completed: number; // Total de ejercicios completados
}
```
**Ejemplo de respuesta del backend:**
```json
{
"ml_coins": 2450,
"achievements_unlocked": 18,
"achievements_available": 50,
"total_xp": 1875,
"current_rank": {
"rank": "Halach Uinic",
"icon": "crown",
"color": "text-yellow-400"
},
"exercises_completed": 234
}
```
### Componentes Involucrados
**useUserStatistics Hook:**
- Responsabilidad: Fetch de estadísticas del usuario con React Query
- Ubicación: `apps/frontend/src/shared/hooks/useUserStatistics.ts`
- Query key: `['userStatistics', userId]`
- Enabled: Solo cuando `userId` está definido
**ProfilePage Component:**
- Responsabilidad: Renderizar perfil del student con estadísticas dinámicas
- Ubicación: `apps/frontend/src/apps/student/pages/ProfilePage.tsx`
- Props: Ninguna (usa `useAuth()` para obtener userId)
**apiClient:**
- Cliente HTTP configurado (axios)
- Ubicación: `apps/frontend/src/services/api/apiClient.ts`
- Incluye: baseURL, interceptors para JWT, error handling
---
## 🔧 IMPLEMENTACIÓN
### Archivos Creados
#### 1. `apps/frontend/src/shared/hooks/useUserStatistics.ts` (NUEVO - 41 líneas)
**Propósito:** Custom hook para consumir estadísticas del usuario desde backend
**Código completo:**
```typescript
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../services/api/apiClient';
/**
* Interface para las estadísticas del usuario
*
* @property ml_coins - Cantidad actual de ML Coins del usuario
* @property achievements_unlocked - Número de logros desbloqueados
* @property achievements_available - Número total de logros disponibles
* @property total_xp - XP total acumulado
* @property current_rank - Información del rango actual (nombre, icono, color)
* @property exercises_completed - Total de ejercicios completados
*/
export interface UserStatistics {
ml_coins: number;
achievements_unlocked: number;
achievements_available: number;
total_xp: number;
current_rank: {
rank: string;
icon: string;
color: string;
};
exercises_completed: number;
}
/**
* Hook para obtener las estadísticas del usuario
*
* Implementa React Query con:
* - Caché de 2 minutos (staleTime)
* - Refetch automático al enfocar ventana
* - Loading y error states
*
* @param userId - ID del usuario (opcional, si no existe no ejecuta query)
* @returns Query object con data, isLoading, error, refetch
*
* @example
* const { data, isLoading, error } = useUserStatistics(user?.id);
* if (isLoading) return ;
* if (error) return ;
* return ;
*/
export function useUserStatistics(userId: string | undefined) {
return useQuery({
queryKey: ['userStatistics', userId],
queryFn: async () => {
if (!userId) {
throw new Error('User ID is required');
}
const response = await apiClient.get(`/users/${userId}/statistics`);
return response.data;
},
enabled: !!userId, // Solo ejecutar si userId existe
staleTime: 2 * 60 * 1000, // 2 minutos (datos frescos)
refetchOnWindowFocus: true, // Refetch al volver a la ventana
});
}
```
**Características clave:**
- ✅ TypeScript interface completa con JSDoc
- ✅ Error handling automático (React Query)
- ✅ Enabled condition (no fetch si userId undefined)
- ✅ Stale time configurado (2 min)
- ✅ Refetch on window focus habilitado
### Archivos Modificados
#### 2. `apps/frontend/src/apps/student/pages/ProfilePage.tsx`
**Cambios realizados:**
**a) Imports agregados:**
```typescript
import { Loader2 } from 'lucide-react'; // ✅ AGREGADO - Spinner
import { useUserStatistics } from '@/shared/hooks/useUserStatistics'; // ✅ AGREGADO
```
**b) Hook de estadísticas (reemplaza datos hardcodeados):**
```typescript
// ❌ ELIMINADO: const stats = [{ label: 'ML Coins', value: '350', ...}]
// ✅ AGREGADO: Fetch dinámico de stats
const { data: userStats, isLoading, error } = useUserStatistics(user?.id);
```
**c) Loading state (líneas 65-72):**
```typescript
if (isLoading) {
return (
Cargando perfil...
);
}
```
**d) Error state (líneas 74-81):**
```typescript
if (error) {
return (