workspace/projects/gamilit/orchestration/analisis/SPEC-FASE-5C-FUSION-ANALYTICS.md
rckrdmrd 608e1e2a2e
Some checks are pending
CI Pipeline / changes (push) Waiting to run
CI Pipeline / core (push) Blocked by required conditions
CI Pipeline / trading-backend (push) Blocked by required conditions
CI Pipeline / trading-data-service (push) Blocked by required conditions
CI Pipeline / trading-frontend (push) Blocked by required conditions
CI Pipeline / erp-core (push) Blocked by required conditions
CI Pipeline / erp-mecanicas (push) Blocked by required conditions
CI Pipeline / gamilit-backend (push) Blocked by required conditions
CI Pipeline / gamilit-frontend (push) Blocked by required conditions
Multi-project update: gamilit, orchestration, trading-platform
Gamilit:
- Backend: Teacher services, assignments, gamification, exercise submissions
- Frontend: Admin/Teacher/Student portals, module 4-5 mechanics, monitoring
- Database: DDL functions, seeds for dev/prod, auth/gamification schemas
- Docs: Architecture, features, guides cleanup and reorganization

Core/Orchestration:
- New workspace directives index
- Documentation directive

Trading-platform:
- Database seeds and inventory updates
- Tech leader validation report

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 07:17:46 -06:00

26 KiB

ESPECIFICACION TECNICA - FASE 5C

Fusion de TeacherAnalytics en TeacherProgressPage (Alto Riesgo)

Fecha: 2025-12-15 Prioridad: MEDIA (ejecutar despues de FASE 5A y 5B) Riesgo: ALTO Tiempo estimado de implementacion: 3-4 horas Prerequisitos: FASE 5A completada


1. OBJETIVO

Fusionar la funcionalidad de TeacherAnalyticsPage dentro de TeacherProgressPage como una nueva pestaña "Engagement", eliminando la redundancia entre ambas paginas y consolidando todas las metricas de rendimiento en un solo lugar.

1.1 Justificacion

Solapamiento_detectado:
  TeacherProgress:
    - Progreso academico por estudiante
    - Stats por modulo
    - Identificacion de estudiantes rezagados
    - Vista por clase

  TeacherAnalytics:
    - Puntuacion promedio (DUPLICADO)
    - Tasa de completitud (DUPLICADO)
    - Tasa de engagement (UNICO)
    - DAU/WAU metrics (UNICO)
    - Session duration (UNICO)
    - Feature usage stats (UNICO)
    - Comparacion con periodo anterior (UNICO)

Funcionalidad_unica_Analytics:
  - Metricas de engagement (DAU, WAU)
  - Duracion promedio de sesion
  - Sesiones por usuario
  - Uso de funcionalidades
  - Comparacion con periodo anterior

2. ARCHIVOS INVOLUCRADOS

2.1 Archivos a MODIFICAR

Archivo Tipo de cambio Complejidad
TeacherProgressPage.tsx Agregar sistema de tabs y tab Engagement ALTA
GamilitSidebar.tsx Remover item analytics BAJA

2.2 Archivos a REUTILIZAR (sin modificar)

Archivo Uso
useAnalytics.ts Hook para obtener datos de engagement
analyticsApi.ts API calls existentes

2.3 Archivos a DEPRECAR (no eliminar)

Archivo Razon
TeacherAnalyticsPage.tsx Wrapper - mantener para rutas legacy
TeacherAnalytics.tsx Componente principal - referencia para migracion

3. CAMBIOS DETALLADOS

3.1 TeacherProgressPage.tsx - REESTRUCTURACION COMPLETA

Ruta: /home/isem/workspace/projects/gamilit/apps/frontend/src/apps/teacher/pages/TeacherProgressPage.tsx

NUEVAS IMPORTACIONES A AGREGAR:

