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:
@@ -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,7 +124,7 @@ 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
|
||||||
@@ -98,12 +133,16 @@ export class DashboardService {
|
|||||||
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
|
||||||
|
ON p.id_cliente = c.id_cliente
|
||||||
|
AND p.id_empresa = c.id_empresa
|
||||||
|
AND p.situa != ${SITUA_CANCELADO}
|
||||||
WHERE c.id_empresa = ${idEmpresa}
|
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
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user