diff --git a/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx b/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx index fd3c179..0f1671e 100644 --- a/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx +++ b/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx @@ -39,6 +39,12 @@ function getDisplayNome(row: FechamentoDaCompetenciaItem): string { return row.parceiroNome; } +/** Pontuação nas colunas da tabela: sempre centésimos (pt-BR). */ +function formatPontuacaoDecimal(valor: number): string { + if (!Number.isFinite(valor)) return "—"; + return valor.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + function AsanaImportLoadingCard() { return (
Logo Nome - Pontuação Total + Pontuação Total + Pontuação Aprovada Status Ações @@ -414,7 +421,7 @@ export default function CompetenciaFechamentos() { {loading ? ( - +
{getDisplayNome(row)} - {row.pontuacaoTotalEntregue} + + {formatPontuacaoDecimal(Number(row.pontuacaoTotalEntregue))} + + + {row.status === "fechado" + ? formatPontuacaoDecimal(Number(row.pontuacaoAprovada)) + : "—"} + 0 ? bloco.totalParceiro : 1; + const data = useMemo( + () => + bloco.linhas.map((l) => ({ + name: l.cliente, + value: l.pontos, + })), + [bloco.linhas], + ); + + if (data.length === 0) { + return ( +
+ Sem clientes neste período. +
+ ); + } + + return ( +
+ + + + (percent ?? 0) >= 0.06 + ? `${String(name).slice(0, 14)}${String(name).length > 14 ? "…" : ""} ${(((percent ?? 0) as number) * 100).toFixed(0)}%` + : "" + } + labelLine={false} + > + {data.map((_, i) => ( + + ))} + + { + if (!active || !payload?.[0]) return null; + const row = payload[0].payload as { name: string; value: number }; + const pct = total > 0 ? (row.value / total) * 100 : 0; + return ( +
+

{row.name}

+

+ {formatPontos(row.value)} pts · {pct.toFixed(1)}% +

+
+ ); + }} + /> + ( + {String(value).slice(0, 28)} + )} + wrapperStyle={{ paddingTop: 4 }} + /> +
+
+
+ ); } export default function DashboardPontos() { const { papel, me } = useAuthAccess(); const [parceiros, setParceiros] = useState([]); - const [dataInicio, setDataInicio] = useState(primeiroDiaMesAtual); - const [dataFim, setDataFim] = useState(hojeIsoDate); - const [parceiroId, setParceiroId] = useState("all"); - const [cliente, setCliente] = useState(""); + const [dataInicio, setDataInicio] = useState(() => inicioDoMesAtual()); + const [dataFim, setDataFim] = useState(() => fimDoMesAtual()); + /** `null` = todos os parceiros (sem filtro na API). */ + const [selectedPartnerIds, setSelectedPartnerIds] = useState(null); + const [parceirosMenuOpen, setParceirosMenuOpen] = useState(false); const [loading, setLoading] = useState(true); const [dados, setDados] = useState([]); - const clienteRef = useRef(cliente); - clienteRef.current = cliente; const parceiroFixoSupervisor = papel === "supervisor" && me?.parceiroId; + const selectionKey = selectedPartnerIds === null ? "all" : [...selectedPartnerIds].sort().join(","); + const periodoKey = `${dataInicio.getTime()}-${dataFim.getTime()}`; + const loadParceiros = useCallback(async () => { if (parceiroFixoSupervisor && me?.parceiroId) { try { @@ -63,12 +151,10 @@ export default function DashboardPontos() { }); const um = res.data.find((p) => p.id === me.parceiroId); setParceiros(um ? [um] : []); - setParceiroId(me.parceiroId); } catch (e) { const message = e instanceof Error ? e.message : "Erro ao carregar parceiros."; toast.error(message); setParceiros([]); - setParceiroId(me.parceiroId); } return; } @@ -85,17 +171,21 @@ export default function DashboardPontos() { const loadRelatorio = useCallback(async () => { try { setLoading(true); - const pid = - parceiroFixoSupervisor && me?.parceiroId - ? me.parceiroId - : parceiroId === "all" - ? undefined - : parceiroId; + let parceiroIds: string[] | undefined; + if (parceiroFixoSupervisor && me?.parceiroId) { + parceiroIds = [me.parceiroId]; + } else if (selectedPartnerIds !== null && selectedPartnerIds.length > 0) { + parceiroIds = selectedPartnerIds; + } + if (startOfDay(dataFim) < startOfDay(dataInicio)) { + toast.error("A data final não pode ser anterior à inicial."); + setDados([]); + return; + } const res = await dashboardPontosPorClienteService.relatorioPontosPorCliente({ - dataInicio: dataInicio.trim() || undefined, - dataFim: dataFim.trim() || undefined, - parceiroId: pid, - cliente: clienteRef.current.trim() || undefined, + dataInicio: dateToApiYmd(dataInicio), + dataFim: dateToApiYmd(dataFim), + parceiroIds, }); setDados(res); } catch (e) { @@ -105,7 +195,7 @@ export default function DashboardPontos() { } finally { setLoading(false); } - }, [dataFim, dataInicio, me?.parceiroId, parceiroFixoSupervisor, parceiroId]); + }, [dataFim, dataInicio, me?.parceiroId, parceiroFixoSupervisor, periodoKey, selectionKey]); useEffect(() => { void loadParceiros(); @@ -113,149 +203,313 @@ export default function DashboardPontos() { useEffect(() => { void loadRelatorio(); - }, [dataFim, dataInicio, loadRelatorio, parceiroId, me?.parceiroId, papel]); + }, [loadRelatorio, me?.parceiroId, papel]); - const totalGeral = useMemo( - () => dados.reduce((acc, p) => acc + p.totalParceiro, 0), - [dados], + const allParceiroIds = useMemo(() => parceiros.map((p) => p.id), [parceiros]); + + const parceirosSelecaoLabel = useMemo(() => { + if (parceiroFixoSupervisor) { + const p = parceiros[0]; + if (!p) return "Parceiro vinculado"; + return p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome; + } + if (selectedPartnerIds === null) return "Todos os parceiros"; + if (selectedPartnerIds.length === 1) { + const p = parceiros.find((x) => x.id === selectedPartnerIds[0]); + if (!p) return "1 parceiro"; + return p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome; + } + return `${selectedPartnerIds.length} parceiros selecionados`; + }, [parceiroFixoSupervisor, parceiros, selectedPartnerIds]); + + const toggleParceiro = useCallback( + (id: string, checked: boolean) => { + setSelectedPartnerIds((prev) => { + if (parceiroFixoSupervisor) return prev; + if (prev === null) { + if (checked) return null; + const next = allParceiroIds.filter((x) => x !== id); + if (next.length === 0) { + toast.error("Selecione ao menos um parceiro."); + return prev; + } + return next; + } + const set = new Set(prev); + if (checked) set.add(id); + else set.delete(id); + const arr = Array.from(set); + if (arr.length === 0) { + toast.error("Selecione ao menos um parceiro."); + return prev; + } + if (arr.length === allParceiroIds.length) return null; + return arr; + }); + }, + [allParceiroIds, parceiroFixoSupervisor], ); + const setTodosParceiros = useCallback(() => { + if (!parceiroFixoSupervisor) setSelectedPartnerIds(null); + }, [parceiroFixoSupervisor]); + + const isParceiroChecked = (id: string) => + selectedPartnerIds === null || (selectedPartnerIds?.includes(id) ?? false); + return ( -
-
-
-

- +
+
+ + +
+
+ + Intelligence Score +
+

+ + + Dashboard

-

- Pontos alocados por cliente (tarefas revisadas), por parceiro. +

+ Distribuição de pontos por cliente — visão rápida do que cada parceiro entregou no período.

- -
+ - - - Filtros - Período por data de conclusão da tarefa, ou data de criação se ainda não concluída. - - -
- - setDataInicio(e.target.value)} - /> -
-
- - setDataFim(e.target.value)} /> -
-
- - -
-
- - setCliente(e.target.value)} - /> -
-
- - - -
+ + + + Filtros + + +
+ + + + + + + { + if (d) setDataInicio(d); + }} + locale={ptBR} + defaultMonth={dataInicio} + initialFocus + className="pointer-events-auto p-3" + /> + + +
- - -
- Resumo - Soma dos pontos no período e filtros atuais. -
- {loading ? ( - - ) : ( - {formatPontos(totalGeral)} pts - )} -
-
+
+ + + + + + + { + if (d) setDataFim(d); + }} + locale={ptBR} + defaultMonth={dataFim} + disabled={(date) => date < startOfDay(dataInicio)} + initialFocus + className="pointer-events-auto p-3" + /> + + +
+ + {!parceiroFixoSupervisor ? ( +
+ + + + + + +
+ Selecionar parceiros +
+ { + if (c === true) { + setTodosParceiros(); + return; + } + if (allParceiroIds.length <= 1) return; + setSelectedPartnerIds([allParceiroIds[0]]); + toast.info("Filtro por um parceiro. Use «Todos» para voltar a exibir todos."); + }} + /> + +
+
+ + +
+ {parceiros.map((p) => ( +
+ toggleParceiro(p.id, c === true)} + /> + +
+ ))} +
+
+
+
+
+ ) : ( +
+ + +
+ )} + +
+ +
+
+
+
{loading && dados.length === 0 ? ( -
- +
+
) : dados.length === 0 ? ( - - - Nenhum dado para os filtros selecionados. + + + Nenhum dado para os filtros atuais. Ajuste o período ou os parceiros. ) : ( -
- {dados.map((bloco) => ( - - -
- {bloco.parceiroNome} - - Total: {formatPontos(bloco.totalParceiro)} pts - -
-
- - - - - Cliente - Pontos - - - - {bloco.linhas.map((linha) => ( - - {linha.cliente} - {formatPontos(linha.pontos)} - - ))} - -
-
-
+ + {dados.map((bloco, idx) => ( + + + +
+ + {bloco.parceiroNome} + + + {formatPontos(bloco.totalParceiro)} pts + +
+
+ + + +
+
))} -
+ )}
); diff --git a/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx b/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx index 2044528..7aa85e0 100644 --- a/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx +++ b/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx @@ -12,9 +12,9 @@ import { Plus, Repeat, RotateCcw, + Sigma, Target, Trash2, - TrendingUp, User, Wallet, } from "lucide-react"; @@ -307,10 +307,13 @@ export default function FechamentoDetalhes() { const totais = useMemo(() => { const aprovadas = tarefas.filter((t) => t.estaRevisada); - const pontos = aprovadas.reduce((acc, t) => acc + Number(t.pontuacao || 0), 0); + const pontosAprovados = aprovadas.reduce((acc, t) => acc + Number(t.pontuacao || 0), 0); + const pontosTotalMes = tarefas.reduce((acc, t) => acc + Number(t.pontuacao || 0), 0); return { - pontos, - aprovadas: aprovadas.length, + /** Soma das pontuações em tarefas marcadas como revisadas (equivale ao que entra no fechamento). */ + pontos: pontosAprovados, + /** Soma de todas as pontuações das tarefas listadas no fechamento (mês). */ + pontosTotalMes, }; }, [tarefas]); const pontuacaoPagaNumero = parsePontuacaoInput(pontuacaoPagaInput || "0"); @@ -371,11 +374,7 @@ export default function FechamentoDetalhes() { return timeA - timeB; }); const estadoInicialBanco = Object.fromEntries(tarefasOrdenadas.map((tarefa) => [tarefa.id, Boolean(tarefa.estaRevisada)])); - const tarefasDefaultRevisadas = tarefasOrdenadas.map((tarefa) => ({ - ...tarefa, - estaRevisada: true, - })); - setTarefas(tarefasDefaultRevisadas); + setTarefas(tarefasOrdenadas); setEstadoRevisaoInicial(estadoInicialBanco); } catch (error) { const message = error instanceof Error ? error.message : "Erro ao carregar detalhes do fechamento."; @@ -1109,17 +1108,21 @@ export default function FechamentoDetalhes() {
- Tarefas aprovadas - + + Pontuação total do mês + +
- {totais.aprovadas} + {formatPontos(totais.pontosTotalMes)}
- Pontuação aprovada - + + Pontuação aprovada + +
{formatPontos(totais.pontos)}
diff --git a/src/services/fechamento/competencias.ts b/src/services/fechamento/competencias.ts index ba884f3..2491d45 100644 --- a/src/services/fechamento/competencias.ts +++ b/src/services/fechamento/competencias.ts @@ -26,6 +26,7 @@ export type FechamentoDaCompetenciaItem = { status: "em_aberto" | "fechado"; versao: number; pontuacaoTotalEntregue: number; + pontuacaoAprovada: number; pontuacaoMeta: number; pontuacaoPaga: number; pontuacaoBanco: number; diff --git a/src/services/fechamento/dashboardPontosPorCliente.ts b/src/services/fechamento/dashboardPontosPorCliente.ts index 37bdcb1..9640f1e 100644 --- a/src/services/fechamento/dashboardPontosPorCliente.ts +++ b/src/services/fechamento/dashboardPontosPorCliente.ts @@ -27,8 +27,10 @@ type ApiErrorShape = { export type RelatorioPontosPorClienteParams = { dataInicio?: string; dataFim?: string; + /** Um único parceiro (legado; preferir `parceiroIds`). */ parceiroId?: string; - cliente?: string; + /** Vários parceiros (CSV na query). Se vazio/ausente com `parceiroId`, usa só o id único. */ + parceiroIds?: string[]; }; class DashboardPontosPorClienteService { @@ -58,8 +60,14 @@ class DashboardPontosPorClienteService { params: { data_inicio: params.dataInicio?.trim() || undefined, data_fim: params.dataFim?.trim() || undefined, - parceiro_id: params.parceiroId?.trim() || undefined, - cliente: params.cliente?.trim() || undefined, + parceiro_ids: + params.parceiroIds && params.parceiroIds.length > 0 + ? params.parceiroIds.join(",") + : undefined, + parceiro_id: + (!params.parceiroIds || params.parceiroIds.length === 0) && params.parceiroId?.trim() + ? params.parceiroId.trim() + : undefined, }, }, );