# US-ADM-007: Vista de Actividad de Aula
**Épica:** EAI-005 (Plataforma de Maestro Básica)
**Sprint:** Mes 1, Semana 3
**Story Points:** 6 SP
**Presupuesto:** $2,400 MXN
**Prioridad:** Alta (Alcance Inicial)
**Estado:** ✅ Completada (Mes 1)
---
## Descripción
Como profesor, quiero ver un resumen de la actividad reciente de mi aula para saber qué estudiantes están activos, qué módulos están usando, y tener una vista rápida del pulso de la clase.
**Contexto del Alcance Inicial:**
Esta vista proporciona un snapshot básico de la actividad del aula en tiempo casi real: estudiantes activos hoy, módulos en progreso, y últimas actividades completadas. NO incluye análisis de patrones, heatmaps de actividad, ni métricas de engagement avanzadas (eso va a US-ANA-005 Tracking de Actividad dentro de EAI-004, y análisis más profundos en EXT-005 Reportes Avanzados).
---
## Criterios de Aceptación
### CA-01: Estudiantes Activos Hoy
- [ ] Card/widget que muestra:
- # de estudiantes activos HOY (que han completado al menos una actividad)
- Lista de avatares de estudiantes activos
- % de estudiantes activos vs total del aula
### CA-02: Módulos en Progreso
- [ ] Lista de módulos que tienen actividad reciente (últimos 7 días)
- [ ] Para cada módulo muestra:
- Nombre del módulo
- # de estudiantes trabajando en él
- Progreso promedio del módulo
### CA-03: Últimas Actividades
- [ ] Feed de las últimas 10 actividades completadas en el aula
- [ ] Para cada actividad muestra:
- Estudiante (avatar + nombre)
- Actividad completada
- Módulo
- Timestamp (relativo: "hace 5 min")
- [ ] Link rápido a perfil del estudiante
### CA-04: Refresh Automático
- [ ] Vista se actualiza automáticamente cada 2 minutos
- [ ] Indicador de última actualización
- [ ] Botón manual de refresh
### CA-05: Integración con Dashboard
- [ ] Esta vista es parte del dashboard del aula (accesible desde US-ANA-001)
- [ ] O puede ser una pestaña/sección dentro del dashboard del aula
---
## Especificaciones Técnicas
### Backend
**Endpoint:**
```typescript
// Actividad del aula
GET /api/teacher/classrooms/{classroomId}/activity-summary
```
**Response:**
```json
{
"classroomId": "uuid",
"timestamp": "2025-11-02T14:30:00Z",
"activeStudentsToday": {
"count": 12,
"total": 25,
"percentage": 48,
"students": [
{
"id": "student-uuid",
"name": "Juan Pérez",
"avatarUrl": "/avatars/student.png"
}
]
},
"modulesInProgress": [
{
"moduleId": "module-uuid",
"moduleName": "Fracciones",
"studentsWorking": 8,
"averageProgress": 65.5
}
],
"recentActivities": [
{
"id": "activity-uuid",
"student": {
"id": "student-uuid",
"name": "Juan Pérez",
"avatarUrl": "/avatars/student.png"
},
"activity": {
"name": "Suma de fracciones",
"moduleName": "Fracciones"
},
"completedAt": "2025-11-02T14:25:00Z"
}
]
}
```
**Controller:**
```typescript
@Get('classrooms/:classroomId/activity-summary')
async getActivitySummary(
@Param('classroomId') classroomId: string,
@CurrentUser() teacher: User
) {
return this.classroomService.getActivitySummary(classroomId, teacher.id);
}
```
**Service:**
```typescript
async getActivitySummary(classroomId: string, teacherId: string) {
await this.validateTeacherAccess(classroomId, teacherId);
const today = startOfDay(new Date());
// Estudiantes activos hoy
const activeStudentsToday = await this.getActiveStudentsToday(
classroomId,
today
);
// Módulos en progreso (con actividad en últimos 7 días)
const modulesInProgress = await this.getModulesInProgress(
classroomId,
subDays(new Date(), 7)
);
// Últimas actividades (últimas 10)
const recentActivities = await this.getRecentActivities(classroomId, 10);
return {
classroomId,
timestamp: new Date().toISOString(),
activeStudentsToday,
modulesInProgress,
recentActivities
};
}
private async getActiveStudentsToday(classroomId: string, today: Date) {
const students = await this.activityLogRepository
.createQueryBuilder('log')
.innerJoinAndSelect('log.student', 'student')
.where('log.classroomId = :classroomId', { classroomId })
.andWhere('log.timestamp >= :today', { today })
.groupBy('student.id')
.select(['student.id', 'student.name', 'student.avatarUrl'])
.getMany();
const totalStudents = await this.getStudentCount(classroomId);
return {
count: students.length,
total: totalStudents,
percentage: totalStudents > 0
? Math.round((students.length / totalStudents) * 100)
: 0,
students: students.map(s => s.student)
};
}
private async getModulesInProgress(classroomId: string, since: Date) {
const modules = await this.activityLogRepository
.createQueryBuilder('log')
.innerJoin('log.module', 'module')
.where('log.classroomId = :classroomId', { classroomId })
.andWhere('log.timestamp >= :since', { since })
.groupBy('module.id')
.select([
'module.id',
'module.name',
'COUNT(DISTINCT log.studentId) as studentsWorking'
])
.getRawMany();
const modulesWithProgress = await Promise.all(
modules.map(async (m) => {
const avgProgress = await this.getModuleAverageProgress(
classroomId,
m.module_id
);
return {
moduleId: m.module_id,
moduleName: m.module_name,
studentsWorking: parseInt(m.studentsWorking),
averageProgress: avgProgress
};
})
);
return modulesWithProgress;
}
private async getRecentActivities(classroomId: string, limit: number) {
const activities = await this.activityLogRepository
.createQueryBuilder('log')
.innerJoinAndSelect('log.student', 'student')
.innerJoinAndSelect('log.activity', 'activity')
.innerJoinAndSelect('log.module', 'module')
.where('log.classroomId = :classroomId', { classroomId })
.andWhere('log.type = :type', { type: 'activity_completed' })
.orderBy('log.timestamp', 'DESC')
.limit(limit)
.getMany();
return activities.map(log => ({
id: log.id,
student: {
id: log.student.id,
name: log.student.name,
avatarUrl: log.student.avatarUrl
},
activity: {
name: log.activity.name,
moduleName: log.module.name
},
completedAt: log.timestamp
}));
}
```
### Frontend
**Ruta:**
```
/teacher/classroom/:classroomId/activity
(Puede ser parte de /teacher/classroom/:classroomId/dashboard)
```
**Componente Principal:**
```typescript
// ClassroomActivityView.tsx
export const ClassroomActivityView = () => {
const { classroomId } = useParams();
const { activityData, isLoading, refetch } = useClassroomActivity(classroomId);
// Auto-refresh cada 2 minutos
useEffect(() => {
const interval = setInterval(() => {
refetch();
}, 120000); // 2 minutos
return () => clearInterval(interval);
}, [refetch]);
if (isLoading) return
{activity.student.name} completó{' '} {activity.activity.name} en{' '} {activity.activity.moduleName}
{formatRelativeTime(activity.completedAt)}