## 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>
584 lines
22 KiB
TypeScript
584 lines
22 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
||
import { ClsService } from 'nestjs-cls';
|
||
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
|
||
// Situa SAR (pedidos novos): 1=Pendente, 2=Aprovado, 3=Cancelado, 4=Faturado
|
||
const SITUA_PENDENTE = 1;
|
||
|
||
// vw_metas.tipo (gestao.metavenda): GL = meta global, GR = meta por grupo.
|
||
const TIPO_META_GLOBAL = 'GL';
|
||
const TIPO_META_GRUPO = 'GR';
|
||
|
||
// Metas/produtos vivem na MATRIZ; pedidos usam idEmpresa cru (ex.: 9001 → matriz 1).
|
||
function matrizEmpresa(idEmpresa: number): number {
|
||
return idEmpresa > 9000 ? idEmpresa - 9000 : idEmpresa;
|
||
}
|
||
|
||
interface MetaErpRow {
|
||
tipo: string;
|
||
cod_grupo: number | null;
|
||
desc_grupo: string | null;
|
||
valor: string;
|
||
qtdade: string;
|
||
peso: string;
|
||
vl_fator: string;
|
||
}
|
||
|
||
interface RealizadoGrupoRow {
|
||
cod_grupo: number | null;
|
||
grupo: string | null;
|
||
pedidos: string;
|
||
valor: string;
|
||
qtd: string;
|
||
peso: string;
|
||
}
|
||
|
||
interface NaoPositivadoRow {
|
||
id_cliente: number;
|
||
nome: string;
|
||
razao: string | null;
|
||
dt_ultimo_pedido: string | null; // YYYY-MM-DD ou null
|
||
dias_sem_pedido: number | null;
|
||
comprou_antes: boolean;
|
||
whatsapp: string | null;
|
||
}
|
||
|
||
interface InativosPorRepRow {
|
||
cod_vendedor: number;
|
||
nome_vendedor: string | null;
|
||
inativos_count: string;
|
||
}
|
||
|
||
@Injectable()
|
||
export class DashboardService {
|
||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||
|
||
async repDashboard(userId: string): Promise<RepDashboard> {
|
||
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 codVendedor = parseInt(userId, 10);
|
||
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = now.getMonth() + 1;
|
||
|
||
const monthStart = new Date(year, month - 1, 1);
|
||
const monthEnd = new Date(year, month, 0, 23, 59, 59, 999);
|
||
|
||
// 1. Metas do mês — vw_metas vive na matriz (normaliza 9001→1).
|
||
// GL = meta global; GR = meta por grupo. A dimensão segue o que o ERP tiver.
|
||
const idEmpresaMatriz = matrizEmpresa(idEmpresa);
|
||
const metaErpRows = await prisma.$queryRawUnsafe<MetaErpRow[]>(`
|
||
SELECT TRIM(tipo) AS tipo, cod_grupo, TRIM(desc_grupo) AS desc_grupo,
|
||
valor::text AS valor, qtdade::text AS qtdade,
|
||
peso::text AS peso, vl_fator::text AS vl_fator
|
||
FROM vw_metas
|
||
WHERE id_empresa = ${idEmpresaMatriz}
|
||
AND cod_vendedor = ${codVendedor}
|
||
AND ano = ${year}
|
||
AND mes = ${month}
|
||
AND TRIM(tipo) IN ('${TIPO_META_GLOBAL}', '${TIPO_META_GRUPO}')
|
||
`);
|
||
const glRows = metaErpRows.filter((m) => m.tipo === TIPO_META_GLOBAL);
|
||
const grRows = metaErpRows.filter((m) => m.tipo === TIPO_META_GRUPO);
|
||
// Total global: usa GL se houver; senão soma as metas por grupo (GR).
|
||
const targetAmount = glRows.length
|
||
? glRows.reduce((a, m) => a + Number(m.valor), 0)
|
||
: grRows.reduce((a, m) => a + Number(m.valor), 0);
|
||
const metaDimensao = grRows.length > 0 ? ('grupo' as const) : ('global' as const);
|
||
|
||
// 2. Atingido do mês — realizado = tudo menos Cancelado(5) e Pendente/não-transmitido(1).
|
||
// Inclui Liberado(2), Enviado(3,6,92,95,200) e Faturado(4). Base: data do pedido.
|
||
const monthStartStr = monthStart.toISOString().slice(0, 10);
|
||
const monthEndStr = monthEnd.toISOString().slice(0, 10);
|
||
|
||
interface TotalRow {
|
||
total: string;
|
||
}
|
||
interface CountRow {
|
||
count: string;
|
||
}
|
||
interface RecentRow {
|
||
id_pedido: number;
|
||
num_ped_sar: string;
|
||
numero: number;
|
||
id_cliente: number;
|
||
nome_cliente: string | null;
|
||
razao_cliente: string | null;
|
||
cod_vendedor: number;
|
||
nome_vendedor: string | null;
|
||
situa: number;
|
||
status_descr: string;
|
||
dt_pedido: Date;
|
||
total: string;
|
||
desconto_perc: string;
|
||
obs: string | null;
|
||
}
|
||
|
||
const [atingidoRows, pedidosMesRows, recentRows, realizadoGrupoRows] = await Promise.all([
|
||
prisma.$queryRawUnsafe<TotalRow[]>(`
|
||
SELECT COALESCE(SUM(total), 0)::text AS total
|
||
FROM vw_pedidos_erp
|
||
WHERE id_empresa = ${idEmpresa}
|
||
AND cod_vendedor = ${codVendedor}
|
||
AND situa NOT IN (1, 5)
|
||
AND dt_pedido >= '${monthStartStr}'
|
||
AND dt_pedido <= '${monthEndStr}'
|
||
`),
|
||
prisma.$queryRawUnsafe<CountRow[]>(`
|
||
SELECT COUNT(*)::text AS count
|
||
FROM vw_pedidos_erp
|
||
WHERE id_empresa = ${idEmpresa}
|
||
AND cod_vendedor = ${codVendedor}
|
||
AND situa != 5
|
||
AND dt_pedido >= '${monthStartStr}'
|
||
AND dt_pedido <= '${monthEndStr}'
|
||
`),
|
||
prisma.$queryRawUnsafe<RecentRow[]>(`
|
||
SELECT e.id_pedido, e.num_ped_sar, e.numero, e.id_cliente, e.cod_vendedor,
|
||
e.situa, e.status_descr, e.dt_pedido, e.total::text, e.desconto_perc::text, e.obs,
|
||
c.nome AS nome_cliente, c.razao AS razao_cliente,
|
||
(SELECT r.nome FROM vw_representantes r
|
||
WHERE r.codigo = e.cod_vendedor
|
||
LIMIT 1) AS nome_vendedor
|
||
FROM vw_pedidos_erp e
|
||
LEFT JOIN vw_clientes c ON c.id_cliente = e.id_cliente
|
||
WHERE e.id_empresa = ${idEmpresa}
|
||
AND e.cod_vendedor = ${codVendedor}
|
||
AND e.situa != 5
|
||
AND e.dt_pedido >= '${new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)}'
|
||
ORDER BY e.dt_pedido DESC
|
||
LIMIT 10
|
||
`),
|
||
// Realizado por grupo — itens faturados (situa 2/4) → produto (matriz) → cod_grupo.
|
||
// peso = qtd × peso_líquido do produto; pedidos = nº de pedidos distintos.
|
||
prisma.$queryRawUnsafe<RealizadoGrupoRow[]>(`
|
||
SELECT p.cod_grupo,
|
||
COALESCE(NULLIF(TRIM(p.grupo), ''), p.cod_grupo::text) AS grupo,
|
||
COUNT(DISTINCT pi.id_pedido)::text AS pedidos,
|
||
COALESCE(SUM(pi.total), 0)::text AS valor,
|
||
COALESCE(SUM(pi.qtd), 0)::text AS qtd,
|
||
COALESCE(SUM(pi.qtd * COALESCE(p.peso_liquido, 0)), 0)::text AS peso
|
||
FROM vw_peditens_erp pi
|
||
JOIN vw_pedidos_erp e ON e.id_pedido = pi.id_pedido
|
||
JOIN vw_produtos p ON p.id_erp = pi.id_produto AND p.id_empresa = ${idEmpresaMatriz}
|
||
WHERE e.id_empresa = ${idEmpresa}
|
||
AND e.cod_vendedor = ${codVendedor}
|
||
AND e.situa NOT IN (1, 5)
|
||
AND e.dt_pedido >= '${monthStartStr}'
|
||
AND e.dt_pedido <= '${monthEndStr}'
|
||
GROUP BY p.cod_grupo, grupo
|
||
`),
|
||
]);
|
||
|
||
const atingido = Number(atingidoRows[0]?.total ?? 0);
|
||
const pedidosMes = Number(pedidosMesRows[0]?.count ?? 0);
|
||
const pct = targetAmount > 0 ? Math.round((atingido / targetAmount) * 100) : 0;
|
||
const falta = Math.max(0, targetAmount - atingido);
|
||
|
||
// Clientes não positivados no mês: ativos do representante sem pedido desde monthStart.
|
||
// Junta o último pedido histórico (ERP+SAR) e o primeiro whatsapp ativo dos contatos.
|
||
const naoPositivadosRows = await prisma.$queryRawUnsafe<NaoPositivadoRow[]>(`
|
||
WITH ultimo_pedido AS (
|
||
SELECT id_cliente,
|
||
MAX(dt_pedido) AS dt_max
|
||
FROM (
|
||
SELECT id_cliente, dt_pedido FROM vw_pedidos_erp
|
||
WHERE situa NOT IN (5) AND id_empresa = ${idEmpresa}
|
||
UNION ALL
|
||
SELECT id_cliente, dt_pedido FROM sar.pedidos
|
||
WHERE situa != 3 AND id_empresa = ${idEmpresa}
|
||
) t
|
||
GROUP BY id_cliente
|
||
),
|
||
pedido_mes AS (
|
||
SELECT DISTINCT id_cliente FROM (
|
||
SELECT id_cliente FROM vw_pedidos_erp
|
||
WHERE situa NOT IN (5) AND id_empresa = ${idEmpresa}
|
||
AND dt_pedido >= '${monthStartStr}'
|
||
UNION
|
||
SELECT id_cliente FROM sar.pedidos
|
||
WHERE situa != 3 AND id_empresa = ${idEmpresa}
|
||
AND dt_pedido >= '${monthStartStr}'
|
||
) t
|
||
)
|
||
SELECT
|
||
c.id_cliente,
|
||
TRIM(c.nome) AS nome,
|
||
TRIM(c.razao) AS razao,
|
||
TO_CHAR(up.dt_max, 'YYYY-MM-DD') AS dt_ultimo_pedido,
|
||
(CURRENT_DATE - up.dt_max::date) AS dias_sem_pedido,
|
||
(up.dt_max IS NOT NULL) AS comprou_antes,
|
||
ct.whatsapp
|
||
FROM vw_clientes c
|
||
LEFT JOIN ultimo_pedido up ON up.id_cliente = c.id_cliente
|
||
LEFT JOIN pedido_mes pm ON pm.id_cliente = c.id_cliente
|
||
LEFT JOIN LATERAL (
|
||
SELECT TRIM(whatsapp) AS whatsapp
|
||
FROM sar.vw_contatos
|
||
WHERE id_corrent = c.id_cliente
|
||
AND whatsapp IS NOT NULL AND TRIM(whatsapp) != ''
|
||
AND ativo = 1
|
||
LIMIT 1
|
||
) ct ON true
|
||
WHERE c.cod_vendedor = ${codVendedor}
|
||
AND c.ativo = 1
|
||
AND pm.id_cliente IS NULL
|
||
ORDER BY up.dt_max ASC NULLS FIRST
|
||
`);
|
||
|
||
// Metas por grupo: junta meta (GR) com realizado (itens), por cod_grupo.
|
||
const realPorGrupo = new Map<number, RealizadoGrupoRow>();
|
||
for (const r of realizadoGrupoRows) {
|
||
if (r.cod_grupo != null) realPorGrupo.set(Number(r.cod_grupo), r);
|
||
}
|
||
const metasPorGrupo = grRows
|
||
.map((m) => {
|
||
const cod = m.cod_grupo != null ? Number(m.cod_grupo) : null;
|
||
const real = cod != null ? realPorGrupo.get(cod) : undefined;
|
||
const valorMeta = Number(m.valor);
|
||
const valorReal = real ? Number(real.valor) : 0;
|
||
const pesoMeta = Number(m.peso);
|
||
const pesoReal = real ? Number(real.peso) : 0;
|
||
return {
|
||
codigo: cod,
|
||
rotulo: m.desc_grupo || real?.grupo || `Grupo ${cod ?? '?'}`,
|
||
pedidos: real ? Number(real.pedidos) : 0,
|
||
valorMeta,
|
||
valorReal,
|
||
qtdMeta: Number(m.qtdade),
|
||
qtdReal: real ? Number(real.qtd) : 0,
|
||
pesoMeta,
|
||
pesoReal,
|
||
fatorMeta: Number(m.vl_fator),
|
||
fatorReal: pesoReal > 0 ? valorReal / pesoReal : 0,
|
||
pct: valorMeta > 0 ? Math.round((valorReal / valorMeta) * 100) : 0,
|
||
falta: Math.max(0, valorMeta - valorReal),
|
||
};
|
||
})
|
||
.sort((a, b) => b.valorMeta - a.valorMeta);
|
||
|
||
const naoPositivados = naoPositivadosRows.map((c) => ({
|
||
idCliente: Number(c.id_cliente),
|
||
nome: c.nome,
|
||
razao: c.razao ?? null,
|
||
diasSemPedido: c.dias_sem_pedido != null ? Number(c.dias_sem_pedido) : 999,
|
||
ultimoPedido: c.dt_ultimo_pedido ?? null,
|
||
comprouAntes: Boolean(c.comprou_antes),
|
||
whatsapp: c.whatsapp ?? null,
|
||
}));
|
||
|
||
return {
|
||
meta: { atingido, total: targetAmount, pct, falta },
|
||
metaDimensao,
|
||
metasPorGrupo,
|
||
pedidosMes,
|
||
pedidosRecentes: recentRows.map((o) => ({
|
||
id: `erp-${o.id_pedido}`,
|
||
numPedSar: (o.num_ped_sar ?? '').trim(),
|
||
numero: Number(o.numero),
|
||
idCliente: Number(o.id_cliente),
|
||
nomeCliente: o.nome_cliente ?? null,
|
||
razaoCliente: o.razao_cliente ?? null,
|
||
codVendedor: Number(o.cod_vendedor),
|
||
nomeVendedor: o.nome_vendedor ?? null,
|
||
situa: Number(o.situa),
|
||
statusDescr: o.status_descr,
|
||
dtPedido: new Date(o.dt_pedido).toISOString(),
|
||
total: o.total ?? '0',
|
||
descontoPerc: o.desconto_perc ?? '0',
|
||
obs: o.obs ?? null,
|
||
createdAt: new Date(o.dt_pedido).toISOString(),
|
||
fonte: 'erp' as const,
|
||
})),
|
||
naoPositivados,
|
||
totalNaoPositivados: naoPositivados.length,
|
||
syncedAt: now.toISOString(),
|
||
};
|
||
}
|
||
|
||
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');
|
||
const idEmpresa = this.cls.get('idEmpresa');
|
||
const now = new Date();
|
||
|
||
// Fila de aprovações — pedidos SAR pendentes (novos, ainda não integrados ao ERP)
|
||
const approvalQueue = await prisma.pedido.findMany({
|
||
where: { idEmpresa, situa: SITUA_PENDENTE },
|
||
orderBy: { dtPedido: 'asc' },
|
||
take: 50,
|
||
});
|
||
|
||
// Pedidos do dia — lê do ERP (situa != 5=Cancelado)
|
||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||
const todayStr = todayStart.toISOString().slice(0, 10);
|
||
const lastWeekStr = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||
.toISOString()
|
||
.slice(0, 10);
|
||
const lastWeekEndStr = new Date(todayStart.getTime() - 6 * 24 * 60 * 60 * 1000)
|
||
.toISOString()
|
||
.slice(0, 10);
|
||
|
||
interface DayRow {
|
||
count: string;
|
||
total: string;
|
||
}
|
||
const [todayRows, lastWeekRows] = await Promise.all([
|
||
prisma.$queryRawUnsafe<DayRow[]>(`
|
||
SELECT COUNT(*)::text AS count, COALESCE(SUM(total),0)::text AS total
|
||
FROM vw_pedidos_erp
|
||
WHERE id_empresa = ${idEmpresa} AND situa != 5 AND dt_pedido >= '${todayStr}'
|
||
`),
|
||
prisma.$queryRawUnsafe<DayRow[]>(`
|
||
SELECT COUNT(*)::text AS count, COALESCE(SUM(total),0)::text AS total
|
||
FROM vw_pedidos_erp
|
||
WHERE id_empresa = ${idEmpresa} AND situa != 5
|
||
AND dt_pedido >= '${lastWeekStr}' AND dt_pedido < '${lastWeekEndStr}'
|
||
`),
|
||
]);
|
||
|
||
// Top 3 reps com mais clientes inativos (>30 dias sem compra no ERP)
|
||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||
const inativosPorRep = await prisma.$queryRawUnsafe<InativosPorRepRow[]>(`
|
||
SELECT inativos.cod_vendedor,
|
||
(SELECT r.nome FROM vw_representantes r
|
||
WHERE r.codigo = inativos.cod_vendedor
|
||
LIMIT 1) AS nome_vendedor,
|
||
COUNT(*)::text AS inativos_count
|
||
FROM (
|
||
SELECT c.id_cliente, c.cod_vendedor
|
||
FROM vw_clientes c
|
||
LEFT JOIN vw_pedidos_erp p
|
||
ON p.id_cliente = c.id_cliente
|
||
AND p.id_empresa = ${idEmpresa}
|
||
AND p.situa != 5
|
||
WHERE 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().slice(0, 10)}'
|
||
) inativos
|
||
GROUP BY inativos.cod_vendedor
|
||
ORDER BY COUNT(*) DESC
|
||
LIMIT 3
|
||
`);
|
||
|
||
// Resolve nomes de cliente e representante da fila (pedidos SAR só têm os códigos)
|
||
const repCods = [...new Set(approvalQueue.map((p) => p.codVendedor))];
|
||
const cliIds = [...new Set(approvalQueue.map((p) => p.idCliente))];
|
||
const [repNameRows, cliNameRows] = await Promise.all([
|
||
repCods.length
|
||
? prisma.$queryRawUnsafe<{ codigo: number; nome: string | null }[]>(
|
||
`SELECT codigo, nome FROM vw_representantes WHERE codigo IN (${repCods.join(',')})`,
|
||
)
|
||
: Promise.resolve([]),
|
||
cliIds.length
|
||
? prisma.$queryRawUnsafe<
|
||
{ id_cliente: number; nome: string | null; razao: string | null }[]
|
||
>(
|
||
`SELECT id_cliente, nome, razao FROM vw_clientes WHERE id_cliente IN (${cliIds.join(',')})`,
|
||
)
|
||
: Promise.resolve([]),
|
||
]);
|
||
const repNameMap = new Map(repNameRows.map((r) => [Number(r.codigo), r.nome]));
|
||
const cliNameMap = new Map(
|
||
cliNameRows.map((c) => [Number(c.id_cliente), { nome: c.nome, razao: c.razao }]),
|
||
);
|
||
|
||
const mapPedido = (o: (typeof approvalQueue)[number]) => ({
|
||
id: o.id,
|
||
numPedSar: o.numPedSar,
|
||
idCliente: o.idCliente,
|
||
nomeCliente: cliNameMap.get(o.idCliente)?.nome ?? null,
|
||
razaoCliente: cliNameMap.get(o.idCliente)?.razao ?? null,
|
||
codVendedor: o.codVendedor,
|
||
nomeVendedor: repNameMap.get(o.codVendedor) ?? null,
|
||
situa: o.situa,
|
||
dtPedido: o.dtPedido.toISOString(),
|
||
total: String(o.total),
|
||
descontoPerc: String(o.descontoPerc),
|
||
obs: o.obs,
|
||
createdAt: o.createdAt.toISOString(),
|
||
fonte: 'sar' as const,
|
||
});
|
||
|
||
return {
|
||
approvalQueue: approvalQueue.map(mapPedido),
|
||
pedidosDia: {
|
||
count: Number(todayRows[0]?.count ?? 0),
|
||
total: Number(todayRows[0]?.total ?? 0),
|
||
countSemanaAnterior: Number(lastWeekRows[0]?.count ?? 0),
|
||
totalSemanaAnterior: Number(lastWeekRows[0]?.total ?? 0),
|
||
},
|
||
inativosPorRep: inativosPorRep.map((r) => ({
|
||
codVendedor: Number(r.cod_vendedor),
|
||
nomeVendedor: r.nome_vendedor ?? null,
|
||
inativosCount: parseInt(r.inativos_count, 10),
|
||
})),
|
||
syncedAt: now.toISOString(),
|
||
};
|
||
}
|
||
}
|