El portal del cliente: darle acceso y qué ve¶
Esta guía cubre dos cosas: (a) cómo un ADMIN le da acceso a un cliente al portal de transparencia, y (b) qué ve ese cliente una vez que entra.
El cliente acá no es el operario ni el técnico: es la empresa contratante (el aeropuerto, el shopping, el hospital, la planta) que quiere ver, en tiempo real y de forma transparente, el estado del servicio de limpieza sobre sus sanitarios. El portal es solo lectura — un ventanal, no una puerta: cero gestión, cero datos internos del personal de la operadora.
Fuente de verdad
El comportamiento descrito acá se valida contra el backend (V21_PORTAL_CLIENTE.md, docs/API.md y el modelo de datos). Donde el detalle visual del front no esté garantizado por el contrato del API, lo señalamos explícitamente.
El portal NO es el BackOffice¶
Antes de los pasos, dejemos clara la distinción que gobierna todo el módulo:
| BackOffice (admin) | Portal del cliente | |
|---|---|---|
| Authority | ROLE_ADMIN |
ROLE_CLIENTE |
| Quién | Personal de la operadora del servicio | La empresa contratante |
| Prefijo de API | /api/v1/admin/* + CRUD /api/* |
/api/v1/cliente/* |
| Alcance | TODAS las empresas | Solo la(s) empresa(s) con acceso explícito |
| Capacidades | Lee y gestiona (CRUD, alertas, NFC, usuarios) | Solo lectura |
| Datos de personal | Visibles | Nunca (nombres de operarios, RRHH) |
| Layout del front | Sidebar de gestión | PortalLayout sobrio, sin sidebar de gestión |
La autorización real es server-side
El guard por rol en el front React es UX, no seguridad. La autorización real vive en SecurityConfiguration y en el scoping por empresa de cada endpoint (Fase A/B del backend). Un ROLE_CLIENTE recibe 403 en todo /api/v1/admin/**, en todo el CRUD scaffold /api/* y en /api/v1/app/**.
Prerequisitos¶
- Ser ADMIN del BackOffice (
ROLE_ADMIN). El alta de usuarios cliente solo la hace el ADMIN — no hay auto-registro ni invitaciones por mail (decisión P3). - Tener ya creada(s) la(s) empresa(s) que el cliente va a ver (la jerarquía Empresa → Sucursal → Sector → Sanitario debe existir). Para montar todo eso desde cero, ver Montar un entorno desde cero.
- Tener el servicio de mail operativo (en dev sale por log / MailHog): el alta dispara un mail de activación.
Paso 1 — Crear el acceso del cliente¶
El cliente necesita una cuenta de BackOffice web (jhi_user) con la authority ROLE_CLIENTE y, sobre todo, el scoping por empresa: a qué empresas puede ver.
Desde el BackOffice (ABM)¶
- Logueate como ADMIN en el BackOffice (
POST /api/authenticate, JHipster estándar). - Sidebar → Usuarios portal (ruta
/usuarios-cliente). - Crear: login (ej.
cliente-aa2000), email, nombre, apellido, y seleccioná la(s) empresa(s) con acceso. - El usuario recibe un mail con una reset key y fija su contraseña por el flujo JHipster estándar. No se setea password en el alta (P3).
Activación por mail, no por password
El ABM crea el User con ROLE_CLIENTE vía UserService.createUser (cuenta activada + reset key por mail). El admin nunca tipea la contraseña del cliente: el cliente la fija él mismo desde el link del mail.
Equivalente por API¶
POST /api/v1/admin/usuarios-cliente
Authorization: Bearer <JWT de admin>
Content-Type: application/json
{
"login": "cliente-aa2000",
"email": "transparencia@aa2000.com.ar",
"nombre": "Gerencia",
"apellido": "Regional",
"empresaIds": [42]
}
El resto del ABM (también ROLE_ADMIN):
| Método | Ruta | Para qué |
|---|---|---|
GET |
/api/v1/admin/usuarios-cliente |
Lista los usuarios cliente con sus empresas |
PUT |
/api/v1/admin/usuarios-cliente/{login}/empresas |
Reemplaza el set de empresas con acceso |
PUT |
/api/v1/admin/usuarios-cliente/{login}/activado/{bool} |
Habilita / deshabilita la cuenta |
POST |
/api/v1/admin/usuarios-cliente/{login}/reset-password |
Dispara reset (flujo JHipster, responde 204 siempre) |
Multi-empresa: el caso del gerente regional
Un usuario cliente puede tener acceso a N empresas (decisión P5). El caso típico es 1, pero un gerente regional con varias plantas se modela como un cliente con varias entradas en cliente_empresa_acceso. "Último acceso" se devuelve null: JHipster no lo trackea nativo y no se sumó schema en el piloto.
El scoping es la regla de oro¶
El alcance de datos se deriva y valida siempre server-side contra la tabla cliente_empresa_acceso. Ningún endpoint del portal confía en un empresaId del request sin validar primero que el usuario autenticado tiene acceso a esa empresa.
Cliente de empresa A pidiendo empresa B → 403
Si cliente-aa2000 solo tiene acceso a la empresa 42 y llama GET /api/v1/cliente/estado?empresaId=99, el backend responde 403. Hay un test de seguridad por endpoint que cubre exactamente este caso (cross-empresa). No es opcional: es criterio de aceptación.
flowchart TD
A[ADMIN en el BackOffice] -->|crea usuario cliente + empresas| B[jhi_user con ROLE_CLIENTE]
B --> C[Mail de activación con reset key]
C --> D[El cliente fija su contraseña]
D --> E[Login en el front]
E -->|guard ROLE_CLIENTE| F[Redirige a /portal]
F --> G{Pide empresaId}
G -->|tiene acceso en cliente_empresa_acceso| H[200 - ve sus datos]
G -->|empresa ajena| I[403 - bloqueado server-side]
Paso 2 — El cliente entra¶
- El cliente entra con sus credenciales. El login lo redirige a
/portal(el guardROLE_CLIENTElo manda ahí; el resto de los roles van a/dashboard). - Si tiene acceso a una sola empresa, el portal la autoselecciona (P5). Si tiene varias, aparece un selector de empresa.
- A partir de ahí, el front consume exclusivamente los endpoints
/api/v1/cliente/*.
Los cuatro endpoints que sirve el portal (todos exigen ROLE_CLIENTE y todos validan empresaId contra cliente_empresa_acceso):
| Método | Ruta | Para qué |
|---|---|---|
GET |
/api/v1/cliente/mis-empresas |
Empresas accesibles del usuario (del token): [{empresa_id, codigo, nombre, logo_cliente_url}] |
GET |
/api/v1/cliente/estado?empresaId= |
Estado en tiempo real de los sanitarios de la empresa |
GET |
/api/v1/cliente/novedades?empresaId=&desde=&hasta= |
Novedades habilitadas + supervisiones conformes |
GET |
/api/v1/cliente/cumplimiento?empresaId=&periodo=YYYY-MM |
% de cumplimiento SLA por sector + total |
Qué ve el cliente¶
Esta es la parte central: el panel/tablero de transparencia. Todo lo de acá es solo lectura — el cliente no edita nada, no ve el CRUD, no ve datos de personal ni de otras empresas.
El tablero de estado en tiempo real¶
GET /api/v1/cliente/estado devuelve los sanitarios de la empresa con su estado actual. El cálculo es el mismo que el del dashboard admin (DashboardService.buildEstado), pero en un DTO propio sin operarios_activos: el cliente ve "limpiado hace 12 min", nunca quién lo limpió (decisión P1 — los nombres de operarios son información de RRHH de la operadora).
Por cada sanitario, el contrato garantiza:
Los estados que ve el cliente son los mismos que calcula el dashboard admin. Su significado y color:
| Estado | Color | Significado |
|---|---|---|
LIBRE |
verde | Al día — dentro del SLA, sin nadie limpiando ahora |
LIMPIANDO |
en curso | Hay un trabajo de limpieza abierto en este momento |
SLA_VENCIDO |
rojo | Venció el plazo de limpieza exigido y no se cumplió |
SIN_DATOS |
— | No hay información suficiente para evaluar el estado |
SIN_EXIGENCIA |
gris | Hoy no se exige limpieza acá (día fuera de sla.dias_semana o feriado de la sucursal). No genera alertas SLA |
TENDENCIA_NEGATIVA |
rojo | Hay una alerta de opinión del público activa para ese sanitario. Mismo color que SLA_VENCIDO pero distinta causa — el front lo rotula "Opinión del público" |
SIN_EXIGENCIA y TENDENCIA_NEGATIVA salen de la robustez y de Smiley
Estos dos estados no son del SLA "clásico". SIN_EXIGENCIA (gris) viene del calendario operativo (V21_ROBUSTEZ): si hoy no es un día exigido o es feriado de la sucursal, el sanitario no dispara alertas de SLA. TENDENCIA_NEGATIVA (rojo) viene del motor de opiniones (V21_SMILEY): es una racha de opiniones negativas del público, no un incumplimiento de limpieza — por eso se rotula aparte. El detalle de cómo se generan esas alertas está en Jobs de fondo y alertas.
Prioridad de estados (igual que en el admin)
Cuando varios aplican, el backend resuelve con esta prioridad: LIMPIANDO → SIN_EXIGENCIA → TENDENCIA_NEGATIVA → SLA_VENCIDO → SIN_DATOS → LIBRE.
stateDiagram-v2
[*] --> LIBRE
LIBRE --> LIMPIANDO: tap de ingreso
LIMPIANDO --> LIBRE: limpieza valida cerrada
LIBRE --> SLA_VENCIDO: vence el plazo sin limpieza
SLA_VENCIDO --> LIMPIANDO: tap de ingreso
LIBRE --> SIN_EXIGENCIA: dia no operativo o feriado
SIN_EXIGENCIA --> LIBRE: vuelve dia exigido
LIBRE --> TENDENCIA_NEGATIVA: racha de opiniones negativas
TENDENCIA_NEGATIVA --> LIBRE: se atiende la tendencia
Novedades del servicio¶
GET /api/v1/cliente/novedades muestra solo lo que la operadora habilitó como visible para el cliente:
- Eventos del catálogo
evento_limpiezacon el flagvisible_cliente = 1. El seed habilitaROTURASyREPONER_INSUMOS; el resto queda oculto. La operadora ajusta esto por ABM, sin release (decisión P2). - Supervisiones conformes (
SUPERVISION_CONFORME= "Supervisión realizada — conforme").
Como todo el portal, las novedades vienen sin operario (P1). El cliente ve qué pasó, nunca quién lo hizo.
Cumplimiento (reportes / histórico)¶
GET /api/v1/cliente/cumplimiento?periodo=YYYY-MM da el % de cumplimiento SLA por sector + total del período. Un periodo mal formado responde 400.
Métrica v1, a validar con el piloto
El cálculo de cumplimiento es la definición v1 (aproximación lineal del reporte admin: realizadas/esperados por sector y total). Está marcada para validarse con datos reales del piloto.
El front exporta el reporte vía window.print() (no suma librería de PDF); el backend ya puede exportar PDF server-side si en algún momento se quiere branding.
Opiniones del público¶
El portal puede mostrar las opiniones del público (Smiley / QR) si y solo si se cumplen dos condiciones (decisión P6):
- El módulo Smiley está activo.
- El flag
visible_opinionesde la empresa está en1.
Opiniones en /estado: pendiente hasta que exista Smiley
La columna empresa.visible_opiniones se creó en este módulo para que Smiley solo la consuma. La integración real de opiniones en GET /api/v1/cliente/estado quedó como TODO en el backend (Fase B) hasta que el módulo Smiley esté disponible. Tomalo como contrato previsto, no como algo garantizado hoy.
Lo que el cliente NUNCA ve¶
- Nombres de operarios ni datos de RRHH de la operadora (P1).
- Datos de otra empresa — el scoping lo corta server-side (403).
- El CRUD scaffold (
/api/*) ni nada de/api/v1/admin/**(403). - Alertas internas de la operación (ej.
OPERARIO_INACTIVO,RELOJ_DESVIADO): están fuera de alcance del portal.
Resumen¶
- El cliente usa el portal de transparencia (
ROLE_CLIENTE, solo lectura) bajo/api/v1/cliente/*, distinto del BackOffice del admin (ROLE_ADMIN). - El ADMIN crea el acceso desde Usuarios portal: cuenta con activación por mail y scoping por empresa (
cliente_empresa_acceso) — pedir una empresa ajena devuelve 403. - El cliente ve un tablero de estado en tiempo real (con los estados
LIBRE/LIMPIANDO/SLA_VENCIDO/SIN_DATOS/SIN_EXIGENCIAgris/TENDENCIA_NEGATIVArojo "Opinión del público"), novedades habilitadas, cumplimiento por sector y, condicionalmente, opiniones del público — todo sin nombres de personal. - Sin claridad total en las fuentes: el detalle visual exacto del front (más allá del contrato del API) y la integración de opiniones en
/estadoestán pendientes de validación E2E / del módulo Smiley; acá describimos lo que el contrato garantiza hoy.
Para los contratos REST completos, ver API y endpoint /sync. Para cómo se generan las alertas y tendencias que el cliente ve reflejadas, ver Jobs de fondo y alertas. Para montar la jerarquía de empresas desde cero, ver Montar un entorno desde cero.