// Agregar al inicio del archivo despues de las importaciones existentes
import { useState, useMemo, useEffect } from 'react';
import { useAnalytics } from '../hooks/useAnalytics';
import { Bar } from 'react-chartjs-2';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend,
} from 'chart.js';
import {
  Activity,
  Clock,
  ArrowUp,
  ArrowDown,
  Users as UsersIcon,
  Download,
} from 'lucide-react';
import { FormField } from '@shared/components/common/FormField';

// Registrar Chart.js
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);

NUEVO ESTADO A AGREGAR:

// Dentro del componente, despues de los estados existentes
const [activeTab, setActiveTab] = useState<'progress' | 'engagement'>('progress');
const [dateRange, setDateRange] = useState({
  start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
  end: new Date().toISOString().split('T')[0],
});

// Query para analytics (solo cuando tab engagement esta activo)
const analyticsQuery = useMemo(
  () =>
    activeTab === 'engagement' && selectedClassroomId !== 'all'
      ? {
          classroom_id: selectedClassroomId,
          start_date: dateRange.start,
          end_date: dateRange.end,
        }
      : undefined,
  [activeTab, selectedClassroomId, dateRange.start, dateRange.end],
);

const engagementQuery = useMemo(
  () =>
    activeTab === 'engagement' && selectedClassroomId !== 'all'
      ? {
          classroom_id: selectedClassroomId,
          start_date: dateRange.start,
          end_date: dateRange.end,
          period: 'daily' as const,
        }
      : undefined,
  [activeTab, selectedClassroomId, dateRange.start, dateRange.end],
);

// Hook de analytics
const {
  analytics: analyticsData,
  engagement: engagementData,
  loading: analyticsLoading,
  error: analyticsError,
  generateReport,
  refresh: refreshAnalytics,
} = useAnalytics(analyticsQuery, engagementQuery);

FUNCIONES AUXILIARES A AGREGAR:

/**
 * Safely format a number to fixed decimals
 */
const safeFormat = (
  value: number | undefined | null,
  decimals: number = 1,
  suffix: string = '',
  fallback: string = 'N/A',
): string => {
  if (typeof value !== 'number' || isNaN(value)) {
    return fallback;
  }
  return `${value.toFixed(decimals)}${suffix}`;
};

/**
 * Export analytics to CSV
 */
const exportToCSV = async () => {
  if (selectedClassroomId === 'all') {
    alert('Por favor selecciona una clase especifica para exportar');
    return;
  }

  try {
    const report = await generateReport({
      type: 'custom',
      title: `Engagement Report - ${selectedClassroomName}`,
      classroom_id: selectedClassroomId,
      start_date: dateRange.start,
      end_date: dateRange.end,
      format: 'csv',
      include_charts: true,
      include_recommendations: true,
    });

    if (report.status === 'completed' && report.file_url) {
      window.open(report.file_url, '_blank');
    } else {
      alert('El reporte esta siendo generado. Por favor intenta nuevamente en unos momentos.');
    }
  } catch (err) {
    console.error('[TeacherProgressPage] Error exporting CSV:', err);
    alert('Error al generar el reporte. Por favor intenta nuevamente.');
  }
};

// Chart options para engagement
const chartOptions = {
  responsive: true,
  maintainAspectRatio: false,
  plugins: {
    legend: {
      labels: { color: '#e5e7eb' },
    },
  },
  scales: {
    x: {
      ticks: { color: '#9ca3af' },
      grid: { color: 'rgba(156, 163, 175, 0.1)' },
    },
    y: {
      ticks: { color: '#9ca3af' },
      grid: { color: 'rgba(156, 163, 175, 0.1)' },
    },
  },
};

ESTRUCTURA JSX - TAB SWITCHER (agregar despues del selector de clase):

