# US-ACT-008: Navegación entre actividades **Épica:** EAI-002 - Actividades Básicas Hardcodeadas **Sprint:** Mes 1, Semana 4 **Story Points:** 4 SP **Presupuesto:** $1,500 MXN **Prioridad:** Alta (Alcance Inicial) **Estado:** ✅ Completada (Mes 1) --- ## Descripción Como **estudiante**, quiero **navegar fluidamente entre actividades de un módulo** para **completar mi aprendizaje de forma ordenada y seguir mi progreso**. **Contexto del Alcance Inicial:** Sistema de navegación lineal entre actividades. Los estudiantes avanzan secuencialmente, ven su progreso, y pueden marcar módulos como completados. Sin navegación libre (debes completar actividad actual para avanzar). --- ## Criterios de Aceptación - [ ] **CA-01:** Barra de progreso muestra actividades completadas / total - [ ] **CA-02:** Botón "Siguiente" lleva a siguiente actividad - [ ] **CA-03:** Botón "Anterior" lleva a actividad previa (si existe) - [ ] **CA-04:** No se puede avanzar sin completar actividad actual - [ ] **CA-05:** Al completar última actividad, se marca módulo como completado - [ ] **CA-06:** Se muestra mensaje de felicitación al completar módulo - [ ] **CA-07:** Botón "Salir" regresa al dashboard - [ ] **CA-08:** Indicador visual de actividad actual - [ ] **CA-09:** Tooltips muestran título de cada actividad --- ## Especificaciones Técnicas ### Backend **Endpoints:** ``` GET /api/modules/:moduleId/activities - Response: { data: [ { id, title, type, order, isCompleted: boolean, isCurrent: boolean } ], meta: { totalActivities: number, completedActivities: number, progressPercentage: number } } POST /api/modules/:moduleId/complete - Marca módulo como completado - Otorga recompensas de módulo - Response: { moduleCompleted: true, rewards: { xp, coins, badge } } GET /api/activities/:id/next - Response: { nextActivity: { id, title } | null } GET /api/activities/:id/previous - Response: { previousActivity: { id, title } | null } ``` **Lógica de Progreso:** ```typescript class ModulesService { async getModuleProgress(moduleId: string, userId: string) { const activities = await this.activitiesRepository.find({ where: { moduleId }, order: { order: 'ASC' } }) const completedAttempts = await this.attemptsRepository.find({ where: { userId, activityId: In(activities.map(a => a.id)), isCorrect: true } }) const completedActivityIds = new Set(completedAttempts.map(a => a.activityId)) return { totalActivities: activities.length, completedActivities: completedActivityIds.size, progressPercentage: (completedActivityIds.size / activities.length) * 100, activities: activities.map(activity => ({ ...activity, isCompleted: completedActivityIds.has(activity.id) })) } } async completeModule(moduleId: string, userId: string) { // Verificar que todas las actividades estén completadas const progress = await this.getModuleProgress(moduleId, userId) if (progress.progressPercentage < 100) { throw new BadRequestException('Module not fully completed') } // Registrar módulo completado await this.moduleProgressRepository.save({ userId, moduleId, completedAt: new Date() }) // Otorgar recompensas const module = await this.modulesRepository.findOne({ where: { id: moduleId } }) await this.gamificationService.awardXP(userId, module.xpReward) await this.gamificationService.awardCoins(userId, module.coinsReward) // Otorgar insignia si existe if (module.badgeId) { await this.gamificationService.awardBadge(userId, module.badgeId) } return { moduleCompleted: true, rewards: { xp: module.xpReward, coins: module.coinsReward, badge: module.badgeId } } } } ``` ### Frontend **Componente de Navegación:** ```typescript // pages/ModuleActivityPage.tsx import { useParams, useNavigate } from 'react-router-dom' import { useEffect, useState } from 'react' export function ModuleActivityPage() { const { moduleId, activityId } = useParams() const navigate = useNavigate() const [activity, setActivity] = useState(null) const [progress, setProgress] = useState(null) const [nextActivity, setNextActivity] = useState(null) const [previousActivity, setPreviousActivity] = useState(null) useEffect(() => { loadActivity() loadProgress() loadNavigation() }, [activityId]) const handleActivityComplete = async () => { // Recargar progreso await loadProgress() // Si hay siguiente, navegar if (nextActivity) { navigate(`/modules/${moduleId}/activities/${nextActivity.id}`) } else { // Última actividad, completar módulo await completeModule() } } const completeModule = async () => { const result = await modulesService.completeModule(moduleId) // Mostrar modal de felicitación setShowCompletionModal(true) } return (
{activity.title}
Has completado exitosamente el módulo "{module.title}"
{/* Recompensas */}XP Ganado
+{rewards.xp}
ML Coins
+{rewards.coins}
Insignia
Desbloqueada