Ir al contenido

Mensajería staff ↔ jugador

Mensajería interna bidireccional entre staff y jugador, con un hilo por jugador. El staff escribe desde el perfil del jugador (/dashboard/plantel/[id]), el jugador desde /player/mensajes. Cada mensaje del staff dispara automáticamente una push notification al teléfono del jugador. Badge de no-leídos del lado staff cuenta los mensajes entrantes con read_at = null.

  • Problema que resuelve: hoy el staff y los jugadores se comunican por WhatsApp / SMS, sin trazabilidad, mezclado con vida privada, y sin control de la org sobre el canal. CÉNIT centraliza ese hilo dentro de la app, con push notifications nativas, y deja el historial auditable.
  • Casos de uso típicos [NEEDS_USER: CASE_STUDY feedback real Nacional]:
    • Avisar al jugador sobre cambio de horario o citación a médico.
    • Jugador reporta dolor al PF sin tener que llamar.
    • HoP responde a una duda del jugador sobre la planificación.
  • Planes que lo incluyen: todos los planes. Sin gate.
  • Diferenciador: integración nativa con PWA + push web (no requiere app store), todo dentro del sistema multi-tenant con RLS por organización — un staff de Org A no puede mandar mensajes a jugadores de Org B (validado en el servidor en S3).
  • Quién puede ver hilos: todos los staff que tienen acceso al perfil del jugador (depende de visible_player_tabs de la org y del rol). En la práctica hop / dir tienen acceso garantizado; fisio / médico / nutri / psi lo tienen si el tab está habilitado.
  • Quién recibe mensajes del jugador: si el jugador envía sin destinatario explícito, el sistema lo asigna al primer hop o dir de la org (sendPlayerMessage busca con .in('role', ['hop','dir']).limit(1)).
  • Seguridad cross-tenant: sendStaffMessage valida que la org destino sea la del caller y que el to_player_id pertenezca a esa org. Fix histórico S3.

Lado staff (staff-message-thread.tsx):

  1. Abrir perfil del jugador → tab Mensajes.
  2. Ver hilo con mensajes ordenados, badge de no-leídos arriba contando mensajes del jugador con read_at = null.
  3. Escribir y enviar — el mensaje se persiste en messages con from_staff_id y to_player_id.
  4. Automáticamente: fetch async a /api/push/send con targetUserId = players.auth_user_id. Falla silenciosamente si el jugador no está suscrito al push.

Lado jugador (/player/mensajes):

  1. Push notification llega al teléfono (si tiene la PWA instalada y suscripción activa).
  2. Tap abre /player/mensajes (definido en el body de la push: url: '/player/mensajes').
  3. El jugador responde — sendPlayerMessage(content) persiste en messages con from_player_id y to_staff_id (asignado al primer hop/dir).

[NEEDS_USER: SCREENSHOT del hilo staff con badge de no-leídos]

  • VAPID keys en Workers Secrets (NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT) — requeridas para push web.
  • NEXT_PUBLIC_APP_URL — usada como base para llamar a /api/push/send. Si falta, el fetch va a /api/push/send relativo y depende del runtime.
  • Suscripción push del jugador: se da de alta desde app/player/_components/push-subscribe-button.tsx → POST a /api/push/subscribe.
  • “El jugador no recibe la push” — chequear push_subscriptions: si no hay fila para ese user_id, no está suscrito. Pasos:
    1. PWA instalada, 2) permiso de notifications otorgado,
    2. tap en “Activar notificaciones” en /player.
  • “Push 410 / 404” — endpoint expirado en el navegador del jugador. El SW limpia el endpoint dead automáticamente (PR #120, safe 2026-05).
  • “Mensaje vacío rechazado”content.trim() debe ser no-vacío y <= 2000 chars.
  • “Mensaje crosseado entre orgs” — bloqueado en server: si el to_player_id no pertenece a la org del caller, devuelve “Jugador no pertenece a tu organización” (fix S3).
  • “Impersonation read-only no manda”assertImpersonationCanWrite bloquea writes durante impersonation en modo read_only. Devuelve code: 'UNAUTHORIZED' con error: 'impersonation_read_only'.

Player surface: SÍ. Es uno de los pocos módulos con superficie de jugador.

  • Pantalla: /player/mensajes — hilo simple, input de texto, history scroll.
  • Push notifications: automáticas en cada mensaje del staff. Título: "Mensaje de {staff_full_name}". Body: primeros 80 chars del mensaje. URL: /player/mensajes.
  • Badge: el portal del jugador puede mostrar badge de no-leídos (mensajes del staff con read_at = null).
  • Sin email: la mensajería no manda email; solo push + in-app.
  • Tabla: messages con columnas id, organization_id, from_player_id, from_staff_id, to_player_id, to_staff_id, content, created_at, read_at. RLS por organization_id.
  • Tabla relacionada: push_subscriptions (endpoint, p256dh, auth_key, user_id) — quién recibe push.
  • API route: /api/push/send — envía push vía web-push.
  • No usa RPCs — todas las queries son SELECT/INSERT directas.
  • PWA / Service Workerpublic/sw.js maneja el evento push y muestra la notificación nativa.
  • VAPID / web-push — librería en el endpoint /api/push/send.
  • Plantel — el tab Mensajes se renderiza desde el perfil del jugador.
  • ImpersonationassertImpersonationCanWrite corre antes de cualquier insert.
  • Sin attachments — solo texto plano hasta 2000 chars.
  • Sin grupo / broadcast — no se puede mandar el mismo mensaje a N jugadores en un solo envío. Si se necesita, hay que iterar sendStaffMessage por jugador.
  • Auto-asignación a hop/dir: cuando el jugador escribe, el destinatario es el primer hop o dir encontrado. No hay UI para que el jugador elija a quién escribir.
  • Sin “leído por staff”read_at solo se setea del lado staff cuando lee mensajes del jugador. La inversa (jugador leyó mensaje del staff) no está implementada.
  • Sin retención / archive — los hilos crecen sin límite. No hay job de cleanup.