Skip to content

Alumnos

Ruta: /teacher/students · Atajo: g s · Sidebar: Alumnos

La pagina de Alumnos es el CRM del profesor. Permite gestionar el roster completo de estudiantes, ver perfiles detallados con enrollments y sesiones, actualizar notas/mood, y detectar acciones sugeridas automaticamente.

Students page

Vista por defecto con tabla responsive de 7 columnas:

ColumnaContenido
NombreNombre + iconos de contacto (mail, telefono, WhatsApp) + tag badges (max 3 + contador overflow)
EstadoBadge coloreado: trial (amarillo), active (verde), inactive (gris)
CreditosBarra de progreso disponibles/total, color warning cuando quedan pocos
Clases totalesContador centrado
Ultima claseFecha de la ultima sesion completada
Accion sugeridaBadge inline con prioridad (ver algoritmo abajo)

Cabecera: Titulo “Alumnos” + boton “Anadir alumno” (icono UserPlus).

Barra de filtros:

  • Input de busqueda (nombre + email, case-insensitive via ILIKE)
  • 4 botones de estado: Todos, Trial, Activo, Inactivo
  • Select de tag: filtra por tag (visible solo si el profesor tiene tags creados). Envia tagId como query param al API
  • Toggle de vista: Tabla (LayoutList) o Kanban (Columns3)

Paginacion: Botones Anterior/Siguiente, 20 alumnos por pagina.

Tablero de 3 columnas: Trial | Activo | Inactivo.

Cada columna muestra badge de estado + contador. Las tarjetas muestran nombre, iconos de contacto, badge de nivel (A1-C2), email truncado, creditos y fecha de ultima clase.

Drag-and-drop para cambiar estado: Implementado con dnd-kit. El profesor puede arrastrar tarjetas entre columnas para cambiar el estado del alumno (trial → active → inactive). Al soltar, se llama PATCH /teacher/students/:id con el nuevo status. La mutacion es inmediata en la UI con rollback si el servidor rechaza el cambio.

Layout de 3 columnas (responsive a 1 en movil):

Columna izquierda (1/3):

  1. Tarjeta de contacto — nombre, email (mailto), telefono (copiar + WhatsApp), timezone, nivel, badge de estado
  2. Estadisticas — 5 cards: Total clases, Completadas, Canceladas, No-shows, Consistencia %
  3. Selector de mood — 5 emojis clickables que actualizan inmediatamente
  4. Notas — Textarea con auto-guardado al perder foco

Columna derecha (2/3):

  1. Enrollments (Paquetes) — Tarjetas expandibles por enrollment:

    • Nombre del servicio + badge de estado
    • Importe pagado + fecha de enrollment
    • Barra de creditos (color warning si menos del 20% restante)
    • Fecha de expiracion + aviso si expira en menos de 7 dias
    • Sesiones anidadas: programadas primero, completadas despues
    • Detalle de sesion expandible: URL de reunion, template picker, recursos, homework, resumen, notas del profesor, tags
    • Placeholders de sesiones sin programar: “Clase 5/10” con borde discontinuo
    • Batch fetching: Al expandir un enrollment, se hace POST /teacher/sessions/batch-detail con todos los sessionIds del enrollment. Los resultados se cachean individualmente en React Query, evitando N+1 queries al expandir sesiones.
  2. Sesiones huerfanas — Sesiones sin enrollmentId asociado

  3. Contrato y politica — Estado de aceptacion (firmado/desactualizado/sin firmar). Las reglas de cancelacion se renderizan dinamicamente desde la cancellation_policies del profesor (formato rule-based con condiciones, acciones y penalizaciones).

Evaluadas en orden, primera coincidencia gana:

PrioridadCondicionLabel
Alta (rojo)Paquete expira en menos de 7 dias Y tiene creditosCreditos por expirar
Alta (rojo)1-2 creditos restantes Y tiene enrollmentsCreditos bajos
Media (naranja)Activo sin clase futura PERO tiene creditosSin proxima clase
Baja (azul)Ultima clase hace mas de 14 diasInactivo
Media (naranja)Trial sin clase futuraTrial pendiente

Sheet lateral con los campos:

  • Nombre (requerido), Email (requerido), Telefono (opcional)
  • Timezone (dropdown con todas las zonas IANA via Intl.supportedValuesOf('timeZone'), auto-detectado del navegador). El schema Zod valida que sea una zona IANA valida via Intl.DateTimeFormat.
  • Nivel (select: A1-C2, opcional)
  • Objetivos y Notas (textarea, max 1000 chars)
  • Estado (select: active/trial/inactive, default active)
  • Enviar email de bienvenida (checkbox, default true)

El email de bienvenida incluye magic link para que el alumno acceda al portal.

Seccion en el perfil del alumno que muestra un timeline de comunicaciones pasadas. Llama al endpoint existente GET /teacher/contact-log?studentId=X. Cada entrada muestra canal (WhatsApp, email, telefono), fecha y nota opcional.

