feat(web+api): metas por grupo no painel gerencial
Agrega GR-metas por cod_grupo (todos os reps) e realizado por grupo via JOIN em vw_peditens_erp/vw_produtos, retornando MetaItem[] no endpoint /dashboard/manager. GerPainel exibe tabela com Meta / Realizado / Falta / % Meta (barra + número) ordenada por valorMeta desc; card oculto quando não há grupos cadastrados. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -339,6 +339,14 @@ export class DashboardService {
|
||||
interface MetaTotalRow {
|
||||
meta_total: string;
|
||||
}
|
||||
interface MetaGrupoRow {
|
||||
cod_grupo: number | null;
|
||||
desc_grupo: string | null;
|
||||
valor: string;
|
||||
qtdade: string;
|
||||
peso: string;
|
||||
vl_fator: string;
|
||||
}
|
||||
interface PositivacaoRow {
|
||||
cod_vendedor: number;
|
||||
nome_vendedor: string | null;
|
||||
@@ -346,7 +354,15 @@ export class DashboardService {
|
||||
clientes_positivados: string;
|
||||
}
|
||||
|
||||
const [statsRows, rankingRows, promoRows, metaTotalRows, positivacaoRows] = await Promise.all([
|
||||
const [
|
||||
statsRows,
|
||||
rankingRows,
|
||||
promoRows,
|
||||
metaTotalRows,
|
||||
positivacaoRows,
|
||||
metaGrupoRows,
|
||||
realizadoGrupoRows,
|
||||
] = await Promise.all([
|
||||
prisma.$queryRawUnsafe<TotalRow[]>(`
|
||||
SELECT COUNT(*)::text AS count, COALESCE(SUM(total), 0)::text AS total
|
||||
FROM vw_pedidos_erp
|
||||
@@ -412,12 +428,70 @@ export class DashboardService {
|
||||
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
|
||||
`),
|
||||
prisma.$queryRawUnsafe<MetaGrupoRow[]>(`
|
||||
SELECT cod_grupo, TRIM(desc_grupo) AS desc_grupo,
|
||||
SUM(valor)::text AS valor,
|
||||
SUM(qtdade)::text AS qtdade,
|
||||
SUM(peso)::text AS peso,
|
||||
AVG(vl_fator)::text AS vl_fator
|
||||
FROM sar.vw_metas
|
||||
WHERE id_empresa = ${idEmpresaMatriz}
|
||||
AND ano = ${year}
|
||||
AND mes = ${month}
|
||||
AND TRIM(tipo) = 'GR'
|
||||
GROUP BY cod_grupo, desc_grupo
|
||||
ORDER BY SUM(valor) DESC
|
||||
`),
|
||||
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.situa NOT IN (1, 5)
|
||||
AND e.dt_pedido >= '${monthStartStr}'
|
||||
AND e.dt_pedido <= '${monthEndStr}'
|
||||
GROUP BY p.cod_grupo, grupo
|
||||
`),
|
||||
]);
|
||||
|
||||
const pedidosMes = Number(statsRows[0]?.count ?? 0);
|
||||
const faturamentoMes = Number(statsRows[0]?.total ?? 0);
|
||||
const ticketMedio = pedidosMes > 0 ? faturamentoMes / pedidosMes : 0;
|
||||
|
||||
const realGrupoPorCod = new Map<number, RealizadoGrupoRow>();
|
||||
for (const r of realizadoGrupoRows) {
|
||||
if (r.cod_grupo != null) realGrupoPorCod.set(Number(r.cod_grupo), r);
|
||||
}
|
||||
const metasPorGrupo = metaGrupoRows.map((m) => {
|
||||
const cod = m.cod_grupo != null ? Number(m.cod_grupo) : null;
|
||||
const real = cod != null ? realGrupoPorCod.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),
|
||||
};
|
||||
});
|
||||
|
||||
const rankingReps: RankingRep[] = rankingRows.map((r) => {
|
||||
const fat = Number(r.faturamento);
|
||||
const meta = r.meta_valor ? Number(r.meta_valor) : 0;
|
||||
@@ -453,6 +527,7 @@ export class DashboardService {
|
||||
totalReps,
|
||||
promocoesAtivas: Number(promoRows[0]?.count ?? 0),
|
||||
metaTotal,
|
||||
metasPorGrupo,
|
||||
rankingReps,
|
||||
positivacaoReps,
|
||||
syncedAt: now.toISOString(),
|
||||
|
||||
@@ -23,10 +23,11 @@ import {
|
||||
faAddressCard,
|
||||
faBullseye,
|
||||
faArrowTrendUp,
|
||||
faLayerGroup,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import type { RankingRep, PositivacaoRep } from '@sar/api-interface';
|
||||
import type { RankingRep, PositivacaoRep, MetaItem } from '@sar/api-interface';
|
||||
import { useManagerDashboard } from '../../lib/queries/gerente';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -78,6 +79,63 @@ const positivacaoColumns: TableColumnsType<PositivacaoRep> = [
|
||||
},
|
||||
];
|
||||
|
||||
const metasGrupoColumns: TableColumnsType<MetaItem> = [
|
||||
{
|
||||
title: 'Grupo',
|
||||
dataIndex: 'rotulo',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Meta',
|
||||
dataIndex: 'valorMeta',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
render: (v: number) => fmt(v),
|
||||
className: 'tabular-nums',
|
||||
},
|
||||
{
|
||||
title: 'Realizado',
|
||||
dataIndex: 'valorReal',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
render: (v: number) => fmt(v),
|
||||
className: 'tabular-nums',
|
||||
},
|
||||
{
|
||||
title: 'Falta',
|
||||
dataIndex: 'falta',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: (v: number) => (
|
||||
<Text
|
||||
className="tabular-nums"
|
||||
style={{ fontSize: 'var(--text-sm)', color: v === 0 ? 'var(--green)' : undefined }}
|
||||
>
|
||||
{v === 0 ? '—' : fmt(v)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '% Meta',
|
||||
dataIndex: 'pct',
|
||||
width: 170,
|
||||
render: (pct: number) => (
|
||||
<Flex align="center" gap={8}>
|
||||
<Progress
|
||||
percent={Math.min(pct, 100)}
|
||||
size="small"
|
||||
strokeColor={pct >= 100 ? 'var(--green)' : pct >= 70 ? 'var(--jcs-blue)' : '#faad14'}
|
||||
showInfo={false}
|
||||
style={{ flex: 1, minWidth: 60 }}
|
||||
/>
|
||||
<Text className="tabular-nums" style={{ fontSize: 'var(--text-sm)', width: 48 }}>
|
||||
{pct}%
|
||||
</Text>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const rankingColumns: TableColumnsType<RankingRep> = [
|
||||
{
|
||||
title: 'Representante',
|
||||
@@ -162,6 +220,7 @@ export function GerPainel() {
|
||||
totalReps,
|
||||
promocoesAtivas,
|
||||
metaTotal,
|
||||
metasPorGrupo,
|
||||
rankingReps,
|
||||
positivacaoReps,
|
||||
syncedAt,
|
||||
@@ -361,6 +420,35 @@ export function GerPainel() {
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Metas por Grupo */}
|
||||
{metasPorGrupo.length > 0 && (
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<FontAwesomeIcon icon={faLayerGroup} style={{ color: 'var(--jcs-blue)' }} />
|
||||
Metas por Grupo — {periodoLabel}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Text type="secondary" style={{ fontSize: 'var(--text-xs)' }}>
|
||||
{metasPorGrupo.filter((g: MetaItem) => g.pct >= 100).length} de {metasPorGrupo.length}{' '}
|
||||
grupo
|
||||
{metasPorGrupo.length !== 1 ? 's' : ''} atingido
|
||||
{metasPorGrupo.filter((g: MetaItem) => g.pct >= 100).length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Table<MetaItem>
|
||||
rowKey={(r) => String(r.codigo ?? r.rotulo)}
|
||||
columns={metasGrupoColumns}
|
||||
dataSource={metasPorGrupo}
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: 'Nenhuma meta por grupo cadastrada.' }}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Ranking */}
|
||||
<Card
|
||||
title={
|
||||
|
||||
Reference in New Issue
Block a user