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:

TipoIconoColorContenidoEditable
contactCanal (Phone/Mail/PhoneCall/CalendarDays)Verde/azul/ambar/morado segun canalNombre del canal + notaSi (editar nota, borrar)
sessionCalendarDaysAzulNombre del servicio + badge de estado (scheduled/completed/cancelled)No
lifecycleActivityGrisTipo de evento (first_contact, trial_requested, churned, etc.)No
reviewStarAmarilloValoracion X/5 + preview del body de la resenaNo
scheduledClockMoradoMensaje programado pendiente + canalSi (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':

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


FeatureDescripcionEstado
SMS via TwilioCanal adicional de comunicacionAplazado

ArchivoProposito
apps/web/src/routes/teacher/messages.lazy.tsxPagina completa (1600+ lineas)
apps/api/src/routes/teacher/contact-log.tsRutas HTTP contact log (GET, POST, PATCH, DELETE, bulk)
apps/api/src/routes/teacher/students-mgmt.tsRuta GET :id/timeline
apps/api/src/routes/teacher/settings.tsRutas GET/PATCH de message-templates y saved-message-templates
apps/api/src/services/teacher/contact-log-service.tsCRUD de contacto (paginacion, update, delete, bulk)
apps/api/src/services/teacher/student-timeline-service.tsTimeline unificado: 5 queries paralelas, merge-sort, paginacion
apps/api/src/services/teacher/scheduled-message-service.tsCRUD + fire + recovery de mensajes programados
apps/api/src/routes/teacher/scheduled-messages.tsRutas HTTP scheduled messages (GET, POST, DELETE)
apps/api/src/jobs/scheduled-messages-worker.tsBullMQ worker: procesa jobs retrasados de mensajes programados
packages/db/src/schema/scheduled-messages.tsSchema tabla scheduled_messages
packages/shared/src/schemas/scheduled-message.tsZod schemas para crear/filtrar mensajes programados
apps/api/src/services/teacher/student-lifecycle-service.tsdetectContactReminders() — cron de recordatorios
apps/api/src/services/scheduling/session-service.tsHooks fire-and-forget de auto-log contacto
apps/api/src/services/teacher/student-management-service.tslistStudents() incluye lastContact via JOIN
apps/api/src/jobs/lifecycle-detection-worker.tsCron diario: gaps, churn, streaks + contact reminders
packages/db/src/schema/contact-log.tsSchema tabla contact_log
packages/db/src/schema/retention-settings.tsSchema tabla retention_settings (incluye contactReminderDays)
packages/shared/src/schemas/contact-log.tscontactChannels (whatsapp, email, phone, other) + bulkContactLogSchema
packages/shared/src/schemas/retention.tsupdateRetentionSettingsSchema (incluye contactReminderDays)
EndpointMetodoDescripcion
/teacher/contact-logGETLista paginada de contactos { items, page, limit, hasMore }. Params: studentId?, page? (default 1), limit? (default 25, max 100)
/teacher/contact-logPOSTCrear entrada { studentId, channel, note? }
/teacher/contact-log/:idPATCHActualizar nota { note?: string }
/teacher/contact-log/:idDELETEBorrar entrada
/teacher/contact-log/bulkPOSTCrear multiples entradas { studentIds[], channel, note? }
/teacher/studentsGETIncluye lastContact: ISO string | null (MAX sentAt desde contact_log)
/teacher/students/:id/timelineGETTimeline unificado { items, page, limit, hasMore }. Params: page? (default 1), limit? (default 30). Cada item: { id, type, timestamp, title, subtitle, metadata }
/teacher/settings/message-templatesGETPlantillas personalizadas del profesor
/teacher/settings/message-templatesPATCHActualizar plantillas
/teacher/settings/saved-message-templatesGETPlantillas guardadas
/teacher/scheduled-messagesGETLista mensajes programados { items, page, limit, hasMore }. Params: status?, page?, limit?
/teacher/scheduled-messagesPOSTCrear mensaje programado { studentId, channel, body?, scheduledFor, note? }
/teacher/scheduled-messages/:idDELETECancelar mensaje programado pendiente
CanalEnumBoton UILink externoAuto-log en sesiones
WhatsAppwhatsappSi (si telefono valido)wa.me/{phone}No
EmailemailSi (siempre)mailto:{email}No
TelefonophoneSi (si hay telefono)tel:{phone}No
SistemaotherNo (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):

ClaveESEN
channelWhatsappWhatsAppWhatsApp
channelEmailEmailEmail
channelPhoneTelefonoPhone
channelOtherSistemaSystem
filterAllTodosAll
filterNoContactSin contactoNo contact
filter7d7+ dias sin contacto7+ days without contact
filter14d14+ dias sin contacto14+ days without contact
filter30d30+ dias sin contacto30+ days without contact
timelineActividadActivity
timelineNoActivityNo hay actividad registradaNo activity recorded
timelineLoadMoreCargar masLoad more
timelineReviewResenaReview
contactReminderDaysRecordatorio de contacto (dias)Contact reminder (days)