{/* Tab Switcher - Agregar despues del selector de clase */}
{!loading && !error && (
  <div className="flex gap-2">
    <button
      onClick={() => setActiveTab('progress')}
      className={`rounded-lg px-4 py-2 font-semibold transition-colors ${
        activeTab === 'progress'
          ? 'bg-detective-orange text-white'
          : 'bg-detective-bg-secondary text-detective-text hover:bg-opacity-80'
      }`}
    >
      <BarChart3 className="mr-2 inline h-5 w-5" />
      Progreso
    </button>
    <button
      onClick={() => setActiveTab('engagement')}
      className={`rounded-lg px-4 py-2 font-semibold transition-colors ${
        activeTab === 'engagement'
          ? 'bg-detective-orange text-white'
          : 'bg-detective-bg-secondary text-detective-text hover:bg-opacity-80'
      }`}
    >
      <Activity className="mr-2 inline h-5 w-5" />
      Engagement
    </button>
  </div>
)}

CONTENIDO TAB ENGAGEMENT (agregar como seccion condicional):

{/* Tab Engagement Content */}
{!loading && !error && activeTab === 'engagement' && (
  <div className="space-y-6">
    {/* Date Range Filter */}
    <DetectiveCard hoverable={false}>
      <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
        <FormField
          label="Fecha Inicio"
          name="startDate"
          type="date"
          value={dateRange.start}
          onChange={(e) => setDateRange({ ...dateRange, start: e.target.value })}
        />
        <FormField
          label="Fecha Fin"
          name="endDate"
          type="date"
          value={dateRange.end}
          onChange={(e) => setDateRange({ ...dateRange, end: e.target.value })}
        />
        <div className="flex items-end gap-2">
          <DetectiveButton
            variant="primary"
            onClick={exportToCSV}
            disabled={selectedClassroomId === 'all'}
          >
            <Download className="h-4 w-4" />
            Exportar CSV
          </DetectiveButton>
          <DetectiveButton variant="secondary" onClick={refreshAnalytics}>
            <RefreshCw className="h-4 w-4" />
          </DetectiveButton>
        </div>
      </div>
    </DetectiveCard>

    {/* Loading state for analytics */}
    {analyticsLoading && (
      <div className="flex flex-col items-center justify-center py-12">
        <Loader2 className="mb-4 h-12 w-12 animate-spin text-detective-orange" />
        <p className="text-detective-text-secondary">Cargando metricas de engagement...</p>
      </div>
    )}

    {/* Error state for analytics */}
    {analyticsError && !analyticsLoading && (
      <DetectiveCard variant="danger">
        <div className="flex items-start gap-4">
          <AlertCircle className="h-8 w-8 flex-shrink-0 text-red-500" />
          <div className="flex-1">
            <h3 className="mb-2 text-lg font-bold text-detective-text">
              Error al cargar engagement
            </h3>
            <p className="mb-4 text-detective-text-secondary">
              {analyticsError.message}
            </p>
            <DetectiveButton onClick={refreshAnalytics} variant="primary">
              <RefreshCw className="h-4 w-4" />
              Reintentar
            </DetectiveButton>
          </div>
        </div>
      </DetectiveCard>
    )}

    {/* Select class prompt for engagement */}
    {selectedClassroomId === 'all' && !analyticsLoading && (
      <DetectiveCard hoverable={false}>
        <div className="py-12 text-center">
          <Activity className="mx-auto mb-4 h-16 w-16 text-detective-text-secondary" />
          <h3 className="mb-2 text-lg font-semibold text-detective-text">
            Selecciona una clase
          </h3>
          <p className="text-detective-text-secondary">
            Las metricas de engagement requieren seleccionar una clase especifica
          </p>
        </div>
      </DetectiveCard>
    )}

    {/* Engagement Metrics - Main Cards */}
    {engagementData && selectedClassroomId !== 'all' && !analyticsLoading && (
      <>
        <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
          <DetectiveCard hoverable={false}>
            <div className="flex items-center gap-3">
              <div className="rounded-lg bg-blue-500/20 p-3">
                <UsersIcon className="h-8 w-8 text-blue-500" />
              </div>
              <div>
                <p className="text-sm text-detective-text-secondary">Usuarios Activos Diarios</p>
                <p className="text-3xl font-bold text-detective-text">{engagementData.dau}</p>
              </div>
            </div>
          </DetectiveCard>

          <DetectiveCard hoverable={false}>
            <div className="flex items-center gap-3">
              <div className="rounded-lg bg-green-500/20 p-3">
                <Activity className="h-8 w-8 text-green-500" />
              </div>
              <div>
                <p className="text-sm text-detective-text-secondary">Usuarios Activos Semanales</p>
                <p className="text-3xl font-bold text-detective-text">{engagementData.wau}</p>
              </div>
            </div>
          </DetectiveCard>

          <DetectiveCard hoverable={false}>
            <div className="flex items-center gap-3">
              <div className="rounded-lg bg-orange-500/20 p-3">
                <Clock className="h-8 w-8 text-orange-500" />
              </div>
              <div>
                <p className="text-sm text-detective-text-secondary">Duracion Promedio (min)</p>
                <p className="text-3xl font-bold text-detective-text">
                  {safeFormat(engagementData?.session_duration_avg, 0, '', '0')}
                </p>
              </div>
            </div>
          </DetectiveCard>

          <DetectiveCard hoverable={false}>
            <div className="flex items-center gap-3">
              <div className="rounded-lg bg-purple-500/20 p-3">
                <TrendingUp className="h-8 w-8 text-purple-500" />
              </div>
              <div>
                <p className="text-sm text-detective-text-secondary">Sesiones por Usuario</p>
                <p className="text-3xl font-bold text-detective-text">
                  {safeFormat(engagementData?.sessions_per_user, 1, '', '0.0')}
                </p>
              </div>
            </div>
          </DetectiveCard>
        </div>

        {/* Comparison with Previous Period */}
        <DetectiveCard hoverable={false}>
          <h3 className="mb-4 text-lg font-bold text-detective-text">
            Comparacion con Periodo Anterior
          </h3>
          <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
            <div className="flex items-center gap-3">
              {(engagementData?.comparison_previous_period?.dau_change ?? 0) >= 0 ? (
                <ArrowUp className="h-6 w-6 text-green-500" />
              ) : (
                <ArrowDown className="h-6 w-6 text-red-500" />
              )}
              <div>
                <p className="text-sm text-detective-text-secondary">Cambio en DAU</p>
                <p
                  className={`text-2xl font-bold ${
                    (engagementData?.comparison_previous_period?.dau_change ?? 0) >= 0
                      ? 'text-green-500'
                      : 'text-red-500'
                  }`}
                >
                  {(engagementData?.comparison_previous_period?.dau_change ?? 0) >= 0 ? '+' : ''}
                  {safeFormat(
                    engagementData?.comparison_previous_period?.dau_change,
                    1,
                    '%',
                    '0.0%',
                  )}
                </p>
              </div>
            </div>

            <div className="flex items-center gap-3">
              {(engagementData?.comparison_previous_period?.wau_change ?? 0) >= 0 ? (
                <ArrowUp className="h-6 w-6 text-green-500" />
              ) : (
                <ArrowDown className="h-6 w-6 text-red-500" />
              )}
              <div>
                <p className="text-sm text-detective-text-secondary">Cambio en WAU</p>
                <p
                  className={`text-2xl font-bold ${
                    (engagementData?.comparison_previous_period?.wau_change ?? 0) >= 0
                      ? 'text-green-500'
                      : 'text-red-500'
                  }`}
                >
                  {(engagementData?.comparison_previous_period?.wau_change ?? 0) >= 0 ? '+' : ''}
                  {safeFormat(
                    engagementData?.comparison_previous_period?.wau_change,
                    1,
                    '%',
                    '0.0%',
                  )}
                </p>
              </div>
            </div>

            <div className="flex items-center gap-3">
              {(engagementData?.comparison_previous_period?.engagement_change ?? 0) >= 0 ? (
                <ArrowUp className="h-6 w-6 text-green-500" />
              ) : (
                <ArrowDown className="h-6 w-6 text-red-500" />
              )}
              <div>
                <p className="text-sm text-detective-text-secondary">Cambio en Engagement</p>
                <p
                  className={`text-2xl font-bold ${
                    (engagementData?.comparison_previous_period?.engagement_change ?? 0) >= 0
                      ? 'text-green-500'
                      : 'text-red-500'
                  }`}
                >
                  {(engagementData?.comparison_previous_period?.engagement_change ?? 0) >= 0
                    ? '+'
                    : ''}
                  {safeFormat(
                    engagementData?.comparison_previous_period?.engagement_change,
                    1,
                    '%',
                    '0.0%',
                  )}
                </p>
              </div>
            </div>
          </div>
        </DetectiveCard>

        {/* Feature Usage Table */}
        {engagementData?.feature_usage && engagementData.feature_usage.length > 0 && (
          <DetectiveCard hoverable={false}>
            <h3 className="mb-4 text-lg font-bold text-detective-text">
              Uso de Funcionalidades
            </h3>
            <div className="overflow-x-auto">
              <table className="w-full">
                <thead>
                  <tr className="border-b border-detective-border">
                    <th className="px-4 py-3 text-left text-sm font-semibold text-detective-text-secondary">
                      Funcionalidad
                    </th>
                    <th className="px-4 py-3 text-left text-sm font-semibold text-detective-text-secondary">
                      Usos Totales
                    </th>
                    <th className="px-4 py-3 text-left text-sm font-semibold text-detective-text-secondary">
                      Usuarios Unicos
                    </th>
                  </tr>
                </thead>
                <tbody>
                  {engagementData.feature_usage
                    .filter(
                      (feature) =>
                        feature &&
                        typeof feature.feature_name === 'string' &&
                        typeof feature.usage_count === 'number',
                    )
                    .map((feature, index) => (
                      <tr
                        key={feature.feature_name || index}
                        className="border-b border-detective-border transition-colors hover:bg-detective-bg-secondary"
                      >
                        <td className="px-4 py-3 font-medium text-detective-text">
                          {feature.feature_name}
                        </td>
                        <td className="px-4 py-3 text-detective-text">
                          {feature.usage_count.toLocaleString()}
                        </td>
                        <td className="px-4 py-3 text-detective-text">
                          {feature.unique_users ?? 0}
                        </td>
                      </tr>
                    ))}
                </tbody>
              </table>
            </div>
          </DetectiveCard>
        )}
      </>
    )}
  </div>
)}

