diff --git a/src/modules/chat/components/ChatWindow.jsx b/src/modules/chat/components/ChatWindow.jsx index ffbd453..8b4691f 100644 --- a/src/modules/chat/components/ChatWindow.jsx +++ b/src/modules/chat/components/ChatWindow.jsx @@ -318,14 +318,16 @@ export function ChatWindow({ assignmentLabel, transferNote, isReplying, + isPaused = false, + pauseDurationLabel = '00:00', isMobile = false, }) { const messagesRef = useRef(null); const safeContact = contact || { id: '', - name: 'Nenhuma conversa ativa', + name: isPaused ? 'Atendimento pausado' : 'Nenhuma conversa ativa', status: 'offline', - lastSeen: 'Aguardando fila do Omnino', + lastSeen: isPaused ? `Pausa em andamento: ${pauseDurationLabel}` : 'Aguardando fila do Omnino', }; useEffect(() => { @@ -340,6 +342,62 @@ export function ChatWindow({ }); }, [messages, isReplying]); + if (isPaused) { + return ( +
+
+ Atendimento pausado + Pausa em andamento: {pauseDurationLabel} +
+ +
+
+ Voce esta em pausa ha {pauseDurationLabel}. Retome o atendimento pela Home para visualizar a fila, + assumir chamados e responder clientes. +
+
+
+ ); + } + return (
- Nenhuma mensagem carregada. + {isPaused + ? `Voce esta em pausa ha ${pauseDurationLabel}. Volte da pausa para visualizar a fila e seus atendimentos.` + : 'Nenhuma mensagem carregada.'} ) : null} @@ -625,7 +685,9 @@ export function ChatWindow({ }} > - {canAssumeChat + {isPaused + ? `Voce esta em pausa ha ${pauseDurationLabel}. Nenhum atendimento sera exibido ate voce voltar.` + : canAssumeChat ? 'Este atendimento está na fila. Assuma para responder ou transferir.' : assignmentLabel || 'Este atendimento está atribuído a outro usuário.'} @@ -681,7 +743,9 @@ export function ChatWindow({ }} disabled={!safeContact.id || !canReply} placeholder={ - !safeContact.id + isPaused + ? 'Voce esta em pausa' + : !safeContact.id ? 'Aguardando conversa entrar em uma fila' : canReply ? 'Escreva sua mensagem...' diff --git a/src/modules/chat/hooks/useChat.js b/src/modules/chat/hooks/useChat.js index 77cdfaa..0b8d37f 100644 --- a/src/modules/chat/hooks/useChat.js +++ b/src/modules/chat/hooks/useChat.js @@ -4,9 +4,33 @@ import { API_BASE_URL } from '../../../shared/services/apiConfig'; import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService'; import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService'; import { transferAreas as fallbackTransferAreas } from '../services/chatMocks'; +import { + getAgentPresence, + listAgentPresence, + pauseAgent, + resumeAgent, +} from '../services/agentPresenceService'; const MAX_ATTACHMENT_SIZE_BYTES = 15 * 1024 * 1024; +function getPresenceByUserId(presenceList, userId) { + return presenceList.find((presence) => Number(presence.user_id) === Number(userId)) || null; +} + +function formatPauseDuration(totalSeconds) { + const seconds = Math.max(0, Number(totalSeconds || 0)); + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + + if (hours > 0) { + return `${hours}h ${String(remainingMinutes).padStart(2, '0')}m`; + } + + return `${String(remainingMinutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`; +} + function getLastMessageFromMe(messages = []) { const lastMessage = [...messages].reverse().find(isDisplayableMessage); if (!lastMessage) return false; @@ -210,6 +234,10 @@ export function useChat() { const [attachedFile, setAttachedFile] = useState(null); const [areaOptions, setAreaOptions] = useState([]); const [accessUsers, setAccessUsers] = useState([]); + const [presenceList, setPresenceList] = useState([]); + const [agentPresence, setAgentPresence] = useState(null); + const [pauseSeconds, setPauseSeconds] = useState(0); + const [isPresenceLoading, setIsPresenceLoading] = useState(false); const [selectedArea, setSelectedArea] = useState('Sem fila'); const [isTransferOpen, setIsTransferOpen] = useState(false); const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte'); @@ -221,6 +249,7 @@ export function useChat() { const [apiError, setApiError] = useState(null); const activeContactRef = useRef(activeContactId); const contactsRef = useRef(contacts); + const isPaused = agentPresence?.status === 'paused'; const activeContact = useMemo( () => { @@ -240,8 +269,12 @@ export function useChat() { const usersInTransferArea = accessUsers.filter((user) => user.areas?.some((area) => area.nome === transferArea) || user.areaPrincipal?.nome === transferArea, ); + const availableUsersInTransferArea = usersInTransferArea.filter((user) => { + const presence = getPresenceByUserId(presenceList, user.id); + return !presence || presence.status === 'available'; + }); const isSameUserArea = currentUserAreas.includes(transferArea); - const attendants = isSameUserArea ? usersInTransferArea : []; + const attendants = isSameUserArea ? availableUsersInTransferArea : []; const activeAssignment = activeContact?.assignment || null; const isAssignedToCurrentUser = Boolean( activeAssignment?.user_id && currentUserId && Number(activeAssignment.user_id) === currentUserId, @@ -251,8 +284,8 @@ export function useChat() { activeAssignment?.status === 'queued' && (isAdminUser || !activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)), ); - const canAssumeChat = Boolean(activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea); - const canReply = Boolean(isAssignedToCurrentUser && !isWaitingCustomerReply); + const canAssumeChat = Boolean(!isPaused && activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea); + const canReply = Boolean(!isPaused && isAssignedToCurrentUser && !isWaitingCustomerReply); const assignmentLabel = activeAssignment?.user_id ? isWaitingCustomerReply ? 'Aguardando resposta do cliente para liberar novas mensagens' @@ -268,7 +301,7 @@ export function useChat() { useEffect(() => { setTransferAttendant(attendants[0]?.id ? String(attendants[0].id) : ''); - }, [transferArea, accessUsers]); + }, [transferArea, accessUsers, presenceList]); useEffect(() => { activeContactRef.current = activeContactId; @@ -283,14 +316,23 @@ export function useChat() { async function loadAccessData() { try { - const [options, users] = await Promise.all([getAccessOptions(), getAccessUsers()]); + const [options, users, presences, currentPresence] = await Promise.all([ + getAccessOptions(), + getAccessUsers(), + listAgentPresence(), + currentUserId ? getAgentPresence(currentUserId) : Promise.resolve(null), + ]); if (!isMounted) return; setAreaOptions(options.areas || []); setAccessUsers(users || []); + setPresenceList(Array.isArray(presences) ? presences : []); + setAgentPresence(currentPresence); + setPauseSeconds(Number(currentPresence?.paused_seconds || 0)); } catch { if (isMounted) { setAreaOptions([]); setAccessUsers([]); + setPresenceList([]); } } } @@ -299,7 +341,43 @@ export function useChat() { return () => { isMounted = false; }; - }, []); + }, [currentUserId]); + + useEffect(() => { + if (!currentUserId) return undefined; + + let isMounted = true; + async function refreshPresence() { + try { + const [presences, currentPresence] = await Promise.all([ + listAgentPresence(), + getAgentPresence(currentUserId), + ]); + if (!isMounted) return; + setPresenceList(Array.isArray(presences) ? presences : []); + setAgentPresence(currentPresence); + setPauseSeconds(Number(currentPresence?.paused_seconds || 0)); + } catch { + if (isMounted) setPresenceList([]); + } + } + + const intervalId = window.setInterval(refreshPresence, 30000); + return () => { + isMounted = false; + window.clearInterval(intervalId); + }; + }, [currentUserId]); + + useEffect(() => { + if (!isPaused) return undefined; + + const intervalId = window.setInterval(() => { + setPauseSeconds((current) => current + 1); + }, 1000); + + return () => window.clearInterval(intervalId); + }, [isPaused]); function canSeeContact(contact) { if (isAdminUser) { @@ -313,7 +391,15 @@ export function useChat() { return currentUserAreas.includes(contact.assignment.area_nome); } - async function loadChats({ showLoading = false } = {}) { + async function loadChats({ showLoading = false, ignorePause = false } = {}) { + if (isPaused && !ignorePause) { + setContacts((current) => (current.length ? [] : current)); + setActiveContactId(''); + setMessagesByContact({}); + setIsLoadingChats(false); + return; + } + if (whatsappStatus !== 'CONNECTED') { setContacts((current) => (current.length ? [] : current)); setActiveContactId(''); @@ -358,7 +444,7 @@ export function useChat() { isMounted = false; window.clearInterval(intervalId); }; - }, [currentUserId, currentUserAreas.join('|'), isAdminUser, whatsappStatus]); + }, [currentUserId, currentUserAreas.join('|'), isAdminUser, isPaused, whatsappStatus]); useEffect(() => { if (!activeContactId) return; @@ -586,6 +672,43 @@ export function useChat() { })); } + async function pauseAttendance() { + if (!currentUserId) return; + setIsPresenceLoading(true); + try { + const result = await pauseAgent(currentUserId); + setAgentPresence(result.presence); + setPauseSeconds(0); + setContacts([]); + setActiveContactId(''); + setMessagesByContact({}); + setIsTransferOpen(false); + setApiError(null); + } catch (error) { + setApiError(error.message); + } finally { + setIsPresenceLoading(false); + } + } + + async function resumeAttendance() { + if (!currentUserId) return; + setIsPresenceLoading(true); + try { + const result = await resumeAgent(currentUserId); + setAgentPresence(result.presence); + setPauseSeconds(0); + setApiError(null); + await loadChats({ showLoading: true, ignorePause: true }); + const presences = await listAgentPresence(); + setPresenceList(Array.isArray(presences) ? presences : []); + } catch (error) { + setApiError(error.message); + } finally { + setIsPresenceLoading(false); + } + } + async function sendMessage(messageText = draft, contactId = activeContactId) { const rawMessage = typeof messageText === 'string' ? messageText : draft; const trimmed = rawMessage.trim(); @@ -769,5 +892,12 @@ export function useChat() { transferNote, setTransferNote, submitTransfer, + agentPresence, + isPaused, + pauseSeconds, + pauseDurationLabel: formatPauseDuration(pauseSeconds), + isPresenceLoading, + pauseAttendance, + resumeAttendance, }; } diff --git a/src/modules/chat/pages/ChatPage.jsx b/src/modules/chat/pages/ChatPage.jsx index 4e60ee7..125fd5a 100644 --- a/src/modules/chat/pages/ChatPage.jsx +++ b/src/modules/chat/pages/ChatPage.jsx @@ -48,6 +48,8 @@ export function ChatPage() { transferNote, setTransferNote, submitTransfer, + isPaused, + pauseDurationLabel, } = useChat(); const requestedChatId = searchParams.get('chatId'); const handledRequestedChatIdRef = useRef(''); @@ -111,7 +113,7 @@ export function ChatPage() { textAlign: 'center', }} > - Atendimento em tempo real + {isPaused ? `Pausado ha ${pauseDurationLabel}` : 'Atendimento em tempo real'} @@ -188,6 +192,7 @@ export function ChatPage() { key={reply} type="button" onClick={() => setDraft(reply)} + disabled={isPaused} style={{ border: '1px solid var(--color-border)', borderRadius: '18px', @@ -196,6 +201,7 @@ export function ChatPage() { color: 'var(--color-primary)', fontWeight: 600, textAlign: 'left', + opacity: isPaused ? 0.55 : 1, }} > {reply} diff --git a/src/modules/chat/services/agentPresenceService.js b/src/modules/chat/services/agentPresenceService.js new file mode 100644 index 0000000..c96d8c3 --- /dev/null +++ b/src/modules/chat/services/agentPresenceService.js @@ -0,0 +1,46 @@ +import { API_BASE_URL } from '../../../shared/services/apiConfig'; + +async function request(path, options = {}) { + const response = await fetch(`${API_BASE_URL}${path}`, { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + throw new Error('Falha ao atualizar presenca do agente'); + } + + return response.json(); +} + +export async function getAgentPresence(userId) { + return request(`/agent/presence/me?userId=${encodeURIComponent(userId)}`); +} + +export async function listAgentPresence() { + return request('/agent/presence'); +} + +export async function pauseAgent(userId) { + return request('/agent/presence/pause', { + method: 'POST', + body: JSON.stringify({ userId }), + }); +} + +export async function resumeAgent(userId) { + return request('/agent/presence/resume', { + method: 'POST', + body: JSON.stringify({ userId }), + }); +} + +export async function markAgentOffline(userId) { + return request('/agent/presence/offline', { + method: 'POST', + body: JSON.stringify({ userId }), + }); +} diff --git a/src/modules/home/components/AttendantOpsPanel.jsx b/src/modules/home/components/AttendantOpsPanel.jsx index 3ccd854..ad4812a 100644 --- a/src/modules/home/components/AttendantOpsPanel.jsx +++ b/src/modules/home/components/AttendantOpsPanel.jsx @@ -1,29 +1,39 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; -export function AttendantOpsPanel({ activeChatsCount }) { - const [isPaused, setIsPaused] = useState(false); +export function AttendantOpsPanel({ + activeChatsCount, + isPaused = false, + pauseDurationLabel = '00:00', + isPresenceLoading = false, + onTogglePause, +}) { const [secondsOnline, setSecondsOnline] = useState(0); useEffect(() => { - let interval; - if (!isPaused) { - interval = setInterval(() => { - setSecondsOnline((s) => s + 1); - }, 1000); - } - return () => clearInterval(interval); + if (isPaused) return undefined; + + const intervalId = window.setInterval(() => { + setSecondsOnline((current) => current + 1); + }, 1000); + + return () => window.clearInterval(intervalId); }, [isPaused]); const formatTime = (totalSeconds) => { - const h = Math.floor(totalSeconds / 3600); - const m = Math.floor((totalSeconds % 3600) / 60); - const s = totalSeconds % 60; - return [h, m, s] - .map(v => v.toString().padStart(2, '0')) - .filter((v, i) => v !== '00' || i > 0) + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + return [hours, minutes, seconds] + .map((value) => value.toString().padStart(2, '0')) + .filter((value, index) => value !== '00' || index > 0) .join(':'); }; + const presenceLabel = isPaused ? 'Tempo em pausa' : 'Tempo online'; + const presenceTime = isPaused ? pauseDurationLabel : formatTime(secondsOnline); + const statusColor = isPaused ? '#ef4444' : '#10b981'; + return (
- Tempo Online + {presenceLabel} - {formatTime(secondsOnline)} + {presenceTime}
-
+
- Atendimentos Abertos + Atendimentos abertos - {activeChatsCount} + {isPaused ? 0 : activeChatsCount}
@@ -93,11 +105,13 @@ export function AttendantOpsPanel({ activeChatsCount }) { boxShadow: 'var(--shadow-sm)', display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }} >
diff --git a/src/modules/home/components/MessagesWorkspace.jsx b/src/modules/home/components/MessagesWorkspace.jsx index 89dfa9a..c18faa1 100644 --- a/src/modules/home/components/MessagesWorkspace.jsx +++ b/src/modules/home/components/MessagesWorkspace.jsx @@ -200,6 +200,8 @@ export function MessagesWorkspace({ isDesktop = false, isTablet = false, isMobile = false, + isPaused = false, + pauseDurationLabel = '00:00', }) { const navigate = useNavigate(); const messagesRef = useRef(null); @@ -311,6 +313,7 @@ export function MessagesWorkspace({ } async function sendSuggestedReply() { + if (isPaused) return; if (!safeActiveConversation.id || safeActiveConversation.id === 'empty') return; await onSendSuggestedReply?.(safeActiveConversation.id, selectedReply); @@ -393,7 +396,10 @@ export function MessagesWorkspace({ {conversations.length > 3 ? (