Mensajes
Ruta: /teacher/messages · Atajo: g m · Sidebar: Mensajes
CRM de comunicaciones: contacto rapido con alumnos via WhatsApp, email o telefono, timeline unificada con sesiones y eventos del ciclo de vida, filtros por recencia de contacto y recordatorios automaticos.
Que hay
Section titled “Que hay”Layout de dos paneles
Section titled “Layout de dos paneles”Panel izquierdo — Lista de alumnos:
- Barra de busqueda: “Buscar alumno…” con icono de busqueda (filtro client-side por nombre o email)
- Filtro por fecha de contacto: dropdown debajo de la busqueda con 5 opciones:
- Todos — sin filtro
- Sin contacto — alumnos sin ningun registro de contacto
- 7+ dias — alumnos no contactados en 7 o mas dias
- 14+ dias — alumnos no contactados en 14 o mas dias
- 30+ dias — alumnos no contactados en 30 o mas dias
- Lista scrollable de alumnos, cada uno muestra:
- Circulo avatar con iniciales (bg-primary-100)
- Nombre del alumno (negrita, truncado)
- Ultimo contacto formateado como “Ultimo: Xd” o “Sin contacto” (texto muted). Dato obtenido via JOIN con
contact_log. - Punto de estado (verde=activo, amarillo=trial, gris=inactivo)
- Alumno seleccionado resaltado con
bg-primary-50
Panel derecho — Detalle del alumno:
- Cabecera: Avatar + nombre + email
- Botones de contacto rapido (fila horizontal):
- WhatsApp (verde): Solo visible si el alumno tiene telefono valido. Valida el numero antes de abrir
wa.me/. Si el numero es invalido, muestra boton deshabilitado con warning. - Email (azul): Siempre visible. Abre
mailto:con asunto y cuerpo template. - Telefono (ambar): Solo visible si el alumno tiene telefono. Abre
tel:para llamada directa.
- WhatsApp (verde): Solo visible si el alumno tiene telefono valido. Valida el numero antes de abrir
- Tras cualquier accion de contacto se abre
ContactNoteDialog: modal opcional para anadir una nota antes de guardar. - Plantillas rapidas: Si hay plantillas guardadas, se muestran como chips debajo de los botones. Click abre el canal con el mensaje rellenado.
- Timeline unificada (“Actividad”): Ver seccion dedicada mas abajo.
Timeline unificada
Section titled “Timeline unificada”El panel derecho muestra una timeline cronologica que combina 5 tipos de evento para cada alumno, ordenados por fecha descendente:
| Tipo | Icono | Color | Contenido | Editable |
|---|---|---|---|---|
| contact | Canal (Phone/Mail/PhoneCall/CalendarDays) | Verde/azul/ambar/morado segun canal | Nombre del canal + nota | Si (editar nota, borrar) |
| session | CalendarDays | Azul | Nombre del servicio + badge de estado (scheduled/completed/cancelled) | No |
| lifecycle | Activity | Gris | Tipo de evento (first_contact, trial_requested, churned, etc.) | No |
| review | Star | Amarillo | Valoracion X/5 + preview del body de la resena | No |
| scheduled | Clock | Morado | Mensaje programado pendiente + canal | Si (cancelar) |
Paginacion: 30 entradas por pagina. Boton “Cargar mas” al final acumula entradas en estado local. Al cambiar de alumno o crear nuevo contacto, se reinicia a pagina 1.
Edicion/borrado de contactos: Las entradas de tipo contact mantienen acciones al hover (lapiz para editar nota, papelera para borrar). El resto de tipos son read-only.
Integracion con sesiones
Section titled “Integracion con sesiones”Cuando se programa una sesion, se registra automaticamente una entrada en contact_log con channel='other':
| Accion | Nota automatica | Metodo |
|---|---|---|
| Alumno reserva sesion | ”Sesion reservada” | SessionService.bookSession() |
| Clase de prueba solicitada | ”Clase de prueba solicitada” | SessionService.createTrialSession() |
| Profesor programa manualmente | ”Sesion programada” | SessionService.scheduleManualSession() |
| Sesion reprogramada | ”Sesion reprogramada” | SessionService.rescheduleSession() |
Todas las llamadas son fire-and-forget (.catch(() => {})). Para sesiones de grupo, se crea una entrada por cada participante.
El canal 'other' se muestra en la timeline con icono CalendarDays y color morado, con label “Sistema”.
Recordatorios de contacto
Section titled “Recordatorios de contacto”Sistema automatico que genera notificaciones in-app cuando un alumno lleva demasiado tiempo sin contacto.
Configuracion: Columna contact_reminder_days en tabla retention_settings (default: 14 dias, 0 = desactivado). Configurable por profesor via la API de retention settings.
Deteccion: El lifecycle-detection-worker (cron diario a las 6 AM) ejecuta StudentLifecycleService.detectContactReminders() para cada profesor:
- Lee
retention_settings.contactReminderDays— si es 0, skip - Query: alumnos activos (
status IN ('trial', 'active')) conMAX(contact_log.sent_at) < now - thresholdDayso sin contacto - Para cada alumno stale: verifica que no exista una notificacion
contact_reminderunread para ese alumno (dedup) - Si no existe: crea
teacher_notificationscontype='contact_reminder',relatedEntityType='student',relatedEntityId
Validacion de telefono
Section titled “Validacion de telefono”El numero de telefono se valida antes de construir el enlace wa.me/:
- Se limpian caracteres no numericos (excepto
+) - Se verifica longitud: minimo 7 digitos, maximo 15 (estandar E.164)
- Si invalido: boton WhatsApp deshabilitado con icono de advertencia
- Si valido: enlace
wa.me/construido con numero limpio
Plantillas de mensaje
Section titled “Plantillas de mensaje”Las plantillas de WhatsApp y email se almacenan en teachers.messageTemplates (JSONB). El profesor puede editarlas desde el panel lateral de configuracion accesible desde la cabecera.
- Variables: {{nombre}}, {{servicio}}, {{email}}
- Vista previa en tiempo real con datos del alumno seleccionado
- Plantillas guardadas: Coleccion reutilizable con nombre y canal. Se muestran como chips en el panel derecho para acceso rapido.
Mensajes masivos
Section titled “Mensajes masivos”Modo multi-seleccion activable desde la cabecera:
- Checkboxes en cada tarjeta de alumno, boton “Seleccionar todos”
- Barra de acciones flotante:
- WhatsApp a todos — abre
wa.me/por alumno con telefono valido - Email a todos — abre
mailto:por alumno
- WhatsApp a todos — abre
- Registro en lote via
POST /teacher/contact-log/bulk
Mensajes programados
Section titled “Mensajes programados”El profesor puede programar recordatorios para contactar a un alumno en una fecha y hora futura.
Flujo:
- Click en boton “Programar” (icono reloj, morado) → abre modal con: canal, fecha, hora, mensaje opcional, nota interna
POST /teacher/scheduled-messages→ crea fila enscheduled_messages+ job retrasado en BullMQ- Cuando el delay expira → worker ejecuta
fire(id):- Crea entrada en
contact_log(canal + nota) - Marca
status='sent', enlazacontactLogId - Crea
teacher_notification(tipo:scheduled_message_due, titulo: “Hora de contactar a {nombre}”)
- Crea entrada en
- El profesor ve la notificacion → navega a Mensajes → usa el boton de WhatsApp/email para contactar
- Cancelar: Click “Cancelar mensaje” en la timeline →
DELETE /teacher/scheduled-messages/:id→ elimina job de BullMQ, marcastatus='cancelled'
Recuperacion al inicio: Al arrancar el worker, consulta scheduled_messages WHERE status='pending' AND scheduled_for < now → re-encola con delay: 0.
Los mensajes programados pendientes se muestran en la timeline con borde morado y badge “Pendiente”.
Que falta
Section titled “Que falta”| Feature | Descripcion | Estado |
|---|---|---|
| SMS via Twilio | Canal adicional de comunicacion | Aplazado |
Referencia tecnica
Section titled “Referencia tecnica”Archivos clave
Section titled “Archivos clave”| Archivo | Proposito |
|---|---|
apps/web/src/routes/teacher/messages.lazy.tsx | Pagina completa (1600+ lineas) |
apps/api/src/routes/teacher/contact-log.ts | Rutas HTTP contact log (GET, POST, PATCH, DELETE, bulk) |
apps/api/src/routes/teacher/students-mgmt.ts | Ruta GET :id/timeline |
apps/api/src/routes/teacher/settings.ts | Rutas GET/PATCH de message-templates y saved-message-templates |
apps/api/src/services/teacher/contact-log-service.ts | CRUD de contacto (paginacion, update, delete, bulk) |
apps/api/src/services/teacher/student-timeline-service.ts | Timeline unificado: 5 queries paralelas, merge-sort, paginacion |
apps/api/src/services/teacher/scheduled-message-service.ts | CRUD + fire + recovery de mensajes programados |
apps/api/src/routes/teacher/scheduled-messages.ts | Rutas HTTP scheduled messages (GET, POST, DELETE) |
apps/api/src/jobs/scheduled-messages-worker.ts | BullMQ worker: procesa jobs retrasados de mensajes programados |
packages/db/src/schema/scheduled-messages.ts | Schema tabla scheduled_messages |
packages/shared/src/schemas/scheduled-message.ts | Zod schemas para crear/filtrar mensajes programados |
apps/api/src/services/teacher/student-lifecycle-service.ts | detectContactReminders() — cron de recordatorios |
apps/api/src/services/scheduling/session-service.ts | Hooks fire-and-forget de auto-log contacto |
apps/api/src/services/teacher/student-management-service.ts | listStudents() incluye lastContact via JOIN |
apps/api/src/jobs/lifecycle-detection-worker.ts | Cron diario: gaps, churn, streaks + contact reminders |
packages/db/src/schema/contact-log.ts | Schema tabla contact_log |
packages/db/src/schema/retention-settings.ts | Schema tabla retention_settings (incluye contactReminderDays) |
packages/shared/src/schemas/contact-log.ts | contactChannels (whatsapp, email, phone, other) + bulkContactLogSchema |
packages/shared/src/schemas/retention.ts | updateRetentionSettingsSchema (incluye contactReminderDays) |
| Endpoint | Metodo | Descripcion |
|---|---|---|
/teacher/contact-log | GET | Lista paginada de contactos { items, page, limit, hasMore }. Params: studentId?, page? (default 1), limit? (default 25, max 100) |
/teacher/contact-log | POST | Crear entrada { studentId, channel, note? } |
/teacher/contact-log/:id | PATCH | Actualizar nota { note?: string } |
/teacher/contact-log/:id | DELETE | Borrar entrada |
/teacher/contact-log/bulk | POST | Crear multiples entradas { studentIds[], channel, note? } |
/teacher/students | GET | Incluye lastContact: ISO string | null (MAX sentAt desde contact_log) |
/teacher/students/:id/timeline | GET | Timeline unificado { items, page, limit, hasMore }. Params: page? (default 1), limit? (default 30). Cada item: { id, type, timestamp, title, subtitle, metadata } |
/teacher/settings/message-templates | GET | Plantillas personalizadas del profesor |
/teacher/settings/message-templates | PATCH | Actualizar plantillas |
/teacher/settings/saved-message-templates | GET | Plantillas guardadas |
/teacher/scheduled-messages | GET | Lista mensajes programados { items, page, limit, hasMore }. Params: status?, page?, limit? |
/teacher/scheduled-messages | POST | Crear mensaje programado { studentId, channel, body?, scheduledFor, note? } |
/teacher/scheduled-messages/:id | DELETE | Cancelar mensaje programado pendiente |
Canales de contacto
Section titled “Canales de contacto”| Canal | Enum | Boton UI | Link externo | Auto-log en sesiones |
|---|---|---|---|---|
whatsapp | Si (si telefono valido) | wa.me/{phone} | No | |
email | Si (siempre) | mailto:{email} | No | |
| Telefono | phone | Si (si hay telefono) | tel:{phone} | No |
| Sistema | other | No (solo timeline) | — | Si (book, trial, schedule, reschedule) |
Queries (TanStack Query)
Section titled “Queries (TanStack Query)”// Lista de alumnos (incluye lastContact)useQuery({ queryKey: ['teacher-students-messages'], queryFn: ... })
// Contact log por alumno (para mutaciones edit/delete)useQuery({ queryKey: ['teacher-contact-log', studentId, page], queryFn: ... })// -> ContactLogPage { items: ContactLogEntry[], page, limit, hasMore }
// Timeline unificado por alumno (para visualizacion)useQuery({ queryKey: ['student-timeline', studentId, page], queryFn: ... })// -> TimelinePage { items: TimelineEntry[], page, limit, hasMore }Timeline: arquitectura del merge
Section titled “Timeline: arquitectura del merge”StudentTimelineService.getTimeline(teacherId, studentId, page, limit):
- 5 queries en paralelo via
Promise.all:contact_log— ORDER BYsent_atDESCclass_sessionsJOINservices— ORDER BYstarts_atDESCstudent_lifecycle_events— ORDER BYoccurred_atDESCreviews— ORDER BYcreated_atDESCscheduled_messages(status=‘pending’) — ORDER BYscheduled_forDESC
- Normalizacion: Cada resultado se transforma a
TimelineEntry { id, type, timestamp, title, subtitle, metadata } - Merge-sort: Todos los entries se ordenan por
timestampDESC - Paginacion:
slice(0, limit),hasMore = entries.length > limit
El id de cada entry lleva prefijo del tipo (contact-{id}, session-{id}, etc.) para evitar colisiones en la key de React.
Schemas relevantes
Section titled “Schemas relevantes”contactChannels = ['whatsapp', 'email', 'phone', 'other'] as constbulkContactLogSchema // { studentIds: string[], channel, note? }
// TimelineEntry (respuesta del endpoint timeline)interface TimelineEntry { id: string; // '{type}-{uuid}' type: 'contact' | 'session' | 'lifecycle' | 'review' | 'scheduled'; timestamp: string; // ISO 8601 title: string; // Canal/servicio/evento/rating/mensaje programado subtitle: string | null; // Nota/status/body metadata: Record<string, unknown>; // channel, entryId, sessionId, rating, scheduledMessageId, etc.}
// ContactLogPage (respuesta de GET /teacher/contact-log)interface ContactLogPage { items: ContactLogEntry[]; page: number; limit: number; hasMore: boolean;}lastContact en listStudents()
Section titled “lastContact en listStudents()”StudentManagementService.listStudents() ejecuta una segunda query sobre contact_log en batch (1 query para todos los alumnos de la pagina) usando MAX(sent_at) GROUP BY student_id. El resultado se mapea en un Map<studentId, ISO string> y se incluye en cada alumno como lastContact.
Internacionalizacion
Section titled “Internacionalizacion”Claves i18n relevantes (namespace messages):
| Clave | ES | EN |
|---|---|---|
channelWhatsapp | ||
channelEmail | ||
channelPhone | Telefono | Phone |
channelOther | Sistema | System |
filterAll | Todos | All |
filterNoContact | Sin contacto | No contact |
filter7d | 7+ dias sin contacto | 7+ days without contact |
filter14d | 14+ dias sin contacto | 14+ days without contact |
filter30d | 30+ dias sin contacto | 30+ days without contact |
timeline | Actividad | Activity |
timelineNoActivity | No hay actividad registrada | No activity recorded |
timelineLoadMore | Cargar mas | Load more |
timelineReview | Resena | Review |
contactReminderDays | Recordatorio de contacto (dias) | Contact reminder (days) |