omnichannel-frontend/src/modules/management/pages/AdminPage.jsx

922 lines
33 KiB
React
Raw Normal View History

import { useEffect, useMemo, useState } from 'react';
import { DataPanel } from '../components/DataPanel';
import { ManagementLayout } from '../components/ManagementLayout';
import { ManagementTable } from '../components/ManagementTable';
import { MetricGrid } from '../components/MetricGrid';
import { OperationalDashboard } from '../components/OperationalDashboard';
import { TemplateManagementPanel } from '../components/TemplateManagementPanel';
import { aiContentRows, areaRows, userRows } from '../services/managementMocks';
import { AttendantOpsPanel } from '../../home/components/AttendantOpsPanel';
import { MessagesWorkspace } from '../../home/components/MessagesWorkspace';
import { useChat } from '../../chat/hooks/useChat';
import {
createAccessArea,
getAccessAreas,
getAccessOptions,
getAccessUsers,
getAdminOverview,
updateUserAccess,
} from '../services/adminAccessService';
import { useViewport } from '../../../shared/hooks/useViewport';
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
const contentColumns = [
{ key: 'title', label: 'Conteúdo' },
{ key: 'area', label: 'Especialidade' },
{ key: 'status', label: 'Status' },
{ key: 'updatedAt', label: 'Atualizado' },
];
const selectStyle = {
width: '100%',
border: '1px solid var(--color-border)',
borderRadius: '14px',
padding: '0.75rem 0.85rem',
background: '#fff',
color: 'var(--color-text)',
fontWeight: 600,
};
const compactSelectStyle = {
...selectStyle,
borderRadius: '10px',
padding: '0.45rem 0.55rem',
fontSize: '0.82rem',
};
const monthlyKpis = [
{ label: 'Total de Atendimentos', value: '1.284', detail: '+12% vs mês anterior' },
{ label: 'Tempo Médio de Atendimento', value: '8m 42s', detail: 'média mensal' },
{ label: 'Taxa de Satisfação', value: '91%', detail: 'avaliações positivas' },
{ label: 'Volume por Canal', value: 'W 982 · E 184 · S 118', detail: 'WhatsApp · Email · SMS' },
{ label: 'Atendentes Ativos', value: '14 de 17', detail: 'ativos no mês' },
];
const dailyAttendance = [28, 34, 42, 39, 51, 47, 58, 62, 55, 69, 73, 66, 71, 88, 79, 84, 91, 86, 94, 101, 97, 108, 112, 104, 118, 123, 116, 129, 134, 141];
const channelDistribution = [
{ label: 'WhatsApp', value: 982, color: '#2bb741' },
{ label: 'Email', value: 184, color: '#e5a22a' },
{ label: 'SMS', value: 118, color: '#00a4b7' },
];
const attendantRanking = [
{ id: 1, name: 'Ana Camolesi', area: 'Suporte', closed: 186, avgTime: '7m 12s', satisfaction: '94%' },
{ id: 2, name: 'Rafael Lopes', area: 'Suporte', closed: 172, avgTime: '8m 01s', satisfaction: '92%' },
{ id: 3, name: 'Marina Alves', area: 'Financeiro', closed: 161, avgTime: '8m 44s', satisfaction: '91%' },
{ id: 4, name: 'Lucas Nunes', area: 'Comercial', closed: 148, avgTime: '9m 02s', satisfaction: '89%' },
{ id: 5, name: 'Camila Rocha', area: 'Comercial', closed: 139, avgTime: '7m 58s', satisfaction: '93%' },
{ id: 6, name: 'Joao Pedro', area: 'Financeiro', closed: 127, avgTime: '10m 11s', satisfaction: '88%' },
{ id: 7, name: 'Beatriz Lima', area: 'Suporte', closed: 121, avgTime: '8m 39s', satisfaction: '90%' },
{ id: 8, name: 'Roberto Pera', area: 'Financeiro', closed: 116, avgTime: '9m 21s', satisfaction: '87%' },
{ id: 9, name: 'Helena Costa', area: 'Comercial', closed: 109, avgTime: '8m 55s', satisfaction: '92%' },
{ id: 10, name: 'Pedro Santos', area: 'Suporte', closed: 103, avgTime: '9m 48s', satisfaction: '86%' },
];
const initialNotices = [
{ id: 'n1', text: 'Revisar atendimentos financeiros com SLA abaixo de 15 minutos.' },
{ id: 'n2', text: 'Templates de abertura ativa atualizados para WhatsApp.' },
];
function mapMockUsers() {
return userRows.map((user) => ({
id: user.id,
nome: user.name,
email: user.email,
perfilPrincipal: { id: user.role, nome: user.role },
areaPrincipal: { id: user.area, nome: user.area },
accessStatus: 'assigned',
}));
}
function formatMinutes(minutes) {
if (minutes === null || minutes === undefined || Number.isNaN(Number(minutes))) return 'Sem dados';
return `${Number(minutes)} min`;
}
function toHomeConversation(contact, messages = []) {
return {
id: contact.id,
name: contact.name,
channel: contact.channel || 'WhatsApp',
status: contact.status || 'online',
lastMessage: contact.preview || messages[messages.length - 1]?.text || '',
unread: contact.unread || 0,
time: contact.time || 'Agora',
lastSeen: contact.lastSeen,
messages: messages.map((message) => ({
id: message.id,
from: message.sender === 'agent' ? 'agent' : 'customer',
text: message.text || (message.hasMedia ? '[Mídia]' : ''),
timestamp: message.timestamp,
})),
};
}
export function AdminAttendanceWorkspace({ isWideDesktop, isDesktop, isTablet, isMobile }) {
const {
contacts,
activeContactId,
setActiveContactId,
messages,
sendMessage,
isLoadingChats,
} = useChat();
const conversations = contacts.map((contact) =>
toHomeConversation(contact, contact.id === activeContactId ? messages : []),
);
const safeConversationId =
conversations.find((conversation) => conversation.id === activeContactId)?.id ||
conversations[0]?.id;
return (
<section style={{ display: 'grid', gap: '1rem' }}>
<AttendantOpsPanel activeChatsCount={conversations.length} />
{isLoadingChats ? (
<div
style={{
border: '1px solid var(--color-border)',
borderRadius: 18,
padding: '0.85rem 1rem',
background: '#fff',
color: 'var(--color-text-soft)',
fontWeight: 700,
}}
>
Atualizando conversas do WhatsApp...
</div>
) : null}
<MessagesWorkspace
conversations={conversations}
activeConversationId={safeConversationId}
onSelectConversation={setActiveContactId}
onSendSuggestedReply={async (conversationId, reply) => {
setActiveContactId(conversationId);
await sendMessage(reply, conversationId);
}}
isWideDesktop={isWideDesktop}
isDesktop={isDesktop}
isTablet={isTablet}
isMobile={isMobile}
/>
</section>
);
}
export function AdminPage() {
const { isWideDesktop, isDesktop, isTablet, isMobile } = useViewport();
const userDisplay = getCurrentUserDisplay();
const [activeAdminSection, setActiveAdminSection] = useState('home');
const [selectedAreaFilter, setSelectedAreaFilter] = useState('all');
const [overview, setOverview] = useState(null);
const [notices, setNotices] = useState(initialNotices);
const [noticeDraft, setNoticeDraft] = useState('');
const [users, setUsers] = useState(mapMockUsers);
const [profiles, setProfiles] = useState([]);
const [areas, setAreas] = useState([]);
const [areaRowsState, setAreaRowsState] = useState(areaRows);
const [userSearch, setUserSearch] = useState('');
const [newAreaName, setNewAreaName] = useState('');
const [isLoadingAccess, setIsLoadingAccess] = useState(true);
const [accessError, setAccessError] = useState('');
const [editingUser, setEditingUser] = useState(null);
const [editUserProfileId, setEditUserProfileId] = useState('');
const [editUserSpecialties, setEditUserSpecialties] = useState([]);
const [specialtyToAdd, setSpecialtyToAdd] = useState('');
useEffect(() => {
let isMounted = true;
async function loadAccessData() {
try {
const [options, accessUsers, accessAreas, adminOverview] = await Promise.all([
getAccessOptions(),
getAccessUsers(),
getAccessAreas(),
getAdminOverview(),
]);
if (!isMounted) {
return;
}
setProfiles(options.profiles || []);
setAreas(options.areas || []);
setUsers(accessUsers || []);
setAreaRowsState(accessAreas || []);
setOverview(adminOverview || null);
setAccessError('');
} catch {
if (isMounted) {
setAccessError('Backend indisponivel. Exibindo dados demonstrativos.');
}
} finally {
if (isMounted) {
setIsLoadingAccess(false);
}
}
}
loadAccessData();
return () => {
isMounted = false;
};
}, []);
function openUserEditor(user) {
setEditingUser(user);
setEditUserProfileId(user.perfilPrincipal?.id ? String(user.perfilPrincipal.id) : '');
setEditUserSpecialties(Array.isArray(user.areas) ? user.areas : []);
setSpecialtyToAdd('');
}
function closeUserEditor() {
setEditingUser(null);
setEditUserProfileId('');
setEditUserSpecialties([]);
setSpecialtyToAdd('');
}
function getProfileName(profileId) {
return profiles.find((profile) => profile.id === Number(profileId))?.nome || '';
}
async function saveUserAccess(user, nextProfileId, nextSpecialties) {
try {
const updatedUser = await updateUserAccess(user.id, {
perfilIds: nextProfileId ? [Number(nextProfileId)] : [],
especialidades: nextSpecialties.map((specialty, index) => ({
areaId: Number(specialty.id),
funcao: specialty.funcao || 'Agente',
principal: index === 0,
ativo: true,
})),
});
if (updatedUser) {
setUsers((current) =>
current.map((item) => (item.id === updatedUser.id ? updatedUser : item)),
);
}
setAccessError('');
await refreshAreas();
} catch {
setAccessError('Não foi possível salvar a atribuição. Confira o backend.');
}
}
function handleEditProfileChange(value) {
setEditUserProfileId(value);
if (getProfileName(value) === 'Admin') {
setEditUserSpecialties([]);
setSpecialtyToAdd('');
}
}
function addSpecialtyToEdit() {
const area = areas.find((item) => item.id === Number(specialtyToAdd));
if (!area) return;
setEditUserSpecialties((current) => {
if (current.some((specialty) => specialty.id === area.id)) return current;
return [
...current,
{ id: area.id, nome: area.nome, funcao: 'Agente', principal: current.length === 0 },
];
});
setSpecialtyToAdd('');
}
function removeSpecialtyFromEdit(areaId) {
setEditUserSpecialties((current) => current.filter((specialty) => specialty.id !== areaId));
}
function updateEditSpecialtyRole(areaId, role) {
setEditUserSpecialties((current) =>
current.map((specialty) =>
specialty.id === areaId ? { ...specialty, funcao: role } : specialty,
),
);
}
async function submitUserEditor() {
if (!editingUser) return;
const isAdmin = getProfileName(editUserProfileId) === 'Admin';
await saveUserAccess(editingUser, editUserProfileId, isAdmin ? [] : editUserSpecialties);
closeUserEditor();
}
async function refreshAreas() {
const [accessAreas, options] = await Promise.all([getAccessAreas(), getAccessOptions()]);
setAreaRowsState(accessAreas || []);
setAreas(options.areas || []);
}
async function handleCreateArea() {
const nome = newAreaName.trim();
if (!nome) return;
try {
await createAccessArea({
nome,
});
setNewAreaName('');
await refreshAreas();
setAccessError('');
} catch {
setAccessError('Não foi possível criar a especialidade.');
}
}
const realMonthlyKpis = [
{
label: 'Total de Atendimentos',
value: overview ? String(overview.totalAttendances) : '...',
detail: overview?.previousMonthVariation === null || overview?.previousMonthVariation === undefined
? 'sem base do mês anterior'
: `${overview.previousMonthVariation >= 0 ? '+' : ''}${overview.previousMonthVariation}% vs mês anterior`,
},
{
label: 'TMA',
value: formatMinutes(overview?.avgHandlingMinutes),
detail: overview?.avgHandlingMinutes === null ? 'aguardando histórico' : 'média mensal',
},
{
label: 'TME',
value: formatMinutes(overview?.avgFirstResponseMinutes),
detail: 'tempo médio de espera',
},
{
label: 'TMR',
value: 'Sem dados',
detail: 'requer eventos de resposta',
},
{
label: 'Atendentes Ativos',
value: overview ? `${overview.activeAttendants} de ${overview.totalActiveUsers}` : '...',
detail: 'ativos no mês',
},
];
const filteredUsers = users.filter((user) => {
const search = userSearch.trim().toLowerCase();
if (!search) return true;
return `${user.nome} ${user.email || ''} ${user.perfilPrincipal?.nome || ''} ${user.areaPrincipal?.nome || ''}`
.toLowerCase()
.includes(search);
});
const availableSpecialtiesToAdd = areas.filter(
(area) => !editUserSpecialties.some((specialty) => specialty.id === area.id),
);
const isEditingAdmin = getProfileName(editUserProfileId) === 'Admin';
const channelDistributionData = overview
? [
{ label: 'WhatsApp', value: overview.channels?.whatsapp || 0, color: '#2bb741' },
{ label: 'Email', value: overview.channels?.email || 0, color: '#e5a22a' },
{ label: 'SMS', value: overview.channels?.sms || 0, color: '#00a4b7' },
]
: channelDistribution;
const userColumns = useMemo(
() => [
{
key: 'nome',
label: 'Usuario',
render: (row) => <strong>{row.nome}</strong>,
},
{
key: 'perfil',
label: 'Perfil',
render: (row) => <span>{row.perfilPrincipal?.nome || 'Sem perfil'}</span>,
},
{
key: 'status',
label: 'Status',
render: (row) => {
const isAssigned = row.accessStatus === 'assigned';
return (
<span
style={{
width: 'fit-content',
borderRadius: 999,
padding: '0.25rem 0.6rem',
background: isAssigned ? 'rgba(0, 164, 183, 0.1)' : 'rgba(229, 162, 42, 0.16)',
color: isAssigned ? 'var(--color-primary)' : '#8a5a00',
fontWeight: 700,
}}
>
{isAssigned ? 'Atribuido' : 'Pendente'}
</span>
);
},
},
{
key: 'actions',
label: 'Ações',
render: (row) => (
<button
type="button"
onClick={() => openUserEditor(row)}
style={{
border: 'none',
borderRadius: 14,
padding: '0.7rem 0.9rem',
background: 'var(--color-primary)',
color: '#fff',
fontWeight: 800,
}}
>
Editar
</button>
),
},
],
[],
);
const areaColumns = useMemo(
() => [
{ key: 'nome', label: 'Especialidade' },
{
key: 'supervisores',
label: 'Supervisores',
render: (row) => {
const supervisors = Array.isArray(row.supervisores) ? row.supervisores : [];
return supervisors.length ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
{supervisors.map((supervisor) => (
<span
key={supervisor.id}
style={{
borderRadius: 999,
padding: '0.25rem 0.55rem',
background: 'rgba(0, 164, 183, 0.1)',
color: 'var(--color-primary)',
fontWeight: 800,
fontSize: '0.82rem',
}}
>
{supervisor.nome}
</span>
))}
</div>
) : (
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Sem supervisor</span>
);
},
},
{ key: 'members', label: 'Usuarios' },
{
key: 'status',
label: 'Status',
render: (row) => (row.ativo ? 'Ativa' : 'Inativa'),
},
],
[],
);
const filteredRanking = selectedAreaFilter === 'all'
? attendantRanking
: attendantRanking.filter((row) => row.area === selectedAreaFilter);
const rankingColumns = [
{ key: 'name', label: 'Nome' },
{ key: 'area', label: 'Especialidade' },
{ key: 'closed', label: 'Atendimentos finalizados' },
{ key: 'avgTime', label: 'Tempo médio' },
{ key: 'satisfaction', label: 'Satisfação' },
];
function sendNotice() {
const text = noticeDraft.trim();
if (!text) return;
setNotices((current) => [{ id: `notice-${Date.now()}`, text }, ...current]);
setNoticeDraft('');
}
function renderLineChart() {
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(' ');
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>
);
}
function renderDonutChart() {
const total = channelDistributionData.reduce((sum, item) => sum + item.value, 0) || 1;
let offset = 0;
return (
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '160px 1fr', gap: '1rem', alignItems: 'center' }}>
<svg viewBox="0 0 42 42" style={{ width: 160, height: 160 }}>
<circle cx="21" cy="21" r="15.9" fill="transparent" stroke="rgba(0,49,80,0.08)" strokeWidth="7" />
{channelDistributionData.map((item) => {
const dash = (item.value / total) * 100;
const circle = (
<circle
key={item.label}
cx="21"
cy="21"
r="15.9"
fill="transparent"
stroke={item.color}
strokeWidth="7"
strokeDasharray={`${dash} ${100 - dash}`}
strokeDashoffset={-offset}
/>
);
offset += dash;
return circle;
})}
</svg>
<div style={{ display: 'grid', gap: '0.65rem' }}>
{channelDistributionData.map((item) => (
<span key={item.label} style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
<span style={{ color: item.color, fontWeight: 800 }}>{item.label}</span>
<strong>{item.value}</strong>
</span>
))}
</div>
</div>
);
}
function renderMonthlyHome() {
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem', flexWrap: 'wrap' }}>
<label style={{ display: 'grid', gap: '0.35rem', minWidth: isMobile ? '100%' : 260 }}>
<span style={{ fontWeight: 700 }}>Filtro por especialidade</span>
<select value={selectedAreaFilter} onChange={(event) => setSelectedAreaFilter(event.target.value)} style={selectStyle}>
<option value="all">Todas as especialidades</option>
{areas.map((area) => (
<option key={area.id} value={area.nome}>{area.nome}</option>
))}
</select>
</label>
</div>
<MetricGrid metrics={realMonthlyKpis} minCardWidth="160px" />
<div style={{ display: 'grid', gridTemplateColumns: isDesktop ? 'minmax(0, 1.85fr) minmax(300px, 1fr)' : '1fr', gap: '1rem' }}>
<DataPanel title="Atendimentos por dia" description="Volume diário do mês selecionado.">
{renderLineChart()}
</DataPanel>
<DataPanel title="Distribuição por canal" description="Participação mensal por canal.">
{renderDonutChart()}
</DataPanel>
</div>
<DataPanel title="Ranking de atendentes" description="Top 10 ordenado por atendimentos finalizados.">
<ManagementTable columns={rankingColumns} rows={filteredRanking.slice(0, 10)} getRowId={(row) => row.id} isMobile={isMobile} />
</DataPanel>
<DataPanel title="Painel de avisos" description="Comunicados enviados para os atendentes.">
<div style={{ display: 'grid', gridTemplateColumns: isDesktop ? 'minmax(0, 1fr) minmax(320px, 0.8fr)' : '1fr', gap: '1rem' }}>
<div style={{ display: 'grid', gap: '0.75rem' }}>
{notices.map((notice) => (
<article key={notice.id} style={{ border: '1px solid var(--color-border)', borderRadius: 18, padding: '0.9rem 1rem', background: '#fff' }}>
{notice.text}
</article>
))}
</div>
<div style={{ display: 'grid', gap: '0.75rem', alignContent: 'start' }}>
<textarea
rows={5}
value={noticeDraft}
onChange={(event) => setNoticeDraft(event.target.value)}
placeholder="Digite um aviso para o time..."
style={{ ...selectStyle, resize: 'vertical', lineHeight: 1.45 }}
/>
<button type="button" onClick={sendNotice} style={{ border: 'none', borderRadius: 16, padding: '0.95rem 1rem', background: 'var(--color-primary)', color: '#fff', fontWeight: 800 }}>
Enviar aviso
</button>
</div>
</div>
</DataPanel>
</>
);
}
function renderUsersAccess() {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr',
gap: '1rem',
alignItems: 'start',
}}
>
<DataPanel
title="Usuarios e niveis de acesso"
description={
isLoadingAccess
? 'Carregando usuários do banco...'
: accessError || 'Gerencie perfil e especialidade principal dos usuários autenticados.'
}
actionLabel="Adicionar usuário"
>
<div style={{ display: 'grid', gap: '0.85rem' }}>
<input
type="search"
value={userSearch}
onChange={(event) => setUserSearch(event.target.value)}
placeholder="Buscar usuário por nome, email, perfil ou especialidade"
style={selectStyle}
/>
<div style={{ maxHeight: 470, overflowY: 'auto', paddingRight: '0.2rem' }}>
<ManagementTable columns={userColumns} rows={filteredUsers} getRowId={(row) => row.id} isMobile={isMobile} />
</div>
{editingUser ? (
<div
role="dialog"
aria-modal="true"
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 49, 80, 0.28)',
display: 'grid',
placeItems: 'center',
padding: '1rem',
zIndex: 30,
}}
>
<div
style={{
width: 'min(560px, 100%)',
background: '#fff',
borderRadius: 24,
boxShadow: 'var(--shadow-lg)',
padding: '1.25rem',
display: 'grid',
gap: '1rem',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
<div>
<h2 style={{ margin: 0, fontSize: '1.2rem' }}>Editar acesso</h2>
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
{editingUser.nome} · {editingUser.email || 'Sem email'}
</p>
</div>
<button
type="button"
onClick={closeUserEditor}
style={{
border: 'none',
background: 'transparent',
color: 'var(--color-text-soft)',
fontWeight: 800,
}}
>
Fechar
</button>
</div>
<label style={{ display: 'grid', gap: '0.45rem' }}>
<span style={{ fontWeight: 800 }}>Perfil global</span>
<select
value={editUserProfileId}
onChange={(event) => handleEditProfileChange(event.target.value)}
style={selectStyle}
>
<option value="">Sem perfil</option>
{profiles.map((profile) => (
<option key={profile.id} value={profile.id}>
{profile.nome}
</option>
))}
</select>
</label>
{isEditingAdmin ? (
<div
style={{
border: '1px solid var(--color-border)',
borderRadius: 16,
padding: '0.85rem',
background: 'rgba(0, 164, 183, 0.08)',
color: 'var(--color-primary)',
fontWeight: 800,
}}
>
Admin tem acesso global. Especialidades não se aplicam para este perfil.
</div>
) : (
<div style={{ display: 'grid', gap: '0.75rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto', gap: '0.65rem' }}>
<select
value={specialtyToAdd}
onChange={(event) => setSpecialtyToAdd(event.target.value)}
style={selectStyle}
>
<option value="">Selecionar especialidade</option>
{availableSpecialtiesToAdd.map((area) => (
<option key={area.id} value={area.id}>
{area.nome}
</option>
))}
</select>
<button
type="button"
onClick={addSpecialtyToEdit}
disabled={!specialtyToAdd}
style={{
border: 'none',
borderRadius: 14,
padding: '0.75rem 0.95rem',
background: specialtyToAdd ? 'var(--color-primary)' : 'rgba(0, 49, 80, 0.12)',
color: specialtyToAdd ? '#fff' : 'var(--color-text-soft)',
fontWeight: 800,
}}
>
Adicionar
</button>
</div>
<div style={{ display: 'grid', gap: '0.55rem', maxHeight: 260, overflowY: 'auto', paddingRight: '0.2rem' }}>
{editUserSpecialties.length ? editUserSpecialties.map((specialty) => (
<div
key={specialty.id}
style={{
border: '1px solid var(--color-border)',
borderRadius: 16,
padding: '0.75rem',
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) 140px auto',
gap: '0.65rem',
alignItems: 'center',
}}
>
<strong>{specialty.nome}</strong>
<select
value={specialty.funcao || 'Agente'}
onChange={(event) => updateEditSpecialtyRole(specialty.id, event.target.value)}
style={compactSelectStyle}
>
<option value="Agente">Agente</option>
<option value="Supervisor">Supervisor</option>
</select>
<button
type="button"
onClick={() => removeSpecialtyFromEdit(specialty.id)}
style={{
border: 'none',
borderRadius: 12,
padding: '0.55rem 0.7rem',
background: 'rgba(181, 31, 31, 0.1)',
color: 'var(--color-secondary)',
fontWeight: 800,
}}
>
Remover
</button>
</div>
)) : (
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>
Nenhuma especialidade selecionada.
</span>
)}
</div>
</div>
)}
<button
type="button"
onClick={submitUserEditor}
style={{
border: 'none',
borderRadius: 16,
padding: '0.9rem 1rem',
background: 'var(--color-primary)',
color: '#fff',
fontWeight: 800,
}}
>
Salvar acesso
</button>
</div>
</div>
) : null}
</div>
</DataPanel>
<DataPanel title="Especialidades" description="Especialidades operacionais. Supervisores são definidos em Usuários e níveis de acesso.">
<div style={{ display: 'grid', gap: '0.85rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto', gap: '0.75rem' }}>
<input
type="text"
value={newAreaName}
onChange={(event) => setNewAreaName(event.target.value)}
placeholder="Nome da nova especialidade"
style={selectStyle}
/>
<button
type="button"
onClick={handleCreateArea}
style={{
border: 'none',
borderRadius: '14px',
padding: '0.75rem 0.95rem',
background: 'var(--color-primary)',
color: '#fff',
fontWeight: 800,
}}
>
Adicionar
</button>
</div>
<ManagementTable columns={areaColumns} rows={areaRowsState} getRowId={(row) => row.id} isMobile={isMobile} />
</div>
</DataPanel>
</div>
);
}
function renderPlaceholder(title, description) {
return (
<DataPanel title={title} description={description}>
<div style={{ border: '1px solid var(--color-border)', borderRadius: 18, padding: '1rem', background: '#fff', color: 'var(--color-text-soft)', fontWeight: 700 }}>
Seção em preparação.
</div>
</DataPanel>
);
}
const sectionContent = {
home: renderMonthlyHome(),
today: <OperationalDashboard isDesktop={isDesktop} isMobile={isMobile} />,
'users-access': renderUsersAccess(),
templates: <TemplateManagementPanel areas={areas} mode="admin" isMobile={isMobile} />,
knowledge: (
<DataPanel title="Base de conhecimento IA" description="Entradas para alimentar a base de conhecimento.">
<ManagementTable columns={contentColumns} rows={aiContentRows} getRowId={(row) => row.id} isMobile={isMobile} />
</DataPanel>
),
audit: renderPlaceholder('Auditoria', 'Eventos administrativos e alterações sensíveis.'),
channels: renderPlaceholder('Canais', 'Status e configurações dos canais conectados.'),
attendance: (
<AdminAttendanceWorkspace
isWideDesktop={isWideDesktop}
isDesktop={isDesktop}
isTablet={isTablet}
isMobile={isMobile}
/>
),
'mass-message': renderPlaceholder('Disparo em massa', 'Fluxo de disparos por templates aprovados.'),
contacts: renderPlaceholder('Contatos', 'Agenda geral de contatos.'),
settings: renderPlaceholder('Configurações', 'Preferencias e parametros do ambiente.'),
};
const pageTitle = activeAdminSection === 'home'
? 'Home do Admin'
: activeAdminSection === 'attendance'
? 'Atendimento'
: activeAdminSection === 'today'
? 'Operação'
: 'Painel administrativo';
const pageSubtitle = activeAdminSection === 'home'
? 'Visão mensal consolidada por especialidade, canal e atendente.'
: activeAdminSection === 'attendance'
? 'Home operacional do atendente dentro do painel administrativo.'
: activeAdminSection === 'today'
? 'Indicadores do dia, fila de espera e acompanhamento operacional do time.'
: 'Controle operacional e configurações administrativas.';
return (
<ManagementLayout
title={pageTitle}
subtitle={pageSubtitle}
activeSection="admin"
profileLabel={userDisplay.name}
initials={userDisplay.initials}
isDesktop={isDesktop}
isMobile={isMobile}
activeNavItem={activeAdminSection}
onNavItemChange={setActiveAdminSection}
>
{sectionContent[activeAdminSection] || sectionContent.home}
</ManagementLayout>
);
}