2026-03-19 18:22:18 -03:00
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
2026-05-18 17:34:23 -03:00
|
|
|
import { useWhatsappSocket } from '../../../shared/hooks/useWhatsappSocket';
|
2026-05-19 09:27:02 -03:00
|
|
|
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
2026-05-19 15:29:43 -03:00
|
|
|
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
|
|
|
|
|
import { getCurrentUser } from '../../auth/services/sessionService';
|
|
|
|
|
import { chatContacts, transferAreas as fallbackTransferAreas } from '../services/chatMocks';
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
function buildInitialMessages() {
|
|
|
|
|
return chatContacts.reduce((acc, contact) => {
|
|
|
|
|
acc[contact.id] = contact.messages;
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
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);
|
2026-05-19 09:45:00 -03:00
|
|
|
const lastActivitySeconds = chat.timestamp ? Math.floor(Date.now() / 1000) - chat.timestamp : null;
|
|
|
|
|
const isRecentlyActive = lastActivitySeconds !== null && lastActivitySeconds < 300;
|
2026-05-19 15:29:43 -03:00
|
|
|
const assignment = chat.assignment || null;
|
2026-05-19 09:45:00 -03:00
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
name: getContactName(chat),
|
|
|
|
|
channel: 'WhatsApp',
|
2026-05-19 09:45:00 -03:00
|
|
|
status: isRecentlyActive ? 'online' : 'away',
|
2026-05-19 15:29:43 -03:00
|
|
|
area: assignment?.area_nome || (assignment?.area_id ? String(assignment.area_id) : 'Sem fila'),
|
|
|
|
|
areaId: assignment?.area_id || null,
|
2026-05-19 09:45:00 -03:00
|
|
|
lastSeen: isRecentlyActive
|
|
|
|
|
? 'Online agora'
|
|
|
|
|
: chat.timestamp
|
|
|
|
|
? `Visto as ${formatTime(chat.timestamp)}`
|
|
|
|
|
: 'Sem atividade recente',
|
2026-05-18 17:34:23 -03:00
|
|
|
preview: chat.preview || chat.lastMessage?.body || '',
|
|
|
|
|
time: formatTime(chat.timestamp) || 'Agora',
|
|
|
|
|
unread: chat.unreadCount || 0,
|
2026-05-19 15:29:43 -03:00
|
|
|
assignment,
|
2026-05-18 17:34:23 -03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
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), []);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
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() {
|
2026-05-19 15:29:43 -03:00
|
|
|
return chatContacts.map((contact) => ({ ...contact, assignment: null, areaId: null }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getUserId(user) {
|
|
|
|
|
const value = user?.databaseId || user?.id;
|
|
|
|
|
const numeric = Number(value);
|
|
|
|
|
return Number.isFinite(numeric) ? numeric : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getUserAreas(user) {
|
|
|
|
|
const areas = Array.isArray(user?.areas) ? user.areas : [];
|
|
|
|
|
if (user?.areaPrincipal && !areas.includes(user.areaPrincipal)) {
|
|
|
|
|
return [user.areaPrincipal, ...areas];
|
|
|
|
|
}
|
|
|
|
|
return areas;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getUserDisplayName(user) {
|
|
|
|
|
return user?.name || user?.nome || user?.username || user?.email || 'Atendente';
|
2026-05-18 17:34:23 -03:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 18:22:18 -03:00
|
|
|
export function useChat() {
|
2026-05-19 15:29:43 -03:00
|
|
|
const currentUser = getCurrentUser();
|
|
|
|
|
const currentUserId = getUserId(currentUser);
|
|
|
|
|
const currentUserAreas = getUserAreas(currentUser);
|
|
|
|
|
const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket();
|
2026-05-18 17:34:23 -03:00
|
|
|
const [contacts, setContacts] = useState(buildFallbackContacts);
|
2026-03-19 18:22:18 -03:00
|
|
|
const [activeContactId, setActiveContactId] = useState(chatContacts[0].id);
|
|
|
|
|
const [messagesByContact, setMessagesByContact] = useState(buildInitialMessages);
|
|
|
|
|
const [draft, setDraft] = useState('');
|
2026-05-18 17:34:23 -03:00
|
|
|
const [attachedFile, setAttachedFile] = useState(null);
|
2026-05-19 15:29:43 -03:00
|
|
|
const [areaOptions, setAreaOptions] = useState([]);
|
|
|
|
|
const [accessUsers, setAccessUsers] = useState([]);
|
2026-03-19 18:22:18 -03:00
|
|
|
const [selectedArea, setSelectedArea] = useState(chatContacts[0].area);
|
|
|
|
|
const [isTransferOpen, setIsTransferOpen] = useState(false);
|
2026-05-19 15:29:43 -03:00
|
|
|
const [transferArea, setTransferArea] = useState(currentUser?.areaPrincipal || 'Suporte');
|
|
|
|
|
const [transferAttendant, setTransferAttendant] = useState('');
|
2026-03-19 18:22:18 -03:00
|
|
|
const [transferNote, setTransferNote] = useState('');
|
2026-05-18 17:34:23 -03:00
|
|
|
const [isReplying] = useState(false);
|
|
|
|
|
const [isLoadingChats, setIsLoadingChats] = useState(false);
|
|
|
|
|
const [isLoadingMessages, setIsLoadingMessages] = useState(false);
|
|
|
|
|
const [apiError, setApiError] = useState(null);
|
|
|
|
|
const activeContactRef = useRef(activeContactId);
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
const activeContact = useMemo(
|
|
|
|
|
() => contacts.find((contact) => contact.id === activeContactId) || contacts[0],
|
|
|
|
|
[contacts, activeContactId],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const messages = messagesByContact[activeContactId] || [];
|
2026-05-19 15:29:43 -03:00
|
|
|
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 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);
|
|
|
|
|
const assignmentLabel = activeAssignment?.user_id
|
|
|
|
|
? `Atendimento com ${activeAssignment.user_nome || 'outro atendente'}`
|
|
|
|
|
: activeAssignment?.area_nome
|
|
|
|
|
? `Na fila de ${activeAssignment.area_nome}`
|
|
|
|
|
: 'Sem fila definida';
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-19 15:29:43 -03:00
|
|
|
setSelectedArea(activeContact?.area || 'Sem fila');
|
2026-03-19 18:22:18 -03:00
|
|
|
}, [activeContact]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-19 15:29:43 -03:00
|
|
|
setTransferAttendant(attendants[0]?.id ? String(attendants[0].id) : '');
|
|
|
|
|
}, [transferArea, accessUsers]);
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-18 17:34:23 -03:00
|
|
|
activeContactRef.current = activeContactId;
|
|
|
|
|
}, [activeContactId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let isMounted = true;
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
async function loadAccessData() {
|
2026-05-18 17:34:23 -03:00
|
|
|
try {
|
2026-05-19 15:29:43 -03:00
|
|
|
const [options, users] = await Promise.all([getAccessOptions(), getAccessUsers()]);
|
|
|
|
|
if (!isMounted) return;
|
|
|
|
|
setAreaOptions(options.areas || []);
|
|
|
|
|
setAccessUsers(users || []);
|
|
|
|
|
} catch {
|
|
|
|
|
if (isMounted) {
|
|
|
|
|
setAreaOptions([]);
|
|
|
|
|
setAccessUsers([]);
|
|
|
|
|
}
|
2026-03-19 18:22:18 -03:00
|
|
|
}
|
2026-05-18 17:34:23 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
loadAccessData();
|
2026-05-18 17:34:23 -03:00
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
2026-03-19 18:22:18 -03:00
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
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() {
|
|
|
|
|
if (whatsappStatus !== 'CONNECTED') {
|
|
|
|
|
const fallbackContacts = buildFallbackContacts();
|
|
|
|
|
setContacts(fallbackContacts);
|
|
|
|
|
setActiveContactId((current) =>
|
|
|
|
|
fallbackContacts.some((contact) => contact.id === current) ? current : fallbackContacts[0]?.id,
|
|
|
|
|
);
|
|
|
|
|
setIsLoadingChats(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guardedLoadChats();
|
|
|
|
|
const intervalId = window.setInterval(guardedLoadChats, 30000);
|
|
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
|
|
|
|
window.clearInterval(intervalId);
|
|
|
|
|
};
|
|
|
|
|
}, [currentUserId, currentUserAreas.join('|'), whatsappStatus]);
|
|
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
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,
|
2026-05-19 15:29:43 -03:00
|
|
|
[activeContactId]: dedupeMessages(
|
|
|
|
|
data.map((message) => ({
|
|
|
|
|
...normalizeMessage(message),
|
|
|
|
|
chatId: activeContactId,
|
|
|
|
|
})),
|
|
|
|
|
),
|
2026-05-18 17:34:23 -03:00
|
|
|
}));
|
|
|
|
|
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] || [];
|
2026-05-19 15:29:43 -03:00
|
|
|
const nextMessages = mergeMessageList(currentMessages, message);
|
|
|
|
|
if (nextMessages === currentMessages) return current;
|
2026-05-18 17:34:23 -03:00
|
|
|
return {
|
|
|
|
|
...current,
|
2026-05-19 15:29:43 -03:00
|
|
|
[contactId]: nextMessages,
|
2026-05-18 17:34:23 -03:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setContacts((current) => {
|
|
|
|
|
const existing = current.find((contact) => contact.id === contactId);
|
2026-05-19 15:29:43 -03:00
|
|
|
if (!existing) {
|
|
|
|
|
return current;
|
|
|
|
|
}
|
2026-05-18 17:34:23 -03:00
|
|
|
const nextContact = {
|
|
|
|
|
...(existing || {
|
|
|
|
|
id: contactId,
|
|
|
|
|
name: incomingMessage.notifyName || contactId.split('@')[0],
|
|
|
|
|
channel: 'WhatsApp',
|
|
|
|
|
status: 'online',
|
2026-05-19 15:29:43 -03:00
|
|
|
area: 'Sem fila',
|
2026-05-18 17:34:23 -03:00
|
|
|
lastSeen: 'Online agora',
|
|
|
|
|
unread: 0,
|
2026-05-19 15:29:43 -03:00
|
|
|
assignment: null,
|
2026-05-18 17:34:23 -03:00
|
|
|
}),
|
|
|
|
|
preview,
|
|
|
|
|
time: 'Agora',
|
|
|
|
|
unread:
|
|
|
|
|
incomingMessage.fromMe || contactId === activeContactRef.current
|
|
|
|
|
? 0
|
|
|
|
|
: (existing?.unread || 0) + 1,
|
|
|
|
|
};
|
|
|
|
|
return [nextContact, ...current.filter((contact) => contact.id !== contactId)];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
clearIncomingMessage();
|
2026-05-19 15:29:43 -03:00
|
|
|
window.setTimeout(loadChats, 1200);
|
2026-05-18 17:34:23 -03:00
|
|
|
}, [incomingMessage, clearIncomingMessage]);
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
function updateContact(contactId, updater) {
|
2026-03-19 18:22:18 -03:00
|
|
|
setContacts((current) =>
|
2026-05-19 15:29:43 -03:00
|
|
|
current.map((contact) => (contact.id === contactId ? updater(contact) : contact)),
|
2026-03-19 18:22:18 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
function updateContactPreview(contactId, preview, media) {
|
|
|
|
|
updateContact(contactId, (contact) => ({
|
|
|
|
|
...contact,
|
|
|
|
|
preview: media ? `[Midia: ${media.filename || 'Arquivo'}]` : preview,
|
|
|
|
|
time: 'Agora',
|
|
|
|
|
unread: 0,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
async function assumeChat() {
|
|
|
|
|
if (!activeContactId?.includes('@') || !currentUserId) return null;
|
|
|
|
|
const areaId = activeContact?.areaId || activeAssignment?.area_id || areaOptions.find((area) => currentUserAreas.includes(area.nome))?.id;
|
|
|
|
|
const response = await fetch(`${API_BASE_URL}/whatsapp/assign`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
chatId: activeContactId,
|
|
|
|
|
userId: String(currentUserId),
|
|
|
|
|
areaId,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error('Nao foi possivel assumir o atendimento.');
|
|
|
|
|
const assignment = await response.json();
|
|
|
|
|
updateContact(activeContactId, (contact) => ({
|
|
|
|
|
...contact,
|
|
|
|
|
assignment,
|
|
|
|
|
area: assignment.area_nome || contact.area,
|
|
|
|
|
areaId: assignment.area_id || contact.areaId,
|
|
|
|
|
}));
|
|
|
|
|
return assignment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
async function sendMessage() {
|
2026-03-19 18:22:18 -03:00
|
|
|
const trimmed = draft.trim();
|
2026-05-19 15:29:43 -03:00
|
|
|
if (!trimmed && !attachedFile) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!canReply) {
|
|
|
|
|
if (canAssumeChat) {
|
|
|
|
|
await assumeChat();
|
|
|
|
|
} else {
|
|
|
|
|
setApiError('Este atendimento esta atribuido a outro usuario.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setApiError(error.message);
|
2026-03-19 18:22:18 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
const media = attachedFile
|
|
|
|
|
? {
|
|
|
|
|
data: attachedFile.data,
|
|
|
|
|
mimetype: attachedFile.type,
|
|
|
|
|
filename: attachedFile.name,
|
|
|
|
|
}
|
|
|
|
|
: null;
|
2026-03-19 18:22:18 -03:00
|
|
|
const newMessage = {
|
2026-05-18 17:34:23 -03:00
|
|
|
id: `temp-${Date.now()}`,
|
|
|
|
|
chatId: activeContactId,
|
2026-03-19 18:22:18 -03:00
|
|
|
sender: 'agent',
|
|
|
|
|
text: trimmed,
|
2026-05-19 15:29:43 -03:00
|
|
|
timestamp: Math.floor(Date.now() / 1000),
|
2026-05-18 17:34:23 -03:00
|
|
|
hasMedia: Boolean(media),
|
|
|
|
|
media,
|
2026-03-19 18:22:18 -03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setMessagesByContact((current) => ({
|
|
|
|
|
...current,
|
2026-05-19 15:29:43 -03:00
|
|
|
[activeContactId]: mergeMessageList(current[activeContactId] || [], newMessage),
|
2026-03-19 18:22:18 -03:00
|
|
|
}));
|
2026-05-18 17:34:23 -03:00
|
|
|
updateContactPreview(activeContactId, trimmed || '[Midia]', media);
|
2026-03-19 18:22:18 -03:00
|
|
|
setDraft('');
|
2026-05-18 17:34:23 -03:00
|
|
|
setAttachedFile(null);
|
2026-03-19 18:22:18 -03:00
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
if (!activeContactId.includes('@')) return;
|
2026-03-19 18:22:18 -03:00
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
try {
|
|
|
|
|
await fetch(`${API_BASE_URL}/whatsapp/send`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
to: activeContactId,
|
|
|
|
|
message: trimmed,
|
2026-05-19 15:29:43 -03:00
|
|
|
senderName: getUserDisplayName(currentUser),
|
2026-05-18 17:34:23 -03:00
|
|
|
media,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
setApiError(null);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setApiError(error.message);
|
|
|
|
|
}
|
2026-03-19 18:22:18 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
async function submitTransfer() {
|
2026-03-19 18:22:18 -03:00
|
|
|
const note = transferNote.trim();
|
2026-05-19 15:29:43 -03:00
|
|
|
const areaId = selectedTransferArea?.id;
|
|
|
|
|
|
|
|
|
|
if (!areaId) {
|
|
|
|
|
setApiError('Selecione uma area valida para transferencia.');
|
|
|
|
|
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.'}`;
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
setMessagesByContact((current) => ({
|
|
|
|
|
...current,
|
|
|
|
|
[activeContactId]: [
|
|
|
|
|
...(current[activeContactId] || []),
|
|
|
|
|
{ id: Date.now() + 2, sender: 'system', text: transferMessage },
|
|
|
|
|
],
|
|
|
|
|
}));
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
updateContact(activeContactId, (contact) => ({
|
|
|
|
|
...contact,
|
|
|
|
|
area: assignment.area_nome || transferArea,
|
|
|
|
|
areaId: assignment.area_id || areaId,
|
|
|
|
|
assignment,
|
|
|
|
|
}));
|
2026-03-19 18:22:18 -03:00
|
|
|
setSelectedArea(transferArea);
|
|
|
|
|
setIsTransferOpen(false);
|
|
|
|
|
setTransferNote('');
|
2026-05-19 15:29:43 -03:00
|
|
|
setApiError(null);
|
2026-03-19 18:22:18 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
contacts,
|
|
|
|
|
activeContact,
|
|
|
|
|
activeContactId,
|
|
|
|
|
setActiveContactId,
|
|
|
|
|
messages,
|
|
|
|
|
draft,
|
|
|
|
|
setDraft,
|
2026-05-18 17:34:23 -03:00
|
|
|
attachedFile,
|
|
|
|
|
attachFile,
|
|
|
|
|
removeAttachedFile,
|
2026-03-19 18:22:18 -03:00
|
|
|
sendMessage,
|
2026-05-18 17:34:23 -03:00
|
|
|
hydrateMessageMedia,
|
2026-05-19 15:29:43 -03:00
|
|
|
assumeChat,
|
|
|
|
|
releaseChat,
|
|
|
|
|
canAssumeChat,
|
|
|
|
|
canReply,
|
|
|
|
|
assignmentLabel,
|
|
|
|
|
isAssignedToCurrentUser,
|
|
|
|
|
activeAssignment,
|
2026-03-19 18:22:18 -03:00
|
|
|
isReplying,
|
2026-05-18 17:34:23 -03:00
|
|
|
isLoadingChats,
|
|
|
|
|
isLoadingMessages,
|
|
|
|
|
apiError,
|
2026-03-19 18:22:18 -03:00
|
|
|
selectedArea,
|
|
|
|
|
setSelectedArea,
|
|
|
|
|
isTransferOpen,
|
|
|
|
|
setIsTransferOpen,
|
|
|
|
|
transferArea,
|
|
|
|
|
setTransferArea,
|
|
|
|
|
transferAreas,
|
|
|
|
|
attendants,
|
2026-05-19 15:29:43 -03:00
|
|
|
isSameUserArea,
|
2026-03-19 18:22:18 -03:00
|
|
|
transferAttendant,
|
|
|
|
|
setTransferAttendant,
|
|
|
|
|
transferNote,
|
|
|
|
|
setTransferNote,
|
|
|
|
|
submitTransfer,
|
|
|
|
|
};
|
|
|
|
|
}
|