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 { RepDashboard, SupervisorDashboard } from '@sar/api-interface';
import type { WorkspaceClsStore } from '../workspace/workspace.types'; import type { WorkspaceClsStore } from '../workspace/workspace.types';
// Situa: 1=Pendente, 2=Aprovado, 3=Cancelado, 4=Faturado // Situa SAR: 1=Pendente, 2=Aprovado, 3=Cancelado, 4=Faturado
const SITUA_PENDENTE = 1;
const SITUA_APROVADO = 2; const SITUA_APROVADO = 2;
const SITUA_FATURADO = 4; const SITUA_FATURADO = 4;
const SITUA_CANCELADO = 3; 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 { interface InativoRow {
id_cliente: number; id_cliente: number;
@@ -38,17 +50,38 @@ export class DashboardService {
const monthStart = new Date(year, month - 1, 1); const monthStart = new Date(year, month - 1, 1);
const monthEnd = new Date(year, month, 0, 23, 59, 59, 999); const monthEnd = new Date(year, month, 0, 23, 59, 59, 999);
// Meta e taxas do mês // 1. Meta geral do mês — fonte: gestao.metavenda (via vw_metas), tipo='G'
const target = await prisma.metaRepresentante.findUnique({ const metaRows = await prisma.$queryRawUnsafe<MetaRow[]>(`
where: { SELECT valor::text
codVendedor_idEmpresa_ano_mes: { codVendedor, idEmpresa, ano: year, mes: month }, FROM vw_metas
}, WHERE id_empresa = ${idEmpresa}
}); AND cod_vendedor = ${codVendedor}
const targetAmount = target ? Number(target.metaValor) : 0; AND TRIM(tipo) = '${TIPO_META_GERAL}'
const commissionRate = target ? Number(target.taxaComissao) : 3; AND ano = ${year}
const flexRate = target ? Number(target.taxaFlex) : 1; 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({ const approvedThisMonth = await prisma.pedido.findMany({
where: { where: {
codVendedor, codVendedor,
@@ -64,9 +97,11 @@ export class DashboardService {
const fixa = Math.round(atingido * commissionRate) / 100; const fixa = Math.round(atingido * commissionRate) / 100;
const flex = 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({ const pedidosMes = await prisma.pedido.count({
where: { where: {
codVendedor, 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 sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const recentOrders = await prisma.pedido.findMany({ const recentOrders = await prisma.pedido.findMany({
where: { where: {
@@ -89,21 +124,25 @@ export class DashboardService {
take: 10, 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 thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const inactiveClients = await prisma.$queryRawUnsafe<InativoRow[]>(` const inactiveClients = await prisma.$queryRawUnsafe<InativoRow[]>(`
SELECT SELECT
c.id_cliente, c.id_cliente,
c.nome, c.nome,
MAX(p.dt_pedido) AS dt_ultima_compra, MAX(p.dt_pedido) AS dt_ultima_compra,
MAX(p.total)::text AS ultima_compra_valor MAX(p.total)::text AS ultima_compra_valor
FROM vw_clientes c 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
WHERE c.id_empresa = ${idEmpresa} 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.cod_vendedor = ${codVendedor}
AND c.ativo = 1 AND c.ativo = 1
GROUP BY c.id_cliente, c.nome 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 ORDER BY dt_ultima_compra ASC NULLS FIRST
LIMIT 10 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 lastWeekStart = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000);
const lastWeekEnd = new Date(lastWeekStart.getTime() + 24 * 60 * 60 * 1000 - 1); const lastWeekEnd = new Date(lastWeekStart.getTime() + 24 * 60 * 60 * 1000 - 1);
const lastWeekOrders = await prisma.pedido.findMany({ 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 thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const inativosPorRep = await prisma.$queryRawUnsafe<InativosPorRepRow[]>(` const inativosPorRep = await prisma.$queryRawUnsafe<InativosPorRepRow[]>(`
SELECT SELECT cod_vendedor, COUNT(*)::text AS inativos_count
c.cod_vendedor, FROM (
COUNT(c.id_cliente)::text AS inativos_count SELECT c.id_cliente, c.cod_vendedor
FROM vw_clientes c 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
WHERE c.id_empresa = ${idEmpresa} AND c.ativo = 1 ON p.id_cliente = c.id_cliente
GROUP BY c.cod_vendedor, c.id_cliente AND p.id_empresa = c.id_empresa
HAVING MAX(p.dt_pedido) IS NULL OR MAX(p.dt_pedido) < '${thirtyDaysAgo.toISOString()}' AND p.situa != ${SITUA_CANCELADO}
ORDER BY inativos_count DESC 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 LIMIT 3
`); `);