From eedd446b36d63ebc335f7a64ad5d5d8d4ca39352 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Mon, 15 Dec 2025 19:52:46 -0300 Subject: [PATCH] fix: corrige hydration, notificacoes e melhora visual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Corrige hydration mismatch no Toaster (sonner) e ChatWidgetProvider - Corrige API de preferencias de notificacao (typePreferences como string) - Melhora visual do Switch (estado ativo em preto) - Adiciona icones em circulos na pagina de notificacoes - Corrige acentuacao em "Obrigatorio" - Corrige centralizacao das estrelas de avaliacao nos e-mails - Aplica Sentence case nos titulos dos templates de e-mail - Adiciona script de teste de e-mail 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- scripts/test-email.ts | 209 ++++++++++++++++++ .../api/notifications/preferences/route.ts | 4 +- src/components/chat/chat-widget-provider.tsx | 10 +- .../notification-preferences-form.tsx | 77 ++++--- src/components/ui/sonner.tsx | 9 + src/components/ui/switch.tsx | 4 +- src/server/email/email-templates.ts | 44 ++-- 7 files changed, 302 insertions(+), 55 deletions(-) create mode 100644 scripts/test-email.ts diff --git a/scripts/test-email.ts b/scripts/test-email.ts new file mode 100644 index 0000000..64a375e --- /dev/null +++ b/scripts/test-email.ts @@ -0,0 +1,209 @@ +/** + * Script para testar envio de e-mail + * Uso: bun scripts/test-email.ts [destinatario] + */ + +import { sendSmtpMail } from "../src/server/email-smtp" +import { renderTemplate } from "../src/server/email/email-templates" + +const DESTINATARIO = process.argv[2] || "renan.pac@paulicon.com.br" + +// Credenciais do SMTP (usando as da documentacao) +const smtpConfig = { + host: "smtp.c.inova.com.br", + port: 587, + username: "envio@rever.com.br", + password: "CAAJQm6ZT6AUdhXRTDYu", + from: '"Sistema de Chamados" ', + starttls: true, + tls: false, + rejectUnauthorized: false, + timeoutMs: 15000, +} + +async function testEmail() { + console.log("=".repeat(50)) + console.log("TESTE DE ENVIO DE E-MAIL") + console.log("=".repeat(50)) + console.log(`Destinatario: ${DESTINATARIO}`) + console.log(`SMTP: ${smtpConfig.host}:${smtpConfig.port}`) + console.log("") + + // 1. Teste basico + console.log("[1/10] Enviando e-mail de teste basico...") + try { + const html = renderTemplate("test", { + title: "Teste do Sistema de E-mail", + message: "Este e-mail confirma que o sistema de notificacoes esta funcionando corretamente.", + timestamp: new Date().toLocaleString("pt-BR", { timeZone: "America/Sao_Paulo" }), + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Teste - Sistema de Chamados Raven", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 2. Teste de abertura de chamado + console.log("\n[2/10] Enviando notificacao de abertura de chamado...") + try { + const html = renderTemplate("ticket_created", { + reference: 12345, + subject: "Problema no sistema de vendas", + status: "PENDING", + priority: "HIGH", + createdAt: new Date().toISOString(), + viewUrl: "https://tickets.esdrasrenan.com.br/tickets/12345", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Chamado #12345 Aberto - Problema no sistema de vendas", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 3. Teste de resolucao de chamado + console.log("\n[3/10] Enviando notificacao de resolucao...") + try { + const html = renderTemplate("ticket_resolved", { + reference: 12345, + subject: "Problema no sistema de vendas", + assigneeName: "Joao Silva", + resolutionSummary: "O problema foi identificado como uma configuracao incorreta no modulo de pagamentos. A configuracao foi corrigida e o sistema esta funcionando normalmente.", + viewUrl: "https://tickets.esdrasrenan.com.br/tickets/12345", + rateUrl: "https://tickets.esdrasrenan.com.br/rate/12345", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Chamado #12345 Resolvido - Problema no sistema de vendas", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 4. Teste de comentario + console.log("\n[4/10] Enviando notificacao de comentario...") + try { + const html = renderTemplate("ticket_comment", { + reference: 12345, + subject: "Problema no sistema de vendas", + authorName: "Joao Silva", + commentBody: "Estou analisando o problema e em breve envio uma atualizacao. Por favor, verifique se o erro persiste apos limpar o cache do navegador.", + commentedAt: new Date().toISOString(), + viewUrl: "https://tickets.esdrasrenan.com.br/tickets/12345", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Nova atualizacao no Chamado #12345", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 5. Teste de atribuicao de chamado + console.log("\n[5/10] Enviando notificacao de atribuicao...") + try { + const html = renderTemplate("ticket_assigned", { + reference: 12345, + subject: "Problema no sistema de vendas", + status: "AWAITING_ATTENDANCE", + priority: "HIGH", + requesterName: "Maria Santos", + assigneeName: "Joao Silva", + isForRequester: false, + viewUrl: "https://tickets.esdrasrenan.com.br/tickets/12345", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Chamado #12345 Atribuido", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 6. Teste de mudanca de status + console.log("\n[6/10] Enviando notificacao de mudanca de status...") + try { + const html = renderTemplate("ticket_status", { + reference: 12345, + subject: "Problema no sistema de vendas", + oldStatus: "PENDING", + newStatus: "AWAITING_ATTENDANCE", + viewUrl: "https://tickets.esdrasrenan.com.br/tickets/12345", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Status do Chamado #12345 Alterado", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 7. Teste de reset de senha + console.log("\n[7/10] Enviando notificacao de reset de senha...") + try { + const html = renderTemplate("password_reset", { + resetUrl: "https://tickets.esdrasrenan.com.br/reset-password?token=abc123", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Redefinicao de Senha - Raven", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 8. Teste de convite + console.log("\n[8/10] Enviando notificacao de convite...") + try { + const html = renderTemplate("invite", { + inviterName: "Admin Sistema", + roleName: "Agente", + companyName: "Empresa Teste", + inviteUrl: "https://tickets.esdrasrenan.com.br/invite?token=xyz789", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Voce foi convidado - Raven", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 9. Teste de novo login + console.log("\n[9/10] Enviando notificacao de novo login...") + try { + const html = renderTemplate("new_login", { + loginAt: new Date().toISOString(), + userAgent: "Chrome 120 no Windows 11", + ipAddress: "189.45.123.78", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "Novo Acesso Detectado - Raven", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + // 10. Teste de SLA em risco + console.log("\n[10/10] Enviando notificacao de SLA em risco...") + try { + const html = renderTemplate("sla_warning", { + reference: 12345, + subject: "Problema no sistema de vendas", + status: "AWAITING_ATTENDANCE", + priority: "HIGH", + requesterName: "Maria Santos", + assigneeName: "Joao Silva", + timeRemaining: "2 horas", + dueAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(), + viewUrl: "https://tickets.esdrasrenan.com.br/tickets/12345", + }) + + await sendSmtpMail(smtpConfig, DESTINATARIO, "ALERTA: SLA em Risco - Chamado #12345", html) + console.log(" SUCESSO!") + } catch (error) { + console.error(" ERRO:", (error as Error).message) + } + + console.log("\n" + "=".repeat(50)) + console.log("TESTE CONCLUIDO - 10 TIPOS DE NOTIFICACAO") + console.log("=".repeat(50)) + console.log(`Verifique a caixa de entrada de: ${DESTINATARIO}`) +} + +testEmail().catch(console.error) diff --git a/src/app/api/notifications/preferences/route.ts b/src/app/api/notifications/preferences/route.ts index 851a313..8255123 100644 --- a/src/app/api/notifications/preferences/route.ts +++ b/src/app/api/notifications/preferences/route.ts @@ -69,8 +69,8 @@ export async function GET(_request: NextRequest) { tenantId: user.tenantId, emailEnabled: true, digestFrequency: "immediate", - typePreferences: {}, - categoryPreferences: {}, + typePreferences: "{}", + categoryPreferences: "{}", }, }) } diff --git a/src/components/chat/chat-widget-provider.tsx b/src/components/chat/chat-widget-provider.tsx index 28a5e38..3993127 100644 --- a/src/components/chat/chat-widget-provider.tsx +++ b/src/components/chat/chat-widget-provider.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect, useState } from "react" import dynamic from "next/dynamic" import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" @@ -17,12 +18,19 @@ function checkLiveChatApiExists() { // Importacao dinamica para evitar problemas de SSR const ChatWidget = dynamic( () => import("./chat-widget").then((mod) => ({ default: mod.ChatWidget })), - { ssr: false } + { ssr: false, loading: () => null } ) export function ChatWidgetProvider() { const { role, isLoading } = useAuth() + const [mounted, setMounted] = useState(false) + useEffect(() => { + setMounted(true) + }, []) + + // Evita hydration mismatch - so renderiza apos montar no cliente + if (!mounted) return null if (isLoading) return null if (!isAgentOrAdmin(role)) return null diff --git a/src/components/settings/notification-preferences-form.tsx b/src/components/settings/notification-preferences-form.tsx index 91c1a42..78fdf0a 100644 --- a/src/components/settings/notification-preferences-form.tsx +++ b/src/components/settings/notification-preferences-form.tsx @@ -12,6 +12,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Separator } from "@/components/ui/separator" import { Badge } from "@/components/ui/badge" import { Input } from "@/components/ui/input" +import { cn } from "@/lib/utils" interface NotificationType { type: string @@ -304,37 +305,57 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr

