omnichannel-frontend/src/modules/chat/hooks/useChat.js

400 lines
12 KiB
JavaScript
Raw Normal View History

import { useEffect, useMemo, useRef, useState } from 'react';
import { useWhatsappSocket } from '../../../shared/hooks/useWhatsappSocket';
import {
attendantsByArea,
chatContacts,
transferAreas,
} from '../services/chatMocks';
const API_BASE_URL = 'http://localhost:3001';
function buildInitialMessages() {
return chatContacts.reduce((acc, contact) => {
acc[contact.id] = contact.messages;
return acc;
}, {});
}
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);
return {
id,
name: getContactName(chat),
channel: 'WhatsApp',
status: 'online',
area: chat.assignment?.area_id ? String(chat.assignment.area_id) : 'Suporte',
lastSeen: chat.timestamp ? `Visto as ${formatTime(chat.timestamp)}` : 'Online agora',
preview: chat.preview || chat.lastMessage?.body || '',
time: formatTime(chat.timestamp) || 'Agora',
unread: chat.unreadCount || 0,
assignment: chat.assignment || null,
};
}
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 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 }));
}
export function useChat() {
const { 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 [selectedArea, setSelectedArea] = useState(chatContacts[0].area);
const [isTransferOpen, setIsTransferOpen] = useState(false);
const [transferArea, setTransferArea] = useState('Suporte');
const [transferAttendant, setTransferAttendant] = useState(attendantsByArea.Suporte[0]);
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 activeContact = useMemo(
() => contacts.find((contact) => contact.id === activeContactId) || contacts[0],
[contacts, activeContactId],
);
const messages = messagesByContact[activeContactId] || [];
const attendants = attendantsByArea[transferArea] || [];
useEffect(() => {
setSelectedArea(activeContact.area);
}, [activeContact]);
useEffect(() => {
setTransferAttendant(attendants[0] || '');
}, [transferArea]);
useEffect(() => {
activeContactRef.current = activeContactId;
}, [activeContactId]);
useEffect(() => {
let isMounted = true;
async function loadChats() {
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 (!isMounted || !Array.isArray(data) || data.length === 0) return;
const nextContacts = data.map(normalizeChat);
setContacts(nextContacts);
setActiveContactId((current) =>
nextContacts.some((contact) => contact.id === current) ? current : nextContacts[0].id,
);
setApiError(null);
} catch (error) {
if (isMounted) setApiError(error.message);
} finally {
if (isMounted) setIsLoadingChats(false);
}
}
loadChats();
const intervalId = window.setInterval(loadChats, 30000);
return () => {
isMounted = false;
window.clearInterval(intervalId);
};
}, []);
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;
setMessagesByContact((current) => ({
...current,
[activeContactId]: data.map((message) => ({
...normalizeMessage(message),
chatId: activeContactId,
})),
}));
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,
};
const preview = getPreviewFromMessage(message);
setMessagesByContact((current) => {
const currentMessages = current[contactId] || [];
if (currentMessages.some((item) => item.id === message.id)) return current;
return {
...current,
[contactId]: [...currentMessages, message],
};
});
setContacts((current) => {
const existing = current.find((contact) => contact.id === contactId);
const nextContact = {
...(existing || {
id: contactId,
name: incomingMessage.notifyName || contactId.split('@')[0],
channel: 'WhatsApp',
status: 'online',
area: 'Suporte',
lastSeen: 'Online agora',
unread: 0,
}),
preview,
time: 'Agora',
unread:
incomingMessage.fromMe || contactId === activeContactRef.current
? 0
: (existing?.unread || 0) + 1,
};
return [nextContact, ...current.filter((contact) => contact.id !== contactId)];
});
clearIncomingMessage();
}, [incomingMessage, clearIncomingMessage]);
function updateContactPreview(contactId, preview, media) {
setContacts((current) =>
current.map((contact) =>
contact.id === contactId
? { ...contact, preview: media ? `[Midia: ${media.filename || 'Arquivo'}]` : preview, time: 'Agora', unread: 0 }
: contact,
),
);
}
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 sendMessage() {
const trimmed = draft.trim();
if (!trimmed && !attachedFile) {
return;
}
const media = attachedFile
? {
data: attachedFile.data,
mimetype: attachedFile.type,
filename: attachedFile.name,
}
: null;
const newMessage = {
id: `temp-${Date.now()}`,
chatId: activeContactId,
sender: 'agent',
text: trimmed,
hasMedia: Boolean(media),
media,
};
setMessagesByContact((current) => ({
...current,
[activeContactId]: [...(current[activeContactId] || []), newMessage],
}));
updateContactPreview(activeContactId, trimmed || '[Midia]', media);
setDraft('');
setAttachedFile(null);
if (!activeContactId.includes('@')) return;
try {
await fetch(`${API_BASE_URL}/whatsapp/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: activeContactId,
message: trimmed,
media,
}),
});
setApiError(null);
} catch (error) {
setApiError(error.message);
}
}
function submitTransfer() {
const note = transferNote.trim();
const transferMessage = note
? `Transferencia solicitada para ${transferArea} com ${transferAttendant}. Obs: ${note}`
: `Transferencia solicitada para ${transferArea} com ${transferAttendant}.`;
setMessagesByContact((current) => ({
...current,
[activeContactId]: [
...(current[activeContactId] || []),
{ id: Date.now() + 2, sender: 'system', text: transferMessage },
],
}));
setContacts((current) =>
current.map((contact) =>
contact.id === activeContactId ? { ...contact, area: transferArea } : contact,
),
);
setSelectedArea(transferArea);
setIsTransferOpen(false);
setTransferNote('');
}
return {
contacts,
activeContact,
activeContactId,
setActiveContactId,
messages,
draft,
setDraft,
attachedFile,
attachFile,
removeAttachedFile,
sendMessage,
hydrateMessageMedia,
isReplying,
isLoadingChats,
isLoadingMessages,
apiError,
selectedArea,
setSelectedArea,
isTransferOpen,
setIsTransferOpen,
transferArea,
setTransferArea,
transferAreas,
attendants,
transferAttendant,
setTransferAttendant,
transferNote,
setTransferNote,
submitTransfer,
};
}