{/* Tab Progress Content - Envolver el contenido existente */}
{!loading && !error && activeTab === 'progress' && (
  <>
    {/* Todo el contenido existente de progreso va aqui */}
    {/* ClassProgressDashboard, Tips card, etc. */}
  </>
)}

3.2 GamilitSidebar.tsx - REMOVER ITEM ANALYTICS

Ruta: /home/isem/workspace/projects/gamilit/apps/frontend/src/shared/components/layout/GamilitSidebar.tsx

Accion: Este cambio YA esta incluido en FASE 5A. El item analytics se remueve junto con los otros items en esa fase.

Verificacion: Confirmar que en SPEC-FASE-5A-SIDEBAR.md el item con id analytics esta en la lista de items removidos.


4. ORDEN DE IMPLEMENTACION

Paso_1:
  descripcion: "Agregar imports necesarios a TeacherProgressPage"
  archivo: TeacherProgressPage.tsx
  lineas: "1-30 (imports)"
  riesgo: BAJO

Paso_2:
  descripcion: "Agregar nuevos estados y queries"
  archivo: TeacherProgressPage.tsx
  ubicacion: "Dentro del componente, despues de estados existentes"
  riesgo: MEDIO

Paso_3:
  descripcion: "Agregar funciones auxiliares"
  archivo: TeacherProgressPage.tsx
  ubicacion: "Despues de los estados, antes del return"
  riesgo: BAJO

