FEAT/FIX: Melhor funcionamento da deifinção de usuários e correção de acentuação
- Front completamente corrigido para tudo estar acentuado - Melhora na exibição de áreas/especialidades eusuários;
This commit is contained in:
parent
c61a913c38
commit
4d287faf28
@ -289,17 +289,17 @@ export function NewAttendancePage() {
|
||||
|
||||
async function handleStartAttendance() {
|
||||
if (!canStartAttendance) {
|
||||
setError('Informe um numero de WhatsApp para iniciar o atendimento.');
|
||||
setError('Informe um número de WhatsApp para iniciar o atendimento.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedTemplateId) {
|
||||
setError('Selecione uma mensagem pre-aprovada para iniciar o atendimento.');
|
||||
setError('Selecione uma mensagem pré-aprovada para iniciar o atendimento.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentUserId) {
|
||||
setError('Nao foi possivel identificar o usuario logado.');
|
||||
setError('Não foi possível identificar o usuário logado.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -367,7 +367,7 @@ export function NewAttendancePage() {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Criacao rapida de atendimento
|
||||
Criação rápida de atendimento
|
||||
</div>
|
||||
<Link
|
||||
to="/home"
|
||||
@ -412,8 +412,8 @@ export function NewAttendancePage() {
|
||||
<div>
|
||||
<strong style={{ display: 'block', fontSize: '1.18rem' }}>Novo atendimento</strong>
|
||||
<p style={{ margin: '0.45rem 0 0', color: 'var(--color-text-soft)', lineHeight: 1.6 }}>
|
||||
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.
|
||||
Informe um contato de WhatsApp ou selecione alguém da agenda para iniciar o atendimento.
|
||||
Para conversas novas, o primeiro envio usa uma mensagem pré-aprovada da Meta.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -421,7 +421,7 @@ export function NewAttendancePage() {
|
||||
type="search"
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
placeholder="Buscar contato salvo por nome ou numero"
|
||||
placeholder="Buscar contato salvo por nome ou número"
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
@ -467,7 +467,7 @@ export function NewAttendancePage() {
|
||||
{channel.label}
|
||||
</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
{isDisabled ? 'Canal em construcao.' : 'Inicia uma conversa pelo WhatsApp.'}
|
||||
{isDisabled ? 'Canal em construção.' : 'Inicia uma conversa pelo WhatsApp.'}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
@ -482,7 +482,7 @@ export function NewAttendancePage() {
|
||||
}}
|
||||
>
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Pais</span>
|
||||
<span style={{ fontWeight: 600 }}>País</span>
|
||||
<select
|
||||
value={selectedCountryId}
|
||||
onChange={(event) => {
|
||||
@ -510,7 +510,7 @@ export function NewAttendancePage() {
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Numero do WhatsApp</span>
|
||||
<span style={{ fontWeight: 600 }}>Número do WhatsApp</span>
|
||||
<input
|
||||
type="text"
|
||||
value={form.phone}
|
||||
@ -569,7 +569,7 @@ export function NewAttendancePage() {
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Mensagem pre-aprovada</span>
|
||||
<span style={{ fontWeight: 600 }}>Mensagem pré-aprovada</span>
|
||||
<select
|
||||
value={selectedTemplateId}
|
||||
onChange={(event) => setSelectedTemplateId(event.target.value)}
|
||||
@ -610,7 +610,7 @@ export function NewAttendancePage() {
|
||||
) : null}
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Observacao</span>
|
||||
<span style={{ fontWeight: 600 }}>Observação</span>
|
||||
<textarea
|
||||
rows={5}
|
||||
value={form.note}
|
||||
@ -665,10 +665,10 @@ export function NewAttendancePage() {
|
||||
Canal: {selectedChannel.label}
|
||||
</span>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.74)' }}>
|
||||
Numero: {buildInternationalPhone(form.phone, selectedCountryId) ? `+${buildInternationalPhone(form.phone, selectedCountryId)}` : 'Nao informado'}
|
||||
Número: {buildInternationalPhone(form.phone, selectedCountryId) ? `+${buildInternationalPhone(form.phone, selectedCountryId)}` : 'Não informado'}
|
||||
</span>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.74)' }}>
|
||||
Empresa: {form.company || 'Nao informada'}
|
||||
Empresa: {form.company || 'Não informada'}
|
||||
</span>
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.74)' }}>
|
||||
Origem: {selectedContactId ? 'Agenda' : 'Novo contato'}
|
||||
@ -685,9 +685,9 @@ export function NewAttendancePage() {
|
||||
gap: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<strong>Proxima rota</strong>
|
||||
<strong>Próxima rota</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
O contato sera salvo, o template sera enviado e a conversa abrira atribuida a voce no chat.
|
||||
O contato será salvo, o template será enviado e a conversa abrirá atribuída a você no chat.
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginTop: '0.4rem' }}>
|
||||
{selectedContactId ? (
|
||||
@ -703,7 +703,7 @@ export function NewAttendancePage() {
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Limpar selecao
|
||||
Limpar seleção
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
|
||||
@ -32,7 +32,7 @@ export function useLogin() {
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
navigate('/home', { replace: true });
|
||||
} catch {
|
||||
setError('Nao foi possivel concluir o login Microsoft.');
|
||||
setError('Não foi possível concluir o login Microsoft.');
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
|
||||
@ -147,7 +147,7 @@ export function LoginPage() {
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Use seu usuario corporativo para acessar o MVP com Active Directory ou Microsoft.
|
||||
Use seu usuário corporativo para acessar o MVP com Active Directory ou Microsoft.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,7 +4,7 @@ async function parseJsonResponse(response) {
|
||||
const data = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message || 'Nao foi possivel autenticar.');
|
||||
throw new Error(data?.message || 'Não foi possível autenticar.');
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
@ -21,7 +21,7 @@ export function CallHeader({ isMobile = false }) {
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Ligacao ativa
|
||||
Ligação ativa
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@ -98,7 +98,7 @@ export function CallPage() {
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: 'Numero', value: activeCall.number },
|
||||
{ label: 'Número', value: activeCall.number },
|
||||
{ label: 'Canal original', value: 'Atendimento omnichannel' },
|
||||
{ label: 'Responsável atual', value: 'Ana Camolesi' },
|
||||
].map((item) => (
|
||||
@ -164,8 +164,8 @@ export function CallPage() {
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Voce esta em uma ligacao ativa com a cliente. Os controles abaixo sao visuais
|
||||
neste MVP e ajudam a demonstrar a experiencia de voz do produto.
|
||||
Você está em uma ligação ativa com a cliente. Os controles abaixo são visuais
|
||||
neste MVP e ajudam a demonstrar a experiência de voz do produto.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -187,7 +187,7 @@ export function CallPage() {
|
||||
color: 'rgba(255, 255, 255, 0.72)',
|
||||
}}
|
||||
>
|
||||
Qualidade da chamada: Estavel
|
||||
Qualidade da chamada: Estável
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@ -197,7 +197,7 @@ export function CallPage() {
|
||||
color: 'rgba(255, 255, 255, 0.72)',
|
||||
}}
|
||||
>
|
||||
Gravacao mock: Habilitada
|
||||
Gravação mock: Habilitada
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -40,7 +40,7 @@ export function ChatTransferPanel({
|
||||
<div>
|
||||
<strong style={{ display: 'block', fontSize: '1.06rem' }}>Transferir atendimento</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
Reencaminhe a conversa para a area ideal.
|
||||
Reencaminhe a conversa para a especialidade ideal.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@ -58,7 +58,7 @@ export function ChatTransferPanel({
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Area</span>
|
||||
<span style={{ fontWeight: 600 }}>Especialidade</span>
|
||||
<select value={transferArea} onChange={(event) => setTransferArea(event.target.value)} style={fieldStyle}>
|
||||
{transferAreas.map((area) => (
|
||||
<option key={area} value={area}>
|
||||
@ -91,18 +91,18 @@ export function ChatTransferPanel({
|
||||
background: 'rgba(0, 49, 80, 0.04)',
|
||||
}}
|
||||
>
|
||||
Ao transferir para outra area, a conversa caira na fila dessa area.
|
||||
Ao transferir para outra especialidade, a conversa cairá na fila dessa especialidade.
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Observacao</span>
|
||||
<span style={{ fontWeight: 600 }}>Observação</span>
|
||||
<textarea
|
||||
rows={5}
|
||||
value={transferNote}
|
||||
onChange={(event) => setTransferNote(event.target.value)}
|
||||
placeholder="Contexto opcional para ajudar o proximo atendente."
|
||||
placeholder="Contexto opcional para ajudar o próximo atendente."
|
||||
style={{ ...fieldStyle, resize: 'vertical' }}
|
||||
/>
|
||||
</label>
|
||||
@ -119,7 +119,7 @@ export function ChatTransferPanel({
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Confirmar transferencia
|
||||
Confirmar transferência
|
||||
</button>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@ -58,7 +58,7 @@ function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) {
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Carregando midia...
|
||||
Carregando mídia...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -66,7 +66,7 @@ function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) {
|
||||
if (message.mediaError) {
|
||||
return (
|
||||
<span style={{ color: isAgent ? '#fff' : 'var(--color-text-soft)', fontWeight: 700 }}>
|
||||
Nao foi possivel carregar a midia.
|
||||
Não foi possível carregar a mídia.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -394,7 +394,7 @@ export function ChatWindow({
|
||||
}}
|
||||
>
|
||||
<strong style={{ display: 'block', color: 'var(--color-primary)', marginBottom: '0.25rem' }}>
|
||||
Observacao da transferencia
|
||||
Observação da transferência
|
||||
</strong>
|
||||
{transferNote}
|
||||
</div>
|
||||
@ -556,8 +556,8 @@ export function ChatWindow({
|
||||
>
|
||||
<span style={{ display: 'block' }}>
|
||||
{canAssumeChat
|
||||
? 'Este atendimento esta na fila. Assuma para responder ou transferir.'
|
||||
: assignmentLabel || 'Este atendimento esta atribuido a outro usuario.'}
|
||||
? 'Este atendimento está na fila. Assuma para responder ou transferir.'
|
||||
: assignmentLabel || 'Este atendimento está atribuído a outro usuário.'}
|
||||
</span>
|
||||
{transferNote ? (
|
||||
<span style={{ display: 'block', marginTop: '0.45rem', color: 'var(--color-text)' }}>
|
||||
|
||||
@ -10,7 +10,7 @@ function getUserId(user) {
|
||||
|
||||
function formatPhone(phone) {
|
||||
const digits = String(phone || '').replace(/\D/g, '');
|
||||
if (!digits) return 'Telefone nao disponivel';
|
||||
if (!digits) return 'Telefone não disponível';
|
||||
|
||||
if (digits.startsWith('55') && digits.length === 13) {
|
||||
return `+55 (${digits.slice(2, 4)}) ${digits.slice(4, 9)}-${digits.slice(9)}`;
|
||||
@ -164,12 +164,12 @@ export function ContactProfilePanel({ isOpen, contact, onClose, onSaved }) {
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Observacao</span>
|
||||
<span style={{ fontWeight: 600 }}>Observação</span>
|
||||
<textarea
|
||||
rows={5}
|
||||
value={form.note}
|
||||
onChange={(event) => setForm((current) => ({ ...current, note: event.target.value }))}
|
||||
placeholder="Informacoes relevantes do cliente."
|
||||
placeholder="Informações relevantes do cliente."
|
||||
style={{ ...fieldStyle, resize: 'vertical' }}
|
||||
/>
|
||||
</label>
|
||||
|
||||
@ -38,7 +38,7 @@ function getContactName(chat) {
|
||||
function getPreviewFromMessage(message) {
|
||||
if (message?.body) return message.body;
|
||||
if (message?.text) return message.text;
|
||||
if (message?.hasMedia || message?.media) return '[Midia]';
|
||||
if (message?.hasMedia || message?.media) return '[Mídia]';
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -480,7 +480,7 @@ export function useChat() {
|
||||
function updateContactPreview(contactId, preview, media) {
|
||||
updateContact(contactId, (contact) => ({
|
||||
...contact,
|
||||
preview: media ? `[Midia: ${media.filename || 'Arquivo'}]` : preview,
|
||||
preview: media ? `[Mídia: ${media.filename || 'Arquivo'}]` : preview,
|
||||
time: 'Agora',
|
||||
unread: 0,
|
||||
lastMessageFromMe: true,
|
||||
@ -523,7 +523,7 @@ export function useChat() {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/whatsapp/media/${encodeURIComponent(contactId)}/${encodeURIComponent(messageId)}`,
|
||||
);
|
||||
if (!response.ok) throw new Error('Falha ao carregar midia.');
|
||||
if (!response.ok) throw new Error('Falha ao carregar mídia.');
|
||||
const media = await response.json();
|
||||
setMessagesByContact((current) => ({
|
||||
...current,
|
||||
@ -536,7 +536,7 @@ export function useChat() {
|
||||
...current,
|
||||
[contactId]: (current[contactId] || []).map((message) =>
|
||||
message.id === messageId
|
||||
? { ...message, mediaLoading: false, mediaError: error.message || 'Erro ao carregar midia.' }
|
||||
? { ...message, mediaLoading: false, mediaError: error.message || 'Erro ao carregar mídia.' }
|
||||
: message,
|
||||
),
|
||||
}));
|
||||
@ -559,7 +559,7 @@ export function useChat() {
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Nao foi possivel assumir o atendimento.');
|
||||
if (!response.ok) throw new Error('Não foi possível assumir o atendimento.');
|
||||
const assignment = await response.json();
|
||||
updateContact(contactId, (contact) => ({
|
||||
...contact,
|
||||
@ -581,7 +581,7 @@ export function useChat() {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Nao foi possivel sair do atendimento.');
|
||||
if (!response.ok) throw new Error('Não foi possível sair do atendimento.');
|
||||
const assignment = await response.json();
|
||||
updateContact(activeContactId, (contact) => ({
|
||||
...contact,
|
||||
@ -636,7 +636,7 @@ export function useChat() {
|
||||
...current,
|
||||
[contactId]: mergeMessageList(current[contactId] || [], newMessage),
|
||||
}));
|
||||
updateContactPreview(contactId, trimmed || '[Midia]', media);
|
||||
updateContactPreview(contactId, trimmed || '[Mídia]', media);
|
||||
if (contactId === activeContactId) {
|
||||
setDraft('');
|
||||
}
|
||||
@ -672,7 +672,7 @@ export function useChat() {
|
||||
const areaId = selectedTransferArea?.id;
|
||||
|
||||
if (!areaId) {
|
||||
setApiError('Selecione uma area valida para transferencia.');
|
||||
setApiError('Selecione uma especialidade válida para transferência.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -694,14 +694,14 @@ export function useChat() {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
setApiError('Nao foi possivel transferir o atendimento.');
|
||||
setApiError('Não foi possível transferir o atendimento.');
|
||||
return;
|
||||
}
|
||||
|
||||
const assignment = await response.json();
|
||||
const transferMessage = targetUserId
|
||||
? `Transferencia solicitada para ${transferArea}. Obs: ${note || 'Sem observacao.'}`
|
||||
: `Transferencia enviada para a fila de ${transferArea}. Obs: ${note || 'Sem observacao.'}`;
|
||||
? `Transferência solicitada para ${transferArea}. Obs: ${note || 'Sem observação.'}`
|
||||
: `Transferência enviada para a fila de ${transferArea}. Obs: ${note || 'Sem observação.'}`;
|
||||
|
||||
setMessagesByContact((current) => ({
|
||||
...current,
|
||||
|
||||
@ -27,7 +27,7 @@ export const chatContacts = [
|
||||
time: '08:15',
|
||||
unread: 1,
|
||||
messages: [
|
||||
{ id: 1, sender: 'customer', text: 'Recebi a cobranca em duplicidade.' },
|
||||
{ id: 1, sender: 'customer', text: 'Recebi a cobrança em duplicidade.' },
|
||||
{ id: 2, sender: 'agent', text: 'Vou analisar isso agora para você.' },
|
||||
{ id: 3, sender: 'customer', text: 'Pode me ligar em 10 minutos?' },
|
||||
],
|
||||
@ -67,7 +67,7 @@ export function getMockReply(contactName) {
|
||||
const replies = [
|
||||
`Perfeito, obrigado pelo retorno, ${contactName.split(' ')[0]}.`,
|
||||
'Tudo bem, fico no aguardo dessa confirmação.',
|
||||
'Entendi. Se precisar, posso encaminhar para a área responsável.',
|
||||
'Entendi. Se precisar, posso encaminhar para a especialidade responsável.',
|
||||
];
|
||||
|
||||
return replies[Math.floor(Math.random() * replies.length)];
|
||||
|
||||
@ -42,7 +42,7 @@ export function CallsWorkspace({ calls, isWideDesktop = false, isDesktop = false
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Nova ligacao
|
||||
Nova ligação
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export function HomeTopbar({
|
||||
const [currentDateTime, setCurrentDateTime] = useState(() => formatCurrentDateTime(new Date()));
|
||||
const tabs = [
|
||||
{ id: 'messages', label: 'Mensagens' },
|
||||
{ id: 'calls', label: 'Ligacoes' },
|
||||
{ id: 'calls', label: 'Ligações' },
|
||||
];
|
||||
|
||||
const gridTemplateColumns = isMobile
|
||||
|
||||
@ -56,7 +56,7 @@ function UnreadBadge({ count }) {
|
||||
|
||||
function buildSuggestedReplies(conversation) {
|
||||
const lastMessage = conversation?.lastMessage || conversation?.messages?.at(-1)?.text || '';
|
||||
const firstName = conversation?.name?.split(' ')?.[0] || 'voce';
|
||||
const firstName = conversation?.name?.split(' ')?.[0] || 'você';
|
||||
const lowerContext = lastMessage.toLowerCase();
|
||||
|
||||
if (
|
||||
@ -65,8 +65,8 @@ function buildSuggestedReplies(conversation) {
|
||||
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.',
|
||||
`${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.',
|
||||
'Consigo te ajudar com isso. Pode me confirmar o CPF/CNPJ ou protocolo vinculado ao atendimento?',
|
||||
];
|
||||
}
|
||||
@ -77,9 +77,9 @@ function buildSuggestedReplies(conversation) {
|
||||
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.',
|
||||
`${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.',
|
||||
];
|
||||
}
|
||||
|
||||
@ -89,16 +89,16 @@ function buildSuggestedReplies(conversation) {
|
||||
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.',
|
||||
`${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.',
|
||||
'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.',
|
||||
'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.',
|
||||
];
|
||||
}
|
||||
|
||||
@ -171,8 +171,8 @@ export function MessagesWorkspace({
|
||||
},
|
||||
{
|
||||
id: 'script',
|
||||
title: 'Atualizacao de script',
|
||||
text: 'Use o novo roteiro de confirmacao de dados em atendimentos financeiros.',
|
||||
title: 'Atualização de script',
|
||||
text: 'Use o novo roteiro de confirmação de dados em atendimentos financeiros.',
|
||||
},
|
||||
];
|
||||
|
||||
@ -279,7 +279,7 @@ export function MessagesWorkspace({
|
||||
<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.
|
||||
Últimos 3 atendimentos em tempo real.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -524,7 +524,7 @@ export function MessagesWorkspace({
|
||||
<button
|
||||
type="button"
|
||||
onClick={selectNextReply}
|
||||
title="Proxima resposta"
|
||||
title="Próxima resposta"
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '14px',
|
||||
@ -585,11 +585,11 @@ export function MessagesWorkspace({
|
||||
))}
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Anotacao rapida</span>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Anotação rápida</span>
|
||||
<textarea
|
||||
value={noteDraft}
|
||||
onChange={(event) => setNoteDraft(event.target.value)}
|
||||
placeholder="Ex: cliente pediu retorno apos as 15h"
|
||||
placeholder="Ex: cliente pediu retorno após as 15h"
|
||||
rows={4}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
@ -618,7 +618,7 @@ export function MessagesWorkspace({
|
||||
opacity: currentUserId ? 1 : 0.55,
|
||||
}}
|
||||
>
|
||||
Salvar anotacao
|
||||
Salvar anotação
|
||||
</button>
|
||||
|
||||
{notesError ? (
|
||||
@ -646,7 +646,7 @@ export function MessagesWorkspace({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeNote(note.id)}
|
||||
title="Excluir anotacao"
|
||||
title="Excluir anotação"
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 999,
|
||||
@ -664,7 +664,7 @@ export function MessagesWorkspace({
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>Nenhuma anotacao salva.</span>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>Nenhuma anotação salva.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -22,7 +22,7 @@ function toHomeConversation(contact, messages = []) {
|
||||
messages: messages.map((message) => ({
|
||||
id: message.id,
|
||||
from: message.sender === 'agent' ? 'agent' : 'customer',
|
||||
text: message.text || (message.hasMedia ? '[Midia]' : ''),
|
||||
text: message.text || (message.hasMedia ? '[Mídia]' : ''),
|
||||
timestamp: message.timestamp,
|
||||
})),
|
||||
};
|
||||
|
||||
@ -45,12 +45,12 @@ export function UnassignedHomePage() {
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Acesso aguardando configuracao
|
||||
Acesso aguardando configuração
|
||||
</span>
|
||||
<h1 style={{ margin: 0, fontSize: '2rem' }}>Seu usuario ainda nao tem atribuicoes</h1>
|
||||
<h1 style={{ margin: 0, fontSize: '2rem' }}>Seu usuário ainda não tem atribuições</h1>
|
||||
<p style={{ margin: 0, color: 'var(--color-text-soft)', lineHeight: 1.7 }}>
|
||||
O login foi realizado, mas um administrador ainda precisa vincular seu usuario a um
|
||||
perfil de acesso e a uma area operacional antes de liberar a plataforma.
|
||||
O login foi realizado, mas um administrador ainda precisa vincular seu usuário a um
|
||||
perfil de acesso e uma especialidade operacional antes de liberar a plataforma.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -63,8 +63,8 @@ export function UnassignedHomePage() {
|
||||
gap: '0.65rem',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>Usuario autenticado</span>
|
||||
<strong>{user?.name || user?.username || 'Usuario'}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>Usuário autenticado</span>
|
||||
<strong>{user?.name || user?.username || 'Usuário'}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
{user?.email || user?.username || 'Sem email informado'}
|
||||
</span>
|
||||
|
||||
@ -3,7 +3,7 @@ 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.');
|
||||
if (!response.ok) throw new Error('Falha ao carregar anotações.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ export async function createAgentNote(userId, text) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId, text }),
|
||||
});
|
||||
if (!response.ok) throw new Error('Falha ao salvar anotacao.');
|
||||
if (!response.ok) throw new Error('Falha ao salvar anotação.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@ -22,6 +22,6 @@ export async function deleteAgentNote(userId, noteId) {
|
||||
`${API_BASE_URL}/agent/notes/${encodeURIComponent(noteId)}?userId=${encodeURIComponent(userId)}`,
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
if (!response.ok) throw new Error('Falha ao excluir anotacao.');
|
||||
if (!response.ok) throw new Error('Falha ao excluir anotação.');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ export const conversations = [
|
||||
];
|
||||
|
||||
export const actionItems = [
|
||||
{ title: 'Área atual', value: 'Suporte' },
|
||||
{ title: 'Especialidade atual', value: 'Suporte' },
|
||||
{ title: 'SLA restante', value: '18 min' },
|
||||
{ title: 'Prioridade', value: 'Alta' },
|
||||
];
|
||||
|
||||
@ -6,32 +6,32 @@ const navigationBySection = {
|
||||
supervisor: [
|
||||
{ id: 'dashboard', label: 'Dashboard', count: null },
|
||||
{ id: 'queues', label: 'Filas em tempo real', count: 42 },
|
||||
{ id: 'areas', label: 'Areas supervisionadas', count: 3 },
|
||||
{ id: 'areas', label: 'Especialidades supervisionadas', count: 3 },
|
||||
{ id: 'agents', label: 'Agentes online', count: 18 },
|
||||
{ id: 'reports', label: 'Relatorios', count: null },
|
||||
{ id: 'reports', label: 'Relatórios', count: null },
|
||||
],
|
||||
admin: [
|
||||
{ id: 'home', label: 'Home' },
|
||||
{ id: 'today', label: 'Operacao' },
|
||||
{ id: 'today', label: 'Operação' },
|
||||
{ type: 'separator' },
|
||||
{ id: 'users-access', label: 'Usuarios & Acessos' },
|
||||
{ id: 'users-access', label: 'Usuários & Acessos' },
|
||||
{ id: 'templates', label: 'Templates' },
|
||||
{ id: 'knowledge', label: 'Base de conhecimento IA' },
|
||||
{ id: 'audit', label: 'Auditoria' },
|
||||
{ id: 'channels', label: 'Canais' },
|
||||
{ type: 'separator' },
|
||||
{ id: 'attendance', label: 'Atendimento', path: '/chat' },
|
||||
{ id: 'attendance', label: 'Atendimento' },
|
||||
{ id: 'new-attendance', label: 'Abrir Atendimento', path: '/new-attendance' },
|
||||
{ id: 'mass-message', label: 'Disparo em Massa' },
|
||||
{ id: 'contacts', label: 'Contatos' },
|
||||
{ type: 'separator' },
|
||||
{ id: 'settings', label: 'Configuracoes' },
|
||||
{ id: 'settings', label: 'Configurações' },
|
||||
],
|
||||
};
|
||||
|
||||
const actionLabelBySection = {
|
||||
supervisor: '+ Redistribuir atendimento',
|
||||
admin: '+ Nova configuracao',
|
||||
admin: '+ Nova configuração',
|
||||
};
|
||||
|
||||
export function ManagementLayout({
|
||||
@ -48,7 +48,7 @@ export function ManagementLayout({
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const navItems = navigationBySection[activeSection] || navigationBySection.supervisor;
|
||||
const actionLabel = actionLabelBySection[activeSection] || '+ Nova acao';
|
||||
const actionLabel = actionLabelBySection[activeSection] || '+ Nova ação';
|
||||
|
||||
function handleLogout() {
|
||||
clearSession();
|
||||
@ -242,7 +242,7 @@ export function ManagementLayout({
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<strong style={{ display: 'block' }}>{profileLabel}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.92rem' }}>
|
||||
Ambiente de gestao
|
||||
Ambiente de gestão
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
330
src/modules/management/components/OperationalDashboard.jsx
Normal file
330
src/modules/management/components/OperationalDashboard.jsx
Normal file
@ -0,0 +1,330 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { DataPanel } from './DataPanel';
|
||||
import { MetricGrid } from './MetricGrid';
|
||||
|
||||
const selectStyle = {
|
||||
width: '100%',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '14px',
|
||||
padding: '0.75rem 0.85rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-text)',
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
const areaOptions = [
|
||||
{ value: 'all', label: 'Todas as especialidades' },
|
||||
{ value: 'Suporte', label: 'Suporte' },
|
||||
{ value: 'Comercial', label: 'Comercial' },
|
||||
{ value: 'Financeiro', label: 'Financeiro' },
|
||||
];
|
||||
|
||||
const operationMockByArea = {
|
||||
all: {
|
||||
kpis: [
|
||||
{ label: 'Finalizados hoje', value: '126', detail: 'todos os canais' },
|
||||
{ label: 'Em aberto agora', value: '42', detail: '18 em atendimento' },
|
||||
{ label: 'Na fila', value: '12', detail: 'aguardando assumir' },
|
||||
{ label: 'Tempo médio do dia', value: '8m 12s', detail: 'TMA operacional' },
|
||||
{ label: 'Atendentes online', value: '8 de 11', detail: '3 em pausa/offline' },
|
||||
],
|
||||
team: [
|
||||
{ id: 'ana', name: 'Ana Camolesi', status: 'online', open: 4, lastClosed: '6 min' },
|
||||
{ id: 'rafael', name: 'Rafael Lopes', status: 'online', open: 3, lastClosed: '12 min' },
|
||||
{ id: 'marina', name: 'Marina Alves', status: 'paused', open: 1, lastClosed: '18 min' },
|
||||
{ id: 'camila', name: 'Camila Rocha', status: 'online', open: 5, lastClosed: '22 min' },
|
||||
{ id: 'joao', name: 'Joao Pedro', status: 'offline', open: 0, lastClosed: '1h 05min' },
|
||||
],
|
||||
queue: [
|
||||
{ id: 'q1', channel: 'WhatsApp', contact: 'Maria Souza', waitingMinutes: 24 },
|
||||
{ id: 'q2', channel: 'Email', contact: 'Empresa Alpha', waitingMinutes: 18 },
|
||||
{ id: 'q3', channel: 'SMS', contact: 'Carlos Nunes', waitingMinutes: 11 },
|
||||
{ id: 'q4', channel: 'WhatsApp', contact: 'Grupo Solaris', waitingMinutes: 7 },
|
||||
],
|
||||
hourly: [8, 11, 15, 13, 19, 22, 18, 26, 24, 31, 28],
|
||||
},
|
||||
Suporte: {
|
||||
kpis: [
|
||||
{ label: 'Finalizados hoje', value: '58', detail: 'suporte técnico' },
|
||||
{ label: 'Em aberto agora', value: '21', detail: '9 em atendimento' },
|
||||
{ label: 'Na fila', value: '7', detail: 'aguardando assumir' },
|
||||
{ label: 'Tempo médio do dia', value: '9m 04s', detail: 'TMA suporte' },
|
||||
{ label: 'Atendentes online', value: '4 de 6', detail: '1 pausa, 1 offline' },
|
||||
],
|
||||
team: [
|
||||
{ id: 'ana', name: 'Ana Camolesi', status: 'online', open: 4, lastClosed: '6 min' },
|
||||
{ id: 'rafael', name: 'Rafael Lopes', status: 'online', open: 3, lastClosed: '12 min' },
|
||||
{ id: 'beatriz', name: 'Beatriz Lima', status: 'paused', open: 2, lastClosed: '21 min' },
|
||||
{ id: 'pedro', name: 'Pedro Santos', status: 'offline', open: 0, lastClosed: '48 min' },
|
||||
],
|
||||
queue: [
|
||||
{ id: 's1', channel: 'WhatsApp', contact: 'Maria Souza', waitingMinutes: 24 },
|
||||
{ id: 's2', channel: 'WhatsApp', contact: 'Bruno Matos', waitingMinutes: 15 },
|
||||
{ id: 's3', channel: 'Email', contact: 'TI Alpha', waitingMinutes: 9 },
|
||||
],
|
||||
hourly: [4, 5, 7, 6, 10, 12, 9, 15, 13, 18, 16],
|
||||
},
|
||||
Comercial: {
|
||||
kpis: [
|
||||
{ label: 'Finalizados hoje', value: '39', detail: 'leads e propostas' },
|
||||
{ label: 'Em aberto agora', value: '13', detail: '6 em atendimento' },
|
||||
{ label: 'Na fila', value: '3', detail: 'aguardando assumir' },
|
||||
{ label: 'Tempo médio do dia', value: '7m 38s', detail: 'TMA comercial' },
|
||||
{ label: 'Atendentes online', value: '3 de 3', detail: 'time completo' },
|
||||
],
|
||||
team: [
|
||||
{ id: 'camila', name: 'Camila Rocha', status: 'online', open: 5, lastClosed: '22 min' },
|
||||
{ id: 'lucas', name: 'Lucas Nunes', status: 'online', open: 4, lastClosed: '14 min' },
|
||||
{ id: 'helena', name: 'Helena Costa', status: 'online', open: 4, lastClosed: '31 min' },
|
||||
],
|
||||
queue: [
|
||||
{ id: 'c1', channel: 'WhatsApp', contact: 'Grupo Solaris', waitingMinutes: 17 },
|
||||
{ id: 'c2', channel: 'Email', contact: 'Empresa Beta', waitingMinutes: 10 },
|
||||
{ id: 'c3', channel: 'SMS', contact: 'Renata Prado', waitingMinutes: 4 },
|
||||
],
|
||||
hourly: [2, 4, 6, 5, 8, 7, 8, 10, 11, 13, 12],
|
||||
},
|
||||
Financeiro: {
|
||||
kpis: [
|
||||
{ label: 'Finalizados hoje', value: '29', detail: 'faturas e boletos' },
|
||||
{ label: 'Em aberto agora', value: '8', detail: '3 em atendimento' },
|
||||
{ label: 'Na fila', value: '2', detail: 'aguardando assumir' },
|
||||
{ label: 'Tempo médio do dia', value: '6m 51s', detail: 'TMA financeiro' },
|
||||
{ label: 'Atendentes online', value: '1 de 2', detail: '1 offline' },
|
||||
],
|
||||
team: [
|
||||
{ id: 'marina', name: 'Marina Alves', status: 'paused', open: 1, lastClosed: '18 min' },
|
||||
{ id: 'joao', name: 'Joao Pedro', status: 'offline', open: 0, lastClosed: '1h 05min' },
|
||||
{ id: 'roberto', name: 'Roberto Pera', status: 'online', open: 3, lastClosed: '9 min' },
|
||||
],
|
||||
queue: [
|
||||
{ id: 'f1', channel: 'WhatsApp', contact: 'Joao Pedro', waitingMinutes: 22 },
|
||||
{ id: 'f2', channel: 'Email', contact: 'Financeiro Omega', waitingMinutes: 8 },
|
||||
],
|
||||
hourly: [2, 2, 2, 2, 4, 5, 4, 6, 5, 7, 6],
|
||||
},
|
||||
};
|
||||
|
||||
const statusMeta = {
|
||||
online: { label: 'Online', background: 'rgba(34, 197, 94, 0.12)', color: '#15803d' },
|
||||
paused: { label: 'Pausado', background: 'rgba(229, 162, 42, 0.16)', color: '#8a5a00' },
|
||||
offline: { label: 'Offline', background: 'rgba(100, 116, 139, 0.14)', color: '#475569' },
|
||||
};
|
||||
|
||||
const channelColors = {
|
||||
WhatsApp: { background: 'rgba(43, 183, 65, 0.12)', color: '#15803d' },
|
||||
Email: { background: 'rgba(229, 162, 42, 0.16)', color: '#8a5a00' },
|
||||
SMS: { background: 'rgba(0, 164, 183, 0.12)', color: 'var(--color-primary)' },
|
||||
};
|
||||
|
||||
function Badge({ children, tone }) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
borderRadius: 999,
|
||||
padding: '0.25rem 0.6rem',
|
||||
background: tone.background,
|
||||
color: tone.color,
|
||||
fontWeight: 800,
|
||||
fontSize: '0.82rem',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function HourlyLineChart({ values }) {
|
||||
const labels = ['08h', '09h', '10h', '11h', '12h', '13h', '14h', '15h', '16h', '17h', '18h'];
|
||||
const maxValue = Math.max(...values, 1);
|
||||
const points = values
|
||||
.map((value, index) => {
|
||||
const x = (index / (values.length - 1)) * 100;
|
||||
const y = 100 - (value / maxValue) * 78 - 10;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ width: '100%', height: 260 }}>
|
||||
<polyline points={points} fill="none" stroke="var(--color-secondary)" strokeWidth="2.4" vectorEffect="non-scaling-stroke" />
|
||||
</svg>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${labels.length}, 1fr)`, gap: '0.25rem', color: 'var(--color-text-soft)', fontSize: '0.78rem', fontWeight: 700 }}>
|
||||
{labels.map((label) => (
|
||||
<span key={label} style={{ textAlign: 'center' }}>{label}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OperationalDashboard({ isDesktop, isMobile }) {
|
||||
const [selectedArea, setSelectedArea] = useState('all');
|
||||
const [assignmentTarget, setAssignmentTarget] = useState(null);
|
||||
const data = operationMockByArea[selectedArea] || operationMockByArea.all;
|
||||
|
||||
const onlineAgents = useMemo(
|
||||
() => data.team.filter((agent) => agent.status === 'online'),
|
||||
[data.team],
|
||||
);
|
||||
|
||||
const sortedQueue = useMemo(
|
||||
() => [...data.queue].sort((a, b) => b.waitingMinutes - a.waitingMinutes),
|
||||
[data.queue],
|
||||
);
|
||||
|
||||
return (
|
||||
<section style={{ display: 'grid', gap: '1rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<label style={{ display: 'grid', gap: '0.35rem', width: isMobile ? '100%' : 260 }}>
|
||||
<span style={{ fontWeight: 700 }}>Filtro por especialidade</span>
|
||||
<select value={selectedArea} onChange={(event) => setSelectedArea(event.target.value)} style={selectStyle}>
|
||||
{areaOptions.map((area) => (
|
||||
<option key={area.value} value={area.value}>{area.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<MetricGrid metrics={data.kpis} minCardWidth="160px" />
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isDesktop ? 'minmax(0, 1.15fr) minmax(360px, 0.85fr)' : '1fr', gap: '1rem', alignItems: 'start' }}>
|
||||
<DataPanel title="Painel do time" description="Status operacional em tempo real simulado.">
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 620 }}>
|
||||
<thead>
|
||||
<tr style={{ textAlign: 'left', color: 'var(--color-text-soft)', fontSize: '0.82rem' }}>
|
||||
<th style={{ padding: '0.65rem 0.75rem' }}>Nome</th>
|
||||
<th style={{ padding: '0.65rem 0.75rem' }}>Status</th>
|
||||
<th style={{ padding: '0.65rem 0.75rem' }}>Atendimentos abertos</th>
|
||||
<th style={{ padding: '0.65rem 0.75rem' }}>Último finalizado há</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.team.map((agent) => (
|
||||
<tr key={agent.id} style={{ borderTop: '1px solid var(--color-border)' }}>
|
||||
<td style={{ padding: '0.8rem 0.75rem', fontWeight: 800 }}>{agent.name}</td>
|
||||
<td style={{ padding: '0.8rem 0.75rem' }}>
|
||||
<Badge tone={statusMeta[agent.status]}>{statusMeta[agent.status].label}</Badge>
|
||||
</td>
|
||||
<td style={{ padding: '0.8rem 0.75rem' }}>{agent.open}</td>
|
||||
<td style={{ padding: '0.8rem 0.75rem', color: 'var(--color-text-soft)', fontWeight: 700 }}>{agent.lastClosed}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</DataPanel>
|
||||
|
||||
<DataPanel title="Fila de espera" description="Conversas não assumidas, ordenadas pelo maior tempo de espera.">
|
||||
<div style={{ display: 'grid', gap: '0.65rem' }}>
|
||||
{sortedQueue.map((item) => (
|
||||
<article
|
||||
key={item.id}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 16,
|
||||
padding: '0.8rem',
|
||||
background: '#fff',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'auto minmax(0, 1fr) auto',
|
||||
gap: '0.75rem',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Badge tone={channelColors[item.channel]}>{item.channel}</Badge>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<strong style={{ display: 'block' }}>{item.contact}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem', fontWeight: 700 }}>
|
||||
Aguardando há {item.waitingMinutes} min
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAssignmentTarget(item)}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 12,
|
||||
padding: '0.65rem 0.8rem',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Atribuir
|
||||
</button>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</DataPanel>
|
||||
</div>
|
||||
|
||||
<DataPanel title="Atendimentos finalizados por hora" description="Volume do dia entre 08h e 18h.">
|
||||
<HourlyLineChart values={data.hourly} />
|
||||
</DataPanel>
|
||||
|
||||
{assignmentTarget ? (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 49, 80, 0.28)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
padding: '1rem',
|
||||
zIndex: 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 'min(460px, 100%)', background: '#fff', borderRadius: 22, padding: '1.25rem', boxShadow: 'var(--shadow-lg)', display: 'grid', gap: '1rem' }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: '1.2rem' }}>Atribuir atendimento</h2>
|
||||
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
|
||||
Selecione um atendente online para {assignmentTarget.contact}.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: '0.55rem' }}>
|
||||
{onlineAgents.length ? onlineAgents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
onClick={() => setAssignmentTarget(null)}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 14,
|
||||
padding: '0.8rem 0.9rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-text)',
|
||||
fontWeight: 800,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{agent.name}
|
||||
</button>
|
||||
)) : (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Nenhum atendente online nesta especialidade.</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAssignmentTarget(null)}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 14,
|
||||
padding: '0.8rem 1rem',
|
||||
background: 'rgba(0, 49, 80, 0.08)',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -3,22 +3,25 @@ import { DataPanel } from '../components/DataPanel';
|
||||
import { ManagementLayout } from '../components/ManagementLayout';
|
||||
import { ManagementTable } from '../components/ManagementTable';
|
||||
import { MetricGrid } from '../components/MetricGrid';
|
||||
import { OperationalDashboard } from '../components/OperationalDashboard';
|
||||
import { aiContentRows, areaRows, userRows } from '../services/managementMocks';
|
||||
import { AttendantOpsPanel } from '../../home/components/AttendantOpsPanel';
|
||||
import { MessagesWorkspace } from '../../home/components/MessagesWorkspace';
|
||||
import { useChat } from '../../chat/hooks/useChat';
|
||||
import {
|
||||
createAccessArea,
|
||||
getAccessAreas,
|
||||
getAccessOptions,
|
||||
getAccessUsers,
|
||||
getAdminOverview,
|
||||
updateAccessArea,
|
||||
updateUserAccess,
|
||||
} from '../services/adminAccessService';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||
|
||||
const contentColumns = [
|
||||
{ key: 'title', label: 'Conteudo' },
|
||||
{ key: 'area', label: 'Area' },
|
||||
{ key: 'title', label: 'Conteúdo' },
|
||||
{ key: 'area', label: 'Especialidade' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'updatedAt', label: 'Atualizado' },
|
||||
];
|
||||
@ -33,12 +36,19 @@ const selectStyle = {
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
const compactSelectStyle = {
|
||||
...selectStyle,
|
||||
borderRadius: '10px',
|
||||
padding: '0.45rem 0.55rem',
|
||||
fontSize: '0.82rem',
|
||||
};
|
||||
|
||||
const monthlyKpis = [
|
||||
{ label: 'Total de Atendimentos', value: '1.284', detail: '+12% vs mes anterior' },
|
||||
{ label: 'Tempo Medio de Atendimento', value: '8m 42s', detail: 'media mensal' },
|
||||
{ label: 'Taxa de Satisfacao', value: '91%', detail: 'avaliacoes positivas' },
|
||||
{ label: 'Total de Atendimentos', value: '1.284', detail: '+12% vs mês anterior' },
|
||||
{ label: 'Tempo Médio de Atendimento', value: '8m 42s', detail: 'média mensal' },
|
||||
{ label: 'Taxa de Satisfação', value: '91%', detail: 'avaliações positivas' },
|
||||
{ label: 'Volume por Canal', value: 'W 982 · E 184 · S 118', detail: 'WhatsApp · Email · SMS' },
|
||||
{ label: 'Atendentes Ativos', value: '14 de 17', detail: 'ativos no mes' },
|
||||
{ label: 'Atendentes Ativos', value: '14 de 17', detail: 'ativos no mês' },
|
||||
];
|
||||
|
||||
const dailyAttendance = [28, 34, 42, 39, 51, 47, 58, 62, 55, 69, 73, 66, 71, 88, 79, 84, 91, 86, 94, 101, 97, 108, 112, 104, 118, 123, 116, 129, 134, 141];
|
||||
@ -82,8 +92,81 @@ function formatMinutes(minutes) {
|
||||
return `${Number(minutes)} min`;
|
||||
}
|
||||
|
||||
function toHomeConversation(contact, messages = []) {
|
||||
return {
|
||||
id: contact.id,
|
||||
name: contact.name,
|
||||
channel: contact.channel || 'WhatsApp',
|
||||
status: contact.status || 'online',
|
||||
lastMessage: contact.preview || messages[messages.length - 1]?.text || '',
|
||||
unread: contact.unread || 0,
|
||||
time: contact.time || 'Agora',
|
||||
lastSeen: contact.lastSeen,
|
||||
messages: messages.map((message) => ({
|
||||
id: message.id,
|
||||
from: message.sender === 'agent' ? 'agent' : 'customer',
|
||||
text: message.text || (message.hasMedia ? '[Mídia]' : ''),
|
||||
timestamp: message.timestamp,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function AdminAttendanceWorkspace({ isWideDesktop, isDesktop, isTablet, isMobile }) {
|
||||
const {
|
||||
contacts,
|
||||
activeContactId,
|
||||
setActiveContactId,
|
||||
messages,
|
||||
sendMessage,
|
||||
isLoadingChats,
|
||||
} = useChat();
|
||||
|
||||
const conversations = contacts.map((contact) =>
|
||||
toHomeConversation(contact, contact.id === activeContactId ? messages : []),
|
||||
);
|
||||
|
||||
const safeConversationId =
|
||||
conversations.find((conversation) => conversation.id === activeContactId)?.id ||
|
||||
conversations[0]?.id;
|
||||
|
||||
return (
|
||||
<section style={{ display: 'grid', gap: '1rem' }}>
|
||||
<AttendantOpsPanel activeChatsCount={conversations.length} />
|
||||
|
||||
{isLoadingChats ? (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 18,
|
||||
padding: '0.85rem 1rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Atualizando conversas do WhatsApp...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<MessagesWorkspace
|
||||
conversations={conversations}
|
||||
activeConversationId={safeConversationId}
|
||||
onSelectConversation={setActiveContactId}
|
||||
onSendSuggestedReply={async (conversationId, reply) => {
|
||||
setActiveContactId(conversationId);
|
||||
await sendMessage(reply, conversationId);
|
||||
}}
|
||||
isWideDesktop={isWideDesktop}
|
||||
isDesktop={isDesktop}
|
||||
isTablet={isTablet}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminPage() {
|
||||
const { isDesktop, isMobile } = useViewport();
|
||||
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
|
||||
const userDisplay = getCurrentUserDisplay();
|
||||
const [activeAdminSection, setActiveAdminSection] = useState('home');
|
||||
const [selectedAreaFilter, setSelectedAreaFilter] = useState('all');
|
||||
@ -96,9 +179,12 @@ export function AdminPage() {
|
||||
const [areaRowsState, setAreaRowsState] = useState(areaRows);
|
||||
const [userSearch, setUserSearch] = useState('');
|
||||
const [newAreaName, setNewAreaName] = useState('');
|
||||
const [newAreaOwnerId, setNewAreaOwnerId] = useState('');
|
||||
const [isLoadingAccess, setIsLoadingAccess] = useState(true);
|
||||
const [accessError, setAccessError] = useState('');
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
const [editUserProfileId, setEditUserProfileId] = useState('');
|
||||
const [editUserSpecialties, setEditUserSpecialties] = useState([]);
|
||||
const [specialtyToAdd, setSpecialtyToAdd] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
@ -140,30 +226,35 @@ export function AdminPage() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleAccessChange(user, field, value) {
|
||||
const currentPerfilId = user.perfilPrincipal?.id || null;
|
||||
const currentAreaId = user.areaPrincipal?.id || null;
|
||||
const nextAccess = {
|
||||
perfilId: field === 'perfil' ? Number(value) || null : currentPerfilId,
|
||||
areaId: field === 'area' ? Number(value) || null : currentAreaId,
|
||||
};
|
||||
function openUserEditor(user) {
|
||||
setEditingUser(user);
|
||||
setEditUserProfileId(user.perfilPrincipal?.id ? String(user.perfilPrincipal.id) : '');
|
||||
setEditUserSpecialties(Array.isArray(user.areas) ? user.areas : []);
|
||||
setSpecialtyToAdd('');
|
||||
}
|
||||
|
||||
setUsers((current) =>
|
||||
current.map((item) =>
|
||||
item.id === user.id
|
||||
? {
|
||||
...item,
|
||||
perfilPrincipal:
|
||||
profiles.find((profile) => profile.id === nextAccess.perfilId) || null,
|
||||
areaPrincipal: areas.find((area) => area.id === nextAccess.areaId) || null,
|
||||
accessStatus: nextAccess.perfilId && nextAccess.areaId ? 'assigned' : 'unassigned',
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
function closeUserEditor() {
|
||||
setEditingUser(null);
|
||||
setEditUserProfileId('');
|
||||
setEditUserSpecialties([]);
|
||||
setSpecialtyToAdd('');
|
||||
}
|
||||
|
||||
function getProfileName(profileId) {
|
||||
return profiles.find((profile) => profile.id === Number(profileId))?.nome || '';
|
||||
}
|
||||
|
||||
async function saveUserAccess(user, nextProfileId, nextSpecialties) {
|
||||
try {
|
||||
const updatedUser = await updateUserAccess(user.id, nextAccess);
|
||||
const updatedUser = await updateUserAccess(user.id, {
|
||||
perfilIds: nextProfileId ? [Number(nextProfileId)] : [],
|
||||
especialidades: nextSpecialties.map((specialty, index) => ({
|
||||
areaId: Number(specialty.id),
|
||||
funcao: specialty.funcao || 'Agente',
|
||||
principal: index === 0,
|
||||
ativo: true,
|
||||
})),
|
||||
});
|
||||
|
||||
if (updatedUser) {
|
||||
setUsers((current) =>
|
||||
@ -172,11 +263,53 @@ export function AdminPage() {
|
||||
}
|
||||
|
||||
setAccessError('');
|
||||
await refreshAreas();
|
||||
} catch {
|
||||
setAccessError('Nao foi possivel salvar a atribuicao. Confira o backend.');
|
||||
setAccessError('Não foi possível salvar a atribuição. Confira o backend.');
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditProfileChange(value) {
|
||||
setEditUserProfileId(value);
|
||||
if (getProfileName(value) === 'Admin') {
|
||||
setEditUserSpecialties([]);
|
||||
setSpecialtyToAdd('');
|
||||
}
|
||||
}
|
||||
|
||||
function addSpecialtyToEdit() {
|
||||
const area = areas.find((item) => item.id === Number(specialtyToAdd));
|
||||
if (!area) return;
|
||||
|
||||
setEditUserSpecialties((current) => {
|
||||
if (current.some((specialty) => specialty.id === area.id)) return current;
|
||||
return [
|
||||
...current,
|
||||
{ id: area.id, nome: area.nome, funcao: 'Agente', principal: current.length === 0 },
|
||||
];
|
||||
});
|
||||
setSpecialtyToAdd('');
|
||||
}
|
||||
|
||||
function removeSpecialtyFromEdit(areaId) {
|
||||
setEditUserSpecialties((current) => current.filter((specialty) => specialty.id !== areaId));
|
||||
}
|
||||
|
||||
function updateEditSpecialtyRole(areaId, role) {
|
||||
setEditUserSpecialties((current) =>
|
||||
current.map((specialty) =>
|
||||
specialty.id === areaId ? { ...specialty, funcao: role } : specialty,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function submitUserEditor() {
|
||||
if (!editingUser) return;
|
||||
const isAdmin = getProfileName(editUserProfileId) === 'Admin';
|
||||
await saveUserAccess(editingUser, editUserProfileId, isAdmin ? [] : editUserSpecialties);
|
||||
closeUserEditor();
|
||||
}
|
||||
|
||||
async function refreshAreas() {
|
||||
const [accessAreas, options] = await Promise.all([getAccessAreas(), getAccessOptions()]);
|
||||
setAreaRowsState(accessAreas || []);
|
||||
@ -190,28 +323,12 @@ export function AdminPage() {
|
||||
try {
|
||||
await createAccessArea({
|
||||
nome,
|
||||
responsavelUsuarioId: Number(newAreaOwnerId) || null,
|
||||
});
|
||||
setNewAreaName('');
|
||||
setNewAreaOwnerId('');
|
||||
await refreshAreas();
|
||||
setAccessError('');
|
||||
} catch {
|
||||
setAccessError('Nao foi possivel criar a area.');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAreaOwnerChange(areaId, userId) {
|
||||
try {
|
||||
await updateAccessArea(areaId, {
|
||||
responsavelUsuarioId: Number(userId) || null,
|
||||
});
|
||||
await refreshAreas();
|
||||
const accessUsers = await getAccessUsers();
|
||||
setUsers(accessUsers || []);
|
||||
setAccessError('');
|
||||
} catch {
|
||||
setAccessError('Nao foi possivel atualizar o responsavel da area.');
|
||||
setAccessError('Não foi possível criar a especialidade.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,18 +337,18 @@ export function AdminPage() {
|
||||
label: 'Total de Atendimentos',
|
||||
value: overview ? String(overview.totalAttendances) : '...',
|
||||
detail: overview?.previousMonthVariation === null || overview?.previousMonthVariation === undefined
|
||||
? 'sem base do mes anterior'
|
||||
: `${overview.previousMonthVariation >= 0 ? '+' : ''}${overview.previousMonthVariation}% vs mes anterior`,
|
||||
? 'sem base do mês anterior'
|
||||
: `${overview.previousMonthVariation >= 0 ? '+' : ''}${overview.previousMonthVariation}% vs mês anterior`,
|
||||
},
|
||||
{
|
||||
label: 'TMA',
|
||||
value: formatMinutes(overview?.avgHandlingMinutes),
|
||||
detail: overview?.avgHandlingMinutes === null ? 'aguardando historico' : 'media mensal',
|
||||
detail: overview?.avgHandlingMinutes === null ? 'aguardando histórico' : 'média mensal',
|
||||
},
|
||||
{
|
||||
label: 'TME',
|
||||
value: formatMinutes(overview?.avgFirstResponseMinutes),
|
||||
detail: 'tempo medio de espera',
|
||||
detail: 'tempo médio de espera',
|
||||
},
|
||||
{
|
||||
label: 'TMR',
|
||||
@ -241,7 +358,7 @@ export function AdminPage() {
|
||||
{
|
||||
label: 'Atendentes Ativos',
|
||||
value: overview ? `${overview.activeAttendants} de ${overview.totalActiveUsers}` : '...',
|
||||
detail: 'ativos no mes',
|
||||
detail: 'ativos no mês',
|
||||
},
|
||||
];
|
||||
|
||||
@ -252,6 +369,10 @@ export function AdminPage() {
|
||||
.toLowerCase()
|
||||
.includes(search);
|
||||
});
|
||||
const availableSpecialtiesToAdd = areas.filter(
|
||||
(area) => !editUserSpecialties.some((specialty) => specialty.id === area.id),
|
||||
);
|
||||
const isEditingAdmin = getProfileName(editUserProfileId) === 'Admin';
|
||||
|
||||
const channelDistributionData = overview
|
||||
? [
|
||||
@ -266,56 +387,12 @@ export function AdminPage() {
|
||||
{
|
||||
key: 'nome',
|
||||
label: 'Usuario',
|
||||
render: (row) => (
|
||||
<div>
|
||||
<strong style={{ display: 'block' }}>{row.nome}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem' }}>
|
||||
{row.email || 'Sem email'}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
render: (row) => <strong>{row.nome}</strong>,
|
||||
},
|
||||
{
|
||||
key: 'perfil',
|
||||
label: 'Perfil',
|
||||
render: (row) =>
|
||||
profiles.length ? (
|
||||
<select
|
||||
value={row.perfilPrincipal?.id || ''}
|
||||
onChange={(event) => handleAccessChange(row, 'perfil', event.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">Sem perfil</option>
|
||||
{profiles.map((profile) => (
|
||||
<option key={profile.id} value={profile.id}>
|
||||
{profile.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<span>{row.perfilPrincipal?.nome || 'Sem perfil'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'area',
|
||||
label: 'Area',
|
||||
render: (row) =>
|
||||
areas.length ? (
|
||||
<select
|
||||
value={row.areaPrincipal?.id || ''}
|
||||
onChange={(event) => handleAccessChange(row, 'area', event.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">Sem area</option>
|
||||
{areas.map((area) => (
|
||||
<option key={area.id} value={area.id}>
|
||||
{area.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<span>{row.areaPrincipal?.nome || 'Sem area'}</span>
|
||||
),
|
||||
render: (row) => <span>{row.perfilPrincipal?.nome || 'Sem perfil'}</span>,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
@ -339,39 +416,70 @@ export function AdminPage() {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: 'Ações',
|
||||
render: (row) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openUserEditor(row)}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 14,
|
||||
padding: '0.7rem 0.9rem',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[areas, profiles],
|
||||
[],
|
||||
);
|
||||
|
||||
const areaColumns = useMemo(
|
||||
() => [
|
||||
{ key: 'nome', label: 'Area' },
|
||||
{ key: 'nome', label: 'Especialidade' },
|
||||
{
|
||||
key: 'responsavel',
|
||||
label: 'Responsavel',
|
||||
render: (row) => (
|
||||
<select
|
||||
value={row.responsavel_usuario_id || ''}
|
||||
onChange={(event) => handleAreaOwnerChange(row.id, event.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">Sem responsavel</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
key: 'supervisores',
|
||||
label: 'Supervisores',
|
||||
render: (row) => {
|
||||
const supervisors = Array.isArray(row.supervisores) ? row.supervisores : [];
|
||||
|
||||
return supervisors.length ? (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
|
||||
{supervisors.map((supervisor) => (
|
||||
<span
|
||||
key={supervisor.id}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
padding: '0.25rem 0.55rem',
|
||||
background: 'rgba(0, 164, 183, 0.1)',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 800,
|
||||
fontSize: '0.82rem',
|
||||
}}
|
||||
>
|
||||
{supervisor.nome}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Sem supervisor</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ key: 'members', label: 'Usuários' },
|
||||
{ key: 'members', label: 'Usuarios' },
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
render: (row) => (row.ativo ? 'Ativa' : 'Inativa'),
|
||||
},
|
||||
],
|
||||
[users],
|
||||
[],
|
||||
);
|
||||
|
||||
const filteredRanking = selectedAreaFilter === 'all'
|
||||
@ -380,10 +488,10 @@ export function AdminPage() {
|
||||
|
||||
const rankingColumns = [
|
||||
{ key: 'name', label: 'Nome' },
|
||||
{ key: 'area', label: 'Area' },
|
||||
{ key: 'area', label: 'Especialidade' },
|
||||
{ key: 'closed', label: 'Atendimentos finalizados' },
|
||||
{ key: 'avgTime', label: 'Tempo medio' },
|
||||
{ key: 'satisfaction', label: 'Satisfacao' },
|
||||
{ key: 'avgTime', label: 'Tempo médio' },
|
||||
{ key: 'satisfaction', label: 'Satisfação' },
|
||||
];
|
||||
|
||||
function sendNotice() {
|
||||
@ -454,9 +562,9 @@ export function AdminPage() {
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<label style={{ display: 'grid', gap: '0.35rem', minWidth: isMobile ? '100%' : 260 }}>
|
||||
<span style={{ fontWeight: 700 }}>Filtro por area</span>
|
||||
<span style={{ fontWeight: 700 }}>Filtro por especialidade</span>
|
||||
<select value={selectedAreaFilter} onChange={(event) => setSelectedAreaFilter(event.target.value)} style={selectStyle}>
|
||||
<option value="all">Todas as areas</option>
|
||||
<option value="all">Todas as especialidades</option>
|
||||
{areas.map((area) => (
|
||||
<option key={area.id} value={area.nome}>{area.nome}</option>
|
||||
))}
|
||||
@ -467,10 +575,10 @@ export function AdminPage() {
|
||||
<MetricGrid metrics={realMonthlyKpis} minCardWidth="160px" />
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isDesktop ? 'minmax(0, 1.85fr) minmax(300px, 1fr)' : '1fr', gap: '1rem' }}>
|
||||
<DataPanel title="Atendimentos por dia" description="Volume diario do mes selecionado.">
|
||||
<DataPanel title="Atendimentos por dia" description="Volume diário do mês selecionado.">
|
||||
{renderLineChart()}
|
||||
</DataPanel>
|
||||
<DataPanel title="Distribuicao por canal" description="Participacao mensal por canal.">
|
||||
<DataPanel title="Distribuição por canal" description="Participação mensal por canal.">
|
||||
{renderDonutChart()}
|
||||
</DataPanel>
|
||||
</div>
|
||||
@ -511,7 +619,7 @@ export function AdminPage() {
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isDesktop ? 'minmax(0, 1.2fr) minmax(320px, 0.8fr)' : '1fr',
|
||||
gridTemplateColumns: '1fr',
|
||||
gap: '1rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
@ -520,43 +628,207 @@ export function AdminPage() {
|
||||
title="Usuarios e niveis de acesso"
|
||||
description={
|
||||
isLoadingAccess
|
||||
? 'Carregando usuarios do banco...'
|
||||
: accessError || 'Gerencie perfil e area principal dos usuarios autenticados.'
|
||||
? 'Carregando usuários do banco...'
|
||||
: accessError || 'Gerencie perfil e especialidade principal dos usuários autenticados.'
|
||||
}
|
||||
actionLabel="Adicionar usuario"
|
||||
actionLabel="Adicionar usuário"
|
||||
>
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<input
|
||||
type="search"
|
||||
value={userSearch}
|
||||
onChange={(event) => setUserSearch(event.target.value)}
|
||||
placeholder="Buscar usuario por nome, email, perfil ou area"
|
||||
placeholder="Buscar usuário por nome, email, perfil ou especialidade"
|
||||
style={selectStyle}
|
||||
/>
|
||||
<div style={{ maxHeight: 470, overflowY: 'auto', paddingRight: '0.2rem' }}>
|
||||
<ManagementTable columns={userColumns} rows={filteredUsers} getRowId={(row) => row.id} isMobile={isMobile} />
|
||||
</div>
|
||||
|
||||
{editingUser ? (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 49, 80, 0.28)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
padding: '1rem',
|
||||
zIndex: 30,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 'min(560px, 100%)',
|
||||
background: '#fff',
|
||||
borderRadius: 24,
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
padding: '1.25rem',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: '1.2rem' }}>Editar acesso</h2>
|
||||
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
|
||||
{editingUser.nome} · {editingUser.email || 'Sem email'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeUserEditor}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 800 }}>Perfil global</span>
|
||||
<select
|
||||
value={editUserProfileId}
|
||||
onChange={(event) => handleEditProfileChange(event.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">Sem perfil</option>
|
||||
{profiles.map((profile) => (
|
||||
<option key={profile.id} value={profile.id}>
|
||||
{profile.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{isEditingAdmin ? (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 16,
|
||||
padding: '0.85rem',
|
||||
background: 'rgba(0, 164, 183, 0.08)',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Admin tem acesso global. Especialidades não se aplicam para este perfil.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto', gap: '0.65rem' }}>
|
||||
<select
|
||||
value={specialtyToAdd}
|
||||
onChange={(event) => setSpecialtyToAdd(event.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">Selecionar especialidade</option>
|
||||
{availableSpecialtiesToAdd.map((area) => (
|
||||
<option key={area.id} value={area.id}>
|
||||
{area.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addSpecialtyToEdit}
|
||||
disabled={!specialtyToAdd}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 14,
|
||||
padding: '0.75rem 0.95rem',
|
||||
background: specialtyToAdd ? 'var(--color-primary)' : 'rgba(0, 49, 80, 0.12)',
|
||||
color: specialtyToAdd ? '#fff' : 'var(--color-text-soft)',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Adicionar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.55rem', maxHeight: 260, overflowY: 'auto', paddingRight: '0.2rem' }}>
|
||||
{editUserSpecialties.length ? editUserSpecialties.map((specialty) => (
|
||||
<div
|
||||
key={specialty.id}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 16,
|
||||
padding: '0.75rem',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) 140px auto',
|
||||
gap: '0.65rem',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<strong>{specialty.nome}</strong>
|
||||
<select
|
||||
value={specialty.funcao || 'Agente'}
|
||||
onChange={(event) => updateEditSpecialtyRole(specialty.id, event.target.value)}
|
||||
style={compactSelectStyle}
|
||||
>
|
||||
<option value="Agente">Agente</option>
|
||||
<option value="Supervisor">Supervisor</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeSpecialtyFromEdit(specialty.id)}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 12,
|
||||
padding: '0.55rem 0.7rem',
|
||||
background: 'rgba(181, 31, 31, 0.1)',
|
||||
color: 'var(--color-secondary)',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Remover
|
||||
</button>
|
||||
</div>
|
||||
)) : (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>
|
||||
Nenhuma especialidade selecionada.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitUserEditor}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 16,
|
||||
padding: '0.9rem 1rem',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Salvar acesso
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DataPanel>
|
||||
|
||||
<DataPanel title="Areas" description="Areas operacionais e seus responsaveis. Alterar o responsavel atribui perfil Supervisor a ele.">
|
||||
<DataPanel title="Especialidades" description="Especialidades operacionais. Supervisores são definidos em Usuários e níveis de acesso.">
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(0, 1fr) auto', gap: '0.75rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto', gap: '0.75rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={newAreaName}
|
||||
onChange={(event) => setNewAreaName(event.target.value)}
|
||||
placeholder="Nome da nova area"
|
||||
placeholder="Nome da nova especialidade"
|
||||
style={selectStyle}
|
||||
/>
|
||||
<select value={newAreaOwnerId} onChange={(event) => setNewAreaOwnerId(event.target.value)} style={selectStyle}>
|
||||
<option value="">Responsavel opcional</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateArea}
|
||||
@ -583,7 +855,7 @@ export function AdminPage() {
|
||||
return (
|
||||
<DataPanel title={title} description={description}>
|
||||
<div style={{ border: '1px solid var(--color-border)', borderRadius: 18, padding: '1rem', background: '#fff', color: 'var(--color-text-soft)', fontWeight: 700 }}>
|
||||
Secao em preparacao.
|
||||
Seção em preparação.
|
||||
</div>
|
||||
</DataPanel>
|
||||
);
|
||||
@ -591,7 +863,7 @@ export function AdminPage() {
|
||||
|
||||
const sectionContent = {
|
||||
home: renderMonthlyHome(),
|
||||
today: renderPlaceholder('Operação', 'Visão operacional diaria sera consolidada aqui.'),
|
||||
today: <OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />,
|
||||
'users-access': renderUsersAccess(),
|
||||
templates: renderPlaceholder('Templates', 'Gestão de templates aprovados pela Meta.'),
|
||||
knowledge: (
|
||||
@ -599,17 +871,41 @@ export function AdminPage() {
|
||||
<ManagementTable columns={contentColumns} rows={aiContentRows} getRowId={(row) => row.id} isMobile={isMobile} />
|
||||
</DataPanel>
|
||||
),
|
||||
audit: renderPlaceholder('Auditoria', 'Eventos administrativos e alteracoes sensiveis.'),
|
||||
audit: renderPlaceholder('Auditoria', 'Eventos administrativos e alterações sensíveis.'),
|
||||
channels: renderPlaceholder('Canais', 'Status e configurações dos canais conectados.'),
|
||||
attendance: (
|
||||
<AdminAttendanceWorkspace
|
||||
isWideDesktop={isWideDesktop}
|
||||
isDesktop={isDesktop}
|
||||
isTablet={isTablet}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
),
|
||||
'mass-message': renderPlaceholder('Disparo em massa', 'Fluxo de disparos por templates aprovados.'),
|
||||
contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
|
||||
settings: renderPlaceholder('Configurações', 'Preferencias e parametros do ambiente.'),
|
||||
};
|
||||
|
||||
const pageTitle = activeAdminSection === 'home'
|
||||
? 'Home do Admin'
|
||||
: activeAdminSection === 'attendance'
|
||||
? 'Atendimento'
|
||||
: activeAdminSection === 'today'
|
||||
? 'Operação'
|
||||
: 'Painel administrativo';
|
||||
|
||||
const pageSubtitle = activeAdminSection === 'home'
|
||||
? 'Visão mensal consolidada por especialidade, canal e atendente.'
|
||||
: activeAdminSection === 'attendance'
|
||||
? 'Home operacional do atendente dentro do painel administrativo.'
|
||||
: activeAdminSection === 'today'
|
||||
? 'Indicadores do dia, fila de espera e acompanhamento operacional do time.'
|
||||
: 'Controle operacional e configurações administrativas.';
|
||||
|
||||
return (
|
||||
<ManagementLayout
|
||||
title={activeAdminSection === 'home' ? 'Home do Admin' : 'Painel administrativo'}
|
||||
subtitle={activeAdminSection === 'home' ? 'Visão mensal consolidada por area, canal e atendente.' : 'Controle operacional e configuracoes administrativas.'}
|
||||
title={pageTitle}
|
||||
subtitle={pageSubtitle}
|
||||
activeSection="admin"
|
||||
profileLabel={userDisplay.name}
|
||||
initials={userDisplay.initials}
|
||||
|
||||
@ -1,323 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DataPanel } from '../components/DataPanel';
|
||||
import { ManagementLayout } from '../components/ManagementLayout';
|
||||
import { ManagementTable } from '../components/ManagementTable';
|
||||
import { MetricGrid } from '../components/MetricGrid';
|
||||
import { areaRows, queueRows, supervisorMetrics } from '../services/managementMocks';
|
||||
import { OperationalDashboard } from '../components/OperationalDashboard';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||
|
||||
const queueColumns = [
|
||||
{ key: 'customer', label: 'Cliente' },
|
||||
{ key: 'channel', label: 'Canal' },
|
||||
{ key: 'area', label: 'Area' },
|
||||
{ key: 'wait', label: 'Espera' },
|
||||
{
|
||||
key: 'priority',
|
||||
label: 'Prioridade',
|
||||
render: (row) => (
|
||||
<span
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
borderRadius: 999,
|
||||
padding: '0.25rem 0.6rem',
|
||||
background: row.priority === 'Alta' ? 'rgba(181, 31, 31, 0.1)' : 'rgba(0, 49, 80, 0.08)',
|
||||
color: row.priority === 'Alta' ? 'var(--color-secondary)' : 'var(--color-primary)',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{row.priority}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const areaColumns = [
|
||||
{ key: 'name', label: 'Area' },
|
||||
{ key: 'owner', label: 'Responsavel' },
|
||||
{ key: 'members', label: 'Usuarios' },
|
||||
{ key: 'openTickets', label: 'Abertos' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
];
|
||||
|
||||
export function SupervisorPage() {
|
||||
const { isDesktop, isMobile } = useViewport();
|
||||
const userDisplay = getCurrentUserDisplay();
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [editingTemplate, setEditingTemplate] = useState(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editContent, setEditContent] = useState('');
|
||||
const [saveStatus, setSaveStatus] = useState('');
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/whatsapp/templates`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setTemplates(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplates();
|
||||
}, []);
|
||||
|
||||
const handleEdit = (tpl) => {
|
||||
setEditingTemplate(tpl);
|
||||
setEditName(tpl.name);
|
||||
setEditContent(tpl.content);
|
||||
setSaveStatus('');
|
||||
};
|
||||
|
||||
const handleSave = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!editName || !editContent) return;
|
||||
try {
|
||||
const url = editingTemplate
|
||||
? `${API_BASE_URL}/whatsapp/templates/update/${editingTemplate.id}`
|
||||
: `${API_BASE_URL}/whatsapp/templates`;
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: editName, content: editContent }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSaveStatus('Salvo com sucesso!');
|
||||
setEditingTemplate(null);
|
||||
setEditName('');
|
||||
setEditContent('');
|
||||
fetchTemplates();
|
||||
setTimeout(() => setSaveStatus(''), 3000);
|
||||
} else {
|
||||
setSaveStatus('Erro ao salvar template.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setSaveStatus('Erro ao salvar template.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ManagementLayout
|
||||
title="Painel do supervisor"
|
||||
subtitle="Acompanhamento operacional das filas, areas e distribuicao de atendimento."
|
||||
subtitle="Indicadores do dia, fila de espera e acompanhamento operacional do time."
|
||||
activeSection="supervisor"
|
||||
profileLabel={userDisplay.name}
|
||||
initials={userDisplay.initials}
|
||||
isDesktop={isDesktop}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<MetricGrid metrics={supervisorMetrics} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isDesktop ? 'minmax(0, 1.35fr) minmax(320px, 0.85fr)' : '1fr',
|
||||
gap: '1rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<DataPanel
|
||||
title="Fila em tempo real"
|
||||
description="Mock da visao que depois sera alimentada pelos atendimentos reais."
|
||||
actionLabel="Redistribuir fila"
|
||||
>
|
||||
<ManagementTable
|
||||
columns={queueColumns}
|
||||
rows={queueRows}
|
||||
getRowId={(row) => row.id}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</DataPanel>
|
||||
|
||||
<DataPanel
|
||||
title="Areas supervisionadas"
|
||||
description="Resumo operacional por area."
|
||||
actionLabel="Ver detalhes"
|
||||
>
|
||||
<ManagementTable
|
||||
columns={areaColumns}
|
||||
rows={areaRows}
|
||||
getRowId={(row) => row.id}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</DataPanel>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1.5rem' }}>
|
||||
<DataPanel
|
||||
title="Homologador de Templates WhatsApp (Meta)"
|
||||
description="Gerencie os modelos de primeiro contato pré-aprovados pela Meta para uso dos atendentes."
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isDesktop ? '1.2fr 0.8fr' : '1fr', gap: '1.5rem', padding: '0.5rem 0' }}>
|
||||
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
{templates.map((tpl) => (
|
||||
<div
|
||||
key={tpl.id}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '16px',
|
||||
padding: '1rem',
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
gap: '0.6rem',
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<strong style={{ fontSize: '0.96rem', color: 'var(--color-primary)', textTransform: 'uppercase', letterSpacing: '0.02em' }}>
|
||||
{tpl.name}
|
||||
</strong>
|
||||
<span style={{ fontSize: '0.75rem', fontWeight: 700, padding: '0.2rem 0.5rem', borderRadius: '999px', background: 'rgba(34, 197, 94, 0.1)', color: '#16a34a' }}>
|
||||
✓ Homologado Meta
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ margin: 0, color: 'var(--color-text-soft)', fontSize: '0.88rem', lineHeight: 1.5 }}>
|
||||
{tpl.content}
|
||||
</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '0.25rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleEdit(tpl)}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'rgba(0, 49, 80, 0.08)',
|
||||
color: 'var(--color-primary)',
|
||||
padding: '0.45rem 0.85rem',
|
||||
borderRadius: '10px',
|
||||
fontSize: '0.82rem',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
Editar Modelo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSave}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '20px',
|
||||
padding: '1.25rem',
|
||||
background: '#f8fafc',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
height: 'fit-content',
|
||||
}}
|
||||
>
|
||||
<strong style={{ fontSize: '1.05rem', display: 'block', borderBottom: '1px solid var(--color-border)', paddingBottom: '0.5rem', color: 'var(--color-text)' }}>
|
||||
{editingTemplate ? `Editar Template #${editingTemplate.id}` : 'Criar Novo Template'}
|
||||
</strong>
|
||||
|
||||
{saveStatus && (
|
||||
<div style={{
|
||||
padding: '0.65rem 0.85rem',
|
||||
borderRadius: '12px',
|
||||
background: saveStatus.includes('sucesso') ? 'rgba(34, 197, 94, 0.12)' : 'rgba(239, 68, 68, 0.12)',
|
||||
color: saveStatus.includes('sucesso') ? '#16a34a' : '#ef4444',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.85rem',
|
||||
}}>
|
||||
{saveStatus}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.35rem', color: 'var(--color-text)' }}>
|
||||
<span style={{ fontSize: '0.84rem', fontWeight: 600 }}>Identificador Único (Nome)</span>
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
placeholder="ex: aviso_fatura"
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '12px',
|
||||
padding: '0.75rem 0.85rem',
|
||||
background: '#fff',
|
||||
outline: 'none',
|
||||
fontSize: '0.88rem',
|
||||
color: 'var(--color-text)',
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.35rem', color: 'var(--color-text)' }}>
|
||||
<span style={{ fontSize: '0.84rem', fontWeight: 600 }}>Mensagem do Template</span>
|
||||
<textarea
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
placeholder="Use placeholders como {nome}, {data} ou {protocolo}..."
|
||||
rows={4}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '12px',
|
||||
padding: '0.75rem 0.85rem',
|
||||
background: '#fff',
|
||||
outline: 'none',
|
||||
fontSize: '0.88rem',
|
||||
resize: 'none',
|
||||
lineHeight: 1.5,
|
||||
color: 'var(--color-text)',
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', marginTop: '0.5rem' }}>
|
||||
{editingTemplate && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingTemplate(null);
|
||||
setEditName('');
|
||||
setEditContent('');
|
||||
}}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'rgba(239, 68, 68, 0.08)',
|
||||
color: '#ef4444',
|
||||
padding: '0.65rem 1rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
padding: '0.65rem 1rem',
|
||||
borderRadius: '12px',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{editingTemplate ? 'Atualizar Modelo' : 'Criar Modelo'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DataPanel>
|
||||
</div>
|
||||
<OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />
|
||||
</ManagementLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,12 +2,12 @@ export const supervisorMetrics = [
|
||||
{ label: 'Atendimentos abertos', value: '42', detail: '12 aguardando agente' },
|
||||
{ label: 'SLA em risco', value: '7', detail: 'Financeiro concentra 4 casos' },
|
||||
{ label: 'Agentes online', value: '18', detail: '3 em pausa operacional' },
|
||||
{ label: 'Transferencias hoje', value: '23', detail: 'Tempo medio 4m 20s' },
|
||||
{ label: 'Transferências hoje', value: '23', detail: 'Tempo médio 4m 20s' },
|
||||
];
|
||||
|
||||
export const adminMetrics = [
|
||||
{ label: 'Usuários ativos', value: '64', detail: '8 supervisores configurados' },
|
||||
{ label: 'Áreas cadastradas', value: '3', detail: 'Suporte, Financeiro e Comercial' },
|
||||
{ label: 'Especialidades cadastradas', value: '3', detail: 'Suporte, Financeiro e Comercial' },
|
||||
{ label: 'Conteúdos IA', value: '28', detail: '6 aguardando revisão' },
|
||||
{ label: 'Canais conectados', value: '1', detail: 'WhatsApp em homologação' },
|
||||
];
|
||||
@ -100,7 +100,7 @@ export const aiContentRows = [
|
||||
id: 'c3',
|
||||
title: 'Argumentario de proposta comercial',
|
||||
area: 'Comercial',
|
||||
status: 'Revisao',
|
||||
status: 'Revisão',
|
||||
updatedAt: 'Segunda',
|
||||
},
|
||||
];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user