atualizacoes modulo fechamento

This commit is contained in:
Vitex Tecnologia
2026-04-26 22:54:56 -03:00
parent 7a01c6f97b
commit 6da067c641
29 changed files with 5973 additions and 3 deletions
@@ -0,0 +1,962 @@
import { useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, CheckCircle2, Clock3, ListChecks, Loader2, Pencil, Plus, RotateCcw, Target, Trash2, TrendingUp } from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useAuthAccess } from "@/contexts/AuthAccessContext";
import { fechamentoCompetenciasService } from "@/services/fechamento/competencias";
import { fechamentoFechamentosService, type FechamentoTarefaItem } from "@/services/fechamento/fechamentos";
function formatHoras(minutos: number | null): string {
if (!minutos || minutos <= 0) return "0h";
const horas = Math.floor(minutos / 60);
const mins = Math.round(minutos % 60);
if (horas === 0) return `${mins}min`;
if (mins === 0) return `${horas}h`;
return `${horas}h ${mins}min`;
}
function formatHorasResumo(minutos: number): string {
const horas = Math.floor(minutos / 60);
const mins = Math.round(minutos % 60);
if (horas === 0) return `${mins}min`;
if (mins === 0) return `${horas}h`;
return `${horas}h ${mins}min`;
}
function formatPontos(valor: number): string {
return valor.toLocaleString("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 4 });
}
function parsePontuacaoInput(value: string): number {
const normalized = value.trim().replace(/\s/g, "").replace(",", ".");
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : Number.NaN;
}
type FechamentoDetalhesLocationState = {
competenciaId?: string;
status?: "em_aberto" | "fechado";
};
export default function FechamentoDetalhes() {
const location = useLocation();
const navigate = useNavigate();
const { me } = useAuthAccess();
const { id: fechamentoId = "" } = useParams();
const initialState = (location.state as FechamentoDetalhesLocationState | null) ?? null;
const [competenciaId, setCompetenciaId] = useState(initialState?.competenciaId ?? "");
const [fechamentoStatus, setFechamentoStatus] = useState<"em_aberto" | "fechado">(initialState?.status ?? "em_aberto");
const [loading, setLoading] = useState(true);
const [tarefas, setTarefas] = useState<FechamentoTarefaItem[]>([]);
const [togglingTaskId, setTogglingTaskId] = useState<string | null>(null);
const [deletingTaskId, setDeletingTaskId] = useState<string | null>(null);
const [isLancamentoOpen, setIsLancamentoOpen] = useState(false);
const [savingLancamento, setSavingLancamento] = useState(false);
const [isConcluirOpen, setIsConcluirOpen] = useState(false);
const [concluindo, setConcluindo] = useState(false);
const [pontuacaoPagaInput, setPontuacaoPagaInput] = useState("");
const [motivoAjuste, setMotivoAjuste] = useState("");
const [isReabrirOpen, setIsReabrirOpen] = useState(false);
const [reabrindo, setReabrindo] = useState(false);
const [motivoReabertura, setMotivoReabertura] = useState("");
const [lancamentoTipo, setLancamentoTipo] = useState<"bonus" | "desconto">("bonus");
const [lancamentoDescricao, setLancamentoDescricao] = useState("");
const [lancamentoPontuacao, setLancamentoPontuacao] = useState("0");
const [pontuacaoMeta, setPontuacaoMeta] = useState<number | null>(null);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [deletingTarefa, setDeletingTarefa] = useState<FechamentoTarefaItem | null>(null);
const [isReprocessAsanaOpen, setIsReprocessAsanaOpen] = useState(false);
const [reprocessandoAsana, setReprocessandoAsana] = useState(false);
const [isEditarOpen, setIsEditarOpen] = useState(false);
const [savingEdicao, setSavingEdicao] = useState(false);
const [editingTarefa, setEditingTarefa] = useState<FechamentoTarefaItem | null>(null);
const [edicaoNumeroTicket, setEdicaoNumeroTicket] = useState("");
const [edicaoDescricao, setEdicaoDescricao] = useState("");
const [edicaoCliente, setEdicaoCliente] = useState("");
const [edicaoTempoMinutos, setEdicaoTempoMinutos] = useState("");
const [edicaoPontuacao, setEdicaoPontuacao] = useState("");
const isFechado = fechamentoStatus === "fechado";
const totais = useMemo(() => {
const aprovadas = tarefas.filter((t) => t.estaRevisada);
const pontos = aprovadas.reduce((acc, t) => acc + Number(t.pontuacao || 0), 0);
const minutos = aprovadas.reduce((acc, t) => acc + Number(t.tempoMinutos || 0), 0);
return {
pontos,
horas: formatHorasResumo(minutos),
aprovadas: aprovadas.length,
};
}, [tarefas]);
const pontuacaoPagaNumero = parsePontuacaoInput(pontuacaoPagaInput || "0");
const bancoCalculado = totais.pontos - pontuacaoPagaNumero;
const diferencaParaMeta = totais.pontos - Number(pontuacaoMeta ?? 0);
const diferencaPagamentoMeta = pontuacaoPagaNumero - Number(pontuacaoMeta ?? 0);
const requerMotivoAjuste = Number.isFinite(bancoCalculado) && Math.abs(bancoCalculado) > 0.0001;
const pontuacaoTotalLabel = String(totais.pontos);
const isValorEditado = pontuacaoPagaInput.trim() !== pontuacaoTotalLabel;
const loadTarefas = async () => {
try {
setLoading(true);
const data = await fechamentoFechamentosService.listarTarefas(fechamentoId);
setTarefas(data);
} catch (error) {
const message = error instanceof Error ? error.message : "Erro ao carregar detalhes do fechamento.";
toast.error(message);
setTarefas([]);
} finally {
setLoading(false);
}
};
const loadFechamentoStatus = async (currentCompetenciaId: string) => {
if (!currentCompetenciaId) return;
try {
const rows = await fechamentoCompetenciasService.listarFechamentosDaCompetencia(currentCompetenciaId);
const current = rows.find((row) => row.id === fechamentoId);
if (!current) return;
setFechamentoStatus(current.status);
setPontuacaoMeta(current.pontuacaoMeta);
if (!competenciaId) {
setCompetenciaId(current.competenciaId);
}
} catch {
// mantém status atual em caso de erro para evitar bloquear navegação
}
};
const toastBloqueioFechado = () => {
toast.error("Fechamento está fechado. Reabra para editar.");
};
useEffect(() => {
if (fechamentoId) {
void loadTarefas();
}
}, [fechamentoId]);
useEffect(() => {
if (competenciaId) {
void loadFechamentoStatus(competenciaId);
}
}, [competenciaId, fechamentoId]);
useEffect(() => {
if (isFechado) {
setIsConcluirOpen(false);
setIsLancamentoOpen(false);
}
}, [isFechado]);
const handleToggleAprovada = async (tarefa: FechamentoTarefaItem, approved: boolean) => {
if (isFechado) {
toastBloqueioFechado();
return;
}
const previous = tarefas;
setTogglingTaskId(tarefa.id);
setTarefas((prev) =>
prev.map((item) =>
item.id === tarefa.id
? {
...item,
estaRevisada: approved,
}
: item,
),
);
try {
if (!me?.id) {
throw new Error("Não foi possível identificar o usuário para registrar a edição.");
}
await fechamentoFechamentosService.patchTarefa(fechamentoId, tarefa.id, {
estaRevisada: approved,
editadoPorId: me.id,
});
} catch (error) {
setTarefas(previous);
const message = error instanceof Error ? error.message : "Erro ao atualizar aprovação da tarefa.";
toast.error(message);
} finally {
setTogglingTaskId(null);
}
};
const resetLancamentoForm = () => {
setLancamentoTipo("bonus");
setLancamentoDescricao("");
setLancamentoPontuacao("0");
};
const handleSalvarLancamento = async () => {
if (isFechado) {
toastBloqueioFechado();
return;
}
const descricao = lancamentoDescricao.trim();
const pontuacao = Number(lancamentoPontuacao);
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;
}
try {
setSavingLancamento(true);
await fechamentoFechamentosService.criarLancamento(fechamentoId, {
tipo: lancamentoTipo,
descricao,
pontuacao,
});
toast.success("Lançamento incluído com sucesso.");
setIsLancamentoOpen(false);
resetLancamentoForm();
await loadTarefas();
} catch (error) {
const message = error instanceof Error ? error.message : "Erro ao incluir lançamento.";
toast.error(message);
} finally {
setSavingLancamento(false);
}
};
const handleAbrirConcluir = () => {
if (isFechado) {
toastBloqueioFechado();
return;
}
setPontuacaoPagaInput(pontuacaoTotalLabel);
setMotivoAjuste("");
setIsConcluirOpen(true);
};
const handleConcluirFechamento = async () => {
if (isFechado) {
toastBloqueioFechado();
return;
}
const pontuacaoPagaRaw = parsePontuacaoInput(pontuacaoPagaInput);
if (!Number.isFinite(pontuacaoPagaRaw)) {
toast.error("Informe uma pontuação paga válida.");
return;
}
if (pontuacaoPagaRaw <= 0) {
toast.error("A pontuação paga deve ser maior que zero.");
return;
}
if (requerMotivoAjuste && !motivoAjuste.trim()) {
toast.error("Informe o motivo do ajuste quando houver diferença de saldo.");
return;
}
try {
setConcluindo(true);
const data = await fechamentoFechamentosService.concluirFechamento(fechamentoId, {
pontuacaoPaga: pontuacaoPagaRaw,
motivoAjuste: motivoAjuste.trim() || undefined,
});
toast.success(`Fechamento concluído. Banco de pontos: ${data.pontuacaoBanco}.`);
setFechamentoStatus("fechado");
navigate(`/fechamento-hgtx/competencias/${data.competenciaId}`);
} catch (error) {
const message = error instanceof Error ? error.message : "Erro ao concluir fechamento.";
toast.error(message);
if (competenciaId) {
await loadFechamentoStatus(competenciaId);
}
await loadTarefas();
} finally {
setConcluindo(false);
}
};
const handleReabrirFechamento = async () => {
if (!me?.id) {
toast.error("Não foi possível identificar o usuário para reabertura.");
return;
}
try {
setReabrindo(true);
const data = await fechamentoFechamentosService.reabrirFechamento(fechamentoId, {
reabertoPorId: me.id,
motivo: motivoReabertura,
});
toast.success("Fechamento reaberto com sucesso.");
setFechamentoStatus(data.status);
setIsReabrirOpen(false);
setMotivoReabertura("");
await loadTarefas();
if (data.competenciaId) {
setCompetenciaId(data.competenciaId);
} else if (competenciaId) {
await loadFechamentoStatus(competenciaId);
}
} catch (error) {
const message = error instanceof Error ? error.message : "Erro ao reabrir fechamento.";
toast.error(message);
if (competenciaId) {
await loadFechamentoStatus(competenciaId);
}
} finally {
setReabrindo(false);
}
};
const handleExcluirLancamento = (tarefa: FechamentoTarefaItem) => {
if (isFechado) {
toastBloqueioFechado();
return;
}
const isManual = tarefa.tipo === "bonus" || tarefa.tipo === "desconto";
if (!isManual) return;
setDeletingTarefa(tarefa);
setIsDeleteDialogOpen(true);
};
const confirmDeleteLancamento = async () => {
if (!deletingTarefa) return;
try {
setDeletingTaskId(deletingTarefa.id);
await fechamentoFechamentosService.excluirLancamento(fechamentoId, deletingTarefa.id);
toast.success("Lançamento excluído com sucesso.");
setIsDeleteDialogOpen(false);
setDeletingTarefa(null);
await loadTarefas();
} catch (error) {
const message = error instanceof Error ? error.message : "Erro ao excluir lançamento.";
toast.error(message);
} finally {
setDeletingTaskId(null);
}
};
const openEditarTarefa = (tarefa: FechamentoTarefaItem) => {
if (isFechado) {
toastBloqueioFechado();
return;
}
setEditingTarefa(tarefa);
setEdicaoNumeroTicket(tarefa.numeroTicket ?? "");
setEdicaoDescricao(tarefa.descricao ?? "");
setEdicaoCliente(tarefa.cliente ?? "");
setEdicaoTempoMinutos(tarefa.tempoMinutos != null ? String(tarefa.tempoMinutos) : "");
setEdicaoPontuacao(String(Number(tarefa.pontuacao ?? 0)));
setIsEditarOpen(true);
};
const handleSalvarEdicao = async () => {
if (!editingTarefa) return;
if (!me?.id) {
toast.error("Não foi possível identificar o usuário para registrar a edição.");
return;
}
const isManual = editingTarefa.tipo === "bonus" || editingTarefa.tipo === "desconto";
const descricao = edicaoDescricao.trim();
const pontuacao = parsePontuacaoInput(edicaoPontuacao);
if (!descricao) {
toast.error("Descrição é obrigatória.");
return;
}
if (!Number.isFinite(pontuacao) || pontuacao <= 0) {
toast.error("Pontuação deve ser maior que zero.");
return;
}
let tempoMinutos: number | null | undefined = undefined;
if (!isManual) {
const tempoRaw = edicaoTempoMinutos.trim();
if (tempoRaw.length > 0) {
const tempoParsed = Number(tempoRaw);
if (!Number.isFinite(tempoParsed) || tempoParsed < 0 || !Number.isInteger(tempoParsed)) {
toast.error("Horas/minutos deve ser um número inteiro maior ou igual a zero.");
return;
}
tempoMinutos = tempoParsed;
} else {
tempoMinutos = null;
}
}
try {
setSavingEdicao(true);
await fechamentoFechamentosService.patchTarefa(fechamentoId, editingTarefa.id, {
descricao,
pontuacao,
...(isManual
? {}
: {
numeroTicket: edicaoNumeroTicket.trim() ? edicaoNumeroTicket.trim() : null,
cliente: edicaoCliente.trim() ? edicaoCliente.trim() : null,
tempoMinutos,
}),
editadoPorId: me.id,
});
toast.success("Tarefa atualizada com sucesso.");
setIsEditarOpen(false);
setEditingTarefa(null);
await loadTarefas();
} catch (error) {
const message = error instanceof Error ? error.message : "Erro ao editar tarefa.";
toast.error(message);
} finally {
setSavingEdicao(false);
}
};
const handleReprocessarAsana = async () => {
if (isFechado) {
toastBloqueioFechado();
return;
}
try {
setReprocessandoAsana(true);
const data = await fechamentoFechamentosService.reprocessarAsana(fechamentoId);
toast.success(
`Reprocessamento concluído: ${data.tarefasImportadas} tarefa(s) atualizada(s) para este parceiro.`,
);
setIsReprocessAsanaOpen(false);
await loadTarefas();
} catch (error) {
const message = error instanceof Error ? error.message : "Erro ao reprocessar Asana.";
toast.error(message);
} finally {
setReprocessandoAsana(false);
}
};
return (
<div className="flex h-full flex-col bg-background pb-16 md:pb-0">
<div className="border-b border-border bg-muted/20 p-3 md:p-6">
<div className="mb-2">
<Button variant="ghost" size="sm" onClick={() => navigate(-1)}>
<ArrowLeft className="mr-1 h-4 w-4" />
Voltar
</Button>
</div>
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<div className="min-w-0">
<h1 className="flex items-center gap-2 text-xl font-bold text-foreground md:text-3xl">
<ListChecks className="h-5 w-5 md:h-6 md:w-6" />
Detalhes do Fechamento
</h1>
<p className="mt-1 text-sm text-muted-foreground">Revisão operacional de tarefas, pontos e horas.</p>
</div>
{!loading ? (
<div className="flex flex-wrap items-center gap-2 xl:justify-end">
<Badge variant={isFechado ? "secondary" : "outline"}>{isFechado ? "Fechado" : "Em aberto"}</Badge>
<Button
size="sm"
variant="outline"
onClick={() => setIsLancamentoOpen(true)}
disabled={isFechado}
className="min-w-[152px]"
>
<Plus className="mr-2 h-4 w-4" />
Fazer lançamento
</Button>
{!isFechado ? (
<Button
size="sm"
variant="outline"
onClick={() => setIsReprocessAsanaOpen(true)}
disabled={reprocessandoAsana}
className="min-w-[152px]"
>
<RotateCcw className="mr-2 h-4 w-4" />
Reprocessar Asana
</Button>
) : null}
{isFechado ? (
<Button
size="sm"
variant="outline"
onClick={() => setIsReabrirOpen(true)}
disabled={reabrindo}
className="min-w-[152px]"
>
<RotateCcw className="mr-2 h-4 w-4" />
Reabrir fechamento
</Button>
) : (
<Button
size="sm"
variant="secondary"
onClick={handleAbrirConcluir}
disabled={tarefas.length === 0 || concluindo}
className="min-w-[152px]"
>
<CheckCircle2 className="mr-2 h-4 w-4" />
Concluir fechamento
</Button>
)}
</div>
) : null}
</div>
{!loading ? (
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
<Card className="border-border/70 bg-card shadow-sm transition hover:shadow-md">
<CardHeader className="space-y-2 p-4">
<div className="flex items-start justify-between">
<CardDescription className="text-[11px] uppercase tracking-wide">Total de tarefas</CardDescription>
<ListChecks className="h-4 w-4 text-muted-foreground" />
</div>
<CardTitle className="text-3xl">{tarefas.length}</CardTitle>
</CardHeader>
</Card>
<Card className="border-border/70 bg-card shadow-sm transition hover:shadow-md">
<CardHeader className="space-y-2 p-4">
<div className="flex items-start justify-between">
<CardDescription className="text-[11px] uppercase tracking-wide">Tarefas aprovadas</CardDescription>
<Target className="h-4 w-4 text-muted-foreground" />
</div>
<CardTitle className="text-3xl">{totais.aprovadas}</CardTitle>
</CardHeader>
</Card>
<Card className="border-border/70 bg-card shadow-sm transition hover:shadow-md">
<CardHeader className="space-y-2 p-4">
<div className="flex items-start justify-between">
<CardDescription className="text-[11px] uppercase tracking-wide">Pontuação aprovada</CardDescription>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</div>
<CardTitle className="text-3xl">{formatPontos(totais.pontos)}</CardTitle>
</CardHeader>
</Card>
<Card className="border-border/70 bg-card shadow-sm transition hover:shadow-md">
<CardHeader className="space-y-2 p-4">
<div className="flex items-start justify-between">
<CardDescription className="text-[11px] uppercase tracking-wide">Horas aprovadas</CardDescription>
<Clock3 className="h-4 w-4 text-muted-foreground" />
</div>
<CardTitle className="text-3xl">{totais.horas}</CardTitle>
</CardHeader>
</Card>
</div>
) : null}
</div>
<div className="flex-1 overflow-auto p-3 md:p-6">
{!loading && tarefas.length === 0 ? (
<Card className="mx-auto mt-12 max-w-2xl">
<CardHeader>
<CardTitle>Nenhuma tarefa encontrada</CardTitle>
<CardDescription>Este fechamento não possui tarefas cadastradas.</CardDescription>
</CardHeader>
</Card>
) : (
<div className="overflow-x-auto rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[120px]">Aprovada</TableHead>
<TableHead className="min-w-[120px]">Ticket</TableHead>
<TableHead className="min-w-[320px]">Descrição</TableHead>
<TableHead className="min-w-[180px]">Cliente</TableHead>
<TableHead className="min-w-[110px]">Tipo</TableHead>
<TableHead className="min-w-[120px]">Horas</TableHead>
<TableHead className="min-w-[120px]">Pontuação</TableHead>
<TableHead className="min-w-[140px] text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={8} className="py-8 text-center text-muted-foreground">
Carregando tarefas...
</TableCell>
</TableRow>
) : (
tarefas.map((tarefa) => (
<TableRow key={tarefa.id}>
<TableCell>
<div className="flex items-center gap-2">
<Checkbox
checked={tarefa.estaRevisada}
onCheckedChange={(checked) => void handleToggleAprovada(tarefa, Boolean(checked))}
disabled={togglingTaskId === tarefa.id || isFechado}
/>
{togglingTaskId === tarefa.id ? <Loader2 className="h-3 w-3 animate-spin" /> : null}
</div>
</TableCell>
<TableCell>{tarefa.numeroTicket || "—"}</TableCell>
<TableCell className="font-medium">{tarefa.descricao}</TableCell>
<TableCell>{tarefa.cliente || "—"}</TableCell>
<TableCell>{tarefa.tipo}</TableCell>
<TableCell>{formatHoras(tarefa.tempoMinutos)}</TableCell>
<TableCell>{Number(tarefa.pontuacao || 0)}</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => openEditarTarefa(tarefa)}
disabled={isFechado}
>
<Pencil className="h-4 w-4" />
<span className="ml-1">Editar</span>
</Button>
{tarefa.tipo === "bonus" || tarefa.tipo === "desconto" ? (
<Button
type="button"
size="sm"
variant="ghost"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => void handleExcluirLancamento(tarefa)}
disabled={isFechado || deletingTaskId === tarefa.id}
>
{deletingTaskId === tarefa.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
<span className="ml-1">Excluir</span>
</Button>
) : null}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)}
</div>
<Dialog open={isLancamentoOpen} onOpenChange={setIsLancamentoOpen}>
<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">
<div className="space-y-2">
<Label htmlFor="lancamento-tipo">Tipo de lançamento</Label>
<Select
value={lancamentoTipo}
onValueChange={(value) => setLancamentoTipo(value as "bonus" | "desconto")}
>
<SelectTrigger id="lancamento-tipo">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="bonus">Bonificação</SelectItem>
<SelectItem value="desconto">Desconto</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="lancamento-pontuacao">Pontuação</Label>
<Input
id="lancamento-pontuacao"
type="number"
min={1}
step={1}
value={lancamentoPontuacao}
onChange={(e) => setLancamentoPontuacao(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="lancamento-descricao">Descrição</Label>
<Input
id="lancamento-descricao"
value={lancamentoDescricao}
onChange={(e) => setLancamentoDescricao(e.target.value)}
placeholder="Ex.: ajuste de meta / retrabalho / bônus de sprint"
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setIsLancamentoOpen(false);
resetLancamentoForm();
}}
disabled={savingLancamento}
>
Cancelar
</Button>
<Button onClick={() => void handleSalvarLancamento()} disabled={savingLancamento || isFechado}>
{savingLancamento ? "Salvando..." : "Salvar lançamento"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isConcluirOpen} onOpenChange={setIsConcluirOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Concluir fechamento</DialogTitle>
<DialogDescription>
Revise os totais e confirme a pontuação paga para concluir este fechamento.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg border bg-card p-3">
<p className="text-xs text-muted-foreground">Aprovada</p>
<p className="text-2xl font-bold">{formatPontos(totais.pontos)}</p>
</div>
<div className="rounded-lg border bg-card p-3">
<p className="text-xs text-muted-foreground">Meta</p>
<p className="text-2xl font-bold">{formatPontos(Number(pontuacaoMeta ?? 0))}</p>
</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>
) : null}
<Input
id="pontuacao-paga"
type="text"
inputMode="decimal"
value={pontuacaoPagaInput}
onChange={(e) => setPontuacaoPagaInput(e.target.value)}
placeholder="Ex.: 10,5"
/>
</div>
{requerMotivoAjuste ? (
<div className="space-y-2">
<Label htmlFor="motivo-ajuste">Motivo do ajuste *</Label>
<Input
id="motivo-ajuste"
value={motivoAjuste}
onChange={(e) => setMotivoAjuste(e.target.value)}
placeholder="Ex.: pagamento parcial acordado com o parceiro"
/>
</div>
) : null}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsConcluirOpen(false)} disabled={concluindo}>
Cancelar
</Button>
<Button onClick={() => void handleConcluirFechamento()} disabled={concluindo || isFechado}>
{concluindo ? "Concluindo..." : "Confirmar conclusão"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isReabrirOpen} onOpenChange={setIsReabrirOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Reabrir fechamento</DialogTitle>
<DialogDescription>
Ao reabrir, o fechamento volta para edição e será necessário concluir novamente depois.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="motivo-reabertura">Motivo (opcional)</Label>
<Input
id="motivo-reabertura"
value={motivoReabertura}
onChange={(e) => setMotivoReabertura(e.target.value)}
placeholder="Ex.: ajuste após revisão financeira"
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsReabrirOpen(false)} disabled={reabrindo}>
Cancelar
</Button>
<Button onClick={() => void handleReabrirFechamento()} disabled={reabrindo}>
{reabrindo ? "Reabrindo..." : "Confirmar reabertura"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Excluir lançamento</DialogTitle>
<DialogDescription>
Deseja realmente excluir o lançamento manual{" "}
<strong>{deletingTarefa?.descricao}</strong>? Esta ação não pode ser desfeita.
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => {
setIsDeleteDialogOpen(false);
setDeletingTarefa(null);
}}
disabled={deletingTaskId !== null}
>
Cancelar
</Button>
<Button
variant="destructive"
onClick={() => void confirmDeleteLancamento()}
disabled={deletingTaskId !== null}
>
{deletingTaskId !== null ? "Excluindo..." : "Confirmar exclusão"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isReprocessAsanaOpen} onOpenChange={setIsReprocessAsanaOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Reprocessar Asana deste fechamento</DialogTitle>
<DialogDescription>
Esta ação atualiza somente as tarefas do Asana para este parceiro no período da competência. Lançamentos
manuais (bônus/desconto) serão preservados.
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setIsReprocessAsanaOpen(false)}
disabled={reprocessandoAsana}
>
Cancelar
</Button>
<Button onClick={() => void handleReprocessarAsana()} disabled={reprocessandoAsana}>
{reprocessandoAsana ? "Reprocessando..." : "Confirmar reprocessamento"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={isEditarOpen} onOpenChange={setIsEditarOpen}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Editar tarefa</DialogTitle>
<DialogDescription>
{editingTarefa?.tipo === "bonus" || editingTarefa?.tipo === "desconto"
? "Para bônus/desconto, você pode editar apenas descrição e pontuação."
: "Edite os campos da tarefa e salve as alterações."}
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
{editingTarefa?.tipo === "tarefa" ? (
<>
<div className="space-y-1">
<Label htmlFor="edit-ticket">Ticket</Label>
<Input id="edit-ticket" value={edicaoNumeroTicket} onChange={(e) => setEdicaoNumeroTicket(e.target.value)} />
</div>
<div className="space-y-1">
<Label htmlFor="edit-descricao">Descrição</Label>
<Input id="edit-descricao" value={edicaoDescricao} onChange={(e) => setEdicaoDescricao(e.target.value)} />
</div>
<div className="space-y-1">
<Label htmlFor="edit-cliente">Cliente</Label>
<Input id="edit-cliente" value={edicaoCliente} onChange={(e) => setEdicaoCliente(e.target.value)} />
</div>
<div className="space-y-1">
<Label htmlFor="edit-tempo">Horas (minutos)</Label>
<Input
id="edit-tempo"
type="number"
min={0}
step={1}
value={edicaoTempoMinutos}
onChange={(e) => setEdicaoTempoMinutos(e.target.value)}
/>
</div>
</>
) : null}
{editingTarefa?.tipo !== "tarefa" ? (
<div className="space-y-1">
<Label htmlFor="edit-descricao">Descrição</Label>
<Input id="edit-descricao" value={edicaoDescricao} onChange={(e) => setEdicaoDescricao(e.target.value)} />
</div>
) : null}
<div className="space-y-1">
<Label htmlFor="edit-pontuacao">Pontuação</Label>
<Input
id="edit-pontuacao"
type="text"
inputMode="decimal"
value={edicaoPontuacao}
onChange={(e) => setEdicaoPontuacao(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditarOpen(false)} disabled={savingEdicao}>
Cancelar
</Button>
<Button onClick={() => void handleSalvarEdicao()} disabled={savingEdicao}>
{savingEdicao ? "Salvando..." : "Salvar"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}