Paso_4:
  descripcion: "Agregar Tab Switcher en JSX"
  archivo: TeacherProgressPage.tsx
  ubicacion: "Despues del selector de clase"
  riesgo: MEDIO

Paso_5:
  descripcion: "Envolver contenido existente en condicional activeTab === 'progress'"
  archivo: TeacherProgressPage.tsx
  ubicacion: "Todo el contenido actual de progreso"
  riesgo: ALTO

Paso_6:
  descripcion: "Agregar seccion de contenido engagement"
  archivo: TeacherProgressPage.tsx
  ubicacion: "Despues del contenido de progress"
  riesgo: MEDIO

Paso_7:
  descripcion: "Verificar que GamilitSidebar no tiene item analytics (FASE 5A)"
  archivo: GamilitSidebar.tsx
  riesgo: BAJO

5. DEPENDENCIAS Y HOOKS

5.1 Hooks Reutilizados

Hook Origen Uso en TeacherProgressPage
useAnalytics ../hooks/useAnalytics Obtener datos de engagement
useClassrooms YA EXISTE Selector de clase (existente)
useClassroomsStats YA EXISTE Stats agregadas (existente)

5.2 APIs Utilizadas

API Endpoint Metodo
analyticsApi.getClassroomAnalytics /teacher/analytics GET
analyticsApi.getEngagementMetrics /teacher/analytics/engagement GET
analyticsApi.generateReport /teacher/reports POST

