novas atualizacoes

This commit is contained in:
Vitex Tecnologia
2026-05-04 10:51:21 -03:00
parent dee9ecfbca
commit 7859230468
4 changed files with 300 additions and 81 deletions
@@ -193,9 +193,13 @@ export default function CompetenciaFechamentos() {
toast.error("Não foi possível identificar o usuário para reabrir a competência."); toast.error("Não foi possível identificar o usuário para reabrir a competência.");
return; return;
} }
if (!motivoReabertura.trim()) {
toast.error("Informe o motivo da reabertura da competência.");
return;
}
try { try {
setReabrindoCompetencia(true); 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."); toast.success("Competência reaberta com sucesso.");
setIsReabrirModalOpen(false); setIsReabrirModalOpen(false);
setMotivoReabertura(""); setMotivoReabertura("");
@@ -601,12 +605,13 @@ export default function CompetenciaFechamentos() {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="motivo-reabrir-competencia">Motivo (opcional)</Label> <Label htmlFor="motivo-reabrir-competencia">Motivo *</Label>
<Input <Input
id="motivo-reabrir-competencia" id="motivo-reabrir-competencia"
value={motivoReabertura} value={motivoReabertura}
onChange={(e) => setMotivoReabertura(e.target.value)} onChange={(e) => setMotivoReabertura(e.target.value)}
placeholder="Ex.: correção de ajustes pós-fechamento" placeholder="Ex.: correção de ajustes pós-fechamento"
required
/> />
</div> </div>
<DialogFooter> <DialogFooter>
@@ -618,7 +623,10 @@ export default function CompetenciaFechamentos() {
> >
Cancelar Cancelar
</Button> </Button>
<Button onClick={() => void handleReabrirCompetencia()} disabled={reabrindoCompetencia}> <Button
onClick={() => void handleReabrirCompetencia()}
disabled={reabrindoCompetencia || !motivoReabertura.trim()}
>
{reabrindoCompetencia ? "Reabrindo..." : "Confirmar reabertura"} {reabrindoCompetencia ? "Reabrindo..." : "Confirmar reabertura"}
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -1,6 +1,22 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom"; 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 { toast } from "sonner";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -17,8 +33,11 @@ import {
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useAuthAccess } from "@/contexts/AuthAccessContext"; import { useAuthAccess } from "@/contexts/AuthAccessContext";
import { fechamentoBancoPontosService } from "@/services/fechamento/bancoPontos";
import { fechamentoCompetenciasService, type CompetenciaItem } from "@/services/fechamento/competencias"; import { fechamentoCompetenciasService, type CompetenciaItem } from "@/services/fechamento/competencias";
import { fechamentoFechamentosService, type FechamentoTarefaItem } from "@/services/fechamento/fechamentos"; 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 }); 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 { function parsePontuacaoInput(value: string): number {
const normalized = value.trim().replace(/\s/g, "").replace(",", "."); const normalized = value.trim().replace(/\s/g, "").replace(",", ".");
const parsed = Number(normalized); const parsed = Number(normalized);
@@ -141,6 +182,11 @@ export default function FechamentoDetalhes() {
const [lancamentoDescricao, setLancamentoDescricao] = useState(""); const [lancamentoDescricao, setLancamentoDescricao] = useState("");
const [lancamentoPontuacao, setLancamentoPontuacao] = useState("0"); const [lancamentoPontuacao, setLancamentoPontuacao] = useState("0");
const [pontuacaoMeta, setPontuacaoMeta] = useState<number | null>(null); const [pontuacaoMeta, setPontuacaoMeta] = useState<number | null>(null);
const [parceiroId, setParceiroId] = useState<string | null>(null);
const [parceiroNome, setParceiroNome] = useState<string | null>(null);
const [saldoBancoAtual, setSaldoBancoAtual] = useState<number | null>(null);
const [loadingSaldoBanco, setLoadingSaldoBanco] = useState(false);
const [erroSaldoBanco, setErroSaldoBanco] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [deletingTarefa, setDeletingTarefa] = useState<FechamentoTarefaItem | null>(null); const [deletingTarefa, setDeletingTarefa] = useState<FechamentoTarefaItem | null>(null);
const [isReprocessAsanaOpen, setIsReprocessAsanaOpen] = useState(false); const [isReprocessAsanaOpen, setIsReprocessAsanaOpen] = useState(false);
@@ -183,6 +229,16 @@ export default function FechamentoDetalhes() {
const requerMotivoAjuste = Number.isFinite(bancoCalculado) && Math.abs(bancoCalculado) > 0.0001; const requerMotivoAjuste = Number.isFinite(bancoCalculado) && Math.abs(bancoCalculado) > 0.0001;
const pontuacaoTotalLabel = toPontuacaoInput(totais.pontos); const pontuacaoTotalLabel = toPontuacaoInput(totais.pontos);
const isValorEditado = pontuacaoPagaInput.trim() !== pontuacaoTotalLabel; 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 todasTarefasRevisadas = tarefas.length > 0 && tarefas.every((tarefa) => tarefa.estaRevisada);
const competenciasDestino = useMemo( const competenciasDestino = useMemo(
() => competenciasAbertas.filter((item) => item.id !== competenciaId), () => competenciasAbertas.filter((item) => item.id !== competenciaId),
@@ -227,6 +283,8 @@ export default function FechamentoDetalhes() {
setFechamentoStatus(current.status); setFechamentoStatus(current.status);
setPontuacaoMeta(current.pontuacaoMeta); setPontuacaoMeta(current.pontuacaoMeta);
setCompetenciaStatus(competenciaAtual?.status ?? "em_aberto"); setCompetenciaStatus(competenciaAtual?.status ?? "em_aberto");
setParceiroId(current.parceiroId);
setParceiroNome(current.parceiroNome);
if (!competenciaId) { if (!competenciaId) {
setCompetenciaId(current.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 = () => { const toastBloqueioFechado = () => {
toast.error("Fechamento está fechado. Reabra para editar."); toast.error("Fechamento está fechado. Reabra para editar.");
}; };
@@ -262,6 +334,16 @@ export default function FechamentoDetalhes() {
} }
}, [isFechado]); }, [isFechado]);
useEffect(() => {
if (isConcluirOpen && parceiroId) {
void loadSaldoBanco(parceiroId);
}
if (!isConcluirOpen) {
setSaldoBancoAtual(null);
setErroSaldoBanco(false);
}
}, [isConcluirOpen, parceiroId]);
const handleToggleAprovada = async (tarefa: FechamentoTarefaItem, approved: boolean) => { const handleToggleAprovada = async (tarefa: FechamentoTarefaItem, approved: boolean) => {
if (isCompetenciaConcluida) { if (isCompetenciaConcluida) {
toastBloqueioCompetenciaConcluida(); toastBloqueioCompetenciaConcluida();
@@ -417,7 +499,7 @@ export default function FechamentoDetalhes() {
pontuacaoPaga: pontuacaoPagaRaw, pontuacaoPaga: pontuacaoPagaRaw,
motivoAjuste: motivoAjuste.trim() || undefined, 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"); setFechamentoStatus("fechado");
navigate(`/fechamento/competencias/${data.competenciaId}`); navigate(`/fechamento/competencias/${data.competenciaId}`);
} catch (error) { } catch (error) {
@@ -437,11 +519,15 @@ export default function FechamentoDetalhes() {
toast.error("Não foi possível identificar o usuário para reabertura."); toast.error("Não foi possível identificar o usuário para reabertura.");
return; return;
} }
if (!motivoReabertura.trim()) {
toast.error("Informe o motivo da reabertura do fechamento.");
return;
}
try { try {
setReabrindo(true); setReabrindo(true);
const data = await fechamentoFechamentosService.reabrirFechamento(fechamentoId, { const data = await fechamentoFechamentosService.reabrirFechamento(fechamentoId, {
reabertoPorId: me.id, reabertoPorId: me.id,
motivo: motivoReabertura, motivo: motivoReabertura.trim(),
}); });
toast.success("Fechamento reaberto com sucesso."); toast.success("Fechamento reaberto com sucesso.");
setFechamentoStatus(data.status); setFechamentoStatus(data.status);
@@ -1082,93 +1168,214 @@ export default function FechamentoDetalhes() {
</Dialog> </Dialog>
<Dialog open={isConcluirOpen} onOpenChange={setIsConcluirOpen}> <Dialog open={isConcluirOpen} onOpenChange={setIsConcluirOpen}>
<DialogContent className="sm:max-w-lg"> <DialogContent className="gap-0 overflow-hidden p-0 sm:max-w-2xl">
<DialogHeader> <div className="border-b border-border bg-muted/40 px-6 pb-4 pt-6 sm:px-8">
<DialogTitle>Concluir fechamento</DialogTitle> <DialogHeader className="space-y-1 text-left">
<DialogDescription> <DialogTitle className="text-xl font-semibold tracking-tight">Concluir fechamento</DialogTitle>
Revise os totais e confirme a pontuação paga para concluir este fechamento. <DialogDescription className="text-sm text-muted-foreground">
Informe a pontuação paga e confirme.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{parceiroNome ? (
<div className="space-y-4"> <div className="mt-4 flex items-center gap-2 rounded-lg border border-border/80 bg-background/80 px-3 py-2.5">
<div className="grid grid-cols-2 gap-3"> <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted">
<div className="rounded-lg border bg-card p-3"> <User className="h-4 w-4 text-muted-foreground" aria-hidden />
<p className="text-xs text-muted-foreground">Aprovada</p>
<p className="text-2xl font-bold">{formatPontos(totais.pontos)}</p>
</div> </div>
<div className="rounded-lg border bg-card p-3"> <div className="min-w-0">
<p className="text-xs text-muted-foreground">Meta</p> <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Parceiro</p>
<p className="text-2xl font-bold">{formatPontos(Number(pontuacaoMeta ?? 0))}</p> <p className="truncate text-sm font-semibold text-foreground">{parceiroNome}</p>
</div> </div>
<div className="rounded-lg border bg-card p-3">
<p className="text-xs text-muted-foreground">Diferença</p>
<p className={`text-2xl font-bold ${diferencaParaMeta >= 0 ? "text-emerald-600" : "text-red-600"}`}>
{formatPontos(diferencaParaMeta)}
</p>
</div>
<div className="rounded-lg border bg-card p-3">
<p className="text-xs text-muted-foreground">Banco de Pontos</p>
<p className={`text-2xl font-bold ${bancoCalculado >= 0 ? "text-emerald-600" : "text-red-600"}`}>
{formatPontos(bancoCalculado)}
</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="outline"
size="sm"
className="h-8"
onClick={() => setPontuacaoPagaInput(pontuacaoTotalLabel)}
>
Pagar total aprovado
</Button>
<Button
type="button"
variant="outline"
size="sm"
className="h-8"
onClick={() => setPontuacaoPagaInput(String(pontuacaoMeta ?? 0))}
>
Pagar meta
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="pontuacao-paga">Pontuação paga</Label>
{isValorEditado ? (
<div>
<button
type="button"
className="text-xs text-muted-foreground underline underline-offset-2 transition hover:text-foreground"
onClick={() => setPontuacaoPagaInput(pontuacaoTotalLabel)}
>
Usar valor total
</button>
</div> </div>
) : null} ) : null}
</div>
<div className="max-h-[min(75vh,36rem)] space-y-0 overflow-y-auto px-6 py-5 sm:px-8">
<div className="grid gap-4 md:grid-cols-2 md:gap-5">
<section className="rounded-xl border border-border bg-card p-4 shadow-sm">
<div className="mb-3 flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-muted">
<ListChecks className="h-4 w-4 text-muted-foreground" aria-hidden />
</div>
<h3 className="text-sm font-semibold text-foreground">Este mês</h3>
</div>
<dl className="space-y-0">
<div className="flex items-baseline justify-between gap-4 border-b border-border/60 py-2.5 first:pt-0">
<dt className="text-sm text-muted-foreground">Aprovados</dt>
<dd className="text-right text-lg font-semibold tabular-nums tracking-tight text-foreground">
{formatPontos(totais.pontos)}
</dd>
</div>
<div className="flex items-baseline justify-between gap-4 border-b border-border/60 py-2.5">
<dt className="text-sm text-muted-foreground">Meta</dt>
<dd className="text-right text-lg font-semibold tabular-nums tracking-tight text-foreground">
{formatPontos(Number(pontuacaoMeta ?? 0))}
</dd>
</div>
<div className="flex items-baseline justify-between gap-4 py-2.5 last:pb-0">
<dt className="text-sm text-muted-foreground">Diferença</dt>
<dd
className={`text-right text-lg font-semibold tabular-nums tracking-tight ${
diferencaParaMeta >= 0 ? "text-emerald-600" : "text-red-600"
}`}
>
{diferencaParaMeta >= 0 ? "+" : ""}
{formatPontos(diferencaParaMeta)}
</dd>
</div>
</dl>
</section>
<section className="rounded-xl border border-primary/20 bg-primary/[0.06] p-4">
<div className="mb-3 flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-background shadow-sm">
<Wallet className="h-4 w-4 text-primary" aria-hidden />
</div>
<h3 className="text-sm font-semibold text-foreground">Banco de pontos</h3>
</div>
<dl className="space-y-0">
<div className="flex items-baseline justify-between gap-4 border-b border-primary/10 py-2.5 first:pt-0">
<dt className="text-sm text-muted-foreground">Saldo hoje</dt>
<dd className="text-right">
{loadingSaldoBanco ? (
<Skeleton className="ml-auto h-7 w-24 rounded-md" />
) : erroSaldoBanco ? (
<div className="flex flex-col items-end gap-1">
<span className="text-sm font-medium text-destructive">Não foi possível carregar</span>
{parceiroId ? (
<button
type="button"
className="text-xs font-medium text-primary underline-offset-2 hover:underline"
onClick={() => void loadSaldoBanco(parceiroId)}
>
Tentar de novo
</button>
) : null}
</div>
) : (
<span
className={`text-lg font-semibold tabular-nums tracking-tight ${corSaldoPontos(saldoBancoAtual ?? 0)}`}
>
{formatPontosSaldoModal(saldoBancoAtual ?? 0)}
</span>
)}
</dd>
</div>
<div className="flex items-baseline justify-between gap-4 border-b border-primary/10 py-2.5">
<dt className="text-sm text-muted-foreground">Movimento</dt>
<dd
className={`text-right text-lg font-semibold tabular-nums tracking-tight ${corSaldoPontos(bancoCalculado)}`}
>
{bancoCalculado > SALDO_PONTOS_EPS ? "+" : ""}
{formatPontosSaldoModal(bancoCalculado)}
</dd>
</div>
<div className="flex items-baseline justify-between gap-4 py-2.5 last:pb-0">
<dt className="text-sm font-medium text-foreground">Depois</dt>
<dd className="text-right">
{saldoBancoPosFechamento == null ? (
<span className="text-lg font-semibold tabular-nums text-muted-foreground"></span>
) : (
<span
className={`text-lg font-semibold tabular-nums tracking-tight ${corSaldoPontos(saldoBancoPosFechamento)}`}
>
{formatPontosSaldoModal(saldoBancoPosFechamento)}
</span>
)}
</dd>
</div>
</dl>
</section>
</div>
<Separator className="my-5" />
<div className="space-y-3">
<Label htmlFor="pontuacao-paga" className="text-sm font-semibold">
Pontuação paga
</Label>
<Input <Input
id="pontuacao-paga" id="pontuacao-paga"
type="text" type="text"
inputMode="decimal" inputMode="decimal"
className="h-11 text-base font-medium tabular-nums"
value={pontuacaoPagaInput} value={pontuacaoPagaInput}
onChange={(e) => setPontuacaoPagaInput(e.target.value)} onChange={(e) => setPontuacaoPagaInput(e.target.value)}
placeholder="Ex.: 10,5" placeholder="Ex.: 10,5"
/> />
{isValorEditado ? (
<button
type="button"
className="text-xs font-medium text-primary underline-offset-2 hover:underline"
onClick={() => setPontuacaoPagaInput(pontuacaoTotalLabel)}
>
Voltar para o total aprovado ({formatPontos(totais.pontos)})
</button>
) : null}
<div className="rounded-lg border border-dashed border-border bg-muted/30 p-3">
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<Button
type="button"
variant="secondary"
size="sm"
className="h-9 justify-center font-normal"
onClick={() => setPontuacaoPagaInput(pontuacaoTotalLabel)}
>
Igual aos aprovados
</Button>
<Button
type="button"
variant="secondary"
size="sm"
className="h-9 justify-center font-normal"
onClick={() => setPontuacaoPagaInput(String(pontuacaoMeta ?? 0))}
>
Igual à meta
</Button>
{dividaParaQuitar > 0 ? (
<Button
type="button"
variant="secondary"
size="sm"
className="h-9 justify-center border border-red-200 bg-red-50 font-normal text-red-800 hover:bg-red-100 dark:border-red-900/50 dark:bg-red-950/40 dark:text-red-200 dark:hover:bg-red-950/60"
title={`${formatPontos(sugestaoQuitarDivida)} pontos — aprovados menos a dívida; zera o banco`}
onClick={() => setPontuacaoPagaInput(toPontuacaoInput(sugestaoQuitarDivida))}
>
Quitar dívida ({formatPontos(dividaParaQuitar)})
</Button>
) : null}
{saldoPositivoDisponivel > 0 ? (
<Button
type="button"
variant="secondary"
size="sm"
className="h-9 justify-center border border-emerald-200 bg-emerald-50 font-normal text-emerald-900 hover:bg-emerald-100 dark:border-emerald-900/50 dark:bg-emerald-950/40 dark:text-emerald-100 dark:hover:bg-emerald-950/60"
title={`${formatPontos(sugestaoUsarSaldoPositivo)} pontos — aprovados + saldo; zera o banco`}
onClick={() => setPontuacaoPagaInput(toPontuacaoInput(sugestaoUsarSaldoPositivo))}
>
Usar saldo positivo ({formatPontos(saldoPositivoDisponivel)})
</Button>
) : null}
</div> </div>
</div>
</div>
{requerMotivoAjuste ? ( {requerMotivoAjuste ? (
<div className="space-y-2"> <div className="mt-5 space-y-2 rounded-lg border border-amber-200/80 bg-amber-50/80 p-3 dark:border-amber-900/40 dark:bg-amber-950/25">
<Label htmlFor="motivo-ajuste">Motivo do ajuste *</Label> <Label htmlFor="motivo-ajuste" className="text-sm font-semibold text-amber-950 dark:text-amber-100">
Motivo do ajuste *
</Label>
<Input <Input
id="motivo-ajuste" id="motivo-ajuste"
value={motivoAjuste} value={motivoAjuste}
onChange={(e) => setMotivoAjuste(e.target.value)} onChange={(e) => setMotivoAjuste(e.target.value)}
placeholder="Ex.: pagamento parcial acordado com o parceiro" placeholder="Ex.: pagamento parcial acordado com o parceiro"
className="bg-background"
/> />
</div> </div>
) : null} ) : null}
</div> </div>
<DialogFooter> <DialogFooter className="gap-2 border-t border-border bg-muted/30 px-6 py-4 sm:justify-end sm:px-8">
<Button <Button
variant="outline" variant="outline"
className="hover:bg-muted/60 hover:text-foreground" className="hover:bg-muted/60 hover:text-foreground"
@@ -1180,15 +1387,15 @@ export default function FechamentoDetalhes() {
<Button <Button
onClick={() => void handleConcluirFechamento()} onClick={() => void handleConcluirFechamento()}
disabled={concluindo || isReadonly} disabled={concluindo || isReadonly}
className="min-w-[172px] shadow-lg hover:shadow-primary/50" className="min-w-[160px] shadow-md hover:shadow-primary/40"
> >
{concluindo ? ( {concluindo ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Concluindo... Concluindo...
</> </>
) : ( ) : (
"Confirmar conclusão" "Concluir"
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -1204,12 +1411,13 @@ export default function FechamentoDetalhes() {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="motivo-reabertura">Motivo (opcional)</Label> <Label htmlFor="motivo-reabertura">Motivo *</Label>
<Input <Input
id="motivo-reabertura" id="motivo-reabertura"
value={motivoReabertura} value={motivoReabertura}
onChange={(e) => setMotivoReabertura(e.target.value)} onChange={(e) => setMotivoReabertura(e.target.value)}
placeholder="Ex.: ajuste após revisão financeira" placeholder="Ex.: ajuste após revisão financeira"
required
/> />
</div> </div>
<DialogFooter> <DialogFooter>
@@ -1221,7 +1429,10 @@ export default function FechamentoDetalhes() {
> >
Cancelar Cancelar
</Button> </Button>
<Button onClick={() => void handleReabrirFechamento()} disabled={reabrindo}> <Button
onClick={() => void handleReabrirFechamento()}
disabled={reabrindo || !motivoReabertura.trim()}
>
{reabrindo ? "Reabrindo..." : "Confirmar reabertura"} {reabrindo ? "Reabrindo..." : "Confirmar reabertura"}
</Button> </Button>
</DialogFooter> </DialogFooter>
+2 -2
View File
@@ -159,14 +159,14 @@ class FechamentoCompetenciasService {
async reabrirCompetencia( async reabrirCompetencia(
competenciaId: string, competenciaId: string,
reabertoPorId: string, reabertoPorId: string,
motivo?: string, motivo: string,
): Promise<ReabrirCompetenciaResponse["data"]> { ): Promise<ReabrirCompetenciaResponse["data"]> {
try { try {
const headers = await buildCommanderHeaders(); const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl(); const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<ReabrirCompetenciaResponse>( const response = await axios.post<ReabrirCompetenciaResponse>(
`${baseUrl}/competencias/${competenciaId}/reabrir`, `${baseUrl}/competencias/${competenciaId}/reabrir`,
{ reaberto_por_id: reabertoPorId, motivo }, { reaberto_por_id: reabertoPorId, motivo: motivo.trim() },
{ headers }, { headers },
); );
return response.data.data; return response.data.data;
+2 -2
View File
@@ -277,7 +277,7 @@ class FechamentoFechamentosService {
async reabrirFechamento( async reabrirFechamento(
fechamentoId: string, fechamentoId: string,
input: { reabertoPorId: string; motivo?: string }, input: { reabertoPorId: string; motivo: string },
): Promise<FechamentoReabertoResponse> { ): Promise<FechamentoReabertoResponse> {
try { try {
const headers = await buildCommanderHeaders(); const headers = await buildCommanderHeaders();
@@ -286,7 +286,7 @@ class FechamentoFechamentosService {
`${baseUrl}/fechamentos/${fechamentoId}/reabrir`, `${baseUrl}/fechamentos/${fechamentoId}/reabrir`,
{ {
reaberto_por_id: input.reabertoPorId, reaberto_por_id: input.reabertoPorId,
...(input.motivo?.trim() ? { motivo: input.motivo.trim() } : {}), motivo: input.motivo.trim(),
}, },
{ headers }, { headers },
); );