{group.description}

- {types.map((notifType) => ( -
-
- {notifType.required ? ( - - ) : localTypePrefs[notifType.type] ? ( - - ) : ( - + {types.map((notifType) => { + const isEnabled = localTypePrefs[notifType.type] ?? notifType.enabled + return ( +
- - {notifType.required && ( - - Obrigatório - - )} + > +
+
+ {notifType.required ? ( + + ) : isEnabled ? ( + + ) : ( + + )} +
+
+ + {notifType.required && ( + + Obrigatório + + )} +
+ toggleType(notifType.type, checked)} + disabled={!notifType.canDisable} + />
- toggleType(notifType.type, checked)} - disabled={!notifType.canDisable} - /> -
- ))} + ) + })}
) diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 3fc2ee2..58382bc 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,10 +1,19 @@ "use client" +import { useEffect, useState } from "react" import { useTheme } from "next-themes" import { Toaster as Sonner, ToasterProps } from "sonner" const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + // Evita hydration mismatch - so renderiza apos montar no cliente + if (!mounted) return null const baseClass = "inline-flex w-auto min-w-0 items-center justify-center gap-2 self-center rounded-xl border border-black bg-black px-3 py-2 text-sm font-medium text-white shadow-lg" diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 4ea9461..b476850 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -12,14 +12,14 @@ const Switch = React.forwardRef< diff --git a/src/server/email/email-templates.ts b/src/server/email/email-templates.ts index c06f365..efaabfa 100644 --- a/src/server/email/email-templates.ts +++ b/src/server/email/email-templates.ts @@ -236,12 +236,12 @@ function ratingStars(rateUrl: string): string { } return ` - +
${stars.join("")}
-

