FEAT: reformula home administrativa e gestão de acessos
- adiciona visão mensal do admin com KPIs, gráficos, ranking e avisos - cria nova navegação lateral administrativa - move usuários e áreas para Usuários & Acessos - adiciona busca e rolagem na lista de usuários - integra métricas reais disponíveis na home admin - permite criar áreas e alterar responsável na interface
This commit is contained in:
parent
eeab43d2a4
commit
c61a913c38
@ -1,4 +1,4 @@
|
||||
export function DataPanel({ title, description, actionLabel, children }) {
|
||||
export function DataPanel({ title, description, actionLabel, onAction, children }) {
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
@ -28,6 +28,7 @@ export function DataPanel({ title, description, actionLabel, children }) {
|
||||
{actionLabel ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAction}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '18px',
|
||||
|
||||
@ -11,12 +11,21 @@ const navigationBySection = {
|
||||
{ id: 'reports', label: 'Relatorios', count: null },
|
||||
],
|
||||
admin: [
|
||||
{ id: 'dashboard', label: 'Dashboard', count: null },
|
||||
{ id: 'users', label: 'Usuarios e acessos', count: 64 },
|
||||
{ id: 'areas', label: 'Areas', count: 3 },
|
||||
{ id: 'knowledge', label: 'Conteudo para IA', count: 28 },
|
||||
{ id: 'channels', label: 'Canais', count: 1 },
|
||||
{ id: 'audit', label: 'Auditoria', count: null },
|
||||
{ id: 'home', label: 'Home' },
|
||||
{ id: 'today', label: 'Operacao' },
|
||||
{ type: 'separator' },
|
||||
{ id: 'users-access', label: 'Usuarios & Acessos' },
|
||||
{ id: 'templates', label: 'Templates' },
|
||||
{ id: 'knowledge', label: 'Base de conhecimento IA' },
|
||||
{ id: 'audit', label: 'Auditoria' },
|
||||
{ id: 'channels', label: 'Canais' },
|
||||
{ type: 'separator' },
|
||||
{ id: 'attendance', label: 'Atendimento', path: '/chat' },
|
||||
{ id: 'new-attendance', label: 'Abrir Atendimento', path: '/new-attendance' },
|
||||
{ id: 'mass-message', label: 'Disparo em Massa' },
|
||||
{ id: 'contacts', label: 'Contatos' },
|
||||
{ type: 'separator' },
|
||||
{ id: 'settings', label: 'Configuracoes' },
|
||||
],
|
||||
};
|
||||
|
||||
@ -34,6 +43,8 @@ export function ManagementLayout({
|
||||
children,
|
||||
isDesktop,
|
||||
isMobile,
|
||||
activeNavItem,
|
||||
onNavItemChange,
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const navItems = navigationBySection[activeSection] || navigationBySection.supervisor;
|
||||
@ -113,12 +124,32 @@ export function ManagementLayout({
|
||||
}}
|
||||
>
|
||||
{navItems.map((item, index) => {
|
||||
const isActive = index === 0;
|
||||
if (item.type === 'separator') {
|
||||
return (
|
||||
<div
|
||||
key={`separator-${index}`}
|
||||
style={{
|
||||
height: 1,
|
||||
background: 'rgba(255, 255, 255, 0.16)',
|
||||
margin: '0.35rem 0',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = activeNavItem ? item.id === activeNavItem : index === 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (item.path) {
|
||||
navigate(item.path);
|
||||
return;
|
||||
}
|
||||
onNavItemChange?.(item.id);
|
||||
}}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '18px',
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
export function MetricGrid({ metrics }) {
|
||||
export function MetricGrid({ metrics, minCardWidth = '180px' }) {
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${minCardWidth}, 1fr))`,
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
|
||||
@ -3,18 +3,19 @@ import { DataPanel } from '../components/DataPanel';
|
||||
import { ManagementLayout } from '../components/ManagementLayout';
|
||||
import { ManagementTable } from '../components/ManagementTable';
|
||||
import { MetricGrid } from '../components/MetricGrid';
|
||||
import { adminMetrics, aiContentRows, areaRows, userRows } from '../services/managementMocks';
|
||||
import { getAccessOptions, getAccessUsers, updateUserAccess } from '../services/adminAccessService';
|
||||
import { aiContentRows, areaRows, userRows } from '../services/managementMocks';
|
||||
import {
|
||||
createAccessArea,
|
||||
getAccessAreas,
|
||||
getAccessOptions,
|
||||
getAccessUsers,
|
||||
getAdminOverview,
|
||||
updateAccessArea,
|
||||
updateUserAccess,
|
||||
} from '../services/adminAccessService';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||
|
||||
const areaColumns = [
|
||||
{ key: 'name', label: 'Area' },
|
||||
{ key: 'owner', label: 'Responsavel' },
|
||||
{ key: 'members', label: 'Usuarios' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
];
|
||||
|
||||
const contentColumns = [
|
||||
{ key: 'title', label: 'Conteudo' },
|
||||
{ key: 'area', label: 'Area' },
|
||||
@ -32,6 +33,39 @@ const selectStyle = {
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
const monthlyKpis = [
|
||||
{ label: 'Total de Atendimentos', value: '1.284', detail: '+12% vs mes anterior' },
|
||||
{ label: 'Tempo Medio de Atendimento', value: '8m 42s', detail: 'media mensal' },
|
||||
{ label: 'Taxa de Satisfacao', value: '91%', detail: 'avaliacoes positivas' },
|
||||
{ label: 'Volume por Canal', value: 'W 982 · E 184 · S 118', detail: 'WhatsApp · Email · SMS' },
|
||||
{ label: 'Atendentes Ativos', value: '14 de 17', detail: 'ativos no mes' },
|
||||
];
|
||||
|
||||
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,
|
||||
@ -43,12 +77,26 @@ function mapMockUsers() {
|
||||
}));
|
||||
}
|
||||
|
||||
function formatMinutes(minutes) {
|
||||
if (minutes === null || minutes === undefined || Number.isNaN(Number(minutes))) return 'Sem dados';
|
||||
return `${Number(minutes)} min`;
|
||||
}
|
||||
|
||||
export function AdminPage() {
|
||||
const { isDesktop, 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 [newAreaOwnerId, setNewAreaOwnerId] = useState('');
|
||||
const [isLoadingAccess, setIsLoadingAccess] = useState(true);
|
||||
const [accessError, setAccessError] = useState('');
|
||||
|
||||
@ -57,7 +105,12 @@ export function AdminPage() {
|
||||
|
||||
async function loadAccessData() {
|
||||
try {
|
||||
const [options, accessUsers] = await Promise.all([getAccessOptions(), getAccessUsers()]);
|
||||
const [options, accessUsers, accessAreas, adminOverview] = await Promise.all([
|
||||
getAccessOptions(),
|
||||
getAccessUsers(),
|
||||
getAccessAreas(),
|
||||
getAdminOverview(),
|
||||
]);
|
||||
|
||||
if (!isMounted) {
|
||||
return;
|
||||
@ -66,6 +119,8 @@ export function AdminPage() {
|
||||
setProfiles(options.profiles || []);
|
||||
setAreas(options.areas || []);
|
||||
setUsers(accessUsers || []);
|
||||
setAreaRowsState(accessAreas || []);
|
||||
setOverview(adminOverview || null);
|
||||
setAccessError('');
|
||||
} catch {
|
||||
if (isMounted) {
|
||||
@ -122,6 +177,90 @@ export function AdminPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
responsavelUsuarioId: Number(newAreaOwnerId) || null,
|
||||
});
|
||||
setNewAreaName('');
|
||||
setNewAreaOwnerId('');
|
||||
await refreshAreas();
|
||||
setAccessError('');
|
||||
} catch {
|
||||
setAccessError('Nao foi possivel criar a area.');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAreaOwnerChange(areaId, userId) {
|
||||
try {
|
||||
await updateAccessArea(areaId, {
|
||||
responsavelUsuarioId: Number(userId) || null,
|
||||
});
|
||||
await refreshAreas();
|
||||
const accessUsers = await getAccessUsers();
|
||||
setUsers(accessUsers || []);
|
||||
setAccessError('');
|
||||
} catch {
|
||||
setAccessError('Nao foi possivel atualizar o responsavel da area.');
|
||||
}
|
||||
}
|
||||
|
||||
const realMonthlyKpis = [
|
||||
{
|
||||
label: 'Total de Atendimentos',
|
||||
value: overview ? String(overview.totalAttendances) : '...',
|
||||
detail: overview?.previousMonthVariation === null || overview?.previousMonthVariation === undefined
|
||||
? 'sem base do mes anterior'
|
||||
: `${overview.previousMonthVariation >= 0 ? '+' : ''}${overview.previousMonthVariation}% vs mes anterior`,
|
||||
},
|
||||
{
|
||||
label: 'TMA',
|
||||
value: formatMinutes(overview?.avgHandlingMinutes),
|
||||
detail: overview?.avgHandlingMinutes === null ? 'aguardando historico' : 'media mensal',
|
||||
},
|
||||
{
|
||||
label: 'TME',
|
||||
value: formatMinutes(overview?.avgFirstResponseMinutes),
|
||||
detail: 'tempo medio 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 mes',
|
||||
},
|
||||
];
|
||||
|
||||
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 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(
|
||||
() => [
|
||||
{
|
||||
@ -204,18 +343,171 @@ export function AdminPage() {
|
||||
[areas, profiles],
|
||||
);
|
||||
|
||||
return (
|
||||
<ManagementLayout
|
||||
title="Painel administrativo"
|
||||
subtitle="Controle de usuarios, perfis, areas e base de conteudo para IA."
|
||||
activeSection="admin"
|
||||
profileLabel={userDisplay.name}
|
||||
initials={userDisplay.initials}
|
||||
isDesktop={isDesktop}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<MetricGrid metrics={adminMetrics} />
|
||||
const areaColumns = useMemo(
|
||||
() => [
|
||||
{ key: 'nome', label: 'Area' },
|
||||
{
|
||||
key: 'responsavel',
|
||||
label: 'Responsavel',
|
||||
render: (row) => (
|
||||
<select
|
||||
value={row.responsavel_usuario_id || ''}
|
||||
onChange={(event) => handleAreaOwnerChange(row.id, event.target.value)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="">Sem responsavel</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
},
|
||||
{ key: 'members', label: 'Usuários' },
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
render: (row) => (row.ativo ? 'Ativa' : 'Inativa'),
|
||||
},
|
||||
],
|
||||
[users],
|
||||
);
|
||||
|
||||
const filteredRanking = selectedAreaFilter === 'all'
|
||||
? attendantRanking
|
||||
: attendantRanking.filter((row) => row.area === selectedAreaFilter);
|
||||
|
||||
const rankingColumns = [
|
||||
{ key: 'name', label: 'Nome' },
|
||||
{ key: 'area', label: 'Area' },
|
||||
{ key: 'closed', label: 'Atendimentos finalizados' },
|
||||
{ key: 'avgTime', label: 'Tempo medio' },
|
||||
{ key: 'satisfaction', label: 'Satisfacao' },
|
||||
];
|
||||
|
||||
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 area</span>
|
||||
<select value={selectedAreaFilter} onChange={(event) => setSelectedAreaFilter(event.target.value)} style={selectStyle}>
|
||||
<option value="all">Todas as areas</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 diario do mes selecionado.">
|
||||
{renderLineChart()}
|
||||
</DataPanel>
|
||||
<DataPanel title="Distribuicao por canal" description="Participacao 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',
|
||||
@ -233,40 +525,100 @@ export function AdminPage() {
|
||||
}
|
||||
actionLabel="Adicionar usuario"
|
||||
>
|
||||
<ManagementTable
|
||||
columns={userColumns}
|
||||
rows={users}
|
||||
getRowId={(row) => row.id}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<input
|
||||
type="search"
|
||||
value={userSearch}
|
||||
onChange={(event) => setUserSearch(event.target.value)}
|
||||
placeholder="Buscar usuario por nome, email, perfil ou area"
|
||||
style={selectStyle}
|
||||
/>
|
||||
<div style={{ maxHeight: 470, overflowY: 'auto', paddingRight: '0.2rem' }}>
|
||||
<ManagementTable columns={userColumns} rows={filteredUsers} getRowId={(row) => row.id} isMobile={isMobile} />
|
||||
</div>
|
||||
</div>
|
||||
</DataPanel>
|
||||
|
||||
<DataPanel
|
||||
title="Areas"
|
||||
description="Areas operacionais e seus responsaveis."
|
||||
actionLabel="Nova area"
|
||||
>
|
||||
<ManagementTable
|
||||
columns={areaColumns}
|
||||
rows={areaRows}
|
||||
getRowId={(row) => row.id}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<DataPanel title="Areas" description="Areas operacionais e seus responsaveis. Alterar o responsavel atribui perfil Supervisor a ele.">
|
||||
<div style={{ display: 'grid', gap: '0.85rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(0, 1fr) auto', gap: '0.75rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={newAreaName}
|
||||
onChange={(event) => setNewAreaName(event.target.value)}
|
||||
placeholder="Nome da nova area"
|
||||
style={selectStyle}
|
||||
/>
|
||||
<select value={newAreaOwnerId} onChange={(event) => setNewAreaOwnerId(event.target.value)} style={selectStyle}>
|
||||
<option value="">Responsavel opcional</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateArea}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
<DataPanel
|
||||
title="Conteudo para IA"
|
||||
description="Entradas mockadas para alimentar a base de conhecimento."
|
||||
actionLabel="Adicionar conteudo"
|
||||
>
|
||||
<ManagementTable
|
||||
columns={contentColumns}
|
||||
rows={aiContentRows}
|
||||
getRowId={(row) => row.id}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
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 }}>
|
||||
Secao em preparacao.
|
||||
</div>
|
||||
</DataPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const sectionContent = {
|
||||
home: renderMonthlyHome(),
|
||||
today: renderPlaceholder('Operação', 'Visão operacional diaria sera consolidada aqui.'),
|
||||
'users-access': renderUsersAccess(),
|
||||
templates: renderPlaceholder('Templates', 'Gestão de templates aprovados pela Meta.'),
|
||||
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 alteracoes sensiveis.'),
|
||||
channels: renderPlaceholder('Canais', 'Status e configurações dos canais conectados.'),
|
||||
'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.'),
|
||||
};
|
||||
|
||||
return (
|
||||
<ManagementLayout
|
||||
title={activeAdminSection === 'home' ? 'Home do Admin' : 'Painel administrativo'}
|
||||
subtitle={activeAdminSection === 'home' ? 'Visão mensal consolidada por area, canal e atendente.' : 'Controle operacional e configuracoes administrativas.'}
|
||||
activeSection="admin"
|
||||
profileLabel={userDisplay.name}
|
||||
initials={userDisplay.initials}
|
||||
isDesktop={isDesktop}
|
||||
isMobile={isMobile}
|
||||
activeNavItem={activeAdminSection}
|
||||
onNavItemChange={setActiveAdminSection}
|
||||
>
|
||||
{sectionContent[activeAdminSection] || sectionContent.home}
|
||||
</ManagementLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,13 +20,35 @@ export async function getAccessOptions() {
|
||||
return request('/admin/access/options');
|
||||
}
|
||||
|
||||
export async function getAdminOverview() {
|
||||
return request('/admin/access/overview');
|
||||
}
|
||||
|
||||
export async function getAccessUsers() {
|
||||
return request('/admin/access/users');
|
||||
}
|
||||
|
||||
export async function getAccessAreas() {
|
||||
return request('/admin/access/areas');
|
||||
}
|
||||
|
||||
export async function updateUserAccess(userId, access) {
|
||||
return request(`/admin/access/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(access),
|
||||
});
|
||||
}
|
||||
|
||||
export async function createAccessArea(payload) {
|
||||
return request('/admin/access/areas', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAccessArea(areaId, payload) {
|
||||
return request(`/admin/access/areas/${areaId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
@ -6,10 +6,10 @@ export const supervisorMetrics = [
|
||||
];
|
||||
|
||||
export const adminMetrics = [
|
||||
{ label: 'Usuarios ativos', value: '64', detail: '8 supervisores configurados' },
|
||||
{ label: 'Areas cadastradas', value: '3', detail: 'Suporte, Financeiro e Comercial' },
|
||||
{ label: 'Conteudos IA', value: '28', detail: '6 aguardando revisao' },
|
||||
{ label: 'Canais conectados', value: '1', detail: 'WhatsApp em homologacao' },
|
||||
{ label: 'Usuários ativos', value: '64', detail: '8 supervisores configurados' },
|
||||
{ label: 'Áreas cadastradas', value: '3', detail: 'Suporte, Financeiro e Comercial' },
|
||||
{ label: 'Conteúdos IA', value: '28', detail: '6 aguardando revisão' },
|
||||
{ label: 'Canais conectados', value: '1', detail: 'WhatsApp em homologação' },
|
||||
];
|
||||
|
||||
export const areaRows = [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user