Rename menus: 'Acessos', 'Filas', 'Produtividade'; add agent productivity section with bar chart; adjust CSV label; update channels page title

This commit is contained in:
codex-bot 2025-10-21 13:17:41 -03:00
parent 347609a186
commit 67df0d4308
8 changed files with 150 additions and 13 deletions

View file

@ -330,6 +330,89 @@ export const backlogOverview = query({
},
});
export const agentProductivity = query({
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
handler: async (ctx, { tenantId, viewerId, range, companyId }) => {
const viewer = await requireStaff(ctx, viewerId, tenantId)
let tickets = await fetchScopedTickets(ctx, tenantId, viewer)
if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90
const end = new Date()
end.setUTCHours(0, 0, 0, 0)
const endMs = end.getTime() + ONE_DAY_MS
const startMs = endMs - days * ONE_DAY_MS
const inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs)
type Acc = {
agentId: Id<"users">
name: string | null
email: string | null
open: number
resolved: number
avgFirstResponseMinValues: number[]
avgResolutionMinValues: number[]
workedMs: number
}
const map = new Map<string, Acc>()
for (const t of inRange) {
const assigneeId = t.assigneeId ?? null
if (!assigneeId) continue
let acc = map.get(assigneeId)
if (!acc) {
const user = await ctx.db.get(assigneeId)
acc = {
agentId: assigneeId,
name: user?.name ?? null,
email: user?.email ?? null,
open: 0,
resolved: 0,
avgFirstResponseMinValues: [],
avgResolutionMinValues: [],
workedMs: 0,
}
map.set(assigneeId, acc)
}
const status = normalizeStatus(t.status)
if (OPEN_STATUSES.has(status)) acc.open += 1
if (status === 'RESOLVED') acc.resolved += 1
if (t.firstResponseAt) acc.avgFirstResponseMinValues.push((t.firstResponseAt - t.createdAt) / 60000)
if (t.resolvedAt) acc.avgResolutionMinValues.push((t.resolvedAt - t.createdAt) / 60000)
}
// Sum work sessions by agent
for (const [agentId, acc] of map) {
const sessions = await ctx.db
.query('ticketWorkSessions')
.withIndex('by_agent', (q) => q.eq('agentId', agentId as Id<'users'>))
.collect()
let total = 0
for (const s of sessions) {
const started = s.startedAt
const ended = s.stoppedAt ?? s.startedAt
if (ended < startMs || started >= endMs) continue
total += s.durationMs ?? Math.max(0, (s.stoppedAt ?? Date.now()) - s.startedAt)
}
acc.workedMs = total
}
const items = Array.from(map.values()).map((acc) => ({
agentId: acc.agentId,
name: acc.name,
email: acc.email,
open: acc.open,
resolved: acc.resolved,
avgFirstResponseMinutes: average(acc.avgFirstResponseMinValues),
avgResolutionMinutes: average(acc.avgResolutionMinValues),
workedHours: Math.round((acc.workedMs / 3600000) * 100) / 100,
}))
// sort by resolved desc
items.sort((a, b) => b.resolved - a.resolved)
return { rangeDays: days, items }
}
})
export const dashboardOverview = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {

View file

@ -205,7 +205,8 @@ export default defineSchema({
pauseNote: v.optional(v.string()),
})
.index("by_ticket", ["ticketId"])
.index("by_ticket_agent", ["ticketId", "agentId"]),
.index("by_ticket_agent", ["ticketId", "agentId"])
.index("by_agent", ["agentId"]),
ticketCategories: defineTable({
tenantId: v.string(),