- Escolha o contato, o canal e a area opcional antes de iniciar. O fluxo e mockado
- e leva voce direto para chat ou ligacao.
+ Informe um contato de WhatsApp ou selecione alguem da agenda para iniciar o atendimento.
+ Para conversas novas, o primeiro envio usa uma mensagem pre-aprovada da Meta.
- setSearchValue(event.target.value)}
+ placeholder="Buscar contato salvo por nome ou numero"
style={{
- display: 'grid',
- gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto',
- gap: '0.85rem',
+ border: '1px solid var(--color-border)',
+ borderRadius: '18px',
+ padding: '0.95rem 1rem',
+ background: '#fff',
+ outline: 'none',
}}
- >
- setSearchValue(event.target.value)}
- placeholder="Buscar contato por nome ou numero"
- style={{
- border: '1px solid var(--color-border)',
- borderRadius: '18px',
- padding: '0.95rem 1rem',
- background: '#fff',
- outline: 'none',
- }}
- />
- setCustomNumber(selectedContact.phone)}
- style={{
- border: '1px solid var(--color-border)',
- borderRadius: '18px',
- padding: '0.95rem 1rem',
- background: '#fff',
- color: 'var(--color-primary)',
- fontWeight: 700,
- }}
- >
- Novo numero
-
-
+ />
{attendanceChannels.map((channel) => {
const isActive = channel.id === selectedChannelId;
+ const isDisabled = Boolean(channel.disabled);
return (
setSelectedChannelId(channel.id)}
+ onClick={() => {
+ if (!isDisabled) setSelectedChannelId(channel.id);
+ }}
+ disabled={isDisabled}
style={{
border: '1px solid',
borderColor: isActive ? `${channel.accent}44` : 'var(--color-border)',
@@ -195,15 +459,15 @@ export function NewAttendancePage() {
textAlign: 'left',
display: 'grid',
gap: '0.45rem',
+ opacity: isDisabled ? 0.58 : 1,
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
}}
>
{channel.label}
- {channel.id === 'call'
- ? 'Inicia uma ligacao mock em tela cheia.'
- : 'Abre o fluxo de conversa em tempo real.'}
+ {isDisabled ? 'Canal em construcao.' : 'Inicia uma conversa pelo WhatsApp.'}
);
@@ -213,15 +477,22 @@ export function NewAttendancePage() {
- Area (opcional)
+ Pais
setSelectedArea(event.target.value)}
+ value={selectedCountryId}
+ onChange={(event) => {
+ const nextCountryId = event.target.value;
+ setSelectedCountryId(nextCountryId);
+ setForm((current) => ({
+ ...current,
+ phone: applyPhoneMask(current.phone, nextCountryId),
+ }));
+ }}
style={{
border: '1px solid var(--color-border)',
borderRadius: '18px',
@@ -230,22 +501,38 @@ export function NewAttendancePage() {
outline: 'none',
}}
>
- Selecionar depois
- {attendanceAreas.map((area) => (
-
- {area}
+ {countryOptions.map((country) => (
+
+ {country.label} +{country.dialCode}
))}
- Numero selecionado
+ Numero do WhatsApp
setCustomNumber(event.target.value)}
- placeholder="+55 11 99999-9999"
+ value={form.phone}
+ onChange={(event) => setForm((current) => ({ ...current, phone: applyPhoneMask(event.target.value, selectedCountryId) }))}
+ placeholder={selectedCountry.placeholder}
+ style={{
+ border: '1px solid var(--color-border)',
+ borderRadius: '18px',
+ padding: '0.95rem 1rem',
+ background: '#fff',
+ outline: 'none',
+ }}
+ />
+
+
+
+ Nome do cliente
+ setForm((current) => ({ ...current, name: event.target.value }))}
+ placeholder="Nome para salvar na agenda"
style={{
border: '1px solid var(--color-border)',
borderRadius: '18px',
@@ -257,6 +544,92 @@ export function NewAttendancePage() {
+
+
+ Empresa
+ setForm((current) => ({ ...current, company: event.target.value }))}
+ placeholder="Empresa ou conta vinculada"
+ style={{
+ border: '1px solid var(--color-border)',
+ borderRadius: '18px',
+ padding: '0.95rem 1rem',
+ background: '#fff',
+ outline: 'none',
+ }}
+ />
+
+
+
+ Mensagem pre-aprovada
+ setSelectedTemplateId(event.target.value)}
+ style={{
+ border: '1px solid var(--color-border)',
+ borderRadius: '18px',
+ padding: '0.95rem 1rem',
+ background: '#fff',
+ outline: 'none',
+ }}
+ >
+ Selecione um template
+ {templates.map((template) => (
+
+ {template.name}
+
+ ))}
+
+
+
+
+ {selectedTemplate ? (
+
+
+ Preview do template
+
+ {renderTemplatePreview(selectedTemplate.content, form)}
+
+ ) : null}
+
+
+ Observacao
+
+
+ {error ?
{error} : null}
+ {isLoadingContacts ?
Carregando agenda... : null}
+
- Resumo do fluxo
+ Resumo
- {selectedContact.name}
+ {form.name || 'Cliente sem nome'}
- Canal escolhido: {selectedChannel.label}
+ Canal: {selectedChannel.label}
- Numero: {customNumber || selectedContact.phone}
+ Numero: {buildInternationalPhone(form.phone, selectedCountryId) ? `+${buildInternationalPhone(form.phone, selectedCountryId)}` : 'Nao informado'}
- Area: {selectedArea || 'Definir depois'}
+ Empresa: {form.company || 'Nao informada'}
+
+
+ Origem: {selectedContactId ? 'Agenda' : 'Novo contato'}
@@ -311,25 +687,42 @@ export function NewAttendancePage() {
>
Proxima rota
- {selectedChannel.route === '/call'
- ? 'Ao iniciar, voce vai para a tela de ligacao.'
- : 'Ao iniciar, voce vai para a tela de chat.'}
+ O contato sera salvo, o template sera enviado e a conversa abrira atribuida a voce no chat.
-
- Iniciar atendimento
-
+
+ {selectedContactId ? (
+
+ Limpar selecao
+
+ ) : null}
+
+ {isStarting ? 'Iniciando...' : 'Iniciar atendimento'}
+
+
diff --git a/src/modules/attendance/services/attendanceMocks.js b/src/modules/attendance/services/attendanceMocks.js
index f61af7e..b3101b3 100644
--- a/src/modules/attendance/services/attendanceMocks.js
+++ b/src/modules/attendance/services/attendanceMocks.js
@@ -1,38 +1,5 @@
export const attendanceChannels = [
{ id: 'whatsapp', label: 'WhatsApp', route: '/chat', accent: '#2bb741' },
- { id: 'sms', label: 'SMS', route: '/chat', accent: '#00a4b7' },
- { id: 'call', label: 'Ligacao', route: '/call', accent: '#e5a22a' },
-];
-
-export const attendanceAreas = ['Suporte', 'Financeiro', 'Comercial'];
-
-export const recentContacts = [
- {
- id: 'maria-souza',
- name: 'Maria Souza',
- phone: '+55 11 99888-7766',
- channel: 'WhatsApp',
- lastContact: 'Hoje, 09:42',
- },
- {
- id: 'empresa-alpha',
- name: 'Empresa Alpha',
- phone: '+55 11 4002-2020',
- channel: 'Email',
- lastContact: 'Ontem, 16:18',
- },
- {
- id: 'joao-pedro',
- name: 'João Pedro',
- phone: '+55 31 98877-1102',
- channel: 'SMS',
- lastContact: 'Hoje, 08:15',
- },
- {
- id: 'beatriz-lima',
- name: 'Beatriz Lima',
- phone: '+55 21 99701-4455',
- channel: 'Ligação',
- lastContact: 'Hoje, 07:51',
- },
+ { id: 'sms', label: 'SMS', route: '/chat', accent: '#00a4b7', disabled: true },
+ { id: 'email', label: 'E-mail', route: '/chat', accent: '#e5a22a', disabled: true },
];
diff --git a/src/modules/chat/components/ChatConversationList.jsx b/src/modules/chat/components/ChatConversationList.jsx
index f1f3929..f4adf6f 100644
--- a/src/modules/chat/components/ChatConversationList.jsx
+++ b/src/modules/chat/components/ChatConversationList.jsx
@@ -88,6 +88,8 @@ function SavedContactLabel({ contact }) {
);
}
+const CHAT_LIST_HEIGHT = 'min(760px, calc(100vh - 160px))';
+
export function ChatConversationList({
contacts,
activeContactId,
@@ -105,9 +107,9 @@ export function ChatConversationList({
display: 'grid',
gridTemplateRows: 'auto minmax(0, 1fr)',
gap: '0.85rem',
- height: isMobile ? 'auto' : '100%',
- maxHeight: isMobile ? 'none' : '100%',
- alignSelf: isMobile ? 'start' : 'stretch',
+ height: isMobile ? 'auto' : CHAT_LIST_HEIGHT,
+ maxHeight: isMobile ? 'none' : CHAT_LIST_HEIGHT,
+ alignSelf: 'start',
minHeight: 0,
}}
>
diff --git a/src/modules/chat/components/ChatWindow.jsx b/src/modules/chat/components/ChatWindow.jsx
index 956ca66..c4c02b4 100644
--- a/src/modules/chat/components/ChatWindow.jsx
+++ b/src/modules/chat/components/ChatWindow.jsx
@@ -274,7 +274,7 @@ export function ChatWindow({
container.scrollTo({
top: container.scrollHeight,
- behavior: 'smooth',
+ behavior: 'auto',
});
}, [messages, isReplying]);
@@ -615,6 +615,8 @@ export function ChatWindow({
? 'Aguardando conversa entrar em uma fila'
: canReply
? 'Escreva sua mensagem...'
+ : assignmentLabel?.includes('Aguardando resposta')
+ ? 'Aguardando resposta do cliente'
: canAssumeChat
? 'Assuma o atendimento para responder'
: 'Atendimento bloqueado para resposta'
diff --git a/src/modules/chat/hooks/useChat.js b/src/modules/chat/hooks/useChat.js
index ce1c142..c3fc331 100644
--- a/src/modules/chat/hooks/useChat.js
+++ b/src/modules/chat/hooks/useChat.js
@@ -154,6 +154,38 @@ function buildFallbackContacts() {
}));
}
+function normalizeComparableContact(contact) {
+ return {
+ id: contact.id,
+ name: contact.name,
+ preview: contact.preview,
+ time: contact.time,
+ unread: contact.unread,
+ area: contact.area,
+ areaId: contact.areaId,
+ lastSeen: contact.lastSeen,
+ lastMessageFromMe: contact.lastMessageFromMe,
+ assignmentStatus: contact.assignment?.status || null,
+ assignmentUserId: contact.assignment?.user_id || null,
+ assignmentAreaId: contact.assignment?.area_id || null,
+ transferNote: contact.assignment?.transfer_note || null,
+ awaitingCustomerReply: contact.assignment?.awaiting_customer_reply || false,
+ contactName: contact.contactProfile?.name || null,
+ contactCompany: contact.contactProfile?.company || null,
+ contactNote: contact.contactProfile?.note || null,
+ contactPhone: contact.contactProfile?.phone || null,
+ };
+}
+
+function areContactListsEqual(currentContacts, nextContacts) {
+ if (currentContacts.length !== nextContacts.length) return false;
+ return currentContacts.every((contact, index) => {
+ const currentComparable = normalizeComparableContact(contact);
+ const nextComparable = normalizeComparableContact(nextContacts[index]);
+ return JSON.stringify(currentComparable) === JSON.stringify(nextComparable);
+ });
+}
+
function getUserId(user) {
const value = user?.databaseId || user?.id;
const numeric = Number(value);
@@ -200,6 +232,7 @@ export function useChat() {
const [isLoadingMessages, setIsLoadingMessages] = useState(false);
const [apiError, setApiError] = useState(null);
const activeContactRef = useRef(activeContactId);
+ const contactsRef = useRef(contacts);
const activeContact = useMemo(
() => {
@@ -225,14 +258,17 @@ export function useChat() {
const isAssignedToCurrentUser = Boolean(
activeAssignment?.user_id && currentUserId && Number(activeAssignment.user_id) === currentUserId,
);
+ const isWaitingCustomerReply = Boolean(activeAssignment?.awaiting_customer_reply);
const isQueuedForUserArea = Boolean(
activeAssignment?.status === 'queued' &&
(!activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)),
);
const canAssumeChat = Boolean(activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea);
- const canReply = Boolean(isAssignedToCurrentUser);
+ const canReply = Boolean(isAssignedToCurrentUser && !isWaitingCustomerReply);
const assignmentLabel = activeAssignment?.user_id
- ? `Atendimento com ${activeAssignment.user_nome || 'outro atendente'}`
+ ? isWaitingCustomerReply
+ ? 'Aguardando resposta do cliente para liberar novas mensagens'
+ : `Atendimento com ${activeAssignment.user_nome || 'outro atendente'}`
: activeAssignment?.area_nome
? `Na fila de ${activeAssignment.area_nome}`
: 'Sem fila definida';
@@ -250,6 +286,10 @@ export function useChat() {
activeContactRef.current = activeContactId;
}, [activeContactId]);
+ useEffect(() => {
+ contactsRef.current = contacts;
+ }, [contacts]);
+
useEffect(() => {
let isMounted = true;
@@ -282,10 +322,10 @@ export function useChat() {
return currentUserAreas.includes(contact.assignment.area_nome);
}
- async function loadChats() {
+ async function loadChats({ showLoading = false } = {}) {
if (whatsappStatus !== 'CONNECTED') {
const fallbackContacts = buildFallbackContacts();
- setContacts(fallbackContacts);
+ setContacts((current) => (areContactListsEqual(current, fallbackContacts) ? current : fallbackContacts));
setActiveContactId((current) =>
fallbackContacts.some((contact) => contact.id === current) ? current : fallbackContacts[0]?.id,
);
@@ -293,7 +333,9 @@ export function useChat() {
return;
}
- setIsLoadingChats(true);
+ if (showLoading) {
+ setIsLoadingChats(true);
+ }
try {
const response = await fetch(`${API_BASE_URL}/whatsapp/chats`);
if (!response.ok) throw new Error('Falha ao carregar chats do WhatsApp.');
@@ -301,7 +343,7 @@ export function useChat() {
if (!Array.isArray(data)) return;
const nextContacts = data.map(normalizeChat).filter(canSeeContact);
- setContacts(nextContacts);
+ setContacts((current) => (areContactListsEqual(current, nextContacts) ? current : nextContacts));
setActiveContactId((current) =>
nextContacts.some((contact) => contact.id === current) ? current : nextContacts[0]?.id || '',
);
@@ -318,7 +360,7 @@ export function useChat() {
async function guardedLoadChats() {
if (!isMounted) return;
- await loadChats();
+ await loadChats({ showLoading: contactsRef.current.length === 0 });
}
guardedLoadChats();
@@ -426,7 +468,7 @@ export function useChat() {
});
clearIncomingMessage();
- window.setTimeout(loadChats, 1200);
+ window.setTimeout(() => loadChats({ showLoading: false }), 1200);
}, [incomingMessage, clearIncomingMessage]);
function updateContact(contactId, updater) {
@@ -558,11 +600,16 @@ export function useChat() {
const targetIsAssignedToCurrentUser = Boolean(
targetAssignment?.user_id && currentUserId && Number(targetAssignment.user_id) === currentUserId,
);
+ const targetIsWaitingCustomerReply = Boolean(targetAssignment?.awaiting_customer_reply);
try {
if (!targetIsAssignedToCurrentUser) {
setApiError('Assuma o atendimento antes de responder.');
return;
}
+ if (targetIsWaitingCustomerReply) {
+ setApiError('Aguarde o cliente responder antes de enviar novas mensagens.');
+ return;
+ }
} catch (error) {
setApiError(error.message);
return;
diff --git a/src/modules/chat/pages/ChatPage.jsx b/src/modules/chat/pages/ChatPage.jsx
index cb3bfd8..ed883a0 100644
--- a/src/modules/chat/pages/ChatPage.jsx
+++ b/src/modules/chat/pages/ChatPage.jsx
@@ -1,5 +1,5 @@
import { Link, useSearchParams } from 'react-router-dom';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { BrandMark } from '../../../shared/components/BrandMark';
import { useViewport } from '../../../shared/hooks/useViewport';
import { ChatConversationList } from '../components/ChatConversationList';
@@ -10,7 +10,7 @@ import { useChat } from '../hooks/useChat';
import { quickReplies } from '../services/chatMocks';
export function ChatPage() {
- const [searchParams] = useSearchParams();
+ const [searchParams, setSearchParams] = useSearchParams();
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
const {
contacts,
@@ -49,13 +49,24 @@ export function ChatPage() {
submitTransfer,
} = useChat();
const requestedChatId = searchParams.get('chatId');
+ const handledRequestedChatIdRef = useRef('');
const [isContactPanelOpen, setIsContactPanelOpen] = useState(false);
useEffect(() => {
if (!requestedChatId) return;
+ if (handledRequestedChatIdRef.current === requestedChatId) return;
if (!contacts.some((contact) => contact.id === requestedChatId)) return;
+ handledRequestedChatIdRef.current = requestedChatId;
setActiveContactId(requestedChatId);
- }, [requestedChatId, contacts, setActiveContactId]);
+ setSearchParams({}, { replace: true });
+ }, [requestedChatId, contacts, setActiveContactId, setSearchParams]);
+
+ function selectContact(contactId) {
+ setActiveContactId(contactId);
+ if (requestedChatId) {
+ setSearchParams({}, { replace: true });
+ }
+ }
const gridTemplateColumns = isMobile
? '1fr'
@@ -128,7 +139,7 @@ export function ChatPage() {
{
setIsTransferOpen(false);
setIsContactPanelOpen(true);
diff --git a/src/modules/chat/services/chatMocks.js b/src/modules/chat/services/chatMocks.js
index b2dd10d..f39f6f9 100644
--- a/src/modules/chat/services/chatMocks.js
+++ b/src/modules/chat/services/chatMocks.js
@@ -5,7 +5,7 @@ export const chatContacts = [
channel: 'WhatsApp',
status: 'away',
area: 'Suporte',
- lastSeen: 'Ultima atividade as 09:42',
+ lastSeen: 'Última atividade as 09:42',
preview: 'Preciso atualizar o cadastro do meu pedido.',
time: '09:42',
unread: 2,
@@ -22,7 +22,7 @@ export const chatContacts = [
channel: 'SMS',
status: 'offline',
area: 'Financeiro',
- lastSeen: 'Ultima atividade as 08:15',
+ lastSeen: 'Última atividade as 08:15',
preview: 'Pode me ligar em 10 minutos?',
time: '08:15',
unread: 1,
diff --git a/src/modules/chat/services/contactProfileService.js b/src/modules/chat/services/contactProfileService.js
index f3a432e..e8c4f96 100644
--- a/src/modules/chat/services/contactProfileService.js
+++ b/src/modules/chat/services/contactProfileService.js
@@ -1,5 +1,11 @@
import { API_BASE_URL } from '../../../shared/services/apiConfig';
+export async function listContactProfiles() {
+ const response = await fetch(`${API_BASE_URL}/contacts`);
+ if (!response.ok) throw new Error('Falha ao carregar agenda.');
+ return response.json();
+}
+
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.');