Componente: ContactHistoryTimeline

Seccion en el perfil que visualiza los student_lifecycle_events como timeline vertical. Soporta los 14 tipos de evento, cada uno con icono y color propios:

TipoDescripcion
first_contactPrimer registro del alumno
first_purchasePrimera compra de un servicio
first_sessionPrimera sesion completada
session_completedSesion completada
session_cancelledSesion cancelada
session_no_showNo-show registrado
enrollment_startedEnrollment creado
enrollment_expiredEnrollment expirado
churn_detectedChurn detectado por el worker
gap_detectedBrecha de inactividad detectada
streak_achievedRacha de sesiones conseguida
reactivatedAlumno reactivado
level_changedCambio de nivel CEFR
mood_updatedActualizacion de mood

API: GET /teacher/students/:id/lifecycle Componente: LifecycleTimeline

Los campos preferredStartTime, preferredEndTime y preferredDays del alumno son editables desde el formulario de edicion del perfil. Se guardan automaticamente al perder foco (auto-save on blur).

Los alumnos se pueden etiquetar con los mismos tags del sistema global (tabla tags). La junction table student_tags conecta alumnos con sus etiquetas.

En la lista:

  • Componente StudentTagBadges muestra hasta 3 badges de color bajo el email del alumno
  • Si hay mas de 3 tags, se muestra un contador +N adicional
  • El filtro por tag (select dropdown) aparece en la barra de filtros cuando el profesor tiene tags. Envia tagId al query param GET /teacher/students?tagId=...

Gestion desde el perfil:

  • TagMultiSelect esta disponible en el formulario de edicion del perfil del alumno para asignar o quitar tags
  • Al guardar, el servicio hace DELETE + INSERT en batch (replace completo via PATCH /teacher/students/:id con tagIds: string[])

API y base de datos:

  • Tabla: student_tags (studentId, tagId). UNIQUE por par (studentId, tagId). CASCADE delete en ambas FKs
  • GET /teacher/students acepta tagId como query param — el backend hace subquery para obtener studentIds con ese tag
  • POST /teacher/students acepta tagIds opcional — inserta en student_tags tras crear el alumno
  • PATCH /teacher/students/:id acepta tagIds opcional — hace DELETE + INSERT batch para reemplazar las etiquetas
  • El perfil del alumno (GET /teacher/students/:id) devuelve tagIds: string[] reconstruido desde la junction table

Patron: Identico al de template_tags, resource_tags, session_tags, service_tags.

Cuando el alumno tiene driveFolderId, aparece un boton “Ver carpeta Drive” en la tarjeta de contacto del perfil. El boton abre la carpeta en Google Drive en una nueva pestana.

Multi-select con checkboxes en las filas de la tabla de alumnos. Al seleccionar uno o mas alumnos aparece una barra flotante con tres acciones:

  • Activar — cambia estado a active
  • Desactivar — cambia estado a inactive
  • Eliminar — soft-delete con ConfirmDialog (variante danger)

API: POST /teacher/students/bulk-action (schema: bulkStudentActionSchema)

Boton de descarga en la barra de herramientas de la lista. Genera un CSV con todos los alumnos del profesor (sin paginacion).

API: GET /teacher/students/export

Boton “Ver como alumno” en el perfil. Crea sesion de impersonacion de 1 hora. La cookie del profesor se guarda en pinteach_teacher_session para poder volver. Durante la impersonacion, las acciones de mood/review estan deshabilitadas.

API: POST /teacher/students/:id/impersonate

Boton “Reenviar enlace de acceso” en la tarjeta de contacto del perfil del alumno. Genera un nuevo magic link y lo envia al email del alumno via el sistema de notificaciones. Util cuando el alumno nunca accedio al portal o su enlace anterior ha expirado.

API: POST /teacher/students/:id/resend-magic-link


No hay features pendientes por implementar.


BugDescripcionEstado
lastContact no disponible en la listaEl backend ahora hace JOIN con contact_log para obtener MAX(sent_at)
Timezone del formulario sin validacion IANAEl campo timezone ahora usa un dropdown con todas las zonas IANA + validacion Zod via Intl.DateTimeFormat
Reglas de politica hardcodeadas en UILas reglas de cancelacion se renderizan dinamicamente desde cancellation_policies (rule-based). El backend devuelve cancellationPolicy en el perfil del alumno
N+1 en expansion de sesionesAl expandir un enrollment, se hace batch fetch via POST /teacher/sessions/batch-detail y se cachea en React Query. Las expansiones individuales usan el cache sin queries adicionales

No hay mejoras pendientes.


