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:
@@ -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