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:
@@ -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';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>;
|
||||
|
||||
70
libs/shared/api-interface/src/lib/report.contract.ts
Normal file
70
libs/shared/api-interface/src/lib/report.contract.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user