diff --git a/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx b/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx
index 5b4a572..88e8638 100644
--- a/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx
+++ b/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx
@@ -193,9 +193,13 @@ export default function CompetenciaFechamentos() {
toast.error("Não foi possível identificar o usuário para reabrir a competência.");
return;
}
+ if (!motivoReabertura.trim()) {
+ toast.error("Informe o motivo da reabertura da competência.");
+ return;
+ }
try {
setReabrindoCompetencia(true);
- await fechamentoCompetenciasService.reabrirCompetencia(competenciaId, me.id, motivoReabertura.trim() || undefined);
+ await fechamentoCompetenciasService.reabrirCompetencia(competenciaId, me.id, motivoReabertura.trim());
toast.success("Competência reaberta com sucesso.");
setIsReabrirModalOpen(false);
setMotivoReabertura("");
@@ -601,12 +605,13 @@ export default function CompetenciaFechamentos() {
-
+
setMotivoReabertura(e.target.value)}
placeholder="Ex.: correção de ajustes pós-fechamento"
+ required
/>
@@ -618,7 +623,10 @@ export default function CompetenciaFechamentos() {
>
Cancelar
-
diff --git a/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx b/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx
index a002ac3..cd5b1cb 100644
--- a/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx
+++ b/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx
@@ -1,6 +1,22 @@
import { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
-import { ArrowLeft, CheckCircle2, Clock3, ExternalLink, ListChecks, Loader2, Pencil, Plus, Repeat, RotateCcw, Target, Trash2, TrendingUp } from "lucide-react";
+import {
+ ArrowLeft,
+ CheckCircle2,
+ Clock3,
+ ExternalLink,
+ ListChecks,
+ Loader2,
+ Pencil,
+ Plus,
+ Repeat,
+ RotateCcw,
+ Target,
+ Trash2,
+ TrendingUp,
+ User,
+ Wallet,
+} from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -17,8 +33,11 @@ import {
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useAuthAccess } from "@/contexts/AuthAccessContext";
+import { fechamentoBancoPontosService } from "@/services/fechamento/bancoPontos";
import { fechamentoCompetenciasService, type CompetenciaItem } from "@/services/fechamento/competencias";
import { fechamentoFechamentosService, type FechamentoTarefaItem } from "@/services/fechamento/fechamentos";
@@ -43,6 +62,28 @@ function formatPontos(valor: number): string {
return valor.toLocaleString("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 4 });
}
+/** Ex.: toast após concluir — vírgula pt-BR e no máximo 2 casas decimais. */
+function formatPontosAte2Casas(valor: number): string {
+ if (!Number.isFinite(valor)) return "—";
+ return valor.toLocaleString("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
+}
+
+/** Alinhado ao `requerMotivoAjuste`: evita "-0" e cor errada por ruído de float. */
+const SALDO_PONTOS_EPS = 0.0001;
+
+function corSaldoPontos(valor: number): string {
+ if (!Number.isFinite(valor)) return "text-foreground";
+ if (valor > SALDO_PONTOS_EPS) return "text-emerald-600";
+ if (valor < -SALDO_PONTOS_EPS) return "text-red-600";
+ return "text-foreground";
+}
+
+function formatPontosSaldoModal(valor: number): string {
+ if (!Number.isFinite(valor)) return "—";
+ const v = Math.abs(valor) < SALDO_PONTOS_EPS ? 0 : valor;
+ return formatPontos(v);
+}
+
function parsePontuacaoInput(value: string): number {
const normalized = value.trim().replace(/\s/g, "").replace(",", ".");
const parsed = Number(normalized);
@@ -141,6 +182,11 @@ export default function FechamentoDetalhes() {
const [lancamentoDescricao, setLancamentoDescricao] = useState("");
const [lancamentoPontuacao, setLancamentoPontuacao] = useState("0");
const [pontuacaoMeta, setPontuacaoMeta] = useState(null);
+ const [parceiroId, setParceiroId] = useState(null);
+ const [parceiroNome, setParceiroNome] = useState(null);
+ const [saldoBancoAtual, setSaldoBancoAtual] = useState(null);
+ const [loadingSaldoBanco, setLoadingSaldoBanco] = useState(false);
+ const [erroSaldoBanco, setErroSaldoBanco] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [deletingTarefa, setDeletingTarefa] = useState(null);
const [isReprocessAsanaOpen, setIsReprocessAsanaOpen] = useState(false);
@@ -183,6 +229,16 @@ export default function FechamentoDetalhes() {
const requerMotivoAjuste = Number.isFinite(bancoCalculado) && Math.abs(bancoCalculado) > 0.0001;
const pontuacaoTotalLabel = toPontuacaoInput(totais.pontos);
const isValorEditado = pontuacaoPagaInput.trim() !== pontuacaoTotalLabel;
+ const saldoBancoPosFechamento =
+ saldoBancoAtual != null ? saldoBancoAtual + bancoCalculado : null;
+ const dividaParaQuitar =
+ saldoBancoAtual != null && saldoBancoAtual < 0 ? -saldoBancoAtual : 0;
+ const saldoPositivoDisponivel =
+ saldoBancoAtual != null && saldoBancoAtual > 0 ? saldoBancoAtual : 0;
+ /** Zera o banco: saldo + (aprovados − pago) = 0 ⇒ pago = aprovados + saldo (saldo negativo = dívida). */
+ const sugestaoQuitarDivida = totais.pontos - dividaParaQuitar;
+ /** Zera o banco com saldo positivo: precisa movimento negativo no banco ⇒ pago = aprovados + saldo. */
+ const sugestaoUsarSaldoPositivo = totais.pontos + saldoPositivoDisponivel;
const todasTarefasRevisadas = tarefas.length > 0 && tarefas.every((tarefa) => tarefa.estaRevisada);
const competenciasDestino = useMemo(
() => competenciasAbertas.filter((item) => item.id !== competenciaId),
@@ -227,6 +283,8 @@ export default function FechamentoDetalhes() {
setFechamentoStatus(current.status);
setPontuacaoMeta(current.pontuacaoMeta);
setCompetenciaStatus(competenciaAtual?.status ?? "em_aberto");
+ setParceiroId(current.parceiroId);
+ setParceiroNome(current.parceiroNome);
if (!competenciaId) {
setCompetenciaId(current.competenciaId);
}
@@ -235,6 +293,20 @@ export default function FechamentoDetalhes() {
}
};
+ const loadSaldoBanco = async (id: string) => {
+ setLoadingSaldoBanco(true);
+ setErroSaldoBanco(false);
+ try {
+ const response = await fechamentoBancoPontosService.listarExtrato(id, { page: 1, perPage: 1 });
+ setSaldoBancoAtual(response.parceiro?.saldo ?? 0);
+ } catch {
+ setErroSaldoBanco(true);
+ setSaldoBancoAtual(null);
+ } finally {
+ setLoadingSaldoBanco(false);
+ }
+ };
+
const toastBloqueioFechado = () => {
toast.error("Fechamento está fechado. Reabra para editar.");
};
@@ -262,6 +334,16 @@ export default function FechamentoDetalhes() {
}
}, [isFechado]);
+ useEffect(() => {
+ if (isConcluirOpen && parceiroId) {
+ void loadSaldoBanco(parceiroId);
+ }
+ if (!isConcluirOpen) {
+ setSaldoBancoAtual(null);
+ setErroSaldoBanco(false);
+ }
+ }, [isConcluirOpen, parceiroId]);
+
const handleToggleAprovada = async (tarefa: FechamentoTarefaItem, approved: boolean) => {
if (isCompetenciaConcluida) {
toastBloqueioCompetenciaConcluida();
@@ -417,7 +499,7 @@ export default function FechamentoDetalhes() {
pontuacaoPaga: pontuacaoPagaRaw,
motivoAjuste: motivoAjuste.trim() || undefined,
});
- toast.success(`Fechamento concluído. Banco de pontos: ${data.pontuacaoBanco}.`);
+ toast.success(`Fechamento concluído. Banco de pontos: ${formatPontosAte2Casas(data.pontuacaoBanco)}.`);
setFechamentoStatus("fechado");
navigate(`/fechamento/competencias/${data.competenciaId}`);
} catch (error) {
@@ -437,11 +519,15 @@ export default function FechamentoDetalhes() {
toast.error("Não foi possível identificar o usuário para reabertura.");
return;
}
+ if (!motivoReabertura.trim()) {
+ toast.error("Informe o motivo da reabertura do fechamento.");
+ return;
+ }
try {
setReabrindo(true);
const data = await fechamentoFechamentosService.reabrirFechamento(fechamentoId, {
reabertoPorId: me.id,
- motivo: motivoReabertura,
+ motivo: motivoReabertura.trim(),
});
toast.success("Fechamento reaberto com sucesso.");
setFechamentoStatus(data.status);
@@ -1082,93 +1168,214 @@ export default function FechamentoDetalhes() {