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>
This commit is contained in:
@@ -7,55 +7,116 @@ import {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar canônica do SAR (260px fixa — brand.md).
|
||||
* Itens mockados para Rafael cockpit. Variantes por cockpit virão depois.
|
||||
*/
|
||||
export function Sidebar() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const 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 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
|
||||
@@ -73,7 +134,7 @@ export function Sidebar() {
|
||||
>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
selectedKeys={selectedKey ? [selectedKey] : [location.pathname]}
|
||||
items={items}
|
||||
onClick={({ key }) => navigate({ to: key as string })}
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user