feat(email): redesign completo dos templates de e-mail
- Sincroniza cores com globals.css e status-badge.tsx - Adiciona botoes grandes e destacados (cyan primary, preto secondary) - Implementa badges pill com border-radius arredondado - Importa fonte Inter do Google Fonts - Adiciona icones em circulos grandes (64px) para templates de status - Cria cards de informacao do ticket bem estruturados - Aumenta espacamentos e padding para layout mais limpo - Centraliza estrelas de avaliacao - Melhora tipografia com pesos bem definidos 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
eedd446b36
commit
61c36dbb7c
1 changed files with 398 additions and 221 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Sistema de Templates de E-mail
|
* Sistema de Templates de E-mail
|
||||||
* Sistema de Chamados Raven
|
* Sistema de Chamados Raven
|
||||||
|
* Design inspirado em boas praticas de e-mail marketing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -24,48 +25,64 @@ export type TemplateName =
|
||||||
export type TemplateData = Record<string, unknown>
|
export type TemplateData = Record<string, unknown>
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Design Tokens
|
// Design Tokens - Sincronizado com globals.css
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
// Primárias
|
// Primarias - cyan do sistema
|
||||||
primary: "#00e8ff",
|
primary: "#00e8ff",
|
||||||
primaryDark: "#00c4d6",
|
primaryDark: "#00d6eb",
|
||||||
primaryForeground: "#020617",
|
primaryForeground: "#020617",
|
||||||
|
|
||||||
// Background
|
// Backgrounds
|
||||||
background: "#f7f8fb",
|
background: "#f7f8fb",
|
||||||
card: "#ffffff",
|
card: "#ffffff",
|
||||||
|
cardAlt: "#f8fafc",
|
||||||
|
|
||||||
|
// Borders
|
||||||
border: "#e2e8f0",
|
border: "#e2e8f0",
|
||||||
|
borderLight: "#f1f5f9",
|
||||||
|
|
||||||
// Texto
|
// Texto
|
||||||
textPrimary: "#0f172a",
|
textPrimary: "#0f172a",
|
||||||
textSecondary: "#475569",
|
textSecondary: "#475569",
|
||||||
textMuted: "#64748b",
|
textMuted: "#94a3b8",
|
||||||
|
|
||||||
// Status
|
// Status - alinhado com status-badge.tsx
|
||||||
statusPending: "#64748b",
|
statusPending: "#64748b",
|
||||||
statusPendingBg: "#f1f5f9",
|
statusPendingBg: "#f1f5f9",
|
||||||
statusProgress: "#0ea5e9",
|
statusProgress: "#0a4760",
|
||||||
statusProgressBg: "#e0f2fe",
|
statusProgressBg: "#dff1fb",
|
||||||
statusPaused: "#f59e0b",
|
statusPaused: "#7a5901",
|
||||||
statusPausedBg: "#fef3c7",
|
statusPausedBg: "#fff3c4",
|
||||||
statusResolved: "#10b981",
|
statusResolved: "#1f6a45",
|
||||||
statusResolvedBg: "#d1fae5",
|
statusResolvedBg: "#dcf4eb",
|
||||||
|
|
||||||
// Prioridade
|
// Prioridade
|
||||||
priorityLow: "#64748b",
|
priorityLow: "#64748b",
|
||||||
priorityLowBg: "#f1f5f9",
|
priorityLowBg: "#f1f5f9",
|
||||||
priorityMedium: "#0a4760",
|
priorityMedium: "#0a4760",
|
||||||
priorityMediumBg: "#dff1fb",
|
priorityMediumBg: "#dff1fb",
|
||||||
priorityHigh: "#7d3b05",
|
priorityHigh: "#92400e",
|
||||||
priorityHighBg: "#fde8d1",
|
priorityHighBg: "#fef3c7",
|
||||||
priorityUrgent: "#8b0f1c",
|
priorityUrgent: "#991b1b",
|
||||||
priorityUrgentBg: "#fbd9dd",
|
priorityUrgentBg: "#fee2e2",
|
||||||
|
|
||||||
|
// Alertas
|
||||||
|
warning: "#d97706",
|
||||||
|
warningBg: "#fffbeb",
|
||||||
|
error: "#dc2626",
|
||||||
|
errorBg: "#fef2f2",
|
||||||
|
success: "#059669",
|
||||||
|
successBg: "#ecfdf5",
|
||||||
|
|
||||||
// Estrelas
|
// Estrelas
|
||||||
starActive: "#fbbf24",
|
starActive: "#f59e0b",
|
||||||
starInactive: "#d1d5db",
|
starInactive: "#e2e8f0",
|
||||||
|
|
||||||
|
// Botao secundario
|
||||||
|
buttonSecondary: "#0f172a",
|
||||||
|
buttonSecondaryForeground: "#f8fafc",
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -96,6 +113,16 @@ function formatDate(date: Date | string | number, options?: Intl.DateTimeFormatO
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateShort(date: Date | string | number): string {
|
||||||
|
const d = date instanceof Date ? date : new Date(date)
|
||||||
|
return d.toLocaleDateString("pt-BR", {
|
||||||
|
timeZone: "America/Sao_Paulo",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Componentes de E-mail
|
// Componentes de E-mail
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -113,15 +140,16 @@ function getStatusStyle(status: string): { bg: string; color: string; label: str
|
||||||
function getPriorityStyle(priority: string): { bg: string; color: string; label: string } {
|
function getPriorityStyle(priority: string): { bg: string; color: string; label: string } {
|
||||||
const priorityMap: Record<string, { bg: string; color: string; label: string }> = {
|
const priorityMap: Record<string, { bg: string; color: string; label: string }> = {
|
||||||
LOW: { bg: COLORS.priorityLowBg, color: COLORS.priorityLow, label: "Baixa" },
|
LOW: { bg: COLORS.priorityLowBg, color: COLORS.priorityLow, label: "Baixa" },
|
||||||
MEDIUM: { bg: COLORS.priorityMediumBg, color: COLORS.priorityMedium, label: "Média" },
|
MEDIUM: { bg: COLORS.priorityMediumBg, color: COLORS.priorityMedium, label: "Media" },
|
||||||
HIGH: { bg: COLORS.priorityHighBg, color: COLORS.priorityHigh, label: "Alta" },
|
HIGH: { bg: COLORS.priorityHighBg, color: COLORS.priorityHigh, label: "Alta" },
|
||||||
URGENT: { bg: COLORS.priorityUrgentBg, color: COLORS.priorityUrgent, label: "Urgente" },
|
URGENT: { bg: COLORS.priorityUrgentBg, color: COLORS.priorityUrgent, label: "Urgente" },
|
||||||
}
|
}
|
||||||
return priorityMap[priority] ?? { bg: COLORS.priorityMediumBg, color: COLORS.priorityMedium, label: priority }
|
return priorityMap[priority] ?? { bg: COLORS.priorityMediumBg, color: COLORS.priorityMedium, label: priority }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Badge pill - estilo arredondado como no sistema
|
||||||
function badge(label: string, bg: string, color: string): string {
|
function badge(label: string, bg: string, color: string): string {
|
||||||
return `<span style="display:inline-block;padding:4px 12px;border-radius:16px;font-size:12px;font-weight:500;background:${bg};color:${color};">${escapeHtml(label)}</span>`
|
return `<span style="display:inline-block;padding:6px 14px;border-radius:9999px;font-size:13px;font-weight:600;background:${bg};color:${color};border:1px solid ${bg};">${escapeHtml(label)}</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusBadge(status: string): string {
|
function statusBadge(status: string): string {
|
||||||
|
|
@ -134,14 +162,38 @@ function priorityBadge(priority: string): string {
|
||||||
return badge(style.label, style.bg, style.color)
|
return badge(style.label, style.bg, style.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
function button(label: string, url: string, variant: "primary" | "secondary" = "primary"): string {
|
// Botao principal - estilo cyan do sistema
|
||||||
const bg = variant === "primary" ? COLORS.primary : COLORS.card
|
function buttonPrimary(label: string, url: string): string {
|
||||||
const color = variant === "primary" ? COLORS.primaryForeground : COLORS.textPrimary
|
return `
|
||||||
const border = variant === "primary" ? COLORS.primary : COLORS.border
|
<table cellpadding="0" cellspacing="0" border="0" style="margin:0 auto;">
|
||||||
|
<tr>
|
||||||
return `<a href="${escapeHtml(url)}" style="display:inline-block;padding:12px 24px;background:${bg};color:${color};text-decoration:none;border-radius:8px;font-weight:600;font-size:14px;border:1px solid ${border};">${escapeHtml(label)}</a>`
|
<td align="center" style="border-radius:12px;background:${COLORS.primary};">
|
||||||
|
<a href="${escapeHtml(url)}" target="_blank" style="display:inline-block;padding:14px 32px;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:15px;font-weight:600;color:${COLORS.primaryForeground};text-decoration:none;border-radius:12px;">${escapeHtml(label)}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Botao secundario - estilo escuro
|
||||||
|
function buttonSecondary(label: string, url: string): string {
|
||||||
|
return `
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" style="margin:0 auto;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="border-radius:12px;background:${COLORS.buttonSecondary};">
|
||||||
|
<a href="${escapeHtml(url)}" target="_blank" style="display:inline-block;padding:14px 32px;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:15px;font-weight:600;color:${COLORS.buttonSecondaryForeground};text-decoration:none;border-radius:12px;">${escapeHtml(label)}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link de texto
|
||||||
|
function textLink(label: string, url: string): string {
|
||||||
|
return `<a href="${escapeHtml(url)}" style="color:${COLORS.primaryDark};text-decoration:none;font-weight:500;">${escapeHtml(label)}</a>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card de informacoes do ticket
|
||||||
function ticketInfoCard(data: {
|
function ticketInfoCard(data: {
|
||||||
reference: number | string
|
reference: number | string
|
||||||
subject: string
|
subject: string
|
||||||
|
|
@ -153,98 +205,139 @@ function ticketInfoCard(data: {
|
||||||
}): string {
|
}): string {
|
||||||
const rows: string[] = []
|
const rows: string[] = []
|
||||||
|
|
||||||
|
// Numero do chamado com destaque
|
||||||
rows.push(`
|
rows.push(`
|
||||||
<tr>
|
<tr>
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;vertical-align:top;">Chamado</td>
|
<td style="padding:12px 16px;border-bottom:1px solid ${COLORS.borderLight};">
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;font-weight:600;padding:4px 0;">#${escapeHtml(data.reference)}</td>
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="color:${COLORS.textMuted};font-size:13px;font-weight:500;width:120px;">Chamado</td>
|
||||||
|
<td style="color:${COLORS.textPrimary};font-size:15px;font-weight:700;">#${escapeHtml(data.reference)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// Assunto
|
||||||
rows.push(`
|
rows.push(`
|
||||||
<tr>
|
<tr>
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;vertical-align:top;">Assunto</td>
|
<td style="padding:12px 16px;border-bottom:1px solid ${COLORS.borderLight};">
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(data.subject)}</td>
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="color:${COLORS.textMuted};font-size:13px;font-weight:500;width:120px;vertical-align:top;">Assunto</td>
|
||||||
|
<td style="color:${COLORS.textPrimary};font-size:14px;line-height:1.5;">${escapeHtml(data.subject)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
if (data.status) {
|
// Status e Prioridade na mesma linha
|
||||||
|
if (data.status || data.priority) {
|
||||||
rows.push(`
|
rows.push(`
|
||||||
<tr>
|
<tr>
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;vertical-align:top;">Status</td>
|
<td style="padding:12px 16px;border-bottom:1px solid ${COLORS.borderLight};">
|
||||||
<td style="padding:4px 0;">${statusBadge(data.status)}</td>
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
${data.status ? `
|
||||||
|
<td style="width:50%;">
|
||||||
|
<span style="color:${COLORS.textMuted};font-size:13px;font-weight:500;display:block;margin-bottom:6px;">Status</span>
|
||||||
|
${statusBadge(data.status)}
|
||||||
|
</td>
|
||||||
|
` : ""}
|
||||||
|
${data.priority ? `
|
||||||
|
<td style="width:50%;">
|
||||||
|
<span style="color:${COLORS.textMuted};font-size:13px;font-weight:500;display:block;margin-bottom:6px;">Prioridade</span>
|
||||||
|
${priorityBadge(data.priority)}
|
||||||
|
</td>
|
||||||
|
` : ""}
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.priority) {
|
// Solicitante e Responsavel
|
||||||
|
if (data.requesterName || data.assigneeName) {
|
||||||
rows.push(`
|
rows.push(`
|
||||||
<tr>
|
<tr>
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;vertical-align:top;">Prioridade</td>
|
<td style="padding:12px 16px;${data.createdAt ? `border-bottom:1px solid ${COLORS.borderLight};` : ""}">
|
||||||
<td style="padding:4px 0;">${priorityBadge(data.priority)}</td>
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
</tr>
|
<tr>
|
||||||
`)
|
${data.requesterName ? `
|
||||||
}
|
<td style="width:50%;">
|
||||||
|
<span style="color:${COLORS.textMuted};font-size:13px;font-weight:500;display:block;margin-bottom:4px;">Solicitante</span>
|
||||||
if (data.requesterName) {
|
<span style="color:${COLORS.textPrimary};font-size:14px;">${escapeHtml(data.requesterName)}</span>
|
||||||
rows.push(`
|
</td>
|
||||||
<tr>
|
` : ""}
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;vertical-align:top;">Solicitante</td>
|
${data.assigneeName ? `
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(data.requesterName)}</td>
|
<td style="width:50%;">
|
||||||
</tr>
|
<span style="color:${COLORS.textMuted};font-size:13px;font-weight:500;display:block;margin-bottom:4px;">Responsavel</span>
|
||||||
`)
|
<span style="color:${COLORS.textPrimary};font-size:14px;">${escapeHtml(data.assigneeName)}</span>
|
||||||
}
|
</td>
|
||||||
|
` : ""}
|
||||||
if (data.assigneeName) {
|
</tr>
|
||||||
rows.push(`
|
</table>
|
||||||
<tr>
|
</td>
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;vertical-align:top;">Responsável</td>
|
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(data.assigneeName)}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data de criacao
|
||||||
if (data.createdAt) {
|
if (data.createdAt) {
|
||||||
rows.push(`
|
rows.push(`
|
||||||
<tr>
|
<tr>
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;vertical-align:top;">Criado em</td>
|
<td style="padding:12px 16px;">
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${formatDate(data.createdAt)}</td>
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="color:${COLORS.textMuted};font-size:13px;font-weight:500;width:120px;">Criado em</td>
|
||||||
|
<td style="color:${COLORS.textSecondary};font-size:14px;">${formatDate(data.createdAt)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:${COLORS.background};border-radius:8px;margin:16px 0;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:${COLORS.cardAlt};border-radius:12px;border:1px solid ${COLORS.border};margin:24px 0;">
|
||||||
<tr>
|
|
||||||
<td style="padding:16px;">
|
|
||||||
<table width="100%" cellpadding="0" cellspacing="0">
|
|
||||||
${rows.join("")}
|
${rows.join("")}
|
||||||
</table>
|
</table>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sistema de estrelas de avaliacao
|
||||||
function ratingStars(rateUrl: string): string {
|
function ratingStars(rateUrl: string): string {
|
||||||
const stars: string[] = []
|
const stars: string[] = []
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
stars.push(`
|
stars.push(`
|
||||||
<td style="padding:0 4px;">
|
<td style="padding:0 6px;">
|
||||||
<a href="${escapeHtml(rateUrl)}?rating=${i}" style="text-decoration:none;font-size:28px;color:${COLORS.starActive};">★</a>
|
<a href="${escapeHtml(rateUrl)}?rating=${i}" style="text-decoration:none;">
|
||||||
|
<span style="font-size:32px;color:${COLORS.starActive};line-height:1;">★</span>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<table cellpadding="0" cellspacing="0" style="margin:16px auto;" align="center">
|
<div style="text-align:center;padding:24px 0;">
|
||||||
|
<table cellpadding="0" cellspacing="0" style="margin:0 auto;" align="center">
|
||||||
<tr>
|
<tr>
|
||||||
${stars.join("")}
|
${stars.join("")}
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<p style="color:${COLORS.textMuted};font-size:12px;margin:4px 0 0 0;text-align:center;">Clique em uma estrela para avaliar</p>
|
<p style="color:${COLORS.textMuted};font-size:13px;margin:12px 0 0 0;text-align:center;">Clique em uma estrela para avaliar</p>
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Divisor
|
||||||
|
function divider(): string {
|
||||||
|
return `<hr style="border:none;border-top:1px solid ${COLORS.border};margin:32px 0;">`
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Template Base
|
// Template Base
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -260,7 +353,7 @@ function baseTemplate(content: string, data: TemplateData): string {
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<title>${escapeHtml(data.subject ?? "Notificação")}</title>
|
<title>${escapeHtml(data.subject ?? "Notificacao")}</title>
|
||||||
<!--[if mso]>
|
<!--[if mso]>
|
||||||
<noscript>
|
<noscript>
|
||||||
<xml>
|
<xml>
|
||||||
|
|
@ -270,24 +363,27 @@ function baseTemplate(content: string, data: TemplateData): string {
|
||||||
</xml>
|
</xml>
|
||||||
</noscript>
|
</noscript>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin:0;padding:0;background:${COLORS.background};font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;">
|
<body style="margin:0;padding:0;background:${COLORS.background};font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;">
|
||||||
|
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:${COLORS.background};padding:32px 16px;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:${COLORS.background};padding:40px 20px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<table width="600" cellpadding="0" cellspacing="0" style="max-width:600px;width:100%;">
|
<table width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;">
|
||||||
|
|
||||||
<!-- Header com logo -->
|
<!-- Header com logo -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:0 0 24px 0;text-align:center;">
|
<td style="padding:0 0 32px 0;text-align:center;">
|
||||||
<table cellpadding="0" cellspacing="0" style="margin:0 auto;">
|
<table cellpadding="0" cellspacing="0" style="margin:0 auto;">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background:${COLORS.primary};width:40px;height:40px;border-radius:8px;text-align:center;vertical-align:middle;">
|
<td style="background:${COLORS.primary};width:44px;height:44px;border-radius:10px;text-align:center;vertical-align:middle;">
|
||||||
<span style="color:${COLORS.primaryForeground};font-size:20px;font-weight:bold;">R</span>
|
<span style="color:${COLORS.primaryForeground};font-size:22px;font-weight:700;font-family:'Inter',sans-serif;">R</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding-left:12px;">
|
<td style="padding-left:14px;">
|
||||||
<span style="color:${COLORS.textPrimary};font-size:20px;font-weight:600;">Raven</span>
|
<span style="color:${COLORS.textPrimary};font-size:22px;font-weight:700;font-family:'Inter',sans-serif;">Raven</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -296,21 +392,21 @@ function baseTemplate(content: string, data: TemplateData): string {
|
||||||
|
|
||||||
<!-- Card principal -->
|
<!-- Card principal -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background:${COLORS.card};border-radius:12px;padding:32px;border:1px solid ${COLORS.border};">
|
<td style="background:${COLORS.card};border-radius:16px;padding:40px;box-shadow:0 1px 3px rgba(0,0,0,0.05);border:1px solid ${COLORS.border};">
|
||||||
${content}
|
${content}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:24px 0;text-align:center;">
|
<td style="padding:32px 0;text-align:center;">
|
||||||
<p style="color:${COLORS.textMuted};font-size:12px;margin:0 0 8px 0;">
|
<p style="color:${COLORS.textMuted};font-size:13px;margin:0 0 12px 0;line-height:1.6;">
|
||||||
Este e-mail foi enviado pelo Sistema de Chamados Raven.
|
Este e-mail foi enviado pelo Sistema de Chamados Raven.
|
||||||
</p>
|
</p>
|
||||||
<p style="margin:0;">
|
<p style="margin:0;">
|
||||||
<a href="${preferencesUrl}" style="color:${COLORS.primaryDark};font-size:12px;text-decoration:none;">Gerenciar notificações</a>
|
${textLink("Gerenciar notificacoes", preferencesUrl)}
|
||||||
<span style="color:${COLORS.textMuted};margin:0 8px;">|</span>
|
<span style="color:${COLORS.textMuted};margin:0 12px;">|</span>
|
||||||
<a href="${helpUrl}" style="color:${COLORS.primaryDark};font-size:12px;text-decoration:none;">Ajuda</a>
|
${textLink("Central de ajuda", helpUrl)}
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -333,14 +429,14 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
test: (data) => {
|
test: (data) => {
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 16px 0;">
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;text-align:center;">
|
||||||
${escapeHtml(data.title)}
|
${escapeHtml(data.title)}
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0 0 32px 0;text-align:center;">
|
||||||
${escapeHtml(data.message)}
|
${escapeHtml(data.message)}
|
||||||
</p>
|
</p>
|
||||||
<p style="color:${COLORS.textMuted};font-size:12px;margin:0;">
|
<p style="color:${COLORS.textMuted};font-size:13px;margin:0;text-align:center;">
|
||||||
Enviado em: ${escapeHtml(data.timestamp)}
|
Enviado em ${escapeHtml(data.timestamp)}
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
|
|
@ -352,11 +448,14 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
const viewUrl = data.viewUrl as string
|
const viewUrl = data.viewUrl as string
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;">
|
||||||
Chamado aberto
|
Chamado aberto
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0 0 8px 0;">
|
||||||
Seu chamado foi registrado com sucesso. Nossa equipe irá analisá-lo em breve.
|
Seu chamado foi registrado com sucesso.
|
||||||
|
</p>
|
||||||
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0;">
|
||||||
|
Nossa equipe ira analisa-lo e entrar em contato em breve.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
${ticketInfoCard({
|
${ticketInfoCard({
|
||||||
|
|
@ -367,26 +466,32 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
createdAt: data.createdAt as string,
|
createdAt: data.createdAt as string,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:24px;">
|
<div style="text-align:center;margin-top:32px;">
|
||||||
${button("Ver chamado", viewUrl)}
|
${buttonPrimary("Ver chamado", viewUrl)}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Resolução de chamado
|
// Resolucao de chamado
|
||||||
ticket_resolved: (data) => {
|
ticket_resolved: (data) => {
|
||||||
const viewUrl = data.viewUrl as string
|
const viewUrl = data.viewUrl as string
|
||||||
const rateUrl = data.rateUrl as string
|
const rateUrl = data.rateUrl as string
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<div style="text-align:center;margin-bottom:24px;">
|
||||||
|
<div style="display:inline-block;width:64px;height:64px;background:${COLORS.statusResolvedBg};border-radius:50%;line-height:64px;">
|
||||||
|
<span style="font-size:28px;">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;text-align:center;">
|
||||||
Chamado resolvido
|
Chamado resolvido
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0;text-align:center;">
|
||||||
Seu chamado foi marcado como resolvido. Esperamos que o atendimento tenha sido satisfatório!
|
Seu chamado foi marcado como resolvido. Esperamos que o atendimento tenha sido satisfatorio!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
${ticketInfoCard({
|
${ticketInfoCard({
|
||||||
|
|
@ -399,44 +504,46 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
${
|
${
|
||||||
data.resolutionSummary
|
data.resolutionSummary
|
||||||
? `
|
? `
|
||||||
<div style="background:${COLORS.statusResolvedBg};border-radius:8px;padding:16px;margin:16px 0;">
|
<div style="background:${COLORS.successBg};border-radius:12px;padding:20px;margin:24px 0;border-left:4px solid ${COLORS.success};">
|
||||||
<p style="color:${COLORS.statusResolved};font-size:12px;font-weight:600;margin:0 0 8px 0;">RESUMO DA RESOLUÇÃO</p>
|
<p style="color:${COLORS.success};font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;margin:0 0 8px 0;">Resumo da resolucao</p>
|
||||||
<p style="color:${COLORS.textPrimary};font-size:14px;line-height:1.6;margin:0;">${escapeHtml(data.resolutionSummary)}</p>
|
<p style="color:${COLORS.textPrimary};font-size:14px;line-height:1.6;margin:0;">${escapeHtml(data.resolutionSummary)}</p>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
<div style="text-align:center;margin:32px 0 16px 0;">
|
${divider()}
|
||||||
<p style="color:${COLORS.textPrimary};font-size:16px;font-weight:600;margin:0 0 8px 0;">Como foi o atendimento?</p>
|
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;margin:0 0 16px 0;">Sua avaliação nos ajuda a melhorar!</p>
|
<div style="text-align:center;">
|
||||||
|
<p style="color:${COLORS.textPrimary};font-size:18px;font-weight:600;margin:0 0 8px 0;">Como foi o atendimento?</p>
|
||||||
|
<p style="color:${COLORS.textSecondary};font-size:14px;margin:0;">Sua avaliacao nos ajuda a melhorar!</p>
|
||||||
${ratingStars(rateUrl)}
|
${ratingStars(rateUrl)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:24px;">
|
<div style="text-align:center;margin-top:24px;">
|
||||||
${button("Ver chamado", viewUrl)}
|
${buttonSecondary("Ver detalhes", viewUrl)}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Atribuição de chamado
|
// Atribuicao de chamado
|
||||||
ticket_assigned: (data) => {
|
ticket_assigned: (data) => {
|
||||||
const viewUrl = data.viewUrl as string
|
const viewUrl = data.viewUrl as string
|
||||||
const isForRequester = data.isForRequester as boolean
|
const isForRequester = data.isForRequester as boolean
|
||||||
|
|
||||||
const title = isForRequester ? "Agente atribuído ao chamado" : "Novo chamado atribuído"
|
const title = isForRequester ? "Agente atribuido" : "Novo chamado atribuido"
|
||||||
const message = isForRequester
|
const message = isForRequester
|
||||||
? `O agente ${escapeHtml(data.assigneeName)} foi atribuído ao seu chamado e em breve entrará em contato.`
|
? `O agente <strong>${escapeHtml(data.assigneeName)}</strong> foi atribuido ao seu chamado e em breve entrara em contato.`
|
||||||
: `Um novo chamado foi atribuído a você. Por favor, verifique os detalhes abaixo.`
|
: `Um novo chamado foi atribuido a voce. Por favor, verifique os detalhes abaixo.`
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;">
|
||||||
${title}
|
${title}
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0;">
|
||||||
${message}
|
${message}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -449,15 +556,15 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
assigneeName: data.assigneeName as string,
|
assigneeName: data.assigneeName as string,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:24px;">
|
<div style="text-align:center;margin-top:32px;">
|
||||||
${button("Ver chamado", viewUrl)}
|
${buttonPrimary("Ver chamado", viewUrl)}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mudança de status
|
// Mudanca de status
|
||||||
ticket_status: (data) => {
|
ticket_status: (data) => {
|
||||||
const viewUrl = data.viewUrl as string
|
const viewUrl = data.viewUrl as string
|
||||||
const oldStatus = getStatusStyle(data.oldStatus as string)
|
const oldStatus = getStatusStyle(data.oldStatus as string)
|
||||||
|
|
@ -465,10 +572,10 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;">
|
||||||
Status atualizado
|
Status atualizado
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0;">
|
||||||
O status do seu chamado foi alterado.
|
O status do seu chamado foi alterado.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -477,39 +584,41 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
subject: data.subject as string,
|
subject: data.subject as string,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div style="text-align:center;margin:24px 0;">
|
<div style="text-align:center;margin:32px 0;">
|
||||||
<table cellpadding="0" cellspacing="0" style="margin:0 auto;">
|
<table cellpadding="0" cellspacing="0" style="margin:0 auto;">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="text-align:center;">
|
<td style="text-align:center;padding-right:16px;">
|
||||||
|
<span style="color:${COLORS.textMuted};font-size:12px;display:block;margin-bottom:8px;">Anterior</span>
|
||||||
${badge(oldStatus.label, oldStatus.bg, oldStatus.color)}
|
${badge(oldStatus.label, oldStatus.bg, oldStatus.color)}
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:0 16px;color:${COLORS.textMuted};font-size:20px;">→</td>
|
<td style="padding:0 8px;color:${COLORS.textMuted};font-size:24px;vertical-align:bottom;padding-bottom:4px;">→</td>
|
||||||
<td style="text-align:center;">
|
<td style="text-align:center;padding-left:16px;">
|
||||||
|
<span style="color:${COLORS.textMuted};font-size:12px;display:block;margin-bottom:8px;">Atual</span>
|
||||||
${badge(newStatus.label, newStatus.bg, newStatus.color)}
|
${badge(newStatus.label, newStatus.bg, newStatus.color)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:24px;">
|
<div style="text-align:center;">
|
||||||
${button("Ver chamado", viewUrl)}
|
${buttonPrimary("Ver chamado", viewUrl)}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Novo comentário
|
// Novo comentario
|
||||||
ticket_comment: (data) => {
|
ticket_comment: (data) => {
|
||||||
const viewUrl = data.viewUrl as string
|
const viewUrl = data.viewUrl as string
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;">
|
||||||
Nova atualização no chamado
|
Nova atualizacao
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0;">
|
||||||
${escapeHtml(data.authorName)} adicionou um comentário ao seu chamado.
|
<strong>${escapeHtml(data.authorName)}</strong> adicionou um comentario ao seu chamado.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
${ticketInfoCard({
|
${ticketInfoCard({
|
||||||
|
|
@ -517,17 +626,18 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
subject: data.subject as string,
|
subject: data.subject as string,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div style="background:${COLORS.background};border-radius:8px;padding:16px;margin:16px 0;border-left:4px solid ${COLORS.primary};">
|
<div style="background:${COLORS.cardAlt};border-radius:12px;padding:20px;margin:24px 0;border-left:4px solid ${COLORS.primary};">
|
||||||
<p style="color:${COLORS.textMuted};font-size:12px;margin:0 0 8px 0;">
|
<div style="margin-bottom:12px;">
|
||||||
${escapeHtml(data.authorName)} • ${formatDate(data.commentedAt as string)}
|
<span style="color:${COLORS.textPrimary};font-size:14px;font-weight:600;">${escapeHtml(data.authorName)}</span>
|
||||||
</p>
|
<span style="color:${COLORS.textMuted};font-size:13px;margin-left:8px;">${formatDate(data.commentedAt as string)}</span>
|
||||||
<p style="color:${COLORS.textPrimary};font-size:14px;line-height:1.6;margin:0;">
|
</div>
|
||||||
|
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.7;margin:0;">
|
||||||
${escapeHtml(data.commentBody)}
|
${escapeHtml(data.commentBody)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:24px;">
|
<div style="text-align:center;">
|
||||||
${button("Ver chamado", viewUrl)}
|
${buttonPrimary("Responder", viewUrl)}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
|
|
@ -540,89 +650,116 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<div style="text-align:center;margin-bottom:24px;">
|
||||||
Redefinição de senha
|
<div style="display:inline-block;width:64px;height:64px;background:${COLORS.cardAlt};border-radius:50%;line-height:64px;border:1px solid ${COLORS.border};">
|
||||||
|
<span style="font-size:28px;">🔒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;text-align:center;">
|
||||||
|
Redefinicao de senha
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0 0 32px 0;text-align:center;">
|
||||||
Recebemos uma solicitação para redefinir a senha da sua conta. Se você não fez essa solicitação, pode ignorar este e-mail.
|
Recebemos uma solicitacao para redefinir a senha da sua conta. Se voce nao fez essa solicitacao, pode ignorar este e-mail.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="text-align:center;margin:32px 0;">
|
<div style="text-align:center;margin:32px 0;">
|
||||||
${button("Redefinir senha", resetUrl)}
|
${buttonPrimary("Redefinir senha", resetUrl)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="color:${COLORS.textMuted};font-size:12px;margin:24px 0 0 0;">
|
<p style="color:${COLORS.textMuted};font-size:13px;margin:32px 0 0 0;text-align:center;line-height:1.6;">
|
||||||
Este link expira em 24 horas. Se você não solicitou a redefinição de senha,
|
Este link expira em 24 horas. Se voce nao solicitou a redefinicao de senha, pode ignorar este e-mail com seguranca.
|
||||||
pode ignorar este e-mail com segurança.
|
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Verificação de e-mail
|
// Verificacao de e-mail
|
||||||
email_verify: (data) => {
|
email_verify: (data) => {
|
||||||
const verifyUrl = data.verifyUrl as string
|
const verifyUrl = data.verifyUrl as string
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<div style="text-align:center;margin-bottom:24px;">
|
||||||
|
<div style="display:inline-block;width:64px;height:64px;background:${COLORS.cardAlt};border-radius:50%;line-height:64px;border:1px solid ${COLORS.border};">
|
||||||
|
<span style="font-size:28px;">✉</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;text-align:center;">
|
||||||
Confirme seu e-mail
|
Confirme seu e-mail
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0 0 32px 0;text-align:center;">
|
||||||
Clique no botão abaixo para confirmar seu endereço de e-mail e ativar sua conta.
|
Clique no botao abaixo para confirmar seu endereco de e-mail e ativar sua conta.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="text-align:center;margin:32px 0;">
|
<div style="text-align:center;margin:32px 0;">
|
||||||
${button("Confirmar e-mail", verifyUrl)}
|
${buttonPrimary("Confirmar e-mail", verifyUrl)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="color:${COLORS.textMuted};font-size:12px;margin:24px 0 0 0;">
|
<p style="color:${COLORS.textMuted};font-size:13px;margin:32px 0 0 0;text-align:center;line-height:1.6;">
|
||||||
Se você não criou uma conta, pode ignorar este e-mail com segurança.
|
Se voce nao criou uma conta, pode ignorar este e-mail com seguranca.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Convite de usuário
|
// Convite de usuario
|
||||||
invite: (data) => {
|
invite: (data) => {
|
||||||
const inviteUrl = data.inviteUrl as string
|
const inviteUrl = data.inviteUrl as string
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<div style="text-align:center;margin-bottom:24px;">
|
||||||
Você foi convidado!
|
<div style="display:inline-block;width:64px;height:64px;background:${COLORS.primary};border-radius:50%;line-height:64px;">
|
||||||
|
<span style="font-size:28px;">🎉</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;text-align:center;">
|
||||||
|
Voce foi convidado!
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0 0 24px 0;text-align:center;">
|
||||||
${escapeHtml(data.inviterName)} convidou você para acessar o Sistema de Chamados Raven.
|
<strong>${escapeHtml(data.inviterName)}</strong> convidou voce para acessar o Sistema de Chamados Raven.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="background:${COLORS.background};border-radius:8px;padding:16px;margin:16px 0;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:${COLORS.cardAlt};border-radius:12px;border:1px solid ${COLORS.border};margin:24px 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:16px 20px;border-bottom:1px solid ${COLORS.borderLight};">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0">
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;">Função</td>
|
<td style="color:${COLORS.textMuted};font-size:13px;font-weight:500;width:100px;">Funcao</td>
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(data.roleName)}</td>
|
<td style="color:${COLORS.textPrimary};font-size:14px;font-weight:600;">${escapeHtml(data.roleName)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
${
|
${
|
||||||
data.companyName
|
data.companyName
|
||||||
? `
|
? `
|
||||||
<tr>
|
<tr>
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;">Empresa</td>
|
<td style="padding:16px 20px;">
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(data.companyName)}</td>
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="color:${COLORS.textMuted};font-size:13px;font-weight:500;width:100px;">Empresa</td>
|
||||||
|
<td style="color:${COLORS.textPrimary};font-size:14px;">${escapeHtml(data.companyName)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`
|
`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align:center;margin:32px 0;">
|
<div style="text-align:center;margin:32px 0;">
|
||||||
${button("Aceitar convite", inviteUrl)}
|
${buttonPrimary("Aceitar convite", inviteUrl)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="color:${COLORS.textMuted};font-size:12px;margin:24px 0 0 0;">
|
<p style="color:${COLORS.textMuted};font-size:13px;margin:24px 0 0 0;text-align:center;line-height:1.6;">
|
||||||
Este convite expira em 7 dias. Se você não esperava este convite, pode ignorá-lo com segurança.
|
Este convite expira em 7 dias. Se voce nao esperava este convite, pode ignora-lo com seguranca.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
|
|
@ -633,32 +770,54 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
new_login: (data) => {
|
new_login: (data) => {
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.textPrimary};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<div style="text-align:center;margin-bottom:24px;">
|
||||||
Novo acesso detectado
|
<div style="display:inline-block;width:64px;height:64px;background:${COLORS.warningBg};border-radius:50%;line-height:64px;border:1px solid ${COLORS.warning};">
|
||||||
</h1>
|
<span style="font-size:28px;">🔒</span>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
</div>
|
||||||
Detectamos um novo acesso à sua conta. Se foi você, pode ignorar este e-mail.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div style="background:${COLORS.background};border-radius:8px;padding:16px;margin:16px 0;">
|
|
||||||
<table width="100%" cellpadding="0" cellspacing="0">
|
|
||||||
<tr>
|
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;">Data/Hora</td>
|
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${formatDate(data.loginAt as string)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;">Dispositivo</td>
|
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(data.userAgent)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="color:${COLORS.textMuted};font-size:12px;padding:4px 8px 4px 0;">Endereço IP</td>
|
|
||||||
<td style="color:${COLORS.textPrimary};font-size:14px;padding:4px 0;">${escapeHtml(data.ipAddress)}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="color:${COLORS.textMuted};font-size:12px;margin:24px 0 0 0;">
|
<h1 style="color:${COLORS.textPrimary};font-size:26px;font-weight:700;margin:0 0 12px 0;text-align:center;">
|
||||||
Se você não reconhece este acesso, recomendamos alterar sua senha imediatamente.
|
Novo acesso detectado
|
||||||
|
</h1>
|
||||||
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0 0 24px 0;text-align:center;">
|
||||||
|
Detectamos um novo acesso a sua conta. Se foi voce, pode ignorar este e-mail.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:${COLORS.cardAlt};border-radius:12px;border:1px solid ${COLORS.border};margin:24px 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:16px 20px;border-bottom:1px solid ${COLORS.borderLight};">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="color:${COLORS.textMuted};font-size:13px;font-weight:500;width:120px;">Data/Hora</td>
|
||||||
|
<td style="color:${COLORS.textPrimary};font-size:14px;">${formatDate(data.loginAt as string)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:16px 20px;border-bottom:1px solid ${COLORS.borderLight};">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="color:${COLORS.textMuted};font-size:13px;font-weight:500;width:120px;">Dispositivo</td>
|
||||||
|
<td style="color:${COLORS.textPrimary};font-size:14px;">${escapeHtml(data.userAgent)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:16px 20px;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="color:${COLORS.textMuted};font-size:13px;font-weight:500;width:120px;">Endereco IP</td>
|
||||||
|
<td style="color:${COLORS.textPrimary};font-size:14px;font-family:monospace;">${escapeHtml(data.ipAddress)}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="color:${COLORS.textMuted};font-size:13px;margin:24px 0 0 0;text-align:center;line-height:1.6;">
|
||||||
|
Se voce nao reconhece este acesso, recomendamos alterar sua senha imediatamente.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
|
|
@ -671,11 +830,20 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.statusPaused};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<div style="text-align:center;margin-bottom:24px;">
|
||||||
|
<div style="display:inline-block;width:64px;height:64px;background:${COLORS.warningBg};border-radius:50%;line-height:64px;border:2px solid ${COLORS.warning};">
|
||||||
|
<span style="font-size:28px;">⚠</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="color:${COLORS.warning};font-size:26px;font-weight:700;margin:0 0 12px 0;text-align:center;">
|
||||||
SLA em risco
|
SLA em risco
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0 0 8px 0;text-align:center;">
|
||||||
O chamado abaixo está próximo de violar o SLA. Ação necessária!
|
O chamado abaixo esta proximo de violar o SLA.
|
||||||
|
</p>
|
||||||
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0;text-align:center;">
|
||||||
|
<strong>Acao necessaria!</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
${ticketInfoCard({
|
${ticketInfoCard({
|
||||||
|
|
@ -687,17 +855,17 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
assigneeName: data.assigneeName as string,
|
assigneeName: data.assigneeName as string,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div style="background:${COLORS.statusPausedBg};border-radius:8px;padding:16px;margin:16px 0;">
|
<div style="background:${COLORS.warningBg};border-radius:12px;padding:20px;margin:24px 0;border:1px solid ${COLORS.warning};text-align:center;">
|
||||||
<p style="color:${COLORS.statusPaused};font-size:14px;font-weight:600;margin:0 0 8px 0;">
|
<p style="color:${COLORS.warning};font-size:24px;font-weight:700;margin:0 0 4px 0;">
|
||||||
Tempo restante: ${escapeHtml(data.timeRemaining)}
|
${escapeHtml(data.timeRemaining)}
|
||||||
</p>
|
</p>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:12px;margin:0;">
|
<p style="color:${COLORS.textSecondary};font-size:13px;margin:0;">
|
||||||
Prazo: ${formatDate(data.dueAt as string)}
|
Prazo: ${formatDate(data.dueAt as string)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:24px;">
|
<div style="text-align:center;">
|
||||||
${button("Ver chamado", viewUrl)}
|
${buttonPrimary("Ver chamado", viewUrl)}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
|
|
@ -710,11 +878,20 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
|
|
||||||
return baseTemplate(
|
return baseTemplate(
|
||||||
`
|
`
|
||||||
<h1 style="color:${COLORS.priorityUrgent};font-size:24px;font-weight:600;margin:0 0 8px 0;">
|
<div style="text-align:center;margin-bottom:24px;">
|
||||||
|
<div style="display:inline-block;width:64px;height:64px;background:${COLORS.errorBg};border-radius:50%;line-height:64px;border:2px solid ${COLORS.error};">
|
||||||
|
<span style="font-size:28px;">❌</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style="color:${COLORS.error};font-size:26px;font-weight:700;margin:0 0 12px 0;text-align:center;">
|
||||||
SLA violado
|
SLA violado
|
||||||
</h1>
|
</h1>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:14px;line-height:1.6;margin:0 0 24px 0;">
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0 0 8px 0;text-align:center;">
|
||||||
O chamado abaixo violou o SLA estabelecido. Atenção urgente necessária!
|
O chamado abaixo violou o SLA estabelecido.
|
||||||
|
</p>
|
||||||
|
<p style="color:${COLORS.textSecondary};font-size:15px;line-height:1.7;margin:0;text-align:center;">
|
||||||
|
<strong>Atencao urgente necessaria!</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
${ticketInfoCard({
|
${ticketInfoCard({
|
||||||
|
|
@ -726,17 +903,17 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
assigneeName: data.assigneeName as string,
|
assigneeName: data.assigneeName as string,
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div style="background:${COLORS.priorityUrgentBg};border-radius:8px;padding:16px;margin:16px 0;">
|
<div style="background:${COLORS.errorBg};border-radius:12px;padding:20px;margin:24px 0;border:1px solid ${COLORS.error};text-align:center;">
|
||||||
<p style="color:${COLORS.priorityUrgent};font-size:14px;font-weight:600;margin:0 0 8px 0;">
|
<p style="color:${COLORS.error};font-size:24px;font-weight:700;margin:0 0 4px 0;">
|
||||||
Tempo excedido: ${escapeHtml(data.timeExceeded)}
|
${escapeHtml(data.timeExceeded)}
|
||||||
</p>
|
</p>
|
||||||
<p style="color:${COLORS.textSecondary};font-size:12px;margin:0;">
|
<p style="color:${COLORS.textSecondary};font-size:13px;margin:0;">
|
||||||
Prazo era: ${formatDate(data.dueAt as string)}
|
Prazo era: ${formatDate(data.dueAt as string)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:24px;">
|
<div style="text-align:center;">
|
||||||
${button("Ver chamado", viewUrl)}
|
${buttonPrimary("Ver chamado", viewUrl)}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
data
|
data
|
||||||
|
|
@ -745,7 +922,7 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Exportação
|
// Exportacao
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -754,13 +931,13 @@ const templates: Record<TemplateName, (data: TemplateData) => string> = {
|
||||||
export function renderTemplate(name: TemplateName, data: TemplateData): string {
|
export function renderTemplate(name: TemplateName, data: TemplateData): string {
|
||||||
const template = templates[name]
|
const template = templates[name]
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new Error(`Template "${name}" não encontrado`)
|
throw new Error(`Template "${name}" nao encontrado`)
|
||||||
}
|
}
|
||||||
return template(data)
|
return template(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retorna a lista de templates disponíveis
|
* Retorna a lista de templates disponiveis
|
||||||
*/
|
*/
|
||||||
export function getAvailableTemplates(): TemplateName[] {
|
export function getAvailableTemplates(): TemplateName[] {
|
||||||
return Object.keys(templates) as TemplateName[]
|
return Object.keys(templates) as TemplateName[]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue