novas atualizacoes
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user