550 lines
19 KiB
TypeScript
550 lines
19 KiB
TypeScript
"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<ConvexReportContext> {
|
|
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<ReportArtifact> {
|
|
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<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`
|
|
|
|
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<ReportArtifact> {
|
|
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<Array<unknown>> = [
|
|
["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<ReportArtifact> {
|
|
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<Array<unknown>> = [
|
|
["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<string> }
|
|
>()
|
|
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<string>(),
|
|
})
|
|
}
|
|
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<string>()
|
|
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<ReportArtifact> {
|
|
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<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`
|
|
|
|
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<ReportArtifact> {
|
|
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<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`
|
|
|
|
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<ReportArtifact> {
|
|
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<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`
|
|
|
|
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<ReportArtifact> {
|
|
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<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`
|
|
|
|
const arrayBuffer = workbook.buffer.slice(workbook.byteOffset, workbook.byteOffset + workbook.byteLength) as ArrayBuffer
|
|
return {
|
|
fileName,
|
|
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
buffer: arrayBuffer,
|
|
}
|
|
}
|