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.
- void loadRelatorio()}>
-
+ void loadRelatorio()}
+ >
+
Atualizar
-
+
-
-
- Filtros
- Período por data de conclusão da tarefa, ou data de criação se ainda não concluída.
-
-
-
- Data início
- setDataInicio(e.target.value)}
- />
-
-
- Data fim
- setDataFim(e.target.value)} />
-
-
- Parceiro
-
-
-
-
-
- {!parceiroFixoSupervisor ? (
- Todos os parceiros
- ) : null}
- {parceiroFixoSupervisor && parceiros.length === 0 && me?.parceiroId ? (
- Parceiro vinculado
- ) : null}
- {parceiros.map((p) => (
-
- {p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome}
-
- ))}
-
-
-
-
- Cliente (contém)
- setCliente(e.target.value)}
- />
-
-
-
- void loadRelatorio()}>
- Aplicar filtros
-
-
-
+
+
+
+ Filtros
+
+
+
+
+ Data inicial
+
+
+
+
+
+ {format(dataInicio, "dd/MM/yyyy", { locale: ptBR })}
+
+
+
+ {
+ 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
- )}
-
-
+
+
+ Data final
+
+
+
+
+
+ {format(dataFim, "dd/MM/yyyy", { locale: ptBR })}
+
+
+
+ {
+ if (d) setDataFim(d);
+ }}
+ locale={ptBR}
+ defaultMonth={dataFim}
+ disabled={(date) => date < startOfDay(dataInicio)}
+ initialFocus
+ className="pointer-events-auto p-3"
+ />
+
+
+
+
+ {!parceiroFixoSupervisor ? (
+
+
+ Parceiros
+
+
+
+
+ {parceirosSelecaoLabel}
+
+
+
+
+
+
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.");
+ }}
+ />
+
+ Todos os parceiros
+
+
+
+
+
+
+ {parceiros.map((p) => (
+
+ toggleParceiro(p.id, c === true)}
+ />
+
+ {p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome}
+
+
+ ))}
+
+
+
+
+
+ ) : (
+
+
+ Parceiro
+
+
+
+
+
+
+ {parceiros.map((p) => (
+
+ {p.codinome?.trim() ? `${p.nome} (${p.codinome.trim()})` : p.nome}
+
+ ))}
+ {parceiros.length === 0 && me?.parceiroId ? (
+ Parceiro vinculado
+ ) : null}
+
+
+
+ )}
+
+
+ void loadRelatorio()}>
+ Aplicar
+
+
+
+
+
{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,
},
},
);