ArchivoProposito
apps/web/src/routes/teacher/students.lazy.tsxPagina completa (lista + perfil + formularios + tag badges + tag filter)
apps/api/src/routes/teacher/students-mgmt.tsRutas HTTP (7 endpoints)
apps/api/src/routes/teacher/sessions.tsIncluye POST /batch-detail para batch fetching
apps/api/src/services/teacher/student-management-service.tsLogica de negocio (queries complejas + tag junction + cancellation policy)
apps/api/src/services/scheduling/session-service.tsgetTeacherSessionDetailsBatch() para N+1 fix
packages/shared/src/schemas/student.tsSchemas Zod (create, update — incluye tagIds, ianaTimezoneSchema)
packages/db/src/schema/students.tsSchema de tabla (soft-delete)
packages/db/src/schema/student-tags.tsJunction table student_tags (studentId, tagId)
EndpointMetodoRespuesta
/teacher/studentsGETLista paginada con creditos, stats, tagIds[], lastContact. Acepta query params tagId, search, status, page, limit
/teacher/students/exportGETCSV descargable con todos los alumnos
/teacher/students/bulk-actionPOSTAcciones en masa (activate/deactivate/delete)
/teacher/students/:idGETPerfil completo con enrollments, sesiones, legal, tagIds[], cancellationPolicy
/teacher/students/:id/lifecycleGETEventos de ciclo de vida del alumno
/teacher/students/:id/timelineGETTimeline unificado (contactos + sesiones + lifecycle + reviews)
/teacher/studentsPOSTCrear alumno + email bienvenida. Acepta tagIds[] opcional
/teacher/students/:idPATCHActualizar notas, mood, nivel, preferencias de horario, tagIds[]
/teacher/students/:idDELETESoft-delete
/teacher/students/:id/impersonatePOSTCrear sesion de impersonacion
/teacher/students/:id/resend-magic-linkPOSTReenviar magic link al alumno
/teacher/sessions/batch-detailPOSTBatch fetch de detalles de sesiones { sessionIds: string[] } — max 50

El schema Zod ianaTimezoneSchema (en packages/shared/src/schemas/student.ts) valida timezones IANA en runtime:

export const ianaTimezoneSchema = z.string().min(1).refine((tz) => {
try {
Intl.DateTimeFormat(undefined, { timeZone: tz });
return true;
} catch {
return false;
}
}, { message: 'Invalid IANA timezone' });

El frontend usa Intl.supportedValuesOf('timeZone') para poblar el dropdown (600+ zonas), con fallback a COMMON_TIMEZONES (20 zonas) para navegadores antiguos.

Cuando se expande un EnrollmentCard, el componente llama a POST /teacher/sessions/batch-detail con todos los sessionIds del enrollment. Los resultados se cachean individualmente en React Query con queryClient.setQueryData(['teacher', 'session-detail', sessionId], detail). Los TeacherSessionExpandedDetail individuales encuentran los datos en cache sin hacer queries adicionales.

El endpoint acepta un maximo de 50 session IDs por llamada. El SessionService.getTeacherSessionDetailsBatch() usa inArray para una sola query con todos los joins necesarios.

El perfil del alumno (GET /teacher/students/:id) incluye cancellationPolicy con la politica por defecto del profesor (isDefault=true en cancellation_policies). La UI renderiza:

  1. Reglas de cancelacion: Iteracion sobre cancellationRules[], mostrando description (si existe) o las condiciones formateadas + la accion resultante
  2. Reglas de reprogramacion: maxReschedules + minHoursBefore (si existen)
  3. Politica de no-show: Accion (forfeit/partial_penalty) con porcentaje

Las claves i18n incluyen traducciones para los 4 campos de condicion (hours_before_session, sessions_completed, sessions_remaining, reschedule_count) y las 5 acciones (full_refund, partial_penalty, forfeit, block, allow_free).

useQuery({ queryKey: ['teacher-students', search, statusFilter, tagFilter, page], queryFn: ... })
useQuery({ queryKey: ['teacher-student', studentId], queryFn: ... })
useQuery({ queryKey: ['tags'], queryFn: () => api.get('/teacher/tags') }) // para StudentTagBadges + filtro
useQuery({ queryKey: ['teacher', 'session-detail', sessionId], queryFn: ... }) // pre-populated via batch
AccionOptimista
Crear alumnoNo
Actualizar moodNo
Actualizar notasNo (on blur)
Actualizar preferencias de horarioNo (on blur)
Actualizar tags del alumnoNo (PATCH con tagIds[])
Acciones en masaNo
ImpersonarNo
Eliminar alumnoNo

Claves i18n relevantes anadidas (namespace students.profile):

ClaveESEN
conditionField.hours_before_sessionHoras antesHours before
conditionField.sessions_completedSesiones completadasSessions completed
conditionField.sessions_remainingSesiones restantesSessions remaining
conditionField.reschedule_countReprogramacionesReschedules
policyAction.full_refundReembolso totalFull refund
policyAction.partial_penaltyPenalizacion parcialPartial penalty
policyAction.forfeitSin reembolsoNo refund
policyAction.blockBloqueadoBlocked
policyAction.allow_freeSin cargoNo charge
rescheduleRulesReprogramacionReschedule
minHoursBeforeHoras min.Min hours