feat(api): dashboard lê meta e comissão diretamente do ERP

meta geral do mês (tipo='G'): vw_metas WHERE TRIM(tipo)='G'
taxa de comissão: vw_representantes.taxa_com
flex: vw_representantes.permitir_flex + sar.meta_representante.taxaFlex
fix: query inativos_por_rep corrigida — subconsulta por cliente, outer por rep

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 11:55:21 +00:00
parent 6cdb4c578e
commit f41d9c2f16

View File

@@ -3,11 +3,23 @@ import { ClsService } from 'nestjs-cls';
import type { RepDashboard, SupervisorDashboard } from '@sar/api-interface';
import type { WorkspaceClsStore } from '../workspace/workspace.types';
// Situa: 1=Pendente, 2=Aprovado, 3=Cancelado, 4=Faturado
const SITUA_PENDENTE = 1;
// Situa SAR: 1=Pendente, 2=Aprovado, 3=Cancelado, 4=Faturado
const SITUA_APROVADO = 2;
const SITUA_FATURADO = 4;
const SITUA_CANCELADO = 3;
const SITUA_PENDENTE = 1;
// tipo='G' em gestao.metavenda = meta geral de valor do mês
const TIPO_META_GERAL = 'G';
interface MetaRow {
valor: string;
}
interface RepRow {
taxa_com: string;
permitir_flex: number; // 0 ou 1 (char do ERP convertido)
}
interface InativoRow {
id_cliente: number;
@@ -38,17 +50,38 @@ export class DashboardService {
const monthStart = new Date(year, month - 1, 1);
const monthEnd = new Date(year, month, 0, 23, 59, 59, 999);
// Meta e taxas do mês
const target = await prisma.metaRepresentante.findUnique({
where: {
codVendedor_idEmpresa_ano_mes: { codVendedor, idEmpresa, ano: year, mes: month },
},
});
const targetAmount = target ? Number(target.metaValor) : 0;
const commissionRate = target ? Number(target.taxaComissao) : 3;
const flexRate = target ? Number(target.taxaFlex) : 1;
// 1. Meta geral do mês — fonte: gestao.metavenda (via vw_metas), tipo='G'
const metaRows = await prisma.$queryRawUnsafe<MetaRow[]>(`
SELECT valor::text
FROM vw_metas
WHERE id_empresa = ${idEmpresa}
AND cod_vendedor = ${codVendedor}
AND TRIM(tipo) = '${TIPO_META_GERAL}'
AND ano = ${year}
AND mes = ${month}
LIMIT 1
`);
const targetAmount = metaRows[0] ? Number(metaRows[0].valor) : 0;
// Pedidos aprovados/faturados do mês
// 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 id_empresa = ${idEmpresa}
AND 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. Pedidos aprovados/faturados do mês (base de cálculo)
const approvedThisMonth = await prisma.pedido.findMany({
where: {
codVendedor,
@@ -64,9 +97,11 @@ export class DashboardService {
const fixa = Math.round(atingido * commissionRate) / 100;
const flex =
targetAmount > 0 && atingido >= targetAmount ? Math.round(atingido * flexRate) / 100 : 0;
permitirFlex && targetAmount > 0 && atingido >= targetAmount
? Math.round(atingido * flexRate) / 100
: 0;
// Contagem total de pedidos no mês (exceto cancelado)
// 5. Contagem de pedidos do mês (exceto cancelado)
const pedidosMes = await prisma.pedido.count({
where: {
codVendedor,
@@ -76,7 +111,7 @@ export class DashboardService {
},
});
// Pedidos recentes — últimos 7 dias
// 6. Pedidos recentes — últimos 7 dias
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const recentOrders = await prisma.pedido.findMany({
where: {
@@ -89,7 +124,7 @@ export class DashboardService {
take: 10,
});
// Clientes inativos — sem compra há >30 dias (via view + pedidos SAR)
// 7. Clientes inativos — sem pedido há >30 dias (vw_clientes + sar.pedidos)
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const inactiveClients = await prisma.$queryRawUnsafe<InativoRow[]>(`
SELECT
@@ -98,12 +133,16 @@ export class DashboardService {
MAX(p.dt_pedido) AS dt_ultima_compra,
MAX(p.total)::text AS ultima_compra_valor
FROM vw_clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id_cliente AND p.id_empresa = c.id_empresa AND p.situa != ${SITUA_CANCELADO}
LEFT JOIN pedidos p
ON p.id_cliente = c.id_cliente
AND p.id_empresa = c.id_empresa
AND p.situa != ${SITUA_CANCELADO}
WHERE c.id_empresa = ${idEmpresa}
AND 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()}'
HAVING MAX(p.dt_pedido) IS NULL
OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString()}'
ORDER BY dt_ultima_compra ASC NULLS FIRST
LIMIT 10
`);
@@ -159,7 +198,7 @@ export class DashboardService {
},
});
// Mesmo dia da semana passada
// Mesmo dia da semana passada (comparativo)
const lastWeekStart = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000);
const lastWeekEnd = new Date(lastWeekStart.getTime() + 24 * 60 * 60 * 1000 - 1);
const lastWeekOrders = await prisma.pedido.findMany({
@@ -170,18 +209,26 @@ export class DashboardService {
},
});
// Inativos por rep — top 3
// Top 3 reps com mais clientes inativos (>30 dias sem compra)
// Subconsulta agrupa por cliente, outer agrupa por rep
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const inativosPorRep = await prisma.$queryRawUnsafe<InativosPorRepRow[]>(`
SELECT
c.cod_vendedor,
COUNT(c.id_cliente)::text AS inativos_count
SELECT cod_vendedor, COUNT(*)::text AS inativos_count
FROM (
SELECT c.id_cliente, c.cod_vendedor
FROM vw_clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id_cliente AND p.id_empresa = c.id_empresa AND p.situa != ${SITUA_CANCELADO}
WHERE c.id_empresa = ${idEmpresa} AND c.ativo = 1
GROUP BY c.cod_vendedor, c.id_cliente
HAVING MAX(p.dt_pedido) IS NULL OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString()}'
ORDER BY inativos_count DESC
LEFT JOIN pedidos p
ON p.id_cliente = c.id_cliente
AND p.id_empresa = c.id_empresa
AND p.situa != ${SITUA_CANCELADO}
WHERE c.id_empresa = ${idEmpresa}
AND c.ativo = 1
GROUP BY c.id_cliente, c.cod_vendedor
HAVING MAX(p.dt_pedido) IS NULL
OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString()}'
) inativos
GROUP BY cod_vendedor
ORDER BY COUNT(*) DESC
LIMIT 3
`);