chore: expand reports coverage and upgrade next
This commit is contained in:
parent
2fb587b01d
commit
8b82284e8c
21 changed files with 2952 additions and 2713 deletions
267
tests/reports.productivity-dashboard.test.ts
Normal file
267
tests/reports.productivity-dashboard.test.ts
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"
|
||||
|
||||
vi.mock("../convex/rbac", () => ({
|
||||
requireStaff: vi.fn(),
|
||||
}))
|
||||
|
||||
import type { Doc, Id } from "../convex/_generated/dataModel"
|
||||
import {
|
||||
agentProductivityHandler,
|
||||
dashboardOverviewHandler,
|
||||
hoursByClientInternalHandler,
|
||||
} from "../convex/reports"
|
||||
import { requireStaff } from "../convex/rbac"
|
||||
import { createReportsCtx } from "./utils/report-test-helpers"
|
||||
|
||||
const TENANT_ID = "tenant-1"
|
||||
const VIEWER_ID = "user-agent" as Id<"users">
|
||||
|
||||
function buildTicket(overrides: Partial<Doc<"tickets">>): Doc<"tickets"> {
|
||||
const base: Record<string, unknown> = {
|
||||
_id: "ticket_base" as Id<"tickets">,
|
||||
tenantId: TENANT_ID,
|
||||
reference: 50000,
|
||||
subject: "Chamado",
|
||||
summary: null,
|
||||
status: "PENDING",
|
||||
priority: "MEDIUM",
|
||||
channel: "EMAIL",
|
||||
queueId: undefined,
|
||||
requesterId: "user_req" as Id<"users">,
|
||||
requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] },
|
||||
assigneeId: "user_assignee" as Id<"users">,
|
||||
assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] },
|
||||
companyId: undefined,
|
||||
companySnapshot: undefined,
|
||||
machineId: undefined,
|
||||
machineSnapshot: undefined,
|
||||
working: false,
|
||||
dueAt: undefined,
|
||||
firstResponseAt: undefined,
|
||||
resolvedAt: undefined,
|
||||
closedAt: undefined,
|
||||
updatedAt: Date.now(),
|
||||
createdAt: Date.now(),
|
||||
tags: [],
|
||||
customFields: [],
|
||||
totalWorkedMs: 0,
|
||||
internalWorkedMs: 0,
|
||||
externalWorkedMs: 0,
|
||||
activeSessionId: undefined,
|
||||
}
|
||||
return { ...(base as Doc<"tickets">), ...overrides }
|
||||
}
|
||||
|
||||
function buildCompany(overrides: Partial<Doc<"companies">>): Doc<"companies"> {
|
||||
const base: Record<string, unknown> = {
|
||||
_id: "company_base" as Id<"companies">,
|
||||
tenantId: TENANT_ID,
|
||||
name: "Empresa",
|
||||
slug: "empresa",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
isAvulso: false,
|
||||
contractedHoursPerMonth: 40,
|
||||
}
|
||||
return { ...(base as Doc<"companies">), ...overrides }
|
||||
}
|
||||
|
||||
describe("convex.reports.agentProductivity", () => {
|
||||
const requireStaffMock = vi.mocked(requireStaff)
|
||||
const FIXED_NOW = Date.UTC(2024, 6, 10, 12, 0, 0)
|
||||
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(FIXED_NOW)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it("aggregates per-agent metrics including work sessions", async () => {
|
||||
requireStaffMock.mockResolvedValue({
|
||||
role: "ADMIN",
|
||||
user: { companyId: undefined },
|
||||
} as unknown as Awaited<ReturnType<typeof requireStaff>>)
|
||||
|
||||
const agentA = { _id: "agent_a" as Id<"users">, name: "Ana", email: "ana@example.com" } as Doc<"users">
|
||||
const agentB = { _id: "agent_b" as Id<"users">, name: "Bruno", email: "bruno@example.com" } as Doc<"users">
|
||||
|
||||
const tickets = [
|
||||
buildTicket({
|
||||
_id: "ticket_open" as Id<"tickets">,
|
||||
assigneeId: agentA._id,
|
||||
createdAt: Date.UTC(2024, 6, 9, 9, 0, 0),
|
||||
status: "PENDING",
|
||||
firstResponseAt: Date.UTC(2024, 6, 9, 9, 30, 0),
|
||||
internalWorkedMs: 45 * 60 * 1000,
|
||||
}),
|
||||
buildTicket({
|
||||
_id: "ticket_resolved" as Id<"tickets">,
|
||||
assigneeId: agentA._id,
|
||||
createdAt: Date.UTC(2024, 6, 8, 10, 0, 0),
|
||||
firstResponseAt: Date.UTC(2024, 6, 8, 10, 20, 0),
|
||||
resolvedAt: Date.UTC(2024, 6, 8, 12, 0, 0),
|
||||
status: "RESOLVED",
|
||||
}),
|
||||
buildTicket({
|
||||
_id: "ticket_old" as Id<"tickets">,
|
||||
assigneeId: agentB._id,
|
||||
createdAt: Date.UTC(2024, 5, 20, 10, 0, 0),
|
||||
status: "RESOLVED",
|
||||
}),
|
||||
]
|
||||
|
||||
const sessionsMap = new Map<string, Array<{ agentId: Id<"users">; startedAt: number; stoppedAt?: number; durationMs?: number }>>([
|
||||
[
|
||||
String(agentA._id),
|
||||
[
|
||||
{ agentId: agentA._id, startedAt: Date.UTC(2024, 6, 9, 9, 0, 0), stoppedAt: Date.UTC(2024, 6, 9, 10, 0, 0) },
|
||||
{ agentId: agentA._id, startedAt: Date.UTC(2024, 6, 8, 11, 0, 0), durationMs: 30 * 60 * 1000 },
|
||||
],
|
||||
],
|
||||
])
|
||||
|
||||
const ctx = createReportsCtx({
|
||||
tickets,
|
||||
users: new Map<string, Doc<"users">>([
|
||||
[String(agentA._id), agentA],
|
||||
[String(agentB._id), agentB],
|
||||
]),
|
||||
ticketWorkSessionsByAgent: sessionsMap,
|
||||
}) as Parameters<typeof agentProductivityHandler>[0]
|
||||
|
||||
const result = await agentProductivityHandler(ctx, {
|
||||
tenantId: TENANT_ID,
|
||||
viewerId: VIEWER_ID,
|
||||
range: "7d",
|
||||
})
|
||||
|
||||
expect(result.rangeDays).toBe(7)
|
||||
expect(result.items).toHaveLength(1)
|
||||
expect(result.items[0]).toMatchObject({
|
||||
agentId: agentA._id,
|
||||
open: 1,
|
||||
resolved: 1,
|
||||
avgFirstResponseMinutes: 25,
|
||||
})
|
||||
expect(result.items[0]?.workedHours).toBeCloseTo(1.5, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("convex.reports.dashboardOverview", () => {
|
||||
const requireStaffMock = vi.mocked(requireStaff)
|
||||
const FIXED_NOW = Date.UTC(2024, 6, 15, 12, 0, 0)
|
||||
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(FIXED_NOW)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it("returns trend metrics for new, in-progress and resolution data", async () => {
|
||||
requireStaffMock.mockResolvedValue({
|
||||
role: "ADMIN",
|
||||
user: { companyId: undefined },
|
||||
} as unknown as Awaited<ReturnType<typeof requireStaff>>)
|
||||
|
||||
const tickets = [
|
||||
buildTicket({
|
||||
_id: "ticket_new" as Id<"tickets">,
|
||||
createdAt: Date.UTC(2024, 6, 15, 8, 0, 0),
|
||||
firstResponseAt: Date.UTC(2024, 6, 15, 8, 30, 0),
|
||||
status: "PENDING",
|
||||
}),
|
||||
buildTicket({
|
||||
_id: "ticket_resolved_recent" as Id<"tickets">,
|
||||
createdAt: Date.UTC(2024, 6, 8, 9, 0, 0),
|
||||
firstResponseAt: Date.UTC(2024, 6, 8, 9, 15, 0),
|
||||
resolvedAt: Date.UTC(2024, 6, 13, 12, 0, 0),
|
||||
status: "RESOLVED",
|
||||
}),
|
||||
buildTicket({
|
||||
_id: "ticket_prev" as Id<"tickets">,
|
||||
createdAt: Date.UTC(2024, 6, 13, 9, 0, 0),
|
||||
firstResponseAt: Date.UTC(2024, 6, 13, 9, 45, 0),
|
||||
status: "PAUSED",
|
||||
dueAt: Date.UTC(2024, 6, 14, 9, 0, 0),
|
||||
}),
|
||||
buildTicket({
|
||||
_id: "ticket_prev_resolved" as Id<"tickets">,
|
||||
createdAt: Date.UTC(2024, 6, 5, 9, 0, 0),
|
||||
firstResponseAt: Date.UTC(2024, 6, 5, 9, 10, 0),
|
||||
resolvedAt: Date.UTC(2024, 6, 7, 10, 0, 0),
|
||||
status: "RESOLVED",
|
||||
}),
|
||||
]
|
||||
|
||||
const ctx = createReportsCtx({ tickets }) as Parameters<typeof dashboardOverviewHandler>[0]
|
||||
|
||||
const result = await dashboardOverviewHandler(ctx, {
|
||||
tenantId: TENANT_ID,
|
||||
viewerId: VIEWER_ID,
|
||||
})
|
||||
|
||||
expect(result.newTickets.last24h).toBe(1)
|
||||
expect(result.inProgress.current).toBe(2)
|
||||
expect(result.awaitingAction.total).toBe(2)
|
||||
expect(result.resolution.resolvedLast7d).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("convex.reports.hoursByClientInternal", () => {
|
||||
const FIXED_NOW = Date.UTC(2024, 7, 1, 12, 0, 0)
|
||||
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(FIXED_NOW)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it("sums internal and external hours per company", async () => {
|
||||
const companyA = buildCompany({ _id: "company_a" as Id<"companies">, name: "Empresa A" })
|
||||
const companyB = buildCompany({ _id: "company_b" as Id<"companies">, name: "Empresa B", isAvulso: true })
|
||||
|
||||
const tickets = [
|
||||
buildTicket({
|
||||
_id: "ticket_a" as Id<"tickets">,
|
||||
companyId: companyA._id,
|
||||
updatedAt: Date.UTC(2024, 6, 30, 10, 0, 0),
|
||||
internalWorkedMs: 2 * 3600000,
|
||||
externalWorkedMs: 3600000,
|
||||
}),
|
||||
buildTicket({
|
||||
_id: "ticket_b" as Id<"tickets">,
|
||||
companyId: companyB._id,
|
||||
updatedAt: Date.UTC(2024, 6, 29, 14, 0, 0),
|
||||
internalWorkedMs: 0,
|
||||
externalWorkedMs: 2 * 3600000,
|
||||
}),
|
||||
]
|
||||
|
||||
const ctx = createReportsCtx({
|
||||
tickets,
|
||||
companies: new Map([
|
||||
[String(companyA._id), companyA],
|
||||
[String(companyB._id), companyB],
|
||||
]),
|
||||
}) as Parameters<typeof hoursByClientInternalHandler>[0]
|
||||
|
||||
const result = await hoursByClientInternalHandler(ctx, { tenantId: TENANT_ID, range: "7d" })
|
||||
|
||||
expect(result.rangeDays).toBe(7)
|
||||
expect(result.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ companyId: companyA._id, internalMs: 2 * 3600000, externalMs: 3600000 }),
|
||||
expect.objectContaining({ companyId: companyB._id, internalMs: 0, externalMs: 2 * 3600000 }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue