Files
sar/apps/web/src/components/layout/Sidebar.tsx
julian 289c1a071e feat(web+api): cockpit gerente — painel, equipe, políticas e positivação
## 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>
2026-06-25 19:14:57 +00:00

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>
);
}