- DevLogin com 3 usuarios reais: Pavei (rep 29), Sidnei (supervisor 191) e Lucas (gerente 156); guard deixa de forcar DEV_REP_CODE em dev e usa sub/id_empresa do JWT (fallback para os valores do .env); idEmpresa do token dev opcional, default DEV_EMPRESA_ID - getTeamCodes (vw_representantes.cod_supervisor): supervisor ve so a equipe em pedidos SAR/ERP, detalhe, aprovar/recusar, clientes (lista e carteira), dashboard supervisor, chamados SAC e relatorios carteira/curva ABC; gerente/admin seguem vendo a empresa toda - Painel gerencial: card "Positivacao Diaria de Clientes" (clientes distintos por dia via vw_pedidos_erp) com campo de meta/dia persistido em localStorage, linha de meta tracejada e contador de dias na meta Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
196 lines
8.0 KiB
TypeScript
196 lines
8.0 KiB
TypeScript
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
|
import { ClsService } from 'nestjs-cls';
|
|
import type { CreateChamadoDto, AddMensagemDto, ResolverChamadoDto } from '@sar/api-interface';
|
|
|
|
type PrismaCtx = {
|
|
chamado: {
|
|
findMany: (args: object) => Promise<unknown[]>;
|
|
findFirst: (args: object) => Promise<unknown | null>;
|
|
findUnique: (args: object) => Promise<unknown | null>;
|
|
create: (args: object) => Promise<unknown>;
|
|
update: (args: object) => Promise<unknown>;
|
|
};
|
|
chamadoMensagem: { create: (args: object) => Promise<unknown> };
|
|
$queryRawUnsafe: <T>(sql: string, ...args: unknown[]) => Promise<T[]>;
|
|
};
|
|
|
|
type ClienteRow = { id_cliente: number; nome: string | null; razao: string | null };
|
|
|
|
@Injectable()
|
|
export class SacService {
|
|
constructor(private readonly cls: ClsService) {}
|
|
|
|
private ctx(): { prisma: PrismaCtx; idEmpresa: number; codVendedor: number } {
|
|
const prisma = this.cls.get('prisma') as PrismaCtx;
|
|
if (!prisma) throw new Error('prisma não disponível no CLS');
|
|
const idEmpresa = this.cls.get('idEmpresa') as number;
|
|
const userId = this.cls.get('userId') as string | undefined;
|
|
const codVendedor = userId ? parseInt(userId, 10) : 0;
|
|
return { prisma, idEmpresa, codVendedor };
|
|
}
|
|
|
|
private async enrichNomes(
|
|
prisma: PrismaCtx,
|
|
idEmpresa: number,
|
|
rows: { idCliente: number }[],
|
|
): Promise<Map<number, string>> {
|
|
const ids = [...new Set(rows.map((r) => r.idCliente))];
|
|
if (!ids.length) return new Map();
|
|
const clientes = await prisma.$queryRawUnsafe<ClienteRow>(
|
|
`SELECT id_cliente, TRIM(nome) AS nome, TRIM(razao) AS razao FROM vw_clientes WHERE id_cliente IN (${ids.join(',')}) AND id_empresa = ${idEmpresa}`,
|
|
);
|
|
const map = new Map<number, string>();
|
|
for (const c of clientes) {
|
|
map.set(c.id_cliente, c.razao ?? c.nome ?? `Cód. ${c.id_cliente}`);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
async listarRep() {
|
|
const { prisma, idEmpresa, codVendedor } = this.ctx();
|
|
const all = (await prisma.chamado.findMany({
|
|
where: { idEmpresa, codVendedor },
|
|
orderBy: { updatedAt: 'desc' },
|
|
})) as { idCliente: number; status: string; [k: string]: unknown }[];
|
|
const nomes = await this.enrichNomes(prisma, idEmpresa, all);
|
|
const enrich = (row: (typeof all)[0]) => ({
|
|
...row,
|
|
nomeCliente:
|
|
(row as { nomeCliente?: string | null }).nomeCliente ?? nomes.get(row.idCliente) ?? null,
|
|
});
|
|
return {
|
|
abertos: all.filter((c) => !['resolvido', 'cancelado'].includes(c.status)).map(enrich),
|
|
fechados: all.filter((c) => ['resolvido', 'cancelado'].includes(c.status)).map(enrich),
|
|
};
|
|
}
|
|
|
|
// Códigos da equipe do supervisor (cod_supervisor em vw_representantes),
|
|
// incluindo o próprio. Retorna null para gerente/admin (sem filtro).
|
|
private async teamCodes(): Promise<number[] | null> {
|
|
const role = (this.cls.get('role') as string | undefined) ?? 'rep';
|
|
if (role !== 'supervisor') return null;
|
|
const { prisma, codVendedor } = this.ctx();
|
|
const rows = await prisma.$queryRawUnsafe<{ codigo: number }>(
|
|
`SELECT DISTINCT codigo FROM vw_representantes WHERE cod_supervisor = ${codVendedor}`,
|
|
);
|
|
return [...new Set([codVendedor, ...rows.map((r) => Number(r.codigo))])];
|
|
}
|
|
|
|
async listarEmpresa() {
|
|
const { prisma, idEmpresa } = this.ctx();
|
|
// Supervisor vê só os chamados da sua equipe; gerente/admin veem a empresa toda
|
|
const team = await this.teamCodes();
|
|
const all = (await prisma.chamado.findMany({
|
|
where: { idEmpresa, ...(team ? { codVendedor: { in: team } } : {}) },
|
|
orderBy: { updatedAt: 'desc' },
|
|
})) as { idCliente: number; status: string; [k: string]: unknown }[];
|
|
const nomes = await this.enrichNomes(prisma, idEmpresa, all);
|
|
const enrich = (row: (typeof all)[0]) => ({
|
|
...row,
|
|
nomeCliente:
|
|
(row as { nomeCliente?: string | null }).nomeCliente ?? nomes.get(row.idCliente) ?? null,
|
|
});
|
|
return {
|
|
abertos: all.filter((c) => !['resolvido', 'cancelado'].includes(c.status)).map(enrich),
|
|
fechados: all.filter((c) => ['resolvido', 'cancelado'].includes(c.status)).map(enrich),
|
|
};
|
|
}
|
|
|
|
async detalhe(id: number, role: 'rep' | 'empresa') {
|
|
const { prisma, idEmpresa, codVendedor } = this.ctx();
|
|
const chamado = (await prisma.chamado.findFirst({
|
|
where: { id, idEmpresa },
|
|
include: { mensagens: { orderBy: { createdAt: 'asc' } } },
|
|
})) as { idCliente: number; codVendedor: number; [k: string]: unknown } | null;
|
|
if (!chamado) throw new NotFoundException('Chamado não encontrado');
|
|
if (role === 'rep' && chamado.codVendedor !== codVendedor) throw new ForbiddenException();
|
|
if (role === 'empresa') {
|
|
const team = await this.teamCodes();
|
|
if (team && !team.includes(chamado.codVendedor)) throw new ForbiddenException();
|
|
}
|
|
const nomes = await this.enrichNomes(prisma, idEmpresa, [chamado]);
|
|
return { ...chamado, nomeCliente: nomes.get(chamado.idCliente) ?? null };
|
|
}
|
|
|
|
async criar(dto: CreateChamadoDto) {
|
|
const { prisma, idEmpresa, codVendedor } = this.ctx();
|
|
const chamado = (await prisma.chamado.create({
|
|
data: {
|
|
idEmpresa,
|
|
codVendedor,
|
|
idPedido: dto.idPedido ?? null,
|
|
numPedSar: dto.numPedSar ?? null,
|
|
numPedErp: dto.numPedErp ?? null,
|
|
nomeCliente: dto.nomeCliente ?? null,
|
|
idCliente: dto.idCliente,
|
|
tipo: dto.tipo,
|
|
assunto: dto.assunto,
|
|
descricao: dto.descricao,
|
|
itensAfetados: dto.itensAfetados ?? [],
|
|
},
|
|
include: { mensagens: true },
|
|
})) as { idCliente: number; nomeCliente: string | null; [k: string]: unknown };
|
|
const nomes = await this.enrichNomes(prisma, idEmpresa, [chamado]);
|
|
return { ...chamado, nomeCliente: chamado.nomeCliente ?? nomes.get(chamado.idCliente) ?? null };
|
|
}
|
|
|
|
async addMensagemRep(id: number, dto: AddMensagemDto) {
|
|
const { prisma, idEmpresa, codVendedor } = this.ctx();
|
|
const c = (await prisma.chamado.findFirst({ where: { id, idEmpresa } })) as {
|
|
codVendedor: number;
|
|
} | null;
|
|
if (!c) throw new NotFoundException();
|
|
if (c.codVendedor !== codVendedor) throw new ForbiddenException();
|
|
await prisma.chamadoMensagem.create({
|
|
data: { idChamado: id, autor: 'rep', texto: dto.texto },
|
|
});
|
|
await prisma.chamado.update({ where: { id }, data: { status: 'em_analise' } });
|
|
return this.detalhe(id, 'rep');
|
|
}
|
|
|
|
async addMensagemEmpresa(id: number, dto: AddMensagemDto) {
|
|
const { prisma, idEmpresa } = this.ctx();
|
|
const c = await prisma.chamado.findFirst({ where: { id, idEmpresa } });
|
|
if (!c) throw new NotFoundException();
|
|
await prisma.chamadoMensagem.create({
|
|
data: { idChamado: id, autor: 'empresa', texto: dto.texto },
|
|
});
|
|
await prisma.chamado.update({ where: { id }, data: { status: 'aguardando_rep' } });
|
|
return this.detalhe(id, 'empresa');
|
|
}
|
|
|
|
async resolver(id: number, dto: ResolverChamadoDto) {
|
|
const { prisma, idEmpresa } = this.ctx();
|
|
const c = await prisma.chamado.findFirst({ where: { id, idEmpresa } });
|
|
if (!c) throw new NotFoundException();
|
|
if (dto.mensagemFinal) {
|
|
await prisma.chamadoMensagem.create({
|
|
data: { idChamado: id, autor: 'empresa', texto: dto.mensagemFinal },
|
|
});
|
|
}
|
|
await prisma.chamado.update({
|
|
where: { id },
|
|
data: {
|
|
status: 'resolvido',
|
|
resolucao: dto.resolucao,
|
|
notaResolucao: dto.notaResolucao ?? null,
|
|
},
|
|
});
|
|
return this.detalhe(id, 'empresa');
|
|
}
|
|
|
|
async cancelarRep(id: number) {
|
|
const { prisma, idEmpresa, codVendedor } = this.ctx();
|
|
const c = (await prisma.chamado.findFirst({ where: { id, idEmpresa } })) as {
|
|
codVendedor: number;
|
|
status: string;
|
|
} | null;
|
|
if (!c) throw new NotFoundException();
|
|
if (c.codVendedor !== codVendedor) throw new ForbiddenException();
|
|
if (['resolvido', 'cancelado'].includes(c.status))
|
|
throw new ForbiddenException('Chamado já encerrado');
|
|
await prisma.chamado.update({ where: { id }, data: { status: 'cancelado' } });
|
|
return this.detalhe(id, 'rep');
|
|
}
|
|
}
|