"use server" import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { buildXlsxWorkbook, type WorksheetConfig } from "@/lib/xlsx" import { REPORT_EXPORT_DEFINITIONS, type ReportExportKey } from "@/lib/report-definitions" import { requireConvexUrl } from "@/server/convex-client" import type { DeviceInventoryColumnConfig } from "@/lib/device-inventory-columns" import { buildInventoryWorksheet, type MachineInventoryRecord } from "@/server/machines/inventory-export" export type { ReportExportKey } type ViewerIdentity = { tenantId: string name: string email: string avatarUrl?: string | null role: string } export type ConvexReportContext = { client: ConvexHttpClient tenantId: string viewerId: Id<"users"> } export async function createConvexContext(identity: ViewerIdentity): Promise { const client = new ConvexHttpClient(requireConvexUrl()) const ensuredUser = await client.mutation(api.users.ensureUser, { tenantId: identity.tenantId, name: identity.name, email: identity.email, avatarUrl: identity.avatarUrl ?? undefined, role: identity.role, }) if (!ensuredUser?._id) { throw new Error("Não foi possível sincronizar usuário com o Convex") } return { client, tenantId: identity.tenantId, viewerId: ensuredUser._id as Id<"users">, } } export type ReportArtifact = { fileName: string mimeType: string buffer: ArrayBuffer } type BaseOptions = { range?: string companyId?: string | null dateFrom?: string | null dateTo?: string | null } export async function buildHoursWorkbook( ctx: ConvexReportContext, options: BaseOptions & { search?: string } ): Promise { const report = await ctx.client.query(api.reports.hoursByClient, { tenantId: ctx.tenantId, viewerId: ctx.viewerId, range: options.range, dateFrom: options.dateFrom ?? undefined, dateTo: options.dateTo ?? undefined, }) type Item = { companyId: string name: string isAvulso: boolean internalMs: number externalMs: number totalMs: number contractedHoursPerMonth: number | null } let items = (report.items as Item[]) ?? [] if (options.companyId) { items = items.filter((item) => String(item.companyId) === options.companyId) } if (options.search) { const term = options.search.toLowerCase() items = items.filter((item) => item.name.toLowerCase().includes(term)) } const summaryRows: Array> = [ ["Relatório", "Horas por cliente"], ["Período", report.rangeDays ? `Últimos ${report.rangeDays} dias` : options.range ?? "90d"], ] if (options.search) summaryRows.push(["Filtro", options.search]) if (options.companyId) summaryRows.push(["EmpresaId", options.companyId]) summaryRows.push(["Total de clientes", items.length]) const dataRows = items.map((item) => { const internalHours = item.internalMs / 3_600_000 const externalHours = item.externalMs / 3_600_000 const totalHours = item.totalMs / 3_600_000 const contracted = item.contractedHoursPerMonth const usagePct = contracted ? (totalHours / contracted) * 100 : null return [ item.name, item.isAvulso ? "Sim" : "Não", Number(internalHours.toFixed(2)), Number(externalHours.toFixed(2)), Number(totalHours.toFixed(2)), contracted ?? null, usagePct !== null ? Number(usagePct.toFixed(1)) : null, ] }) const workbook = buildXlsxWorkbook([ { name: "Resumo", headers: ["Item", "Valor"], rows: summaryRows }, { name: "Clientes", headers: ["Cliente", "Avulso", "Horas internas", "Horas externas", "Horas totais", "Horas contratadas/mês", "% uso"], rows: dataRows.length > 0 ? dataRows : [["—", "—", 0, 0, 0, null, null]], }, ]) const fileName = `hours-by-client-${ctx.tenantId}-${report.rangeDays ?? "90"}d${ options.companyId ? `-${options.companyId}` : "" }${options.search ? `-${encodeURIComponent(options.search)}` : ""}.xlsx` const arrayBuffer = workbook.buffer.slice(workbook.byteOffset, workbook.byteOffset + workbook.byteLength) as ArrayBuffer return { fileName, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer: arrayBuffer, } } export async function buildCategoryInsightsWorkbook( ctx: ConvexReportContext, options: BaseOptions ): Promise { const report = await ctx.client.query(api.reports.categoryInsights, { tenantId: ctx.tenantId, viewerId: ctx.viewerId, range: options.range, companyId: (options.companyId ?? undefined) as Id<"companies"> | undefined, dateFrom: options.dateFrom ?? undefined, dateTo: options.dateTo ?? undefined, }) const categories = (report.categories ?? []) as Array<{ name: string total: number resolved: number topAgent: { name: string | null; total: number } | null }> const totalTickets = typeof report.totalTickets === "number" ? report.totalTickets : categories.reduce((acc, item) => acc + (item.total ?? 0), 0) const summaryRows: Array> = [ ["Relatório", "Categorias"], ["Período", report.rangeDays ? `Últimos ${report.rangeDays} dias` : options.range ?? "90d"], ["Total de tickets", totalTickets], ] if (options.companyId) summaryRows.push(["EmpresaId", options.companyId]) if (report.spotlight) { summaryRows.push(["Destaque", report.spotlight.categoryName]) summaryRows.push(["Tickets no destaque", report.spotlight.tickets ?? 0]) if (report.spotlight.agentName) { summaryRows.push(["Agente destaque", report.spotlight.agentName]) } } const categoryRows = categories.map((category) => { const resolvedRate = category.total > 0 ? (category.resolved / category.total) * 100 : null return [ category.name, category.total, category.resolved, resolvedRate === null ? null : Number(resolvedRate.toFixed(1)), category.topAgent?.name ?? "Sem responsável", category.topAgent?.total ?? 0, ] }) const workbook = buildXlsxWorkbook([ { name: "Resumo", headers: ["Item", "Valor"], rows: summaryRows }, { name: "Categorias", headers: ["Categoria", "Tickets", "Resolvidos", "% resolvidos", "Agente destaque", "Tickets agente"], rows: categoryRows.length > 0 ? categoryRows : [["—", 0, 0, null, "—", 0]], }, ]) const fileName = `category-insights-${ctx.tenantId}-${report.rangeDays ?? "90"}d${ options.companyId ? `-${options.companyId}` : "" }.xlsx` const arrayBuffer = workbook.buffer.slice(workbook.byteOffset, workbook.byteOffset + workbook.byteLength) as ArrayBuffer return { fileName, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer: arrayBuffer, } } export async function buildMachineCategoryWorkbook( ctx: ConvexReportContext, options: BaseOptions & { machineId?: string | null; userId?: string | null; columns?: DeviceInventoryColumnConfig[] } ): Promise { const response = await ctx.client.query(api.reports.ticketsByMachineAndCategory, { tenantId: ctx.tenantId, viewerId: ctx.viewerId, range: options.range, companyId: (options.companyId ?? undefined) as Id<"companies"> | undefined, machineId: options.machineId ? (options.machineId as Id<"machines">) : undefined, userId: options.userId ? (options.userId as Id<"users">) : undefined, dateFrom: options.dateFrom ?? undefined, dateTo: options.dateTo ?? undefined, }) as { rangeDays: number items: Array<{ date: string machineHostname: string | null machineId: string | null companyName: string | null categoryName: string total: number }> } const items = response.items ?? [] const summaryRows: Array> = [ ["Relatório", "Máquinas x categorias"], [ "Período", response.rangeDays && response.rangeDays > 0 ? `Últimos ${response.rangeDays} dias` : options.range ?? "30d", ], ["Total de registros", items.length], ] if (options.companyId) summaryRows.push(["EmpresaId", options.companyId]) if (options.machineId) summaryRows.push(["MáquinaId", options.machineId]) if (options.userId) summaryRows.push(["SolicitanteId", options.userId]) const machineAggregation = new Map< string, { machine: string; company: string; total: number; categories: Set } >() for (const item of items) { const key = item.machineId ?? item.machineHostname ?? "sem-maquina" if (!machineAggregation.has(key)) { machineAggregation.set(key, { machine: item.machineHostname ?? key, company: item.companyName ?? "—", total: 0, categories: new Set(), }) } const entry = machineAggregation.get(key)! entry.total += item.total entry.categories.add(item.categoryName) } const perMachineRows = Array.from(machineAggregation.values()) .sort((a, b) => b.total - a.total) .map((entry) => [entry.machine, entry.company, entry.total, Array.from(entry.categories).join(", ")]) const occurrencesRows = items.map((item) => [ item.date, item.machineHostname ?? "Sem identificação", item.companyName ?? "—", item.categoryName, item.total, ]) const sheets: WorksheetConfig[] = [ { name: "Resumo", headers: ["Item", "Valor"], rows: summaryRows }, { name: "Máquinas", headers: ["Máquina", "Empresa", "Tickets", "Categorias"], rows: perMachineRows.length > 0 ? perMachineRows : [["—", "—", 0, "—"]], }, { name: "Ocorrências", headers: ["Data", "Máquina", "Empresa", "Categoria", "Total"], rows: occurrencesRows.length > 0 ? occurrencesRows : [["—", "—", "—", "—", 0]], }, ] if (options.columns && options.columns.length > 0) { const machineIds = new Set() for (const item of items) { if (item.machineId) { machineIds.add(String(item.machineId)) } } if (machineIds.size > 0) { const machines = (await ctx.client.query(api.devices.listByTenant, { tenantId: ctx.tenantId, includeMetadata: true, })) as MachineInventoryRecord[] const filteredMachines = machines.filter((machine) => machineIds.has(String(machine.id))) if (filteredMachines.length > 0) { const inventorySheet = buildInventoryWorksheet(filteredMachines, options.columns, "Máquinas detalhadas") sheets.push(inventorySheet) } } } const workbook = buildXlsxWorkbook(sheets) const fileName = `machine-category-${ctx.tenantId}-${options.range ?? response.rangeDays ?? "30"}d${ options.companyId ? `-${options.companyId}` : "" }.xlsx` const arrayBuffer = workbook.buffer.slice(workbook.byteOffset, workbook.byteOffset + workbook.byteLength) as ArrayBuffer return { fileName, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer: arrayBuffer, } } export async function buildBacklogWorkbook( ctx: ConvexReportContext, options: BaseOptions ): Promise { const report = await ctx.client.query(api.reports.backlogOverview, { tenantId: ctx.tenantId, viewerId: ctx.viewerId, range: options.range, companyId: (options.companyId ?? undefined) as Id<"companies"> | undefined, dateFrom: options.dateFrom ?? undefined, dateTo: options.dateTo ?? undefined, }) const summaryRows: Array> = [ ["Relatório", "Backlog"], ["Período", report.rangeDays ? `Últimos ${report.rangeDays} dias` : options.range ?? "90d"], ] if (options.companyId) summaryRows.push(["EmpresaId", options.companyId]) summaryRows.push(["Chamados em aberto", report.totalOpen]) const STATUS_PT: Record = { PENDING: "Pendentes", AWAITING_ATTENDANCE: "Em andamento", PAUSED: "Pausados", RESOLVED: "Resolvidos", } const PRIORITY_PT: Record = { LOW: "Baixa", MEDIUM: "Média", HIGH: "Alta", URGENT: "Crítica", } const distributionRows: Array> = [] for (const [status, total] of Object.entries(report.statusCounts ?? {})) { distributionRows.push(["Status", STATUS_PT[status] ?? status, total]) } for (const [priority, total] of Object.entries(report.priorityCounts ?? {})) { distributionRows.push(["Prioridade", PRIORITY_PT[priority] ?? priority, total]) } for (const queue of report.queueCounts ?? []) { distributionRows.push(["Fila", queue.name || queue.id, queue.total]) } const workbook = buildXlsxWorkbook([ { name: "Resumo", headers: ["Item", "Valor"], rows: summaryRows }, { name: "Distribuições", headers: ["Categoria", "Chave", "Total"], rows: distributionRows.length > 0 ? distributionRows : [["—", "—", 0]], }, ]) const fileName = `backlog-${ctx.tenantId}-${report.rangeDays ?? "90"}d${ options.companyId ? `-${options.companyId}` : "" }.xlsx` const arrayBuffer = workbook.buffer.slice(workbook.byteOffset, workbook.byteOffset + workbook.byteLength) as ArrayBuffer return { fileName, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer: arrayBuffer, } } export async function buildSlaWorkbook( ctx: ConvexReportContext, options: BaseOptions ): Promise { const report = await ctx.client.query(api.reports.slaOverview, { tenantId: ctx.tenantId, viewerId: ctx.viewerId, range: options.range, companyId: (options.companyId ?? undefined) as Id<"companies"> | undefined, dateFrom: options.dateFrom ?? undefined, dateTo: options.dateTo ?? undefined, }) const summaryRows: Array> = [ ["Relatório", "Produtividade"], ["Período", report.rangeDays ? `Últimos ${report.rangeDays} dias` : options.range ?? "90d"], ] if (options.companyId) summaryRows.push(["EmpresaId", options.companyId]) summaryRows.push(["Tickets totais", report.totals.total]) summaryRows.push(["Tickets abertos", report.totals.open]) summaryRows.push(["Tickets resolvidos", report.totals.resolved]) summaryRows.push(["Atrasados (SLA)", report.totals.overdue]) summaryRows.push(["Tempo médio 1ª resposta (min)", report.response.averageFirstResponseMinutes ?? "—"]) summaryRows.push(["Respostas registradas", report.response.responsesRegistered ?? 0]) summaryRows.push(["Tempo médio resolução (min)", report.resolution.averageResolutionMinutes ?? "—"]) summaryRows.push(["Tickets resolvidos (amostra)", report.resolution.resolvedCount ?? 0]) const queueRows = (report.queueBreakdown ?? []).map((queue) => [queue.name || queue.id, queue.open]) const workbook = buildXlsxWorkbook([ { name: "Resumo", headers: ["Indicador", "Valor"], rows: summaryRows }, { name: "Filas", headers: ["Fila", "Chamados abertos"], rows: queueRows.length > 0 ? queueRows : [["—", 0]], }, ]) const fileName = `sla-${ctx.tenantId}-${report.rangeDays ?? "90"}d${ options.companyId ? `-${options.companyId}` : "" }.xlsx` const arrayBuffer = workbook.buffer.slice(workbook.byteOffset, workbook.byteOffset + workbook.byteLength) as ArrayBuffer return { fileName, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer: arrayBuffer, } } export async function buildCsatWorkbook( ctx: ConvexReportContext, options: BaseOptions ): Promise { const report = await ctx.client.query(api.reports.csatOverview, { tenantId: ctx.tenantId, viewerId: ctx.viewerId, range: options.range, companyId: (options.companyId ?? undefined) as Id<"companies"> | undefined, dateFrom: options.dateFrom ?? undefined, dateTo: options.dateTo ?? undefined, }) const summaryRows: Array> = [ ["Relatório", "CSAT"], ["Período", report.rangeDays ? `Últimos ${report.rangeDays} dias` : options.range ?? "90d"], ] if (options.companyId) summaryRows.push(["EmpresaId", options.companyId]) summaryRows.push(["CSAT médio", report.averageScore ?? "—"]) summaryRows.push(["Total de respostas", report.totalSurveys ?? 0]) const distributionRows = (report.distribution ?? []).map((entry) => [entry.score, entry.total]) const recentRows = (report.recent ?? []).map((item) => [ `#${item.reference}`, item.score, new Date(item.receivedAt).toISOString(), ]) const workbook = buildXlsxWorkbook([ { name: "Resumo", headers: ["Métrica", "Valor"], rows: summaryRows }, { name: "Distribuição", headers: ["Nota", "Total"], rows: distributionRows.length > 0 ? distributionRows : [["—", 0]], }, { name: "Respostas recentes", headers: ["Ticket", "Nota", "Recebido em"], rows: recentRows.length > 0 ? recentRows : [["—", "—", "—"]], }, ]) const fileName = `csat-${ctx.tenantId}-${report.rangeDays ?? "90"}d${ options.companyId ? `-${options.companyId}` : "" }.xlsx` const arrayBuffer = workbook.buffer.slice(workbook.byteOffset, workbook.byteOffset + workbook.byteLength) as ArrayBuffer return { fileName, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer: arrayBuffer, } } export async function buildTicketsByChannelWorkbook( ctx: ConvexReportContext, options: BaseOptions ): Promise { const report = await ctx.client.query(api.reports.ticketsByChannel, { tenantId: ctx.tenantId, viewerId: ctx.viewerId, range: options.range, companyId: (options.companyId ?? undefined) as Id<"companies"> | undefined, }) const CHANNEL_PT: Record = { EMAIL: "E-mail", PHONE: "Telefone", CHAT: "Chat", WHATSAPP: "WhatsApp", API: "API", MANUAL: "Manual", WEB: "Portal", PORTAL: "Portal", } const summaryRows: Array> = [ ["Relatório", "Tickets por canal"], ["Período", report.rangeDays ? `Últimos ${report.rangeDays} dias` : options.range ?? "90d"], ] if (options.companyId) summaryRows.push(["EmpresaId", options.companyId]) summaryRows.push(["Total de linhas", report.points.length]) const header = ["Data", ...(report.channels ?? []).map((ch) => CHANNEL_PT[ch] ?? ch)] const dataRows = (report.points ?? []).map((point) => [ point.date, ...(report.channels ?? []).map((ch) => point.values[ch] ?? 0), ]) const workbook = buildXlsxWorkbook([ { name: "Resumo", headers: ["Item", "Valor"], rows: summaryRows }, { name: "Distribuição", headers: header, rows: dataRows.length > 0 ? dataRows : [[new Date().toISOString().slice(0, 10), ...(report.channels ?? []).map(() => 0)]], }, ]) const fileName = `tickets-by-channel-${ctx.tenantId}-${options.range ?? "90d"}${ options.companyId ? `-${options.companyId}` : "" }.xlsx` const arrayBuffer = workbook.buffer.slice(workbook.byteOffset, workbook.byteOffset + workbook.byteLength) as ArrayBuffer return { fileName, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buffer: arrayBuffer, } }