"use client" import { FormEvent, useMemo, useRef, useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" import dynamic from "next/dynamic" import { toast } from "sonner" import { Share2, ShieldCheck, UserPlus, Users2, Layers3, MessageSquareText, BellRing, ClipboardList, LogOut, Mail, Key, User, Shield, Clock, Camera, Loader2, Trash2, } from "lucide-react" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Separator } from "@/components/ui/separator" import { useAuth, signOut } from "@/lib/auth-client" const ShaderBackground = dynamic( () => import("@/components/background-paper-shaders-wrapper"), { ssr: false } ) import type { LucideIcon } from "lucide-react" type RoleRequirement = "admin" | "staff" type SettingsAction = { title: string description: string href: string cta: string requiredRole?: RoleRequirement icon: LucideIcon } const ROLE_LABELS: Record = { admin: "Administrador", manager: "Gestor", agent: "Agente", collaborator: "Colaborador", customer: "Cliente", } const ROLE_COLORS: Record = { admin: "bg-neutral-900 text-white border-neutral-900", manager: "bg-neutral-800 text-white border-neutral-800", agent: "bg-neutral-700 text-white border-neutral-700", collaborator: "bg-neutral-600 text-white border-neutral-600", customer: "bg-neutral-500 text-white border-neutral-500", } const SETTINGS_ACTIONS: SettingsAction[] = [ { title: "Campos personalizados", description: "Configure campos extras para enriquecer os tickets.", href: "/admin/custom-fields", cta: "Configurar", requiredRole: "admin", icon: ClipboardList, }, { title: "Times e equipes", description: "Gerencie times e atribua permissões por equipe.", href: "/admin/teams", cta: "Gerenciar", requiredRole: "admin", icon: Users2, }, { title: "Filas de atendimento", description: "Configure filas e regras de distribuição.", href: "/admin/channels", cta: "Configurar", requiredRole: "admin", icon: Share2, }, { title: "Categorias", description: "Gerencie categorias e formulários de tickets.", href: "/admin/fields", cta: "Gerenciar", requiredRole: "admin", icon: Layers3, }, { title: "Usuários e convites", description: "Convide novos usuários e gerencie acessos.", href: "/admin/users", cta: "Gerenciar", requiredRole: "admin", icon: UserPlus, }, { title: "Templates de comentários", description: "Mensagens rápidas para os atendimentos.", href: "/settings/templates", cta: "Gerenciar", requiredRole: "staff", icon: MessageSquareText, }, { title: "Notificações", description: "Configure quais e-mails deseja receber.", href: "/settings/notifications", cta: "Configurar", requiredRole: "staff", icon: BellRing, }, { title: "Políticas de SLA", description: "Acompanhe e configure níveis de serviço.", href: "/admin/slas", cta: "Gerenciar", requiredRole: "admin", icon: ShieldCheck, }, ] export function SettingsContent() { const { session, isAdmin, isStaff } = useAuth() const [isSigningOut, setIsSigningOut] = useState(false) const router = useRouter() const normalizedRole = session?.user.role?.toLowerCase() ?? "agent" const roleLabel = ROLE_LABELS[normalizedRole] ?? "Agente" const roleColorClass = ROLE_COLORS[normalizedRole] ?? ROLE_COLORS.agent const sessionExpiry = useMemo(() => { const expiresAt = session?.session?.expiresAt if (!expiresAt) return null return new Intl.DateTimeFormat("pt-BR", { dateStyle: "long", timeStyle: "short", }).format(new Date(expiresAt)) }, [session?.session?.expiresAt]) async function handleSignOut() { if (isSigningOut) return setIsSigningOut(true) try { await signOut() toast.success("Sessão encerrada") router.replace("/login") } catch (error) { console.error(error) toast.error("Não foi possível encerrar a sessão.") } finally { setIsSigningOut(false) } } function canAccess(requiredRole?: RoleRequirement) { if (!requiredRole) return true if (requiredRole === "admin") return isAdmin if (requiredRole === "staff") return isStaff return false } const initials = useMemo(() => { const name = session?.user.name ?? "" const parts = name.split(" ").filter(Boolean) if (parts.length >= 2) { return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase() } return name.slice(0, 2).toUpperCase() || "U" }, [session?.user.name]) return (
{/* Perfil do usuario */}

Meu perfil

Suas informações pessoais e configurações de conta.

Acesso
Papel {roleLabel}
Sessão
Ativa
{sessionExpiry && (

Expira em {sessionExpiry}

)} Segurança Gerencie a segurança da sua conta
{/* Configuracoes do workspace */}

Configurações

Gerencie times, filas, categorias e outras configurações do sistema.

{SETTINGS_ACTIONS.map((action) => { const allowed = canAccess(action.requiredRole) const Icon = action.icon return (
{action.title} {action.description}
{allowed ? ( ) : ( )}
) })}
) } function ProfileEditCard({ name, email, avatarUrl, initials, }: { name: string email: string avatarUrl: string | null initials: string }) { const [editName, setEditName] = useState(name) const [editEmail, setEditEmail] = useState(email) const [newPassword, setNewPassword] = useState("") const [confirmPassword, setConfirmPassword] = useState("") const [isSubmitting, setIsSubmitting] = useState(false) const [localAvatarUrl, setLocalAvatarUrl] = useState(avatarUrl) const [pendingAvatarFile, setPendingAvatarFile] = useState(null) const [pendingAvatarPreview, setPendingAvatarPreview] = useState(null) const [pendingRemoveAvatar, setPendingRemoveAvatar] = useState(false) const fileInputRef = useRef(null) // URL de exibição: preview pendente > URL atual (se não marcado para remoção) const displayAvatarUrl = pendingAvatarPreview ?? (pendingRemoveAvatar ? null : localAvatarUrl) function handleAvatarSelect(event: React.ChangeEvent) { const file = event.target.files?.[0] if (!file) return // Valida tamanho (5MB) if (file.size > 5 * 1024 * 1024) { toast.error("Arquivo muito grande. Máximo 5MB.") return } // Valida tipo if (!["image/jpeg", "image/png", "image/webp", "image/gif"].includes(file.type)) { toast.error("Tipo de arquivo não permitido. Use JPG, PNG, WebP ou GIF.") return } // Cria preview local const previewUrl = URL.createObjectURL(file) setPendingAvatarFile(file) setPendingAvatarPreview(previewUrl) setPendingRemoveAvatar(false) // Limpa o input para permitir reselecionar o mesmo arquivo if (fileInputRef.current) { fileInputRef.current.value = "" } } function handleRemoveAvatarClick() { // Limpa preview pendente se houver if (pendingAvatarPreview) { URL.revokeObjectURL(pendingAvatarPreview) } setPendingAvatarFile(null) setPendingAvatarPreview(null) // Marca para remoção apenas se já tiver avatar salvo if (localAvatarUrl) { setPendingRemoveAvatar(true) } } const hasChanges = useMemo(() => { const nameChanged = editName.trim() !== name const emailChanged = editEmail.trim().toLowerCase() !== email.toLowerCase() const passwordChanged = newPassword.length > 0 || confirmPassword.length > 0 const avatarChanged = pendingAvatarFile !== null || pendingRemoveAvatar return nameChanged || emailChanged || passwordChanged || avatarChanged }, [editName, name, editEmail, email, newPassword, confirmPassword, pendingAvatarFile, pendingRemoveAvatar]) async function handleSubmit(event: FormEvent) { event.preventDefault() if (!hasChanges) { toast.info("Nenhuma alteração a salvar.") return } const payload: Record = {} const trimmedEmail = editEmail.trim() if (trimmedEmail && trimmedEmail.toLowerCase() !== email.toLowerCase()) { payload.email = trimmedEmail } if (newPassword || confirmPassword) { if (newPassword !== confirmPassword) { toast.error("As senhas não conferem") return } if (newPassword.length < 8) { toast.error("A senha deve ter pelo menos 8 caracteres") return } payload.password = { newPassword, confirmPassword } } setIsSubmitting(true) try { // Processa avatar primeiro if (pendingAvatarFile) { const formData = new FormData() formData.append("file", pendingAvatarFile) const avatarRes = await fetch("/api/profile/avatar", { method: "POST", body: formData, }) if (!avatarRes.ok) { const data = await avatarRes.json().catch(() => ({ error: "Erro ao fazer upload" })) throw new Error(data.error || "Erro ao fazer upload da foto") } const avatarData = await avatarRes.json() setLocalAvatarUrl(avatarData.avatarUrl) // Limpa preview if (pendingAvatarPreview) { URL.revokeObjectURL(pendingAvatarPreview) } setPendingAvatarFile(null) setPendingAvatarPreview(null) } else if (pendingRemoveAvatar) { const avatarRes = await fetch("/api/profile/avatar", { method: "DELETE", }) if (!avatarRes.ok) { const data = await avatarRes.json().catch(() => ({ error: "Erro ao remover foto" })) throw new Error(data.error || "Erro ao remover foto") } setLocalAvatarUrl(null) setPendingRemoveAvatar(false) } // Processa outros dados do perfil se houver if (Object.keys(payload).length > 0) { const res = await fetch("/api/portal/profile", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) if (!res.ok) { const data = await res.json().catch(() => ({ error: "Falha ao atualizar perfil" })) const message = typeof data.error === "string" ? data.error : "Falha ao atualizar perfil" toast.error(message) return } const data = (await res.json().catch(() => null)) as { email?: string } | null if (data?.email) { setEditEmail(data.email) } } setNewPassword("") setConfirmPassword("") toast.success("Dados atualizados com sucesso!") } catch (error) { console.error("Falha ao atualizar perfil", error) toast.error(error instanceof Error ? error.message : "Não foi possível atualizar o perfil.") } finally { setIsSubmitting(false) } } return ( {/* Background absoluto no topo do card */}
{/* Conteudo com padding-top para ficar abaixo do background */}
{initials}
{isSubmitting ? ( ) : (
{(displayAvatarUrl || pendingAvatarFile) && !pendingRemoveAvatar && ( )}
)}
{name || "Usuário"} {email}
setEditName(e.target.value)} placeholder="Seu nome" disabled className="bg-neutral-50" />

Editável apenas por administradores

setEditEmail(e.target.value)} placeholder="seu@email.com" />
setNewPassword(e.target.value)} placeholder="Nova senha" autoComplete="new-password" /> setConfirmPassword(e.target.value)} placeholder="Confirmar senha" autoComplete="new-password" />

Mínimo de 8 caracteres. Deixe em branco se não quiser alterar.

) }