chore: sync staging

This commit is contained in:
Esdras Renan 2025-11-10 01:57:45 -03:00
parent c5ddd54a3e
commit 561b19cf66
610 changed files with 105285 additions and 1206 deletions

View file

@ -0,0 +1,346 @@
"use server"
import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { env } from "@/lib/env"
import { buildXlsxWorkbook } from "@/lib/xlsx"
import { REPORT_EXPORT_DEFINITIONS, type ReportExportKey } from "@/lib/report-definitions"
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<ConvexReportContext> {
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
throw new Error("Convex URL não configurada para exportações")
}
const client = new ConvexHttpClient(convexUrl)
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: Uint8Array
}
type BaseOptions = {
range?: string
companyId?: string
}
export async function buildHoursWorkbook(
ctx: ConvexReportContext,
options: BaseOptions & { search?: string }
): Promise<ReportArtifact> {
const report = await ctx.client.query(api.reports.hoursByClient, {
tenantId: ctx.tenantId,
viewerId: ctx.viewerId,
range: options.range,
})
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<Array<unknown>> = [
["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`
return {
fileName,
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buffer: new Uint8Array(workbook),
}
}
export async function buildBacklogWorkbook(
ctx: ConvexReportContext,
options: BaseOptions
): Promise<ReportArtifact> {
const report = await ctx.client.query(api.reports.backlogOverview, {
tenantId: ctx.tenantId,
viewerId: ctx.viewerId,
range: options.range,
companyId: options.companyId as Id<"companies"> | undefined,
})
const summaryRows: Array<Array<unknown>> = [
["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<string, string> = {
PENDING: "Pendentes",
AWAITING_ATTENDANCE: "Em andamento",
PAUSED: "Pausados",
RESOLVED: "Resolvidos",
}
const PRIORITY_PT: Record<string, string> = {
LOW: "Baixa",
MEDIUM: "Média",
HIGH: "Alta",
URGENT: "Crítica",
}
const distributionRows: Array<Array<unknown>> = []
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`
return {
fileName,
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buffer: new Uint8Array(workbook),
}
}
export async function buildSlaWorkbook(
ctx: ConvexReportContext,
options: BaseOptions
): Promise<ReportArtifact> {
const report = await ctx.client.query(api.reports.slaOverview, {
tenantId: ctx.tenantId,
viewerId: ctx.viewerId,
range: options.range,
companyId: options.companyId as Id<"companies"> | undefined,
})
const summaryRows: Array<Array<unknown>> = [
["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`
return {
fileName,
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buffer: new Uint8Array(workbook),
}
}
export async function buildCsatWorkbook(
ctx: ConvexReportContext,
options: BaseOptions
): Promise<ReportArtifact> {
const report = await ctx.client.query(api.reports.csatOverview, {
tenantId: ctx.tenantId,
viewerId: ctx.viewerId,
range: options.range,
companyId: options.companyId as Id<"companies"> | undefined,
})
const summaryRows: Array<Array<unknown>> = [
["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`
return {
fileName,
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buffer: new Uint8Array(workbook),
}
}
export async function buildTicketsByChannelWorkbook(
ctx: ConvexReportContext,
options: BaseOptions
): Promise<ReportArtifact> {
const report = await ctx.client.query(api.reports.ticketsByChannel, {
tenantId: ctx.tenantId,
viewerId: ctx.viewerId,
range: options.range,
companyId: options.companyId as Id<"companies"> | undefined,
})
const CHANNEL_PT: Record<string, string> = {
EMAIL: "E-mail",
PHONE: "Telefone",
CHAT: "Chat",
WHATSAPP: "WhatsApp",
API: "API",
MANUAL: "Manual",
WEB: "Portal",
PORTAL: "Portal",
}
const summaryRows: Array<Array<unknown>> = [
["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`
return {
fileName,
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buffer: new Uint8Array(workbook),
}
}