## API - GET /dashboard/manager: KPIs agregados (faturamento, pedidos, ticket médio, promoções ativas), meta total do período, ranking top 10 com clientes atendidos e % meta, positivação por representante, venda necessária/dia - Parâmetros opcionais ?mes&ano para filtrar período - GET /equipe: lista de reps com pedidos, faturamento, ticket médio e % meta (deduplicação de vw_representantes) - GET/POST /politicas/descontos: alçada de desconto por rep (AlcadaDesconto) - GET/POST/PATCH/DELETE /politicas/promocoes: CRUD de promoções com validade - Metas lidas de sar.vw_metas (tipo=GR), não de vw_metas do ERP ## Schema - Novo model Promocao (sar.promocoes) criado via prisma db execute ## Frontend - Cockpit /ger: GerPainel, EquipePage, PoliticasPage - GerPainel: filtro mes/ano, cards KPI, cards de meta vs realizado (atingimento, falta, venda/dia), ranking com clientes atendidos, positivação de clientes por representante (paginada) - Sidebar e BottomNav role-aware: rep / supervisor / gerente (manager+admin) - Clientes acessível ao gerente (carteira completa de todos os reps) - Rotas: HomeRoute redireciona por role; /ger, /ger/equipe, /ger/politicas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
4.1 KiB
TypeScript
148 lines
4.1 KiB
TypeScript
import { Menu } from 'antd';
|
|
import { useLocation, useNavigate } from '@tanstack/react-router';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import {
|
|
faChartLine,
|
|
faClipboardList,
|
|
faClipboardCheck,
|
|
faFunnelDollar,
|
|
faUsers,
|
|
faAddressCard,
|
|
faBoxesStacked,
|
|
faFileInvoiceDollar,
|
|
faTags,
|
|
} from '@fortawesome/free-solid-svg-icons';
|
|
import type { ItemType } from 'antd/es/menu/interface';
|
|
import { authStore } from '../../lib/auth-store';
|
|
|
|
function getRole(): string {
|
|
const token = authStore.get();
|
|
if (!token) return 'rep';
|
|
try {
|
|
const payload = JSON.parse(atob(token.split('.')[1] ?? ''));
|
|
return (payload.role as string) ?? 'rep';
|
|
} catch {
|
|
return 'rep';
|
|
}
|
|
}
|
|
|
|
const REP_ITEMS: ItemType[] = [
|
|
{ key: '/', icon: <FontAwesomeIcon icon={faChartLine} fixedWidth />, label: 'Painel' },
|
|
{ key: '/clientes', icon: <FontAwesomeIcon icon={faUsers} fixedWidth />, label: 'Clientes' },
|
|
{
|
|
key: '/catalogo',
|
|
icon: <FontAwesomeIcon icon={faBoxesStacked} fixedWidth />,
|
|
label: 'Catálogo',
|
|
},
|
|
{
|
|
key: '/funil',
|
|
icon: <FontAwesomeIcon icon={faFunnelDollar} fixedWidth />,
|
|
label: 'Funil de Vendas',
|
|
},
|
|
{
|
|
key: '/pedidos',
|
|
icon: <FontAwesomeIcon icon={faClipboardList} fixedWidth />,
|
|
label: 'Pedidos',
|
|
},
|
|
{
|
|
key: '/pedidos-erp',
|
|
icon: <FontAwesomeIcon icon={faClipboardCheck} fixedWidth />,
|
|
label: 'Consulta ERP',
|
|
},
|
|
{
|
|
key: '/relatorios',
|
|
icon: <FontAwesomeIcon icon={faFileInvoiceDollar} fixedWidth />,
|
|
label: 'Relatórios',
|
|
},
|
|
];
|
|
|
|
const SUPERVISOR_ITEMS: ItemType[] = [
|
|
{ key: '/', icon: <FontAwesomeIcon icon={faChartLine} fixedWidth />, label: 'Painel' },
|
|
{
|
|
key: '/aprovacoes',
|
|
icon: <FontAwesomeIcon icon={faClipboardCheck} fixedWidth />,
|
|
label: 'Aprovações',
|
|
},
|
|
{ key: '/clientes', icon: <FontAwesomeIcon icon={faUsers} fixedWidth />, label: 'Clientes' },
|
|
{
|
|
key: '/pedidos',
|
|
icon: <FontAwesomeIcon icon={faClipboardList} fixedWidth />,
|
|
label: 'Pedidos',
|
|
},
|
|
{
|
|
key: '/catalogo',
|
|
icon: <FontAwesomeIcon icon={faBoxesStacked} fixedWidth />,
|
|
label: 'Catálogo',
|
|
},
|
|
{
|
|
key: '/relatorios',
|
|
icon: <FontAwesomeIcon icon={faFileInvoiceDollar} fixedWidth />,
|
|
label: 'Relatórios',
|
|
},
|
|
];
|
|
|
|
const GERENTE_ITEMS: ItemType[] = [
|
|
{ key: '/ger', icon: <FontAwesomeIcon icon={faChartLine} fixedWidth />, label: 'Painel' },
|
|
{ key: '/ger/equipe', icon: <FontAwesomeIcon icon={faUsers} fixedWidth />, label: 'Equipe' },
|
|
{
|
|
key: '/clientes',
|
|
icon: <FontAwesomeIcon icon={faAddressCard} fixedWidth />,
|
|
label: 'Clientes',
|
|
},
|
|
{
|
|
key: '/relatorios',
|
|
icon: <FontAwesomeIcon icon={faFileInvoiceDollar} fixedWidth />,
|
|
label: 'Relatórios',
|
|
},
|
|
{
|
|
key: '/ger/politicas',
|
|
icon: <FontAwesomeIcon icon={faTags} fixedWidth />,
|
|
label: 'Políticas Comerciais',
|
|
},
|
|
];
|
|
|
|
function getItems(role: string): ItemType[] {
|
|
if (role === 'manager' || role === 'admin') return GERENTE_ITEMS;
|
|
if (role === 'supervisor') return SUPERVISOR_ITEMS;
|
|
return REP_ITEMS;
|
|
}
|
|
|
|
export function Sidebar() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const role = getRole();
|
|
const items = getItems(role);
|
|
|
|
const selectedKey = items.find((item) => {
|
|
const key = (item as { key?: string }).key ?? '';
|
|
return key === '/' ? location.pathname === '/' : location.pathname.startsWith(key);
|
|
})?.key as string | undefined;
|
|
|
|
return (
|
|
<aside
|
|
style={{
|
|
width: 'var(--layout-sidebar-width)',
|
|
flexShrink: 0,
|
|
height: 'calc(100vh - var(--layout-topbar-height))',
|
|
background: 'var(--bg-surface)',
|
|
borderRight: '1px solid var(--border-subtle)',
|
|
position: 'sticky',
|
|
top: 'var(--layout-topbar-height)',
|
|
overflowY: 'auto',
|
|
padding: 'var(--space-md) var(--space-xs)',
|
|
}}
|
|
>
|
|
<Menu
|
|
mode="inline"
|
|
selectedKeys={selectedKey ? [selectedKey] : [location.pathname]}
|
|
items={items}
|
|
onClick={({ key }) => navigate({ to: key as string })}
|
|
style={{
|
|
border: 'none',
|
|
background: 'transparent',
|
|
}}
|
|
/>
|
|
</aside>
|
|
);
|
|
}
|