feat: add SLA category breakdown report
This commit is contained in:
parent
6ab8a6ce89
commit
a62f3d5283
8 changed files with 231 additions and 10 deletions
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue