Skip to content

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.

Messages page

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.
  • 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.

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.

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”.

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:

  1. Lee retention_settings.contactReminderDays — si es 0, skip
  2. Query: alumnos activos (status IN ('trial', 'active')) con MAX(contact_log.sent_at) < now - thresholdDays o sin contacto
  3. Para cada alumno stale: verifica que no exista una notificacion contact_reminder unread para ese alumno (dedup)
  4. Si no existe: crea teacher_notifications con type='contact_reminder', relatedEntityType='student', relatedEntityId

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

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.

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
  • Registro en lote via POST /teacher/contact-log/bulk

El profesor puede programar recordatorios para contactar a un alumno en una fecha y hora futura.

Flujo:

  1. Click en boton “Programar” (icono reloj, morado) → abre modal con: canal, fecha, hora, mensaje opcional, nota interna
  2. POST /teacher/scheduled-messages → crea fila en scheduled_messages + job retrasado en BullMQ
  3. Cuando el delay expira → worker ejecuta fire(id):
    • Crea entrada en contact_log (canal + nota)
    • Marca status='sent', enlaza contactLogId
    • Crea teacher_notification (tipo: scheduled_message_due, titulo: “Hora de contactar a {nombre}”)
  4. El profesor ve la notificacion → navega a Mensajes → usa el boton de WhatsApp/email para contactar
  5. Cancelar: Click “Cancelar mensaje” en la timeline → DELETE /teacher/scheduled-messages/:id → elimina job de BullMQ, marca status='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”.


Feature Descripcion Estado
SMS via Twilio Canal adicional de comunicacion Aplazado

Archivo Proposito
apps/app/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
Canal Enum Boton UI Link externo Auto-log en sesiones
WhatsApp whatsapp Si (si telefono valido) wa.me/{phone} No
Email 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)
// 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 }

StudentTimelineService.getTimeline(teacherId, studentId, page, limit):

  1. 5 queries en paralelo via Promise.all:
    • contact_log — ORDER BY sent_at DESC
    • class_sessions JOIN services — ORDER BY starts_at DESC
    • student_lifecycle_events — ORDER BY occurred_at DESC
    • reviews — ORDER BY created_at DESC
    • scheduled_messages (status=‘pending’) — ORDER BY scheduled_for DESC
  2. Normalizacion: Cada resultado se transforma a TimelineEntry { id, type, timestamp, title, subtitle, metadata }
  3. Merge-sort: Todos los entries se ordenan por timestamp DESC
  4. 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.

packages/shared/src/schemas/contact-log.ts
contactChannels = ['whatsapp', 'email', 'phone', 'other'] as const
bulkContactLogSchema // { 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;
}

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.

Claves i18n relevantes (namespace messages):

Clave ES EN
channelWhatsapp WhatsApp WhatsApp
channelEmail Email Email
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)