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:

Columna Contenido
Nombre Nombre + iconos de contacto (mail, telefono, WhatsApp) + tag badges (max 3 + contador overflow)
Estado Badge coloreado: trial (amarillo), active (verde), inactive (gris)
Creditos Barra de progreso disponibles/total, color warning cuando quedan pocos
Clases totales Contador centrado
Ultima clase Fecha de la ultima sesion completada
Accion sugerida Badge 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:

Prioridad Condicion Label
Alta (rojo) Paquete expira en menos de 7 dias Y tiene creditos Creditos por expirar
Alta (rojo) 1-2 creditos restantes Y tiene enrollments Creditos bajos
Media (naranja) Activo sin clase futura PERO tiene creditos Sin proxima clase
Baja (azul) Ultima clase hace mas de 14 dias Inactivo
Media (naranja) Trial sin clase futura Trial 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:

Tipo Descripcion
first_contact Primer registro del alumno
first_purchase Primera compra de un servicio
first_session Primera sesion completada
session_completed Sesion completada
session_cancelled Sesion cancelada
session_no_show No-show registrado
enrollment_started Enrollment creado
enrollment_expired Enrollment expirado
churn_detected Churn detectado por el worker
gap_detected Brecha de inactividad detectada
streak_achieved Racha de sesiones conseguida
reactivated Alumno reactivado
level_changed Cambio de nivel CEFR
mood_updated Actualizacion 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.


Bug Descripcion Estado
lastContact no disponible en la lista El backend ahora hace JOIN con contact_log para obtener MAX(sent_at) ✅
Timezone del formulario sin validacion IANA El campo timezone ahora usa un dropdown con todas las zonas IANA + validacion Zod via Intl.DateTimeFormat ✅
Reglas de politica hardcodeadas en UI Las 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 sesiones Al 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.


Archivo Proposito
apps/app/src/routes/teacher/students.lazy.tsx Pagina completa (lista + perfil + formularios + tag badges + tag filter)
apps/api/src/routes/teacher/students-mgmt.ts Rutas HTTP (7 endpoints)
apps/api/src/routes/teacher/sessions.ts Incluye POST /batch-detail para batch fetching
apps/api/src/services/teacher/student-management-service.ts Logica de negocio (queries complejas + tag junction + cancellation policy)
apps/api/src/services/scheduling/session-service.ts getTeacherSessionDetailsBatch() para N+1 fix
packages/shared/src/schemas/student.ts Schemas Zod (create, update — incluye tagIds, ianaTimezoneSchema)
packages/db/src/schema/students.ts Schema de tabla (soft-delete)
packages/db/src/schema/student-tags.ts Junction table student_tags (studentId, tagId)
Endpoint Metodo Respuesta
/teacher/students GET Lista paginada con creditos, stats, tagIds[], lastContact. Acepta query params tagId, search, status, page, limit
/teacher/students/export GET CSV descargable con todos los alumnos
/teacher/students/bulk-action POST Acciones en masa (activate/deactivate/delete)
/teacher/students/:id GET Perfil completo con enrollments, sesiones, legal, tagIds[], cancellationPolicy
/teacher/students/:id/lifecycle GET Eventos de ciclo de vida del alumno
/teacher/students/:id/timeline GET Timeline unificado (contactos + sesiones + lifecycle + reviews)
/teacher/students POST Crear alumno + email bienvenida. Acepta tagIds[] opcional
/teacher/students/:id PATCH Actualizar notas, mood, nivel, preferencias de horario, tagIds[]
/teacher/students/:id DELETE Soft-delete
/teacher/students/:id/impersonate POST Crear sesion de impersonacion
/teacher/students/:id/resend-magic-link POST Reenviar magic link al alumno
/teacher/sessions/batch-detail POST Batch 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
Accion Optimista
Crear alumno No
Actualizar mood No
Actualizar notas No (on blur)
Actualizar preferencias de horario No (on blur)
Actualizar tags del alumno No (PATCH con tagIds[])
Acciones en masa No
Impersonar No
Eliminar alumno No

Claves i18n relevantes anadidas (namespace students.profile):

Clave ES EN
conditionField.hours_before_session Horas antes Hours before
conditionField.sessions_completed Sesiones completadas Sessions completed
conditionField.sessions_remaining Sesiones restantes Sessions remaining
conditionField.reschedule_count Reprogramaciones Reschedules
policyAction.full_refund Reembolso total Full refund
policyAction.partial_penalty Penalizacion parcial Partial penalty
policyAction.forfeit Sin reembolso No refund
policyAction.block Bloqueado Blocked
policyAction.allow_free Sin cargo No charge
rescheduleRules Reprogramacion Reschedule
minHoursBefore Horas min. Min hours