feat(web+api): ficha do cliente — contatos, CTR, NF-e, pedidos e produtos

- Contatos: grid de cards 3 colunas, botão "Novo Contato" no topo da página
- Sync ERP↔SAR corrigido: vw_contatos aceita formato COR#<id>, trigger normaliza id_empresa (9001→1)
- CTR + NF-e: layout 50/50 — lista de títulos abertos com badge vencido/a vencer e lista de notas com botão copiar chave NF-e
- Histórico de pedidos: UNION SAR+ERP, top 5 + modal "Ver todos"
- Produtos mais comprados: top 5 com último preço + modal "Ver todos"
- Novos endpoints: ctr-list, notas, orders-history, top-produtos
- AppShell: overflow-x travado, sem scroll horizontal na aplicação

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 14:13:21 +00:00
parent a2bab75bad
commit 400fcb3360
23 changed files with 2532 additions and 356 deletions

View File

@@ -3,13 +3,16 @@ import { PedidoSummarySchema } from './order.contract';
// ADR 0006 revogado: OrderSummary → PedidoSummary, ids numéricos.
export const ClienteInativoSchema = z.object({
export const ClienteNaoPositivadoSchema = z.object({
idCliente: z.number().int(),
nome: z.string(),
diasSemCompra: z.number().int(),
ultimaCompraValor: z.string().nullable(),
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 ClienteInativo = z.infer<typeof ClienteInativoSchema>;
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/
@@ -22,7 +25,7 @@ export type MetaDimensao = z.infer<typeof MetaDimensaoSchema>;
export const MetaItemSchema = z.object({
codigo: z.number().int().nullable(),
rotulo: z.string(),
pedidos: z.number().int(), // qtd de pedidos faturados no grupo (realizado)
pedidos: z.number().int(),
valorMeta: z.number(),
valorReal: z.number(),
qtdMeta: z.number(),
@@ -31,8 +34,8 @@ export const MetaItemSchema = z.object({
pesoReal: z.number(),
fatorMeta: z.number(),
fatorReal: z.number(),
pct: z.number(), // % de valor (real/meta) — base da barra de progresso
falta: z.number(), // valor faltante p/ a meta
pct: z.number(),
falta: z.number(),
});
export type MetaItem = z.infer<typeof MetaItemSchema>;
@@ -43,17 +46,12 @@ export const RepDashboardSchema = z.object({
pct: z.number(),
falta: z.number(),
}),
// Dimensão detectada do ERP e detalhamento por grupo (vazio quando global).
metaDimensao: MetaDimensaoSchema.default('global'),
metasPorGrupo: z.array(MetaItemSchema).default([]),
comissao: z.object({
fixa: z.number(),
flex: z.number(),
total: z.number(),
}),
pedidosMes: z.number().int(),
pedidosRecentes: z.array(PedidoSummarySchema),
clientesInativos: z.array(ClienteInativoSchema),
naoPositivados: z.array(ClienteNaoPositivadoSchema),
totalNaoPositivados: z.number().int(),
syncedAt: z.iso.datetime(),
});
export type RepDashboard = z.infer<typeof RepDashboardSchema>;