2026-05-18 19:11:01 -03:00
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
2026-03-19 18:22:18 -03:00
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2026-05-20 11:37:29 -03:00
|
|
|
|
import { createAgentNote, deleteAgentNote, listAgentNotes } from '../services/agentNotesService';
|
|
|
|
|
|
import { getCurrentUser } from '../../auth/services/sessionService';
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
2026-05-18 19:11:01 -03:00
|
|
|
|
const WORKSPACE_HEIGHT = 660;
|
|
|
|
|
|
|
2026-03-19 18:22:18 -03:00
|
|
|
|
function ChannelBadge({ channel }) {
|
|
|
|
|
|
const colors = {
|
|
|
|
|
|
WhatsApp: '#2bb741',
|
|
|
|
|
|
Email: '#e5a22a',
|
|
|
|
|
|
SMS: '#00a4b7',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'inline-flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
borderRadius: 999,
|
|
|
|
|
|
padding: '0.22rem 0.6rem',
|
|
|
|
|
|
background: `${colors[channel] || '#003150'}16`,
|
|
|
|
|
|
color: colors[channel] || '#003150',
|
|
|
|
|
|
fontSize: '0.8rem',
|
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{channel}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
|
function UnreadBadge({ count }) {
|
|
|
|
|
|
if (!count) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 26,
|
|
|
|
|
|
height: 26,
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
background: 'var(--color-secondary)',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '0.78rem',
|
|
|
|
|
|
fontWeight: 800,
|
|
|
|
|
|
display: 'inline-grid',
|
|
|
|
|
|
placeItems: 'center',
|
|
|
|
|
|
lineHeight: 1,
|
|
|
|
|
|
flex: '0 0 auto',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{count > 99 ? '99+' : count}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 19:11:01 -03:00
|
|
|
|
function buildSuggestedReplies(conversation) {
|
|
|
|
|
|
const lastMessage = conversation?.lastMessage || conversation?.messages?.at(-1)?.text || '';
|
2026-05-21 15:50:55 -03:00
|
|
|
|
const firstName = conversation?.name?.split(' ')?.[0] || 'você';
|
2026-05-18 19:11:01 -03:00
|
|
|
|
const lowerContext = lastMessage.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
lowerContext.includes('fatura') ||
|
|
|
|
|
|
lowerContext.includes('cobranca') ||
|
|
|
|
|
|
lowerContext.includes('pagamento')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return [
|
2026-05-21 15:50:55 -03:00
|
|
|
|
`${firstName}, vou conferir os dados financeiros e já te retorno com a posição correta.`,
|
|
|
|
|
|
'Recebi sua mensagem sobre cobrança. Vou validar o histórico antes de seguir com a orientação.',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
'Consigo te ajudar com isso. Pode me confirmar o CPF/CNPJ ou protocolo vinculado ao atendimento?',
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
lowerContext.includes('endereco') ||
|
|
|
|
|
|
lowerContext.includes('cadastro') ||
|
|
|
|
|
|
lowerContext.includes('atualizar')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return [
|
2026-05-21 15:50:55 -03:00
|
|
|
|
`${firstName}, vou validar seu cadastro e confirmar se a alteração já foi registrada.`,
|
|
|
|
|
|
'Para seguir com a atualização, me confirme por favor os dados que precisam ser ajustados.',
|
|
|
|
|
|
'Entendi. Vou verificar o cadastro atual e te retorno com o próximo passo.',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
lowerContext.includes('ligar') ||
|
|
|
|
|
|
lowerContext.includes('telefone') ||
|
|
|
|
|
|
lowerContext.includes('retorno')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return [
|
2026-05-21 15:50:55 -03:00
|
|
|
|
`${firstName}, consigo organizar esse retorno. Qual o melhor horário para contato?`,
|
|
|
|
|
|
'Vou registrar sua solicitação e direcionar o retorno para o time responsável.',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
'Obrigado pelo aviso. Vou confirmar disponibilidade e te retorno por aqui.',
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
`${firstName}, recebi sua mensagem e vou verificar o contexto para te orientar corretamente.`,
|
2026-05-21 15:50:55 -03:00
|
|
|
|
'Entendi. Vou analisar as informações do atendimento e retorno com o melhor encaminhamento.',
|
|
|
|
|
|
'Posso acionar o time responsável e te atualizar por aqui assim que tiver uma posição.',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 11:37:29 -03:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 18:22:18 -03:00
|
|
|
|
export function MessagesWorkspace({
|
|
|
|
|
|
conversations,
|
|
|
|
|
|
activeConversationId,
|
|
|
|
|
|
onSelectConversation,
|
2026-05-20 11:37:29 -03:00
|
|
|
|
onSendSuggestedReply,
|
2026-03-19 18:22:18 -03:00
|
|
|
|
isWideDesktop = false,
|
|
|
|
|
|
isDesktop = false,
|
|
|
|
|
|
isTablet = false,
|
|
|
|
|
|
isMobile = false,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const navigate = useNavigate();
|
2026-05-20 11:37:29 -03:00
|
|
|
|
const currentUser = getCurrentUser();
|
|
|
|
|
|
const currentUserId = getUserId(currentUser);
|
2026-05-18 19:11:01 -03:00
|
|
|
|
const recentConversations = conversations.slice(0, 3);
|
2026-03-19 18:22:18 -03:00
|
|
|
|
const activeConversation =
|
2026-05-18 19:11:01 -03:00
|
|
|
|
recentConversations.find((conversation) => conversation.id === activeConversationId) ||
|
|
|
|
|
|
recentConversations[0] ||
|
2026-03-19 18:22:18 -03:00
|
|
|
|
conversations[0];
|
2026-05-18 19:11:01 -03:00
|
|
|
|
const safeActiveConversation = activeConversation || {
|
|
|
|
|
|
id: 'empty',
|
|
|
|
|
|
name: 'Nenhuma conversa',
|
|
|
|
|
|
status: 'offline',
|
|
|
|
|
|
messages: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
const suggestedReplies = useMemo(
|
|
|
|
|
|
() => buildSuggestedReplies(safeActiveConversation),
|
|
|
|
|
|
[safeActiveConversation],
|
|
|
|
|
|
);
|
|
|
|
|
|
const [selectedReplyIndex, setSelectedReplyIndex] = useState(0);
|
|
|
|
|
|
const [noteDraft, setNoteDraft] = useState('');
|
2026-05-20 11:37:29 -03:00
|
|
|
|
const [notes, setNotes] = useState([]);
|
|
|
|
|
|
const [notesError, setNotesError] = useState('');
|
2026-05-18 19:11:01 -03:00
|
|
|
|
|
|
|
|
|
|
const selectedReply = suggestedReplies[selectedReplyIndex] || suggestedReplies[0];
|
|
|
|
|
|
const managerMessages = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'sla',
|
|
|
|
|
|
title: 'Comunicado do supervisor',
|
|
|
|
|
|
text: 'Priorizar atendimentos com SLA abaixo de 15 minutos antes de abrir novos casos.',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'script',
|
2026-05-21 15:50:55 -03:00
|
|
|
|
title: 'Atualização de script',
|
|
|
|
|
|
text: 'Use o novo roteiro de confirmação de dados em atendimentos financeiros.',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setSelectedReplyIndex(0);
|
|
|
|
|
|
}, [safeActiveConversation.id]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-20 11:37:29 -03:00
|
|
|
|
let isMounted = true;
|
|
|
|
|
|
|
|
|
|
|
|
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]);
|
2026-05-18 19:11:01 -03:00
|
|
|
|
|
|
|
|
|
|
function selectPreviousReply() {
|
|
|
|
|
|
setSelectedReplyIndex((current) =>
|
|
|
|
|
|
current === 0 ? suggestedReplies.length - 1 : current - 1,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectNextReply() {
|
|
|
|
|
|
setSelectedReplyIndex((current) => (current + 1) % suggestedReplies.length);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 11:37:29 -03:00
|
|
|
|
async function saveNote() {
|
2026-05-18 19:11:01 -03:00
|
|
|
|
const text = noteDraft.trim();
|
2026-05-20 11:37:29 -03:00
|
|
|
|
if (!text || !currentUserId) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const note = await createAgentNote(currentUserId, text);
|
|
|
|
|
|
setNotes((current) => [note, ...current]);
|
|
|
|
|
|
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)}`);
|
2026-05-18 19:11:01 -03:00
|
|
|
|
}
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
|
|
const gridTemplateColumns = isMobile
|
|
|
|
|
|
? '1fr'
|
|
|
|
|
|
: isWideDesktop
|
|
|
|
|
|
? 'minmax(240px, 0.95fr) minmax(360px, 1.8fr) minmax(220px, 0.8fr)'
|
|
|
|
|
|
: isDesktop || isTablet
|
2026-05-18 19:11:01 -03:00
|
|
|
|
? 'minmax(260px, 320px) minmax(0, 1fr)'
|
|
|
|
|
|
: '1fr';
|
|
|
|
|
|
|
|
|
|
|
|
const panelHeight = isMobile ? 'auto' : WORKSPACE_HEIGHT;
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns,
|
|
|
|
|
|
gap: '1rem',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
alignItems: 'stretch',
|
2026-03-19 18:22:18 -03:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<section
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
borderRadius: '26px',
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gap: '0.75rem',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
alignContent: 'start',
|
|
|
|
|
|
height: panelHeight,
|
2026-03-19 18:22:18 -03:00
|
|
|
|
minWidth: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong style={{ fontSize: '1.05rem' }}>Conversas</strong>
|
|
|
|
|
|
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
|
2026-05-21 15:50:55 -03:00
|
|
|
|
Últimos 3 atendimentos em tempo real.
|
2026-03-19 18:22:18 -03:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-18 19:11:01 -03:00
|
|
|
|
{recentConversations.map((conversation) => {
|
|
|
|
|
|
const isActive = conversation.id === safeActiveConversation.id;
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={conversation.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => onSelectConversation(conversation.id)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid',
|
|
|
|
|
|
borderColor: isActive ? 'rgba(0, 164, 183, 0.26)' : 'var(--color-border)',
|
|
|
|
|
|
borderRadius: '20px',
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
background: isActive ? 'rgba(0, 164, 183, 0.08)' : '#fff',
|
|
|
|
|
|
textAlign: 'left',
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gap: '0.6rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
|
|
|
|
|
|
<strong>{conversation.name}</strong>
|
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.86rem' }}>
|
|
|
|
|
|
{conversation.time}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
|
|
|
|
|
|
<ChannelBadge channel={conversation.channel} />
|
2026-05-19 15:29:43 -03:00
|
|
|
|
<UnreadBadge count={conversation.unread} />
|
2026-03-19 18:22:18 -03:00
|
|
|
|
</div>
|
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)' }}>{conversation.lastMessage}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-05-18 19:11:01 -03:00
|
|
|
|
|
|
|
|
|
|
{conversations.length > 3 ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => navigate('/chat')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
borderRadius: '16px',
|
|
|
|
|
|
padding: '0.85rem 1rem',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Ver todos no chat
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
2026-03-19 18:22:18 -03:00
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
borderRadius: '26px',
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
display: 'grid',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
gridTemplateRows: 'auto minmax(0, 1fr) auto',
|
|
|
|
|
|
height: panelHeight,
|
|
|
|
|
|
minHeight: isMobile ? 580 : 'auto',
|
2026-03-19 18:22:18 -03:00
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
minWidth: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<header
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '1.15rem 1.25rem',
|
|
|
|
|
|
borderBottom: '1px solid var(--color-border)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
gap: '1rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
2026-05-18 19:11:01 -03:00
|
|
|
|
<strong style={{ display: 'block', fontSize: '1.08rem' }}>
|
|
|
|
|
|
{safeActiveConversation.name}
|
|
|
|
|
|
</strong>
|
2026-03-19 18:22:18 -03:00
|
|
|
|
<span style={{ color: 'var(--color-text-soft)' }}>
|
2026-05-19 16:39:01 -03:00
|
|
|
|
{safeActiveConversation.lastSeen || 'Sem atividade recente'}
|
2026-03-19 18:22:18 -03:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => navigate('/chat')}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
|
padding: '0.7rem 0.9rem',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Abrir chat
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
|
padding: '0.7rem 0.9rem',
|
|
|
|
|
|
background: 'rgba(0, 49, 80, 0.08)',
|
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Transferir
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '1.25rem',
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gap: '0.9rem',
|
|
|
|
|
|
alignContent: 'start',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
overflowY: 'auto',
|
2026-03-19 18:22:18 -03:00
|
|
|
|
background:
|
|
|
|
|
|
'linear-gradient(180deg, rgba(245, 248, 251, 0.45), rgba(255, 255, 255, 0.9))',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-05-18 19:11:01 -03:00
|
|
|
|
{safeActiveConversation.messages.map((message) => {
|
2026-03-19 18:22:18 -03:00
|
|
|
|
const isAgent = message.from === 'agent';
|
2026-05-20 11:37:29 -03:00
|
|
|
|
const parsedText = parseMessageText(message.text);
|
|
|
|
|
|
const messageTime = formatMessageTime(message.timestamp);
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={message.id}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
justifySelf: isAgent ? 'end' : 'start',
|
|
|
|
|
|
maxWidth: '72%',
|
|
|
|
|
|
padding: '0.95rem 1rem',
|
|
|
|
|
|
borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
|
|
|
|
|
|
background: isAgent ? 'var(--color-primary)' : '#edf1f5',
|
|
|
|
|
|
color: isAgent ? '#fff' : 'var(--color-text)',
|
|
|
|
|
|
boxShadow: 'var(--shadow-md)',
|
2026-05-20 11:37:29 -03:00
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gap: '0.55rem',
|
2026-03-19 18:22:18 -03:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-05-20 11:37:29 -03:00
|
|
|
|
{parsedText.senderLabel ? (
|
|
|
|
|
|
<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}
|
2026-03-19 18:22:18 -03:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<footer
|
|
|
|
|
|
style={{
|
2026-05-18 19:11:01 -03:00
|
|
|
|
padding: '0.85rem 1.25rem 1rem',
|
2026-03-19 18:22:18 -03:00
|
|
|
|
borderTop: '1px solid var(--color-border)',
|
|
|
|
|
|
display: 'grid',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
gap: '0.65rem',
|
2026-03-19 18:22:18 -03:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-05-18 19:11:01 -03:00
|
|
|
|
<strong style={{ display: 'block', fontSize: '0.94rem' }}>Resposta sugerida</strong>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
2026-03-19 18:22:18 -03:00
|
|
|
|
style={{
|
2026-05-18 19:11:01 -03:00
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns: '40px minmax(0, 1fr) 40px',
|
|
|
|
|
|
gap: '0.6rem',
|
|
|
|
|
|
alignItems: 'stretch',
|
2026-03-19 18:22:18 -03:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-05-18 19:11:01 -03:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={selectPreviousReply}
|
|
|
|
|
|
title="Resposta anterior"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
|
fontWeight: 900,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
‹
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-05-20 11:37:29 -03:00
|
|
|
|
onClick={sendSuggestedReply}
|
2026-05-18 19:11:01 -03:00
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid rgba(0, 164, 183, 0.32)',
|
|
|
|
|
|
borderRadius: '16px',
|
|
|
|
|
|
padding: '0.75rem 0.9rem',
|
|
|
|
|
|
background: 'rgba(0, 164, 183, 0.07)',
|
|
|
|
|
|
color: 'var(--color-text)',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
textAlign: 'left',
|
|
|
|
|
|
lineHeight: 1.35,
|
|
|
|
|
|
minWidth: 0,
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
display: '-webkit-box',
|
|
|
|
|
|
WebkitLineClamp: 2,
|
|
|
|
|
|
WebkitBoxOrient: 'vertical',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selectedReply}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={selectNextReply}
|
2026-05-21 15:50:55 -03:00
|
|
|
|
title="Próxima resposta"
|
2026-05-18 19:11:01 -03:00
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
|
fontWeight: 900,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
›
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-03-19 18:22:18 -03:00
|
|
|
|
</footer>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<aside
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
borderRadius: '26px',
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
padding: '1.2rem',
|
|
|
|
|
|
display: 'grid',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
gridTemplateRows: 'auto minmax(0, 1fr)',
|
2026-03-19 18:22:18 -03:00
|
|
|
|
gap: '1rem',
|
|
|
|
|
|
gridColumn: isWideDesktop ? 'auto' : '1 / -1',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
height: panelHeight,
|
2026-03-19 18:22:18 -03:00
|
|
|
|
minWidth: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
2026-05-18 19:11:01 -03:00
|
|
|
|
<strong style={{ fontSize: '1.05rem' }}>Comunicados e notas</strong>
|
2026-03-19 18:22:18 -03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-18 19:11:01 -03:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gap: '0.85rem',
|
|
|
|
|
|
alignContent: 'start',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
paddingRight: '0.15rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{managerMessages.map((message) => (
|
|
|
|
|
|
<article
|
|
|
|
|
|
key={message.id}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: '18px',
|
|
|
|
|
|
padding: '0.95rem',
|
|
|
|
|
|
background: 'rgba(0, 49, 80, 0.04)',
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gap: '0.4rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>{message.title}</strong>
|
|
|
|
|
|
<p style={{ margin: 0, color: 'var(--color-text-soft)', lineHeight: 1.5 }}>
|
|
|
|
|
|
{message.text}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
2026-05-21 15:50:55 -03:00
|
|
|
|
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Anotação rápida</span>
|
2026-05-18 19:11:01 -03:00
|
|
|
|
<textarea
|
|
|
|
|
|
value={noteDraft}
|
|
|
|
|
|
onChange={(event) => setNoteDraft(event.target.value)}
|
2026-05-21 15:50:55 -03:00
|
|
|
|
placeholder="Ex: cliente pediu retorno após as 15h"
|
2026-05-18 19:11:01 -03:00
|
|
|
|
rows={4}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
|
padding: '0.85rem 0.9rem',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
color: 'var(--color-text)',
|
|
|
|
|
|
resize: 'none',
|
|
|
|
|
|
outline: 'none',
|
|
|
|
|
|
lineHeight: 1.45,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={saveNote}
|
2026-05-20 11:37:29 -03:00
|
|
|
|
disabled={!currentUserId}
|
2026-03-19 18:22:18 -03:00
|
|
|
|
style={{
|
2026-05-18 19:11:01 -03:00
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: '18px',
|
|
|
|
|
|
padding: '0.95rem 1rem',
|
|
|
|
|
|
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontWeight: 800,
|
2026-05-20 11:37:29 -03:00
|
|
|
|
opacity: currentUserId ? 1 : 0.55,
|
2026-03-19 18:22:18 -03:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-05-21 15:50:55 -03:00
|
|
|
|
Salvar anotação
|
2026-05-18 19:11:01 -03:00
|
|
|
|
</button>
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
2026-05-20 11:37:29 -03:00
|
|
|
|
{notesError ? (
|
|
|
|
|
|
<span style={{ color: '#b42318', fontWeight: 700 }}>{notesError}</span>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
2026-05-18 19:11:01 -03:00
|
|
|
|
<div style={{ display: 'grid', gap: '0.55rem' }}>
|
|
|
|
|
|
{notes.length ? (
|
|
|
|
|
|
notes.map((note) => (
|
|
|
|
|
|
<article
|
|
|
|
|
|
key={note.id}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
|
borderRadius: '16px',
|
|
|
|
|
|
padding: '0.8rem',
|
|
|
|
|
|
background: '#fff',
|
2026-05-20 11:37:29 -03:00
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gap: '0.35rem',
|
2026-05-18 19:11:01 -03:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-05-20 11:37:29 -03:00
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
|
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.82rem' }}>
|
|
|
|
|
|
{formatMessageTime(new Date(note.created_at).getTime())}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => removeNote(note.id)}
|
2026-05-21 15:50:55 -03:00
|
|
|
|
title="Excluir anotação"
|
2026-05-20 11:37:29 -03:00
|
|
|
|
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>
|
2026-05-18 19:11:01 -03:00
|
|
|
|
</article>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
2026-05-21 15:50:55 -03:00
|
|
|
|
<span style={{ color: 'var(--color-text-soft)' }}>Nenhuma anotação salva.</span>
|
2026-05-18 19:11:01 -03:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-19 18:22:18 -03:00
|
|
|
|
</aside>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|