Panel de tickets
El Panel de tickets es la vista de analytics del sistema de tickets de soporte. Agrega métricas de actividad, cumplimiento de SLA, antigüedad del backlog y distribución por tipo, calculadas en tiempo real desdechat_tickets (sin tabla precomputada en v1).
Las reglas de negocio (objetivos de SLA, mapeos de tipo, umbral de dormido) viven en apps/backend/src/chat/tickets/config/sla.config.ts como única fuente de verdad. Los campos derivados (slaStatus, ticketType, daysSinceCreated) se calculan al leer cada ticket — el frontend no reimplementa las reglas.
KPIs
GET /chat/tickets/kpis — admite filtros por ventana de fechas (dateFrom/dateTo), equipo (team / resolution_team) y prioridad. Los tickets marcados como invalid no cuentan.
El FCR cruza
chat_tickets con chat_ticket_comments: una consulta agrupada cuenta los comentarios públicos por ticket resuelto. Pasa el FCR quien tiene commentCount ≤ 1 y reopenedCount === 0.created, assigned, in_progress. Estados resueltos: resolved, closed.
El estado
improvement_proposal era un estado falso ya migrado a la dimensión propia ticket_kind (ver Campos). Sigue en el CHECK de status durante la transición two-phase, pero las propuestas ya cuentan como abiertas por su lifecycle real (created/assigned). Excluir las propuestas de los KPIs (si CX lo pide) es un follow-up de producto.SLAs
Objetivos en minutos (wall-clock 24/7), definidos ensla.config.ts:
computeSlaStatus(ticket) devuelve el estado de la etapa actual del SLA:
- Tickets resueltos → siempre
ok. - Antes de la 1ª respuesta → se compara el tiempo transcurrido contra el SLA de respuesta.
- Tras la 1ª respuesta → se compara contra el SLA de resolución.
Por ahora el cálculo usa tiempo de reloj 24/7. La flag
SLA_USE_BUSINESS_HOURS está reservada para una futura utilidad de horario laboral (zona horaria + festivos españoles + fines de semana) que podrá enchufarse sin tocar los servicios.Tipos de ticket
resolveTicketType(ticket) deriva uno de cuatro tipos: Casa, Experiencia, Administrativo o Dormido. Regla híbrida, en orden:
- Override Dormido — si el ticket está abierto y sin actividad (
updatedAt/lastCommentAt) durante ≥ 30 días (DORMIDO_THRESHOLD_DAYS), el tipo esdormido, ignorando el resto. - Equipo primario —
resolution_team→TICKET_TYPE_BY_TEAM. - Categoría (fallback) —
category→TICKET_TYPE_BY_CATEGORY, cuando el equipo no resuelve un tipo. - Default —
administrativo(TICKET_TYPE_DEFAULT).
TICKET_TYPE_BY_TEAM):
Mapeo por categoría (
TICKET_TYPE_BY_CATEGORY): facturacion / contratos / documentacion → Administrativo; reservas / check_in / check_out → Experiencia.
Aging
GET /chat/tickets/aging — cuenta los tickets abiertos agrupados por antigüedad desde su creación. Admite filtro por team. Cohortes (AGING_COHORTS), con límite inferior inclusivo y superior exclusivo:
Stats
GET /chat/tickets/stats?groupBy=... — agrega los tickets por una dimensión, aplicando la misma superficie de filtros que el listado (estado, prioridad, agente, propiedad, fechas y columnas nativas de Zendesk). Cada bucket devuelve count, openCount, resolvedCount y sumas/medias de coste (totalEstimatedCost, totalActualCost, avgEstimatedCost).
Valores de groupBy: status, priority, property, resolution_team, zone, category, destination, payer, repair_status, owner_approval, finance_approval, ticket_type. Para property se adjunta el nombre como label; ticket_type es una dimensión derivada (no columna) y se calcula con resolveTicketType.
Paneles del dashboard
La página (TicketsDashboardPage.tsx) abre con una rejilla de 3 columnas de 6 KPI cards: Tickets abiertos, Críticos sin resolver (urgentes + altas abiertas), Resueltos último 30 días, FCR % (objetivo 70-80 %), Reaperturas % y Backlog > 3 días (alerta si supera el umbral del 10 %). Debajo, los paneles van en una rejilla uniforme de 3 columnas (xl:grid-cols-3) para que las cards no se agranden:
Se retiraron dos paneles que perdieron valor con el modelo nuevo de equipos:
TicketTypeDistributionChart (con los equipos actuales casi todo cae en “Casa”) y TicketsByPropertyChart (el gráfico era redundante con la tabla TicketsByPropertyTable, que se mantiene).Filtros de la lista
El listado de tickets (GET /chat/tickets) admite, además de los filtros por columna (estado, prioridad, agente, propiedad, fechas y campos nativos de Zendesk), filtros sobre los campos derivados:
Los filtros derivados dependen de funciones puras (
resolveTicketType, computeSlaStatus, isDormant) que no se pueden traducir a SQL sin duplicar las reglas. En v1 el listado trae el conjunto filtrado por columnas, deriva en memoria y pagina localmente — barato para el working set de CX (miles, no decenas de miles). Cuando no hay filtros derivados, la paginación se hace en Postgres (.range). pendingCost es la excepción: al ser una columna booleana es SQL-pushable, así que se aplica en la query (.eq('cost_pending', true)), compone por AND con el resto y conserva la paginación en servidor.Búsqueda de texto libre
El parámetrosearch hace una búsqueda server-side sobre el título y la descripción del ticket, y también por número de ticket de Zendesk:
title ILIKE %term%ORdescription ILIKE %term%(case-insensitive).- Si el término (quitando un
#inicial opcional) es un número, se añade una rama de igualdad exactazendesk_ticket_id = N, de modo que pegar#12345o12345encuentra el ticket por su id de Zendesk.
.or(...) de PostgREST (, ( ) " \) y los comodines de LIKE (% _ *) se colapsan a espacios, así una coma o un porcentaje suelto ni rompen la sintaxis ni provocan un comodín accidental. La búsqueda se combina con AND con el resto de filtros.
Intersección de filtros independientes
Los filtroschannelId, propertyId y clientUserId se combinan con AND por defecto: son filtros independientes, así que Casa + Cliente devuelve los tickets que cumplen ambos, no la unión. Un único filtro (p. ej. el channelId del sidebar de chat) se comporta igual que antes.
Con matchAny=true esos tres pasan a combinarse con OR (unión): un ticket coincide si comparte el canal o la propiedad o el cliente. Lo usa el sidebar de tickets relacionados (useRelatedTicketsQuery), donde cualquiera de esas dimensiones hace que un ticket sea “relacionado” con la conversación actual.