novas features, geracao pdf, regras de usuarios, novo perfil de acesso
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
ListChecks,
|
||||
Loader2,
|
||||
Pencil,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -30,6 +33,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
@@ -103,8 +107,55 @@ function sanitizePontuacaoLancamentoDigitando(raw: string): string {
|
||||
return intPart + sep + fracStr;
|
||||
}
|
||||
|
||||
/** Valor em R$ no modal de lançamento: dígitos + um separador decimal, até 2 casas (pt-BR). */
|
||||
function sanitizeValorRealLancamentoDigitando(raw: string): string {
|
||||
const cleaned = raw.replace(/[^\d.,]/g, "");
|
||||
if (!cleaned) return "";
|
||||
|
||||
let int = "";
|
||||
let frac = "";
|
||||
let sawSeparator = false;
|
||||
|
||||
for (const ch of cleaned) {
|
||||
if (ch === "," || ch === ".") {
|
||||
if (!sawSeparator) sawSeparator = true;
|
||||
continue;
|
||||
}
|
||||
if (!sawSeparator) int += ch;
|
||||
else if (frac.length < 2) frac += ch;
|
||||
}
|
||||
|
||||
if (!sawSeparator) return int;
|
||||
return frac.length > 0 ? `${int},${frac}` : `${int},`;
|
||||
}
|
||||
|
||||
function parseValorRealMonetarioInput(raw: string): number {
|
||||
let t = raw.trim().replace(/\s/g, "").replace(/R\$\s?/gi, "");
|
||||
if (!t) return Number.NaN;
|
||||
if (t.includes(",") && t.includes(".")) {
|
||||
t = t.replace(/\./g, "").replace(",", ".");
|
||||
} else if (t.includes(",")) {
|
||||
t = t.replace(",", ".");
|
||||
}
|
||||
return Number(t);
|
||||
}
|
||||
|
||||
/** Pontuação: no máximo 2 casas decimais, sempre para cima (ex.: 1,1111 → 1,12). Valores já em centésimos exatos (ex.: 1,00) não são inflados por erro de float. */
|
||||
function pontuacaoParaCimaAte2Casas(value: number): number {
|
||||
if (!Number.isFinite(value)) return value;
|
||||
if (value === 0) return 0;
|
||||
const sign = value < 0 ? -1 : 1;
|
||||
const m = Math.abs(value);
|
||||
const scaled = m * 100;
|
||||
const nearest = Math.round(scaled);
|
||||
const n =
|
||||
Math.abs(scaled - nearest) < 1e-7 ? nearest : Math.ceil(scaled - 1e-9);
|
||||
return sign * (n / 100);
|
||||
}
|
||||
|
||||
function toPontuacaoInput(value: number): string {
|
||||
const rounded = Math.round((value + Number.EPSILON) * 100) / 100;
|
||||
if (!Number.isFinite(value)) return "0";
|
||||
const rounded = pontuacaoParaCimaAte2Casas(value);
|
||||
return String(rounded);
|
||||
}
|
||||
|
||||
@@ -164,7 +215,7 @@ type FechamentoDetalhesLocationState = {
|
||||
export default function FechamentoDetalhes() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { me } = useAuthAccess();
|
||||
const { me, papel } = useAuthAccess();
|
||||
const { id: fechamentoId = "" } = useParams();
|
||||
const initialState = (location.state as FechamentoDetalhesLocationState | null) ?? null;
|
||||
const [competenciaId, setCompetenciaId] = useState(initialState?.competenciaId ?? "");
|
||||
@@ -178,6 +229,7 @@ export default function FechamentoDetalhes() {
|
||||
const [savingLancamento, setSavingLancamento] = useState(false);
|
||||
const [isConcluirOpen, setIsConcluirOpen] = useState(false);
|
||||
const [concluindo, setConcluindo] = useState(false);
|
||||
const [exportandoPdf, setExportandoPdf] = useState(false);
|
||||
const [pontuacaoPagaInput, setPontuacaoPagaInput] = useState("");
|
||||
const [motivoAjuste, setMotivoAjuste] = useState("");
|
||||
const [isReabrirOpen, setIsReabrirOpen] = useState(false);
|
||||
@@ -185,10 +237,13 @@ export default function FechamentoDetalhes() {
|
||||
const [motivoReabertura, setMotivoReabertura] = useState("");
|
||||
const [lancamentoTipo, setLancamentoTipo] = useState<"bonus" | "desconto">("bonus");
|
||||
const [lancamentoDescricao, setLancamentoDescricao] = useState("");
|
||||
const [lancamentoModo, setLancamentoModo] = useState<"pontos" | "real">("pontos");
|
||||
const [lancamentoPontuacao, setLancamentoPontuacao] = useState("0");
|
||||
const [lancamentoValorReal, setLancamentoValorReal] = useState("");
|
||||
const [pontuacaoMeta, setPontuacaoMeta] = useState<number | null>(null);
|
||||
const [parceiroId, setParceiroId] = useState<string | null>(null);
|
||||
const [parceiroNome, setParceiroNome] = useState<string | null>(null);
|
||||
const [parceiroFator, setParceiroFator] = useState<number | null>(null);
|
||||
const [saldoBancoAtual, setSaldoBancoAtual] = useState<number | null>(null);
|
||||
const [loadingSaldoBanco, setLoadingSaldoBanco] = useState(false);
|
||||
const [erroSaldoBanco, setErroSaldoBanco] = useState(false);
|
||||
@@ -224,7 +279,31 @@ export default function FechamentoDetalhes() {
|
||||
const isFechado = fechamentoStatus === "fechado";
|
||||
const isCompetenciaConcluida = competenciaStatus === "concluido";
|
||||
const isReadonly = Boolean(initialState?.readonlyView) || isFechado || isCompetenciaConcluida;
|
||||
const isAdmin = me?.papel === "admin";
|
||||
const caps = me?.capabilities;
|
||||
const podeTarefaEditarRevisao = caps?.fechamentos.tarefaEditarRevisao ?? false;
|
||||
const podeTarefaLancarAjuste = caps?.fechamentos.tarefaLancarAjuste ?? false;
|
||||
const verFatorParceiro = caps?.parceiros.verFator ?? false;
|
||||
const fatorParceiroValidoParaReal =
|
||||
parceiroFator != null && Number.isFinite(parceiroFator) && parceiroFator > 0;
|
||||
/** Quem vê o fator na API sabe se está cadastrado; sem fator não dá para lançar em Real no front. */
|
||||
const lancamentoEmRealBloqueado = verFatorParceiro && !fatorParceiroValidoParaReal;
|
||||
const podeReprocessarAsana = caps?.fechamentos.reprocessarAsana ?? false;
|
||||
const podeConcluirFechamento = caps?.fechamentos.concluir ?? false;
|
||||
const podeReabrirFechamento = caps?.fechamentos.reabrir ?? false;
|
||||
const podeExportarPdf = caps?.fechamentos.exportarPdf ?? false;
|
||||
const supervisorPodeExportarPdf =
|
||||
podeExportarPdf &&
|
||||
(papel !== "supervisor" || Boolean(me?.parceiroId && parceiroId != null && parceiroId === me.parceiroId));
|
||||
const podeAlterarCompetenciaTarefa = caps?.fechamentos.tarefaAlterarCompetencia ?? false;
|
||||
const podeExcluirLancamentoManual = caps?.fechamentos.tarefaExcluirManual ?? false;
|
||||
const bloqueioEdicaoTarefa = isReadonly || !podeTarefaEditarRevisao;
|
||||
|
||||
const lancamentoPreviewPontos = useMemo(() => {
|
||||
if (lancamentoModo !== "real" || parceiroFator == null || !(parceiroFator > 0)) return null;
|
||||
const valor = parseValorRealMonetarioInput(lancamentoValorReal.trim());
|
||||
if (!Number.isFinite(valor) || valor <= 0) return null;
|
||||
return pontuacaoParaCimaAte2Casas(valor / (parceiroFator * 3));
|
||||
}, [lancamentoModo, parceiroFator, lancamentoValorReal]);
|
||||
|
||||
const totais = useMemo(() => {
|
||||
const aprovadas = tarefas.filter((t) => t.estaRevisada);
|
||||
@@ -322,6 +401,10 @@ export default function FechamentoDetalhes() {
|
||||
setCompetenciaStatus(competenciaAtual?.status ?? "em_aberto");
|
||||
setParceiroId(current.parceiroId);
|
||||
setParceiroNome(current.parceiroNome);
|
||||
const f = current.parceiroFator;
|
||||
setParceiroFator(
|
||||
f !== undefined && f !== null && Number.isFinite(Number(f)) && Number(f) > 0 ? Number(f) : null,
|
||||
);
|
||||
if (!competenciaId) {
|
||||
setCompetenciaId(current.competenciaId);
|
||||
}
|
||||
@@ -371,6 +454,11 @@ export default function FechamentoDetalhes() {
|
||||
}
|
||||
}, [isFechado]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLancamentoOpen || !lancamentoEmRealBloqueado) return;
|
||||
setLancamentoModo("pontos");
|
||||
}, [isLancamentoOpen, lancamentoEmRealBloqueado]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConcluirOpen && parceiroId) {
|
||||
void loadSaldoBanco(parceiroId);
|
||||
@@ -499,7 +587,9 @@ export default function FechamentoDetalhes() {
|
||||
const resetLancamentoForm = () => {
|
||||
setLancamentoTipo("bonus");
|
||||
setLancamentoDescricao("");
|
||||
setLancamentoModo("pontos");
|
||||
setLancamentoPontuacao("0");
|
||||
setLancamentoValorReal("");
|
||||
};
|
||||
|
||||
const handleSalvarLancamento = async () => {
|
||||
@@ -512,27 +602,54 @@ export default function FechamentoDetalhes() {
|
||||
return;
|
||||
}
|
||||
const descricao = lancamentoDescricao.trim();
|
||||
const pontuacao = (() => {
|
||||
const n = parsePontuacaoInput(lancamentoPontuacao);
|
||||
if (!Number.isFinite(n)) return Number.NaN;
|
||||
return Math.round(n * 100) / 100;
|
||||
})();
|
||||
if (!descricao) {
|
||||
toast.error("Informe a descrição do lançamento.");
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(pontuacao) || pontuacao <= 0) {
|
||||
toast.error("Informe uma pontuação válida maior que zero.");
|
||||
return;
|
||||
|
||||
if (lancamentoModo === "real") {
|
||||
if (lancamentoEmRealBloqueado) {
|
||||
toast.error("Primeiro cadastre o fator do parceiro para poder lançar em Real.");
|
||||
return;
|
||||
}
|
||||
const valorNum = parseValorRealMonetarioInput(lancamentoValorReal.trim());
|
||||
if (!Number.isFinite(valorNum) || valorNum <= 0) {
|
||||
toast.error("Informe um valor em real válido maior que zero.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const pontuacao = (() => {
|
||||
const n = parsePontuacaoInput(lancamentoPontuacao);
|
||||
if (!Number.isFinite(n)) return Number.NaN;
|
||||
return pontuacaoParaCimaAte2Casas(n);
|
||||
})();
|
||||
if (!Number.isFinite(pontuacao) || pontuacao <= 0) {
|
||||
toast.error("Informe uma pontuação válida maior que zero.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setSavingLancamento(true);
|
||||
await fechamentoFechamentosService.criarLancamento(fechamentoId, {
|
||||
tipo: lancamentoTipo,
|
||||
descricao,
|
||||
pontuacao,
|
||||
});
|
||||
if (lancamentoModo === "real") {
|
||||
const valorNum = parseValorRealMonetarioInput(lancamentoValorReal.trim());
|
||||
await fechamentoFechamentosService.criarLancamento(fechamentoId, {
|
||||
tipo: lancamentoTipo,
|
||||
descricao,
|
||||
valorReal: valorNum,
|
||||
});
|
||||
} else {
|
||||
const pontuacao = (() => {
|
||||
const n = parsePontuacaoInput(lancamentoPontuacao);
|
||||
if (!Number.isFinite(n)) return Number.NaN;
|
||||
return pontuacaoParaCimaAte2Casas(n);
|
||||
})();
|
||||
await fechamentoFechamentosService.criarLancamento(fechamentoId, {
|
||||
tipo: lancamentoTipo,
|
||||
descricao,
|
||||
pontuacao,
|
||||
});
|
||||
}
|
||||
toast.success("Lançamento incluído com sucesso.");
|
||||
setIsLancamentoOpen(false);
|
||||
resetLancamentoForm();
|
||||
@@ -559,6 +676,29 @@ export default function FechamentoDetalhes() {
|
||||
setIsConcluirOpen(true);
|
||||
};
|
||||
|
||||
const handleExportarPdf = async () => {
|
||||
if (!fechamentoId || !isFechado) return;
|
||||
try {
|
||||
setExportandoPdf(true);
|
||||
const { buffer, filename } = await fechamentoFechamentosService.exportarPdf(fechamentoId);
|
||||
const blob = new Blob([buffer], { type: "application/pdf" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename ?? `fechamento-${fechamentoId}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success("PDF exportado com sucesso.");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Erro ao exportar PDF.";
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setExportandoPdf(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConcluirFechamento = async () => {
|
||||
if (isCompetenciaConcluida) {
|
||||
toastBloqueioCompetenciaConcluida();
|
||||
@@ -590,6 +730,25 @@ export default function FechamentoDetalhes() {
|
||||
});
|
||||
toast.success(`Fechamento concluído. Banco de pontos: ${formatPontosAte2Casas(data.pontuacaoBanco)}.`);
|
||||
setFechamentoStatus("fechado");
|
||||
if (supervisorPodeExportarPdf) {
|
||||
try {
|
||||
const { buffer, filename } = await fechamentoFechamentosService.exportarPdf(fechamentoId);
|
||||
const blob = new Blob([buffer], { type: "application/pdf" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename ?? `fechamento-${fechamentoId}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success("PDF do fechamento baixado.");
|
||||
} catch {
|
||||
toast.warning(
|
||||
"Fechamento concluído, mas o PDF não pôde ser gerado automaticamente. Use Exportar PDF na tela.",
|
||||
);
|
||||
}
|
||||
}
|
||||
navigate(`/intelligence-score/competencias/${data.competenciaId}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Erro ao concluir fechamento.";
|
||||
@@ -705,8 +864,8 @@ export default function FechamentoDetalhes() {
|
||||
};
|
||||
|
||||
const openAlterarCompetencia = async (tarefa: FechamentoTarefaItem) => {
|
||||
if (!isAdmin) {
|
||||
toast.error("Apenas administradores podem alterar competência.");
|
||||
if (!podeAlterarCompetenciaTarefa) {
|
||||
toast.error("Sem permissão para alterar competência da tarefa.");
|
||||
return;
|
||||
}
|
||||
if (isCompetenciaConcluida) {
|
||||
@@ -768,7 +927,7 @@ export default function FechamentoDetalhes() {
|
||||
}
|
||||
const isManual = editingTarefa.tipo === "bonus" || editingTarefa.tipo === "desconto";
|
||||
const descricao = edicaoDescricao.trim();
|
||||
const pontuacao = parsePontuacaoInput(edicaoPontuacao);
|
||||
const pontuacao = pontuacaoParaCimaAte2Casas(parsePontuacaoInput(edicaoPontuacao));
|
||||
if (!descricao) {
|
||||
toast.error("Descrição é obrigatória.");
|
||||
return;
|
||||
@@ -866,14 +1025,17 @@ export default function FechamentoDetalhes() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsLancamentoOpen(true)}
|
||||
disabled={isReadonly}
|
||||
onClick={() => {
|
||||
resetLancamentoForm();
|
||||
setIsLancamentoOpen(true);
|
||||
}}
|
||||
disabled={isReadonly || !podeTarefaLancarAjuste}
|
||||
className="min-w-[152px] hover:bg-muted/60 hover:text-foreground"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Fazer lançamento
|
||||
</Button>
|
||||
{!isReadonly ? (
|
||||
{!isReadonly && podeReprocessarAsana ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -886,17 +1048,33 @@ export default function FechamentoDetalhes() {
|
||||
</Button>
|
||||
) : null}
|
||||
{isFechado ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsReabrirOpen(true)}
|
||||
disabled={reabrindo || isCompetenciaConcluida}
|
||||
className="min-w-[152px]"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
Reabrir fechamento
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{supervisorPodeExportarPdf ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void handleExportarPdf()}
|
||||
disabled={exportandoPdf || isCompetenciaConcluida}
|
||||
className="min-w-[152px] hover:bg-muted/60 hover:text-foreground"
|
||||
>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
{exportandoPdf ? "Gerando PDF..." : "Exportar PDF"}
|
||||
</Button>
|
||||
) : null}
|
||||
{podeReabrirFechamento ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsReabrirOpen(true)}
|
||||
disabled={reabrindo || isCompetenciaConcluida}
|
||||
className="min-w-[152px]"
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
Reabrir fechamento
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
) : podeConcluirFechamento ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
@@ -907,7 +1085,7 @@ export default function FechamentoDetalhes() {
|
||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||
Concluir fechamento
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -980,7 +1158,7 @@ export default function FechamentoDetalhes() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void handleToggleTodasAprovadas()}
|
||||
disabled={isReadonly || bulkUpdatingRevisao}
|
||||
disabled={bloqueioEdicaoTarefa || bulkUpdatingRevisao}
|
||||
>
|
||||
{bulkUpdatingRevisao ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
@@ -1070,7 +1248,7 @@ export default function FechamentoDetalhes() {
|
||||
<Checkbox
|
||||
checked={tarefa.estaRevisada}
|
||||
onCheckedChange={(checked) => void handleToggleAprovada(tarefa, Boolean(checked))}
|
||||
disabled={isReadonly || bulkUpdatingRevisao}
|
||||
disabled={bloqueioEdicaoTarefa || bulkUpdatingRevisao}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -1110,18 +1288,18 @@ export default function FechamentoDetalhes() {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => openEditarTarefa(tarefa)}
|
||||
disabled={isReadonly}
|
||||
disabled={bloqueioEdicaoTarefa}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
<span className="ml-1">Editar</span>
|
||||
</Button>
|
||||
{isAdmin && tarefa.tipo === "tarefa" ? (
|
||||
{podeAlterarCompetenciaTarefa && tarefa.tipo === "tarefa" ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => void openAlterarCompetencia(tarefa)}
|
||||
disabled={isReadonly || savingAlteracaoCompetencia}
|
||||
disabled={bloqueioEdicaoTarefa || savingAlteracaoCompetencia}
|
||||
>
|
||||
<Repeat className="h-4 w-4" />
|
||||
<span className="ml-1">Alterar competência</span>
|
||||
@@ -1134,7 +1312,7 @@ export default function FechamentoDetalhes() {
|
||||
variant="ghost"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => void handleExcluirLancamento(tarefa)}
|
||||
disabled={isReadonly || deletingTaskId === tarefa.id}
|
||||
disabled={bloqueioEdicaoTarefa || !podeExcluirLancamentoManual || deletingTaskId === tarefa.id}
|
||||
>
|
||||
{deletingTaskId === tarefa.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -1163,13 +1341,18 @@ export default function FechamentoDetalhes() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={isLancamentoOpen} onOpenChange={setIsLancamentoOpen}>
|
||||
<Dialog
|
||||
open={isLancamentoOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsLancamentoOpen(open);
|
||||
if (!open) {
|
||||
resetLancamentoForm();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Fazer lançamento</DialogTitle>
|
||||
<DialogDescription>
|
||||
Adicione uma bonificação ou desconto em pontuação para este fechamento.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -1190,17 +1373,76 @@ export default function FechamentoDetalhes() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lancamento-pontuacao">Pontuação</Label>
|
||||
<Input
|
||||
id="lancamento-pontuacao"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={lancamentoPontuacao}
|
||||
onChange={(e) => setLancamentoPontuacao(sanitizePontuacaoLancamentoDigitando(e.target.value))}
|
||||
placeholder="Ex.: 1,5"
|
||||
/>
|
||||
<Label>Forma do lançamento</Label>
|
||||
{lancamentoEmRealBloqueado ? (
|
||||
<Alert variant="destructive" className="py-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Primeiro cadastre o fator do parceiro para poder lançar em Real.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<RadioGroup
|
||||
value={lancamentoModo}
|
||||
onValueChange={(v) => setLancamentoModo(v as "pontos" | "real")}
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="pontos" id="lancamento-modo-pontos" />
|
||||
<Label htmlFor="lancamento-modo-pontos" className="cursor-pointer font-normal">
|
||||
Pontuação
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="real" id="lancamento-modo-real" disabled={lancamentoEmRealBloqueado} />
|
||||
<Label
|
||||
htmlFor="lancamento-modo-real"
|
||||
className={
|
||||
lancamentoEmRealBloqueado ? "cursor-not-allowed font-normal text-muted-foreground" : "cursor-pointer font-normal"
|
||||
}
|
||||
>
|
||||
Valor em real (R$)
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{lancamentoModo === "pontos" ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lancamento-pontuacao">Pontuação</Label>
|
||||
<Input
|
||||
id="lancamento-pontuacao"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={lancamentoPontuacao}
|
||||
onChange={(e) => setLancamentoPontuacao(sanitizePontuacaoLancamentoDigitando(e.target.value))}
|
||||
placeholder="Ex.: 1,5"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lancamento-valor-real">Valor (R$)</Label>
|
||||
<Input
|
||||
id="lancamento-valor-real"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
autoComplete="off"
|
||||
value={lancamentoValorReal}
|
||||
onChange={(e) => setLancamentoValorReal(sanitizeValorRealLancamentoDigitando(e.target.value))}
|
||||
placeholder="Ex.: 90,00"
|
||||
/>
|
||||
{lancamentoPreviewPontos != null ? (
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Pontuação estimada: {formatPontos(lancamentoPreviewPontos)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
A conversão é feita ao salvar com o fator cadastrado do parceiro (valor ÷ (fator × 3)).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lancamento-descricao">Descrição</Label>
|
||||
<Input
|
||||
@@ -1224,7 +1466,15 @@ export default function FechamentoDetalhes() {
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={() => void handleSalvarLancamento()} disabled={savingLancamento || isReadonly}>
|
||||
<Button
|
||||
onClick={() => void handleSalvarLancamento()}
|
||||
disabled={
|
||||
savingLancamento ||
|
||||
isReadonly ||
|
||||
!podeTarefaLancarAjuste ||
|
||||
(lancamentoModo === "real" && lancamentoEmRealBloqueado)
|
||||
}
|
||||
>
|
||||
{savingLancamento ? "Salvando..." : "Salvar lançamento"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -1531,7 +1781,7 @@ export default function FechamentoDetalhes() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void handleConcluirFechamento()}
|
||||
disabled={concluindo || isReadonly}
|
||||
disabled={concluindo || isReadonly || !podeConcluirFechamento}
|
||||
className="min-w-[160px] shadow-md hover:shadow-primary/40"
|
||||
>
|
||||
{concluindo ? (
|
||||
@@ -1717,7 +1967,7 @@ export default function FechamentoDetalhes() {
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={() => void handleSalvarEdicao()} disabled={savingEdicao || isReadonly}>
|
||||
<Button onClick={() => void handleSalvarEdicao()} disabled={savingEdicao || bloqueioEdicaoTarefa}>
|
||||
{savingEdicao ? "Salvando..." : "Salvar"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user