5.3 Tipos Requeridos

// Importar desde @apps/teacher/types
import type { ClassroomAnalytics, EngagementMetrics } from '@apps/teacher/types';

// Importar desde @services/api/teacher/analyticsApi
import type {
  GetAnalyticsQueryDto,
  GetEngagementMetricsDto,
  GenerateReportsDto,
  Report,
} from '@services/api/teacher/analyticsApi';

6. PLAN DE PRUEBAS

6.1 Pruebas Manuales

Prueba_1_Tab_Switching:
  precondicion: "Login como teacher, navegar a /teacher/progress"
  pasos:
    - Verificar que aparecen dos tabs: Progreso y Engagement
    - Click en tab Engagement
    - Verificar que se muestra mensaje de seleccionar clase
    - Click en tab Progreso
    - Verificar que se muestra el contenido original
  resultado_esperado: "Tabs funcionan correctamente"

Prueba_2_Engagement_Data:
  precondicion: "Login como teacher, navegar a /teacher/progress"
  pasos:
    - Seleccionar una clase especifica
    - Click en tab Engagement
    - Verificar que se cargan datos (DAU, WAU, etc.)
    - Verificar filtro de fechas
    - Click en Exportar CSV
  resultado_esperado: "Datos de engagement se muestran correctamente"

Prueba_3_Progress_Unchanged:
  precondicion: "Login como teacher, navegar a /teacher/progress"
  pasos:
    - Seleccionar clase
    - Verificar ClassProgressDashboard funciona
    - Verificar tips card visible
    - Verificar estadisticas generales
  resultado_esperado: "Funcionalidad de progreso no afectada"

Prueba_4_Analytics_Route:
  precondicion: "Login como teacher"
  pasos:
    - Navegar directamente a /teacher/analytics
    - Verificar que la pagina carga (ruta mantenida)
  resultado_esperado: "Ruta legacy funcional"

Prueba_5_Sidebar_Analytics_Removed:
  precondicion: "Login como teacher"
  pasos:
    - Verificar que el sidebar NO muestra item "Analiticas"
    - Verificar que SI muestra item "Progreso"
  resultado_esperado: "Sidebar correcto despues de FASE 5A"

6.2 Criterios de Aceptacion

  • Tab switcher visible y funcional
  • Tab Progreso muestra contenido original sin cambios
  • Tab Engagement muestra metricas cuando hay clase seleccionada
  • Tab Engagement muestra mensaje apropiado cuando no hay clase
  • Filtro de fechas funciona
  • Exportar CSV funciona
  • Comparacion con periodo anterior visible
  • Tabla de uso de funcionalidades visible (si hay datos)
  • No hay errores en consola
  • Loading states correctos
  • Error handling correcto

