FEAT: Ajustes no na pagina do Admin
This commit is contained in:
parent
c4b846a079
commit
78b63e72c2
138
src/modules/attendance/pages/AgentNewAttendancePage.jsx
Normal file
138
src/modules/attendance/pages/AgentNewAttendancePage.jsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { BrandMark } from '../../../shared/components/BrandMark';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||
import { listContactProfiles } from '../../chat/services/contactProfileService';
|
||||
import { HomeSidebar } from '../../home/components/HomeSidebar';
|
||||
import { sidebarItems } from '../../home/services/homeMocks';
|
||||
import { NewAttendancePage } from './NewAttendancePage';
|
||||
|
||||
export function AgentNewAttendancePage() {
|
||||
const { isDesktop, isMobile } = useViewport();
|
||||
const userDisplay = getCurrentUserDisplay();
|
||||
const [contactCount, setContactCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
listContactProfiles()
|
||||
.then((items) => {
|
||||
if (isMounted) setContactCount(Array.isArray(items) ? items.length : 0);
|
||||
})
|
||||
.catch(() => {
|
||||
if (isMounted) setContactCount(0);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sidebarWithContactCount = useMemo(
|
||||
() => sidebarItems.map((item) => (item.id === 'contacts' ? { ...item, count: contactCount } : item)),
|
||||
[contactCount],
|
||||
);
|
||||
|
||||
return (
|
||||
<main style={{ minHeight: '100vh', padding: '1.5rem' }}>
|
||||
<section
|
||||
style={{
|
||||
width: 'min(1680px, calc(100vw - 3rem))',
|
||||
margin: '0 auto',
|
||||
background: 'var(--color-surface-strong)',
|
||||
borderRadius: '32px',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
padding: '1.5rem',
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isDesktop ? 'minmax(340px, 380px) minmax(0, 1fr)' : '1fr',
|
||||
gap: '1.5rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: '1.25rem' }}>
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '28px',
|
||||
padding: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<BrandMark size="lg" />
|
||||
</div>
|
||||
<HomeSidebar items={sidebarWithContactCount} activeItem="new-attendance" isMobile={!isDesktop} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '1.25rem', minWidth: 0 }}>
|
||||
<header
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '1.1rem 1.25rem',
|
||||
borderRadius: '22px',
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ margin: 0, fontSize: '1.65rem' }}>Abrir Atendimento</h1>
|
||||
<p style={{ margin: '0.45rem 0 0', color: 'var(--color-text-soft)' }}>
|
||||
Inicie um contato ativo por WhatsApp usando mensagens pré-aprovadas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.9rem',
|
||||
justifySelf: isMobile ? 'stretch' : 'end',
|
||||
justifyContent: isMobile ? 'space-between' : 'flex-end',
|
||||
padding: '0.85rem 1rem',
|
||||
borderRadius: '22px',
|
||||
background: '#fff',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<strong style={{ display: 'block' }}>{userDisplay.name}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.92rem' }}>
|
||||
Atendimento omnichannel
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: '16px',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: 'linear-gradient(135deg, var(--color-accent), var(--color-primary))',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
{userDisplay.initials}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<NewAttendancePage embedded />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -140,11 +140,11 @@ export function ContactProfilePanel({ isOpen, contact, onClose, onSaved }) {
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Empresa</span>
|
||||
<span style={{ fontWeight: 600 }}>Etiqueta de identificação</span>
|
||||
<input
|
||||
value={form.company}
|
||||
onChange={(event) => setForm((current) => ({ ...current, company: event.target.value }))}
|
||||
placeholder="Empresa ou conta vinculada"
|
||||
placeholder="Ex: Departamento, vaga ou conta vinculada"
|
||||
style={fieldStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
@ -56,7 +56,7 @@ function emptyDraft() {
|
||||
};
|
||||
}
|
||||
|
||||
export function ContactsPage() {
|
||||
export function ContactsPanel({ embedded = false }) {
|
||||
const { isDesktop, isMobile } = useViewport();
|
||||
const currentUser = getCurrentUser();
|
||||
const currentUserId = getUserId(currentUser);
|
||||
@ -146,6 +146,202 @@ export function ContactsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(320px, 0.85fr) minmax(0, 1fr)',
|
||||
gap: '1rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<aside
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 24,
|
||||
padding: '1rem',
|
||||
background: '#fff',
|
||||
display: 'grid',
|
||||
gap: '0.8rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem', alignItems: 'center' }}>
|
||||
<div>
|
||||
<strong style={{ display: 'block' }}>Agenda</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem' }}>
|
||||
{contacts.length} contato(s)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startNewContact}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 14,
|
||||
padding: '0.7rem 0.85rem',
|
||||
background: 'var(--color-highlight)',
|
||||
color: '#132534',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
>
|
||||
Novo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Buscar contato"
|
||||
style={inputStyle}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.45rem', maxHeight: 560, overflowY: 'auto', paddingRight: '0.2rem' }}>
|
||||
{filteredContacts.map((contact) => {
|
||||
const isSelected = selectedChatId === contact.chatId;
|
||||
return (
|
||||
<button
|
||||
key={contact.chatId}
|
||||
type="button"
|
||||
onClick={() => selectContact(contact)}
|
||||
style={{
|
||||
border: '1px solid',
|
||||
borderColor: isSelected ? 'rgba(0, 164, 183, 0.36)' : 'var(--color-border)',
|
||||
borderRadius: 16,
|
||||
padding: '0.8rem',
|
||||
background: isSelected ? 'rgba(0, 164, 183, 0.08)' : '#fff',
|
||||
textAlign: 'left',
|
||||
display: 'grid',
|
||||
gap: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<strong>{contact.name}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.88rem' }}>
|
||||
WhatsApp: +{contact.whatsappPhone}
|
||||
</span>
|
||||
{contact.email ? (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.84rem' }}>
|
||||
{contact.email}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!filteredContacts.length ? (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>
|
||||
Nenhum contato encontrado.
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<form
|
||||
onSubmit={handleSave}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 24,
|
||||
padding: '1.2rem',
|
||||
background: '#fff',
|
||||
display: 'grid',
|
||||
gap: '0.9rem',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong style={{ display: 'block', fontSize: '1.08rem' }}>
|
||||
{selectedChatId ? 'Editar contato' : 'Novo contato'}
|
||||
</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
O WhatsApp é usado para vincular o contato à conversa.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, minmax(0, 1fr))', gap: '0.85rem' }}>
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Nome</span>
|
||||
<input
|
||||
value={draft.name}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Etiqueta de identificação</span>
|
||||
<input
|
||||
value={draft.tag}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, tag: event.target.value }))}
|
||||
placeholder="Ex: Departamento, vaga ou conta vinculada"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Número WhatsApp</span>
|
||||
<input
|
||||
value={draft.whatsappPhone}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, whatsappPhone: event.target.value }))}
|
||||
placeholder="5511988267544"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Ligação/SMS</span>
|
||||
<input
|
||||
value={draft.callSmsPhone}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, callSmsPhone: event.target.value }))}
|
||||
placeholder="5511988267544"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>E-mail</span>
|
||||
<input
|
||||
type="email"
|
||||
value={draft.email}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, email: event.target.value }))}
|
||||
placeholder="nome@empresa.com"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Observação</span>
|
||||
<textarea
|
||||
rows={5}
|
||||
value={draft.note}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, note: event.target.value }))}
|
||||
style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.5 }}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{status ? <span style={{ color: status.includes('sucesso') ? 'var(--color-primary)' : '#b42318', fontWeight: 800 }}>{status}</span> : null}
|
||||
{isLoading ? <span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Carregando agenda...</span> : null}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 16,
|
||||
padding: '0.95rem 1rem',
|
||||
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
||||
color: '#fff',
|
||||
fontWeight: 900,
|
||||
opacity: isSaving ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{isSaving ? 'Salvando...' : 'Salvar contato'}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<main style={{ minHeight: '100vh', padding: '1.5rem' }}>
|
||||
<section
|
||||
@ -201,7 +397,7 @@ export function ContactsPage() {
|
||||
>
|
||||
<h1 style={{ margin: 0, fontSize: '1.65rem' }}>Contatos</h1>
|
||||
<p style={{ margin: '0.45rem 0 0', color: 'var(--color-text-soft)' }}>
|
||||
Agenda geral com WhatsApp, telefone para ligação/SMS, e-mail, tag e observação.
|
||||
Agenda geral com WhatsApp, telefone para ligação/SMS, e-mail, etiqueta e observação.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -242,198 +438,14 @@ export function ContactsPage() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'minmax(320px, 0.85fr) minmax(0, 1fr)',
|
||||
gap: '1rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<aside
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 24,
|
||||
padding: '1rem',
|
||||
background: '#fff',
|
||||
display: 'grid',
|
||||
gap: '0.8rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem', alignItems: 'center' }}>
|
||||
<div>
|
||||
<strong style={{ display: 'block' }}>Agenda</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem' }}>
|
||||
{contacts.length} contato(s)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={startNewContact}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 14,
|
||||
padding: '0.7rem 0.85rem',
|
||||
background: 'var(--color-highlight)',
|
||||
color: '#132534',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
>
|
||||
Novo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Buscar contato"
|
||||
style={inputStyle}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.45rem', maxHeight: 560, overflowY: 'auto', paddingRight: '0.2rem' }}>
|
||||
{filteredContacts.map((contact) => {
|
||||
const isSelected = selectedChatId === contact.chatId;
|
||||
return (
|
||||
<button
|
||||
key={contact.chatId}
|
||||
type="button"
|
||||
onClick={() => selectContact(contact)}
|
||||
style={{
|
||||
border: '1px solid',
|
||||
borderColor: isSelected ? 'rgba(0, 164, 183, 0.36)' : 'var(--color-border)',
|
||||
borderRadius: 16,
|
||||
padding: '0.8rem',
|
||||
background: isSelected ? 'rgba(0, 164, 183, 0.08)' : '#fff',
|
||||
textAlign: 'left',
|
||||
display: 'grid',
|
||||
gap: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<strong>{contact.name}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.88rem' }}>
|
||||
WhatsApp: +{contact.whatsappPhone}
|
||||
</span>
|
||||
{contact.email ? (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.84rem' }}>
|
||||
{contact.email}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!filteredContacts.length ? (
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>
|
||||
Nenhum contato encontrado.
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<form
|
||||
onSubmit={handleSave}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 24,
|
||||
padding: '1.2rem',
|
||||
background: '#fff',
|
||||
display: 'grid',
|
||||
gap: '0.9rem',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong style={{ display: 'block', fontSize: '1.08rem' }}>
|
||||
{selectedChatId ? 'Editar contato' : 'Novo contato'}
|
||||
</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
O WhatsApp é usado para vincular o contato à conversa.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, minmax(0, 1fr))', gap: '0.85rem' }}>
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Nome</span>
|
||||
<input
|
||||
value={draft.name}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Tag</span>
|
||||
<input
|
||||
value={draft.tag}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, tag: event.target.value }))}
|
||||
placeholder="Ex: Analista de TI III"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Número WhatsApp</span>
|
||||
<input
|
||||
value={draft.whatsappPhone}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, whatsappPhone: event.target.value }))}
|
||||
placeholder="5511988267544"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Ligação/SMS</span>
|
||||
<input
|
||||
value={draft.callSmsPhone}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, callSmsPhone: event.target.value }))}
|
||||
placeholder="5511988267544"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>E-mail</span>
|
||||
<input
|
||||
type="email"
|
||||
value={draft.email}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, email: event.target.value }))}
|
||||
placeholder="nome@empresa.com"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<span style={{ fontWeight: 700 }}>Observação</span>
|
||||
<textarea
|
||||
rows={5}
|
||||
value={draft.note}
|
||||
onChange={(event) => setDraft((current) => ({ ...current, note: event.target.value }))}
|
||||
style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.5 }}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{status ? <span style={{ color: status.includes('sucesso') ? 'var(--color-primary)' : '#b42318', fontWeight: 800 }}>{status}</span> : null}
|
||||
{isLoading ? <span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Carregando agenda...</span> : null}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 16,
|
||||
padding: '0.95rem 1rem',
|
||||
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
||||
color: '#fff',
|
||||
fontWeight: 900,
|
||||
opacity: isSaving ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{isSaving ? 'Salvando...' : 'Salvar contato'}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContactsPage() {
|
||||
return <ContactsPanel />;
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ const navigationBySection = {
|
||||
{ id: 'contacts', label: 'Contatos' },
|
||||
],
|
||||
admin: [
|
||||
{ id: 'home', label: 'Home' },
|
||||
{ id: 'today', label: 'Operação' },
|
||||
{ type: 'separator' },
|
||||
{ id: 'users-access', label: 'Usuários & Acessos' },
|
||||
@ -37,7 +36,7 @@ const navigationBySection = {
|
||||
|
||||
const actionLabelBySection = {
|
||||
supervisor: '+ Redistribuir atendimento',
|
||||
admin: 'Abrir painel do atendente',
|
||||
admin: 'Home',
|
||||
};
|
||||
|
||||
export function ManagementLayout({
|
||||
@ -108,7 +107,14 @@ export function ManagementLayout({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/home')}
|
||||
onClick={() => {
|
||||
if (activeSection === 'admin') {
|
||||
onNavItemChange?.('home');
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('/home');
|
||||
}}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '20px',
|
||||
@ -198,7 +204,7 @@ export function ManagementLayout({
|
||||
borderRadius: '18px',
|
||||
padding: '0.9rem 1rem',
|
||||
background: 'transparent',
|
||||
color: '#fff',
|
||||
color: '#ef4444',
|
||||
fontWeight: 700,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
|
||||
@ -135,22 +135,34 @@ function Badge({ children, tone }) {
|
||||
);
|
||||
}
|
||||
|
||||
function HourlyLineChart({ values }) {
|
||||
function HourlyBarChart({ 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={{
|
||||
height: 260,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${values.length}, minmax(10px, 1fr))`,
|
||||
gap: '0.65rem',
|
||||
alignItems: 'end',
|
||||
padding: '0.5rem 0 0',
|
||||
}}
|
||||
>
|
||||
{values.map((value, index) => (
|
||||
<div
|
||||
key={`${value}-${index}`}
|
||||
title={`${labels[index]}: ${value} atendimentos`}
|
||||
style={{
|
||||
height: `${Math.max(8, (value / maxValue) * 100)}%`,
|
||||
borderRadius: '10px 10px 4px 4px',
|
||||
background: 'linear-gradient(180deg, var(--color-secondary), rgba(181, 31, 31, 0.62))',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<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>
|
||||
@ -262,7 +274,7 @@ export function OperationalDashboard({ isDesktop, isMobile }) {
|
||||
</div>
|
||||
|
||||
<DataPanel title="Atendimentos finalizados por hora" description="Volume do dia entre 08h e 18h.">
|
||||
<HourlyLineChart values={data.hourly} />
|
||||
<HourlyBarChart values={data.hourly} />
|
||||
</DataPanel>
|
||||
|
||||
{assignmentTarget ? (
|
||||
|
||||
@ -9,6 +9,7 @@ import { TemplateManagementPanel } from '../components/TemplateManagementPanel';
|
||||
import { KnowledgeBasePanel } from '../components/KnowledgeBasePanel';
|
||||
import { MassMessagePanel } from '../components/MassMessagePanel';
|
||||
import { NewAttendancePage } from '../../attendance/pages/NewAttendancePage';
|
||||
import { ContactsPanel } from '../../home/pages/ContactsPage';
|
||||
import { AttendantOpsPanel } from '../../home/components/AttendantOpsPanel';
|
||||
import { MessagesWorkspace } from '../../home/components/MessagesWorkspace';
|
||||
import { useChat } from '../../chat/hooks/useChat';
|
||||
@ -21,6 +22,7 @@ import {
|
||||
getAccessOptions,
|
||||
getAccessUsers,
|
||||
getAdminOverview,
|
||||
getAiContentFile,
|
||||
getAiContents,
|
||||
getAttendantRanking,
|
||||
getAuditLogs,
|
||||
@ -59,6 +61,13 @@ const initialNotices = [
|
||||
{ id: 'n2', text: 'Templates de abertura ativa atualizados para WhatsApp.' },
|
||||
];
|
||||
|
||||
const defaultAiGuardrails = [
|
||||
'Não informar dados sensíveis sem validação do colaborador.',
|
||||
'Direcionar casos de assédio, denúncia ou risco trabalhista para atendimento humano.',
|
||||
'Não inventar políticas: responder apenas com base nos conteúdos cadastrados.',
|
||||
'Quando houver dúvida ou conflito de informação, encaminhar para especialista.',
|
||||
];
|
||||
|
||||
const integrationCards = [
|
||||
{
|
||||
id: 'whatsapp',
|
||||
@ -107,6 +116,23 @@ const integrationCards = [
|
||||
},
|
||||
];
|
||||
|
||||
const authProviderCards = [
|
||||
{
|
||||
id: 'ldap',
|
||||
name: 'LDAP / AD',
|
||||
icon: 'AD',
|
||||
color: '#003150',
|
||||
description: 'Autenticação corporativa via Active Directory para usuários internos.',
|
||||
},
|
||||
{
|
||||
id: 'microsoft',
|
||||
name: 'Microsoft OAuth',
|
||||
icon: 'MS',
|
||||
color: '#2563eb',
|
||||
description: 'Login com Microsoft Entra ID usando OAuth para contas corporativas.',
|
||||
},
|
||||
];
|
||||
|
||||
function formatMinutes(minutes) {
|
||||
if (minutes === null || minutes === undefined || Number.isNaN(Number(minutes))) return 'Sem dados';
|
||||
return `${Number(minutes)} min`;
|
||||
@ -202,6 +228,9 @@ export function AdminPage() {
|
||||
const [auditData, setAuditData] = useState({ page: 1, limit: 100, total: 0, items: [] });
|
||||
const [aiContents, setAiContents] = useState([]);
|
||||
const [aiContentForm, setAiContentForm] = useState({ title: '', areaId: '', notes: '', file: null });
|
||||
const [aiGuardrails, setAiGuardrails] = useState(defaultAiGuardrails);
|
||||
const [aiGuardrailDraft, setAiGuardrailDraft] = useState('');
|
||||
const [isOpeningAiContentId, setIsOpeningAiContentId] = useState(null);
|
||||
const [userSearch, setUserSearch] = useState('');
|
||||
const [newAreaName, setNewAreaName] = useState('');
|
||||
const [isLoadingAccess, setIsLoadingAccess] = useState(true);
|
||||
@ -220,6 +249,11 @@ export function AdminPage() {
|
||||
sharepoint: false,
|
||||
gupy: false,
|
||||
});
|
||||
const [authProviderStates, setAuthProviderStates] = useState({
|
||||
ldap: true,
|
||||
microsoft: false,
|
||||
});
|
||||
const [authConfigModal, setAuthConfigModal] = useState(null);
|
||||
const [integrationNotice, setIntegrationNotice] = useState('');
|
||||
const [configurationModal, setConfigurationModal] = useState(null);
|
||||
|
||||
@ -444,6 +478,38 @@ export function AdminPage() {
|
||||
});
|
||||
}
|
||||
|
||||
function base64ToBlob(base64, mimetype = 'application/octet-stream') {
|
||||
const binary = window.atob(base64 || '');
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return new Blob([bytes], { type: mimetype });
|
||||
}
|
||||
|
||||
async function openAiContent(contentId) {
|
||||
setIsOpeningAiContentId(contentId);
|
||||
try {
|
||||
const file = await getAiContentFile(contentId);
|
||||
if (!file?.content_base64) throw new Error('Arquivo não disponível.');
|
||||
|
||||
const blob = base64ToBlob(file.content_base64, file.mimetype);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = file.filename || `${file.title || 'conteudo-ia'}.txt`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
setAccessError('');
|
||||
} catch {
|
||||
setAccessError('Não foi possível baixar o conteúdo da IA.');
|
||||
} finally {
|
||||
setIsOpeningAiContentId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAiContent(event) {
|
||||
event.preventDefault();
|
||||
const title = aiContentForm.title.trim();
|
||||
@ -468,6 +534,15 @@ export function AdminPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function addAiGuardrail(event) {
|
||||
event.preventDefault();
|
||||
const text = aiGuardrailDraft.trim();
|
||||
if (!text) return;
|
||||
|
||||
setAiGuardrails((current) => [...current, text]);
|
||||
setAiGuardrailDraft('');
|
||||
}
|
||||
|
||||
async function removeAiContent(contentId) {
|
||||
const confirmed = window.confirm('Tem certeza que deseja remover este conteúdo da IA?');
|
||||
if (!confirmed) return;
|
||||
@ -693,20 +768,82 @@ export function AdminPage() {
|
||||
setNoticeDraft('');
|
||||
}
|
||||
|
||||
function renderLineChart() {
|
||||
function renderBarChart() {
|
||||
const maxValue = Math.max(...dailyAttendance);
|
||||
const points = dailyAttendance
|
||||
.map((value, index) => {
|
||||
const x = (index / (dailyAttendance.length - 1)) * 100;
|
||||
const y = 100 - (value / maxValue) * 86 - 7;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(' ');
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const currentMonth = today.getMonth();
|
||||
const monthLabel = today.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' });
|
||||
const dateLabels = dailyAttendance.map((_, index) => {
|
||||
const date = new Date(currentYear, currentMonth, index + 1);
|
||||
return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
|
||||
});
|
||||
const visibleDateLabels = [
|
||||
{ index: 0, label: dateLabels[0] },
|
||||
{ index: Math.floor(dateLabels.length / 2), label: dateLabels[Math.floor(dateLabels.length / 2)] },
|
||||
{ index: dateLabels.length - 1, label: dateLabels[dateLabels.length - 1] },
|
||||
];
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ width: '100%', height: 260 }}>
|
||||
<polyline points={points} fill="none" stroke="var(--color-secondary)" strokeWidth="2.2" vectorEffect="non-scaling-stroke" />
|
||||
</svg>
|
||||
<div style={{ display: 'grid', gap: '0.65rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontWeight: 800,
|
||||
fontSize: '0.88rem',
|
||||
}}
|
||||
>
|
||||
<span>{monthLabel}</span>
|
||||
<span>{dateLabels[0]} a {dateLabels[dateLabels.length - 1]}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: 240,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${dailyAttendance.length}, minmax(4px, 1fr))`,
|
||||
gap: '0.28rem',
|
||||
alignItems: 'end',
|
||||
padding: '0.5rem 0 0',
|
||||
}}
|
||||
>
|
||||
{dailyAttendance.map((value, index) => (
|
||||
<div
|
||||
key={`${value}-${index}`}
|
||||
title={`${dateLabels[index]}: ${value} atendimentos`}
|
||||
style={{
|
||||
height: `${Math.max(8, (value / maxValue) * 100)}%`,
|
||||
borderRadius: '8px 8px 3px 3px',
|
||||
background: 'linear-gradient(180deg, var(--color-secondary), rgba(181, 31, 31, 0.62))',
|
||||
minWidth: 4,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${dailyAttendance.length}, minmax(4px, 1fr))`,
|
||||
gap: '0.28rem',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
{dailyAttendance.map((_, index) => {
|
||||
const visibleLabel = visibleDateLabels.find((item) => item.index === index);
|
||||
return (
|
||||
<span key={index} style={{ textAlign: index === 0 ? 'left' : index === dailyAttendance.length - 1 ? 'right' : 'center' }}>
|
||||
{visibleLabel?.label || ''}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -768,7 +905,7 @@ export function AdminPage() {
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isDesktop ? 'minmax(0, 1.85fr) minmax(300px, 1fr)' : '1fr', gap: '1rem' }}>
|
||||
<DataPanel title="Atendimentos por dia" description="Volume diário do mês selecionado.">
|
||||
{renderLineChart()}
|
||||
{renderBarChart()}
|
||||
</DataPanel>
|
||||
<DataPanel title="Distribuição por canal" description="Participação mensal por canal.">
|
||||
{renderDonutChart()}
|
||||
@ -1173,29 +1310,83 @@ export function AdminPage() {
|
||||
}
|
||||
|
||||
function renderAiContents() {
|
||||
function getFileType(row) {
|
||||
const filename = String(row.filename || '');
|
||||
const extension = filename.includes('.') ? filename.split('.').pop().toUpperCase() : '';
|
||||
if (extension) return extension;
|
||||
if (row.mimetype?.includes('pdf')) return 'PDF';
|
||||
if (row.mimetype?.includes('word')) return 'DOC';
|
||||
if (row.mimetype?.includes('text')) return 'TXT';
|
||||
return 'Arquivo';
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ key: 'title', label: 'Conteúdo' },
|
||||
{ key: 'area_nome', label: 'Especialidade', render: (row) => row.area_nome || 'Geral' },
|
||||
{ key: 'filename', label: 'Arquivo' },
|
||||
{
|
||||
key: 'filename',
|
||||
label: 'Arquivo',
|
||||
render: (row) => (
|
||||
<div style={{ display: 'grid', gap: '0.18rem', minWidth: 0 }}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isOpeningAiContentId === row.id}
|
||||
onClick={() => openAiContent(row.id)}
|
||||
style={{
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
background: 'transparent',
|
||||
color: '#2563eb',
|
||||
fontWeight: 700,
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
opacity: isOpeningAiContentId === row.id ? 0.6 : 1,
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: 3,
|
||||
width: 'fit-content',
|
||||
}}
|
||||
title={row.filename || 'Baixar arquivo'}
|
||||
>
|
||||
Baixar documento
|
||||
</button>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--color-text-soft)',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 700,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 180,
|
||||
}}
|
||||
title={row.filename || getFileType(row)}
|
||||
>
|
||||
{getFileType(row)}{row.filename ? ` · ${row.filename}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'status', label: 'Status', render: () => 'Disponível para consulta' },
|
||||
{
|
||||
key: 'actions',
|
||||
label: 'Ações',
|
||||
render: (row) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeAiContent(row.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 style={{ display: 'flex', gap: '0.45rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeAiContent(row.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>
|
||||
),
|
||||
},
|
||||
];
|
||||
@ -1264,11 +1455,66 @@ export function AdminPage() {
|
||||
</DataPanel>
|
||||
|
||||
<DataPanel title="Regras e travas" description="Diretrizes de segurança para a IA respeitar durante respostas ao colaborador.">
|
||||
<div style={{ display: 'grid', gap: '0.65rem', color: 'var(--color-text-soft)', fontWeight: 700 }}>
|
||||
<span>Não informar dados sensíveis sem validação do colaborador.</span>
|
||||
<span>Direcionar casos de assédio, denúncia ou risco trabalhista para atendimento humano.</span>
|
||||
<span>Não inventar políticas: responder apenas com base nos conteúdos cadastrados.</span>
|
||||
<span>Quando houver dúvida ou conflito de informação, encaminhar para especialista.</span>
|
||||
<form onSubmit={addAiGuardrail} style={{ display: 'grid', gap: '0.75rem' }}>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={aiGuardrailDraft}
|
||||
onChange={(event) => setAiGuardrailDraft(event.target.value)}
|
||||
placeholder="Adicionar regra ou trava. Ex: Não responder dúvidas trabalhistas sem encaminhar para especialista."
|
||||
style={{ ...selectStyle, resize: 'vertical', lineHeight: 1.5 }}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 16,
|
||||
padding: '0.85rem 1rem',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Adicionar regra
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.55rem' }}>
|
||||
{aiGuardrails.map((rule, index) => (
|
||||
<div
|
||||
key={`${rule}-${index}`}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 16,
|
||||
padding: '0.85rem 0.9rem',
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '0.85rem',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700, lineHeight: 1.45 }}>
|
||||
{rule}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAiGuardrails((current) => current.filter((_, itemIndex) => itemIndex !== index))}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 12,
|
||||
padding: '0.45rem 0.65rem',
|
||||
background: 'rgba(181, 31, 31, 0.1)',
|
||||
color: 'var(--color-secondary)',
|
||||
fontWeight: 800,
|
||||
flex: '0 0 auto',
|
||||
}}
|
||||
>
|
||||
Remover
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DataPanel>
|
||||
</section>
|
||||
@ -1578,6 +1824,243 @@ export function AdminPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function renderSettings() {
|
||||
const enabledCount = authProviderCards.filter((item) => authProviderStates[item.id]).length;
|
||||
|
||||
return (
|
||||
<section style={{ display: 'grid', gap: '1rem' }}>
|
||||
<DataPanel
|
||||
title="Configurações de Login"
|
||||
description="Provedores de autenticação disponíveis para acesso ao ambiente."
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, minmax(0, 1fr))',
|
||||
gap: '0.8rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 18,
|
||||
padding: '1rem',
|
||||
background: '#f8fbfc',
|
||||
display: 'grid',
|
||||
gap: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Provedores ativos</span>
|
||||
<strong style={{ fontSize: '1.55rem' }}>{enabledCount}</strong>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 18,
|
||||
padding: '1rem',
|
||||
background: '#f8fbfc',
|
||||
display: 'grid',
|
||||
gap: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Segurança</span>
|
||||
<strong style={{ fontSize: '1.05rem' }}>Login corporativo</strong>
|
||||
</div>
|
||||
</div>
|
||||
</DataPanel>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, minmax(0, 1fr))',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
{authProviderCards.map((item) => {
|
||||
const isEnabled = Boolean(authProviderStates[item.id]);
|
||||
|
||||
return (
|
||||
<article
|
||||
key={item.id}
|
||||
style={{
|
||||
border: `1px solid ${isEnabled ? `${item.color}55` : 'var(--color-border)'}`,
|
||||
borderRadius: 22,
|
||||
padding: '1.1rem',
|
||||
background: isEnabled ? `${item.color}0f` : '#fff',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
minHeight: 220,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '0.85rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', minWidth: 0 }}>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 16,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: item.color,
|
||||
color: '#fff',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
borderRadius: 999,
|
||||
padding: '0.18rem 0.55rem',
|
||||
background: `${item.color}18`,
|
||||
color: item.color,
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</span>
|
||||
<strong style={{ display: 'block', marginTop: '0.35rem', fontSize: '1.15rem' }}>{item.name}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={isEnabled}
|
||||
onClick={() => {
|
||||
setAuthProviderStates((current) => ({
|
||||
...current,
|
||||
[item.id]: !current[item.id],
|
||||
}));
|
||||
}}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 999,
|
||||
width: 54,
|
||||
height: 30,
|
||||
padding: 3,
|
||||
background: isEnabled ? item.color : '#d6e0e5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: isEnabled ? 'flex-end' : 'flex-start',
|
||||
cursor: 'pointer',
|
||||
flex: '0 0 auto',
|
||||
}}
|
||||
title={isEnabled ? `Desativar ${item.name}` : `Ativar ${item.name}`}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
background: '#fff',
|
||||
boxShadow: '0 3px 8px rgba(0, 0, 0, 0.18)',
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p style={{ margin: 0, color: 'var(--color-text-soft)', lineHeight: 1.5, fontWeight: 650 }}>
|
||||
{item.description}
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
paddingTop: '0.85rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '0.75rem',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: isEnabled ? item.color : 'var(--color-text-soft)', fontWeight: 800 }}>
|
||||
{isEnabled ? 'Ativo' : 'Inativo'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAuthConfigModal(item)}
|
||||
style={{
|
||||
border: `1px solid ${item.color}44`,
|
||||
borderRadius: 14,
|
||||
padding: '0.65rem 0.8rem',
|
||||
background: '#fff',
|
||||
color: item.color,
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Configurar
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{authConfigModal ? (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="auth-config-title"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 20, 32, 0.42)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
padding: '1rem',
|
||||
zIndex: 50,
|
||||
}}
|
||||
onClick={() => setAuthConfigModal(null)}
|
||||
>
|
||||
<div
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{
|
||||
width: 'min(460px, 100%)',
|
||||
borderRadius: 22,
|
||||
border: '1px solid var(--color-border)',
|
||||
background: '#fff',
|
||||
padding: '1.25rem',
|
||||
display: 'grid',
|
||||
gap: '0.9rem',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
}}
|
||||
>
|
||||
<strong id="auth-config-title" style={{ display: 'block', fontSize: '1.08rem' }}>
|
||||
Configurar {authConfigModal.name}
|
||||
</strong>
|
||||
<p style={{ margin: 0, color: 'var(--color-text-soft)', lineHeight: 1.5, fontWeight: 700 }}>
|
||||
Configure os parâmetros, credenciais e regras do provedor de login para controlar o acesso ao ambiente.
|
||||
</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAuthConfigModal(null)}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: 14,
|
||||
padding: '0.75rem 0.95rem',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Entendi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const sectionContent = {
|
||||
home: renderMonthlyHome(),
|
||||
today: <OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />,
|
||||
@ -1597,8 +2080,8 @@ export function AdminPage() {
|
||||
),
|
||||
'new-attendance': <NewAttendancePage embedded />,
|
||||
'mass-message': <MassMessagePanel areas={areas} mode="admin" isMobile={isMobile} />,
|
||||
contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
|
||||
settings: renderPlaceholder('Configurações', 'Preferencias e parametros do ambiente.'),
|
||||
contacts: <ContactsPanel embedded />,
|
||||
settings: renderSettings(),
|
||||
};
|
||||
|
||||
const pageTitle = activeAdminSection === 'home'
|
||||
@ -1613,8 +2096,12 @@ export function AdminPage() {
|
||||
? 'Auditoria'
|
||||
: activeAdminSection === 'channels'
|
||||
? 'Canais e Integração'
|
||||
: activeAdminSection === 'ai-contents'
|
||||
? 'Conteúdos da IA'
|
||||
: activeAdminSection === 'contacts'
|
||||
? 'Contatos'
|
||||
: activeAdminSection === 'settings'
|
||||
? 'Configurações'
|
||||
: activeAdminSection === 'ai-contents'
|
||||
? 'Conteúdos da IA'
|
||||
: activeAdminSection === 'knowledge'
|
||||
? 'Fluxo do Bot'
|
||||
: 'Painel administrativo';
|
||||
@ -1631,8 +2118,12 @@ export function AdminPage() {
|
||||
? 'Logs administrativos e operacionais com paginação de 100 eventos.'
|
||||
: activeAdminSection === 'channels'
|
||||
? 'Canais de atendimento e integrações que alimentam a operação e a IA.'
|
||||
: activeAdminSection === 'ai-contents'
|
||||
? 'Base de documentos que será consultada pela IA em fase de testes.'
|
||||
: activeAdminSection === 'contacts'
|
||||
? 'Agenda geral com WhatsApp, telefone, e-mail, etiqueta e observação.'
|
||||
: activeAdminSection === 'settings'
|
||||
? 'Configurações de autenticação e acesso ao ambiente.'
|
||||
: activeAdminSection === 'ai-contents'
|
||||
? 'Base de documentos que será consultada pela IA em fase de testes.'
|
||||
: activeAdminSection === 'knowledge'
|
||||
? 'Árvore de decisão configurável para roteamento do Agente Virtual Sothis.'
|
||||
: 'Controle operacional e configurações administrativas.';
|
||||
|
||||
@ -6,6 +6,7 @@ import { KnowledgeBasePanel } from '../components/KnowledgeBasePanel';
|
||||
import { MassMessagePanel } from '../components/MassMessagePanel';
|
||||
import { DataPanel } from '../components/DataPanel';
|
||||
import { NewAttendancePage } from '../../attendance/pages/NewAttendancePage';
|
||||
import { ContactsPanel } from '../../home/pages/ContactsPage';
|
||||
import { getAccessOptions } from '../services/adminAccessService';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { getCurrentUser, getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||
@ -97,7 +98,7 @@ export function SupervisorPage() {
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
),
|
||||
contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
|
||||
contacts: <ContactsPanel embedded />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -44,6 +44,10 @@ export async function getAiContents() {
|
||||
return request('/admin/access/ai-contents');
|
||||
}
|
||||
|
||||
export async function getAiContentFile(id) {
|
||||
return request(`/admin/access/ai-contents/${id}/file`);
|
||||
}
|
||||
|
||||
export async function createAiContent(payload) {
|
||||
return request('/admin/access/ai-contents', {
|
||||
method: 'POST',
|
||||
|
||||
@ -5,7 +5,7 @@ import { AgentMassMessagePage } from '../modules/home/pages/AgentMassMessagePage
|
||||
import { ContactsPage } from '../modules/home/pages/ContactsPage';
|
||||
import { ChatPage } from '../modules/chat/pages/ChatPage';
|
||||
import { CallPage } from '../modules/call/pages/CallPage';
|
||||
import { NewAttendancePage } from '../modules/attendance/pages/NewAttendancePage';
|
||||
import { AgentNewAttendancePage } from '../modules/attendance/pages/AgentNewAttendancePage';
|
||||
import { WhatsappAdminPage } from '../modules/management/pages/WhatsappAdminPage';
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
@ -31,7 +31,7 @@ export const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: '/new-attendance',
|
||||
element: <NewAttendancePage />,
|
||||
element: <AgentNewAttendancePage />,
|
||||
},
|
||||
{
|
||||
path: '/mass-message',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user