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; findFirst: (args: object) => Promise; findUnique: (args: object) => Promise; create: (args: object) => Promise; update: (args: object) => Promise; }; chamadoMensagem: { create: (args: object) => Promise }; $queryRawUnsafe: (sql: string, ...args: unknown[]) => Promise; }; 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> { const ids = [...new Set(rows.map((r) => r.idCliente))]; if (!ids.length) return new Map(); const clientes = await prisma.$queryRawUnsafe( `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(); 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 { 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'); } }