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 { chatContacts, transferAreas as fallbackTransferAreas } from '../services/chatMocks'; function buildInitialMessages() { return chatContacts.reduce((acc, contact) => { acc[contact.id] = contact.messages; return acc; }, {}); } function getLastMessageFromMe(messages = []) { const lastMessage = [...messages].reverse().find(isDisplayableMessage); if (!lastMessage) return false; return lastMessage.sender === 'agent' || lastMessage.fromMe === true; } function getSerializedId(value) { if (!value) return ''; if (typeof value === 'string') return value; return value._serialized || `${value.user || ''}@${value.server || 'c.us'}`; } function formatTime(timestamp) { if (!timestamp) return ''; const date = new Date(timestamp * 1000); return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); } function getContactName(chat) { const serializedId = getSerializedId(chat.id); return chat.name || chat.pushname || serializedId.split('@')[0] || 'Contato'; } function getPreviewFromMessage(message) { if (message?.body) return message.body; if (message?.text) return message.text; if (message?.hasMedia || message?.media) return '[Midia]'; return ''; } function normalizeChat(chat) { const id = getSerializedId(chat.id); const assignment = chat.assignment || null; const lastSeenTimestamp = chat.timestamp || null; const hasLastMessageFromMe = typeof chat.lastMessageFromMe === 'boolean'; return { id, name: getContactName(chat), channel: 'WhatsApp', status: lastSeenTimestamp ? 'away' : 'offline', area: assignment?.area_nome || (assignment?.area_id ? String(assignment.area_id) : 'Sem fila'), areaId: assignment?.area_id || null, lastSeen: lastSeenTimestamp ? `Última atividade as ${formatTime(lastSeenTimestamp)}` : 'Sem atividade recente', preview: chat.preview || chat.lastMessage?.body || '', time: formatTime(chat.timestamp) || 'Agora', unread: chat.unreadCount || 0, lastMessageFromMe: hasLastMessageFromMe ? chat.lastMessageFromMe : Boolean(chat.lastMessage?.fromMe), contactProfile: chat.contactProfile || null, assignment, }; } function normalizeMessage(message) { const id = getSerializedId(message.id) || message.id || `msg-${Date.now()}`; const sender = message.sender || (message.fromMe ? 'agent' : 'customer'); return { id, chatId: message.from || message.to || message.chatId, sender, text: message.body ?? message.text ?? '', timestamp: message.timestamp, hasMedia: Boolean(message.hasMedia || message.media), media: message.media || null, mediaLoading: false, mediaError: null, }; } function isDisplayableMessage(message) { const text = String(message?.text ?? message?.body ?? '').trim(); return Boolean(text || message?.hasMedia || message?.media); } function getComparableMessageTime(message) { if (message.timestamp) return Number(message.timestamp); if (typeof message.id === 'string' && message.id.startsWith('temp-')) { return Math.floor(Number(message.id.replace('temp-', '')) / 1000); } return 0; } function stripSenderHeader(text) { return String(text || '') .replace(/^\*(Atendente(?: virtual)?:\s*[^*]+)\*\s*\n+/i, '') .trim(); } function areLikelySameMessage(currentMessage, nextMessage) { if (!currentMessage || !nextMessage) return false; if (currentMessage.id && nextMessage.id && currentMessage.id === nextMessage.id) return true; if (currentMessage.chatId && nextMessage.chatId && currentMessage.chatId !== nextMessage.chatId) return false; if (currentMessage.sender !== nextMessage.sender) return false; if (stripSenderHeader(currentMessage.text) !== stripSenderHeader(nextMessage.text)) return false; if (Boolean(currentMessage.hasMedia) !== Boolean(nextMessage.hasMedia)) return false; const currentTime = getComparableMessageTime(currentMessage); const nextTime = getComparableMessageTime(nextMessage); if (!currentTime || !nextTime) return false; return Math.abs(currentTime - nextTime) <= 10; } function mergeMessageList(currentMessages, nextMessage) { const exactIndex = currentMessages.findIndex((message) => message.id === nextMessage.id); if (exactIndex >= 0) { return currentMessages.map((message, index) => (index === exactIndex ? { ...message, ...nextMessage } : message)); } const likelyIndex = currentMessages.findIndex((message) => areLikelySameMessage(message, nextMessage)); if (likelyIndex >= 0) { return currentMessages.map((message, index) => (index === likelyIndex ? { ...message, ...nextMessage } : message)); } return [...currentMessages, nextMessage]; } function dedupeMessages(messages) { return messages.reduce((acc, message) => mergeMessageList(acc, message), []); } function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = String(reader.result || ''); resolve(result.includes(',') ? result.split(',')[1] : result); }; reader.onerror = reject; reader.readAsDataURL(file); }); } function buildFallbackContacts() { return chatContacts.map((contact) => ({ ...contact, assignment: null, areaId: null, lastMessageFromMe: getLastMessageFromMe(contact.messages), })); } function normalizeComparableContact(contact) { return { id: contact.id, name: contact.name, preview: contact.preview, time: contact.time, unread: contact.unread, area: contact.area, areaId: contact.areaId, lastSeen: contact.lastSeen, lastMessageFromMe: contact.lastMessageFromMe, assignmentStatus: contact.assignment?.status || null, assignmentUserId: contact.assignment?.user_id || null, assignmentAreaId: contact.assignment?.area_id || null, transferNote: contact.assignment?.transfer_note || null, awaitingCustomerReply: contact.assignment?.awaiting_customer_reply || false, contactName: contact.contactProfile?.name || null, contactCompany: contact.contactProfile?.company || null, contactNote: contact.contactProfile?.note || null, contactPhone: contact.contactProfile?.phone || null, }; } function areContactListsEqual(currentContacts, nextContacts) { if (currentContacts.length !== nextContacts.length) return false; return currentContacts.every((contact, index) => { const currentComparable = normalizeComparableContact(contact); const nextComparable = normalizeComparableContact(nextContacts[index]); return JSON.stringify(currentComparable) === JSON.stringify(nextComparable); }); } function getUserId(user) { const value = user?.databaseId || user?.id; const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; } function getUserAreas(user) { const normalizeArea = (area) => { if (!area) return null; if (typeof area === 'string') return area; return area.nome || area.name || null; }; const areas = (Array.isArray(user?.areas) ? user.areas : []).map(normalizeArea).filter(Boolean); const primaryArea = normalizeArea(user?.areaPrincipal); if (primaryArea && !areas.includes(primaryArea)) { return [primaryArea, ...areas]; } return areas; } function getUserDisplayName(user) { return user?.name || user?.nome || user?.username || user?.email || 'Atendente'; } export function useChat() { const currentUser = getCurrentUser(); const currentUserId = getUserId(currentUser); const currentUserAreas = getUserAreas(currentUser); const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket(); const [contacts, setContacts] = useState(buildFallbackContacts); const [activeContactId, setActiveContactId] = useState(chatContacts[0].id); const [messagesByContact, setMessagesByContact] = useState(buildInitialMessages); const [draft, setDraft] = useState(''); const [attachedFile, setAttachedFile] = useState(null); const [areaOptions, setAreaOptions] = useState([]); const [accessUsers, setAccessUsers] = useState([]); const [selectedArea, setSelectedArea] = useState(chatContacts[0].area); const [isTransferOpen, setIsTransferOpen] = useState(false); const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte'); const [transferAttendant, setTransferAttendant] = useState(''); const [transferNote, setTransferNote] = useState(''); const [isReplying] = useState(false); const [isLoadingChats, setIsLoadingChats] = useState(false); const [isLoadingMessages, setIsLoadingMessages] = useState(false); const [apiError, setApiError] = useState(null); const activeContactRef = useRef(activeContactId); const contactsRef = useRef(contacts); const activeContact = useMemo( () => { const contact = contacts.find((item) => item.id === activeContactId) || contacts[0]; if (!contact || typeof contact.lastMessageFromMe === 'boolean') return contact; return { ...contact, lastMessageFromMe: getLastMessageFromMe(messagesByContact[contact.id] || []), }; }, [contacts, activeContactId, messagesByContact], ); const messages = messagesByContact[activeContactId] || []; const transferAreas = areaOptions.length ? areaOptions.map((area) => area.nome) : fallbackTransferAreas; const selectedTransferArea = areaOptions.find((area) => area.nome === transferArea) || null; const usersInTransferArea = accessUsers.filter((user) => user.areas?.some((area) => area.nome === transferArea) || user.areaPrincipal?.nome === transferArea, ); const isSameUserArea = currentUserAreas.includes(transferArea); const attendants = isSameUserArea ? usersInTransferArea : []; const activeAssignment = activeContact?.assignment || null; const isAssignedToCurrentUser = Boolean( activeAssignment?.user_id && currentUserId && Number(activeAssignment.user_id) === currentUserId, ); const isWaitingCustomerReply = Boolean(activeAssignment?.awaiting_customer_reply); const isQueuedForUserArea = Boolean( activeAssignment?.status === 'queued' && (!activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)), ); const canAssumeChat = Boolean(activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea); const canReply = Boolean(isAssignedToCurrentUser && !isWaitingCustomerReply); const assignmentLabel = activeAssignment?.user_id ? isWaitingCustomerReply ? 'Aguardando resposta do cliente para liberar novas mensagens' : `Atendimento com ${activeAssignment.user_nome || 'outro atendente'}` : activeAssignment?.area_nome ? `Na fila de ${activeAssignment.area_nome}` : 'Sem fila definida'; const transferNoteLabel = activeAssignment?.transfer_note || ''; useEffect(() => { setSelectedArea(activeContact?.area || 'Sem fila'); }, [activeContact]); useEffect(() => { setTransferAttendant(attendants[0]?.id ? String(attendants[0].id) : ''); }, [transferArea, accessUsers]); useEffect(() => { activeContactRef.current = activeContactId; }, [activeContactId]); useEffect(() => { contactsRef.current = contacts; }, [contacts]); useEffect(() => { let isMounted = true; async function loadAccessData() { try { const [options, users] = await Promise.all([getAccessOptions(), getAccessUsers()]); if (!isMounted) return; setAreaOptions(options.areas || []); setAccessUsers(users || []); } catch { if (isMounted) { setAreaOptions([]); setAccessUsers([]); } } } loadAccessData(); return () => { isMounted = false; }; }, []); function canSeeContact(contact) { if (!currentUserAreas.length) return false; if (!contact.assignment) return false; if (contact.assignment.status === 'bot_triage') return false; if (!contact.assignment.area_nome) return false; if (contact.assignment.user_id && Number(contact.assignment.user_id) === currentUserId) return true; return currentUserAreas.includes(contact.assignment.area_nome); } async function loadChats({ showLoading = false } = {}) { if (whatsappStatus !== 'CONNECTED') { const fallbackContacts = buildFallbackContacts(); setContacts((current) => (areContactListsEqual(current, fallbackContacts) ? current : fallbackContacts)); setActiveContactId((current) => fallbackContacts.some((contact) => contact.id === current) ? current : fallbackContacts[0]?.id, ); setIsLoadingChats(false); return; } if (showLoading) { setIsLoadingChats(true); } try { const response = await fetch(`${API_BASE_URL}/whatsapp/chats`); if (!response.ok) throw new Error('Falha ao carregar chats do WhatsApp.'); const data = await response.json(); if (!Array.isArray(data)) return; const nextContacts = data.map(normalizeChat).filter(canSeeContact); setContacts((current) => (areContactListsEqual(current, nextContacts) ? current : nextContacts)); setActiveContactId((current) => nextContacts.some((contact) => contact.id === current) ? current : nextContacts[0]?.id || '', ); setApiError(null); } catch (error) { setApiError(error.message); } finally { setIsLoadingChats(false); } } useEffect(() => { let isMounted = true; async function guardedLoadChats() { if (!isMounted) return; await loadChats({ showLoading: contactsRef.current.length === 0 }); } guardedLoadChats(); const intervalId = window.setInterval(guardedLoadChats, 30000); return () => { isMounted = false; window.clearInterval(intervalId); }; }, [currentUserId, currentUserAreas.join('|'), whatsappStatus]); useEffect(() => { if (!activeContactId) return; let isMounted = true; async function loadMessages() { if (!activeContactId.includes('@')) return; setIsLoadingMessages(true); try { const response = await fetch(`${API_BASE_URL}/whatsapp/messages/${encodeURIComponent(activeContactId)}`); if (!response.ok) throw new Error('Falha ao carregar mensagens do WhatsApp.'); const data = await response.json(); if (!isMounted || !Array.isArray(data)) return; const normalizedMessages = dedupeMessages( data .map((message) => ({ ...normalizeMessage(message), chatId: activeContactId, })) .filter(isDisplayableMessage), ); setMessagesByContact((current) => ({ ...current, [activeContactId]: normalizedMessages, })); updateContact(activeContactId, (contact) => ({ ...contact, lastMessageFromMe: getLastMessageFromMe(normalizedMessages), })); setApiError(null); } catch (error) { if (isMounted) setApiError(error.message); } finally { if (isMounted) setIsLoadingMessages(false); } } loadMessages(); return () => { isMounted = false; }; }, [activeContactId]); useEffect(() => { if (!incomingMessage) return; const contactId = incomingMessage.from || incomingMessage.to || incomingMessage.chatId; if (!contactId) return; const message = { ...normalizeMessage(incomingMessage), chatId: contactId, }; if (!isDisplayableMessage(message)) { clearIncomingMessage(); return; } const preview = getPreviewFromMessage(message); setMessagesByContact((current) => { const currentMessages = current[contactId] || []; const nextMessages = mergeMessageList(currentMessages, message); if (nextMessages === currentMessages) return current; return { ...current, [contactId]: nextMessages, }; }); setContacts((current) => { const existing = current.find((contact) => contact.id === contactId); if (!existing) { return current; } const nextContact = { ...(existing || { id: contactId, name: incomingMessage.notifyName || contactId.split('@')[0], channel: 'WhatsApp', status: 'away', area: 'Sem fila', lastSeen: 'Visto agora', unread: 0, assignment: null, }), preview, time: 'Agora', status: 'away', lastSeen: 'Última atividade agora', lastMessageFromMe: Boolean(incomingMessage.fromMe), unread: incomingMessage.fromMe || contactId === activeContactRef.current ? 0 : (existing?.unread || 0) + 1, }; return [nextContact, ...current.filter((contact) => contact.id !== contactId)]; }); clearIncomingMessage(); window.setTimeout(() => loadChats({ showLoading: false }), 1200); }, [incomingMessage, clearIncomingMessage]); function updateContact(contactId, updater) { setContacts((current) => current.map((contact) => (contact.id === contactId ? updater(contact) : contact)), ); } function updateContactPreview(contactId, preview, media) { updateContact(contactId, (contact) => ({ ...contact, preview: media ? `[Midia: ${media.filename || 'Arquivo'}]` : preview, time: 'Agora', unread: 0, lastMessageFromMe: true, })); } function updateContactProfile(contactId, profile) { updateContact(contactId, (contact) => ({ ...contact, name: profile.name || contact.name, contactProfile: profile, })); } async function attachFile(file) { if (!file) return; const data = await fileToBase64(file); setAttachedFile({ name: file.name, type: file.type || 'application/octet-stream', data, }); } function removeAttachedFile() { setAttachedFile(null); } async function hydrateMessageMedia(contactId, messageId) { if (!contactId || !messageId) return; setMessagesByContact((current) => ({ ...current, [contactId]: (current[contactId] || []).map((message) => message.id === messageId ? { ...message, mediaLoading: true, mediaError: null } : message, ), })); try { const response = await fetch( `${API_BASE_URL}/whatsapp/media/${encodeURIComponent(contactId)}/${encodeURIComponent(messageId)}`, ); if (!response.ok) throw new Error('Falha ao carregar midia.'); const media = await response.json(); setMessagesByContact((current) => ({ ...current, [contactId]: (current[contactId] || []).map((message) => message.id === messageId ? { ...message, media, mediaLoading: false } : message, ), })); } catch (error) { setMessagesByContact((current) => ({ ...current, [contactId]: (current[contactId] || []).map((message) => message.id === messageId ? { ...message, mediaLoading: false, mediaError: error.message || 'Erro ao carregar midia.' } : message, ), })); } } async function assumeChat(contactId = activeContactId) { if (!contactId?.includes('@') || !currentUserId) return null; const targetContact = contacts.find((contact) => contact.id === contactId) || activeContact; const targetAssignment = targetContact?.assignment || null; const areaId = targetContact?.areaId || targetAssignment?.area_id || areaOptions.find((area) => currentUserAreas.includes(area.nome))?.id; try { const response = await fetch(`${API_BASE_URL}/whatsapp/assign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chatId: contactId, userId: String(currentUserId), areaId, }), }); if (!response.ok) throw new Error('Nao foi possivel assumir o atendimento.'); const assignment = await response.json(); updateContact(contactId, (contact) => ({ ...contact, assignment, area: assignment.area_nome || contact.area, areaId: assignment.area_id || contact.areaId, })); setApiError(null); return assignment; } catch (error) { setApiError(error.message); return null; } } async function releaseChat() { if (!activeContactId?.includes('@')) return; const response = await fetch(`${API_BASE_URL}/whatsapp/release/${encodeURIComponent(activeContactId)}`, { method: 'DELETE', }); if (!response.ok) throw new Error('Nao foi possivel sair do atendimento.'); const assignment = await response.json(); updateContact(activeContactId, (contact) => ({ ...contact, assignment, area: assignment?.area_nome || contact.area, areaId: assignment?.area_id || contact.areaId, })); } async function sendMessage(messageText = draft, contactId = activeContactId) { const trimmed = String(messageText || '').trim(); if (!trimmed && !attachedFile) return; const targetContact = contacts.find((contact) => contact.id === contactId) || activeContact; const targetAssignment = targetContact?.assignment || null; const targetIsAssignedToCurrentUser = Boolean( targetAssignment?.user_id && currentUserId && Number(targetAssignment.user_id) === currentUserId, ); const targetIsWaitingCustomerReply = Boolean(targetAssignment?.awaiting_customer_reply); try { if (!targetIsAssignedToCurrentUser) { setApiError('Assuma o atendimento antes de responder.'); return; } if (targetIsWaitingCustomerReply) { setApiError('Aguarde o cliente responder antes de enviar novas mensagens.'); return; } } catch (error) { setApiError(error.message); return; } const media = attachedFile ? { data: attachedFile.data, mimetype: attachedFile.type, filename: attachedFile.name, } : null; const newMessage = { id: `temp-${Date.now()}`, chatId: contactId, sender: 'agent', text: trimmed, timestamp: Math.floor(Date.now() / 1000), hasMedia: Boolean(media), media, }; setMessagesByContact((current) => ({ ...current, [contactId]: mergeMessageList(current[contactId] || [], newMessage), })); updateContactPreview(contactId, trimmed || '[Midia]', media); if (contactId === activeContactId) { setDraft(''); } setAttachedFile(null); if (!contactId.includes('@')) return; try { await fetch(`${API_BASE_URL}/whatsapp/send`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: contactId, message: trimmed, senderName: getUserDisplayName(currentUser), media, }), }); setApiError(null); updateContact(contactId, (contact) => ({ ...contact, assignment: contact.assignment ? { ...contact.assignment, transfer_note: null } : contact.assignment, })); } catch (error) { setApiError(error.message); } } async function submitTransfer() { const note = transferNote.trim(); const areaId = selectedTransferArea?.id; if (!areaId) { setApiError('Selecione uma area valida para transferencia.'); return; } if (!isAssignedToCurrentUser) { setApiError('Assuma o atendimento antes de transferir.'); return; } const targetUserId = isSameUserArea && transferAttendant ? Number(transferAttendant) : null; const response = await fetch(`${API_BASE_URL}/whatsapp/transfer`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chatId: activeContactId, areaId, userId: targetUserId, note, }), }); if (!response.ok) { setApiError('Nao foi possivel transferir o atendimento.'); return; } const assignment = await response.json(); const transferMessage = targetUserId ? `Transferencia solicitada para ${transferArea}. Obs: ${note || 'Sem observacao.'}` : `Transferencia enviada para a fila de ${transferArea}. Obs: ${note || 'Sem observacao.'}`; setMessagesByContact((current) => ({ ...current, [activeContactId]: [ ...(current[activeContactId] || []), { id: Date.now() + 2, sender: 'system', text: transferMessage }, ], })); updateContact(activeContactId, (contact) => ({ ...contact, area: assignment.area_nome || transferArea, areaId: assignment.area_id || areaId, assignment, })); setSelectedArea(transferArea); setIsTransferOpen(false); setTransferNote(''); setApiError(null); } return { contacts, activeContact, activeContactId, setActiveContactId, messages, draft, setDraft, attachedFile, attachFile, removeAttachedFile, sendMessage, hydrateMessageMedia, assumeChat, releaseChat, canAssumeChat, canReply, assignmentLabel, transferNoteLabel, isAssignedToCurrentUser, activeAssignment, updateContactProfile, isReplying, isLoadingChats, isLoadingMessages, apiError, selectedArea, setSelectedArea, isTransferOpen, setIsTransferOpen, transferArea, setTransferArea, transferAreas, attendants, isSameUserArea, transferAttendant, setTransferAttendant, transferNote, setTransferNote, submitTransfer, }; }