Clique em uma estrela para avaliar

+

Clique em uma estrela para avaliar

` } @@ -353,7 +353,7 @@ const templates: Record string> = { return baseTemplate( `

- Chamado Aberto + Chamado aberto

Seu chamado foi registrado com sucesso. Nossa equipe irá analisá-lo em breve. @@ -368,7 +368,7 @@ const templates: Record string> = { })}

- ${button("Ver Chamado", viewUrl)} + ${button("Ver chamado", viewUrl)}
`, data @@ -383,7 +383,7 @@ const templates: Record string> = { return baseTemplate( `

- Chamado Resolvido + Chamado resolvido

Seu chamado foi marcado como resolvido. Esperamos que o atendimento tenha sido satisfatório! @@ -414,7 +414,7 @@ const templates: Record string> = {

- ${button("Ver Chamado", viewUrl)} + ${button("Ver chamado", viewUrl)}
`, data @@ -426,7 +426,7 @@ const templates: Record string> = { const viewUrl = data.viewUrl as string const isForRequester = data.isForRequester as boolean - const title = isForRequester ? "Agente Atribuído ao Chamado" : "Novo Chamado Atribuído" + const title = isForRequester ? "Agente atribuído ao chamado" : "Novo chamado atribuído" const message = isForRequester ? `O agente ${escapeHtml(data.assigneeName)} foi atribuído ao seu chamado e em breve entrará em contato.` : `Um novo chamado foi atribuído a você. Por favor, verifique os detalhes abaixo.` @@ -450,7 +450,7 @@ const templates: Record string> = { })}
- ${button("Ver Chamado", viewUrl)} + ${button("Ver chamado", viewUrl)}
`, data @@ -466,7 +466,7 @@ const templates: Record string> = { return baseTemplate( `

- Status Atualizado + Status atualizado

O status do seu chamado foi alterado. @@ -492,7 +492,7 @@ const templates: Record string> = {

- ${button("Ver Chamado", viewUrl)} + ${button("Ver chamado", viewUrl)}
`, data @@ -506,7 +506,7 @@ const templates: Record string> = { return baseTemplate( `

- Nova Atualização no Chamado + Nova atualização no chamado

${escapeHtml(data.authorName)} adicionou um comentário ao seu chamado. @@ -527,7 +527,7 @@ const templates: Record string> = {

- ${button("Ver Chamado", viewUrl)} + ${button("Ver chamado", viewUrl)}
`, data @@ -541,14 +541,14 @@ const templates: Record string> = { return baseTemplate( `

- Redefinição de Senha + Redefinição de senha

Recebemos uma solicitação para redefinir a senha da sua conta. Se você não fez essa solicitação, pode ignorar este e-mail.

- ${button("Redefinir Senha", resetUrl)} + ${button("Redefinir senha", resetUrl)}

@@ -567,14 +567,14 @@ const templates: Record string> = { return baseTemplate( `

- Confirme seu E-mail + Confirme seu e-mail

Clique no botão abaixo para confirmar seu endereço de e-mail e ativar sua conta.

- ${button("Confirmar E-mail", verifyUrl)} + ${button("Confirmar e-mail", verifyUrl)}

@@ -618,7 +618,7 @@ const templates: Record string> = {

- ${button("Aceitar Convite", inviteUrl)} + ${button("Aceitar convite", inviteUrl)}

@@ -634,7 +634,7 @@ const templates: Record string> = { return baseTemplate( `

- Novo Acesso Detectado + Novo acesso detectado

Detectamos um novo acesso à sua conta. Se foi você, pode ignorar este e-mail. @@ -672,7 +672,7 @@ const templates: Record string> = { return baseTemplate( `

- SLA em Risco + SLA em risco

O chamado abaixo está próximo de violar o SLA. Ação necessária! @@ -697,7 +697,7 @@ const templates: Record string> = {

- ${button("Ver Chamado", viewUrl)} + ${button("Ver chamado", viewUrl)}
`, data @@ -711,7 +711,7 @@ const templates: Record string> = { return baseTemplate( `

- SLA Violado + SLA violado

O chamado abaixo violou o SLA estabelecido. Atenção urgente necessária! @@ -736,7 +736,7 @@ const templates: Record string> = {

- ${button("Ver Chamado", viewUrl)} + ${button("Ver chamado", viewUrl)}
`, data