sistema-de-chamados/src/app/api/admin/alerts/hours-usage/route.ts
2025-10-16 21:39:43 -03:00

129 lines
4.5 KiB
TypeScript

import { NextResponse } from "next/server"
import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { assertAdminSession } from "@/lib/auth-server"
import { env } from "@/lib/env"
import { prisma } from "@/lib/prisma"
import { sendSmtpMail } from "@/server/email-smtp"
export const runtime = "nodejs"
function fmtHours(ms: number) {
return (ms / 3600000).toFixed(2)
}
export async function POST(request: Request) {
const session = await assertAdminSession()
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
if (!env.SMTP) return NextResponse.json({ error: "SMTP não configurado" }, { status: 500 })
const { searchParams } = new URL(request.url)
const range = searchParams.get("range") ?? "30d"
const threshold = Number(searchParams.get("threshold") ?? 90)
const client = new ConvexHttpClient(convexUrl)
const tenantId = session.user.tenantId ?? "tenant-atlas"
// Ensure user exists in Convex to obtain a typed viewerId
let viewerId: Id<"users"> | null = null
try {
const ensured = await client.mutation(api.users.ensureUser, {
tenantId,
name: session.user.name ?? session.user.email,
email: session.user.email,
avatarUrl: session.user.avatarUrl ?? undefined,
role: session.user.role.toUpperCase(),
})
viewerId = (ensured?._id ?? null) as Id<"users"> | null
} catch (error) {
console.error("Failed to synchronize user with Convex for alerts", error)
return NextResponse.json({ error: "Falha ao sincronizar usuário com Convex" }, { status: 500 })
}
if (!viewerId) return NextResponse.json({ error: "Usuário não encontrado no Convex" }, { status: 403 })
const report = await client.query(api.reports.hoursByClient, {
tenantId,
viewerId,
range,
})
type HoursByClientItem = {
companyId: Id<"companies">
name: string
internalMs: number
externalMs: number
totalMs: number
contractedHoursPerMonth: number | null
}
const items = (report.items ?? []) as HoursByClientItem[]
const alerts = items.filter((i) => i.contractedHoursPerMonth != null && (i.totalMs / 3600000) / (i.contractedHoursPerMonth || 1) * 100 >= threshold)
type ManagerUser = { email: string; name: string | null }
for (const item of alerts) {
// Find managers of the company in Prisma
const managers: ManagerUser[] = await prisma.user.findMany({
where: {
tenantId,
companyId: item.companyId,
role: "MANAGER",
},
select: { email: true, name: true },
})
if (managers.length === 0) continue
const subject = `Alerta: uso de horas em ${item.name} acima de ${threshold}%`
const body = `
<p>Olá,</p>
<p>O uso de horas contratadas para <strong>${item.name}</strong> atingiu <strong>${(((item.totalMs/3600000)/(item.contractedHoursPerMonth || 1))*100).toFixed(1)}%</strong>.</p>
<ul>
<li>Horas internas: <strong>${fmtHours(item.internalMs)}</strong></li>
<li>Horas externas: <strong>${fmtHours(item.externalMs)}</strong></li>
<li>Total: <strong>${fmtHours(item.totalMs)}</strong></li>
<li>Contratadas/mês: <strong>${item.contractedHoursPerMonth}</strong></li>
</ul>
<p>Reveja a alocação da equipe e, se necessário, ajuste o atendimento.</p>
`
let delivered = 0
for (const m of managers) {
try {
await sendSmtpMail(
{
host: env.SMTP!.host,
port: env.SMTP!.port,
username: env.SMTP!.username,
password: env.SMTP!.password,
from: env.SMTP!.from!,
},
m.email,
subject,
body
)
delivered += 1
} catch (error) {
console.error("Failed to send alert to", m.email, error)
}
}
try {
await client.mutation(api.alerts.log, {
tenantId,
companyId: item.companyId,
companyName: item.name,
usagePct: (((item.totalMs/3600000)/(item.contractedHoursPerMonth || 1))*100),
threshold,
range,
recipients: managers.map((m) => m.email),
deliveredCount: delivered,
})
} catch (error) {
console.error("Failed to log alert in Convex", error)
}
}
return NextResponse.json({ sent: alerts.length })
}