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:
@@ -35,16 +35,14 @@ interface RealizadoGrupoRow {
|
||||
peso: string;
|
||||
}
|
||||
|
||||
interface RepRow {
|
||||
taxa_com: string;
|
||||
permitir_flex: number; // 0 ou 1 (char do ERP convertido)
|
||||
}
|
||||
|
||||
interface InativoRow {
|
||||
interface NaoPositivadoRow {
|
||||
id_cliente: number;
|
||||
nome: string;
|
||||
dt_ultima_compra: Date | null;
|
||||
ultima_compra_valor: string | null;
|
||||
razao: string | null;
|
||||
dt_ultimo_pedido: string | null; // YYYY-MM-DD ou null
|
||||
dias_sem_pedido: number | null;
|
||||
comprou_antes: boolean;
|
||||
whatsapp: string | null;
|
||||
}
|
||||
|
||||
interface InativosPorRepRow {
|
||||
@@ -92,24 +90,7 @@ export class DashboardService {
|
||||
: grRows.reduce((a, m) => a + Number(m.valor), 0);
|
||||
const metaDimensao = grRows.length > 0 ? ('grupo' as const) : ('global' as const);
|
||||
|
||||
// 2. Taxas do representante — fonte: gestao.vendedor (via vw_representantes)
|
||||
const repRows = await prisma.$queryRawUnsafe<RepRow[]>(`
|
||||
SELECT taxa_com::text, COALESCE(permitir_flex, 0) AS permitir_flex
|
||||
FROM vw_representantes
|
||||
WHERE codigo = ${codVendedor}
|
||||
LIMIT 1
|
||||
`);
|
||||
const commissionRate = repRows[0] ? Number(repRows[0].taxa_com) : 3;
|
||||
const permitirFlex = (repRows[0]?.permitir_flex ?? 0) === 1;
|
||||
|
||||
// 3. Taxa flex — fonte: sar.meta_representante (override SAR; default 1%)
|
||||
const flexOverride = await prisma.metaRepresentante.findUnique({
|
||||
where: { codVendedor_idEmpresa_ano_mes: { codVendedor, idEmpresa, ano: year, mes: month } },
|
||||
select: { taxaFlex: true },
|
||||
});
|
||||
const flexRate = flexOverride ? Number(flexOverride.taxaFlex) : 1;
|
||||
|
||||
// 4. Atingido do mês — realizado = tudo menos Cancelado(5) e Pendente/não-transmitido(1).
|
||||
// 2. Atingido do mês — realizado = tudo menos Cancelado(5) e Pendente/não-transmitido(1).
|
||||
// Inclui Liberado(2), Enviado(3,6,92,95,200) e Faturado(4). Base: data do pedido.
|
||||
const monthStartStr = monthStart.toISOString().slice(0, 10);
|
||||
const monthEndStr = monthEnd.toISOString().slice(0, 10);
|
||||
@@ -198,32 +179,55 @@ export class DashboardService {
|
||||
const pct = targetAmount > 0 ? Math.round((atingido / targetAmount) * 100) : 0;
|
||||
const falta = Math.max(0, targetAmount - atingido);
|
||||
|
||||
const fixa = Math.round(atingido * commissionRate) / 100;
|
||||
const flex =
|
||||
permitirFlex && targetAmount > 0 && atingido >= targetAmount
|
||||
? Math.round(atingido * flexRate) / 100
|
||||
: 0;
|
||||
|
||||
// 7. Clientes inativos — sem pedido no ERP há >30 dias
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const inactiveClients = await prisma.$queryRawUnsafe<InativoRow[]>(`
|
||||
// Clientes não positivados no mês: ativos do representante sem pedido desde monthStart.
|
||||
// Junta o último pedido histórico (ERP+SAR) e o primeiro whatsapp ativo dos contatos.
|
||||
const naoPositivadosRows = await prisma.$queryRawUnsafe<NaoPositivadoRow[]>(`
|
||||
WITH ultimo_pedido AS (
|
||||
SELECT id_cliente,
|
||||
MAX(dt_pedido) AS dt_max
|
||||
FROM (
|
||||
SELECT id_cliente, dt_pedido FROM vw_pedidos_erp
|
||||
WHERE situa NOT IN (5) AND id_empresa = ${idEmpresa}
|
||||
UNION ALL
|
||||
SELECT id_cliente, dt_pedido FROM sar.pedidos
|
||||
WHERE situa != 3 AND id_empresa = ${idEmpresa}
|
||||
) t
|
||||
GROUP BY id_cliente
|
||||
),
|
||||
pedido_mes AS (
|
||||
SELECT DISTINCT id_cliente FROM (
|
||||
SELECT id_cliente FROM vw_pedidos_erp
|
||||
WHERE situa NOT IN (5) AND id_empresa = ${idEmpresa}
|
||||
AND dt_pedido >= '${monthStartStr}'
|
||||
UNION
|
||||
SELECT id_cliente FROM sar.pedidos
|
||||
WHERE situa != 3 AND id_empresa = ${idEmpresa}
|
||||
AND dt_pedido >= '${monthStartStr}'
|
||||
) t
|
||||
)
|
||||
SELECT
|
||||
c.id_cliente,
|
||||
c.nome,
|
||||
MAX(p.dt_pedido) AS dt_ultima_compra,
|
||||
MAX(p.total)::text AS ultima_compra_valor
|
||||
TRIM(c.nome) AS nome,
|
||||
TRIM(c.razao) AS razao,
|
||||
TO_CHAR(up.dt_max, 'YYYY-MM-DD') AS dt_ultimo_pedido,
|
||||
(CURRENT_DATE - up.dt_max::date) AS dias_sem_pedido,
|
||||
(up.dt_max IS NOT NULL) AS comprou_antes,
|
||||
ct.whatsapp
|
||||
FROM vw_clientes c
|
||||
LEFT JOIN vw_pedidos_erp p
|
||||
ON p.id_cliente = c.id_cliente
|
||||
AND p.id_empresa = ${idEmpresa}
|
||||
AND p.situa != 5
|
||||
LEFT JOIN ultimo_pedido up ON up.id_cliente = c.id_cliente
|
||||
LEFT JOIN pedido_mes pm ON pm.id_cliente = c.id_cliente
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT TRIM(whatsapp) AS whatsapp
|
||||
FROM sar.vw_contatos
|
||||
WHERE id_corrent = c.id_cliente
|
||||
AND whatsapp IS NOT NULL AND TRIM(whatsapp) != ''
|
||||
AND ativo = 1
|
||||
LIMIT 1
|
||||
) ct ON true
|
||||
WHERE c.cod_vendedor = ${codVendedor}
|
||||
AND c.ativo = 1
|
||||
GROUP BY c.id_cliente, c.nome
|
||||
HAVING MAX(p.dt_pedido) IS NULL
|
||||
OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString().slice(0, 10)}'
|
||||
ORDER BY dt_ultima_compra ASC NULLS FIRST
|
||||
LIMIT 10
|
||||
AND pm.id_cliente IS NULL
|
||||
ORDER BY up.dt_max ASC NULLS FIRST
|
||||
`);
|
||||
|
||||
// Metas por grupo: junta meta (GR) com realizado (itens), por cod_grupo.
|
||||
@@ -257,11 +261,20 @@ export class DashboardService {
|
||||
})
|
||||
.sort((a, b) => b.valorMeta - a.valorMeta);
|
||||
|
||||
const naoPositivados = naoPositivadosRows.map((c) => ({
|
||||
idCliente: Number(c.id_cliente),
|
||||
nome: c.nome,
|
||||
razao: c.razao ?? null,
|
||||
diasSemPedido: c.dias_sem_pedido != null ? Number(c.dias_sem_pedido) : 999,
|
||||
ultimoPedido: c.dt_ultimo_pedido ?? null,
|
||||
comprouAntes: Boolean(c.comprou_antes),
|
||||
whatsapp: c.whatsapp ?? null,
|
||||
}));
|
||||
|
||||
return {
|
||||
meta: { atingido, total: targetAmount, pct, falta },
|
||||
metaDimensao,
|
||||
metasPorGrupo,
|
||||
comissao: { fixa, flex, total: fixa + flex },
|
||||
pedidosMes,
|
||||
pedidosRecentes: recentRows.map((o) => ({
|
||||
id: `erp-${o.id_pedido}`,
|
||||
@@ -281,14 +294,8 @@ export class DashboardService {
|
||||
createdAt: new Date(o.dt_pedido).toISOString(),
|
||||
fonte: 'erp' as const,
|
||||
})),
|
||||
clientesInativos: inactiveClients.map((c) => ({
|
||||
idCliente: Number(c.id_cliente),
|
||||
nome: c.nome,
|
||||
diasSemCompra: c.dt_ultima_compra
|
||||
? Math.floor((now.getTime() - c.dt_ultima_compra.getTime()) / 86_400_000)
|
||||
: 999,
|
||||
ultimaCompraValor: c.ultima_compra_valor,
|
||||
})),
|
||||
naoPositivados,
|
||||
totalNaoPositivados: naoPositivados.length,
|
||||
syncedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user