diff --git a/src/modules/attendance/pages/NewAttendancePage.jsx b/src/modules/attendance/pages/NewAttendancePage.jsx index c0feb0e..3592589 100644 --- a/src/modules/attendance/pages/NewAttendancePage.jsx +++ b/src/modules/attendance/pages/NewAttendancePage.jsx @@ -219,7 +219,7 @@ export function NewAttendancePage() { if (!isMounted) return; setContacts(Array.isArray(contactsData) ? contactsData.map(normalizeAgendaContact) : []); const supportedTemplates = Array.isArray(templatesData) - ? templatesData.filter((template) => !requiresUnsupportedTemplateFields(template)) + ? templatesData.filter((template) => template.status === 'approved' && !requiresUnsupportedTemplateFields(template)) : []; setTemplates(supportedTemplates); setSelectedTemplateId((current) => current || (supportedTemplates?.[0]?.id ? String(supportedTemplates[0].id) : '')); diff --git a/src/modules/chat/components/ChatConversationList.jsx b/src/modules/chat/components/ChatConversationList.jsx index f4adf6f..f7d69e9 100644 --- a/src/modules/chat/components/ChatConversationList.jsx +++ b/src/modules/chat/components/ChatConversationList.jsx @@ -23,26 +23,69 @@ function ChannelBadge({ channel }) { ); } -function LastMessageDot({ fromMe }) { - const color = fromMe ? '#e5a22a' : '#00a4b7'; - const label = fromMe ? 'Última mensagem enviada pelo atendimento' : 'Última mensagem enviada pelo cliente'; +function AssignmentDot({ contact, currentUserId }) { + const assignment = contact.assignment; + const assignedUserId = assignment?.user_id ? Number(assignment.user_id) : null; + const isQueued = assignment?.status === 'queued' && !assignedUserId; + const isMine = assignedUserId && currentUserId && assignedUserId === Number(currentUserId); + const meta = isQueued + ? { + color: '#e5a22a', + label: 'Chamado na fila da especialidade, ainda sem atribuição', + } + : isMine + ? { + color: '#00a4b7', + label: 'Chamado atribuído a mim', + } + : assignedUserId + ? { + color: '#d62828', + label: `Chamado atribuído a ${assignment?.user_nome || 'outra pessoa'}`, + } + : null; + + if (!meta) return null; return ( ); } +function SpecialtyBadge({ contact }) { + const specialty = contact.assignment?.area_nome || contact.area; + if (!specialty || specialty === 'Sem fila') return null; + + return ( + + {specialty} + + ); +} + function UnreadBadge({ count }) { if (!count) return null; @@ -83,7 +126,7 @@ function SavedContactLabel({ contact }) { lineHeight: 1, }} > - •Salvo• + •Salvo• ); } @@ -95,6 +138,7 @@ export function ChatConversationList({ activeContactId, onSelectContact, onOpenContact, + currentUserId, isMobile = false, }) { return ( @@ -158,7 +202,7 @@ export function ChatConversationList({ >
- + {contact.name} @@ -170,6 +214,7 @@ export function ChatConversationList({
+ diff --git a/src/modules/chat/hooks/useChat.js b/src/modules/chat/hooks/useChat.js index f6fe6fb..f86a570 100644 --- a/src/modules/chat/hooks/useChat.js +++ b/src/modules/chat/hooks/useChat.js @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useWhatsappSocket } from '../../../shared/hooks/useWhatsappSocket'; import { API_BASE_URL } from '../../../shared/services/apiConfig'; import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService'; -import { getCurrentUser } from '../../auth/services/sessionService'; +import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService'; import { chatContacts, transferAreas as fallbackTransferAreas } from '../services/chatMocks'; function buildInitialMessages() { @@ -212,8 +212,10 @@ function getUserDisplayName(user) { export function useChat() { const currentUser = getCurrentUser(); + const currentUserProfile = getCurrentUserProfile(); const currentUserId = getUserId(currentUser); const currentUserAreas = getUserAreas(currentUser); + const isAdminUser = currentUserProfile === 'admin'; const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket(); const [contacts, setContacts] = useState(buildFallbackContacts); const [activeContactId, setActiveContactId] = useState(chatContacts[0].id); @@ -261,7 +263,7 @@ export function useChat() { const isWaitingCustomerReply = Boolean(activeAssignment?.awaiting_customer_reply); const isQueuedForUserArea = Boolean( activeAssignment?.status === 'queued' && - (!activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)), + (isAdminUser || !activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)), ); const canAssumeChat = Boolean(activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea); const canReply = Boolean(isAssignedToCurrentUser && !isWaitingCustomerReply); @@ -314,6 +316,9 @@ export function useChat() { }, []); function canSeeContact(contact) { + if (isAdminUser) { + return Boolean(contact.assignment && contact.assignment.status !== 'bot_triage'); + } if (!currentUserAreas.length) return false; if (!contact.assignment) return false; if (contact.assignment.status === 'bot_triage') return false; @@ -369,7 +374,7 @@ export function useChat() { isMounted = false; window.clearInterval(intervalId); }; - }, [currentUserId, currentUserAreas.join('|'), whatsappStatus]); + }, [currentUserId, currentUserAreas.join('|'), isAdminUser, whatsappStatus]); useEffect(() => { if (!activeContactId) return; @@ -724,6 +729,7 @@ export function useChat() { } return { + currentUserId, contacts, activeContact, activeContactId, diff --git a/src/modules/chat/pages/ChatPage.jsx b/src/modules/chat/pages/ChatPage.jsx index ed883a0..4e60ee7 100644 --- a/src/modules/chat/pages/ChatPage.jsx +++ b/src/modules/chat/pages/ChatPage.jsx @@ -13,6 +13,7 @@ export function ChatPage() { const [searchParams, setSearchParams] = useSearchParams(); const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport(); const { + currentUserId, contacts, activeContact, activeContactId, @@ -144,6 +145,7 @@ export function ChatPage() { setIsTransferOpen(false); setIsContactPanelOpen(true); }} + currentUserId={currentUserId} isMobile={isMobile} /> diff --git a/src/modules/management/components/ManagementLayout.jsx b/src/modules/management/components/ManagementLayout.jsx index 76e2b65..6d54664 100644 --- a/src/modules/management/components/ManagementLayout.jsx +++ b/src/modules/management/components/ManagementLayout.jsx @@ -4,11 +4,15 @@ import { clearSession } from '../../auth/services/sessionService'; const navigationBySection = { supervisor: [ - { id: 'dashboard', label: 'Dashboard', count: null }, - { id: 'queues', label: 'Filas em tempo real', count: 42 }, - { id: 'areas', label: 'Especialidades supervisionadas', count: 3 }, - { id: 'agents', label: 'Agentes online', count: 18 }, - { id: 'reports', label: 'Relatórios', count: null }, + { id: 'dashboard', label: 'Home' }, + { id: 'templates', label: 'Templates' }, + { id: 'knowledge', label: 'Base de conhecimento IA' }, + { id: 'audit', label: 'Auditoria' }, + { type: 'separator' }, + { id: 'attendance', label: 'Atendimento' }, + { id: 'new-attendance', label: 'Abrir Atendimento', path: '/new-attendance' }, + { id: 'mass-message', label: 'Disparo em Massa' }, + { id: 'contacts', label: 'Contatos' }, ], admin: [ { id: 'home', label: 'Home' }, @@ -31,7 +35,7 @@ const navigationBySection = { const actionLabelBySection = { supervisor: '+ Redistribuir atendimento', - admin: '+ Nova configuração', + admin: 'Home', }; export function ManagementLayout({ @@ -48,7 +52,7 @@ export function ManagementLayout({ }) { const navigate = useNavigate(); const navItems = navigationBySection[activeSection] || navigationBySection.supervisor; - const actionLabel = actionLabelBySection[activeSection] || '+ Nova ação'; + const actionLabel = actionLabelBySection[activeSection] || 'Home'; function handleLogout() { clearSession(); diff --git a/src/modules/management/components/TemplateManagementPanel.jsx b/src/modules/management/components/TemplateManagementPanel.jsx new file mode 100644 index 0000000..8b77cb0 --- /dev/null +++ b/src/modules/management/components/TemplateManagementPanel.jsx @@ -0,0 +1,347 @@ +import { useEffect, useMemo, useState } from 'react'; +import { DataPanel } from './DataPanel'; +import { + approveTemplateByAdmin, + deleteTemplate, + listTemplates, + rejectTemplateByAdmin, + saveTemplate, +} from '../services/templateService'; + +const fieldStyle = { + width: '100%', + border: '1px solid var(--color-border)', + borderRadius: '14px', + padding: '0.75rem 0.85rem', + background: '#fff', + color: 'var(--color-text)', + fontWeight: 600, +}; + +const statusMeta = { + approved: { + label: 'Aprovado pela Meta', + background: 'rgba(34, 197, 94, 0.12)', + color: '#15803d', + }, + meta_review: { + label: 'Em análise pela Meta', + background: 'rgba(229, 162, 42, 0.16)', + color: '#8a5a00', + }, + admin_review: { + label: 'Aguardando aprovação do admin', + background: 'rgba(0, 49, 80, 0.1)', + color: 'var(--color-primary)', + }, + rejected: { + label: 'Reprovado pelo admin', + background: 'rgba(181, 31, 31, 0.1)', + color: 'var(--color-secondary)', + }, +}; + +function getTemplateStatus(template) { + return statusMeta[template.status] || statusMeta.approved; +} + +function getRemainingMetaText(template) { + if (template.status !== 'meta_review' || !template.meta_submitted_at) return ''; + const submittedAt = new Date(template.meta_submitted_at).getTime(); + const approvedAt = submittedAt + 15 * 60 * 1000; + const remainingMs = approvedAt - Date.now(); + if (remainingMs <= 0) return 'Aprovação fake disponível ao atualizar.'; + const minutes = Math.ceil(remainingMs / 60000); + return `Aprovação fake em aproximadamente ${minutes} min.`; +} + +export function TemplateManagementPanel({ + areas = [], + mode = 'admin', + managedAreaNames = [], + isMobile = false, +}) { + const [templates, setTemplates] = useState([]); + const [selectedArea, setSelectedArea] = useState('all'); + const [form, setForm] = useState({ name: '', content: '', areaId: '' }); + const [statusMessage, setStatusMessage] = useState(''); + const [isLoading, setIsLoading] = useState(true); + + const isAdmin = mode === 'admin'; + const visibleAreas = isAdmin + ? areas + : areas.filter((area) => managedAreaNames.includes(area.nome)); + + const filteredTemplates = useMemo(() => { + return templates.filter((template) => { + const areaMatches = selectedArea === 'all' || String(template.area_id || '') === selectedArea; + const supervisorAreaMatches = + isAdmin || + !template.area_nome || + managedAreaNames.includes(template.area_nome); + return areaMatches && supervisorAreaMatches; + }); + }, [templates, selectedArea, isAdmin, managedAreaNames]); + + async function loadTemplates() { + setIsLoading(true); + try { + const data = await listTemplates(); + setTemplates(Array.isArray(data) ? data : []); + setStatusMessage(''); + } catch (error) { + setStatusMessage(error.message); + } finally { + setIsLoading(false); + } + } + + useEffect(() => { + loadTemplates(); + }, []); + + async function submitTemplate(event) { + event.preventDefault(); + const name = form.name.trim(); + const content = form.content.trim(); + if (!name || !content) return; + + try { + await saveTemplate({ + name, + content, + areaId: Number(form.areaId) || null, + requestedByRole: isAdmin ? 'admin' : 'supervisor', + }); + setForm({ name: '', content: '', areaId: '' }); + setStatusMessage( + isAdmin + ? 'Template enviado para aprovação.' + : 'Template enviado para aprovação do admin.', + ); + await loadTemplates(); + } catch (error) { + setStatusMessage(error.message); + } + } + + async function approveTemplate(templateId) { + try { + await approveTemplateByAdmin(templateId); + setStatusMessage('Template aprovado pelo admin e enviado para análise fake da Meta.'); + await loadTemplates(); + } catch (error) { + setStatusMessage(error.message); + } + } + + async function rejectTemplate(templateId) { + try { + await rejectTemplateByAdmin(templateId); + setStatusMessage('Template reprovado pelo admin.'); + await loadTemplates(); + } catch (error) { + setStatusMessage(error.message); + } + } + + async function removeTemplate(templateId) { + try { + await deleteTemplate(templateId); + setStatusMessage('Template excluído.'); + await loadTemplates(); + } catch (error) { + setStatusMessage(error.message); + } + } + + return ( +
+
+ +
+ + +
+
+ setForm((current) => ({ ...current, name: event.target.value }))} + placeholder="Identificador do template" + style={fieldStyle} + /> + +
+ +