-
-
-
+
+ {values.map((value, index) => (
+
+ ))}
+
{labels.map((label) => (
{label}
@@ -262,7 +274,7 @@ export function OperationalDashboard({ isDesktop, isMobile }) {
-
+
{assignmentTarget ? (
diff --git a/src/modules/management/pages/AdminPage.jsx b/src/modules/management/pages/AdminPage.jsx
index 8c32c2c..833c75c 100644
--- a/src/modules/management/pages/AdminPage.jsx
+++ b/src/modules/management/pages/AdminPage.jsx
@@ -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 (
-
-
-
+
+
+ {monthLabel}
+ {dateLabels[0]} a {dateLabels[dateLabels.length - 1]}
+
+
+
+ {dailyAttendance.map((value, index) => (
+
+ ))}
+
+
+
+ {dailyAttendance.map((_, index) => {
+ const visibleLabel = visibleDateLabels.find((item) => item.index === index);
+ return (
+
+ {visibleLabel?.label || ''}
+
+ );
+ })}
+
+
);
}
@@ -768,7 +905,7 @@ export function AdminPage() {
- {renderLineChart()}
+ {renderBarChart()}
{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) => (
+
+ 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
+
+
+ {getFileType(row)}{row.filename ? ` · ${row.filename}` : ''}
+
+
+ ),
+ },
{ key: 'status', label: 'Status', render: () => 'Disponível para consulta' },
{
key: 'actions',
label: 'Ações',
render: (row) => (
- 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
-
+
+ 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
+
+
),
},
];
@@ -1264,11 +1455,66 @@ export function AdminPage() {
-
-
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.
+
+
+
+ {aiGuardrails.map((rule, index) => (
+
+
+ {rule}
+
+ 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
+
+
+ ))}
@@ -1578,6 +1824,243 @@ export function AdminPage() {
);
}
+ function renderSettings() {
+ const enabledCount = authProviderCards.filter((item) => authProviderStates[item.id]).length;
+
+ return (
+
+
+
+
+ Provedores ativos
+ {enabledCount}
+
+
+ Segurança
+ Login corporativo
+
+
+
+
+
+ {authProviderCards.map((item) => {
+ const isEnabled = Boolean(authProviderStates[item.id]);
+
+ return (
+
+
+
+
+ {item.icon}
+
+
+
+ Login
+
+ {item.name}
+
+
+
+
{
+ 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}`}
+ >
+
+
+
+
+
+ {item.description}
+
+
+
+
+ {isEnabled ? 'Ativo' : 'Inativo'}
+
+ setAuthConfigModal(item)}
+ style={{
+ border: `1px solid ${item.color}44`,
+ borderRadius: 14,
+ padding: '0.65rem 0.8rem',
+ background: '#fff',
+ color: item.color,
+ fontWeight: 800,
+ }}
+ >
+ Configurar
+
+
+
+ );
+ })}
+
+
+ {authConfigModal ? (
+ setAuthConfigModal(null)}
+ >
+
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)',
+ }}
+ >
+
+ Configurar {authConfigModal.name}
+
+
+ Configure os parâmetros, credenciais e regras do provedor de login para controlar o acesso ao ambiente.
+
+
+ setAuthConfigModal(null)}
+ style={{
+ border: 'none',
+ borderRadius: 14,
+ padding: '0.75rem 0.95rem',
+ background: 'var(--color-primary)',
+ color: '#fff',
+ fontWeight: 800,
+ }}
+ >
+ Entendi
+
+
+
+
+ ) : null}
+
+ );
+ }
+
const sectionContent = {
home: renderMonthlyHome(),
today:
,
@@ -1597,8 +2080,8 @@ export function AdminPage() {
),
'new-attendance':
,
'mass-message':
,
- contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
- settings: renderPlaceholder('Configurações', 'Preferencias e parametros do ambiente.'),
+ contacts:
,
+ 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.';
diff --git a/src/modules/management/pages/SupervisorPage.jsx b/src/modules/management/pages/SupervisorPage.jsx
index f9dce33..1b4d57f 100644
--- a/src/modules/management/pages/SupervisorPage.jsx
+++ b/src/modules/management/pages/SupervisorPage.jsx
@@ -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:
,
};
return (
diff --git a/src/modules/management/services/adminAccessService.js b/src/modules/management/services/adminAccessService.js
index 74ddde2..3dd3c00 100644
--- a/src/modules/management/services/adminAccessService.js
+++ b/src/modules/management/services/adminAccessService.js
@@ -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',
diff --git a/src/routes/router.jsx b/src/routes/router.jsx
index 1b3f6c0..12c3a6f 100644
--- a/src/routes/router.jsx
+++ b/src/routes/router.jsx
@@ -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:
,
+ element:
,
},
{
path: '/mass-message',