feat(web+api): cockpit gerente — painel, equipe, políticas e positivação
## API - GET /dashboard/manager: KPIs agregados (faturamento, pedidos, ticket médio, promoções ativas), meta total do período, ranking top 10 com clientes atendidos e % meta, positivação por representante, venda necessária/dia - Parâmetros opcionais ?mes&ano para filtrar período - GET /equipe: lista de reps com pedidos, faturamento, ticket médio e % meta (deduplicação de vw_representantes) - GET/POST /politicas/descontos: alçada de desconto por rep (AlcadaDesconto) - GET/POST/PATCH/DELETE /politicas/promocoes: CRUD de promoções com validade - Metas lidas de sar.vw_metas (tipo=GR), não de vw_metas do ERP ## Schema - Novo model Promocao (sar.promocoes) criado via prisma db execute ## Frontend - Cockpit /ger: GerPainel, EquipePage, PoliticasPage - GerPainel: filtro mes/ano, cards KPI, cards de meta vs realizado (atingimento, falta, venda/dia), ranking com clientes atendidos, positivação de clientes por representante (paginada) - Sidebar e BottomNav role-aware: rep / supervisor / gerente (manager+admin) - Clientes acessível ao gerente (carteira completa de todos os reps) - Rotas: HomeRoute redireciona por role; /ger, /ger/equipe, /ger/politicas Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ import { CatalogModule } from './catalog/catalog.module';
|
||||
import { DashboardModule } from './dashboard/dashboard.module';
|
||||
import { NotificationsModule } from './notifications/notifications.module';
|
||||
import { ReportsModule } from './reports/reports.module';
|
||||
import { PoliticasModule } from './politicas/politicas.module';
|
||||
import { EquipeModule } from './equipe/equipe.module';
|
||||
import { ProblemDetailsFilter } from './filters/problem-details.filter';
|
||||
|
||||
@Module({
|
||||
@@ -31,6 +33,8 @@ import { ProblemDetailsFilter } from './filters/problem-details.filter';
|
||||
DashboardModule,
|
||||
NotificationsModule,
|
||||
ReportsModule,
|
||||
PoliticasModule,
|
||||
EquipeModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_PIPE, useClass: ZodValidationPipe },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type { RepDashboard, SupervisorDashboard } from '@sar/api-interface';
|
||||
import type { RepDashboard, SupervisorDashboard, ManagerDashboard } from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
@@ -20,4 +20,15 @@ export class DashboardController {
|
||||
supervisorDashboard(): Promise<SupervisorDashboard> {
|
||||
return this.dashboard.supervisorDashboard();
|
||||
}
|
||||
|
||||
@Get('manager')
|
||||
managerDashboard(
|
||||
@Query('mes') mes?: string,
|
||||
@Query('ano') ano?: string,
|
||||
): Promise<ManagerDashboard> {
|
||||
return this.dashboard.managerDashboard(
|
||||
mes ? parseInt(mes, 10) : undefined,
|
||||
ano ? parseInt(ano, 10) : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type { RepDashboard, SupervisorDashboard } from '@sar/api-interface';
|
||||
import type {
|
||||
RepDashboard,
|
||||
SupervisorDashboard,
|
||||
ManagerDashboard,
|
||||
RankingRep,
|
||||
PositivacaoRep,
|
||||
} from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
|
||||
// Situa ERP: 2=Liberado, 4=Faturado, 5=Cancelado
|
||||
@@ -300,6 +306,159 @@ export class DashboardService {
|
||||
};
|
||||
}
|
||||
|
||||
async managerDashboard(mes?: number, ano?: number): Promise<ManagerDashboard> {
|
||||
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();
|
||||
|
||||
function matrizEmpresa(id: number): number {
|
||||
return id > 9000 ? id - 9000 : id;
|
||||
}
|
||||
const idEmpresaMatriz = matrizEmpresa(idEmpresa);
|
||||
const year = ano ?? now.getFullYear();
|
||||
const month = mes ?? 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);
|
||||
|
||||
interface TotalRow {
|
||||
count: string;
|
||||
total: string;
|
||||
}
|
||||
interface RankingRow {
|
||||
cod_vendedor: number;
|
||||
nome: string | null;
|
||||
pedidos: string;
|
||||
clientes_atendidos: string;
|
||||
faturamento: string;
|
||||
meta_valor: string | null;
|
||||
}
|
||||
interface PromoRow {
|
||||
count: string;
|
||||
}
|
||||
interface MetaTotalRow {
|
||||
meta_total: string;
|
||||
}
|
||||
interface PositivacaoRow {
|
||||
cod_vendedor: number;
|
||||
nome_vendedor: string | null;
|
||||
total_clientes: string;
|
||||
clientes_positivados: string;
|
||||
}
|
||||
|
||||
const [statsRows, rankingRows, promoRows, metaTotalRows, positivacaoRows] = await Promise.all([
|
||||
prisma.$queryRawUnsafe<TotalRow[]>(`
|
||||
SELECT COUNT(*)::text AS count, COALESCE(SUM(total), 0)::text AS total
|
||||
FROM vw_pedidos_erp
|
||||
WHERE id_empresa = ${idEmpresa}
|
||||
AND situa NOT IN (1, 5)
|
||||
AND dt_pedido >= '${monthStartStr}'
|
||||
AND dt_pedido <= '${monthEndStr}'
|
||||
`),
|
||||
prisma.$queryRawUnsafe<RankingRow[]>(`
|
||||
SELECT p.cod_vendedor,
|
||||
(SELECT r.nome FROM vw_representantes r
|
||||
WHERE r.codigo = p.cod_vendedor LIMIT 1) AS nome,
|
||||
COUNT(*)::text AS pedidos,
|
||||
COUNT(DISTINCT p.id_cliente)::text AS clientes_atendidos,
|
||||
COALESCE(SUM(p.total), 0)::text AS faturamento,
|
||||
(SELECT SUM(m.valor)::text FROM sar.vw_metas m
|
||||
WHERE m.id_empresa = ${idEmpresaMatriz}
|
||||
AND m.cod_vendedor = p.cod_vendedor
|
||||
AND m.ano = ${year}
|
||||
AND m.mes = ${month}
|
||||
AND TRIM(m.tipo) = 'GR') AS meta_valor
|
||||
FROM vw_pedidos_erp p
|
||||
WHERE p.id_empresa = ${idEmpresa}
|
||||
AND p.situa NOT IN (1, 5)
|
||||
AND p.dt_pedido >= '${monthStartStr}'
|
||||
AND p.dt_pedido <= '${monthEndStr}'
|
||||
GROUP BY p.cod_vendedor
|
||||
ORDER BY SUM(p.total) DESC
|
||||
LIMIT 10
|
||||
`),
|
||||
prisma.$queryRawUnsafe<PromoRow[]>(`
|
||||
SELECT COUNT(*)::text AS count
|
||||
FROM sar.promocoes
|
||||
WHERE id_empresa = ${idEmpresa}
|
||||
AND ativa = true
|
||||
AND data_fim >= CURRENT_DATE
|
||||
`),
|
||||
prisma.$queryRawUnsafe<MetaTotalRow[]>(`
|
||||
SELECT COALESCE(SUM(valor), 0)::text AS meta_total
|
||||
FROM sar.vw_metas
|
||||
WHERE id_empresa = ${idEmpresaMatriz}
|
||||
AND ano = ${year}
|
||||
AND mes = ${month}
|
||||
AND TRIM(tipo) = 'GR'
|
||||
`),
|
||||
prisma.$queryRawUnsafe<PositivacaoRow[]>(`
|
||||
SELECT
|
||||
c.cod_vendedor,
|
||||
(SELECT r.nome FROM vw_representantes r WHERE r.codigo = c.cod_vendedor LIMIT 1) AS nome_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 clientes_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
|
||||
HAVING COUNT(DISTINCT c.id_cliente) > 0
|
||||
ORDER BY COUNT(DISTINCT CASE WHEN ped.id_cliente IS NOT NULL THEN c.id_cliente END) DESC
|
||||
`),
|
||||
]);
|
||||
|
||||
const pedidosMes = Number(statsRows[0]?.count ?? 0);
|
||||
const faturamentoMes = Number(statsRows[0]?.total ?? 0);
|
||||
const ticketMedio = pedidosMes > 0 ? faturamentoMes / pedidosMes : 0;
|
||||
|
||||
const rankingReps: RankingRep[] = rankingRows.map((r) => {
|
||||
const fat = Number(r.faturamento);
|
||||
const meta = r.meta_valor ? Number(r.meta_valor) : 0;
|
||||
return {
|
||||
codVendedor: Number(r.cod_vendedor),
|
||||
nomeVendedor: r.nome ?? null,
|
||||
pedidos: Number(r.pedidos),
|
||||
clientesAtendidos: Number(r.clientes_atendidos),
|
||||
faturamento: fat,
|
||||
pctMeta: meta > 0 ? Math.round((fat / meta) * 100) : 0,
|
||||
};
|
||||
});
|
||||
|
||||
const totalReps = new Set(rankingRows.map((r) => Number(r.cod_vendedor))).size;
|
||||
const metaTotal = Number(metaTotalRows[0]?.meta_total ?? 0);
|
||||
|
||||
const positivacaoReps: PositivacaoRep[] = positivacaoRows.map((r) => {
|
||||
const total = Number(r.total_clientes);
|
||||
const positivados = Number(r.clientes_positivados);
|
||||
return {
|
||||
codVendedor: Number(r.cod_vendedor),
|
||||
nomeVendedor: r.nome_vendedor ?? null,
|
||||
totalClientes: total,
|
||||
clientesPositivados: positivados,
|
||||
pctPositivacao: total > 0 ? Math.round((positivados / total) * 100) : 0,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
faturamentoMes,
|
||||
pedidosMes,
|
||||
ticketMedio,
|
||||
totalReps,
|
||||
promocoesAtivas: Number(promoRows[0]?.count ?? 0),
|
||||
metaTotal,
|
||||
rankingReps,
|
||||
positivacaoReps,
|
||||
syncedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async supervisorDashboard(): Promise<SupervisorDashboard> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
13
apps/api/src/app/equipe/equipe.controller.ts
Normal file
13
apps/api/src/app/equipe/equipe.controller.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import type { EquipeResponse } from '@sar/api-interface';
|
||||
import { EquipeService } from './equipe.service';
|
||||
|
||||
@Controller({ path: 'equipe' })
|
||||
export class EquipeController {
|
||||
constructor(private readonly equipe: EquipeService) {}
|
||||
|
||||
@Get()
|
||||
listEquipe(): Promise<EquipeResponse> {
|
||||
return this.equipe.listEquipe();
|
||||
}
|
||||
}
|
||||
9
apps/api/src/app/equipe/equipe.module.ts
Normal file
9
apps/api/src/app/equipe/equipe.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EquipeController } from './equipe.controller';
|
||||
import { EquipeService } from './equipe.service';
|
||||
|
||||
@Module({
|
||||
controllers: [EquipeController],
|
||||
providers: [EquipeService],
|
||||
})
|
||||
export class EquipeModule {}
|
||||
113
apps/api/src/app/equipe/equipe.service.ts
Normal file
113
apps/api/src/app/equipe/equipe.service.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type { EquipeResponse } from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
|
||||
function matrizEmpresa(idEmpresa: number): number {
|
||||
return idEmpresa > 9000 ? idEmpresa - 9000 : idEmpresa;
|
||||
}
|
||||
|
||||
interface RepRow {
|
||||
cod_vendedor: number;
|
||||
nome: string | null;
|
||||
}
|
||||
|
||||
interface PedidosRepRow {
|
||||
cod_vendedor: number;
|
||||
pedidos: string;
|
||||
faturamento: string;
|
||||
}
|
||||
|
||||
interface MetaRepRow {
|
||||
cod_vendedor: number;
|
||||
meta_valor: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EquipeService {
|
||||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||||
|
||||
async listEquipe(): Promise<EquipeResponse> {
|
||||
const role = this.cls.get('role') ?? 'rep';
|
||||
if (role !== 'manager' && role !== 'admin') {
|
||||
throw new ForbiddenException('Apenas gerentes podem consultar a equipe');
|
||||
}
|
||||
|
||||
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 [repRows, pedidosRows, metaRows] = await Promise.all([
|
||||
prisma.$queryRawUnsafe<RepRow[]>(
|
||||
`SELECT codigo AS cod_vendedor, nome FROM vw_representantes ORDER BY nome`,
|
||||
),
|
||||
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<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
|
||||
`),
|
||||
]);
|
||||
|
||||
const pedidosMap = new Map(
|
||||
pedidosRows.map((r) => [
|
||||
Number(r.cod_vendedor),
|
||||
{ pedidos: Number(r.pedidos), faturamento: Number(r.faturamento) },
|
||||
]),
|
||||
);
|
||||
const metaMap = new Map(metaRows.map((r) => [Number(r.cod_vendedor), Number(r.meta_valor)]));
|
||||
|
||||
// vw_representantes pode retornar duplicatas por cod_vendedor — deduplicar
|
||||
const seenCods = new Set<number>();
|
||||
const uniqueReps = repRows.filter((r) => {
|
||||
const cod = Number(r.cod_vendedor);
|
||||
if (seenCods.has(cod)) return false;
|
||||
seenCods.add(cod);
|
||||
return true;
|
||||
});
|
||||
|
||||
const reps = uniqueReps.map((r) => {
|
||||
const cod = Number(r.cod_vendedor);
|
||||
const stats = pedidosMap.get(cod);
|
||||
const meta = metaMap.get(cod) ?? 0;
|
||||
const faturamento = stats?.faturamento ?? 0;
|
||||
const pedidos = stats?.pedidos ?? 0;
|
||||
return {
|
||||
codVendedor: cod,
|
||||
nomeVendedor: r.nome ?? null,
|
||||
pedidosMes: pedidos,
|
||||
faturamentoMes: faturamento,
|
||||
ticketMedio: pedidos > 0 ? faturamento / pedidos : 0,
|
||||
pctMeta: meta > 0 ? Math.round((faturamento / meta) * 100) : 0,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
reps,
|
||||
totalReps: reps.length,
|
||||
syncedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
67
apps/api/src/app/politicas/politicas.controller.ts
Normal file
67
apps/api/src/app/politicas/politicas.controller.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import {
|
||||
UpsertDescontoBodySchema,
|
||||
CreatePromocaoBodySchema,
|
||||
UpdatePromocaoBodySchema,
|
||||
type UpsertDescontoBody,
|
||||
type CreatePromocaoBody,
|
||||
type UpdatePromocaoBody,
|
||||
type AlcadaDescontosResponse,
|
||||
type PromocoesResponse,
|
||||
} from '@sar/api-interface';
|
||||
import { PoliticasService } from './politicas.service';
|
||||
|
||||
class UpsertDescontoDto extends createZodDto(UpsertDescontoBodySchema) {}
|
||||
class CreatePromocaoDto extends createZodDto(CreatePromocaoBodySchema) {}
|
||||
class UpdatePromocaoDto extends createZodDto(UpdatePromocaoBodySchema) {}
|
||||
|
||||
@Controller({ path: 'politicas' })
|
||||
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();
|
||||
}
|
||||
|
||||
@Post('promocoes')
|
||||
createPromocao(@Body() body: CreatePromocaoDto): Promise<{ id: number }> {
|
||||
return this.politicas.createPromocao(body as unknown as CreatePromocaoBody);
|
||||
}
|
||||
|
||||
@Patch('promocoes/:id')
|
||||
updatePromocao(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() body: UpdatePromocaoDto,
|
||||
): Promise<void> {
|
||||
return this.politicas.updatePromocao(id, body as unknown as UpdatePromocaoBody);
|
||||
}
|
||||
|
||||
@Delete('promocoes/:id')
|
||||
@HttpCode(204)
|
||||
deletePromocao(@Param('id', ParseIntPipe) id: number): Promise<void> {
|
||||
return this.politicas.deletePromocao(id);
|
||||
}
|
||||
}
|
||||
9
apps/api/src/app/politicas/politicas.module.ts
Normal file
9
apps/api/src/app/politicas/politicas.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PoliticasController } from './politicas.controller';
|
||||
import { PoliticasService } from './politicas.service';
|
||||
|
||||
@Module({
|
||||
controllers: [PoliticasController],
|
||||
providers: [PoliticasService],
|
||||
})
|
||||
export class PoliticasModule {}
|
||||
161
apps/api/src/app/politicas/politicas.service.ts
Normal file
161
apps/api/src/app/politicas/politicas.service.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type {
|
||||
AlcadaDescontosResponse,
|
||||
UpsertDescontoBody,
|
||||
PromocoesResponse,
|
||||
CreatePromocaoBody,
|
||||
UpdatePromocaoBody,
|
||||
} from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
|
||||
@Injectable()
|
||||
export class PoliticasService {
|
||||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||||
|
||||
private assertManager(): void {
|
||||
const role = this.cls.get('role') ?? 'rep';
|
||||
if (role !== 'manager' && role !== 'admin') {
|
||||
throw new ForbiddenException('Apenas gerentes podem gerenciar políticas comerciais');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = this.cls.get('idEmpresa');
|
||||
|
||||
const rows = await prisma.promocao.findMany({
|
||||
where: { idEmpresa },
|
||||
orderBy: [{ dataFim: 'desc' }, { id: 'desc' }],
|
||||
});
|
||||
|
||||
return {
|
||||
promocoes: rows.map((p) => ({
|
||||
id: p.id,
|
||||
descricao: p.descricao,
|
||||
codProduto: p.codProduto,
|
||||
grpProd: p.grpProd,
|
||||
descPct: Number(p.descPct),
|
||||
dataInicio: p.dataInicio.toISOString().slice(0, 10),
|
||||
dataFim: p.dataFim.toISOString().slice(0, 10),
|
||||
ativa: p.ativa,
|
||||
createdAt: p.createdAt.toISOString(),
|
||||
createdBy: p.createdBy,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async createPromocao(body: CreatePromocaoBody): Promise<{ id: number }> {
|
||||
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 userId = this.cls.get('userId') ?? 'system';
|
||||
|
||||
const p = await prisma.promocao.create({
|
||||
data: {
|
||||
idEmpresa,
|
||||
descricao: body.descricao,
|
||||
codProduto: body.codProduto ?? null,
|
||||
grpProd: body.grpProd ?? null,
|
||||
descPct: body.descPct,
|
||||
dataInicio: new Date(body.dataInicio),
|
||||
dataFim: new Date(body.dataFim),
|
||||
createdBy: userId,
|
||||
},
|
||||
});
|
||||
return { id: p.id };
|
||||
}
|
||||
|
||||
async updatePromocao(id: number, body: UpdatePromocaoBody): 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');
|
||||
|
||||
const exists = await prisma.promocao.findFirst({ where: { id, idEmpresa } });
|
||||
if (!exists) throw new NotFoundException('Promoção não encontrada');
|
||||
|
||||
await prisma.promocao.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.descricao !== undefined && { descricao: body.descricao }),
|
||||
...(body.codProduto !== undefined && { codProduto: body.codProduto }),
|
||||
...(body.grpProd !== undefined && { grpProd: body.grpProd }),
|
||||
...(body.descPct !== undefined && { descPct: body.descPct }),
|
||||
...(body.dataInicio !== undefined && { dataInicio: new Date(body.dataInicio) }),
|
||||
...(body.dataFim !== undefined && { dataFim: new Date(body.dataFim) }),
|
||||
...(body.ativa !== undefined && { ativa: body.ativa }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deletePromocao(id: number): 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');
|
||||
|
||||
const exists = await prisma.promocao.findFirst({ where: { id, idEmpresa } });
|
||||
if (!exists) throw new NotFoundException('Promoção não encontrada');
|
||||
|
||||
await prisma.promocao.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user