From 78592304680785dcae8059a3e0f8a5a509892d52 Mon Sep 17 00:00:00 2001 From: Vitex Tecnologia Date: Mon, 4 May 2026 10:51:21 -0300 Subject: [PATCH] novas atualizacoes --- .../pages/CompetenciaFechamentos.tsx | 14 +- .../pages/FechamentoDetalhes.tsx | 359 ++++++++++++++---- src/services/fechamento/competencias.ts | 4 +- src/services/fechamento/fechamentos.ts | 4 +- 4 files changed, 300 insertions(+), 81 deletions(-) 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() { - - - Concluir fechamento - - Revise os totais e confirme a pontuação paga para concluir este fechamento. - - - -
-
-
-

Aprovada

-

{formatPontos(totais.pontos)}

-
-
-

Meta

-

{formatPontos(Number(pontuacaoMeta ?? 0))}

-
-
-

Diferença

-

= 0 ? "text-emerald-600" : "text-red-600"}`}> - {formatPontos(diferencaParaMeta)} -

-
-
-

Banco de Pontos

-

= 0 ? "text-emerald-600" : "text-red-600"}`}> - {formatPontos(bancoCalculado)} -

-
-
-
- - -
-
- - {isValorEditado ? ( -
- + +
+ + Concluir fechamento + + Informe a pontuação paga e confirme. + + + {parceiroNome ? ( +
+
+
- ) : null} +
+

Parceiro

+

{parceiroNome}

+
+
+ ) : null} +
+ +
+
+
+
+
+ +
+

Este mês

+
+
+
+
Aprovados
+
+ {formatPontos(totais.pontos)} +
+
+
+
Meta
+
+ {formatPontos(Number(pontuacaoMeta ?? 0))} +
+
+
+
Diferença
+
= 0 ? "text-emerald-600" : "text-red-600" + }`} + > + {diferencaParaMeta >= 0 ? "+" : ""} + {formatPontos(diferencaParaMeta)} +
+
+
+
+ +
+
+
+ +
+

Banco de pontos

+
+
+
+
Saldo hoje
+
+ {loadingSaldoBanco ? ( + + ) : erroSaldoBanco ? ( +
+ Não foi possível carregar + {parceiroId ? ( + + ) : null} +
+ ) : ( + + {formatPontosSaldoModal(saldoBancoAtual ?? 0)} + + )} +
+
+
+
Movimento
+
+ {bancoCalculado > SALDO_PONTOS_EPS ? "+" : ""} + {formatPontosSaldoModal(bancoCalculado)} +
+
+
+
Depois
+
+ {saldoBancoPosFechamento == null ? ( + + ) : ( + + {formatPontosSaldoModal(saldoBancoPosFechamento)} + + )} +
+
+
+
+
+ + + +
+ setPontuacaoPagaInput(e.target.value)} placeholder="Ex.: 10,5" /> + {isValorEditado ? ( + + ) : null} +
+
+ + + {dividaParaQuitar > 0 ? ( + + ) : null} + {saldoPositivoDisponivel > 0 ? ( + + ) : null} +
+
+ {requerMotivoAjuste ? ( -
- +
+ setMotivoAjuste(e.target.value)} placeholder="Ex.: pagamento parcial acordado com o parceiro" + className="bg-background" />
) : null}
- + @@ -1204,12 +1411,13 @@ export default function FechamentoDetalhes() {
- + setMotivoReabertura(e.target.value)} placeholder="Ex.: ajuste após revisão financeira" + required />
@@ -1221,7 +1429,10 @@ export default function FechamentoDetalhes() { > Cancelar - diff --git a/src/services/fechamento/competencias.ts b/src/services/fechamento/competencias.ts index 6d1c4e1..7ddc668 100644 --- a/src/services/fechamento/competencias.ts +++ b/src/services/fechamento/competencias.ts @@ -159,14 +159,14 @@ class FechamentoCompetenciasService { async reabrirCompetencia( competenciaId: string, reabertoPorId: string, - motivo?: string, + motivo: string, ): Promise { try { const headers = await buildCommanderHeaders(); const baseUrl = resolveCommanderBaseUrl(); const response = await axios.post( `${baseUrl}/competencias/${competenciaId}/reabrir`, - { reaberto_por_id: reabertoPorId, motivo }, + { reaberto_por_id: reabertoPorId, motivo: motivo.trim() }, { headers }, ); return response.data.data; diff --git a/src/services/fechamento/fechamentos.ts b/src/services/fechamento/fechamentos.ts index cc238c6..0bcc2fe 100644 --- a/src/services/fechamento/fechamentos.ts +++ b/src/services/fechamento/fechamentos.ts @@ -277,7 +277,7 @@ class FechamentoFechamentosService { async reabrirFechamento( fechamentoId: string, - input: { reabertoPorId: string; motivo?: string }, + input: { reabertoPorId: string; motivo: string }, ): Promise { try { const headers = await buildCommanderHeaders(); @@ -286,7 +286,7 @@ class FechamentoFechamentosService { `${baseUrl}/fechamentos/${fechamentoId}/reabrir`, { reaberto_por_id: input.reabertoPorId, - ...(input.motivo?.trim() ? { motivo: input.motivo.trim() } : {}), + motivo: input.motivo.trim(), }, { headers }, );