fix: corrige tipo JSON para String no SQLite e acentuação nos textos

- Altera typePreferences e categoryPreferences de Json para String no Prisma
- Atualiza API de preferências para fazer parse/stringify de JSON
- Corrige todos os textos sem acentuação nos componentes de notificação

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-07 21:09:02 -03:00
parent f2c0298285
commit 7ecb4c1110
7 changed files with 104 additions and 89 deletions

View file

@ -462,11 +462,11 @@ model NotificationPreferences {
// Preferências por tipo de notificação (JSON)
// Ex: { "ticket_created": true, "ticket_resolved": true, "comment_public": false }
typePreferences Json @default("{}")
typePreferences String @default("{}")
// Preferências por categoria de ticket (JSON)
// Ex: { "category_id_1": true, "category_id_2": false }
categoryPreferences Json @default("{}")
categoryPreferences String @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View file

@ -80,10 +80,17 @@ export async function GET(_request: NextRequest) {
? Object.keys(NOTIFICATION_TYPES)
: COLLABORATOR_VISIBLE_TYPES
// Parse JSON strings
const typePrefs: Record<string, boolean> = prefs!.typePreferences
? JSON.parse(prefs!.typePreferences as string)
: {}
const catPrefs = prefs!.categoryPreferences
? JSON.parse(prefs!.categoryPreferences as string)
: {}
// Monta resposta com configuração de cada tipo
const typeConfigs = availableTypes.map((type) => {
const config = NOTIFICATION_TYPES[type as NotificationType]
const typePrefs = prefs!.typePreferences as Record<string, boolean>
const enabled = typePrefs[type] ?? config.defaultEnabled
return {
@ -104,7 +111,7 @@ export async function GET(_request: NextRequest) {
timezone: prefs.timezone,
digestFrequency: prefs.digestFrequency,
types: typeConfigs,
categoryPreferences: prefs.categoryPreferences,
categoryPreferences: catPrefs,
isStaff,
})
} catch (error) {
@ -194,11 +201,19 @@ export async function PUT(request: NextRequest) {
quietHoursEnd: quietHoursEnd ?? null,
timezone: timezone ?? "America/Sao_Paulo",
digestFrequency: digestFrequency ?? "immediate",
typePreferences: validatedTypePrefs,
categoryPreferences: categoryPreferences ?? {},
typePreferences: JSON.stringify(validatedTypePrefs),
categoryPreferences: JSON.stringify(categoryPreferences ?? {}),
},
})
} else {
// Parse existing JSON strings
const existingTypePrefs = existingPrefs.typePreferences
? JSON.parse(existingPrefs.typePreferences as string)
: {}
const existingCatPrefs = existingPrefs.categoryPreferences
? JSON.parse(existingPrefs.categoryPreferences as string)
: {}
// Atualiza preferências existentes
await prisma.notificationPreferences.update({
where: { userId },
@ -208,11 +223,11 @@ export async function PUT(request: NextRequest) {
quietHoursEnd: quietHoursEnd !== undefined ? quietHoursEnd : existingPrefs.quietHoursEnd,
timezone: timezone ?? existingPrefs.timezone,
digestFrequency: digestFrequency ?? existingPrefs.digestFrequency,
typePreferences: {
...(existingPrefs.typePreferences as Record<string, boolean>),
typePreferences: JSON.stringify({
...existingTypePrefs,
...validatedTypePrefs,
},
categoryPreferences: categoryPreferences ?? existingPrefs.categoryPreferences,
}),
categoryPreferences: JSON.stringify(categoryPreferences ?? existingCatPrefs),
},
})
}

View file

@ -5,8 +5,8 @@ import { NotificationPreferencesForm } from "@/components/settings/notification-
import { requireAuthenticatedSession } from "@/lib/auth-server"
export const metadata: Metadata = {
title: "Preferencias de notificacao",
description: "Configure quais notificacoes por e-mail deseja receber.",
title: "Preferências de notificação",
description: "Configure quais notificações por e-mail deseja receber.",
}
export default async function PortalNotificationSettingsPage() {
@ -14,7 +14,7 @@ export default async function PortalNotificationSettingsPage() {
const role = (session.user.role ?? "").toLowerCase()
const persona = (session.user.machinePersona ?? "").toLowerCase()
// Colaboradores e maquinas com persona de colaborador podem acessar
// Colaboradores e máquinas com persona de colaborador podem acessar
const allowedRoles = new Set(["collaborator", "manager", "admin", "agent"])
const isMachinePersonaAllowed = role === "machine" && (persona === "collaborator" || persona === "manager")
const allowed = allowedRoles.has(role) || isMachinePersonaAllowed
@ -23,7 +23,7 @@ export default async function PortalNotificationSettingsPage() {
redirect("/portal")
}
// Staff deve usar a pagina de configuracoes completa
// Staff deve usar a página de configurações completa
const staffRoles = new Set(["admin", "manager", "agent"])
if (staffRoles.has(role)) {
redirect("/settings/notifications")
@ -32,9 +32,9 @@ export default async function PortalNotificationSettingsPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Preferencias de notificacao</h1>
<h1 className="text-2xl font-semibold tracking-tight">Preferências de notificação</h1>
<p className="text-muted-foreground">
Configure quais notificacoes por e-mail deseja receber sobre seus chamados.
Configure quais notificações por e-mail deseja receber sobre seus chamados.
</p>
</div>
<NotificationPreferencesForm isPortal />

View file

@ -28,7 +28,7 @@ export default function RatePage() {
const [comment, setComment] = useState("")
const [error, setError] = useState<string | null>(null)
// Se ja avaliou, mostra mensagem
// Se já avaliou, mostra mensagem
if (alreadyRated && existingRating) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
@ -37,9 +37,9 @@ export default function RatePage() {
<div className="flex justify-center mb-4">
<CheckCircle className="h-16 w-16 text-emerald-500" />
</div>
<CardTitle>Chamado ja avaliado</CardTitle>
<CardTitle>Chamado já avaliado</CardTitle>
<CardDescription>
Voce ja avaliou este chamado anteriormente.
Você avaliou este chamado anteriormente.
</CardDescription>
</CardHeader>
<CardContent className="text-center">
@ -54,7 +54,7 @@ export default function RatePage() {
))}
</div>
<p className="text-muted-foreground text-sm">
Sua avaliacao: {existingRating} estrela{existingRating > 1 ? "s" : ""}
Sua avaliação: {existingRating} estrela{existingRating > 1 ? "s" : ""}
</p>
</CardContent>
</Card>
@ -62,7 +62,7 @@ export default function RatePage() {
)
}
// Se acabou de avaliar, mostra formulario para comentario
// Se acabou de avaliar, mostra formulário para comentário
if (submitted && rating > 0) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
@ -71,9 +71,9 @@ export default function RatePage() {
<div className="flex justify-center mb-4">
<CheckCircle className="h-16 w-16 text-emerald-500" />
</div>
<CardTitle>Obrigado pela avaliacao!</CardTitle>
<CardTitle>Obrigado pela avaliação!</CardTitle>
<CardDescription>
Sua opiniao e muito importante para nos.
Sua opinião é muito importante para s.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
@ -89,10 +89,10 @@ export default function RatePage() {
</div>
<div className="space-y-2">
<Label htmlFor="comment">Gostaria de deixar um comentario? (opcional)</Label>
<Label htmlFor="comment">Gostaria de deixar um comentário? (opcional)</Label>
<Textarea
id="comment"
placeholder="Conte-nos mais sobre sua experiencia..."
placeholder="Conte-nos mais sobre sua experiência..."
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={4}
@ -134,7 +134,7 @@ export default function RatePage() {
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || "Erro ao enviar comentario")
throw new Error(data.error || "Erro ao enviar comentário")
}
window.close()
@ -151,7 +151,7 @@ export default function RatePage() {
Enviando...
</>
) : (
"Enviar comentario"
"Enviar comentário"
)}
</Button>
</div>
@ -161,7 +161,7 @@ export default function RatePage() {
)
}
// Formulario de avaliacao
// Formulário de avaliação
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md">
@ -174,7 +174,7 @@ export default function RatePage() {
</div>
<CardTitle>Como foi o atendimento?</CardTitle>
<CardDescription>
Sua avaliacao nos ajuda a melhorar nosso servico.
Sua avaliação nos ajuda a melhorar nosso serviço.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
@ -206,12 +206,12 @@ export default function RatePage() {
<span>Excelente</span>
</div>
{/* Comentario */}
{/* Comentário */}
<div className="space-y-2">
<Label htmlFor="comment">Comentario (opcional)</Label>
<Label htmlFor="comment">Comentário (opcional)</Label>
<Textarea
id="comment"
placeholder="Conte-nos mais sobre sua experiencia..."
placeholder="Conte-nos mais sobre sua experiência..."
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={4}
@ -225,7 +225,7 @@ export default function RatePage() {
</div>
)}
{/* Botao */}
{/* Botão */}
<Button
className="w-full"
size="lg"
@ -243,7 +243,7 @@ export default function RatePage() {
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || "Erro ao enviar avaliacao")
throw new Error(data.error || "Erro ao enviar avaliação")
}
setSubmitted(true)
@ -260,7 +260,7 @@ export default function RatePage() {
Enviando...
</>
) : (
"Enviar avaliacao"
"Enviar avaliação"
)}
</Button>
</CardContent>

View file

@ -7,15 +7,15 @@ import { NotificationPreferencesForm } from "@/components/settings/notification-
import { requireAuthenticatedSession } from "@/lib/auth-server"
export const metadata: Metadata = {
title: "Preferencias de notificacao",
description: "Configure quais notificacoes por e-mail deseja receber.",
title: "Preferências de notificação",
description: "Configure quais notificações por e-mail deseja receber.",
}
export default async function NotificationSettingsPage() {
const session = await requireAuthenticatedSession()
const role = (session.user.role ?? "").toLowerCase()
// Apenas staff pode acessar esta pagina
// Apenas staff pode acessar esta página
const staffRoles = new Set(["admin", "manager", "agent"])
if (!staffRoles.has(role)) {
redirect("/portal/profile/notifications")
@ -25,8 +25,8 @@ export default async function NotificationSettingsPage() {
<AppShell
header={
<SiteHeader
title="Preferencias de notificacao"
lead="Configure como e quando deseja receber notificacoes por e-mail"
title="Preferências de notificação"
lead="Configure como e quando deseja receber notificações por e-mail"
/>
}
>

View file

@ -114,7 +114,7 @@ export default function TicketViewPage() {
try {
window.location.href = ravenUrl
} catch {
// Protocolo nao suportado
// Protocolo não suportado
}
// Aguarda 2 segundos para ver se o protocolo foi aceito
@ -123,7 +123,7 @@ export default function TicketViewPage() {
document.body.removeChild(iframe)
if (!protocolHandled) {
// Protocolo nao foi aceito, carrega o ticket no navegador
// Protocolo não foi aceito, carrega o ticket no navegador
setTryingRaven(false)
loadTicket()
}
@ -153,10 +153,10 @@ export default function TicketViewPage() {
<Loader2 className="h-12 w-12 animate-spin mx-auto mb-4 text-primary" />
<h2 className="text-xl font-semibold mb-2">Abrindo no Raven...</h2>
<p className="text-muted-foreground text-sm">
Se o aplicativo Raven estiver instalado, ele abrira automaticamente.
Se o aplicativo Raven estiver instalado, ele abrirá automaticamente.
</p>
<p className="text-muted-foreground text-sm mt-2">
Caso contrario, o chamado sera exibido aqui em instantes.
Caso contrário, o chamado será exibido aqui em instantes.
</p>
</CardContent>
</Card>
@ -243,15 +243,15 @@ export default function TicketViewPage() {
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Informacoes */}
{/* Informações */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Solicitante</span>
<p className="font-medium">{ticket.requester.name}</p>
</div>
<div>
<span className="text-muted-foreground">Responsavel</span>
<p className="font-medium">{ticket.assignee?.name ?? "Nao atribuido"}</p>
<span className="text-muted-foreground">Responsável</span>
<p className="font-medium">{ticket.assignee?.name ?? "Não atribuído"}</p>
</div>
<div>
<span className="text-muted-foreground">Criado em</span>
@ -274,17 +274,17 @@ export default function TicketViewPage() {
{/* Resumo */}
{ticket.summary && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">Descricao</h3>
<h3 className="text-sm font-medium text-muted-foreground mb-2">Descrição</h3>
<p className="text-sm whitespace-pre-wrap">{ticket.summary}</p>
</div>
)}
{/* Avaliacao */}
{/* Avaliação */}
{ticket.rating && (
<div className="bg-muted/50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="h-4 w-4 text-emerald-500" />
<span className="text-sm font-medium">Avaliacao</span>
<span className="text-sm font-medium">Avaliação</span>
</div>
<div className="flex items-center gap-1 mb-2">
{[1, 2, 3, 4, 5].map((star) => (
@ -302,12 +302,12 @@ export default function TicketViewPage() {
</div>
)}
{/* Comentarios */}
{/* Comentários */}
{ticket.comments.length > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-4 flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
Ultimas atualizacoes
Últimas atualizações
</h3>
<div className="space-y-4">
{ticket.comments.map((comment) => (

View file

@ -36,26 +36,26 @@ interface NotificationPreferencesFormProps {
isPortal?: boolean
}
// Agrupamento de tipos de notificacao
// Agrupamento de tipos de notificação
const TYPE_GROUPS = {
lifecycle: {
label: "Ciclo de vida do chamado",
description: "Notificacoes sobre abertura, resolucao e mudancas de status",
description: "Notificações sobre abertura, resolução e mudanças de status",
types: ["ticket_created", "ticket_assigned", "ticket_resolved", "ticket_reopened", "ticket_status_changed", "ticket_priority_changed"],
},
communication: {
label: "Comunicacao",
description: "Comentarios e respostas nos chamados",
label: "Comunicação",
description: "Comentários e respostas nos chamados",
types: ["comment_public", "comment_response", "comment_mention"],
},
sla: {
label: "SLA e alertas",
description: "Alertas de prazo e metricas",
description: "Alertas de prazo e métricas",
types: ["sla_at_risk", "sla_breached", "sla_daily_digest"],
},
security: {
label: "Seguranca",
description: "Notificacoes de autenticacao e acesso",
label: "Segurança",
description: "Notificações de autenticação e acesso",
types: ["security_password_reset", "security_email_verify", "security_email_change", "security_new_login", "security_invite"],
},
}
@ -74,20 +74,20 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
try {
const response = await fetch("/api/notifications/preferences")
if (!response.ok) {
throw new Error("Erro ao carregar preferencias")
throw new Error("Erro ao carregar preferências")
}
const data = await response.json()
setPreferences(data)
// Inicializa preferencias locais
// Inicializa preferências locais
const typePrefs: Record<string, boolean> = {}
data.types.forEach((t: NotificationType) => {
typePrefs[t.type] = t.enabled
})
setLocalTypePrefs(typePrefs)
} catch (error) {
console.error("Erro ao carregar preferencias:", error)
toast.error("Nao foi possivel carregar suas preferencias")
console.error("Erro ao carregar preferências:", error)
toast.error("Não foi possível carregar suas preferências")
} finally {
setLoading(false)
}
@ -112,13 +112,13 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
})
if (!response.ok) {
throw new Error("Erro ao salvar preferencias")
throw new Error("Erro ao salvar preferências")
}
toast.success("Preferencias salvas com sucesso")
toast.success("Preferências salvas com sucesso")
} catch (error) {
console.error("Erro ao salvar preferencias:", error)
toast.error("Nao foi possivel salvar suas preferencias")
console.error("Erro ao salvar preferências:", error)
toast.error("Não foi possível salvar suas preferências")
} finally {
setSaving(false)
}
@ -153,7 +153,7 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
<Card>
<CardContent className="py-8">
<p className="text-center text-muted-foreground">
Nao foi possivel carregar suas preferencias de notificacao.
Não foi possível carregar suas preferências de notificação.
</p>
<div className="flex justify-center mt-4">
<Button onClick={loadPreferences}>Tentar novamente</Button>
@ -163,7 +163,7 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
)
}
// Filtra tipos visiveis para o usuario
// Filtra tipos visíveis para o usuário
const visibleTypes = preferences.types
// Agrupa tipos por categoria
@ -177,15 +177,15 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
return (
<div className="space-y-6">
{/* Configuracao global de e-mail */}
{/* Configuração global de e-mail */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Notificacoes por e-mail
Notificações por e-mail
</CardTitle>
<CardDescription>
Controle se deseja receber notificacoes por e-mail.
Controle se deseja receber notificações por e-mail.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
@ -194,8 +194,8 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
<Label className="text-base">Receber e-mails</Label>
<p className="text-sm text-muted-foreground">
{preferences.emailEnabled
? "Voce recebera notificacoes por e-mail conforme suas preferencias"
: "Todas as notificacoes por e-mail estao desativadas"}
? "Você receberá notificações por e-mail conforme suas preferências"
: "Todas as notificações por e-mail estão desativadas"}
</p>
</div>
<Switch
@ -208,14 +208,14 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
<>
<Separator />
{/* Horario de silencio - apenas staff */}
{/* Horário de silêncio - apenas staff */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<Label className="text-base">Horario de silencio</Label>
<Label className="text-base">Horário de silêncio</Label>
</div>
<p className="text-sm text-muted-foreground">
Durante este periodo, notificacoes nao urgentes serao adiadas.
Durante este período, notificações não urgentes serão adiadas.
</p>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
@ -229,7 +229,7 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="quietEnd" className="text-sm">as</Label>
<Label htmlFor="quietEnd" className="text-sm">às</Label>
<Input
id="quietEnd"
type="time"
@ -252,11 +252,11 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
<Separator />
{/* Frequencia de resumo - apenas staff */}
{/* Frequência de resumo - apenas staff */}
<div className="space-y-4">
<Label className="text-base">Frequencia de resumo</Label>
<Label className="text-base">Frequência de resumo</Label>
<p className="text-sm text-muted-foreground">
Como voce prefere receber as notificacoes nao urgentes.
Como você prefere receber as notificações o urgentes.
</p>
<Select
value={preferences.digestFrequency}
@ -267,7 +267,7 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
</SelectTrigger>
<SelectContent>
<SelectItem value="immediate">Imediato</SelectItem>
<SelectItem value="daily">Resumo diario</SelectItem>
<SelectItem value="daily">Resumo diário</SelectItem>
<SelectItem value="weekly">Resumo semanal</SelectItem>
</SelectContent>
</Select>
@ -277,19 +277,19 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
</CardContent>
</Card>
{/* Tipos de notificacao */}
{/* Tipos de notificação */}
{preferences.emailEnabled && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="h-5 w-5" />
Tipos de notificacao
Tipos de notificação
</CardTitle>
<CardDescription>
Escolha quais tipos de notificacao deseja receber.
Escolha quais tipos de notificação deseja receber.
{!preferences.isStaff && (
<span className="block mt-1 text-amber-600">
Algumas notificacoes sao obrigatorias e nao podem ser desativadas.
Algumas notificações são obrigatórias e o podem ser desativadas.
</span>
)}
</CardDescription>
@ -323,7 +323,7 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
</Label>
{notifType.required && (
<Badge variant="secondary" className="ml-2 text-xs">
Obrigatorio
Obrigatório
</Badge>
)}
</div>
@ -343,7 +343,7 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
</Card>
)}
{/* Botao de salvar */}
{/* Botão de salvar */}
<div className="flex justify-end">
<Button onClick={savePreferences} disabled={saving}>
{saving ? (
@ -352,7 +352,7 @@ export function NotificationPreferencesForm({ isPortal = false }: NotificationPr
Salvando...
</>
) : (
"Salvar preferencias"
"Salvar preferências"
)}
</Button>
</div>