feat(api,web): regras de desconto por pauta e cards de supervisor na equipe
Regras de Desconto (Politicas Comerciais): - Nova dimensao de escopo por pauta de precos: cria 1 regra por pauta selecionada (idPautas[]), aplicada a todos os produtos da pauta. - Coluna id_pauta em sar.regras_desconto (migration idempotente + schema canonico); motor de alcada (orders) casa regra<->pedido por idPauta. - Remove a aba/endpoints "Desconto por Representante" (gestao de alcada_desconto); o teto por rep continua no motor (default 5%) e no carrinho (/politicas/alcada), agora liberado via Regra de Desconto. Aba Equipe do gerente: - GET /equipe/supervisores: cards de disputa por supervisor (faturamento, meta, positivacao, variacao mensal, reps ativos). - SupervisorDetalheModal: detalhe do time (ranking + positivacao) reusando /dashboard/supervisor escopado por codSupervisor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
-- AlterTable: sar.regras_desconto -- adiciona escopo por pauta de precos.
|
||||
-- Regra pode ser restrita a uma pauta especifica; id_pauta NULL = qualquer pauta.
|
||||
-- provision-client.ts roda scripts/sar-erp-schema.sql (idempotente) antes de
|
||||
-- `prisma migrate deploy`, entao este ALTER precisa ser idempotente tambem.
|
||||
-- ATENCAO: bancos ERP usam LATIN1 - nao usar em-dash nem box-drawing aqui.
|
||||
ALTER TABLE sar.regras_desconto ADD COLUMN IF NOT EXISTS id_pauta INTEGER;
|
||||
@@ -174,7 +174,8 @@ model Promocao {
|
||||
// ─── RegraDesconto (Políticas Comerciais) ────────────────────────────────────
|
||||
//
|
||||
// Regra de desconto criada pelo Gerente: percentual liberado por grupo e/ou
|
||||
// subgrupo de produto e/ou representante, com vigência. Campos nulos = "todos".
|
||||
// subgrupo de produto, pauta de preços e/ou representante, com vigência.
|
||||
// Campos nulos = "todos" (id_pauta nulo = qualquer pauta).
|
||||
|
||||
model RegraDesconto {
|
||||
id Int @id @default(autoincrement())
|
||||
@@ -183,6 +184,7 @@ model RegraDesconto {
|
||||
codGrupo Int? @map("cod_grupo")
|
||||
codSubgrupo Int? @map("cod_subgrupo")
|
||||
codVendedor Int? @map("cod_vendedor")
|
||||
idPauta Int? @map("id_pauta")
|
||||
descPct Decimal @db.Decimal(5, 2) @map("desc_pct")
|
||||
dataInicio DateTime @db.Date @map("data_inicio")
|
||||
dataFim DateTime @db.Date @map("data_fim")
|
||||
|
||||
@@ -45,9 +45,13 @@ export class DashboardController {
|
||||
}
|
||||
|
||||
@Get('supervisor')
|
||||
supervisorDashboard(): Promise<SupervisorDashboard> {
|
||||
supervisorDashboard(
|
||||
@Query('codSupervisor') codSupervisor?: string,
|
||||
): Promise<SupervisorDashboard> {
|
||||
this.assertGestor();
|
||||
return this.dashboard.supervisorDashboard();
|
||||
return this.dashboard.supervisorDashboard(
|
||||
codSupervisor ? parseInt(codSupervisor, 10) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('supervisor/inativos')
|
||||
|
||||
@@ -729,18 +729,25 @@ export class DashboardService {
|
||||
});
|
||||
}
|
||||
|
||||
async supervisorDashboard(): Promise<SupervisorDashboard> {
|
||||
async supervisorDashboard(codSupervisor?: number): Promise<SupervisorDashboard> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
const now = new Date();
|
||||
|
||||
// Supervisor vê só a própria equipe (cod_supervisor em vw_representantes);
|
||||
// gerente/admin acessando este painel veem a empresa toda.
|
||||
// Supervisor vê só a própria equipe (cod_supervisor em vw_representantes).
|
||||
// Gerente/admin: sem codSupervisor veem a empresa toda; com codSupervisor
|
||||
// (Detalhar da aba Equipe) veem o time daquele supervisor. codSupervisor é
|
||||
// ignorado para role 'supervisor' — não pode espiar outra equipe.
|
||||
const role = this.cls.get('role');
|
||||
const userId = this.cls.get('userId');
|
||||
const team =
|
||||
role === 'supervisor' ? await getTeamCodes(prisma, userId ? parseInt(userId, 10) : 0) : null;
|
||||
let teamCode: number | null = null;
|
||||
if (role === 'supervisor') {
|
||||
teamCode = userId ? parseInt(userId, 10) : 0;
|
||||
} else if (codSupervisor && codSupervisor > 0) {
|
||||
teamCode = codSupervisor;
|
||||
}
|
||||
const team = teamCode ? await getTeamCodes(prisma, teamCode) : null;
|
||||
const teamSqlFilter = (col: string) => (team ? `AND ${col} IN (${team.join(',')})` : '');
|
||||
|
||||
// Pedidos do dia — lê do ERP (situa != 5=Cancelado)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import type { EquipeResponse } from '@sar/api-interface';
|
||||
import type { EquipeResponse, SupervisoresResponse } from '@sar/api-interface';
|
||||
import { EquipeService } from './equipe.service';
|
||||
|
||||
@Controller({ path: 'equipe' })
|
||||
@@ -10,4 +10,9 @@ export class EquipeController {
|
||||
listEquipe(): Promise<EquipeResponse> {
|
||||
return this.equipe.listEquipe();
|
||||
}
|
||||
|
||||
@Get('supervisores')
|
||||
listSupervisores(): Promise<SupervisoresResponse> {
|
||||
return this.equipe.listSupervisores();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type { EquipeResponse } from '@sar/api-interface';
|
||||
import type { EquipeResponse, SupervisorCard, SupervisoresResponse } from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
|
||||
function matrizEmpresa(idEmpresa: number): number {
|
||||
@@ -110,4 +110,173 @@ export class EquipeService {
|
||||
syncedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Cards de supervisor da aba Equipe: agrega as métricas do mês por equipe.
|
||||
// Time de um supervisor = reps cujo cod_supervisor aponta para ele + ele
|
||||
// próprio (mesma regra de getTeamCodes, que o Detalhe reusa via
|
||||
// /dashboard/supervisor?codSupervisor=). Agregação feita em JS a partir de
|
||||
// métricas por rep — as mesmas contas do painel, sem escopo por equipe.
|
||||
async listSupervisores(): Promise<SupervisoresResponse> {
|
||||
const role = this.cls.get('role') ?? 'rep';
|
||||
if (role !== 'manager' && role !== 'admin') {
|
||||
throw new ForbiddenException('Apenas gerentes podem consultar os supervisores');
|
||||
}
|
||||
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
const idEmpresaMatriz = matrizEmpresa(idEmpresa);
|
||||
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1;
|
||||
const monthStartStr = new Date(year, month - 1, 1).toISOString().slice(0, 10);
|
||||
const monthEndStr = new Date(year, month, 0).toISOString().slice(0, 10);
|
||||
const prevStartStr = new Date(year, month - 2, 1).toISOString().slice(0, 10);
|
||||
const prevEndStr = new Date(year, month - 1, 0).toISOString().slice(0, 10);
|
||||
|
||||
const [repRows, pedidosRows, prevRows, metaRows, positivacaoRows] = await Promise.all([
|
||||
prisma.$queryRawUnsafe<
|
||||
{ cod_vendedor: number; cod_supervisor: number | null; nome: string | null }[]
|
||||
>(`SELECT codigo AS cod_vendedor, cod_supervisor, nome FROM vw_representantes`),
|
||||
prisma.$queryRawUnsafe<PedidosRepRow[]>(`
|
||||
SELECT cod_vendedor,
|
||||
COUNT(*)::text AS pedidos,
|
||||
COALESCE(SUM(total), 0)::text AS faturamento
|
||||
FROM vw_pedidos_erp
|
||||
WHERE id_empresa = ${idEmpresa}
|
||||
AND situa NOT IN (1, 5)
|
||||
AND dt_pedido >= '${monthStartStr}'
|
||||
AND dt_pedido <= '${monthEndStr}'
|
||||
GROUP BY cod_vendedor
|
||||
`),
|
||||
prisma.$queryRawUnsafe<{ cod_vendedor: number; faturamento: string }[]>(`
|
||||
SELECT cod_vendedor,
|
||||
COALESCE(SUM(total), 0)::text AS faturamento
|
||||
FROM vw_pedidos_erp
|
||||
WHERE id_empresa = ${idEmpresa}
|
||||
AND situa NOT IN (1, 5)
|
||||
AND dt_pedido >= '${prevStartStr}'
|
||||
AND dt_pedido <= '${prevEndStr}'
|
||||
GROUP BY cod_vendedor
|
||||
`),
|
||||
prisma.$queryRawUnsafe<MetaRepRow[]>(`
|
||||
SELECT cod_vendedor,
|
||||
SUM(valor)::text AS meta_valor
|
||||
FROM sar.vw_metas
|
||||
WHERE id_empresa = ${idEmpresaMatriz}
|
||||
AND ano = ${year}
|
||||
AND mes = ${month}
|
||||
AND TRIM(tipo) = 'GR'
|
||||
GROUP BY cod_vendedor
|
||||
`),
|
||||
prisma.$queryRawUnsafe<
|
||||
{ cod_vendedor: number; total_clientes: string; positivados: string }[]
|
||||
>(`
|
||||
SELECT c.cod_vendedor,
|
||||
COUNT(DISTINCT c.id_cliente)::text AS total_clientes,
|
||||
COUNT(DISTINCT CASE WHEN ped.id_cliente IS NOT NULL THEN c.id_cliente END)::text AS positivados
|
||||
FROM vw_clientes c
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT id_cliente, cod_vendedor
|
||||
FROM vw_pedidos_erp
|
||||
WHERE id_empresa = ${idEmpresa}
|
||||
AND situa NOT IN (1, 5)
|
||||
AND dt_pedido >= '${monthStartStr}'
|
||||
AND dt_pedido <= '${monthEndStr}'
|
||||
) ped ON ped.id_cliente = c.id_cliente AND ped.cod_vendedor = c.cod_vendedor
|
||||
WHERE c.cod_vendedor > 0
|
||||
GROUP BY c.cod_vendedor
|
||||
`),
|
||||
]);
|
||||
|
||||
const pedidosMap = new Map(
|
||||
pedidosRows.map((r) => [
|
||||
Number(r.cod_vendedor),
|
||||
{ pedidos: Number(r.pedidos), faturamento: Number(r.faturamento) },
|
||||
]),
|
||||
);
|
||||
const prevMap = new Map(prevRows.map((r) => [Number(r.cod_vendedor), Number(r.faturamento)]));
|
||||
const metaMap = new Map(metaRows.map((r) => [Number(r.cod_vendedor), Number(r.meta_valor)]));
|
||||
const posMap = new Map(
|
||||
positivacaoRows.map((r) => [
|
||||
Number(r.cod_vendedor),
|
||||
{ total: Number(r.total_clientes), positivados: Number(r.positivados) },
|
||||
]),
|
||||
);
|
||||
|
||||
// Monta a equipe de cada supervisor: código do supervisor -> códigos dos reps.
|
||||
// O supervisor entra no próprio time (getTeamCodes inclui o supervisor).
|
||||
const nomeByCod = new Map<number, string | null>();
|
||||
const membrosBySup = new Map<number, Set<number>>();
|
||||
for (const r of repRows) {
|
||||
const cod = Number(r.cod_vendedor);
|
||||
if (!nomeByCod.has(cod)) nomeByCod.set(cod, r.nome ?? null);
|
||||
const sup = r.cod_supervisor != null ? Number(r.cod_supervisor) : 0;
|
||||
if (sup > 0) {
|
||||
let membros = membrosBySup.get(sup);
|
||||
if (!membros) {
|
||||
membros = new Set();
|
||||
membrosBySup.set(sup, membros);
|
||||
}
|
||||
membros.add(cod);
|
||||
}
|
||||
}
|
||||
|
||||
const supervisores: SupervisorCard[] = [...membrosBySup.entries()].map(([sup, membros]) => {
|
||||
const team = new Set(membros);
|
||||
team.add(sup); // o próprio supervisor faz parte do time
|
||||
|
||||
let faturamento = 0;
|
||||
let faturamentoAnterior = 0;
|
||||
let pedidos = 0;
|
||||
let meta = 0;
|
||||
let totalClientes = 0;
|
||||
let positivados = 0;
|
||||
let repsAtivos = 0;
|
||||
for (const cod of team) {
|
||||
const p = pedidosMap.get(cod);
|
||||
if (p) {
|
||||
faturamento += p.faturamento;
|
||||
pedidos += p.pedidos;
|
||||
if (p.faturamento > 0) repsAtivos += 1;
|
||||
}
|
||||
faturamentoAnterior += prevMap.get(cod) ?? 0;
|
||||
meta += metaMap.get(cod) ?? 0;
|
||||
const pos = posMap.get(cod);
|
||||
if (pos) {
|
||||
totalClientes += pos.total;
|
||||
positivados += pos.positivados;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
codSupervisor: sup,
|
||||
nomeSupervisor: nomeByCod.get(sup) ?? null,
|
||||
faturamentoMes: faturamento,
|
||||
faturamentoMesAnterior: faturamentoAnterior,
|
||||
variacaoPct:
|
||||
faturamentoAnterior > 0
|
||||
? Math.round(((faturamento - faturamentoAnterior) / faturamentoAnterior) * 100)
|
||||
: null,
|
||||
pedidosMes: pedidos,
|
||||
ticketMedio: pedidos > 0 ? faturamento / pedidos : 0,
|
||||
metaTotal: meta,
|
||||
pctMeta: meta > 0 ? Math.round((faturamento / meta) * 100) : 0,
|
||||
totalClientes,
|
||||
clientesPositivados: positivados,
|
||||
pctPositivacao: totalClientes > 0 ? Math.round((positivados / totalClientes) * 100) : 0,
|
||||
repsTotal: team.size,
|
||||
repsAtivos,
|
||||
};
|
||||
});
|
||||
|
||||
// Ranking/disputa: maior faturamento primeiro.
|
||||
supervisores.sort((a, b) => b.faturamentoMes - a.faturamentoMes);
|
||||
|
||||
return {
|
||||
supervisores,
|
||||
syncedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,14 +626,16 @@ export class OrdersService {
|
||||
)
|
||||
.reduce((max, p) => Math.max(max, Number(p.descPct)), 0);
|
||||
|
||||
// Regra sem grupo/subgrupo vale para qualquer produto; com grupo e/ou
|
||||
// subgrupo, o produto precisa casar com o que a regra especifica.
|
||||
// Regra sem alvo vale para qualquer produto; com grupo, subgrupo e/ou
|
||||
// pauta, precisa casar com o que a regra especifica. Regra por pauta só
|
||||
// vale quando o pedido usa aquela pauta (id_pauta nulo = qualquer pauta).
|
||||
const codSubgrupo = prod?.cod_subgrupo != null ? Number(prod.cod_subgrupo) : null;
|
||||
const regraPct = regras
|
||||
.filter(
|
||||
(r) =>
|
||||
(r.codGrupo == null || (codGrupo != null && r.codGrupo === codGrupo)) &&
|
||||
(r.codSubgrupo == null || (codSubgrupo != null && r.codSubgrupo === codSubgrupo)),
|
||||
(r.codSubgrupo == null || (codSubgrupo != null && r.codSubgrupo === codSubgrupo)) &&
|
||||
(r.idPauta == null || (pedido.idPauta != null && r.idPauta === pedido.idPauta)),
|
||||
)
|
||||
.reduce((max, r) => Math.max(max, Number(r.descPct)), 0);
|
||||
|
||||
|
||||
@@ -11,17 +11,14 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import {
|
||||
UpsertDescontoBodySchema,
|
||||
CreatePromocaoBodySchema,
|
||||
UpdatePromocaoBodySchema,
|
||||
CreateRegraDescontoBodySchema,
|
||||
UpdateRegraDescontoBodySchema,
|
||||
type UpsertDescontoBody,
|
||||
type CreatePromocaoBody,
|
||||
type UpdatePromocaoBody,
|
||||
type CreateRegraDescontoBody,
|
||||
type UpdateRegraDescontoBody,
|
||||
type AlcadaDescontosResponse,
|
||||
type PromocoesResponse,
|
||||
type PromocoesVigentesResponse,
|
||||
type AlcadaRepResponse,
|
||||
@@ -31,7 +28,6 @@ import {
|
||||
} from '@sar/api-interface';
|
||||
import { PoliticasService } from './politicas.service';
|
||||
|
||||
class UpsertDescontoDto extends createZodDto(UpsertDescontoBodySchema) {}
|
||||
class CreatePromocaoDto extends createZodDto(CreatePromocaoBodySchema) {}
|
||||
class UpdatePromocaoDto extends createZodDto(UpdatePromocaoBodySchema) {}
|
||||
class CreateRegraDescontoDto extends createZodDto(CreateRegraDescontoBodySchema) {}
|
||||
@@ -41,17 +37,6 @@ class UpdateRegraDescontoDto extends createZodDto(UpdateRegraDescontoBodySchema)
|
||||
export class PoliticasController {
|
||||
constructor(private readonly politicas: PoliticasService) {}
|
||||
|
||||
@Get('descontos')
|
||||
listDescontos(): Promise<AlcadaDescontosResponse> {
|
||||
return this.politicas.listDescontos();
|
||||
}
|
||||
|
||||
@Post('descontos')
|
||||
@HttpCode(200)
|
||||
upsertDesconto(@Body() body: UpsertDescontoDto): Promise<void> {
|
||||
return this.politicas.upsertDesconto(body as unknown as UpsertDescontoBody);
|
||||
}
|
||||
|
||||
@Get('promocoes')
|
||||
listPromocoes(): Promise<PromocoesResponse> {
|
||||
return this.politicas.listPromocoes();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type {
|
||||
AlcadaDescontosResponse,
|
||||
UpsertDescontoBody,
|
||||
PromocoesResponse,
|
||||
CreatePromocaoBody,
|
||||
UpdatePromocaoBody,
|
||||
@@ -27,61 +25,6 @@ export class PoliticasService {
|
||||
}
|
||||
}
|
||||
|
||||
async listDescontos(): Promise<AlcadaDescontosResponse> {
|
||||
this.assertManager();
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
|
||||
const rows = await prisma.alcadaDesconto.findMany({
|
||||
where: { idEmpresa },
|
||||
orderBy: [{ codVendedor: 'asc' }, { codGrupo: 'asc' }],
|
||||
});
|
||||
|
||||
// Busca nomes dos representantes
|
||||
const codigos = [...new Set(rows.map((r) => r.codVendedor))];
|
||||
const repRows =
|
||||
codigos.length > 0
|
||||
? await prisma.$queryRawUnsafe<{ codigo: number; nome: string | null }[]>(
|
||||
`SELECT codigo, nome FROM vw_representantes WHERE codigo IN (${codigos.join(',')})`,
|
||||
)
|
||||
: [];
|
||||
const repNameMap = new Map(repRows.map((r) => [Number(r.codigo), r.nome]));
|
||||
|
||||
return {
|
||||
descontos: rows.map((r) => ({
|
||||
codVendedor: r.codVendedor,
|
||||
codGrupo: r.codGrupo,
|
||||
limitePerc: Number(r.limitePerc),
|
||||
nomeVendedor: repNameMap.get(r.codVendedor) ?? null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async upsertDesconto(body: UpsertDescontoBody): Promise<void> {
|
||||
this.assertManager();
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
|
||||
await prisma.alcadaDesconto.upsert({
|
||||
where: {
|
||||
codVendedor_idEmpresa_codGrupo: {
|
||||
codVendedor: body.codVendedor,
|
||||
idEmpresa,
|
||||
codGrupo: body.codGrupo ?? 0,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
codVendedor: body.codVendedor,
|
||||
idEmpresa,
|
||||
codGrupo: body.codGrupo ?? 0,
|
||||
limitePerc: body.limitePerc,
|
||||
},
|
||||
update: { limitePerc: body.limitePerc },
|
||||
});
|
||||
}
|
||||
|
||||
async listPromocoes(): Promise<PromocoesResponse> {
|
||||
this.assertManager();
|
||||
const prisma = this.cls.get('prisma');
|
||||
@@ -220,6 +163,23 @@ export class PoliticasService {
|
||||
};
|
||||
}
|
||||
|
||||
// Descrições das pautas informadas — UI sempre mostra o nome, nunca só o id.
|
||||
private async loadPautaNames(idPautas: number[]): Promise<Map<number, string | null>> {
|
||||
if (idPautas.length === 0) return new Map();
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
const idMatriz = idEmpresa > 9000 ? idEmpresa - 9000 : idEmpresa;
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe<{ id_pauta: number; descricao: string | null }[]>(
|
||||
`SELECT id_pauta, TRIM(descricao) AS descricao
|
||||
FROM vw_pautas
|
||||
WHERE id_empresa = ${idMatriz}
|
||||
AND id_pauta IN (${idPautas.join(',')})`,
|
||||
);
|
||||
return new Map(rows.map((r) => [Number(r.id_pauta), r.descricao]));
|
||||
}
|
||||
|
||||
async listRegras(): Promise<RegrasDescontoResponse> {
|
||||
this.assertManager();
|
||||
const prisma = this.cls.get('prisma');
|
||||
@@ -252,6 +212,9 @@ export class PoliticasService {
|
||||
grupos.filter((g) => g.cod_subgrupo != null).map((g) => [Number(g.cod_subgrupo), g.subgrupo]),
|
||||
);
|
||||
|
||||
const idPautas = [...new Set(rows.map((r) => r.idPauta).filter((p): p is number => p != null))];
|
||||
const pautaNameMap = await this.loadPautaNames(idPautas);
|
||||
|
||||
return {
|
||||
regras: rows.map((r) => ({
|
||||
id: r.id,
|
||||
@@ -260,6 +223,8 @@ export class PoliticasService {
|
||||
grupo: r.codGrupo != null ? (grupoNameMap.get(r.codGrupo) ?? null) : null,
|
||||
codSubgrupo: r.codSubgrupo,
|
||||
subgrupo: r.codSubgrupo != null ? (subgrupoNameMap.get(r.codSubgrupo) ?? null) : null,
|
||||
idPauta: r.idPauta,
|
||||
pauta: r.idPauta != null ? (pautaNameMap.get(r.idPauta) ?? null) : null,
|
||||
codVendedor: r.codVendedor,
|
||||
nomeVendedor: r.codVendedor != null ? (repNameMap.get(r.codVendedor) ?? null) : null,
|
||||
descPct: Number(r.descPct),
|
||||
@@ -272,8 +237,9 @@ export class PoliticasService {
|
||||
};
|
||||
}
|
||||
|
||||
// Cria uma regra POR ALVO: cada grupo selecionado vira regra por grupo e
|
||||
// cada subgrupo vira regra por subgrupo. Sem alvos, regra vale para todos.
|
||||
// Cria uma regra POR ALVO: cada grupo vira regra por grupo, cada subgrupo
|
||||
// vira regra por subgrupo e cada pauta vira regra por pauta. O representante
|
||||
// escolhido é copiado em todas. Sem alvos, regra vale para todos.
|
||||
async createRegra(body: CreateRegraDescontoBody): Promise<{ ids: number[] }> {
|
||||
this.assertManager();
|
||||
const prisma = this.cls.get('prisma');
|
||||
@@ -281,11 +247,17 @@ export class PoliticasService {
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
const userId = this.cls.get('userId') ?? 'system';
|
||||
|
||||
const alvos: { codGrupo: number | null; codSubgrupo: number | null }[] = [
|
||||
...(body.codGrupos ?? []).map((g) => ({ codGrupo: g, codSubgrupo: null })),
|
||||
...(body.codSubgrupos ?? []).map((s) => ({ codGrupo: null, codSubgrupo: s })),
|
||||
];
|
||||
if (alvos.length === 0) alvos.push({ codGrupo: null, codSubgrupo: null });
|
||||
const alvos: { codGrupo: number | null; codSubgrupo: number | null; idPauta: number | null }[] =
|
||||
[
|
||||
...(body.codGrupos ?? []).map((g) => ({ codGrupo: g, codSubgrupo: null, idPauta: null })),
|
||||
...(body.codSubgrupos ?? []).map((s) => ({
|
||||
codGrupo: null,
|
||||
codSubgrupo: s,
|
||||
idPauta: null,
|
||||
})),
|
||||
...(body.idPautas ?? []).map((p) => ({ codGrupo: null, codSubgrupo: null, idPauta: p })),
|
||||
];
|
||||
if (alvos.length === 0) alvos.push({ codGrupo: null, codSubgrupo: null, idPauta: null });
|
||||
|
||||
const ids = await prisma.$transaction(async (tx) => {
|
||||
const created: number[] = [];
|
||||
@@ -296,6 +268,7 @@ export class PoliticasService {
|
||||
descricao: body.descricao,
|
||||
codGrupo: alvo.codGrupo,
|
||||
codSubgrupo: alvo.codSubgrupo,
|
||||
idPauta: alvo.idPauta,
|
||||
codVendedor: body.codVendedor ?? null,
|
||||
descPct: body.descPct,
|
||||
dataInicio: new Date(body.dataInicio),
|
||||
@@ -325,6 +298,7 @@ export class PoliticasService {
|
||||
...(body.descricao !== undefined && { descricao: body.descricao }),
|
||||
...(body.codGrupo !== undefined && { codGrupo: body.codGrupo }),
|
||||
...(body.codSubgrupo !== undefined && { codSubgrupo: body.codSubgrupo }),
|
||||
...(body.idPauta !== undefined && { idPauta: body.idPauta }),
|
||||
...(body.codVendedor !== undefined && { codVendedor: body.codVendedor }),
|
||||
...(body.descPct !== undefined && { descPct: body.descPct }),
|
||||
...(body.dataInicio !== undefined && { dataInicio: new Date(body.dataInicio) }),
|
||||
@@ -453,6 +427,9 @@ export class PoliticasService {
|
||||
grupos.filter((g) => g.cod_subgrupo != null).map((g) => [Number(g.cod_subgrupo), g.subgrupo]),
|
||||
);
|
||||
|
||||
const idPautas = [...new Set(rows.map((r) => r.idPauta).filter((p): p is number => p != null))];
|
||||
const pautaNameMap = await this.loadPautaNames(idPautas);
|
||||
|
||||
return {
|
||||
regras: rows.map((r) => ({
|
||||
id: r.id,
|
||||
@@ -461,6 +438,8 @@ export class PoliticasService {
|
||||
grupo: r.codGrupo != null ? (grupoNameMap.get(r.codGrupo) ?? null) : null,
|
||||
codSubgrupo: r.codSubgrupo,
|
||||
subgrupo: r.codSubgrupo != null ? (subgrupoNameMap.get(r.codSubgrupo) ?? null) : null,
|
||||
idPauta: r.idPauta,
|
||||
pauta: r.idPauta != null ? (pautaNameMap.get(r.idPauta) ?? null) : null,
|
||||
descPct: Number(r.descPct),
|
||||
dataFim: r.dataFim.toISOString().slice(0, 10),
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user