diff --git a/apps/api/src/app/dashboard/dashboard.service.ts b/apps/api/src/app/dashboard/dashboard.service.ts index 8523f63..6b3f828 100644 --- a/apps/api/src/app/dashboard/dashboard.service.ts +++ b/apps/api/src/app/dashboard/dashboard.service.ts @@ -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(` + 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(` + 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,21 +124,25 @@ 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(` SELECT c.id_cliente, c.nome, - MAX(p.dt_pedido) AS dt_ultima_compra, - MAX(p.total)::text AS ultima_compra_valor + 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} - WHERE c.id_empresa = ${idEmpresa} + 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 + 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(` - SELECT - c.cod_vendedor, - COUNT(c.id_cliente)::text AS inativos_count - 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 + 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.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 `);