Files
sar/libs/shared/api-interface/src/lib/dashboard.contract.ts
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

110 lines
3.6 KiB
TypeScript

import { z } from 'zod';
import { PedidoSummarySchema } from './order.contract';
// ADR 0006 revogado: OrderSummary → PedidoSummary, ids numéricos.
export const ClienteNaoPositivadoSchema = z.object({
idCliente: z.number().int(),
nome: z.string(),
razao: z.string().nullable(),
diasSemPedido: z.number().int(), // 999 = nunca comprou
ultimoPedido: z.string().nullable(), // ISO date YYYY-MM-DD
comprouAntes: z.boolean(),
whatsapp: z.string().nullable(),
});
export type ClienteNaoPositivado = z.infer<typeof ClienteNaoPositivadoSchema>;
// Dimensão de meta. O ERP (vw_metas.tipo) define como o cliente acompanha metas:
// GL = global, GR = por grupo. Motor único; outras dimensões (marca/subgrupo/
// produto) entram aqui depois sem mudar a forma.
export const MetaDimensaoSchema = z.enum(['global', 'grupo']);
export type MetaDimensao = z.infer<typeof MetaDimensaoSchema>;
// Linha de meta vs realizado por grupo, multi-medida (codigo=null = rollup global).
// fator = R$/kg (valor/peso); o ERP guarda vl_fator na meta.
export const MetaItemSchema = z.object({
codigo: z.number().int().nullable(),
rotulo: z.string(),
pedidos: z.number().int(),
valorMeta: z.number(),
valorReal: z.number(),
qtdMeta: z.number(),
qtdReal: z.number(),
pesoMeta: z.number(),
pesoReal: z.number(),
fatorMeta: z.number(),
fatorReal: z.number(),
pct: z.number(),
falta: z.number(),
});
export type MetaItem = z.infer<typeof MetaItemSchema>;
export const RepDashboardSchema = z.object({
meta: z.object({
atingido: z.number(),
total: z.number(),
pct: z.number(),
falta: z.number(),
}),
metaDimensao: MetaDimensaoSchema.default('global'),
metasPorGrupo: z.array(MetaItemSchema).default([]),
pedidosMes: z.number().int(),
pedidosRecentes: z.array(PedidoSummarySchema),
naoPositivados: z.array(ClienteNaoPositivadoSchema),
totalNaoPositivados: z.number().int(),
syncedAt: z.iso.datetime(),
});
export type RepDashboard = z.infer<typeof RepDashboardSchema>;
export const RepInativosSummarySchema = z.object({
codVendedor: z.number().int(),
nomeVendedor: z.string().nullable().optional(),
inativosCount: z.number().int(),
});
export type RepInativosSummary = z.infer<typeof RepInativosSummarySchema>;
export const SupervisorDashboardSchema = z.object({
approvalQueue: z.array(PedidoSummarySchema),
pedidosDia: z.object({
count: z.number().int(),
total: z.number(),
countSemanaAnterior: z.number().int(),
totalSemanaAnterior: z.number(),
}),
inativosPorRep: z.array(RepInativosSummarySchema),
syncedAt: z.iso.datetime(),
});
export type SupervisorDashboard = z.infer<typeof SupervisorDashboardSchema>;
export const PositivacaoRepSchema = z.object({
codVendedor: z.number().int(),
nomeVendedor: z.string().nullable(),
totalClientes: z.number().int(),
clientesPositivados: z.number().int(),
pctPositivacao: z.number(),
});
export type PositivacaoRep = z.infer<typeof PositivacaoRepSchema>;
export const RankingRepSchema = z.object({
codVendedor: z.number().int(),
nomeVendedor: z.string().nullable(),
pedidos: z.number().int(),
clientesAtendidos: z.number().int(),
faturamento: z.number(),
pctMeta: z.number(),
});
export type RankingRep = z.infer<typeof RankingRepSchema>;
export const ManagerDashboardSchema = z.object({
faturamentoMes: z.number(),
pedidosMes: z.number().int(),
ticketMedio: z.number(),
totalReps: z.number().int(),
promocoesAtivas: z.number().int(),
metaTotal: z.number(),
rankingReps: z.array(RankingRepSchema),
positivacaoReps: z.array(PositivacaoRepSchema),
syncedAt: z.iso.datetime(),
});
export type ManagerDashboard = z.infer<typeof ManagerDashboardSchema>;