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.
Que hay
Section titled “Que hay”Vista de lista (tabla)
Section titled “Vista de lista (tabla)”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
tagIdcomo query param al API - Toggle de vista: Tabla (LayoutList) o Kanban (Columns3)
Paginacion: Botones Anterior/Siguiente, 20 alumnos por pagina.
Vista Kanban
Section titled “Vista Kanban”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.
Perfil del alumno
Section titled “Perfil del alumno”Layout de 3 columnas (responsive a 1 en movil):
Columna izquierda (1/3):
- Tarjeta de contacto — nombre, email (mailto), telefono (copiar + WhatsApp), timezone, nivel, badge de estado
- Estadisticas — 5 cards: Total clases, Completadas, Canceladas, No-shows, Consistencia %
- Selector de mood — 5 emojis clickables que actualizan inmediatamente
- Notas — Textarea con auto-guardado al perder foco
Columna derecha (2/3):
-
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-detailcon todos lossessionIdsdel enrollment. Los resultados se cachean individualmente en React Query, evitando N+1 queries al expandir sesiones.
-
Sesiones huerfanas — Sesiones sin enrollmentId asociado
-
Contrato y politica — Estado de aceptacion (firmado/desactualizado/sin firmar). Las reglas de cancelacion se renderizan dinamicamente desde la
cancellation_policiesdel profesor (formato rule-based con condiciones, acciones y penalizaciones).
Acciones sugeridas (algoritmo)
Section titled “Acciones sugeridas (algoritmo)”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 |
Formulario “Anadir alumno”
Section titled “Formulario “Anadir alumno””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 viaIntl.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.
Historial de contacto
Section titled “Historial de contacto”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
Timeline de ciclo de vida
Section titled “Timeline de ciclo de vida”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
Preferencias de horario
Section titled “Preferencias de horario”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).
Sistema de tags de alumnos
Section titled “Sistema de tags de alumnos”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
StudentTagBadgesmuestra hasta 3 badges de color bajo el email del alumno - Si hay mas de 3 tags, se muestra un contador
+Nadicional - El filtro por tag (select dropdown) aparece en la barra de filtros cuando el profesor tiene tags. Envia
tagIdal query paramGET /teacher/students?tagId=...
Gestion desde el perfil:
TagMultiSelectesta 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/:idcontagIds: string[])
API y base de datos:
- Tabla:
student_tags(studentId, tagId). UNIQUE por par(studentId, tagId). CASCADE delete en ambas FKs GET /teacher/studentsaceptatagIdcomo query param — el backend hace subquery para obtenerstudentIdscon ese tagPOST /teacher/studentsaceptatagIdsopcional — inserta enstudent_tagstras crear el alumnoPATCH /teacher/students/:idaceptatagIdsopcional — hace DELETE + INSERT batch para reemplazar las etiquetas- El perfil del alumno (
GET /teacher/students/:id) devuelvetagIds: string[]reconstruido desde la junction table
Patron: Identico al de template_tags, resource_tags, session_tags, service_tags.
Carpeta Drive del alumno
Section titled “Carpeta Drive del alumno”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.
Acciones en masa
Section titled “Acciones en masa”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)
Exportar CSV
Section titled “Exportar CSV”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
Impersonacion
Section titled “Impersonacion”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
Reenviar magic link
Section titled “Reenviar magic link”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
Que falta
Section titled “Que falta”No hay features pendientes por implementar.
Que falla
Section titled “Que falla”| 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 | ✅ |
Que cambiaria
Section titled “Que cambiaria”No hay mejoras pendientes.
Referencia tecnica
Section titled “Referencia tecnica”Archivos clave
Section titled “Archivos clave”| Archivo | Proposito |
|---|---|
apps/web/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 |
Validacion de timezone
Section titled “Validacion de timezone”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.
Batch session detail
Section titled “Batch session detail”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.
Politica de cancelacion dinamica
Section titled “Politica de cancelacion dinamica”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:
- Reglas de cancelacion: Iteracion sobre
cancellationRules[], mostrandodescription(si existe) o las condiciones formateadas + la accion resultante - Reglas de reprogramacion:
maxReschedules+minHoursBefore(si existen) - 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).
Queries (TanStack Query)
Section titled “Queries (TanStack Query)”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 + filtrouseQuery({ queryKey: ['teacher', 'session-detail', sessionId], queryFn: ... }) // pre-populated via batchMutations
Section titled “Mutations”| 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 |
Internacionalizacion
Section titled “Internacionalizacion”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 |