feat(web+api): gráfico anual realizado vs meta no painel do representante
Barras (azul/verde) por mês + linha tracejada de meta; meses futuros em cinza. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -124,7 +124,24 @@ export class DashboardService {
|
||||
obs: string | null;
|
||||
}
|
||||
|
||||
const [atingidoRows, pedidosMesRows, recentRows, realizadoGrupoRows] = await Promise.all([
|
||||
interface RealizadoMesRow {
|
||||
mes: string;
|
||||
valor: string;
|
||||
}
|
||||
interface MetaMesRow {
|
||||
mes: string;
|
||||
tipo: string;
|
||||
valor: string;
|
||||
}
|
||||
|
||||
const [
|
||||
atingidoRows,
|
||||
pedidosMesRows,
|
||||
recentRows,
|
||||
realizadoGrupoRows,
|
||||
realizadoMesRows,
|
||||
metaMesRows,
|
||||
] = await Promise.all([
|
||||
prisma.$queryRawUnsafe<TotalRow[]>(`
|
||||
SELECT COALESCE(SUM(total), 0)::text AS total
|
||||
FROM vw_pedidos_erp
|
||||
@@ -178,6 +195,30 @@ export class DashboardService {
|
||||
AND e.dt_pedido <= '${monthEndStr}'
|
||||
GROUP BY p.cod_grupo, grupo
|
||||
`),
|
||||
// Realizado por mês do ano corrente.
|
||||
prisma.$queryRawUnsafe<RealizadoMesRow[]>(`
|
||||
SELECT EXTRACT(MONTH FROM dt_pedido)::int::text AS mes,
|
||||
COALESCE(SUM(total), 0)::text AS valor
|
||||
FROM vw_pedidos_erp
|
||||
WHERE id_empresa = ${idEmpresa}
|
||||
AND cod_vendedor = ${codVendedor}
|
||||
AND situa NOT IN (1, 5)
|
||||
AND EXTRACT(YEAR FROM dt_pedido) = ${year}
|
||||
GROUP BY mes
|
||||
ORDER BY mes
|
||||
`),
|
||||
// Meta (GL + GR) por mês do ano corrente.
|
||||
prisma.$queryRawUnsafe<MetaMesRow[]>(`
|
||||
SELECT mes::text, TRIM(tipo) AS tipo,
|
||||
COALESCE(SUM(valor), 0)::text AS valor
|
||||
FROM vw_metas
|
||||
WHERE id_empresa = ${idEmpresaMatriz}
|
||||
AND cod_vendedor = ${codVendedor}
|
||||
AND ano = ${year}
|
||||
AND TRIM(tipo) IN ('GL', 'GR')
|
||||
GROUP BY mes, tipo
|
||||
ORDER BY mes
|
||||
`),
|
||||
]);
|
||||
|
||||
const atingido = Number(atingidoRows[0]?.total ?? 0);
|
||||
@@ -267,21 +308,48 @@ export class DashboardService {
|
||||
})
|
||||
.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,
|
||||
}));
|
||||
// Deduplica por idCliente — vw_clientes pode retornar o mesmo cliente em
|
||||
// mais de uma empresa; mantemos a primeira ocorrência (ORDER BY dt_max ASC NULLS FIRST).
|
||||
const seenNP = new Set<number>();
|
||||
const naoPositivados = naoPositivadosRows
|
||||
.filter((c) => {
|
||||
const id = Number(c.id_cliente);
|
||||
if (seenNP.has(id)) return false;
|
||||
seenNP.add(id);
|
||||
return true;
|
||||
})
|
||||
.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,
|
||||
}));
|
||||
|
||||
// Monta histório mensal: 12 meses do ano, cruzando realizado e meta.
|
||||
const realizadoPorMes = new Map(realizadoMesRows.map((r) => [Number(r.mes), Number(r.valor)]));
|
||||
const metaGlPorMes = new Map<number, number>();
|
||||
const metaGrPorMes = new Map<number, number>();
|
||||
for (const r of metaMesRows) {
|
||||
const m = Number(r.mes);
|
||||
if (r.tipo === 'GL') metaGlPorMes.set(m, (metaGlPorMes.get(m) ?? 0) + Number(r.valor));
|
||||
else metaGrPorMes.set(m, (metaGrPorMes.get(m) ?? 0) + Number(r.valor));
|
||||
}
|
||||
const historicoMensal = Array.from({ length: 12 }, (_, i) => {
|
||||
const m = i + 1;
|
||||
const valor = realizadoPorMes.get(m) ?? 0;
|
||||
const meta = metaGlPorMes.has(m) ? (metaGlPorMes.get(m) ?? 0) : (metaGrPorMes.get(m) ?? 0);
|
||||
return { mes: m, valor, meta, atingiu: meta > 0 && valor >= meta };
|
||||
});
|
||||
|
||||
return {
|
||||
meta: { atingido, total: targetAmount, pct, falta },
|
||||
metaDimensao,
|
||||
metasPorGrupo,
|
||||
pedidosMes,
|
||||
historicoMensal,
|
||||
pedidosRecentes: recentRows.map((o) => ({
|
||||
id: `erp-${o.id_pedido}`,
|
||||
numPedSar: (o.num_ped_sar ?? '').trim(),
|
||||
|
||||
Reference in New Issue
Block a user