Skip to main content

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 desde chat_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.
Métricas de respuesta reales (t4). avgResponseTimeMinutes y slaResponseCompliance quedaron obsoletas: salían de chat_tickets.first_response_at, un campo custom de Zendesk cuya sincronización se desconectó en agosto de 2026 (NULL en todo ticket nuevo y ~92 % de deltas negativos en el histórico, que inflaban el cumplimiento). Las sustituyen avgFirstPublicReplyMinutes y slaFirstPublicReplyCompliance (primer comentario público) más avgFirstInternalActionMinutes (primer movimiento interno). Todas aplican la misma regla de higiene “delta estrictamente > 0”: un evento en o antes de la creación se descarta (elapsedMinutesSince en utils/response-metrics.util.ts, espejada en la vista analytics.v_tickets_clean, migración 226).
Estados considerados abiertos: 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 en sla.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:
  1. Override Dormido — si el ticket está abierto y sin actividad (updatedAt / lastCommentAt) durante ≥ 30 días (DORMIDO_THRESHOLD_DAYS), el tipo es dormido, ignorando el resto.
  2. Equipo primarioresolution_teamTICKET_TYPE_BY_TEAM.
  3. Categoría (fallback)categoryTICKET_TYPE_BY_CATEGORY, cuando el equipo no resuelve un tipo.
  4. Defaultadministrativo (TICKET_TYPE_DEFAULT).
Mapeo por equipo (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ámetro search 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% OR description ILIKE %term% (case-insensitive).
  • Si el término (quitando un # inicial opcional) es un número, se añade una rama de igualdad exacta zendesk_ticket_id = N, de modo que pegar #12345 o 12345 encuentra el ticket por su id de Zendesk.
La entrada se sanea antes de interpolarse: los caracteres con significado en el filtro .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 filtros channelId, 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.