244 lines
6.8 KiB
TypeScript
244 lines
6.8 KiB
TypeScript
"use server"
|
|
|
|
import type { Prisma as PrismaTypes } from "@/generated/prisma/client"
|
|
import { prisma } from "@/lib/prisma"
|
|
import { env } from "@/lib/env"
|
|
import { sendSmtpMail } from "@/server/email-smtp"
|
|
import {
|
|
buildBacklogWorkbook,
|
|
buildCsatWorkbook,
|
|
buildHoursWorkbook,
|
|
buildSlaWorkbook,
|
|
buildTicketsByChannelWorkbook,
|
|
createConvexContext,
|
|
type ConvexReportContext,
|
|
type ReportArtifact,
|
|
type ReportExportKey,
|
|
} from "@/server/report-exporters"
|
|
import {
|
|
computeNextRunAt,
|
|
sanitizeRecipients,
|
|
sanitizeReportKeys,
|
|
} from "@/server/report-schedule-service"
|
|
|
|
const GENERATORS: Record<
|
|
ReportExportKey,
|
|
(
|
|
ctx: ConvexReportContext,
|
|
options: { range?: string; companyId?: string | null }
|
|
) => Promise<ReportArtifact>
|
|
> = {
|
|
hours: buildHoursWorkbook,
|
|
backlog: buildBacklogWorkbook,
|
|
sla: buildSlaWorkbook,
|
|
csat: buildCsatWorkbook,
|
|
"tickets-by-channel": buildTicketsByChannelWorkbook,
|
|
}
|
|
|
|
type ScheduleRunnerIdentity = {
|
|
userId?: string
|
|
name?: string | null
|
|
email?: string | null
|
|
avatarUrl?: string | null
|
|
role?: string | null
|
|
}
|
|
|
|
export type RunReportSchedulesOptions = {
|
|
tenantId?: string
|
|
scheduleId?: string
|
|
initiatedBy?: ScheduleRunnerIdentity
|
|
now?: Date
|
|
}
|
|
|
|
export type RunReportSchedulesResult = {
|
|
processed: number
|
|
results: Array<{ id: string; status: "completed" | "failed" | "skipped"; error?: string }>
|
|
message?: string
|
|
}
|
|
|
|
export async function runReportSchedules(
|
|
options: RunReportSchedulesOptions = {}
|
|
): Promise<RunReportSchedulesResult> {
|
|
const now = options.now ?? new Date()
|
|
|
|
const where: PrismaTypes.ReportExportScheduleWhereInput = {
|
|
status: "ACTIVE",
|
|
}
|
|
if (options.tenantId) {
|
|
where.tenantId = options.tenantId
|
|
}
|
|
if (options.scheduleId) {
|
|
where.id = options.scheduleId
|
|
} else {
|
|
where.nextRunAt = { lte: now }
|
|
}
|
|
|
|
const schedules = await prisma.reportExportSchedule.findMany({
|
|
where,
|
|
orderBy: { nextRunAt: "asc" },
|
|
})
|
|
|
|
if (schedules.length === 0) {
|
|
return {
|
|
processed: 0,
|
|
results: [],
|
|
message: options.scheduleId
|
|
? "Agendamento não encontrado ou inativo."
|
|
: "Nenhum agendamento pendente.",
|
|
}
|
|
}
|
|
|
|
const fallbackEmail =
|
|
options.initiatedBy?.email ??
|
|
env.SMTP?.from ??
|
|
process.env.REPORTS_CRON_FALLBACK_EMAIL ??
|
|
"scheduler@sistema.local"
|
|
|
|
const results: RunReportSchedulesResult["results"] = []
|
|
|
|
for (const schedule of schedules) {
|
|
const reportKeys = sanitizeReportKeys((schedule.reportKeys as string[] | null) ?? [])
|
|
if (!reportKeys.length) {
|
|
results.push({ id: schedule.id, status: "skipped", error: "Nenhum relatório configurado." })
|
|
continue
|
|
}
|
|
|
|
const owner =
|
|
(await prisma.user.findUnique({
|
|
where: { id: schedule.createdBy },
|
|
select: { name: true, email: true, avatarUrl: true, role: true },
|
|
})) ?? null
|
|
|
|
let context: ConvexReportContext | null = null
|
|
try {
|
|
context = await createConvexContext({
|
|
tenantId: schedule.tenantId,
|
|
name: owner?.name ?? options.initiatedBy?.name ?? options.initiatedBy?.email ?? fallbackEmail,
|
|
email: owner?.email ?? fallbackEmail,
|
|
avatarUrl: owner?.avatarUrl ?? options.initiatedBy?.avatarUrl ?? undefined,
|
|
role: (owner?.role ?? options.initiatedBy?.role ?? "ADMIN").toUpperCase(),
|
|
})
|
|
} catch (error) {
|
|
results.push({
|
|
id: schedule.id,
|
|
status: "failed",
|
|
error: error instanceof Error ? error.message : "Falha ao inicializar contexto Convex",
|
|
})
|
|
continue
|
|
}
|
|
|
|
const run = await prisma.reportExportRun.create({
|
|
data: {
|
|
tenantId: schedule.tenantId,
|
|
scheduleId: schedule.id,
|
|
status: "RUNNING",
|
|
},
|
|
})
|
|
|
|
try {
|
|
const artifacts: Array<{ key: ReportExportKey; fileName: string; mimeType: string; data: string }> = []
|
|
for (const key of reportKeys) {
|
|
const generator = GENERATORS[key]
|
|
if (!generator) continue
|
|
const artifact = await generator(context, {
|
|
range: schedule.range,
|
|
companyId: schedule.companyId ?? undefined,
|
|
})
|
|
artifacts.push({
|
|
key,
|
|
fileName: artifact.fileName,
|
|
mimeType: artifact.mimeType,
|
|
data: Buffer.from(new Uint8Array(artifact.buffer)).toString("base64"),
|
|
})
|
|
}
|
|
|
|
await prisma.reportExportRun.update({
|
|
where: { id: run.id },
|
|
data: {
|
|
status: "COMPLETED",
|
|
completedAt: new Date(),
|
|
artifacts,
|
|
},
|
|
})
|
|
|
|
await prisma.reportExportSchedule.update({
|
|
where: { id: schedule.id },
|
|
data: {
|
|
lastRunAt: new Date(),
|
|
nextRunAt: computeNextRunAt(
|
|
{
|
|
frequency: schedule.frequency as "daily" | "weekly" | "monthly",
|
|
dayOfWeek: schedule.dayOfWeek,
|
|
dayOfMonth: schedule.dayOfMonth,
|
|
hour: schedule.hour,
|
|
minute: schedule.minute,
|
|
timezone: schedule.timezone,
|
|
},
|
|
now
|
|
),
|
|
},
|
|
})
|
|
|
|
const recipients = sanitizeRecipients((schedule.recipients as string[] | null) ?? [])
|
|
if (artifacts.length && recipients.length && env.SMTP) {
|
|
await notifyRecipients(run.id, schedule.name, artifacts, recipients)
|
|
}
|
|
|
|
results.push({ id: schedule.id, status: "completed" })
|
|
} catch (error) {
|
|
await prisma.reportExportRun.update({
|
|
where: { id: run.id },
|
|
data: {
|
|
status: "FAILED",
|
|
completedAt: new Date(),
|
|
error: error instanceof Error ? error.message : "Erro inesperado",
|
|
},
|
|
})
|
|
results.push({
|
|
id: schedule.id,
|
|
status: "failed",
|
|
error: error instanceof Error ? error.message : "Erro inesperado",
|
|
})
|
|
}
|
|
}
|
|
|
|
return {
|
|
processed: results.length,
|
|
results,
|
|
}
|
|
}
|
|
|
|
async function notifyRecipients(
|
|
runId: string,
|
|
scheduleName: string,
|
|
artifacts: Array<{ key: string; fileName: string; mimeType: string; data: string }>,
|
|
recipients: string[]
|
|
) {
|
|
if (!env.SMTP) return
|
|
const baseUrl =
|
|
env.REPORTS_CRON_BASE_URL ?? env.NEXT_PUBLIC_APP_URL ?? env.BETTER_AUTH_URL ?? "http://localhost:3000"
|
|
|
|
const links = artifacts
|
|
.map(
|
|
(artifact, index) =>
|
|
`${baseUrl.replace(/\/$/, "")}/api/reports/schedules/runs/${runId}?artifact=${index}`
|
|
)
|
|
.map(
|
|
(href, index) =>
|
|
`<li><a href="${href}">${artifacts[index].fileName}</a> (${artifacts[index].mimeType})</li>`
|
|
)
|
|
.join("")
|
|
|
|
const html = `
|
|
<p>Os relatórios agendados <strong>${scheduleName}</strong> foram gerados.</p>
|
|
<p>Arquivos disponíveis:</p>
|
|
<ul>${links}</ul>
|
|
<p>Este e-mail foi enviado automaticamente.</p>
|
|
`
|
|
|
|
try {
|
|
await sendSmtpMail(env.SMTP, recipients, `Relatórios agendados - ${scheduleName}`, html)
|
|
} catch (error) {
|
|
console.error("Falha ao enviar e-mail do agendamento", error)
|
|
}
|
|
}
|