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"> { const base: Record = { _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"> { const base: Record = { _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>) 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; 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(agentA._id), agentA], [String(agentB._id), agentB], ]), ticketWorkSessionsByAgent: sessionsMap, }) as Parameters[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>) 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[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[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 }), ]) ) }) })