feat(api,web): mapa do brasil com faturamento por estado no painel gerencial

- KPIs financeiros agrupados (faturamento, meta, atingimento, falta) em metade da primeira dobra
- MapaBrasilCard: choropleth por UF (@svg-maps/brazil), tooltip, ranking lateral e modal Detalhar com representantes por estado
- API: faturamentoPorUf no /dashboard/manager (pedido -> cliente -> municipio -> UF)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 11:22:21 +00:00
parent dfdcca4674
commit 78612d59bd
7 changed files with 640 additions and 143 deletions

View File

@@ -422,6 +422,14 @@ export class DashboardService {
cod_vendedor: number;
novos: string;
}
interface UfRepRow {
uf: string;
cod_vendedor: number;
nome_vendedor: string | null;
pedidos: string;
clientes: string;
faturamento: string;
}
const [
statsRows,
@@ -433,6 +441,7 @@ export class DashboardService {
realizadoGrupoRows,
positivacaoDiariaRows,
novosClientesRows,
ufRepRows,
] = await Promise.all([
prisma.$queryRawUnsafe<TotalRow[]>(`
SELECT COUNT(*)::text AS count, COALESCE(SUM(total), 0)::text AS total
@@ -555,6 +564,31 @@ export class DashboardService {
AND f.primeira_compra <= '${monthEndStr}'
GROUP BY c.cod_vendedor
`),
// Faturamento por UF (município do cliente) e representante.
// vw_clientes pode repetir o cliente por empresa → LATERAL pega 1 município.
prisma.$queryRawUnsafe<UfRepRow[]>(`
SELECT COALESCE(NULLIF(TRIM(loc.uf), ''), 'ND') AS uf,
p.cod_vendedor,
(SELECT r.nome FROM vw_representantes r
WHERE r.codigo = p.cod_vendedor LIMIT 1) AS nome_vendedor,
COUNT(*)::text AS pedidos,
COUNT(DISTINCT p.id_cliente)::text AS clientes,
COALESCE(SUM(p.total), 0)::text AS faturamento
FROM vw_pedidos_erp p
LEFT JOIN LATERAL (
SELECT mu.uf::text AS uf
FROM vw_clientes c
JOIN sar.vw_municipios mu ON mu.id_municipio = c.id_municipio
WHERE c.id_cliente = p.id_cliente
LIMIT 1
) loc ON true
WHERE p.id_empresa = ${idEmpresa}
AND p.situa NOT IN (1, 5)
AND p.dt_pedido >= '${monthStartStr}'
AND p.dt_pedido <= '${monthEndStr}'
GROUP BY 1, p.cod_vendedor
ORDER BY 1, SUM(p.total) DESC
`),
]);
const pedidosMes = Number(statsRows[0]?.count ?? 0);
@@ -626,6 +660,38 @@ export class DashboardService {
};
});
// Agrega as linhas UF×rep em UF → { totais, reps[] } ordenado por faturamento.
const porUf = new Map<
string,
{ faturamento: number; pedidos: number; clientes: number; reps: UfRepRow[] }
>();
for (const r of ufRepRows) {
const acc = porUf.get(r.uf) ?? { faturamento: 0, pedidos: 0, clientes: 0, reps: [] };
acc.faturamento += Number(r.faturamento);
acc.pedidos += Number(r.pedidos);
acc.clientes += Number(r.clientes);
acc.reps.push(r);
porUf.set(r.uf, acc);
}
const faturamentoPorUf = [...porUf.entries()]
.map(([uf, t]) => ({
uf,
faturamento: t.faturamento,
pedidos: t.pedidos,
clientes: t.clientes,
pct: faturamentoMes > 0 ? Math.round((t.faturamento / faturamentoMes) * 1000) / 10 : 0,
reps: t.reps
.map((r) => ({
codVendedor: Number(r.cod_vendedor),
nomeVendedor: r.nome_vendedor ?? null,
faturamento: Number(r.faturamento),
pedidos: Number(r.pedidos),
clientes: Number(r.clientes),
}))
.sort((a, b) => b.faturamento - a.faturamento),
}))
.sort((a, b) => b.faturamento - a.faturamento);
return {
faturamentoMes,
pedidosMes,
@@ -640,6 +706,7 @@ export class DashboardService {
dia: r.dia,
clientes: Number(r.clientes),
})),
faturamentoPorUf,
metaPositivacaoDia: config?.metaPositivacaoDia ?? null,
syncedAt: now.toISOString(),
};