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.
Para comercial
Sección titulada «Para comercial»- 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).
Cómo lo usa el staff
Sección titulada «Cómo lo usa el staff»Acceso y permisos
Sección titulada «Acceso y permisos»- Quién puede ver hilos: todos los staff que tienen acceso al
perfil del jugador (depende de
visible_player_tabsde la org y del rol). En la prácticahop / dirtienen 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
hopodirde la org (sendPlayerMessagebusca con.in('role', ['hop','dir']).limit(1)). - Seguridad cross-tenant:
sendStaffMessagevalida que la org destino sea la del caller y que elto_player_idpertenezca a esa org. Fix histórico S3.
Flujos paso a paso
Sección titulada «Flujos paso a paso»Lado staff (staff-message-thread.tsx):
- Abrir perfil del jugador → tab Mensajes.
- Ver hilo con mensajes ordenados, badge de no-leídos arriba
contando mensajes del jugador con
read_at = null. - Escribir y enviar — el mensaje se persiste en
messagesconfrom_staff_idyto_player_id. - Automáticamente:
fetchasync a/api/push/sendcontargetUserId = players.auth_user_id. Falla silenciosamente si el jugador no está suscrito al push.
Lado jugador (/player/mensajes):
- Push notification llega al teléfono (si tiene la PWA instalada y suscripción activa).
- Tap abre
/player/mensajes(definido en el body de la push:url: '/player/mensajes'). - El jugador responde —
sendPlayerMessage(content)persiste enmessagesconfrom_player_idyto_staff_id(asignado al primer hop/dir).
[NEEDS_USER: SCREENSHOT del hilo staff con badge de no-leídos]
Configuración relacionada
Sección titulada «Configuración relacionada»- 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/sendrelativo 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.
FAQ / casos límite
Sección titulada «FAQ / casos límite»- “El jugador no recibe la push” — chequear
push_subscriptions: si no hay fila para eseuser_id, no está suscrito. Pasos:- PWA instalada, 2) permiso de notifications otorgado,
- 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<= 2000chars. - “Mensaje crosseado entre orgs” — bloqueado en server: si el
to_player_idno pertenece a la org del caller, devuelve “Jugador no pertenece a tu organización” (fix S3). - “Impersonation read-only no manda” —
assertImpersonationCanWritebloquea writes durante impersonation en modoread_only. Devuelvecode: 'UNAUTHORIZED'conerror: 'impersonation_read_only'.
Cómo lo ve el jugador
Sección titulada «Cómo lo ve el jugador»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.
Datos y métricas
Sección titulada «Datos y métricas»Tablas DB / RPCs / vistas materializadas
Sección titulada «Tablas DB / RPCs / vistas materializadas»- Tabla:
messagescon columnasid,organization_id,from_player_id,from_staff_id,to_player_id,to_staff_id,content,created_at,read_at. RLS pororganization_id. - Tabla relacionada:
push_subscriptions(endpoint,p256dh,auth_key,user_id) — quién recibe push. - API route:
/api/push/send— envía push víaweb-push. - No usa RPCs — todas las queries son SELECT/INSERT directas.
Integraciones
Sección titulada «Integraciones»- PWA / Service Worker —
public/sw.jsmaneja el eventopushy 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.
- Impersonation —
assertImpersonationCanWritecorre antes de cualquier insert.
Limitaciones / roadmap
Sección titulada «Limitaciones / roadmap»- 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
sendStaffMessagepor 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_atsolo 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.