omnichannel-frontend/src/modules/home/components/MessagesWorkspace.jsx

549 lines
16 KiB
React
Raw Normal View History

import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
const WORKSPACE_HEIGHT = 660;
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>
);
}
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>
);
}
function buildSuggestedReplies(conversation) {
const lastMessage = conversation?.lastMessage || conversation?.messages?.at(-1)?.text || '';
const firstName = conversation?.name?.split(' ')?.[0] || 'voce';
const lowerContext = lastMessage.toLowerCase();
if (
lowerContext.includes('fatura') ||
lowerContext.includes('cobranca') ||
lowerContext.includes('pagamento')
) {
return [
`${firstName}, vou conferir os dados financeiros e ja te retorno com a posicao correta.`,
'Recebi sua mensagem sobre cobranca. Vou validar o historico antes de seguir com a orientacao.',
'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 [
`${firstName}, vou validar seu cadastro e confirmar se a alteracao ja foi registrada.`,
'Para seguir com a atualizacao, me confirme por favor os dados que precisam ser ajustados.',
'Entendi. Vou verificar o cadastro atual e te retorno com o proximo passo.',
];
}
if (
lowerContext.includes('ligar') ||
lowerContext.includes('telefone') ||
lowerContext.includes('retorno')
) {
return [
`${firstName}, consigo organizar esse retorno. Qual o melhor horario para contato?`,
'Vou registrar sua solicitacao e direcionar o retorno para o time responsavel.',
'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.`,
'Entendi. Vou analisar as informacoes do atendimento e retorno com o melhor encaminhamento.',
'Posso acionar o time responsavel e te atualizar por aqui assim que tiver uma posicao.',
];
}
export function MessagesWorkspace({
conversations,
activeConversationId,
onSelectConversation,
isWideDesktop = false,
isDesktop = false,
isTablet = false,
isMobile = false,
}) {
const navigate = useNavigate();
const recentConversations = conversations.slice(0, 3);
const activeConversation =
recentConversations.find((conversation) => conversation.id === activeConversationId) ||
recentConversations[0] ||
conversations[0];
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('');
const [notes, setNotes] = useState(() => {
try {
return JSON.parse(window.localStorage.getItem('agentNotes') || '[]');
} catch {
return [];
}
});
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',
title: 'Atualizacao de script',
text: 'Use o novo roteiro de confirmacao de dados em atendimentos financeiros.',
},
];
useEffect(() => {
setSelectedReplyIndex(0);
}, [safeActiveConversation.id]);
useEffect(() => {
window.localStorage.setItem('agentNotes', JSON.stringify(notes));
}, [notes]);
function selectPreviousReply() {
setSelectedReplyIndex((current) =>
current === 0 ? suggestedReplies.length - 1 : current - 1,
);
}
function selectNextReply() {
setSelectedReplyIndex((current) => (current + 1) % suggestedReplies.length);
}
function saveNote() {
const text = noteDraft.trim();
if (!text) return;
setNotes((current) => [
{
id: Date.now(),
text,
time: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }),
},
...current,
]);
setNoteDraft('');
}
const gridTemplateColumns = isMobile
? '1fr'
: isWideDesktop
? 'minmax(240px, 0.95fr) minmax(360px, 1.8fr) minmax(220px, 0.8fr)'
: isDesktop || isTablet
? 'minmax(260px, 320px) minmax(0, 1fr)'
: '1fr';
const panelHeight = isMobile ? 'auto' : WORKSPACE_HEIGHT;
return (
<div
style={{
display: 'grid',
gridTemplateColumns,
gap: '1rem',
alignItems: 'stretch',
}}
>
<section
style={{
background: '#fff',
borderRadius: '26px',
border: '1px solid var(--color-border)',
padding: '1rem',
display: 'grid',
gap: '0.75rem',
alignContent: 'start',
height: panelHeight,
minWidth: 0,
}}
>
<div>
<strong style={{ fontSize: '1.05rem' }}>Conversas</strong>
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
Ultimos 3 atendimentos em tempo real.
</p>
</div>
{recentConversations.map((conversation) => {
const isActive = conversation.id === safeActiveConversation.id;
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} />
<UnreadBadge count={conversation.unread} />
</div>
<span style={{ color: 'var(--color-text-soft)' }}>{conversation.lastMessage}</span>
</button>
);
})}
{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}
</section>
<section
style={{
background: '#fff',
borderRadius: '26px',
border: '1px solid var(--color-border)',
display: 'grid',
gridTemplateRows: 'auto minmax(0, 1fr) auto',
height: panelHeight,
minHeight: isMobile ? 580 : 'auto',
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>
<strong style={{ display: 'block', fontSize: '1.08rem' }}>
{safeActiveConversation.name}
</strong>
<span style={{ color: 'var(--color-text-soft)' }}>
{safeActiveConversation.status === 'online' ? 'Online agora' : 'Offline'}
</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',
overflowY: 'auto',
background:
'linear-gradient(180deg, rgba(245, 248, 251, 0.45), rgba(255, 255, 255, 0.9))',
}}
>
{safeActiveConversation.messages.map((message) => {
const isAgent = message.from === 'agent';
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)',
}}
>
{message.text}
</div>
);
})}
</div>
<footer
style={{
padding: '0.85rem 1.25rem 1rem',
borderTop: '1px solid var(--color-border)',
display: 'grid',
gap: '0.65rem',
}}
>
<strong style={{ display: 'block', fontSize: '0.94rem' }}>Resposta sugerida</strong>
<div
style={{
display: 'grid',
gridTemplateColumns: '40px minmax(0, 1fr) 40px',
gap: '0.6rem',
alignItems: 'stretch',
}}
>
<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"
onClick={() => navigate('/chat')}
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}
title="Proxima resposta"
style={{
border: '1px solid var(--color-border)',
borderRadius: '14px',
background: '#fff',
color: 'var(--color-primary)',
fontWeight: 900,
}}
>
</button>
</div>
</footer>
</section>
<aside
style={{
background: '#fff',
borderRadius: '26px',
border: '1px solid var(--color-border)',
padding: '1.2rem',
display: 'grid',
gridTemplateRows: 'auto minmax(0, 1fr)',
gap: '1rem',
gridColumn: isWideDesktop ? 'auto' : '1 / -1',
height: panelHeight,
minWidth: 0,
}}
>
<div>
<strong style={{ fontSize: '1.05rem' }}>Comunicados e notas</strong>
</div>
<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' }}>
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Anotacao rapida</span>
<textarea
value={noteDraft}
onChange={(event) => setNoteDraft(event.target.value)}
placeholder="Ex: cliente pediu retorno apos as 15h"
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}
style={{
border: 'none',
borderRadius: '18px',
padding: '0.95rem 1rem',
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
color: '#fff',
fontWeight: 800,
}}
>
Salvar anotacao
</button>
<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',
}}
>
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.82rem' }}>
{note.time}
</span>
<p style={{ margin: '0.35rem 0 0', lineHeight: 1.45 }}>{note.text}</p>
</article>
))
) : (
<span style={{ color: 'var(--color-text-soft)' }}>Nenhuma anotacao salva.</span>
)}
</div>
</div>
</aside>
</div>
);
}