diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..95f6793 Binary files /dev/null and b/dist.zip differ diff --git a/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx b/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx index 8862eea..a002ac3 100644 --- a/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx +++ b/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; -import { ArrowLeft, CheckCircle2, Clock3, ExternalLink, ListChecks, Loader2, Pencil, Plus, RotateCcw, Target, Trash2, TrendingUp } from "lucide-react"; +import { ArrowLeft, CheckCircle2, Clock3, ExternalLink, ListChecks, Loader2, Pencil, Plus, Repeat, RotateCcw, Target, Trash2, TrendingUp } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -19,7 +19,7 @@ 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 { fechamentoCompetenciasService, type CompetenciaItem } from "@/services/fechamento/competencias"; import { fechamentoFechamentosService, type FechamentoTarefaItem } from "@/services/fechamento/fechamentos"; function formatHoras(minutos: number | null): string { @@ -105,6 +105,10 @@ function formatDateTime(value: string | null): string { }); } +function formatCompetenciaMesAno(item: Pick): string { + return `${String(item.mes).padStart(2, "0")}/${item.ano}`; +} + type FechamentoDetalhesLocationState = { competenciaId?: string; status?: "em_aberto" | "fechado"; @@ -141,6 +145,13 @@ export default function FechamentoDetalhes() { const [deletingTarefa, setDeletingTarefa] = useState(null); const [isReprocessAsanaOpen, setIsReprocessAsanaOpen] = useState(false); const [reprocessandoAsana, setReprocessandoAsana] = useState(false); + const [isAlterarCompetenciaOpen, setIsAlterarCompetenciaOpen] = useState(false); + const [loadingCompetenciasAbertas, setLoadingCompetenciasAbertas] = useState(false); + const [savingAlteracaoCompetencia, setSavingAlteracaoCompetencia] = useState(false); + const [competenciasAbertas, setCompetenciasAbertas] = useState([]); + const [tarefaAlteracaoCompetencia, setTarefaAlteracaoCompetencia] = useState(null); + const [competenciaDestinoId, setCompetenciaDestinoId] = useState(""); + const [motivoAlteracaoCompetencia, setMotivoAlteracaoCompetencia] = useState(""); const [isEditarOpen, setIsEditarOpen] = useState(false); const [savingEdicao, setSavingEdicao] = useState(false); const [editingTarefa, setEditingTarefa] = useState(null); @@ -153,6 +164,7 @@ 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 totais = useMemo(() => { const aprovadas = tarefas.filter((t) => t.estaRevisada); @@ -172,6 +184,10 @@ export default function FechamentoDetalhes() { const pontuacaoTotalLabel = toPontuacaoInput(totais.pontos); const isValorEditado = pontuacaoPagaInput.trim() !== pontuacaoTotalLabel; const todasTarefasRevisadas = tarefas.length > 0 && tarefas.every((tarefa) => tarefa.estaRevisada); + const competenciasDestino = useMemo( + () => competenciasAbertas.filter((item) => item.id !== competenciaId), + [competenciasAbertas, competenciaId], + ); const loadTarefas = async () => { try { @@ -498,6 +514,77 @@ export default function FechamentoDetalhes() { setIsEditarOpen(true); }; + const carregarCompetenciasAbertas = async () => { + try { + setLoadingCompetenciasAbertas(true); + const competencias = await fechamentoCompetenciasService.listarCompetencias({ status: "em_aberto" }); + setCompetenciasAbertas(competencias); + return competencias; + } catch (error) { + const message = error instanceof Error ? error.message : "Erro ao carregar competências em aberto."; + toast.error(message); + return [] as CompetenciaItem[]; + } finally { + setLoadingCompetenciasAbertas(false); + } + }; + + const openAlterarCompetencia = async (tarefa: FechamentoTarefaItem) => { + if (!isAdmin) { + toast.error("Apenas administradores podem alterar competência."); + return; + } + if (isCompetenciaConcluida) { + toastBloqueioCompetenciaConcluida(); + return; + } + if (isFechado) { + toastBloqueioFechado(); + return; + } + const competencias = await carregarCompetenciasAbertas(); + const opcoes = competencias.filter((item) => item.id !== competenciaId); + if (opcoes.length === 0) { + toast.error("Não há outra competência em aberto para mover a tarefa."); + return; + } + setTarefaAlteracaoCompetencia(tarefa); + setCompetenciaDestinoId(opcoes[0]?.id ?? ""); + setMotivoAlteracaoCompetencia(""); + setIsAlterarCompetenciaOpen(true); + }; + + const handleConfirmarAlteracaoCompetencia = async () => { + if (!tarefaAlteracaoCompetencia) return; + if (!me?.id) { + toast.error("Não foi possível identificar o usuário para registrar a alteração."); + return; + } + if (!competenciaDestinoId) { + toast.error("Selecione a competência de destino."); + return; + } + try { + setSavingAlteracaoCompetencia(true); + await fechamentoFechamentosService.alterarCompetenciaTarefa(fechamentoId, tarefaAlteracaoCompetencia.id, { + competenciaDestinoId, + alteradoPorId: me.id, + motivo: motivoAlteracaoCompetencia, + }); + toast.success("Competência da tarefa alterada com sucesso."); + setIsAlterarCompetenciaOpen(false); + setTarefaAlteracaoCompetencia(null); + setCompetenciaDestinoId(""); + setMotivoAlteracaoCompetencia(""); + await loadTarefas(); + } catch (error) { + const message = error instanceof Error ? error.message : "Erro ao alterar competência da tarefa."; + toast.error(message); + } finally { + setSavingAlteracaoCompetencia(false); + } + }; + const handleSalvarEdicao = async () => { if (!editingTarefa) return; if (!me?.id) { @@ -804,6 +891,18 @@ export default function FechamentoDetalhes() { Editar + {isAdmin && tarefa.tipo === "tarefa" ? ( + + ) : null} {tarefa.tipo === "bonus" || tarefa.tipo === "desconto" ? ( + + + + + diff --git a/src/services/fechamento/fechamentos.ts b/src/services/fechamento/fechamentos.ts index c04243d..cc238c6 100644 --- a/src/services/fechamento/fechamentos.ts +++ b/src/services/fechamento/fechamentos.ts @@ -95,6 +95,7 @@ export type ExportarPlanilhaResponse = { type ApiErrorShape = { error?: { + code?: string; message?: string; }; }; @@ -309,6 +310,29 @@ class FechamentoFechamentosService { this.handleAxiosError(error, "Erro ao reprocessar tarefas do Asana."); } } + + async alterarCompetenciaTarefa( + fechamentoId: string, + tarefaId: string, + input: { competenciaDestinoId: string; alteradoPorId: string; motivo?: string }, + ): Promise { + try { + const headers = await buildCommanderHeaders(); + const baseUrl = resolveCommanderBaseUrl(); + const response = await axios.post( + `${baseUrl}/fechamentos/${fechamentoId}/tarefas/${tarefaId}/alterar-competencia`, + { + competencia_destino_id: input.competenciaDestinoId, + alterado_por_id: input.alteradoPorId, + ...(input.motivo?.trim() ? { motivo: input.motivo.trim() } : {}), + }, + { headers }, + ); + return response.data.data; + } catch (error) { + this.handleAxiosError(error, "Erro ao alterar competência da tarefa."); + } + } } export const fechamentoFechamentosService = new FechamentoFechamentosService();