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

@@ -6,3 +6,4 @@ export * from './lib/product.contract';
export * from './lib/dashboard.contract';
export * from './lib/notifications.contract';
export * from './lib/company.contract';
export * from './lib/report.contract';

View File

@@ -111,6 +111,54 @@ export const ContatoResultSchema = z.object({
});
export type ContatoResult = z.infer<typeof ContatoResultSchema>;
// ─── Produtos mais comprados por cliente ─────────────────────────────────────
export const TopProdutoSchema = z.object({
idProduto: z.number().int(),
descricao: z.string(),
unidade: z.string().nullable(),
qtdTotal: z.number(),
numPedidos: z.number().int(),
ultimaCompra: z.string(),
ultimoPreco: z.number(),
});
export type TopProduto = z.infer<typeof TopProdutoSchema>;
// ─── Contas a Receber (resumo por cliente) ───────────────────────────────────
export const CtrSummarySchema = z.object({
qtdAberto: z.number().int(),
totalAberto: z.number(),
qtdVencido: z.number().int(),
totalVencido: z.number(),
maiorAtrasoDias: z.number().int(),
});
export type CtrSummary = z.infer<typeof CtrSummarySchema>;
// ─── Contas a Receber (lista de títulos em aberto) ────────────────────────────
export const CtrTituloSchema = z.object({
idCtr: z.number().int(),
numero: z.string(),
prefixo: z.string(),
dtEmissao: z.string(),
dtVencimento: z.string(),
saldo: z.number(),
});
export type CtrTitulo = z.infer<typeof CtrTituloSchema>;
// ─── Notas Fiscais por cliente ───────────────────────────────────────────────
export const NotaFiscalSchema = z.object({
idNf: z.number().int(),
numero: z.number().int(),
serie: z.string(),
dtEmissao: z.string(),
vlNf: z.number(),
chaveAcessoNfe: z.string().nullable(),
});
export type NotaFiscal = z.infer<typeof NotaFiscalSchema>;
// ─── Municipio ───────────────────────────────────────────────────────────────
export const MunicipioSchema = z.object({

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

View File

@@ -0,0 +1,70 @@
import { z } from 'zod';
// ─── Desempenho vs. Meta ─────────────────────────────────────────────────────
export const ReportMetaQuerySchema = z.object({
mes: z.coerce.number().int().min(1).max(12),
ano: z.coerce.number().int().min(2000).max(2100),
});
export type ReportMetaQuery = z.infer<typeof ReportMetaQuerySchema>;
export const ReportMetaResponseSchema = z.object({
mes: z.number(),
ano: z.number(),
realizado: z.string(),
meta: z.string(),
percentual: z.string(),
comissaoEstimada: z.string(),
totalPedidos: z.number(),
});
export type ReportMetaResponse = z.infer<typeof ReportMetaResponseSchema>;
// ─── Carteira de Clientes ────────────────────────────────────────────────────
export const CarteiraClienteSchema = z.object({
idCliente: z.number(),
nome: z.string().nullable(),
razao: z.string().nullable(),
ativo: z.number(),
ultimoPedido: z.string().nullable(),
diasSemPedido: z.number().nullable(),
totalPedidos: z.number(),
ticketMedio: z.string(),
});
export type CarteiraCliente = z.infer<typeof CarteiraClienteSchema>;
export const ReportCarteiraResponseSchema = z.object({
data: z.array(CarteiraClienteSchema),
total: z.number(),
inativos30: z.number(),
inativos60: z.number(),
semPedido: z.number(),
});
export type ReportCarteiraResponse = z.infer<typeof ReportCarteiraResponseSchema>;
// ─── Curva ABC de Produtos ───────────────────────────────────────────────────
export const ReportAbcQuerySchema = z.object({
mes: z.coerce.number().int().min(1).max(12).optional(),
ano: z.coerce.number().int().min(2000).max(2100).optional(),
});
export type ReportAbcQuery = z.infer<typeof ReportAbcQuerySchema>;
export const AbcProdutoSchema = z.object({
codProduto: z.string().nullable(),
descProduto: z.string(),
totalFaturado: z.string(),
qtdVendida: z.string(),
participacao: z.string(),
curva: z.enum(['A', 'B', 'C']),
descontoMedio: z.string(),
comissaoTotal: z.string(),
});
export type AbcProduto = z.infer<typeof AbcProdutoSchema>;
export const ReportAbcResponseSchema = z.object({
data: z.array(AbcProdutoSchema),
totalFaturado: z.string(),
periodo: z.string(),
});
export type ReportAbcResponse = z.infer<typeof ReportAbcResponseSchema>;