feat: melhora página de perfil e integra preferências de notificação

- Atualiza cores das badges para padrão cyan do projeto
- Adiciona degradê no header do card de perfil
- Implementa upload de foto de perfil via API Convex
- Integra notificações do Convex com preferências do usuário
- Cria API /api/notifications/send para verificar preferências
- Melhora layout das páginas de login/recuperação com degradê
- Adiciona badge "Helpdesk" e título "Raven" consistente

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-15 11:00:02 -03:00
parent 1bc08d3a5f
commit ab7dfa81ca
8 changed files with 543 additions and 59 deletions

View file

@ -8,6 +8,45 @@ import { v } from "convex/values"
import { renderSimpleNotificationEmailHtml } from "./reactEmail"
import { buildBaseUrl } from "./url"
// API do Next.js para verificar preferências
async function sendViaNextApi(params: {
type: string
to: { email: string; name?: string; userId?: string }
subject: string
data: Record<string, unknown>
tenantId?: string
}): Promise<{ success: boolean; skipped?: boolean; reason?: string }> {
const baseUrl = buildBaseUrl()
const token = process.env.INTERNAL_HEALTH_TOKEN ?? process.env.REPORTS_CRON_SECRET
if (!token) {
console.warn("[ticketNotifications] Token interno não configurado, enviando diretamente")
return { success: false, reason: "no_token" }
}
try {
const response = await fetch(`${baseUrl}/api/notifications/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(params),
})
if (!response.ok) {
const error = await response.text()
console.error("[ticketNotifications] Erro na API:", error)
return { success: false, reason: "api_error" }
}
return await response.json()
} catch (error) {
console.error("[ticketNotifications] Erro ao chamar API:", error)
return { success: false, reason: "fetch_error" }
}
}
function b64(input: string) {
return Buffer.from(input, "utf8").toString("base64")
}
@ -284,17 +323,15 @@ async function sendSmtpMail(cfg: SmtpConfig, to: string, subject: string, html:
export const sendTicketCreatedEmail = action({
args: {
to: v.string(),
userId: v.optional(v.string()),
userName: v.optional(v.string()),
ticketId: v.string(),
reference: v.number(),
subject: v.string(),
priority: v.string(),
tenantId: v.optional(v.string()),
},
handler: async (_ctx, { to, ticketId, reference, subject, priority }) => {
const smtp = buildSmtpConfig()
if (!smtp) {
console.warn("SMTP not configured; skipping ticket created email")
return { skipped: true }
}
handler: async (_ctx, { to, userId, userName, ticketId, reference, subject, priority, tenantId }) => {
const baseUrl = buildBaseUrl()
const url = `${baseUrl}/portal/tickets/${ticketId}`
@ -305,8 +342,34 @@ export const sendTicketCreatedEmail = action({
URGENT: "Urgente",
}
const priorityLabel = priorityLabels[priority] ?? priority
const mailSubject = `Novo chamado #${reference} aberto`
// Tenta usar a API do Next.js para verificar preferências
const apiResult = await sendViaNextApi({
type: "ticket_created",
to: { email: to, name: userName, userId },
subject: mailSubject,
data: {
reference,
subject,
status: "Pendente",
priority: priorityLabel,
viewUrl: url,
},
tenantId,
})
if (apiResult.success || apiResult.skipped) {
return apiResult
}
// Fallback: envia diretamente se a API falhar
const smtp = buildSmtpConfig()
if (!smtp) {
console.warn("SMTP not configured; skipping ticket created email")
return { skipped: true }
}
const html = await renderSimpleNotificationEmailHtml({
title: `Novo chamado #${reference} aberto`,
message: `Seu chamado foi registrado com sucesso. Nossa equipe irá analisá-lo em breve.\n\nAssunto: ${subject}\nPrioridade: ${priorityLabel}\nStatus: Pendente`,
@ -321,22 +384,45 @@ export const sendTicketCreatedEmail = action({
export const sendPublicCommentEmail = action({
args: {
to: v.string(),
userId: v.optional(v.string()),
userName: v.optional(v.string()),
ticketId: v.string(),
reference: v.number(),
subject: v.string(),
tenantId: v.optional(v.string()),
},
handler: async (_ctx, { to, ticketId, reference, subject }) => {
handler: async (_ctx, { to, userId, userName, ticketId, reference, subject, tenantId }) => {
const baseUrl = buildBaseUrl()
const url = `${baseUrl}/portal/tickets/${ticketId}`
const mailSubject = `Atualização no chamado #${reference}: ${subject}`
// Tenta usar a API do Next.js para verificar preferências
const apiResult = await sendViaNextApi({
type: "comment_public",
to: { email: to, name: userName, userId },
subject: mailSubject,
data: {
reference,
subject,
viewUrl: url,
},
tenantId,
})
if (apiResult.success || apiResult.skipped) {
return apiResult
}
// Fallback: envia diretamente se a API falhar
const smtp = buildSmtpConfig()
if (!smtp) {
console.warn("SMTP not configured; skipping ticket comment email")
return { skipped: true }
}
const baseUrl = buildBaseUrl()
const url = `${baseUrl}/portal/tickets/${ticketId}`
const mailSubject = `Atualização no chamado #${reference}: ${subject}`
const html = await renderSimpleNotificationEmailHtml({
title: `Nova atualização no seu chamado #${reference}`,
message: `Um novo comentário foi adicionado ao chamado “${subject}”. Clique abaixo para visualizar e responder pelo portal.`,
message: `Um novo comentário foi adicionado ao chamado "${subject}". Clique abaixo para visualizar e responder pelo portal.`,
ctaLabel: "Abrir e responder",
ctaUrl: url,
})
@ -348,22 +434,45 @@ export const sendPublicCommentEmail = action({
export const sendResolvedEmail = action({
args: {
to: v.string(),
userId: v.optional(v.string()),
userName: v.optional(v.string()),
ticketId: v.string(),
reference: v.number(),
subject: v.string(),
tenantId: v.optional(v.string()),
},
handler: async (_ctx, { to, ticketId, reference, subject }) => {
handler: async (_ctx, { to, userId, userName, ticketId, reference, subject, tenantId }) => {
const baseUrl = buildBaseUrl()
const url = `${baseUrl}/portal/tickets/${ticketId}`
const mailSubject = `Seu chamado #${reference} foi encerrado`
// Tenta usar a API do Next.js para verificar preferências
const apiResult = await sendViaNextApi({
type: "ticket_resolved",
to: { email: to, name: userName, userId },
subject: mailSubject,
data: {
reference,
subject,
viewUrl: url,
},
tenantId,
})
if (apiResult.success || apiResult.skipped) {
return apiResult
}
// Fallback: envia diretamente se a API falhar
const smtp = buildSmtpConfig()
if (!smtp) {
console.warn("SMTP not configured; skipping ticket resolution email")
return { skipped: true }
}
const baseUrl = buildBaseUrl()
const url = `${baseUrl}/portal/tickets/${ticketId}`
const mailSubject = `Seu chamado #${reference} foi encerrado`
const html = await renderSimpleNotificationEmailHtml({
title: `Chamado #${reference} encerrado`,
message: `O chamado “${subject}” foi marcado como concluído. Caso necessário, você pode responder pelo portal para reabrir dentro do prazo.`,
message: `O chamado "${subject}" foi marcado como concluído. Caso necessário, você pode responder pelo portal para reabrir dentro do prazo.`,
ctaLabel: "Ver detalhes",
ctaUrl: url,
})