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>
|
||||||
|
|
||||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||||
<span style={{ fontWeight: 600 }}>Empresa</span>
|
<span style={{ fontWeight: 600 }}>Etiqueta de identificação</span>
|
||||||
<input
|
<input
|
||||||
value={form.company}
|
value={form.company}
|
||||||
onChange={(event) => setForm((current) => ({ ...current, company: event.target.value }))}
|
onChange={(event) => setForm((current) => ({ ...current, company: event.target.value }))}
|
||||||
placeholder="Empresa ou conta vinculada"
|
placeholder="Ex: Departamento, vaga ou conta vinculada"
|
||||||
style={fieldStyle}
|
style={fieldStyle}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -56,7 +56,7 @@ function emptyDraft() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContactsPage() {
|
export function ContactsPanel({ embedded = false }) {
|
||||||
const { isDesktop, isMobile } = useViewport();
|
const { isDesktop, isMobile } = useViewport();
|
||||||
const currentUser = getCurrentUser();
|
const currentUser = getCurrentUser();
|
||||||
const currentUserId = getUserId(currentUser);
|
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 (
|
return (
|
||||||
<main style={{ minHeight: '100vh', padding: '1.5rem' }}>
|
<main style={{ minHeight: '100vh', padding: '1.5rem' }}>
|
||||||
<section
|
<section
|
||||||
@ -201,7 +397,7 @@ export function ContactsPage() {
|
|||||||
>
|
>
|
||||||
<h1 style={{ margin: 0, fontSize: '1.65rem' }}>Contatos</h1>
|
<h1 style={{ margin: 0, fontSize: '1.65rem' }}>Contatos</h1>
|
||||||
<p style={{ margin: '0.45rem 0 0', color: 'var(--color-text-soft)' }}>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -242,198 +438,14 @@ export function ContactsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section
|
{content}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ContactsPage() {
|
||||||
|
return <ContactsPanel />;
|
||||||
|
}
|
||||||
|
|||||||
@ -16,7 +16,6 @@ const navigationBySection = {
|
|||||||
{ id: 'contacts', label: 'Contatos' },
|
{ id: 'contacts', label: 'Contatos' },
|
||||||
],
|
],
|
||||||
admin: [
|
admin: [
|
||||||
{ id: 'home', label: 'Home' },
|
|
||||||
{ id: 'today', label: 'Operação' },
|
{ id: 'today', label: 'Operação' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ id: 'users-access', label: 'Usuários & Acessos' },
|
{ id: 'users-access', label: 'Usuários & Acessos' },
|
||||||
@ -37,7 +36,7 @@ const navigationBySection = {
|
|||||||
|
|
||||||
const actionLabelBySection = {
|
const actionLabelBySection = {
|
||||||
supervisor: '+ Redistribuir atendimento',
|
supervisor: '+ Redistribuir atendimento',
|
||||||
admin: 'Abrir painel do atendente',
|
admin: 'Home',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ManagementLayout({
|
export function ManagementLayout({
|
||||||
@ -108,7 +107,14 @@ export function ManagementLayout({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate('/home')}
|
onClick={() => {
|
||||||
|
if (activeSection === 'admin') {
|
||||||
|
onNavItemChange?.('home');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate('/home');
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
@ -198,7 +204,7 @@ export function ManagementLayout({
|
|||||||
borderRadius: '18px',
|
borderRadius: '18px',
|
||||||
padding: '0.9rem 1rem',
|
padding: '0.9rem 1rem',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
color: '#fff',
|
color: '#ef4444',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
textAlign: 'left',
|
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 labels = ['08h', '09h', '10h', '11h', '12h', '13h', '14h', '15h', '16h', '17h', '18h'];
|
||||||
const maxValue = Math.max(...values, 1);
|
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 (
|
return (
|
||||||
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
||||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ width: '100%', height: 260 }}>
|
<div
|
||||||
<polyline points={points} fill="none" stroke="var(--color-secondary)" strokeWidth="2.4" vectorEffect="non-scaling-stroke" />
|
style={{
|
||||||
</svg>
|
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 }}>
|
<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) => (
|
{labels.map((label) => (
|
||||||
<span key={label} style={{ textAlign: 'center' }}>{label}</span>
|
<span key={label} style={{ textAlign: 'center' }}>{label}</span>
|
||||||
@ -262,7 +274,7 @@ export function OperationalDashboard({ isDesktop, isMobile }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataPanel title="Atendimentos finalizados por hora" description="Volume do dia entre 08h e 18h.">
|
<DataPanel title="Atendimentos finalizados por hora" description="Volume do dia entre 08h e 18h.">
|
||||||
<HourlyLineChart values={data.hourly} />
|
<HourlyBarChart values={data.hourly} />
|
||||||
</DataPanel>
|
</DataPanel>
|
||||||
|
|
||||||
{assignmentTarget ? (
|
{assignmentTarget ? (
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { TemplateManagementPanel } from '../components/TemplateManagementPanel';
|
|||||||
import { KnowledgeBasePanel } from '../components/KnowledgeBasePanel';
|
import { KnowledgeBasePanel } from '../components/KnowledgeBasePanel';
|
||||||
import { MassMessagePanel } from '../components/MassMessagePanel';
|
import { MassMessagePanel } from '../components/MassMessagePanel';
|
||||||
import { NewAttendancePage } from '../../attendance/pages/NewAttendancePage';
|
import { NewAttendancePage } from '../../attendance/pages/NewAttendancePage';
|
||||||
|
import { ContactsPanel } from '../../home/pages/ContactsPage';
|
||||||
import { AttendantOpsPanel } from '../../home/components/AttendantOpsPanel';
|
import { AttendantOpsPanel } from '../../home/components/AttendantOpsPanel';
|
||||||
import { MessagesWorkspace } from '../../home/components/MessagesWorkspace';
|
import { MessagesWorkspace } from '../../home/components/MessagesWorkspace';
|
||||||
import { useChat } from '../../chat/hooks/useChat';
|
import { useChat } from '../../chat/hooks/useChat';
|
||||||
@ -21,6 +22,7 @@ import {
|
|||||||
getAccessOptions,
|
getAccessOptions,
|
||||||
getAccessUsers,
|
getAccessUsers,
|
||||||
getAdminOverview,
|
getAdminOverview,
|
||||||
|
getAiContentFile,
|
||||||
getAiContents,
|
getAiContents,
|
||||||
getAttendantRanking,
|
getAttendantRanking,
|
||||||
getAuditLogs,
|
getAuditLogs,
|
||||||
@ -59,6 +61,13 @@ const initialNotices = [
|
|||||||
{ id: 'n2', text: 'Templates de abertura ativa atualizados para WhatsApp.' },
|
{ 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 = [
|
const integrationCards = [
|
||||||
{
|
{
|
||||||
id: 'whatsapp',
|
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) {
|
function formatMinutes(minutes) {
|
||||||
if (minutes === null || minutes === undefined || Number.isNaN(Number(minutes))) return 'Sem dados';
|
if (minutes === null || minutes === undefined || Number.isNaN(Number(minutes))) return 'Sem dados';
|
||||||
return `${Number(minutes)} min`;
|
return `${Number(minutes)} min`;
|
||||||
@ -202,6 +228,9 @@ export function AdminPage() {
|
|||||||
const [auditData, setAuditData] = useState({ page: 1, limit: 100, total: 0, items: [] });
|
const [auditData, setAuditData] = useState({ page: 1, limit: 100, total: 0, items: [] });
|
||||||
const [aiContents, setAiContents] = useState([]);
|
const [aiContents, setAiContents] = useState([]);
|
||||||
const [aiContentForm, setAiContentForm] = useState({ title: '', areaId: '', notes: '', file: null });
|
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 [userSearch, setUserSearch] = useState('');
|
||||||
const [newAreaName, setNewAreaName] = useState('');
|
const [newAreaName, setNewAreaName] = useState('');
|
||||||
const [isLoadingAccess, setIsLoadingAccess] = useState(true);
|
const [isLoadingAccess, setIsLoadingAccess] = useState(true);
|
||||||
@ -220,6 +249,11 @@ export function AdminPage() {
|
|||||||
sharepoint: false,
|
sharepoint: false,
|
||||||
gupy: false,
|
gupy: false,
|
||||||
});
|
});
|
||||||
|
const [authProviderStates, setAuthProviderStates] = useState({
|
||||||
|
ldap: true,
|
||||||
|
microsoft: false,
|
||||||
|
});
|
||||||
|
const [authConfigModal, setAuthConfigModal] = useState(null);
|
||||||
const [integrationNotice, setIntegrationNotice] = useState('');
|
const [integrationNotice, setIntegrationNotice] = useState('');
|
||||||
const [configurationModal, setConfigurationModal] = useState(null);
|
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) {
|
async function submitAiContent(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const title = aiContentForm.title.trim();
|
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) {
|
async function removeAiContent(contentId) {
|
||||||
const confirmed = window.confirm('Tem certeza que deseja remover este conteúdo da IA?');
|
const confirmed = window.confirm('Tem certeza que deseja remover este conteúdo da IA?');
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
@ -693,20 +768,82 @@ export function AdminPage() {
|
|||||||
setNoticeDraft('');
|
setNoticeDraft('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLineChart() {
|
function renderBarChart() {
|
||||||
const maxValue = Math.max(...dailyAttendance);
|
const maxValue = Math.max(...dailyAttendance);
|
||||||
const points = dailyAttendance
|
const today = new Date();
|
||||||
.map((value, index) => {
|
const currentYear = today.getFullYear();
|
||||||
const x = (index / (dailyAttendance.length - 1)) * 100;
|
const currentMonth = today.getMonth();
|
||||||
const y = 100 - (value / maxValue) * 86 - 7;
|
const monthLabel = today.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' });
|
||||||
return `${x},${y}`;
|
const dateLabels = dailyAttendance.map((_, index) => {
|
||||||
})
|
const date = new Date(currentYear, currentMonth, index + 1);
|
||||||
.join(' ');
|
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 (
|
return (
|
||||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ width: '100%', height: 260 }}>
|
<div style={{ display: 'grid', gap: '0.65rem' }}>
|
||||||
<polyline points={points} fill="none" stroke="var(--color-secondary)" strokeWidth="2.2" vectorEffect="non-scaling-stroke" />
|
<div
|
||||||
</svg>
|
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' }}>
|
<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.">
|
<DataPanel title="Atendimentos por dia" description="Volume diário do mês selecionado.">
|
||||||
{renderLineChart()}
|
{renderBarChart()}
|
||||||
</DataPanel>
|
</DataPanel>
|
||||||
<DataPanel title="Distribuição por canal" description="Participação mensal por canal.">
|
<DataPanel title="Distribuição por canal" description="Participação mensal por canal.">
|
||||||
{renderDonutChart()}
|
{renderDonutChart()}
|
||||||
@ -1173,29 +1310,83 @@ export function AdminPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAiContents() {
|
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 = [
|
const columns = [
|
||||||
{ key: 'title', label: 'Conteúdo' },
|
{ key: 'title', label: 'Conteúdo' },
|
||||||
{ key: 'area_nome', label: 'Especialidade', render: (row) => row.area_nome || 'Geral' },
|
{ 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: 'status', label: 'Status', render: () => 'Disponível para consulta' },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
label: 'Ações',
|
label: 'Ações',
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<button
|
<div style={{ display: 'flex', gap: '0.45rem', flexWrap: 'wrap' }}>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => removeAiContent(row.id)}
|
type="button"
|
||||||
style={{
|
onClick={() => removeAiContent(row.id)}
|
||||||
border: 'none',
|
style={{
|
||||||
borderRadius: 12,
|
border: 'none',
|
||||||
padding: '0.55rem 0.7rem',
|
borderRadius: 12,
|
||||||
background: 'rgba(181, 31, 31, 0.1)',
|
padding: '0.55rem 0.7rem',
|
||||||
color: 'var(--color-secondary)',
|
background: 'rgba(181, 31, 31, 0.1)',
|
||||||
fontWeight: 800,
|
color: 'var(--color-secondary)',
|
||||||
}}
|
fontWeight: 800,
|
||||||
>
|
}}
|
||||||
Remover
|
>
|
||||||
</button>
|
Remover
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -1264,11 +1455,66 @@ export function AdminPage() {
|
|||||||
</DataPanel>
|
</DataPanel>
|
||||||
|
|
||||||
<DataPanel title="Regras e travas" description="Diretrizes de segurança para a IA respeitar durante respostas ao colaborador.">
|
<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 }}>
|
<form onSubmit={addAiGuardrail} style={{ display: 'grid', gap: '0.75rem' }}>
|
||||||
<span>Não informar dados sensíveis sem validação do colaborador.</span>
|
<textarea
|
||||||
<span>Direcionar casos de assédio, denúncia ou risco trabalhista para atendimento humano.</span>
|
rows={3}
|
||||||
<span>Não inventar políticas: responder apenas com base nos conteúdos cadastrados.</span>
|
value={aiGuardrailDraft}
|
||||||
<span>Quando houver dúvida ou conflito de informação, encaminhar para especialista.</span>
|
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>
|
</div>
|
||||||
</DataPanel>
|
</DataPanel>
|
||||||
</section>
|
</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 = {
|
const sectionContent = {
|
||||||
home: renderMonthlyHome(),
|
home: renderMonthlyHome(),
|
||||||
today: <OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />,
|
today: <OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />,
|
||||||
@ -1597,8 +2080,8 @@ export function AdminPage() {
|
|||||||
),
|
),
|
||||||
'new-attendance': <NewAttendancePage embedded />,
|
'new-attendance': <NewAttendancePage embedded />,
|
||||||
'mass-message': <MassMessagePanel areas={areas} mode="admin" isMobile={isMobile} />,
|
'mass-message': <MassMessagePanel areas={areas} mode="admin" isMobile={isMobile} />,
|
||||||
contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
|
contacts: <ContactsPanel embedded />,
|
||||||
settings: renderPlaceholder('Configurações', 'Preferencias e parametros do ambiente.'),
|
settings: renderSettings(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageTitle = activeAdminSection === 'home'
|
const pageTitle = activeAdminSection === 'home'
|
||||||
@ -1613,8 +2096,12 @@ export function AdminPage() {
|
|||||||
? 'Auditoria'
|
? 'Auditoria'
|
||||||
: activeAdminSection === 'channels'
|
: activeAdminSection === 'channels'
|
||||||
? 'Canais e Integração'
|
? 'Canais e Integração'
|
||||||
: activeAdminSection === 'ai-contents'
|
: activeAdminSection === 'contacts'
|
||||||
? 'Conteúdos da IA'
|
? 'Contatos'
|
||||||
|
: activeAdminSection === 'settings'
|
||||||
|
? 'Configurações'
|
||||||
|
: activeAdminSection === 'ai-contents'
|
||||||
|
? 'Conteúdos da IA'
|
||||||
: activeAdminSection === 'knowledge'
|
: activeAdminSection === 'knowledge'
|
||||||
? 'Fluxo do Bot'
|
? 'Fluxo do Bot'
|
||||||
: 'Painel administrativo';
|
: 'Painel administrativo';
|
||||||
@ -1631,8 +2118,12 @@ export function AdminPage() {
|
|||||||
? 'Logs administrativos e operacionais com paginação de 100 eventos.'
|
? 'Logs administrativos e operacionais com paginação de 100 eventos.'
|
||||||
: activeAdminSection === 'channels'
|
: activeAdminSection === 'channels'
|
||||||
? 'Canais de atendimento e integrações que alimentam a operação e a IA.'
|
? 'Canais de atendimento e integrações que alimentam a operação e a IA.'
|
||||||
: activeAdminSection === 'ai-contents'
|
: activeAdminSection === 'contacts'
|
||||||
? 'Base de documentos que será consultada pela IA em fase de testes.'
|
? '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'
|
: activeAdminSection === 'knowledge'
|
||||||
? 'Árvore de decisão configurável para roteamento do Agente Virtual Sothis.'
|
? 'Árvore de decisão configurável para roteamento do Agente Virtual Sothis.'
|
||||||
: 'Controle operacional e configurações administrativas.';
|
: 'Controle operacional e configurações administrativas.';
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { KnowledgeBasePanel } from '../components/KnowledgeBasePanel';
|
|||||||
import { MassMessagePanel } from '../components/MassMessagePanel';
|
import { MassMessagePanel } from '../components/MassMessagePanel';
|
||||||
import { DataPanel } from '../components/DataPanel';
|
import { DataPanel } from '../components/DataPanel';
|
||||||
import { NewAttendancePage } from '../../attendance/pages/NewAttendancePage';
|
import { NewAttendancePage } from '../../attendance/pages/NewAttendancePage';
|
||||||
|
import { ContactsPanel } from '../../home/pages/ContactsPage';
|
||||||
import { getAccessOptions } from '../services/adminAccessService';
|
import { getAccessOptions } from '../services/adminAccessService';
|
||||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||||
import { getCurrentUser, getCurrentUserDisplay } from '../../auth/services/sessionService';
|
import { getCurrentUser, getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||||
@ -97,7 +98,7 @@ export function SupervisorPage() {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
|
contacts: <ContactsPanel embedded />,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -44,6 +44,10 @@ export async function getAiContents() {
|
|||||||
return request('/admin/access/ai-contents');
|
return request('/admin/access/ai-contents');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAiContentFile(id) {
|
||||||
|
return request(`/admin/access/ai-contents/${id}/file`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function createAiContent(payload) {
|
export async function createAiContent(payload) {
|
||||||
return request('/admin/access/ai-contents', {
|
return request('/admin/access/ai-contents', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { AgentMassMessagePage } from '../modules/home/pages/AgentMassMessagePage
|
|||||||
import { ContactsPage } from '../modules/home/pages/ContactsPage';
|
import { ContactsPage } from '../modules/home/pages/ContactsPage';
|
||||||
import { ChatPage } from '../modules/chat/pages/ChatPage';
|
import { ChatPage } from '../modules/chat/pages/ChatPage';
|
||||||
import { CallPage } from '../modules/call/pages/CallPage';
|
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';
|
import { WhatsappAdminPage } from '../modules/management/pages/WhatsappAdminPage';
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
@ -31,7 +31,7 @@ export const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/new-attendance',
|
path: '/new-attendance',
|
||||||
element: <NewAttendancePage />,
|
element: <AgentNewAttendancePage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/mass-message',
|
path: '/mass-message',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user