Compare commits
No commits in common. "b605a4c50796d7f8a642e415634f2a0e33988a47" and "217d566057941065d6e29fe4f153ac0e889be32c" have entirely different histories.
b605a4c507
...
217d566057
@ -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>
|
||||||
|
|||||||
@ -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 envie uma mensagem para assumir automaticamente.'
|
||||||
? 'Este atendimento esta na fila. Assuma para responder ou transferir.'
|
: 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,15 +568,13 @@ 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
|
: 'Atendimento bloqueado para resposta'
|
||||||
? 'Assuma o atendimento para responder'
|
|
||||||
: 'Atendimento bloqueado para resposta'
|
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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,36 +450,28 @@ 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 response = await fetch(`${API_BASE_URL}/whatsapp/assign`, {
|
||||||
const areaId = targetContact?.areaId || targetAssignment?.area_id || areaOptions.find((area) => currentUserAreas.includes(area.nome))?.id;
|
method: 'POST',
|
||||||
try {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
const response = await fetch(`${API_BASE_URL}/whatsapp/assign`, {
|
body: JSON.stringify({
|
||||||
method: 'POST',
|
chatId: activeContactId,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
userId: String(currentUserId),
|
||||||
body: JSON.stringify({
|
areaId,
|
||||||
chatId: contactId,
|
}),
|
||||||
userId: String(currentUserId),
|
});
|
||||||
areaId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
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,19 +490,18 @@ 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) {
|
||||||
return;
|
await assumeChat();
|
||||||
|
} else {
|
||||||
|
setApiError('Este atendimento esta atribuido a outro usuario.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setApiError(error.message);
|
setApiError(error.message);
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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,33 +171,6 @@ export function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isWideDesktop ? (
|
{isWideDesktop ? (
|
||||||
<>
|
|
||||||
<ChatTransferPanel
|
|
||||||
isOpen={isTransferOpen}
|
|
||||||
transferArea={transferArea}
|
|
||||||
setTransferArea={setTransferArea}
|
|
||||||
transferAreas={transferAreas}
|
|
||||||
attendants={attendants}
|
|
||||||
isSameUserArea={isSameUserArea}
|
|
||||||
transferAttendant={transferAttendant}
|
|
||||||
setTransferAttendant={setTransferAttendant}
|
|
||||||
transferNote={transferNote}
|
|
||||||
setTransferNote={setTransferNote}
|
|
||||||
onSubmit={submitTransfer}
|
|
||||||
onClose={() => setIsTransferOpen(false)}
|
|
||||||
/>
|
|
||||||
<ContactProfilePanel
|
|
||||||
isOpen={isContactPanelOpen}
|
|
||||||
contact={activeContact}
|
|
||||||
onClose={() => setIsContactPanelOpen(false)}
|
|
||||||
onSaved={updateContactProfile}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{!isWideDesktop ? (
|
|
||||||
<>
|
|
||||||
<ChatTransferPanel
|
<ChatTransferPanel
|
||||||
isOpen={isTransferOpen}
|
isOpen={isTransferOpen}
|
||||||
transferArea={transferArea}
|
transferArea={transferArea}
|
||||||
@ -233,13 +185,24 @@ export function ChatPage() {
|
|||||||
onSubmit={submitTransfer}
|
onSubmit={submitTransfer}
|
||||||
onClose={() => setIsTransferOpen(false)}
|
onClose={() => setIsTransferOpen(false)}
|
||||||
/>
|
/>
|
||||||
<ContactProfilePanel
|
) : null}
|
||||||
isOpen={isContactPanelOpen}
|
</section>
|
||||||
contact={activeContact}
|
|
||||||
onClose={() => setIsContactPanelOpen(false)}
|
{!isWideDesktop ? (
|
||||||
onSaved={updateContactProfile}
|
<ChatTransferPanel
|
||||||
/>
|
isOpen={isTransferOpen}
|
||||||
</>
|
transferArea={transferArea}
|
||||||
|
setTransferArea={setTransferArea}
|
||||||
|
transferAreas={transferAreas}
|
||||||
|
attendants={attendants}
|
||||||
|
isSameUserArea={isSameUserArea}
|
||||||
|
transferAttendant={transferAttendant}
|
||||||
|
setTransferAttendant={setTransferAttendant}
|
||||||
|
transferNote={transferNote}
|
||||||
|
setTransferNote={setTransferNote}
|
||||||
|
onSubmit={submitTransfer}
|
||||||
|
onClose={() => setIsTransferOpen(false)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
@ -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(),
|
||||||
setNoteDraft('');
|
text,
|
||||||
setNotesError('');
|
time: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }),
|
||||||
} catch (error) {
|
},
|
||||||
setNotesError(error.message);
|
...current,
|
||||||
}
|
]);
|
||||||
}
|
setNoteDraft('');
|
||||||
|
|
||||||
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' }}>
|
{note.time}
|
||||||
{formatMessageTime(new Date(note.created_at).getTime())}
|
</span>
|
||||||
</span>
|
<p style={{ margin: '0.35rem 0 0', lineHeight: 1.45 }}>{note.text}</p>
|
||||||
<button
|
|
||||||
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>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user