feat: add queue summary widget and layout fixes

This commit is contained in:
Esdras Renan 2025-11-06 17:05:31 -03:00
parent f7976e2c39
commit a542846313
12 changed files with 350 additions and 45 deletions

View file

@ -13,6 +13,7 @@ const WIDGET_TYPES = [
"radar",
"gauge",
"table",
"queue-summary",
"text",
] as const
@ -145,6 +146,12 @@ function normalizeWidgetConfig(type: WidgetType, config: unknown) {
],
options: { downloadCSV: true },
}
case "queue-summary":
return {
type: "queue-summary",
title: "Resumo por fila",
dataSource: { metricKey: "queues.summary_cards" },
}
case "text":
default:
return {

View file

@ -90,6 +90,24 @@ function filterTicketsByQueue<T extends { queueId?: Id<"queues"> | null }>(
})
}
const QUEUE_RENAME_LOOKUP: Record<string, string> = {
"Suporte N1": "Chamados",
"suporte-n1": "Chamados",
chamados: "Chamados",
"Suporte N2": "Laboratório",
"suporte-n2": "Laboratório",
laboratorio: "Laboratório",
Laboratorio: "Laboratório",
visitas: "Visitas",
}
function renameQueueName(value: string) {
const direct = QUEUE_RENAME_LOOKUP[value]
if (direct) return direct
const normalizedKey = value.replace(/\s+/g, "-").toLowerCase()
return QUEUE_RENAME_LOOKUP[normalizedKey] ?? value
}
type AgentStatsRaw = {
agentId: Id<"users">
name: string | null
@ -441,6 +459,97 @@ const metricResolvers: Record<string, MetricResolver> = {
data,
}
},
"queues.summary_cards": async (ctx, { tenantId, viewer, params }) => {
const queueFilter = parseQueueIds(params)
const filterHas = queueFilter && queueFilter.length > 0
const normalizeKey = (id: Id<"queues"> | null) => (id ? String(id) : "sem-fila")
const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect()
const queueNameMap = new Map<string, string>()
queues.forEach((queue) => {
const key = String(queue._id)
queueNameMap.set(key, renameQueueName(queue.name))
})
const now = Date.now()
const stats = new Map<
string,
{ id: string; name: string; pending: number; inProgress: number; paused: number; breached: number }
>()
const ensureEntry = (key: string, fallbackName?: string) => {
if (!stats.has(key)) {
const resolvedName =
queueNameMap.get(key) ??
(key === "sem-fila" ? "Sem fila" : fallbackName ?? "Fila desconhecida")
stats.set(key, {
id: key,
name: resolvedName,
pending: 0,
inProgress: 0,
paused: 0,
breached: 0,
})
}
return stats.get(key)!
}
for (const queue of queues) {
const key = String(queue._id)
if (filterHas && queueFilter && !queueFilter.includes(key)) continue
ensureEntry(key)
}
const scopedTickets = await fetchScopedTickets(ctx, tenantId, viewer)
for (const ticket of scopedTickets) {
const key = normalizeKey(ticket.queueId ?? null)
if (filterHas && queueFilter && !queueFilter.includes(key)) continue
const entry = ensureEntry(key)
const status = normalizeStatus(ticket.status)
if (status === "PENDING") {
entry.pending += 1
} else if (status === "AWAITING_ATTENDANCE") {
entry.inProgress += 1
} else if (status === "PAUSED") {
entry.paused += 1
}
if (status !== "RESOLVED") {
const dueAt = typeof ticket.dueAt === "number" ? ticket.dueAt : null
if (dueAt && dueAt < now) {
entry.breached += 1
}
}
}
if (!(filterHas && queueFilter && !queueFilter.includes("sem-fila"))) {
ensureEntry("sem-fila", "Sem fila")
} else if (filterHas) {
stats.delete("sem-fila")
}
const data = Array.from(stats.values()).map((item) => ({
id: item.id,
name: item.name,
pending: item.pending,
inProgress: item.inProgress,
paused: item.paused,
breached: item.breached,
}))
data.sort((a, b) => {
const totalA = a.pending + a.inProgress + a.paused
const totalB = b.pending + b.inProgress + b.paused
if (totalA === totalB) {
return a.name.localeCompare(b.name, "pt-BR")
}
return totalB - totalA
})
return {
meta: { kind: "collection", key: "queues.summary_cards" },
data,
}
},
"tickets.sla_compliance_by_queue": async (ctx, { tenantId, viewer, params }) => {
const rangeDays = parseRange(params)
const companyId = parseCompanyId(params)