feat: add SLA category breakdown report

This commit is contained in:
Esdras Renan 2025-11-08 02:47:39 -03:00
parent 6ab8a6ce89
commit a62f3d5283
8 changed files with 231 additions and 10 deletions

View file

@ -30,6 +30,23 @@ function average(values: number[]) {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function resolveCategoryName(
categoryId: string | null,
snapshot: { categoryName?: string } | null,
categories: Map<string, Doc<"ticketCategories">>
) {
if (categoryId) {
const category = categories.get(categoryId)
if (category?.name) {
return category.name
}
}
if (snapshot?.categoryName && snapshot.categoryName.trim().length > 0) {
return snapshot.categoryName.trim()
}
return "Sem categoria"
}
export const OPEN_STATUSES = new Set<TicketStatusNormalized>(["PENDING", "AWAITING_ATTENDANCE", "PAUSED"]);
export const ONE_DAY_MS = 24 * 60 * 60 * 1000;
@ -95,6 +112,18 @@ export async function fetchTickets(ctx: QueryCtx, tenantId: string) {
.collect();
}
async function fetchCategoryMap(ctx: QueryCtx, tenantId: string) {
const categories = await ctx.db
.query("ticketCategories")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.collect();
const map = new Map<string, Doc<"ticketCategories">>();
for (const category of categories) {
map.set(String(category._id), category);
}
return map;
}
export async function fetchScopedTickets(
ctx: QueryCtx,
tenantId: string,
@ -375,6 +404,7 @@ export async function slaOverviewHandler(
const startMs = endMs - days * ONE_DAY_MS;
const inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs);
const queues = await fetchQueues(ctx, tenantId);
const categoriesMap = await fetchCategoryMap(ctx, tenantId);
const now = Date.now();
const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
@ -400,6 +430,53 @@ export async function slaOverviewHandler(
};
});
const categoryStats = new Map<
string,
{
categoryId: string | null
categoryName: string
priority: string
total: number
responseMet: number
solutionMet: number
}
>()
for (const ticket of inRange) {
const snapshot = (ticket.slaSnapshot ?? null) as { categoryId?: Id<"ticketCategories">; categoryName?: string; priority?: string } | null
const rawCategoryId = ticket.categoryId ? String(ticket.categoryId) : snapshot?.categoryId ? String(snapshot.categoryId) : null
const categoryName = resolveCategoryName(rawCategoryId, snapshot, categoriesMap)
const priority = (snapshot?.priority ?? ticket.priority ?? "MEDIUM").toUpperCase()
const key = `${rawCategoryId ?? "uncategorized"}::${priority}`
let stat = categoryStats.get(key)
if (!stat) {
stat = {
categoryId: rawCategoryId,
categoryName,
priority,
total: 0,
responseMet: 0,
solutionMet: 0,
}
categoryStats.set(key, stat)
}
stat.total += 1
if (ticket.slaResponseStatus === "met") {
stat.responseMet += 1
}
if (ticket.slaSolutionStatus === "met") {
stat.solutionMet += 1
}
}
const categoryBreakdown = Array.from(categoryStats.values())
.map((entry) => ({
...entry,
responseRate: entry.total > 0 ? entry.responseMet / entry.total : null,
solutionRate: entry.total > 0 ? entry.solutionMet / entry.total : null,
}))
.sort((a, b) => b.total - a.total)
return {
totals: {
total: inRange.length,
@ -416,6 +493,7 @@ export async function slaOverviewHandler(
resolvedCount: resolutionTimes.length,
},
queueBreakdown,
categoryBreakdown,
rangeDays: days,
};
}