Carregando mensagens...
)}
+
{!commentsQuery.isLoading && comments.length === 0 && (
Nenhuma mensagem ainda.
)}
{comments.map((comment) => {
- const isTeam = comment.internal
- const isEditing = editingComment?.commentId === comment.comment_id
- const isDeleting = deletingCommentId === comment.comment_id
+ const commentId = getCommentId(comment)
+ const isTeam = Boolean(comment.internal)
+ const isEditing = editingComment?.commentId === commentId
+ const isDeleting = deletingCommentId === commentId
return (
)
}
-// — helpers —
+function normalizeUsers(data) {
+ if (Array.isArray(data)) {
+ return data
+ }
+
+ if (Array.isArray(data?.items)) {
+ return data.items
+ }
+
+ if (Array.isArray(data?.data?.items)) {
+ return data.data.items
+ }
+
+ if (Array.isArray(data?.data)) {
+ return data.data
+ }
+
+ return []
+}
+
+function getApiErrorMessage(error, fallback) {
+ const detail = error?.response?.data?.detail
+
+ if (Array.isArray(detail) && detail[0]?.msg) {
+ return detail[0].msg
+ }
+
+ if (typeof detail === 'string') {
+ return detail
+ }
+
+ return error?.response?.data?.message || fallback
+}
+
+function getTicketStatus(ticket) {
+ return String(ticket?.status ?? 'open').toLowerCase()
+}
+
+function isTerminalStatus(status) {
+ return ['finished', 'closed', 'cancelled', 'resolved'].includes(String(status).toLowerCase())
+}
function getAssignedAgent(ticket) {
- const directId = ticket?.assigned_agent_id ?? ticket?.assignedAgentId ?? null
- const directName = ticket?.assigned_agent_name ?? ticket?.assignedAgentName ?? null
+ const directId = ticket?.assigned_agent_id ?? ticket?.assignedAgentId ?? null
+ const directName = ticket?.assigned_agent_name ?? ticket?.assignedAgentName ?? null
if (directId || directName) {
- return { id: directId ? String(directId) : null, name: directName || 'Atendente atribuído', label: 'Responsável atual' }
+ return {
+ id: directId ? String(directId) : null,
+ name: directName || 'Atendente atribuído',
+ label: 'Responsável atual'
+ }
}
- const history = Array.isArray(ticket?.agent_history) ? ticket.agent_history : []
- const latestHistory = history.length ? history[history.length - 1] : null
+ const history = Array.isArray(ticket?.agent_history) ? ticket.agent_history : []
+ const activeHistory = [...history].reverse().find((item) => !item.exit_date)
+ const latestHistory = activeHistory || (history.length ? history[history.length - 1] : null)
if (latestHistory) {
return {
- id: latestHistory.agent_id ? String(latestHistory.agent_id) : null,
- name: latestHistory.name || 'Atendente atribuído',
- label: latestHistory.level || 'Responsável atual',
+ id: latestHistory.agent_id ? String(latestHistory.agent_id) : null,
+ name: latestHistory.name || 'Atendente atribuído',
+ label: latestHistory.level || 'Responsável atual'
}
}
return { id: null, name: null, label: 'Sem atendente' }
}
+function getRoleNames(user) {
+ const roles = []
+
+ if (Array.isArray(user?.roles)) {
+ for (const role of user.roles) {
+ if (typeof role === 'string') {
+ roles.push(role)
+ } else if (role?.name) {
+ roles.push(role.name)
+ }
+ }
+ }
+
+ if (Array.isArray(user?.role_names)) {
+ roles.push(...user.role_names)
+ }
+
+ if (user?.role) {
+ roles.push(user.role)
+ }
+
+ return roles.map((role) => String(role).trim().toLowerCase()).filter(Boolean)
+}
+
+function canUserReceiveTicket(user) {
+ const roles = getRoleNames(user)
+
+ return roles.some((role) => ['admin', 'agent', 'n1', 'n2', 'n3'].includes(role))
+}
+
+function getUserDisplayName(user) {
+ return user?.name || user?.username || user?.email || 'Usuário'
+}
+
+function getUserRoleLabel(user) {
+ const roles = getRoleNames(user)
+
+ if (!roles.length) {
+ return 'Sem papel'
+ }
+
+ return roles.map((role) => role.toUpperCase()).join(', ')
+}
+
+function getCommentId(comment) {
+ return String(comment?.comment_id ?? comment?.id ?? `${comment?.date}-${comment?.text}`)
+}
+
function InfoBlock({ label, value }) {
return (
)
}
function formatStatusLabel(status) {
- const map = { open: 'Aberto', in_progress: 'Em andamento', waiting_for_provider: 'Aguardando fornecedor', waiting_for_validation: 'Aguardando validação', finished: 'Finalizado' }
- return map[status] || status || 'Não informado'
+ const value = String(status ?? '').toLowerCase()
+
+ const map = {
+ open: 'Aberto',
+ awaiting_assignment: 'Aguardando atribuição',
+ assigned: 'Atribuído',
+ in_progress: 'Em andamento',
+ waiting_for_customer: 'Aguardando cliente',
+ waiting_customer: 'Aguardando cliente',
+ waiting_for_provider: 'Aguardando fornecedor',
+ waiting_for_validation: 'Aguardando validação',
+ resolved: 'Resolvido',
+ closed: 'Fechado',
+ finished: 'Finalizado',
+ cancelled: 'Cancelado'
+ }
+
+ return map[value] || status || 'Não informado'
}
function formatCriticality(value) {
- const map = { high: 'Alta', medium: 'Média', low: 'Baixa' }
- return map[value] || value || 'Não informada'
+ const normalized = String(value ?? '').toLowerCase()
+
+ const map = {
+ high: 'Alta',
+ medium: 'Média',
+ low: 'Baixa',
+ critical: 'Crítica'
+ }
+
+ return map[normalized] || value || 'Não informada'
}
function formatTicketType(value) {
- const map = { issue: 'Problema', access: 'Acesso', new_feature: 'Nova funcionalidade' }
- return map[value] || value || 'Não informado'
+ const normalized = String(value ?? '').toLowerCase()
+
+ const map = {
+ issue: 'Problema',
+ request: 'Solicitação',
+ access: 'Acesso',
+ question: 'Dúvida',
+ incident: 'Incidente',
+ new_feature: 'Nova funcionalidade'
+ }
+
+ return map[normalized] || value || 'Não informado'
+}
+
+function formatDateTime(rawDate) {
+ if (!rawDate) {
+ return 'Não informado'
+ }
+
+ const date = new Date(rawDate)
+
+ if (Number.isNaN(date.getTime())) {
+ return 'Não informado'
+ }
+
+ return date.toLocaleString('pt-BR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
}
function NavItem({ icon, label, active, onClick }) {
@@ -671,11 +1146,11 @@ function NavItem({ icon, label, active, onClick }) {
)
}
\ No newline at end of file
diff --git a/src/features/ticket/utils/ticket-presentation.js b/src/features/ticket/utils/ticket-presentation.js
new file mode 100644
index 0000000..3a72e4c
--- /dev/null
+++ b/src/features/ticket/utils/ticket-presentation.js
@@ -0,0 +1,35 @@
+export const TICKET_STATUS_LABELS = {
+ open: 'Aberto',
+ awaiting_assignment: 'Aguardando atendimento',
+ in_progress: 'Em andamento',
+ waiting_for_provider: 'Aguardando fornecedor',
+ waiting_for_validation: 'Aguardando validação',
+ finished: 'Finalizado'
+}
+
+export const TICKET_STATUS_CLASSES = {
+ open: 'bg-orange-50 text-orange-700',
+ awaiting_assignment: 'bg-amber-50 text-amber-700',
+ in_progress: 'bg-blue-50 text-blue-700',
+ waiting_for_provider: 'bg-yellow-50 text-yellow-700',
+ waiting_for_validation: 'bg-purple-50 text-purple-700',
+ finished: 'bg-green-50 text-green-700'
+}
+
+export function normalizeStatus(status) {
+ return String(status ?? '').trim().toLowerCase()
+}
+
+export function getTicketStatusLabel(status) {
+ const normalized = normalizeStatus(status)
+ return TICKET_STATUS_LABELS[normalized] || status || 'Sem status'
+}
+
+export function getTicketStatusBadgeClass(status) {
+ const normalized = normalizeStatus(status)
+ return TICKET_STATUS_CLASSES[normalized] || 'bg-gray-100 text-gray-600'
+}
+
+export function isTicketFinishedStatus(status) {
+ return normalizeStatus(status) === 'finished'
+}
\ No newline at end of file
diff --git a/src/features/users/api/user-service.js b/src/features/users/api/user-service.js
index 41b3b8c..56e55dc 100644
--- a/src/features/users/api/user-service.js
+++ b/src/features/users/api/user-service.js
@@ -1,18 +1,9 @@
import { http } from '@/lib/http'
function normalizeListResponse(data) {
- if (Array.isArray(data)) {
- return data
- }
-
- if (Array.isArray(data?.items)) {
- return data.items
- }
-
- if (Array.isArray(data?.data)) {
- return data.data
- }
-
+ if (Array.isArray(data)) return data
+ if (Array.isArray(data?.data)) return data.data
+ if (Array.isArray(data?.items)) return data.items
return []
}
@@ -30,6 +21,13 @@ export async function getUserById(userId) {
return normalizeObjectResponse(data)
}
+export async function getRoles() {
+ const { data } = await http.get('/roles')
+ if (Array.isArray(data?.data)) return data.data
+ if (Array.isArray(data)) return data
+ return []
+}
+
export async function createUser(payload) {
const { data } = await http.post('/users', payload)
return normalizeObjectResponse(data)
@@ -38,4 +36,22 @@ export async function createUser(payload) {
export async function patchUser({ userId, payload }) {
const { data } = await http.patch(`/users/${userId}`, payload)
return normalizeObjectResponse(data)
+}
+
+export async function deleteUser(userId) {
+ const { data } = await http.delete(`/users/${userId}`)
+ return normalizeObjectResponse(data)
+}
+
+export async function patchUserRoles({ userId, addRoleIds = [], removeRoleIds = [] }) {
+ const { data } = await http.patch(`/users/${userId}/roles`, {
+ add_role_ids: addRoleIds,
+ remove_role_ids: removeRoleIds,
+ })
+ return normalizeObjectResponse(data)
+}
+
+export async function deactivateUser(userId) {
+ const { data } = await http.patch(`/users/${userId}/deactivate`)
+ return normalizeObjectResponse(data)
}
\ No newline at end of file
diff --git a/src/features/users/hooks/useDeactivateUserMutation.js b/src/features/users/hooks/useDeactivateUserMutation.js
new file mode 100644
index 0000000..4baea89
--- /dev/null
+++ b/src/features/users/hooks/useDeactivateUserMutation.js
@@ -0,0 +1,15 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { deactivateUser } from '@/features/users/api/user-service'
+
+export function useDeactivateUserMutation() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: deactivateUser,
+ onSuccess: (_, userId) => {
+ queryClient.invalidateQueries({ queryKey: ['users'] })
+ queryClient.invalidateQueries({ queryKey: ['users', userId] })
+ }
+ })
+}
+
\ No newline at end of file
diff --git a/src/features/users/hooks/useRolesQuery.js b/src/features/users/hooks/useRolesQuery.js
new file mode 100644
index 0000000..632f947
--- /dev/null
+++ b/src/features/users/hooks/useRolesQuery.js
@@ -0,0 +1,10 @@
+import { useQuery } from '@tanstack/react-query'
+import { getRoles } from '@/features/users/api/user-service'
+
+export function useRolesQuery() {
+ return useQuery({
+ queryKey: ['roles'],
+ queryFn: getRoles,
+ staleTime: 1000 * 60 * 10,
+ })
+}
\ No newline at end of file
diff --git a/src/features/users/hooks/useUpdateUserRolesMutation.js b/src/features/users/hooks/useUpdateUserRolesMutation.js
new file mode 100644
index 0000000..4d66eb3
--- /dev/null
+++ b/src/features/users/hooks/useUpdateUserRolesMutation.js
@@ -0,0 +1,14 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { patchUserRoles } from '@/features/users/api/user-service'
+
+export function usePatchUserRolesMutation() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: patchUserRoles,
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({ queryKey: ['users'] })
+ queryClient.invalidateQueries({ queryKey: ['users', variables.userId] })
+ }
+ })
+}
\ No newline at end of file
diff --git a/src/features/users/pages/CadastrarUsuario.jsx b/src/features/users/pages/CadastrarUsuario.jsx
index f865aa3..6e17290 100644
--- a/src/features/users/pages/CadastrarUsuario.jsx
+++ b/src/features/users/pages/CadastrarUsuario.jsx
@@ -8,7 +8,8 @@ import {
RefreshCcw,
LogOut,
MessageSquare,
- Loader2
+ Loader2,
+ Settings
} from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '@/stores/auth-stores'
@@ -18,6 +19,7 @@ import { ROLE_OPTIONS } from '@/features/users/utils/role-utils'
export default function CadastrarUsuario() {
const navigate = useNavigate()
const clearSession = useAuthStore((state) => state.clearSession)
+ const loggedUser = useAuthStore((state) => state.user)
const [nome, setNome] = useState('')
const [email, setEmail] = useState('')
@@ -125,14 +127,26 @@ export default function CadastrarUsuario() {
{menuPerfilAberto && (
-
+
+
+
{loggedUser?.name || 'Usuário'}
+
{loggedUser?.email || ''}
+
+
{ setMenuPerfilAberto(false); navigate('/configuracoes') }}
+ className="w-full flex items-center gap-3 px-4 py-3 text-[10px] font-bold text-white/70 hover:bg-white/10 rounded-xl transition-colors uppercase"
+ >
+
+ Configurações
+
- Sair da Conta
+ Sair
)}
diff --git a/src/features/users/pages/EditarAtendente.jsx b/src/features/users/pages/EditarAtendente.jsx
new file mode 100644
index 0000000..49b2d18
--- /dev/null
+++ b/src/features/users/pages/EditarAtendente.jsx
@@ -0,0 +1,484 @@
+import { useEffect, useRef, useState } from 'react'
+import {
+ LayoutDashboard,
+ Users,
+ Ticket,
+ User as UserIcon,
+ LogOut,
+ MessageSquare,
+ Save,
+ ArrowLeft,
+ Loader2,
+ ShieldCheck,
+ BarChart3,
+ Settings,
+ Briefcase,
+ AlertTriangle,
+ CheckCircle2,
+ Pencil,
+} from 'lucide-react'
+import { useNavigate, useParams } from 'react-router-dom'
+import { useAuthStore } from '@/stores/auth-stores'
+import { useUserQuery } from '@/features/users/hooks/useUserQuery'
+import { usePatchUserMutation } from '@/features/users/hooks/usePatchUserMutation'
+import { usePatchUserRolesMutation } from '@/features/users/hooks/useUpdateUserRolesMutation'
+import { useDeactivateUserMutation } from '@/features/users/hooks/useDeactivateUserMutation'
+import { getRoleInfo } from '@/features/users/utils/role-utils'
+
+const CARGO_OPTIONS = [
+ { key: 'admin', label: 'Gerente', roleId: 1, description: 'Supervisiona toda a equipe e finanças.', icon:
},
+ { key: 'agent', label: 'Operador', roleId: 3, description: 'Gerencia os tickets e atende clientes.', icon:
},
+ { key: 'user', label: 'Suporte', roleId: 2, description: 'Auxilia clientes e fecha tickets.', icon:
},
+]
+
+const PERMISSIONS_CONFIG = [
+ { key: 'manage_users', label: 'Gerenciar Usuários', description: 'Capacidade de criar, editar e desativar perfis.', icon:
},
+ { key: 'manage_tickets', label: 'Gerenciar Chamados', description: 'Atribuição de chamados e resolução de tickets.', icon:
},
+ { key: 'view_reports', label: 'Ver Relatórios', description: 'Acesso ao dashboard de métricas e relatórios.', icon:
},
+ { key: 'system_settings', label: 'Configurações do Sistema', description: 'Alteração de preferências globais da plataforma.', icon:
},
+]
+
+export default function EditarAtendente() {
+ const navigate = useNavigate()
+ const { userId } = useParams()
+ const clearSession = useAuthStore((state) => state.clearSession)
+ const loggedUser = useAuthStore((state) => state.user)
+ const [menuPerfilAberto, setMenuPerfilAberto] = useState(false)
+ const menuRef = useRef(null)
+
+ const userQuery = useUserQuery(userId)
+ const patchUserMutation = usePatchUserMutation()
+ const patchUserRolesMutation = usePatchUserRolesMutation()
+ const deactivateUserMutation = useDeactivateUserMutation()
+
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setMenuPerfilAberto(false)
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [])
+
+ function handleLogout() {
+ clearSession()
+ navigate('/login', { replace: true })
+ }
+
+ if (userQuery.isLoading) {
+ return (
+
+ Carregando...
+
+ )
+ }
+
+ if (userQuery.isError || !userQuery.data) {
+ return (
+
+ Erro ao carregar usuário
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+function EditarAtendenteForm({
+ user,
+ userId,
+ menuPerfilAberto,
+ setMenuPerfilAberto,
+ menuRef,
+ onLogout,
+ navigate,
+ patchUserMutation,
+ patchUserRolesMutation,
+ deactivateUserMutation,
+ loggedUser,
+}) {
+ const initialRole = getRoleInfo(user)
+ const initials = getInitials(user.name || user.username)
+ const initialCargo = CARGO_OPTIONS.find((c) => c.key === initialRole.key) ? initialRole.key : 'agent'
+
+ const [nome, setNome] = useState(user.name || '')
+ const [email, setEmail] = useState(user.email || '')
+ const [isActive, setIsActive] = useState(Boolean(user.is_active ?? user.isActive))
+ const [selectedCargo, setSelectedCargo] = useState(initialCargo)
+ const [editandoNome, setEditandoNome] = useState(false)
+ const [permissions, setPermissions] = useState({
+ manage_users: user.permissions?.manage_users ?? false,
+ manage_tickets: user.permissions?.manage_tickets ?? true,
+ view_reports: user.permissions?.view_reports ?? true,
+ system_settings: user.permissions?.system_settings ?? false,
+ })
+ const [errorMessage, setErrorMessage] = useState('')
+ const [revogarErrorMessage, setRevogarErrorMessage] = useState('')
+ const [showDangerConfirm, setShowDangerConfirm] = useState(false)
+
+ const isSaving = patchUserMutation.isPending || patchUserRolesMutation.isPending
+
+ function togglePermission(key) {
+ setPermissions((prev) => ({ ...prev, [key]: !prev[key] }))
+ }
+
+ async function handleUpdate(event) {
+ event.preventDefault()
+ setErrorMessage('')
+
+ try {
+ // 1. Atualiza dados do usuário
+ await patchUserMutation.mutateAsync({
+ userId,
+ payload: {
+ email: email.trim().toLowerCase(),
+ name: nome.trim(),
+ username: user.username,
+ oauth_provider: user.oauth_provider ?? 'local',
+ oauth_provider_id: user.oauth_provider_id ?? `local_${user.id}`,
+ is_active: isActive,
+ is_verified: user.is_verified ?? false,
+ },
+ })
+
+ // 2. Atualiza o role via PATCH /users/{user_id}/roles
+ const selectedCargoOption = CARGO_OPTIONS.find((c) => c.key === selectedCargo)
+ const initialCargoOption = CARGO_OPTIONS.find((c) => c.key === initialCargo)
+ if (selectedCargoOption && selectedCargoOption.key !== initialCargo) {
+ await patchUserRolesMutation.mutateAsync({
+ userId,
+ addRoleIds: [selectedCargoOption.roleId],
+ removeRoleIds: initialCargoOption ? [initialCargoOption.roleId] : [],
+ })
+ }
+
+ navigate('/usuarios', { replace: true })
+ } catch (error) {
+ const detail = error.response?.data?.detail
+ const message =
+ detail?.[0]?.msg ||
+ error.response?.data?.message ||
+ String(detail || '') ||
+ 'Erro ao atualizar atendente.'
+ setErrorMessage(message)
+ }
+ }
+
+ async function handleRevogar() {
+ setRevogarErrorMessage('')
+ try {
+ await deactivateUserMutation.mutateAsync(userId)
+ navigate('/usuarios', { replace: true })
+ } catch (error) {
+ const detail = error?.response?.data?.detail
+ const message =
+ detail?.[0]?.msg ||
+ error?.response?.data?.message ||
+ String(detail || '') ||
+ 'Erro ao desativar usuário.'
+ setRevogarErrorMessage(message)
+ setShowDangerConfirm(false)
+ }
+ }
+
+ const ultimoAcesso = user.last_login
+ ? new Date(user.last_login).toLocaleDateString('pt-BR', {
+ day: '2-digit', month: '2-digit', year: '2-digit',
+ hour: '2-digit', minute: '2-digit',
+ })
+ : 'Hoje, às 14:23'
+
+ return (
+
+
+
+
+
+ } label="Dashboard" onClick={() => navigate('/')} />
+ } label="Usuários" active onClick={() => navigate('/usuarios')} />
+ } label="Chamados" onClick={() => navigate('/chamados')} />
+ } label="Chat" onClick={() => navigate('/chat')} />
+
+
+
+
+
+
+
+
+
+
+
Configurações do Atendente
+
+
navigate('/usuarios')}
+ className="text-xs font-bold text-gray-400 hover:text-gray-600 uppercase px-4 py-2.5 rounded-lg border border-gray-200 hover:bg-gray-50 transition-all flex items-center gap-1.5"
+ >
+
+ Descartar
+
+
+ {isSaving ? (
+ <> Salvando...>
+ ) : (
+ <> Salvar Alterações>
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function getInitials(name) {
+ return name?.split(' ').map((p) => p[0]).join('').toUpperCase().slice(0, 2) || '??'
+}
+
+function NavItem({ icon, label, active, onClick }) {
+ return (
+
+ {icon} {label}
+
+ )
+}
\ No newline at end of file
diff --git a/src/features/users/pages/EditarCliente.jsx b/src/features/users/pages/EditarCliente.jsx
new file mode 100644
index 0000000..ac3c075
--- /dev/null
+++ b/src/features/users/pages/EditarCliente.jsx
@@ -0,0 +1,430 @@
+import { useEffect, useRef, useState } from 'react'
+import {
+ LayoutDashboard,
+ Users,
+ Ticket,
+ User as UserIcon,
+ LogOut,
+ MessageSquare,
+ Save,
+ ArrowLeft,
+ Loader2,
+ Building2,
+ ShieldAlert,
+ StickyNote,
+ Package,
+ Settings,
+} from 'lucide-react'
+import { useNavigate, useParams } from 'react-router-dom'
+import { useAuthStore } from '@/stores/auth-stores'
+import { useUserQuery } from '@/features/users/hooks/useUserQuery'
+import { usePatchUserMutation } from '@/features/users/hooks/usePatchUserMutation'
+
+export default function EditarCliente() {
+ const navigate = useNavigate()
+ const { userId } = useParams()
+ const clearSession = useAuthStore((state) => state.clearSession)
+ const loggedUser = useAuthStore((state) => state.user)
+ const [menuPerfilAberto, setMenuPerfilAberto] = useState(false)
+ const menuRef = useRef(null)
+
+ const userQuery = useUserQuery(userId)
+ const patchUserMutation = usePatchUserMutation()
+
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setMenuPerfilAberto(false)
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [])
+
+ function handleLogout() {
+ clearSession()
+ navigate('/login', { replace: true })
+ }
+
+ if (userQuery.isLoading) {
+ return (
+
+ Carregando...
+
+ )
+ }
+
+ if (userQuery.isError || !userQuery.data) {
+ return (
+
+ Erro ao carregar usuário
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+function EditarClienteForm({ user, userId, menuPerfilAberto, setMenuPerfilAberto, menuRef, onLogout, navigate, patchUserMutation, loggedUser }) {
+ const isActiveInitial = Boolean(user.is_active ?? user.isActive)
+ const initials = getInitials(user.name || user.username)
+
+ const [nome, setNome] = useState(user.name || '')
+ const [email, setEmail] = useState(user.email || '')
+ const [isActive, setIsActive] = useState(isActiveInitial)
+ const [notasInternas, setNotasInternas] = useState(user.internal_notes || '')
+ const [produtoContratado, setProdutoContratado] = useState(user.contracted_product || '')
+ const [dataExpiracao, setDataExpiracao] = useState(user.contract_expiration || '')
+ const [errorMessage, setErrorMessage] = useState('')
+ const [suspendErrorMessage, setSuspendErrorMessage] = useState('')
+ const [showSuspendConfirm, setShowSuspendConfirm] = useState(false)
+
+ async function handleUpdate(event) {
+ event.preventDefault()
+ setErrorMessage('')
+
+ // Apenas campos aceitos pelo PATCH /api/users/{id}
+ const payload = {
+ email: email.trim().toLowerCase(),
+ name: nome.trim(),
+ username: user.username,
+ oauth_provider: user.oauth_provider ?? 'local',
+ oauth_provider_id: user.oauth_provider_id ?? `local_${user.id}`,
+ is_active: isActive,
+ is_verified: user.is_verified ?? false,
+ }
+
+ try {
+ await patchUserMutation.mutateAsync({ userId, payload })
+ navigate('/usuarios', { replace: true })
+ } catch (error) {
+ const detail = error.response?.data?.detail
+ const message =
+ detail?.[0]?.msg ||
+ error.response?.data?.message ||
+ String(detail || '') ||
+ 'Erro ao atualizar usuário.'
+ setErrorMessage(message)
+ }
+ }
+
+ async function handleToggleSuspend() {
+ setSuspendErrorMessage('')
+ const newActiveState = !isActive
+ try {
+ await patchUserMutation.mutateAsync({
+ userId,
+ payload: {
+ email: user.email,
+ name: user.name,
+ username: user.username,
+ oauth_provider: user.oauth_provider ?? 'local',
+ oauth_provider_id: user.oauth_provider_id ?? `local_${user.id}`,
+ is_active: newActiveState,
+ is_verified: user.is_verified ?? false,
+ },
+ })
+ setIsActive(newActiveState)
+ setShowSuspendConfirm(false)
+ } catch (error) {
+ const detail = error?.response?.data?.detail
+ const message =
+ detail?.[0]?.msg ||
+ error?.response?.data?.message ||
+ String(detail || '') ||
+ 'Erro ao alterar status do usuário.'
+ setSuspendErrorMessage(message)
+ setShowSuspendConfirm(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ } label="Dashboard" onClick={() => navigate('/')} />
+ } label="Usuários" active onClick={() => navigate('/usuarios')} />
+ } label="Chamados" onClick={() => navigate('/chamados')} />
+ } label="Chat" onClick={() => navigate('/chat')} />
+
+
+
+
+
+
+
+
+
+
+ {/* Header card */}
+
+
+
+ {initials}
+
+
+
+
{nome || user.username}
+
+ {isActive ? 'ATIVO' : 'SUSPENSO'}
+
+
+
+ Cliente desde{' '}
+ {user.created_at
+ ? new Date(user.created_at).toLocaleDateString('pt-BR', { month: 'short', year: 'numeric' })
+ : 'data não disponível'}
+
+
+
+
+
navigate('/usuarios')}
+ className="text-xs font-bold text-gray-400 hover:text-gray-600 uppercase px-4 py-2.5 rounded-lg border border-gray-200 hover:bg-gray-50 transition-all flex items-center gap-1.5"
+ >
+
+ Descartar
+
+
+ {patchUserMutation.isPending ? (
+ <> Salvando...>
+ ) : (
+ <> Salvar Alterações>
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function getInitials(name) {
+ return name?.split(' ').map((p) => p[0]).join('').toUpperCase().slice(0, 2) || '??'
+}
+
+function NavItem({ icon, label, active, onClick }) {
+ return (
+
+ {icon} {label}
+
+ )
+}
\ No newline at end of file
diff --git a/src/features/users/pages/EditarUsuario.jsx b/src/features/users/pages/EditarUsuario.jsx
deleted file mode 100644
index 493618d..0000000
--- a/src/features/users/pages/EditarUsuario.jsx
+++ /dev/null
@@ -1,310 +0,0 @@
-import { useEffect, useRef, useState } from 'react'
-import {
- LayoutDashboard,
- Users,
- Ticket,
- User as UserIcon,
- LogOut,
- MessageSquare,
- Save,
- ArrowLeft,
- RefreshCcw,
- Loader2
-} from 'lucide-react'
-import { useNavigate, useParams } from 'react-router-dom'
-import { useAuthStore } from '@/stores/auth-stores'
-import { useUserQuery } from '@/features/users/hooks/useUserQuery'
-import { usePatchUserMutation } from '@/features/users/hooks/usePatchUserMutation'
-import { getRoleInfo, ROLE_OPTIONS } from '@/features/users/utils/role-utils'
-
-export default function EditarUsuario() {
- const navigate = useNavigate()
- const { userId } = useParams()
- const clearSession = useAuthStore((state) => state.clearSession)
-
- const [menuPerfilAberto, setMenuPerfilAberto] = useState(false)
- const menuRef = useRef(null)
-
- const userQuery = useUserQuery(userId)
- const patchUserMutation = usePatchUserMutation()
-
- useEffect(() => {
- function handleClickOutside(event) {
- if (menuRef.current && !menuRef.current.contains(event.target)) {
- setMenuPerfilAberto(false)
- }
- }
-
- document.addEventListener('mousedown', handleClickOutside)
-
- return () => {
- document.removeEventListener('mousedown', handleClickOutside)
- }
- }, [])
-
- function handleLogout() {
- clearSession()
- navigate('/login', { replace: true })
- }
-
- if (userQuery.isLoading) {
- return (
-
- Carregando...
-
- )
- }
-
- if (userQuery.isError || !userQuery.data) {
- return (
-
- Erro ao carregar usuário
-
- )
- }
-
- return (
-
- )
-}
-
-function EditarUsuarioForm({
- user,
- userId,
- menuPerfilAberto,
- setMenuPerfilAberto,
- menuRef,
- onLogout,
- navigate,
- patchUserMutation
-}) {
- const initialRole = getRoleInfo(user)
-
- const [nome, setNome] = useState(user.name || '')
- const [email, setEmail] = useState(user.email || '')
- const [isActive, setIsActive] = useState(Boolean(user.is_active ?? user.isActive))
- const [selectedRole, setSelectedRole] = useState(initialRole.key === 'unknown' ? 'user' : initialRole.key)
- const [errorMessage, setErrorMessage] = useState('')
-
- async function handleUpdate(event) {
- event.preventDefault()
- setErrorMessage('')
-
- const selectedRoleOption = ROLE_OPTIONS.find((role) => role.key === selectedRole)
-
- const payload = {
- email: email.trim().toLowerCase(),
- name: nome.trim(),
- username: user.username,
- is_active: isActive,
- role_ids: selectedRoleOption ? [selectedRoleOption.roleId] : []
- }
-
- try {
- await patchUserMutation.mutateAsync({
- userId,
- payload
- })
-
- navigate('/usuarios', { replace: true })
- } catch (error) {
- const detail = error.response?.data?.detail
- const message =
- detail?.[0]?.msg ||
- error.response?.data?.message ||
- String(detail || '') ||
- 'Erro ao atualizar usuário.'
-
- setErrorMessage(message)
- }
- }
-
- return (
-
-
-
-
-
-
- } label="Dashboard" onClick={() => navigate('/')} />
- } label="Usuários" active onClick={() => navigate('/usuarios')} />
- } label="Chamados" onClick={() => navigate('/chamados')} />
- } label="Chat" onClick={() => navigate('/chat')} />
-
-
-
-
-
-
-
-
-
-
-
-
Editar Perfil
-
Editando: {user.username}
-
-
-
navigate('/usuarios')}
- className="flex items-center gap-2 text-xs font-bold text-gray-400 hover:text-[#500D0D] uppercase"
- >
-
- Voltar
-
-
-
-
-
-
-
-
-
Role
-
- {ROLE_OPTIONS.map((role) => (
- setSelectedRole(role.key)}
- className={`px-4 py-3 rounded-lg text-sm font-bold border transition-all ${selectedRole === role.key
- ? 'border-[#BD3B0F] bg-[#fff8f6] text-[#BD3B0F]'
- : 'border-gray-200 bg-white text-gray-500 hover:bg-gray-50'
- }`}
- >
- {role.label}
-
- ))}
-
-
-
-
- Status da Conta:
- setIsActive((value) => !value)}
- className={`px-6 py-2 rounded-lg text-[10px] font-bold transition-all border ${isActive
- ? 'border-green-500 bg-green-50 text-green-600'
- : 'border-red-500 bg-red-50 text-red-600'
- }`}
- >
- {isActive ? 'CONTA ATIVA' : 'CONTA INATIVA'}
-
-
-
- {errorMessage && (
- {errorMessage}
- )}
-
-
- navigate('/usuarios')}
- className="text-xs font-bold text-gray-400 uppercase"
- >
- Descartar
-
-
-
- {patchUserMutation.isPending ? (
- <>
-
- Salvando...
- >
- ) : (
- <>
-
- Salvar Alterações
- >
- )}
-
-
-
-
-
-
-
- Atualização via API
-
-
-
-
-
- )
-}
-
-function NavItem({ icon, label, active, onClick }) {
- return (
-
- {icon} {label}
-
- )
-}
\ No newline at end of file
diff --git a/src/features/users/pages/Usuarios.jsx b/src/features/users/pages/Usuarios.jsx
index 1dd3b25..c9d95e6 100644
--- a/src/features/users/pages/Usuarios.jsx
+++ b/src/features/users/pages/Usuarios.jsx
@@ -9,7 +9,8 @@ import {
ShieldAlert,
Pencil,
Search,
- Filter
+ Filter,
+ Settings
} from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '@/stores/auth-stores'
@@ -26,6 +27,7 @@ export default function Usuarios() {
const [statusFilter, setStatusFilter] = useState('')
const [roleFilter, setRoleFilter] = useState('')
const menuPerfilRef = useRef(null)
+ const loggedUser = useAuthStore((state) => state.user)
const debouncedSearch = useDebouncedValue(search, 300)
@@ -38,12 +40,8 @@ export default function Usuarios() {
setMenuPerfilAberto(false)
}
}
-
document.addEventListener('mousedown', handleClickOutside)
-
- return () => {
- document.removeEventListener('mousedown', handleClickOutside)
- }
+ return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
function handleLogout() {
@@ -55,12 +53,7 @@ export default function Usuarios() {
const total = usersData.length
const active = usersData.filter((user) => Boolean(user.is_active ?? user.isActive)).length
const inactive = total - active
-
- return {
- total,
- active,
- inactive
- }
+ return { total, active, inactive }
}, [usersData])
const filteredUsers = useMemo(() => {
@@ -79,13 +72,23 @@ export default function Usuarios() {
(statusFilter === 'active' && isActive) ||
(statusFilter === 'inactive' && !isActive)
- const matchesRole =
- !roleFilter || roleData.key === roleFilter
+ const matchesRole = !roleFilter || roleData.key === roleFilter
return matchesSearch && matchesStatus && matchesRole
})
}, [usersData, debouncedSearch, statusFilter, roleFilter])
+ function handleEditUser(user) {
+ const roleData = getRoleInfo(user)
+ // cliente → EditarCliente
+ // admin, agent, user → EditarAtendente
+ if (roleData.key === 'client') {
+ navigate(`/usuarios/${user.id}/editar-cliente`)
+ } else {
+ navigate(`/usuarios/${user.id}/editar-atendente`)
+ }
+ }
+
return (