feat: automações com envio de e-mail

This commit is contained in:
esdrasrenan 2025-12-13 12:38:08 -03:00
parent 469608a10b
commit 58a1ed6b36
6 changed files with 958 additions and 49 deletions

268
convex/emailTemplates.ts Normal file
View file

@ -0,0 +1,268 @@
const COLORS = {
primary: "#00e8ff",
primaryDark: "#00c4d6",
primaryForeground: "#020617",
background: "#f8fafc",
card: "#ffffff",
border: "#e2e8f0",
textPrimary: "#0f172a",
textSecondary: "#334155",
textMuted: "#64748b",
statusPending: "#64748b",
statusPendingBg: "#f1f5f9",
statusProgress: "#0ea5e9",
statusProgressBg: "#e0f2fe",
statusPaused: "#f59e0b",
statusPausedBg: "#fef3c7",
statusResolved: "#10b981",
statusResolvedBg: "#d1fae5",
priorityLow: "#64748b",
priorityLowBg: "#f1f5f9",
priorityMedium: "#0a4760",
priorityMediumBg: "#dff1fb",
priorityHigh: "#7d3b05",
priorityHighBg: "#fde8d1",
priorityUrgent: "#8b0f1c",
priorityUrgentBg: "#fbd9dd",
}
export type EmailTicketSummary = {
reference: number
subject: string
status?: string | null
priority?: string | null
companyName?: string | null
requesterName?: string | null
assigneeName?: string | null
}
function escapeHtml(value: unknown): string {
if (value === null || value === undefined) return ""
const s = String(value)
return s
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
}
function textToHtmlParagraphs(text: string): string {
const trimmed = text.replace(/\r\n/g, "\n").trim()
if (!trimmed) return ""
const paragraphs = trimmed.split(/\n{2,}/g)
return paragraphs
.map((p) => `<p style="font-size:14px;line-height:1.65;margin:0 0 12px 0;color:${COLORS.textSecondary};">${escapeHtml(p).replace(/\n/g, "<br/>")}</p>`)
.join("")
}
function badge(label: string, bg: string, color: string) {
return `<span style="display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:600;background:${bg};color:${color};white-space:nowrap;">${escapeHtml(label)}</span>`
}
function statusBadge(statusRaw: string) {
const status = statusRaw.trim().toUpperCase()
const map: Record<string, { label: string; bg: string; color: string }> = {
PENDING: { label: "Pendente", bg: COLORS.statusPendingBg, color: COLORS.statusPending },
AWAITING_ATTENDANCE: { label: "Em andamento", bg: COLORS.statusProgressBg, color: COLORS.statusProgress },
PAUSED: { label: "Pausado", bg: COLORS.statusPausedBg, color: COLORS.statusPaused },
RESOLVED: { label: "Resolvido", bg: COLORS.statusResolvedBg, color: COLORS.statusResolved },
}
const entry = map[status] ?? { label: statusRaw, bg: COLORS.statusPendingBg, color: COLORS.statusPending }
return badge(entry.label, entry.bg, entry.color)
}
function priorityBadge(priorityRaw: string) {
const priority = priorityRaw.trim().toUpperCase()
const map: Record<string, { label: string; bg: string; color: string }> = {
LOW: { label: "Baixa", bg: COLORS.priorityLowBg, color: COLORS.priorityLow },
MEDIUM: { label: "Média", bg: COLORS.priorityMediumBg, color: COLORS.priorityMedium },
HIGH: { label: "Alta", bg: COLORS.priorityHighBg, color: COLORS.priorityHigh },
URGENT: { label: "Urgente", bg: COLORS.priorityUrgentBg, color: COLORS.priorityUrgent },
}
const entry = map[priority] ?? { label: priorityRaw, bg: COLORS.priorityMediumBg, color: COLORS.priorityMedium }
return badge(entry.label, entry.bg, entry.color)
}
export function buildBaseUrl() {
return process.env.NEXT_PUBLIC_APP_URL || process.env.APP_BASE_URL || "http://localhost:3000"
}
function ticketInfoCard(ticket: EmailTicketSummary) {
const rows: string[] = []
rows.push(`
<tr>
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 10px 4px 0;vertical-align:top;">Chamado</td>
<td style="color:${COLORS.textPrimary};font-size:14px;font-weight:700;padding:4px 0;">#${escapeHtml(ticket.reference)}</td>
</tr>
`)
rows.push(`
<tr>
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 10px 4px 0;vertical-align:top;">Assunto</td>
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(ticket.subject)}</td>
</tr>
`)
if (ticket.companyName) {
rows.push(`
<tr>
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 10px 4px 0;vertical-align:top;">Empresa</td>
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(ticket.companyName)}</td>
</tr>
`)
}
if (ticket.status) {
rows.push(`
<tr>
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 10px 4px 0;vertical-align:top;">Status</td>
<td style="padding:4px 0;">${statusBadge(ticket.status)}</td>
</tr>
`)
}
if (ticket.priority) {
rows.push(`
<tr>
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 10px 4px 0;vertical-align:top;">Prioridade</td>
<td style="padding:4px 0;">${priorityBadge(ticket.priority)}</td>
</tr>
`)
}
if (ticket.requesterName) {
rows.push(`
<tr>
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 10px 4px 0;vertical-align:top;">Solicitante</td>
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(ticket.requesterName)}</td>
</tr>
`)
}
if (ticket.assigneeName) {
rows.push(`
<tr>
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 10px 4px 0;vertical-align:top;">Responsável</td>
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(ticket.assigneeName)}</td>
</tr>
`)
}
return `
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:${COLORS.background};border-radius:10px;margin:18px 0 0 0;">
<tr>
<td style="padding:16px;">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
${rows.join("")}
</table>
</td>
</tr>
</table>
`
}
function button(label: string, url: string) {
return `<a href="${escapeHtml(url)}" style="display:inline-block;background:${COLORS.primary};color:${COLORS.primaryForeground};text-decoration:none;border-radius:12px;padding:12px 18px;font-weight:800;font-size:14px;border:1px solid ${COLORS.primaryDark};">${escapeHtml(label)}</a>`
}
export function renderAutomationEmail(params: {
title: string
message: string
ticket: EmailTicketSummary
ctaLabel: string
ctaUrl: string
}) {
const baseUrl = buildBaseUrl()
const content = `
<table cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="vertical-align:middle;">
<img src="${baseUrl}/logo-raven.png" alt="Raven" style="width:36px;height:36px;border-radius:10px;display:block;" />
</td>
<td style="vertical-align:middle;padding-left:12px;">
<span style="font-weight:800;font-size:18px;color:${COLORS.textPrimary};">Raven</span>
</td>
</tr>
</table>
<h1 style="font-size:20px;line-height:1.3;margin:16px 0 8px 0;color:${COLORS.textPrimary};">${escapeHtml(params.title)}</h1>
${textToHtmlParagraphs(params.message)}
${ticketInfoCard(params.ticket)}
<div style="margin-top:18px;">
${button(params.ctaLabel, params.ctaUrl)}
</div>
<p style="font-size:12px;color:${COLORS.textMuted};margin-top:18px;">
Se o botão não funcionar, copie e cole esta URL no navegador:<br/>
<a href="${escapeHtml(params.ctaUrl)}" style="color:${COLORS.primaryDark};text-decoration:none;">${escapeHtml(params.ctaUrl)}</a>
</p>
`
return `
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:${COLORS.background};padding:28px 0;font-family:Arial,Helvetica,sans-serif;color:${COLORS.textPrimary};">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" role="presentation" style="background:${COLORS.card};border:1px solid ${COLORS.border};border-radius:16px;padding:24px;max-width:600px;width:100%;">
<tr>
<td style="text-align:left;">
${content}
</td>
</tr>
</table>
<p style="font-size:12px;color:${COLORS.textMuted};margin-top:14px;">&copy; ${new Date().getFullYear()} Raven Rever Tecnologia</p>
</td>
</tr>
</table>
`
}
export function renderSimpleNotificationEmail(params: {
title: string
message: string
ctaLabel: string
ctaUrl: string
}) {
const baseUrl = buildBaseUrl()
const content = `
<table cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="vertical-align:middle;">
<img src="${baseUrl}/logo-raven.png" alt="Raven" style="width:36px;height:36px;border-radius:10px;display:block;" />
</td>
<td style="vertical-align:middle;padding-left:12px;">
<span style="font-weight:800;font-size:18px;color:${COLORS.textPrimary};">Raven</span>
</td>
</tr>
</table>
<h1 style="font-size:20px;line-height:1.3;margin:16px 0 8px 0;color:${COLORS.textPrimary};">${escapeHtml(params.title)}</h1>
${textToHtmlParagraphs(params.message)}
<div style="margin-top:18px;">
${button(params.ctaLabel, params.ctaUrl)}
</div>
<p style="font-size:12px;color:${COLORS.textMuted};margin-top:18px;">
Se o botão não funcionar, copie e cole esta URL no navegador:<br/>
<a href="${escapeHtml(params.ctaUrl)}" style="color:${COLORS.primaryDark};text-decoration:none;">${escapeHtml(params.ctaUrl)}</a>
</p>
`
return `
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:${COLORS.background};padding:28px 0;font-family:Arial,Helvetica,sans-serif;color:${COLORS.textPrimary};">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" role="presentation" style="background:${COLORS.card};border:1px solid ${COLORS.border};border-radius:16px;padding:24px;max-width:600px;width:100%;">
<tr>
<td style="text-align:left;">
${content}
</td>
</tr>
</table>
<p style="font-size:12px;color:${COLORS.textMuted};margin-top:14px;">&copy; ${new Date().getFullYear()} Raven Rever Tecnologia</p>
</td>
</tr>
</table>
`
}