feat(api,web): positivacao de novos clientes por representante no painel gerencial
- novosClientes por rep no dashboard do gerente: clientes da carteira cuja PRIMEIRA compra (historico todo do ERP) caiu no periodo - Coluna "Novos no mes" (tag verde +N, ordenavel) na tabela de positivacao + total de novos clientes no cabecalho do card Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -418,6 +418,10 @@ export class DashboardService {
|
|||||||
dia: string;
|
dia: string;
|
||||||
clientes: string;
|
clientes: string;
|
||||||
}
|
}
|
||||||
|
interface NovosClientesRow {
|
||||||
|
cod_vendedor: number;
|
||||||
|
novos: string;
|
||||||
|
}
|
||||||
|
|
||||||
const [
|
const [
|
||||||
statsRows,
|
statsRows,
|
||||||
@@ -428,6 +432,7 @@ export class DashboardService {
|
|||||||
metaGrupoRows,
|
metaGrupoRows,
|
||||||
realizadoGrupoRows,
|
realizadoGrupoRows,
|
||||||
positivacaoDiariaRows,
|
positivacaoDiariaRows,
|
||||||
|
novosClientesRows,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
prisma.$queryRawUnsafe<TotalRow[]>(`
|
prisma.$queryRawUnsafe<TotalRow[]>(`
|
||||||
SELECT COUNT(*)::text AS count, COALESCE(SUM(total), 0)::text AS total
|
SELECT COUNT(*)::text AS count, COALESCE(SUM(total), 0)::text AS total
|
||||||
@@ -535,6 +540,21 @@ export class DashboardService {
|
|||||||
GROUP BY dt_pedido::date
|
GROUP BY dt_pedido::date
|
||||||
ORDER BY dia
|
ORDER BY dia
|
||||||
`),
|
`),
|
||||||
|
prisma.$queryRawUnsafe<NovosClientesRow[]>(`
|
||||||
|
SELECT c.cod_vendedor, COUNT(*)::text AS novos
|
||||||
|
FROM (
|
||||||
|
SELECT id_cliente, MIN(dt_pedido) AS primeira_compra
|
||||||
|
FROM vw_pedidos_erp
|
||||||
|
WHERE id_empresa = ${idEmpresa}
|
||||||
|
AND situa NOT IN (1, 5)
|
||||||
|
GROUP BY id_cliente
|
||||||
|
) f
|
||||||
|
JOIN (SELECT DISTINCT id_cliente, cod_vendedor FROM vw_clientes) c
|
||||||
|
ON c.id_cliente = f.id_cliente
|
||||||
|
WHERE f.primeira_compra >= '${monthStartStr}'
|
||||||
|
AND f.primeira_compra <= '${monthEndStr}'
|
||||||
|
GROUP BY c.cod_vendedor
|
||||||
|
`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const pedidosMes = Number(statsRows[0]?.count ?? 0);
|
const pedidosMes = Number(statsRows[0]?.count ?? 0);
|
||||||
@@ -585,6 +605,9 @@ export class DashboardService {
|
|||||||
const totalReps = new Set(rankingRows.map((r) => Number(r.cod_vendedor))).size;
|
const totalReps = new Set(rankingRows.map((r) => Number(r.cod_vendedor))).size;
|
||||||
const metaTotal = Number(metaTotalRows[0]?.meta_total ?? 0);
|
const metaTotal = Number(metaTotalRows[0]?.meta_total ?? 0);
|
||||||
|
|
||||||
|
const novosMap = new Map(
|
||||||
|
novosClientesRows.map((n) => [Number(n.cod_vendedor), Number(n.novos)]),
|
||||||
|
);
|
||||||
const positivacaoReps: PositivacaoRep[] = positivacaoRows.map((r) => {
|
const positivacaoReps: PositivacaoRep[] = positivacaoRows.map((r) => {
|
||||||
const total = Number(r.total_clientes);
|
const total = Number(r.total_clientes);
|
||||||
const positivados = Number(r.clientes_positivados);
|
const positivados = Number(r.clientes_positivados);
|
||||||
@@ -594,6 +617,7 @@ export class DashboardService {
|
|||||||
totalClientes: total,
|
totalClientes: total,
|
||||||
clientesPositivados: positivados,
|
clientesPositivados: positivados,
|
||||||
pctPositivacao: total > 0 ? Math.round((positivados / total) * 100) : 0,
|
pctPositivacao: total > 0 ? Math.round((positivados / total) * 100) : 0,
|
||||||
|
novosClientes: novosMap.get(Number(r.cod_vendedor)) ?? 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
Space,
|
Space,
|
||||||
Table,
|
Table,
|
||||||
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
@@ -84,6 +85,21 @@ const positivacaoColumns: TableColumnsType<PositivacaoRep> = [
|
|||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Novos no mês',
|
||||||
|
key: 'novos',
|
||||||
|
width: 120,
|
||||||
|
align: 'center' as const,
|
||||||
|
sorter: (a: PositivacaoRep, b: PositivacaoRep) => a.novosClientes - b.novosClientes,
|
||||||
|
render: (_: unknown, row: PositivacaoRep) =>
|
||||||
|
row.novosClientes > 0 ? (
|
||||||
|
<Tag color="green" style={{ margin: 0, fontWeight: 600 }}>
|
||||||
|
+{row.novosClientes}
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">—</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '% Positivação',
|
title: '% Positivação',
|
||||||
dataIndex: 'pctPositivacao',
|
dataIndex: 'pctPositivacao',
|
||||||
@@ -643,9 +659,20 @@ export function GerPainel() {
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
|
<Flex align="center" gap={12}>
|
||||||
|
{positivacaoReps.reduce((acc, r) => acc + r.novosClientes, 0) > 0 && (
|
||||||
|
<Tag color="green" style={{ margin: 0, fontWeight: 600 }}>
|
||||||
|
+{positivacaoReps.reduce((acc, r) => acc + r.novosClientes, 0)} novo
|
||||||
|
{positivacaoReps.reduce((acc, r) => acc + r.novosClientes, 0) !== 1 ? 's' : ''}{' '}
|
||||||
|
cliente
|
||||||
|
{positivacaoReps.reduce((acc, r) => acc + r.novosClientes, 0) !== 1 ? 's' : ''} no
|
||||||
|
período
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
<Text type="secondary" style={{ fontSize: 'var(--text-xs)' }}>
|
<Text type="secondary" style={{ fontSize: 'var(--text-xs)' }}>
|
||||||
{positivacaoReps.filter((r) => r.clientesPositivados > 0).length} reps com positivação
|
{positivacaoReps.filter((r) => r.clientesPositivados > 0).length} reps com positivação
|
||||||
</Text>
|
</Text>
|
||||||
|
</Flex>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Table<PositivacaoRep>
|
<Table<PositivacaoRep>
|
||||||
|
|||||||
@@ -85,7 +85,10 @@ export const SupervisorDashboardSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type SupervisorDashboard = z.infer<typeof SupervisorDashboardSchema>;
|
export type SupervisorDashboard = z.infer<typeof SupervisorDashboardSchema>;
|
||||||
|
|
||||||
|
// novosClientes: clientes da carteira cuja PRIMEIRA compra (historico todo)
|
||||||
|
// caiu no periodo selecionado
|
||||||
export const PositivacaoRepSchema = z.object({
|
export const PositivacaoRepSchema = z.object({
|
||||||
|
novosClientes: z.number().int().default(0),
|
||||||
codVendedor: z.number().int(),
|
codVendedor: z.number().int(),
|
||||||
nomeVendedor: z.string().nullable(),
|
nomeVendedor: z.string().nullable(),
|
||||||
totalClientes: z.number().int(),
|
totalClientes: z.number().int(),
|
||||||
|
|||||||
Reference in New Issue
Block a user