Saltar a contenido

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)

  1. Logueate como ADMIN en el BackOffice (POST /api/authenticate, JHipster estándar).
  2. Sidebar → Usuarios portal (ruta /usuarios-cliente).
  3. Crear: login (ej. cliente-aa2000), email, nombre, apellido, y seleccioná la(s) empresa(s) con acceso.
  4. 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

  1. El cliente entra con sus credenciales. El login lo redirige a /portal (el guard ROLE_CLIENTE lo manda ahí; el resto de los roles van a /dashboard).
  2. Si tiene acceso a una sola empresa, el portal la autoselecciona (P5). Si tiene varias, aparece un selector de empresa.
  3. 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:

{
  "sanitario_id": 5,
  "estado": "LIBRE",
  "minutos_desde_ultima": 12,
  "proximo_vencimiento_min": 38
}

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: LIMPIANDOSIN_EXIGENCIATENDENCIA_NEGATIVASLA_VENCIDOSIN_DATOSLIBRE.

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_limpieza con el flag visible_cliente = 1. El seed habilita ROTURAS y REPONER_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):

  1. El módulo Smiley está activo.
  2. El flag visible_opiniones de la empresa está en 1.

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_EXIGENCIA gris/TENDENCIA_NEGATIVA rojo "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 /estado está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.