Compare commits

..

No commits in common. "b605a4c50796d7f8a642e415634f2a0e33988a47" and "217d566057941065d6e29fe4f153ac0e889be32c" have entirely different histories.

11 changed files with 151 additions and 697 deletions

View File

@ -23,14 +23,17 @@ function ChannelBadge({ channel }) {
); );
} }
function LastMessageDot({ fromMe }) { function PresenceDot({ status }) {
const color = fromMe ? '#e5a22a' : '#00a4b7'; const color =
const label = fromMe ? 'Última mensagem enviada pelo atendimento' : 'Última mensagem enviada pelo cliente'; status === 'online'
? '#16a34a'
: status === 'away'
? '#e5a22a'
: '#dc2626';
return ( return (
<span <span
title={label} aria-hidden="true"
aria-label={label}
style={{ style={{
width: 10, width: 10,
height: 10, height: 10,
@ -67,32 +70,12 @@ function UnreadBadge({ count }) {
); );
} }
function SavedContactLabel({ contact }) { const CHAT_LIST_HEIGHT = 'min(640px, calc(100vh - 220px))';
const profile = contact.contactProfile;
const hasSavedContact = Boolean(profile?.created_at || profile?.name || profile?.company || profile?.note);
if (!hasSavedContact) return null;
return (
<span
title="Contato salvo na agenda"
style={{
color: '#b7791f',
flex: '0 0 auto',
fontSize: '0.72rem',
fontWeight: 800,
lineHeight: 1,
}}
>
Salvo
</span>
);
}
export function ChatConversationList({ export function ChatConversationList({
contacts, contacts,
activeContactId, activeContactId,
onSelectContact, onSelectContact,
onOpenContact,
isMobile = false, isMobile = false,
}) { }) {
return ( return (
@ -105,16 +88,16 @@ export function ChatConversationList({
display: 'grid', display: 'grid',
gridTemplateRows: 'auto minmax(0, 1fr)', gridTemplateRows: 'auto minmax(0, 1fr)',
gap: '0.85rem', gap: '0.85rem',
height: isMobile ? 'auto' : '100%', height: isMobile ? 'auto' : CHAT_LIST_HEIGHT,
maxHeight: isMobile ? 'none' : '100%', maxHeight: isMobile ? 'none' : CHAT_LIST_HEIGHT,
alignSelf: isMobile ? 'start' : 'stretch', alignSelf: 'start',
minHeight: 0, minHeight: 0,
}} }}
> >
<div> <div>
<strong style={{ display: 'block', fontSize: '1.08rem' }}>Conversas ativas</strong> <strong style={{ display: 'block', fontSize: '1.08rem' }}>Conversas ativas</strong>
<span style={{ color: 'var(--color-text-soft)' }}> <span style={{ color: 'var(--color-text-soft)' }}>
WhatsApp, SMS e e-mail em uma fila visual. WhatsApp, SMS e email em uma fila visual.
</span> </span>
</div> </div>
@ -138,11 +121,6 @@ export function ChatConversationList({
key={contact.id} key={contact.id}
type="button" type="button"
onClick={() => onSelectContact(contact.id)} onClick={() => onSelectContact(contact.id)}
onContextMenu={(event) => {
event.preventDefault();
onSelectContact(contact.id);
onOpenContact?.(contact);
}}
style={{ style={{
border: '1px solid', border: '1px solid',
borderColor: isActive ? 'rgba(0, 164, 183, 0.26)' : 'var(--color-border)', borderColor: isActive ? 'rgba(0, 164, 183, 0.26)' : 'var(--color-border)',
@ -156,7 +134,7 @@ export function ChatConversationList({
> >
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}> <div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', minWidth: 0 }}> <span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', minWidth: 0 }}>
<LastMessageDot fromMe={contact.lastMessageFromMe} /> <PresenceDot status={contact.status} />
<strong style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <strong style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{contact.name} {contact.name}
</strong> </strong>
@ -166,10 +144,7 @@ export function ChatConversationList({
</span> </span>
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}> <div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', minWidth: 0 }}>
<ChannelBadge channel={contact.channel} /> <ChannelBadge channel={contact.channel} />
<SavedContactLabel contact={contact} />
</span>
<UnreadBadge count={contact.unread} /> <UnreadBadge count={contact.unread} />
</div> </div>
<span style={{ color: 'var(--color-text-soft)' }}>{contact.preview}</span> <span style={{ color: 'var(--color-text-soft)' }}>{contact.preview}</span>

View File

@ -22,13 +22,6 @@ function parseMessageText(text) {
}; };
} }
function formatMessageTime(timestamp) {
if (!timestamp) return '';
const numericTimestamp = Number(timestamp);
const date = new Date(numericTimestamp > 1000000000000 ? numericTimestamp : numericTimestamp * 1000);
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}
function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) { function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) {
const mediaUrl = useMemo(() => getMediaUrl(message.media), [message.media]); const mediaUrl = useMemo(() => getMediaUrl(message.media), [message.media]);
const mimetype = message.media?.mimetype || ''; const mimetype = message.media?.mimetype || '';
@ -203,14 +196,19 @@ function AttachmentPreview({ file, onRemove }) {
); );
} }
function ContactActivity({ contact }) { function ContactPresence({ contact }) {
if (!contact) { if (!contact) {
return null; return null;
} }
const status = contact.status || 'offline'; const status = contact.status || 'offline';
const color = status === 'away' ? '#e5a22a' : '#dc2626'; const color =
const label = contact.lastSeen || 'Sem atividade recente'; status === 'online'
? '#16a34a'
: status === 'away'
? '#e5a22a'
: '#dc2626';
const label = status === 'online' ? 'Online agora' : contact.lastSeen || 'Offline';
return ( return (
<span <span
@ -254,7 +252,6 @@ export function ChatWindow({
canAssumeChat = false, canAssumeChat = false,
canReply = true, canReply = true,
assignmentLabel, assignmentLabel,
transferNote,
isReplying, isReplying,
isMobile = false, isMobile = false,
}) { }) {
@ -304,7 +301,7 @@ export function ChatWindow({
> >
<div> <div>
<strong style={{ display: 'block', fontSize: '1.15rem' }}>{safeContact.name}</strong> <strong style={{ display: 'block', fontSize: '1.15rem' }}>{safeContact.name}</strong>
<ContactActivity contact={safeContact} /> <ContactPresence contact={safeContact} />
</div> </div>
<div <div
@ -335,7 +332,7 @@ export function ChatWindow({
{canAssumeChat ? ( {canAssumeChat ? (
<button <button
type="button" type="button"
onClick={() => onAssumeChat?.()} onClick={onAssumeChat}
style={{ style={{
border: 'none', border: 'none',
borderRadius: '14px', borderRadius: '14px',
@ -381,24 +378,6 @@ export function ChatWindow({
Transferir Transferir
</button> </button>
</div> </div>
{transferNote ? (
<div
style={{
gridColumn: '1 / -1',
border: '1px solid rgba(0, 164, 183, 0.24)',
borderRadius: 16,
padding: '0.85rem 1rem',
background: 'rgba(0, 164, 183, 0.07)',
color: 'var(--color-text)',
lineHeight: 1.45,
}}
>
<strong style={{ display: 'block', color: 'var(--color-primary)', marginBottom: '0.25rem' }}>
Observacao da transferencia
</strong>
{transferNote}
</div>
) : null}
</header> </header>
<div <div
@ -418,7 +397,6 @@ export function ChatWindow({
const isAgent = message.sender === 'agent'; const isAgent = message.sender === 'agent';
const isSystem = message.sender === 'system'; const isSystem = message.sender === 'system';
const parsedText = parseMessageText(message.text); const parsedText = parseMessageText(message.text);
const messageTime = formatMessageTime(message.timestamp);
if (isSystem) { if (isSystem) {
return ( return (
@ -486,18 +464,6 @@ export function ChatWindow({
{parsedText.body} {parsedText.body}
</span> </span>
) : null} ) : null}
{messageTime ? (
<span
style={{
justifySelf: 'end',
fontSize: '0.72rem',
lineHeight: 1,
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
}}
>
{messageTime}
</span>
) : null}
</div> </div>
); );
})} })}
@ -554,16 +520,9 @@ export function ChatWindow({
fontWeight: 700, fontWeight: 700,
}} }}
> >
<span style={{ display: 'block' }}>
{canAssumeChat {canAssumeChat
? 'Este atendimento esta na fila. Assuma para responder ou transferir.' ? 'Este atendimento esta na fila. Assuma para responder, ou envie uma mensagem para assumir automaticamente.'
: assignmentLabel || 'Este atendimento esta atribuido a outro usuario.'} : assignmentLabel || 'Este atendimento esta atribuido a outro usuario.'}
</span>
{transferNote ? (
<span style={{ display: 'block', marginTop: '0.45rem', color: 'var(--color-text)' }}>
Obs: {transferNote}
</span>
) : null}
</div> </div>
) : null} ) : null}
<div <div
@ -609,14 +568,12 @@ export function ChatWindow({
onSend(); onSend();
} }
}} }}
disabled={!safeContact.id || !canReply} disabled={!safeContact.id || (!canReply && !canAssumeChat)}
placeholder={ placeholder={
!safeContact.id !safeContact.id
? 'Aguardando conversa entrar em uma fila' ? 'Aguardando conversa entrar em uma fila'
: canReply : canReply || canAssumeChat
? 'Escreva sua mensagem...' ? 'Escreva sua mensagem...'
: canAssumeChat
? 'Assuma o atendimento para responder'
: 'Atendimento bloqueado para resposta' : 'Atendimento bloqueado para resposta'
} }
style={{ style={{
@ -626,13 +583,13 @@ export function ChatWindow({
background: '#fff', background: '#fff',
outline: 'none', outline: 'none',
minWidth: 0, minWidth: 0,
opacity: safeContact.id && canReply ? 1 : 0.6, opacity: safeContact.id && (canReply || canAssumeChat) ? 1 : 0.6,
}} }}
/> />
<button <button
type="button" type="button"
onClick={onSend} onClick={onSend}
disabled={!safeContact.id || !canReply} disabled={!safeContact.id || (!canReply && !canAssumeChat)}
style={{ style={{
border: 'none', border: 'none',
borderRadius: '18px', borderRadius: '18px',
@ -641,7 +598,7 @@ export function ChatWindow({
color: '#fff', color: '#fff',
fontWeight: 700, fontWeight: 700,
gridColumn: isMobile ? '1 / -1' : 'auto', gridColumn: isMobile ? '1 / -1' : 'auto',
opacity: safeContact.id && canReply ? 1 : 0.6, opacity: safeContact.id && (canReply || canAssumeChat) ? 1 : 0.6,
}} }}
> >
Enviar Enviar

View File

@ -1,197 +0,0 @@
import { useEffect, useState } from 'react';
import { getCurrentUser } from '../../auth/services/sessionService';
import { getContactProfile, saveContactProfile } from '../services/contactProfileService';
function getUserId(user) {
const value = user?.databaseId || user?.id;
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
function formatPhone(phone) {
const digits = String(phone || '').replace(/\D/g, '');
if (!digits) return 'Telefone nao disponivel';
if (digits.startsWith('55') && digits.length === 13) {
return `+55 (${digits.slice(2, 4)}) ${digits.slice(4, 9)}-${digits.slice(9)}`;
}
if (digits.startsWith('55') && digits.length === 12) {
return `+55 (${digits.slice(2, 4)}) ${digits.slice(4, 8)}-${digits.slice(8)}`;
}
if (digits.length === 11) {
return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`;
}
if (digits.length === 10) {
return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6)}`;
}
return phone;
}
export function ContactProfilePanel({ isOpen, contact, onClose, onSaved }) {
const [profile, setProfile] = useState(null);
const [form, setForm] = useState({ name: '', company: '', note: '' });
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
let isMounted = true;
async function loadProfile() {
if (!isOpen || !contact?.id) return;
try {
const data = await getContactProfile(contact.id);
if (!isMounted) return;
setProfile(data);
setForm({
name: data.name || contact.name || '',
company: data.company || '',
note: data.note || '',
});
setError('');
} catch (err) {
if (isMounted) setError(err.message);
}
}
loadProfile();
return () => {
isMounted = false;
};
}, [isOpen, contact?.id]);
if (!isOpen) {
return null;
}
const fieldStyle = {
width: '100%',
border: '1px solid var(--color-border)',
borderRadius: '16px',
padding: '0.9rem 1rem',
background: '#fff',
outline: 'none',
};
async function submit() {
if (!contact?.id) return;
setIsSaving(true);
try {
const userId = getUserId(getCurrentUser());
const saved = await saveContactProfile(contact.id, {
phone: profile?.phone || contact?.contactProfile?.phone || '',
name: form.name,
company: form.company,
note: form.note,
userId,
});
setProfile(saved);
onSaved?.(contact.id, saved);
setError('');
} catch (err) {
setError(err.message);
} finally {
setIsSaving(false);
}
}
return (
<aside
style={{
background: '#fff',
border: '1px solid var(--color-border)',
borderRadius: '28px',
padding: '1.25rem',
display: 'grid',
gap: '1rem',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
<div>
<strong style={{ display: 'block', fontSize: '1.06rem' }}>Contato do cliente</strong>
<span style={{ color: 'var(--color-text-soft)' }}>
Atualize os dados de agenda deste atendimento.
</span>
</div>
<button
type="button"
onClick={onClose}
style={{
border: 'none',
background: 'transparent',
color: 'var(--color-text-soft)',
fontWeight: 700,
}}
>
Fechar
</button>
</div>
<label style={{ display: 'grid', gap: '0.45rem' }}>
<span style={{ fontWeight: 600 }}>Nome</span>
<input
value={form.name}
onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))}
style={fieldStyle}
/>
</label>
<label style={{ display: 'grid', gap: '0.45rem' }}>
<span style={{ fontWeight: 600 }}>Empresa</span>
<input
value={form.company}
onChange={(event) => setForm((current) => ({ ...current, company: event.target.value }))}
placeholder="Empresa ou conta vinculada"
style={fieldStyle}
/>
</label>
<label style={{ display: 'grid', gap: '0.45rem' }}>
<span style={{ fontWeight: 600 }}>Telefone</span>
<input
value={formatPhone(profile?.phone || contact?.contactProfile?.phone)}
disabled
style={{
...fieldStyle,
background: 'rgba(0, 49, 80, 0.04)',
color: 'var(--color-text-soft)',
fontWeight: 700,
}}
/>
</label>
<label style={{ display: 'grid', gap: '0.45rem' }}>
<span style={{ fontWeight: 600 }}>Observacao</span>
<textarea
rows={5}
value={form.note}
onChange={(event) => setForm((current) => ({ ...current, note: event.target.value }))}
placeholder="Informacoes relevantes do cliente."
style={{ ...fieldStyle, resize: 'vertical' }}
/>
</label>
{error ? <span style={{ color: '#b42318', fontWeight: 700 }}>{error}</span> : null}
<button
type="button"
onClick={submit}
disabled={isSaving}
style={{
border: 'none',
borderRadius: '16px',
padding: '0.95rem 1rem',
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
color: '#fff',
fontWeight: 700,
opacity: isSaving ? 0.65 : 1,
}}
>
{isSaving ? 'Salvando...' : 'Salvar contato'}
</button>
</aside>
);
}

View File

@ -12,12 +12,6 @@ function buildInitialMessages() {
}, {}); }, {});
} }
function getLastMessageFromMe(messages = []) {
const lastMessage = [...messages].reverse().find(isDisplayableMessage);
if (!lastMessage) return false;
return lastMessage.sender === 'agent' || lastMessage.fromMe === true;
}
function getSerializedId(value) { function getSerializedId(value) {
if (!value) return ''; if (!value) return '';
if (typeof value === 'string') return value; if (typeof value === 'string') return value;
@ -44,23 +38,25 @@ function getPreviewFromMessage(message) {
function normalizeChat(chat) { function normalizeChat(chat) {
const id = getSerializedId(chat.id); const id = getSerializedId(chat.id);
const lastActivitySeconds = chat.timestamp ? Math.floor(Date.now() / 1000) - chat.timestamp : null;
const isRecentlyActive = lastActivitySeconds !== null && lastActivitySeconds < 300;
const assignment = chat.assignment || null; const assignment = chat.assignment || null;
const lastSeenTimestamp = chat.timestamp || null;
const hasLastMessageFromMe = typeof chat.lastMessageFromMe === 'boolean';
return { return {
id, id,
name: getContactName(chat), name: getContactName(chat),
channel: 'WhatsApp', channel: 'WhatsApp',
status: lastSeenTimestamp ? 'away' : 'offline', status: isRecentlyActive ? 'online' : 'away',
area: assignment?.area_nome || (assignment?.area_id ? String(assignment.area_id) : 'Sem fila'), area: assignment?.area_nome || (assignment?.area_id ? String(assignment.area_id) : 'Sem fila'),
areaId: assignment?.area_id || null, areaId: assignment?.area_id || null,
lastSeen: lastSeenTimestamp ? `Última atividade as ${formatTime(lastSeenTimestamp)}` : 'Sem atividade recente', lastSeen: isRecentlyActive
? 'Online agora'
: chat.timestamp
? `Visto as ${formatTime(chat.timestamp)}`
: 'Sem atividade recente',
preview: chat.preview || chat.lastMessage?.body || '', preview: chat.preview || chat.lastMessage?.body || '',
time: formatTime(chat.timestamp) || 'Agora', time: formatTime(chat.timestamp) || 'Agora',
unread: chat.unreadCount || 0, unread: chat.unreadCount || 0,
lastMessageFromMe: hasLastMessageFromMe ? chat.lastMessageFromMe : Boolean(chat.lastMessage?.fromMe),
contactProfile: chat.contactProfile || null,
assignment, assignment,
}; };
} }
@ -81,11 +77,6 @@ function normalizeMessage(message) {
}; };
} }
function isDisplayableMessage(message) {
const text = String(message?.text ?? message?.body ?? '').trim();
return Boolean(text || message?.hasMedia || message?.media);
}
function getComparableMessageTime(message) { function getComparableMessageTime(message) {
if (message.timestamp) return Number(message.timestamp); if (message.timestamp) return Number(message.timestamp);
if (typeof message.id === 'string' && message.id.startsWith('temp-')) { if (typeof message.id === 'string' && message.id.startsWith('temp-')) {
@ -146,12 +137,7 @@ function fileToBase64(file) {
} }
function buildFallbackContacts() { function buildFallbackContacts() {
return chatContacts.map((contact) => ({ return chatContacts.map((contact) => ({ ...contact, assignment: null, areaId: null }));
...contact,
assignment: null,
areaId: null,
lastMessageFromMe: getLastMessageFromMe(contact.messages),
}));
} }
function getUserId(user) { function getUserId(user) {
@ -161,15 +147,9 @@ function getUserId(user) {
} }
function getUserAreas(user) { function getUserAreas(user) {
const normalizeArea = (area) => { const areas = Array.isArray(user?.areas) ? user.areas : [];
if (!area) return null; if (user?.areaPrincipal && !areas.includes(user.areaPrincipal)) {
if (typeof area === 'string') return area; return [user.areaPrincipal, ...areas];
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; return areas;
} }
@ -192,7 +172,7 @@ export function useChat() {
const [accessUsers, setAccessUsers] = useState([]); const [accessUsers, setAccessUsers] = useState([]);
const [selectedArea, setSelectedArea] = useState(chatContacts[0].area); const [selectedArea, setSelectedArea] = useState(chatContacts[0].area);
const [isTransferOpen, setIsTransferOpen] = useState(false); const [isTransferOpen, setIsTransferOpen] = useState(false);
const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte'); const [transferArea, setTransferArea] = useState(currentUser?.areaPrincipal || 'Suporte');
const [transferAttendant, setTransferAttendant] = useState(''); const [transferAttendant, setTransferAttendant] = useState('');
const [transferNote, setTransferNote] = useState(''); const [transferNote, setTransferNote] = useState('');
const [isReplying] = useState(false); const [isReplying] = useState(false);
@ -202,15 +182,8 @@ export function useChat() {
const activeContactRef = useRef(activeContactId); const activeContactRef = useRef(activeContactId);
const activeContact = useMemo( const activeContact = useMemo(
() => { () => contacts.find((contact) => contact.id === activeContactId) || contacts[0],
const contact = contacts.find((item) => item.id === activeContactId) || contacts[0]; [contacts, activeContactId],
if (!contact || typeof contact.lastMessageFromMe === 'boolean') return contact;
return {
...contact,
lastMessageFromMe: getLastMessageFromMe(messagesByContact[contact.id] || []),
};
},
[contacts, activeContactId, messagesByContact],
); );
const messages = messagesByContact[activeContactId] || []; const messages = messagesByContact[activeContactId] || [];
@ -236,7 +209,6 @@ export function useChat() {
: activeAssignment?.area_nome : activeAssignment?.area_nome
? `Na fila de ${activeAssignment.area_nome}` ? `Na fila de ${activeAssignment.area_nome}`
: 'Sem fila definida'; : 'Sem fila definida';
const transferNoteLabel = activeAssignment?.transfer_note || '';
useEffect(() => { useEffect(() => {
setSelectedArea(activeContact?.area || 'Sem fila'); setSelectedArea(activeContact?.area || 'Sem fila');
@ -341,21 +313,14 @@ export function useChat() {
if (!response.ok) throw new Error('Falha ao carregar mensagens do WhatsApp.'); if (!response.ok) throw new Error('Falha ao carregar mensagens do WhatsApp.');
const data = await response.json(); const data = await response.json();
if (!isMounted || !Array.isArray(data)) return; if (!isMounted || !Array.isArray(data)) return;
const normalizedMessages = dedupeMessages(
data
.map((message) => ({
...normalizeMessage(message),
chatId: activeContactId,
}))
.filter(isDisplayableMessage),
);
setMessagesByContact((current) => ({ setMessagesByContact((current) => ({
...current, ...current,
[activeContactId]: normalizedMessages, [activeContactId]: dedupeMessages(
})); data.map((message) => ({
updateContact(activeContactId, (contact) => ({ ...normalizeMessage(message),
...contact, chatId: activeContactId,
lastMessageFromMe: getLastMessageFromMe(normalizedMessages), })),
),
})); }));
setApiError(null); setApiError(null);
} catch (error) { } catch (error) {
@ -380,10 +345,6 @@ export function useChat() {
...normalizeMessage(incomingMessage), ...normalizeMessage(incomingMessage),
chatId: contactId, chatId: contactId,
}; };
if (!isDisplayableMessage(message)) {
clearIncomingMessage();
return;
}
const preview = getPreviewFromMessage(message); const preview = getPreviewFromMessage(message);
setMessagesByContact((current) => { setMessagesByContact((current) => {
@ -406,17 +367,14 @@ export function useChat() {
id: contactId, id: contactId,
name: incomingMessage.notifyName || contactId.split('@')[0], name: incomingMessage.notifyName || contactId.split('@')[0],
channel: 'WhatsApp', channel: 'WhatsApp',
status: 'away', status: 'online',
area: 'Sem fila', area: 'Sem fila',
lastSeen: 'Visto agora', lastSeen: 'Online agora',
unread: 0, unread: 0,
assignment: null, assignment: null,
}), }),
preview, preview,
time: 'Agora', time: 'Agora',
status: 'away',
lastSeen: 'Última atividade agora',
lastMessageFromMe: Boolean(incomingMessage.fromMe),
unread: unread:
incomingMessage.fromMe || contactId === activeContactRef.current incomingMessage.fromMe || contactId === activeContactRef.current
? 0 ? 0
@ -441,15 +399,6 @@ export function useChat() {
preview: media ? `[Midia: ${media.filename || 'Arquivo'}]` : preview, preview: media ? `[Midia: ${media.filename || 'Arquivo'}]` : preview,
time: 'Agora', time: 'Agora',
unread: 0, unread: 0,
lastMessageFromMe: true,
}));
}
function updateContactProfile(contactId, profile) {
updateContact(contactId, (contact) => ({
...contact,
name: profile.name || contact.name,
contactProfile: profile,
})); }));
} }
@ -501,17 +450,14 @@ export function useChat() {
} }
} }
async function assumeChat(contactId = activeContactId) { async function assumeChat() {
if (!contactId?.includes('@') || !currentUserId) return null; if (!activeContactId?.includes('@') || !currentUserId) return null;
const targetContact = contacts.find((contact) => contact.id === contactId) || activeContact; const areaId = activeContact?.areaId || activeAssignment?.area_id || areaOptions.find((area) => currentUserAreas.includes(area.nome))?.id;
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`, { const response = await fetch(`${API_BASE_URL}/whatsapp/assign`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
chatId: contactId, chatId: activeContactId,
userId: String(currentUserId), userId: String(currentUserId),
areaId, areaId,
}), }),
@ -519,18 +465,13 @@ export function useChat() {
if (!response.ok) throw new Error('Nao foi possivel assumir o atendimento.'); if (!response.ok) throw new Error('Nao foi possivel assumir o atendimento.');
const assignment = await response.json(); const assignment = await response.json();
updateContact(contactId, (contact) => ({ updateContact(activeContactId, (contact) => ({
...contact, ...contact,
assignment, assignment,
area: assignment.area_nome || contact.area, area: assignment.area_nome || contact.area,
areaId: assignment.area_id || contact.areaId, areaId: assignment.area_id || contact.areaId,
})); }));
setApiError(null);
return assignment; return assignment;
} catch (error) {
setApiError(error.message);
return null;
}
} }
async function releaseChat() { async function releaseChat() {
@ -549,20 +490,19 @@ export function useChat() {
})); }));
} }
async function sendMessage(messageText = draft, contactId = activeContactId) { async function sendMessage() {
const trimmed = String(messageText || '').trim(); const trimmed = draft.trim();
if (!trimmed && !attachedFile) return; 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,
);
try { try {
if (!targetIsAssignedToCurrentUser) { if (!canReply) {
setApiError('Assuma o atendimento antes de responder.'); if (canAssumeChat) {
await assumeChat();
} else {
setApiError('Este atendimento esta atribuido a outro usuario.');
return; return;
} }
}
} catch (error) { } catch (error) {
setApiError(error.message); setApiError(error.message);
return; return;
@ -577,7 +517,7 @@ export function useChat() {
: null; : null;
const newMessage = { const newMessage = {
id: `temp-${Date.now()}`, id: `temp-${Date.now()}`,
chatId: contactId, chatId: activeContactId,
sender: 'agent', sender: 'agent',
text: trimmed, text: trimmed,
timestamp: Math.floor(Date.now() / 1000), timestamp: Math.floor(Date.now() / 1000),
@ -587,34 +527,26 @@ export function useChat() {
setMessagesByContact((current) => ({ setMessagesByContact((current) => ({
...current, ...current,
[contactId]: mergeMessageList(current[contactId] || [], newMessage), [activeContactId]: mergeMessageList(current[activeContactId] || [], newMessage),
})); }));
updateContactPreview(contactId, trimmed || '[Midia]', media); updateContactPreview(activeContactId, trimmed || '[Midia]', media);
if (contactId === activeContactId) {
setDraft(''); setDraft('');
}
setAttachedFile(null); setAttachedFile(null);
if (!contactId.includes('@')) return; if (!activeContactId.includes('@')) return;
try { try {
await fetch(`${API_BASE_URL}/whatsapp/send`, { await fetch(`${API_BASE_URL}/whatsapp/send`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
to: contactId, to: activeContactId,
message: trimmed, message: trimmed,
senderName: getUserDisplayName(currentUser), senderName: getUserDisplayName(currentUser),
media, media,
}), }),
}); });
setApiError(null); setApiError(null);
updateContact(contactId, (contact) => ({
...contact,
assignment: contact.assignment
? { ...contact.assignment, transfer_note: null }
: contact.assignment,
}));
} catch (error) { } catch (error) {
setApiError(error.message); setApiError(error.message);
} }
@ -629,11 +561,6 @@ export function useChat() {
return; return;
} }
if (!isAssignedToCurrentUser) {
setApiError('Assuma o atendimento antes de transferir.');
return;
}
const targetUserId = isSameUserArea && transferAttendant ? Number(transferAttendant) : null; const targetUserId = isSameUserArea && transferAttendant ? Number(transferAttendant) : null;
const response = await fetch(`${API_BASE_URL}/whatsapp/transfer`, { const response = await fetch(`${API_BASE_URL}/whatsapp/transfer`, {
method: 'POST', method: 'POST',
@ -694,10 +621,8 @@ export function useChat() {
canAssumeChat, canAssumeChat,
canReply, canReply,
assignmentLabel, assignmentLabel,
transferNoteLabel,
isAssignedToCurrentUser, isAssignedToCurrentUser,
activeAssignment, activeAssignment,
updateContactProfile,
isReplying, isReplying,
isLoadingChats, isLoadingChats,
isLoadingMessages, isLoadingMessages,

View File

@ -1,16 +1,13 @@
import { Link, useSearchParams } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { BrandMark } from '../../../shared/components/BrandMark'; import { BrandMark } from '../../../shared/components/BrandMark';
import { useViewport } from '../../../shared/hooks/useViewport'; import { useViewport } from '../../../shared/hooks/useViewport';
import { ChatConversationList } from '../components/ChatConversationList'; import { ChatConversationList } from '../components/ChatConversationList';
import { ChatTransferPanel } from '../components/ChatTransferPanel'; import { ChatTransferPanel } from '../components/ChatTransferPanel';
import { ContactProfilePanel } from '../components/ContactProfilePanel';
import { ChatWindow } from '../components/ChatWindow'; import { ChatWindow } from '../components/ChatWindow';
import { useChat } from '../hooks/useChat'; import { useChat } from '../hooks/useChat';
import { quickReplies } from '../services/chatMocks'; import { quickReplies } from '../services/chatMocks';
export function ChatPage() { export function ChatPage() {
const [searchParams] = useSearchParams();
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport(); const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
const { const {
contacts, contacts,
@ -30,8 +27,6 @@ export function ChatPage() {
canAssumeChat, canAssumeChat,
canReply, canReply,
assignmentLabel, assignmentLabel,
transferNoteLabel,
updateContactProfile,
isReplying, isReplying,
selectedArea, selectedArea,
setSelectedArea, setSelectedArea,
@ -48,14 +43,6 @@ export function ChatPage() {
setTransferNote, setTransferNote,
submitTransfer, submitTransfer,
} = useChat(); } = useChat();
const requestedChatId = searchParams.get('chatId');
const [isContactPanelOpen, setIsContactPanelOpen] = useState(false);
useEffect(() => {
if (!requestedChatId) return;
if (!contacts.some((contact) => contact.id === requestedChatId)) return;
setActiveContactId(requestedChatId);
}, [requestedChatId, contacts, setActiveContactId]);
const gridTemplateColumns = isMobile const gridTemplateColumns = isMobile
? '1fr' ? '1fr'
@ -129,10 +116,6 @@ export function ChatPage() {
contacts={contacts} contacts={contacts}
activeContactId={activeContactId} activeContactId={activeContactId}
onSelectContact={setActiveContactId} onSelectContact={setActiveContactId}
onOpenContact={() => {
setIsTransferOpen(false);
setIsContactPanelOpen(true);
}}
isMobile={isMobile} isMobile={isMobile}
/> />
@ -149,16 +132,12 @@ export function ChatPage() {
onRemoveAttachedFile={removeAttachedFile} onRemoveAttachedFile={removeAttachedFile}
onLoadMedia={hydrateMessageMedia} onLoadMedia={hydrateMessageMedia}
onSend={sendMessage} onSend={sendMessage}
onToggleTransfer={() => { onToggleTransfer={() => setIsTransferOpen((current) => !current)}
setIsContactPanelOpen(false);
setIsTransferOpen((current) => !current);
}}
onAssumeChat={assumeChat} onAssumeChat={assumeChat}
onReleaseChat={releaseChat} onReleaseChat={releaseChat}
canAssumeChat={canAssumeChat} canAssumeChat={canAssumeChat}
canReply={canReply} canReply={canReply}
assignmentLabel={assignmentLabel} assignmentLabel={assignmentLabel}
transferNote={transferNoteLabel}
isReplying={isReplying} isReplying={isReplying}
isMobile={isMobile} isMobile={isMobile}
/> />
@ -192,7 +171,6 @@ export function ChatPage() {
</div> </div>
{isWideDesktop ? ( {isWideDesktop ? (
<>
<ChatTransferPanel <ChatTransferPanel
isOpen={isTransferOpen} isOpen={isTransferOpen}
transferArea={transferArea} transferArea={transferArea}
@ -207,18 +185,10 @@ export function ChatPage() {
onSubmit={submitTransfer} onSubmit={submitTransfer}
onClose={() => setIsTransferOpen(false)} onClose={() => setIsTransferOpen(false)}
/> />
<ContactProfilePanel
isOpen={isContactPanelOpen}
contact={activeContact}
onClose={() => setIsContactPanelOpen(false)}
onSaved={updateContactProfile}
/>
</>
) : null} ) : null}
</section> </section>
{!isWideDesktop ? ( {!isWideDesktop ? (
<>
<ChatTransferPanel <ChatTransferPanel
isOpen={isTransferOpen} isOpen={isTransferOpen}
transferArea={transferArea} transferArea={transferArea}
@ -233,13 +203,6 @@ export function ChatPage() {
onSubmit={submitTransfer} onSubmit={submitTransfer}
onClose={() => setIsTransferOpen(false)} onClose={() => setIsTransferOpen(false)}
/> />
<ContactProfilePanel
isOpen={isContactPanelOpen}
contact={activeContact}
onClose={() => setIsContactPanelOpen(false)}
onSaved={updateContactProfile}
/>
</>
) : null} ) : null}
</section> </section>
</main> </main>

View File

@ -3,9 +3,9 @@ export const chatContacts = [
id: 'maria-souza', id: 'maria-souza',
name: 'Maria Souza', name: 'Maria Souza',
channel: 'WhatsApp', channel: 'WhatsApp',
status: 'away', status: 'online',
area: 'Suporte', area: 'Suporte',
lastSeen: 'Ultima atividade as 09:42', lastSeen: 'Online agora',
preview: 'Preciso atualizar o cadastro do meu pedido.', preview: 'Preciso atualizar o cadastro do meu pedido.',
time: '09:42', time: '09:42',
unread: 2, unread: 2,
@ -22,7 +22,7 @@ export const chatContacts = [
channel: 'SMS', channel: 'SMS',
status: 'offline', status: 'offline',
area: 'Financeiro', area: 'Financeiro',
lastSeen: 'Ultima atividade as 08:15', lastSeen: 'Visto ha 12 min',
preview: 'Pode me ligar em 10 minutos?', preview: 'Pode me ligar em 10 minutos?',
time: '08:15', time: '08:15',
unread: 1, unread: 1,

View File

@ -1,17 +0,0 @@
import { API_BASE_URL } from '../../../shared/services/apiConfig';
export async function getContactProfile(chatId) {
const response = await fetch(`${API_BASE_URL}/contacts/${encodeURIComponent(chatId)}`);
if (!response.ok) throw new Error('Falha ao carregar contato.');
return response.json();
}
export async function saveContactProfile(chatId, payload) {
const response = await fetch(`${API_BASE_URL}/contacts/${encodeURIComponent(chatId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error('Falha ao salvar contato.');
return response.json();
}

View File

@ -1,7 +1,5 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { createAgentNote, deleteAgentNote, listAgentNotes } from '../services/agentNotesService';
import { getCurrentUser } from '../../auth/services/sessionService';
const WORKSPACE_HEIGHT = 660; const WORKSPACE_HEIGHT = 660;
@ -102,46 +100,16 @@ function buildSuggestedReplies(conversation) {
]; ];
} }
function parseMessageText(text) {
const rawText = String(text || '');
const match = rawText.match(/^\*(Atendente(?: virtual)?:\s*[^*]+)\*\s*\n+/i);
if (!match) {
return { senderLabel: null, body: rawText };
}
return {
senderLabel: match[1],
body: rawText.slice(match[0].length),
};
}
function formatMessageTime(timestamp) {
if (!timestamp) return '';
const numericTimestamp = Number(timestamp);
const date = new Date(numericTimestamp > 1000000000000 ? numericTimestamp : numericTimestamp * 1000);
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}
function getUserId(user) {
const value = user?.databaseId || user?.id;
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
export function MessagesWorkspace({ export function MessagesWorkspace({
conversations, conversations,
activeConversationId, activeConversationId,
onSelectConversation, onSelectConversation,
onSendSuggestedReply,
isWideDesktop = false, isWideDesktop = false,
isDesktop = false, isDesktop = false,
isTablet = false, isTablet = false,
isMobile = false, isMobile = false,
}) { }) {
const navigate = useNavigate(); const navigate = useNavigate();
const currentUser = getCurrentUser();
const currentUserId = getUserId(currentUser);
const recentConversations = conversations.slice(0, 3); const recentConversations = conversations.slice(0, 3);
const activeConversation = const activeConversation =
recentConversations.find((conversation) => conversation.id === activeConversationId) || recentConversations.find((conversation) => conversation.id === activeConversationId) ||
@ -159,8 +127,13 @@ export function MessagesWorkspace({
); );
const [selectedReplyIndex, setSelectedReplyIndex] = useState(0); const [selectedReplyIndex, setSelectedReplyIndex] = useState(0);
const [noteDraft, setNoteDraft] = useState(''); const [noteDraft, setNoteDraft] = useState('');
const [notes, setNotes] = useState([]); const [notes, setNotes] = useState(() => {
const [notesError, setNotesError] = useState(''); try {
return JSON.parse(window.localStorage.getItem('agentNotes') || '[]');
} catch {
return [];
}
});
const selectedReply = suggestedReplies[selectedReplyIndex] || suggestedReplies[0]; const selectedReply = suggestedReplies[selectedReplyIndex] || suggestedReplies[0];
const managerMessages = [ const managerMessages = [
@ -181,25 +154,8 @@ export function MessagesWorkspace({
}, [safeActiveConversation.id]); }, [safeActiveConversation.id]);
useEffect(() => { useEffect(() => {
let isMounted = true; window.localStorage.setItem('agentNotes', JSON.stringify(notes));
}, [notes]);
async function loadNotes() {
try {
const data = await listAgentNotes(currentUserId);
if (isMounted) {
setNotes(Array.isArray(data) ? data : []);
setNotesError('');
}
} catch (error) {
if (isMounted) setNotesError(error.message);
}
}
loadNotes();
return () => {
isMounted = false;
};
}, [currentUserId]);
function selectPreviousReply() { function selectPreviousReply() {
setSelectedReplyIndex((current) => setSelectedReplyIndex((current) =>
@ -211,37 +167,19 @@ export function MessagesWorkspace({
setSelectedReplyIndex((current) => (current + 1) % suggestedReplies.length); setSelectedReplyIndex((current) => (current + 1) % suggestedReplies.length);
} }
async function saveNote() { function saveNote() {
const text = noteDraft.trim(); const text = noteDraft.trim();
if (!text || !currentUserId) return; if (!text) return;
try { setNotes((current) => [
const note = await createAgentNote(currentUserId, text); {
setNotes((current) => [note, ...current]); id: Date.now(),
text,
time: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }),
},
...current,
]);
setNoteDraft(''); setNoteDraft('');
setNotesError('');
} catch (error) {
setNotesError(error.message);
}
}
async function removeNote(noteId) {
if (!currentUserId) return;
try {
await deleteAgentNote(currentUserId, noteId);
setNotes((current) => current.filter((note) => note.id !== noteId));
setNotesError('');
} catch (error) {
setNotesError(error.message);
}
}
async function sendSuggestedReply() {
if (!safeActiveConversation.id || safeActiveConversation.id === 'empty') return;
await onSendSuggestedReply?.(safeActiveConversation.id, selectedReply);
navigate(`/chat?chatId=${encodeURIComponent(safeActiveConversation.id)}`);
} }
const gridTemplateColumns = isMobile const gridTemplateColumns = isMobile
@ -363,7 +301,7 @@ export function MessagesWorkspace({
{safeActiveConversation.name} {safeActiveConversation.name}
</strong> </strong>
<span style={{ color: 'var(--color-text-soft)' }}> <span style={{ color: 'var(--color-text-soft)' }}>
{safeActiveConversation.lastSeen || 'Sem atividade recente'} {safeActiveConversation.status === 'online' ? 'Online agora' : 'Offline'}
</span> </span>
</div> </div>
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
@ -410,8 +348,6 @@ export function MessagesWorkspace({
> >
{safeActiveConversation.messages.map((message) => { {safeActiveConversation.messages.map((message) => {
const isAgent = message.from === 'agent'; const isAgent = message.from === 'agent';
const parsedText = parseMessageText(message.text);
const messageTime = formatMessageTime(message.timestamp);
return ( return (
<div <div
@ -424,45 +360,9 @@ export function MessagesWorkspace({
background: isAgent ? 'var(--color-primary)' : '#edf1f5', background: isAgent ? 'var(--color-primary)' : '#edf1f5',
color: isAgent ? '#fff' : 'var(--color-text)', color: isAgent ? '#fff' : 'var(--color-text)',
boxShadow: 'var(--shadow-md)', boxShadow: 'var(--shadow-md)',
display: 'grid',
gap: '0.55rem',
}} }}
> >
{parsedText.senderLabel ? ( {message.text}
<strong
style={{
display: 'block',
fontSize: '0.76rem',
lineHeight: 1.2,
letterSpacing: '0.02em',
textTransform: 'uppercase',
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
}}
>
{parsedText.senderLabel}
</strong>
) : null}
<span
style={{
whiteSpace: 'pre-wrap',
lineHeight: 1.45,
overflowWrap: 'anywhere',
}}
>
{parsedText.body}
</span>
{messageTime ? (
<span
style={{
justifySelf: 'end',
fontSize: '0.72rem',
lineHeight: 1,
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
}}
>
{messageTime}
</span>
) : null}
</div> </div>
); );
})} })}
@ -502,7 +402,7 @@ export function MessagesWorkspace({
</button> </button>
<button <button
type="button" type="button"
onClick={sendSuggestedReply} onClick={() => navigate('/chat')}
style={{ style={{
border: '1px solid rgba(0, 164, 183, 0.32)', border: '1px solid rgba(0, 164, 183, 0.32)',
borderRadius: '16px', borderRadius: '16px',
@ -607,7 +507,6 @@ export function MessagesWorkspace({
<button <button
type="button" type="button"
onClick={saveNote} onClick={saveNote}
disabled={!currentUserId}
style={{ style={{
border: 'none', border: 'none',
borderRadius: '18px', borderRadius: '18px',
@ -615,16 +514,11 @@ export function MessagesWorkspace({
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)', background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
color: '#fff', color: '#fff',
fontWeight: 800, fontWeight: 800,
opacity: currentUserId ? 1 : 0.55,
}} }}
> >
Salvar anotacao Salvar anotacao
</button> </button>
{notesError ? (
<span style={{ color: '#b42318', fontWeight: 700 }}>{notesError}</span>
) : null}
<div style={{ display: 'grid', gap: '0.55rem' }}> <div style={{ display: 'grid', gap: '0.55rem' }}>
{notes.length ? ( {notes.length ? (
notes.map((note) => ( notes.map((note) => (
@ -635,32 +529,12 @@ export function MessagesWorkspace({
borderRadius: '16px', borderRadius: '16px',
padding: '0.8rem', padding: '0.8rem',
background: '#fff', background: '#fff',
display: 'grid',
gap: '0.35rem',
}} }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.82rem' }}> <span style={{ color: 'var(--color-text-soft)', fontSize: '0.82rem' }}>
{formatMessageTime(new Date(note.created_at).getTime())} {note.time}
</span> </span>
<button <p style={{ margin: '0.35rem 0 0', lineHeight: 1.45 }}>{note.text}</p>
type="button"
onClick={() => removeNote(note.id)}
title="Excluir anotacao"
style={{
border: 'none',
borderRadius: 999,
width: 26,
height: 26,
background: 'rgba(214, 40, 40, 0.1)',
color: '#b42318',
fontWeight: 900,
}}
>
x
</button>
</div>
<p style={{ margin: 0, lineHeight: 1.45 }}>{note.text}</p>
</article> </article>
)) ))
) : ( ) : (

View File

@ -18,12 +18,10 @@ function toHomeConversation(contact, messages = []) {
lastMessage: contact.preview || messages[messages.length - 1]?.text || '', lastMessage: contact.preview || messages[messages.length - 1]?.text || '',
unread: contact.unread || 0, unread: contact.unread || 0,
time: contact.time || 'Agora', time: contact.time || 'Agora',
lastSeen: contact.lastSeen,
messages: messages.map((message) => ({ messages: messages.map((message) => ({
id: message.id, id: message.id,
from: message.sender === 'agent' ? 'agent' : 'customer', from: message.sender === 'agent' ? 'agent' : 'customer',
text: message.text || (message.hasMedia ? '[Midia]' : ''), text: message.text || (message.hasMedia ? '[Midia]' : ''),
timestamp: message.timestamp,
})), })),
}; };
} }
@ -35,7 +33,6 @@ export function HomePage() {
activeContactId, activeContactId,
setActiveContactId, setActiveContactId,
messages, messages,
sendMessage,
isLoadingChats, isLoadingChats,
} = useChat(); } = useChat();
const [activeTab, setActiveTab] = useState('messages'); const [activeTab, setActiveTab] = useState('messages');
@ -144,10 +141,6 @@ export function HomePage() {
conversations={filteredConversations} conversations={filteredConversations}
activeConversationId={safeConversationId} activeConversationId={safeConversationId}
onSelectConversation={setActiveContactId} onSelectConversation={setActiveContactId}
onSendSuggestedReply={async (conversationId, reply) => {
setActiveContactId(conversationId);
await sendMessage(reply, conversationId);
}}
isWideDesktop={isWideDesktop} isWideDesktop={isWideDesktop}
isDesktop={isDesktop} isDesktop={isDesktop}
isTablet={isTablet} isTablet={isTablet}

View File

@ -1,27 +0,0 @@
import { API_BASE_URL } from '../../../shared/services/apiConfig';
export async function listAgentNotes(userId) {
if (!userId) return [];
const response = await fetch(`${API_BASE_URL}/agent/notes?userId=${encodeURIComponent(userId)}`);
if (!response.ok) throw new Error('Falha ao carregar anotacoes.');
return response.json();
}
export async function createAgentNote(userId, text) {
const response = await fetch(`${API_BASE_URL}/agent/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, text }),
});
if (!response.ok) throw new Error('Falha ao salvar anotacao.');
return response.json();
}
export async function deleteAgentNote(userId, noteId) {
const response = await fetch(
`${API_BASE_URL}/agent/notes/${encodeURIComponent(noteId)}?userId=${encodeURIComponent(userId)}`,
{ method: 'DELETE' },
);
if (!response.ok) throw new Error('Falha ao excluir anotacao.');
return response.json();
}

View File

@ -7,6 +7,7 @@ export function useWhatsappSocket() {
const [qrCode, setQrCode] = useState(null); const [qrCode, setQrCode] = useState(null);
const [status, setStatus] = useState('DISCONNECTED'); const [status, setStatus] = useState('DISCONNECTED');
const [incomingMessage, setIncomingMessage] = useState(null); const [incomingMessage, setIncomingMessage] = useState(null);
const [presenceUpdate, setPresenceUpdate] = useState(null);
const socketRef = useRef(null); const socketRef = useRef(null);
useEffect(() => { useEffect(() => {
@ -43,6 +44,11 @@ export function useWhatsappSocket() {
setIncomingMessage(message); setIncomingMessage(message);
}); });
newSocket.on('presence', (presence) => {
console.log('Atualização de presença:', presence);
setPresenceUpdate(presence);
});
return () => { return () => {
newSocket.disconnect(); newSocket.disconnect();
socketRef.current = null; socketRef.current = null;
@ -54,6 +60,8 @@ export function useWhatsappSocket() {
qrCode, qrCode,
status, status,
incomingMessage, incomingMessage,
presenceUpdate,
clearIncomingMessage: () => setIncomingMessage(null), clearIncomingMessage: () => setIncomingMessage(null),
clearPresenceUpdate: () => setPresenceUpdate(null)
}; };
} }