7. ROLLBACK

En caso de necesitar revertir:

  1. Restaurar TeacherProgressPage.tsx:

    • Revertir a version anterior sin tabs
    • Remover imports de analytics
    • Remover estados de engagement
  2. Restaurar GamilitSidebar.tsx:

    • Agregar item analytics de vuelta al array teacherItems
    • Este paso depende de si FASE 5A se revierte tambien
  3. No hay archivos eliminados:

    • TeacherAnalyticsPage.tsx sigue existiendo
    • Ruta /teacher/analytics sigue funcional

8. CONSIDERACIONES TECNICAS

8.1 Performance

Optimizaciones_implementadas:
  - useMemo para queries (evita re-fetches innecesarios)
  - Carga condicional de engagement (solo cuando tab activo)
  - Queries undefined cuando no hay classroom seleccionado

Carga_lazy:
  - Datos de engagement NO se cargan hasta que:
    1. Tab engagement esta activo
    2. Una clase especifica esta seleccionada

8.2 UX Considerations

Flujo_usuario:
  1. Usuario entra a /teacher/progress
  2. Ve contenido de progreso por defecto
  3. Si quiere engagement, click en tab
  4. Debe seleccionar clase primero
  5. Se muestran metricas de engagement

Mensajes_claros:
  - "Selecciona una clase" cuando no hay clase seleccionada
  - Loading spinner mientras carga
  - Error con boton de reintentar

8.3 Compatibilidad

Rutas_legacy:
  - /teacher/analytics: FUNCIONAL (no se elimina)
  - Bookmarks existentes: FUNCIONAN
  - Links externos: FUNCIONAN

Recomendacion_futura:
  - Agregar redirect de /teacher/analytics a /teacher/progress#engagement
  - Esto puede hacerse en una fase posterior

9. ESTRUCTURA FINAL DEL ARCHIVO

TeacherProgressPage.tsx (despues de cambios)
├── Imports (originales + nuevos de engagement)
├── ChartJS.register()
├── safeFormat() helper
├── TeacherProgressPage component
│   ├── Estados originales (selectedClassroomId, showClassroomDropdown)
│   ├── Estados nuevos (activeTab, dateRange)
│   ├── Hooks originales (useClassrooms, useClassroomsStats, useUserGamification)
│   ├── Hooks nuevos (useAnalytics - condicional)
│   ├── useMemo (selectedClassroomName, overallStats, analyticsQuery, engagementQuery)
│   ├── Funciones (handleLogout, exportToCSV)
│   └── JSX
│       ├── Header
│       ├── Loading/Error states
│       ├── Stats cards (cuando all selected)
│       ├── Classroom selector
│       ├── Tab switcher (NUEVO)
│       ├── Tab Progress content (contenido original envuelto)
│       └── Tab Engagement content (NUEVO)

10. NOTAS ADICIONALES

Sobre la ruta /teacher/analytics

La pagina TeacherAnalyticsPage.tsx NO se elimina. Permanece funcional para:

  • Usuarios con bookmarks
  • Links externos
  • Compatibilidad hacia atras

En una fase futura se puede agregar un redirect automatico.

Sobre el orden de ejecucion

Esta fase (5C) DEBE ejecutarse DESPUES de FASE 5A porque:

  1. FASE 5A remueve el item "Analiticas" del sidebar
  2. Sin este cambio, habria dos formas de acceder a la misma funcionalidad

Sobre testing

Se recomienda hacer testing manual exhaustivo porque:

  1. La funcionalidad de progreso NO debe cambiar
  2. La funcionalidad de engagement debe ser identica a TeacherAnalytics
  3. Los tabs deben funcionar correctamente

Estado: LISTO PARA IMPLEMENTAR Prerequisitos: FASE 5A completada Siguiente fase: Validacion final y ejecucion