feat(auth,ger): usuarios dev por papel, escopo do supervisor por equipe e positivacao diaria
- DevLogin com 3 usuarios reais: Pavei (rep 29), Sidnei (supervisor 191) e Lucas (gerente 156); guard deixa de forcar DEV_REP_CODE em dev e usa sub/id_empresa do JWT (fallback para os valores do .env); idEmpresa do token dev opcional, default DEV_EMPRESA_ID - getTeamCodes (vw_representantes.cod_supervisor): supervisor ve so a equipe em pedidos SAR/ERP, detalhe, aprovar/recusar, clientes (lista e carteira), dashboard supervisor, chamados SAC e relatorios carteira/curva ABC; gerente/admin seguem vendo a empresa toda - Painel gerencial: card "Positivacao Diaria de Clientes" (clientes distintos por dia via vw_pedidos_erp) com campo de meta/dia persistido em localStorage, linha de meta tracejada e contador de dias na meta Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
Col,
|
||||
DatePicker,
|
||||
Flex,
|
||||
InputNumber,
|
||||
Progress,
|
||||
Row,
|
||||
Skeleton,
|
||||
@@ -23,15 +24,40 @@ import {
|
||||
faAddressCard,
|
||||
faBullseye,
|
||||
faArrowTrendUp,
|
||||
faCalendarDay,
|
||||
faLayerGroup,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Tooltip as ChartTooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
import { Chart } from 'react-chartjs-2';
|
||||
import dayjs from 'dayjs';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import type { RankingRep, PositivacaoRep, MetaItem } from '@sar/api-interface';
|
||||
import type { RankingRep, PositivacaoRep, PositivacaoDia, MetaItem } from '@sar/api-interface';
|
||||
import { useManagerDashboard } from '../../lib/queries/gerente';
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
LineElement,
|
||||
PointElement,
|
||||
ChartTooltip,
|
||||
Legend,
|
||||
);
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
// Meta diária de positivação é preferência local do gerente (não vem do ERP)
|
||||
const META_POSITIVACAO_DIA_KEY = 'sar:ger:meta-positivacao-dia';
|
||||
|
||||
function fmt(v: number): string {
|
||||
return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
|
||||
}
|
||||
@@ -185,6 +211,127 @@ const rankingColumns: TableColumnsType<RankingRep> = [
|
||||
},
|
||||
];
|
||||
|
||||
function PositivacaoDiariaCard({
|
||||
dados,
|
||||
periodo,
|
||||
periodoLabel,
|
||||
isMesAtual,
|
||||
}: {
|
||||
dados: PositivacaoDia[];
|
||||
periodo: Dayjs;
|
||||
periodoLabel: string;
|
||||
isMesAtual: boolean;
|
||||
}) {
|
||||
const [meta, setMeta] = useState<number>(() => {
|
||||
const saved = Number(localStorage.getItem(META_POSITIVACAO_DIA_KEY));
|
||||
return Number.isFinite(saved) && saved > 0 ? saved : 0;
|
||||
});
|
||||
|
||||
function changeMeta(v: number | null) {
|
||||
const val = v ?? 0;
|
||||
setMeta(val);
|
||||
localStorage.setItem(META_POSITIVACAO_DIA_KEY, String(val));
|
||||
}
|
||||
|
||||
// Eixo com todos os dias: até hoje no mês atual, mês inteiro nos anteriores
|
||||
const ultimoDia = isMesAtual ? dayjs().date() : periodo.endOf('month').date();
|
||||
const porDia = new Map(dados.map((d) => [dayjs(d.dia).date(), d.clientes]));
|
||||
const labels = Array.from({ length: ultimoDia }, (_, i) => String(i + 1));
|
||||
const valores = labels.map((d) => porDia.get(Number(d)) ?? 0);
|
||||
const diasComMeta = meta > 0 ? valores.filter((v) => v >= meta).length : 0;
|
||||
|
||||
const chartData = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
type: 'bar' as const,
|
||||
label: 'Clientes positivados',
|
||||
data: valores,
|
||||
backgroundColor: 'rgba(0, 74, 153, 0.8)',
|
||||
borderRadius: 4,
|
||||
order: 2,
|
||||
},
|
||||
...(meta > 0
|
||||
? [
|
||||
{
|
||||
type: 'line' as const,
|
||||
label: `Meta (${meta}/dia)`,
|
||||
data: labels.map(() => meta),
|
||||
borderColor: '#f5222d',
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
borderDash: [5, 4],
|
||||
pointRadius: 0,
|
||||
order: 1,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: 'index' as const, intersect: false },
|
||||
plugins: {
|
||||
legend: { position: 'top' as const, labels: { boxWidth: 12, font: { size: 11 } } },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: (items: { label?: string }[]) => `Dia ${items[0]?.label ?? ''}`,
|
||||
label: (ctx: { dataset: { label?: string }; parsed: { y: number | null } }) => {
|
||||
const val = ctx.parsed.y;
|
||||
if (val == null) return '';
|
||||
return ` ${ctx.dataset.label}: ${val.toLocaleString('pt-BR')}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { display: false } },
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { precision: 0, font: { size: 10 } },
|
||||
grid: { color: '#f0f0f0' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<FontAwesomeIcon icon={faCalendarDay} style={{ color: 'var(--jcs-blue)' }} />
|
||||
Positivação Diária de Clientes — {periodoLabel}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Flex align="center" gap={8}>
|
||||
{meta > 0 && (
|
||||
<Text type="secondary" style={{ fontSize: 'var(--text-xs)' }}>
|
||||
{diasComMeta} de {ultimoDia} dia{ultimoDia !== 1 ? 's' : ''} na meta
|
||||
</Text>
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 'var(--text-sm)' }}>
|
||||
Meta/dia:
|
||||
</Text>
|
||||
<InputNumber
|
||||
min={0}
|
||||
size="small"
|
||||
value={meta > 0 ? meta : undefined}
|
||||
onChange={changeMeta}
|
||||
placeholder="—"
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<div style={{ height: 280 }}>
|
||||
<Chart type="bar" data={chartData} options={options} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function GerPainel() {
|
||||
const [periodo, setPeriodo] = useState<Dayjs>(dayjs());
|
||||
const mes = periodo.month() + 1;
|
||||
@@ -223,6 +370,7 @@ export function GerPainel() {
|
||||
metasPorGrupo,
|
||||
rankingReps,
|
||||
positivacaoReps,
|
||||
positivacaoDiaria,
|
||||
syncedAt,
|
||||
} = data;
|
||||
|
||||
@@ -478,6 +626,14 @@ export function GerPainel() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Positivação Diária */}
|
||||
<PositivacaoDiariaCard
|
||||
dados={positivacaoDiaria}
|
||||
periodo={periodo}
|
||||
periodoLabel={periodoLabel}
|
||||
isMesAtual={isMesAtual}
|
||||
/>
|
||||
|
||||
{/* Positivação por Representante */}
|
||||
<Card
|
||||
title={
|
||||
|
||||
@@ -9,12 +9,14 @@ import { AuthTokenResponseSchema } from '@sar/api-interface';
|
||||
|
||||
type DevUser = { key: string; userId: string; role: string; label: string };
|
||||
|
||||
// userId = cod_vendedor como string; idEmpresa = empresa no ERP (dev default = 1)
|
||||
// Em dev, o backend força DEV_REP_CODE=29 independente do userId enviado.
|
||||
// userId = cod_vendedor como string; idEmpresa fica a cargo do backend (DEV_EMPRESA_ID).
|
||||
// Usuários provisórios para testar as telas por papel enquanto o cadastro de
|
||||
// usuários não existe: Pavei (carteira própria), Sidnei (equipe via
|
||||
// vw_representantes.cod_supervisor = 191), Lucas (empresa toda).
|
||||
const DEV_USERS: DevUser[] = [
|
||||
{ key: 'rep-29', userId: '29', role: 'rep', label: 'Representante (cód. 29)' },
|
||||
{ key: 'sup-29', userId: '29', role: 'supervisor', label: 'Supervisor (cód. 29)' },
|
||||
{ key: 'mgr-29', userId: '29', role: 'manager', label: 'Gerente (cód. 29)' },
|
||||
{ key: 'rep-29', userId: '29', role: 'rep', label: 'Pavei — Representante (cód. 29)' },
|
||||
{ key: 'sup-191', userId: '191', role: 'supervisor', label: 'Sidnei — Supervisor (cód. 191)' },
|
||||
{ key: 'mgr-156', userId: '156', role: 'manager', label: 'Lucas — Gerente/Admin (cód. 156)' },
|
||||
];
|
||||
|
||||
export function DevLogin({ onLogin }: { onLogin: () => void }) {
|
||||
@@ -27,7 +29,7 @@ export function DevLogin({ onLogin }: { onLogin: () => void }) {
|
||||
try {
|
||||
const raw = await apiFetch('/auth/dev/token', {
|
||||
method: 'POST',
|
||||
body: { userId: user.userId, idEmpresa: 1, role: user.role },
|
||||
body: { userId: user.userId, role: user.role },
|
||||
});
|
||||
const { accessToken } = AuthTokenResponseSchema.parse(raw);
|
||||
authStore.set(accessToken);
|
||||
|
||||
Reference in New Issue
Block a user