implementacao de logs de auditoria e alterar multiplos registros de competencia

This commit is contained in:
Vitex Tecnologia
2026-06-01 17:19:00 -03:00
parent 34ae317fed
commit 8a77c0dd31
5 changed files with 253 additions and 44 deletions
@@ -413,7 +413,12 @@ export default function CompetenciaFechamentos() {
<TableRow> <TableRow>
<TableHead className="min-w-[100px]">Logo</TableHead> <TableHead className="min-w-[100px]">Logo</TableHead>
<TableHead className="min-w-[280px]">Nome</TableHead> <TableHead className="min-w-[280px]">Nome</TableHead>
<TableHead className="min-w-[140px] text-right">Pontuação Total</TableHead> <TableHead
className="min-w-[140px] text-right"
title="Soma de todas as tarefas do fechamento (revisadas ou não), igual ao total do mês no detalhe e ao dashboard."
>
Pontuação Total
</TableHead>
<TableHead className="min-w-[150px] text-right">Pontuação Paga</TableHead> <TableHead className="min-w-[150px] text-right">Pontuação Paga</TableHead>
<TableHead className="min-w-[120px]">Status</TableHead> <TableHead className="min-w-[120px]">Status</TableHead>
<TableHead className="text-center">Ações</TableHead> <TableHead className="text-center">Ações</TableHead>
@@ -639,7 +644,7 @@ export default function CompetenciaFechamentos() {
> >
<p className="text-sm font-semibold">Buscar novos fechamentos</p> <p className="text-sm font-semibold">Buscar novos fechamentos</p>
<p className="mt-1 text-xs text-muted-foreground"> <p className="mt-1 text-xs text-muted-foreground">
Busca no Asana e cria apenas os fechamentos que ainda não existem. Busca no Asana e cria fechamentos que ainda não existem (inclusive vazios, se o parceiro não tiver tasks no período).
</p> </p>
</button> </button>
</div> </div>
@@ -279,7 +279,8 @@ export default function DashboardPontos() {
Dashboard Dashboard
</h1> </h1>
<p className="max-w-xl text-sm text-muted-foreground"> <p className="max-w-xl text-sm text-muted-foreground">
Distribuição de pontos por cliente visão rápida do que cada parceiro entregou no período. Distribuição por cliente total da competência no período (todas as tarefas do fechamento, alinhado à
pontuação total do mês na tela de fechamento).
</p> </p>
</div> </div>
<Button <Button
@@ -302,7 +302,9 @@ export default function FechamentoDetalhes() {
const [loadingCompetenciasAbertas, setLoadingCompetenciasAbertas] = useState(false); const [loadingCompetenciasAbertas, setLoadingCompetenciasAbertas] = useState(false);
const [savingAlteracaoCompetencia, setSavingAlteracaoCompetencia] = useState(false); const [savingAlteracaoCompetencia, setSavingAlteracaoCompetencia] = useState(false);
const [competenciasAbertas, setCompetenciasAbertas] = useState<CompetenciaItem[]>([]); const [competenciasAbertas, setCompetenciasAbertas] = useState<CompetenciaItem[]>([]);
const [tarefaAlteracaoCompetencia, setTarefaAlteracaoCompetencia] = useState<FechamentoTarefaItem | null>(null); const [tarefasAlteracaoCompetencia, setTarefasAlteracaoCompetencia] = useState<FechamentoTarefaItem[]>([]);
const [selectedTarefaIds, setSelectedTarefaIds] = useState<Set<string>>(() => new Set());
const [modoSelecaoAlterarCompetencia, setModoSelecaoAlterarCompetencia] = useState(false);
const [competenciaDestinoId, setCompetenciaDestinoId] = useState(""); const [competenciaDestinoId, setCompetenciaDestinoId] = useState("");
const [motivoAlteracaoCompetencia, setMotivoAlteracaoCompetencia] = useState(""); const [motivoAlteracaoCompetencia, setMotivoAlteracaoCompetencia] = useState("");
const [isEditarOpen, setIsEditarOpen] = useState(false); const [isEditarOpen, setIsEditarOpen] = useState(false);
@@ -442,6 +444,16 @@ export default function FechamentoDetalhes() {
filtroDataConclusaoFim, filtroDataConclusaoFim,
intervaloDataConclusaoInvalido, intervaloDataConclusaoInvalido,
]); ]);
const tarefasSelecionaveisCompetencia = useMemo(
() => tarefasFiltradas.filter((t) => t.tipo === "tarefa"),
[tarefasFiltradas],
);
const selectedTarefaCount = selectedTarefaIds.size;
const todasVisiveisSelecionadas =
tarefasSelecionaveisCompetencia.length > 0 &&
tarefasSelecionaveisCompetencia.every((t) => selectedTarefaIds.has(t.id));
const exibirColunaSelecaoCompetencia = podeAlterarCompetenciaTarefa && modoSelecaoAlterarCompetencia;
const tabelaColSpan = exibirColunaSelecaoCompetencia ? 12 : 11;
const loadTarefas = async () => { const loadTarefas = async () => {
try { try {
@@ -947,33 +959,91 @@ export default function FechamentoDetalhes() {
} }
}; };
const openAlterarCompetencia = async (tarefa: FechamentoTarefaItem) => { const assertPodeAlterarCompetencia = (): boolean => {
if (!podeAlterarCompetenciaTarefa) { if (!podeAlterarCompetenciaTarefa) {
toast.error("Sem permissão para alterar competência da tarefa."); toast.error("Sem permissão para alterar competência da tarefa.");
return; return false;
} }
if (isCompetenciaConcluida) { if (isCompetenciaConcluida) {
toastBloqueioCompetenciaConcluida(); toastBloqueioCompetenciaConcluida();
return; return false;
} }
if (isFechado) { if (isFechado) {
toastBloqueioFechado(); toastBloqueioFechado();
return; return false;
} }
const competencias = await carregarCompetenciasAbertas(); return true;
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."); const abrirDialogAlterarCompetencia = (lista: FechamentoTarefaItem[], opcoes: CompetenciaItem[]) => {
return; setTarefasAlteracaoCompetencia(lista);
}
setTarefaAlteracaoCompetencia(tarefa);
setCompetenciaDestinoId(opcoes[0]?.id ?? ""); setCompetenciaDestinoId(opcoes[0]?.id ?? "");
setMotivoAlteracaoCompetencia(""); setMotivoAlteracaoCompetencia("");
setIsAlterarCompetenciaOpen(true); setIsAlterarCompetenciaOpen(true);
}; };
const sairModoSelecaoAlterarCompetencia = () => {
setModoSelecaoAlterarCompetencia(false);
setSelectedTarefaIds(new Set());
};
const entrarModoSelecaoAlterarCompetencia = () => {
if (!assertPodeAlterarCompetencia()) return;
setModoSelecaoAlterarCompetencia(true);
setSelectedTarefaIds(new Set());
};
const openAlterarCompetenciaEmLote = async () => {
if (!assertPodeAlterarCompetencia()) return;
if (selectedTarefaCount === 0) {
toast.error("Selecione ao menos uma tarefa.");
return;
}
const lista = tarefas.filter((t) => selectedTarefaIds.has(t.id) && t.tipo === "tarefa");
if (lista.length === 0) {
toast.error("Nenhuma tarefa válida selecionada.");
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 as tarefas.");
return;
}
abrirDialogAlterarCompetencia(lista, opcoes);
};
const handleToggleSelecaoTarefa = (tarefaId: string, checked: boolean) => {
setSelectedTarefaIds((prev) => {
const next = new Set(prev);
if (checked) next.add(tarefaId);
else next.delete(tarefaId);
return next;
});
};
const handleSelecionarTodasVisiveisCompetencia = () => {
if (todasVisiveisSelecionadas) {
setSelectedTarefaIds((prev) => {
const next = new Set(prev);
for (const t of tarefasSelecionaveisCompetencia) next.delete(t.id);
return next;
});
return;
}
setSelectedTarefaIds((prev) => {
const next = new Set(prev);
for (const t of tarefasSelecionaveisCompetencia) next.add(t.id);
return next;
});
};
const handleLimparSelecaoTarefas = () => {
setSelectedTarefaIds(new Set());
};
const handleConfirmarAlteracaoCompetencia = async () => { const handleConfirmarAlteracaoCompetencia = async () => {
if (!tarefaAlteracaoCompetencia) return; if (tarefasAlteracaoCompetencia.length === 0) return;
if (!me?.id) { if (!me?.id) {
toast.error("Não foi possível identificar o usuário para registrar a alteração."); toast.error("Não foi possível identificar o usuário para registrar a alteração.");
return; return;
@@ -982,21 +1052,33 @@ export default function FechamentoDetalhes() {
toast.error("Selecione a competência de destino."); toast.error("Selecione a competência de destino.");
return; return;
} }
try { const payload = {
setSavingAlteracaoCompetencia(true);
await fechamentoFechamentosService.alterarCompetenciaTarefa(fechamentoId, tarefaAlteracaoCompetencia.id, {
competenciaDestinoId, competenciaDestinoId,
alteradoPorId: me.id, alteradoPorId: me.id,
motivo: motivoAlteracaoCompetencia, motivo: motivoAlteracaoCompetencia,
}); };
try {
setSavingAlteracaoCompetencia(true);
const ids = tarefasAlteracaoCompetencia.map((t) => t.id);
if (ids.length === 1) {
await fechamentoFechamentosService.alterarCompetenciaTarefa(fechamentoId, ids[0]!, payload);
toast.success("Competência da tarefa alterada com sucesso."); toast.success("Competência da tarefa alterada com sucesso.");
} else {
const res = await fechamentoFechamentosService.alterarCompetenciaTarefasEmLote(fechamentoId, {
tarefaIds: ids,
...payload,
});
toast.success(`Competência alterada em ${res.alteradas} tarefa(s).`);
}
setIsAlterarCompetenciaOpen(false); setIsAlterarCompetenciaOpen(false);
setTarefaAlteracaoCompetencia(null); setTarefasAlteracaoCompetencia([]);
setCompetenciaDestinoId(""); setCompetenciaDestinoId("");
setMotivoAlteracaoCompetencia(""); setMotivoAlteracaoCompetencia("");
setSelectedTarefaIds(new Set());
setModoSelecaoAlterarCompetencia(false);
await loadTarefas(); await loadTarefas();
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Erro ao alterar competência da tarefa."; const message = error instanceof Error ? error.message : "Erro ao alterar competência da(s) tarefa(s).";
toast.error(message); toast.error(message);
} finally { } finally {
setSavingAlteracaoCompetencia(false); setSavingAlteracaoCompetencia(false);
@@ -1244,6 +1326,66 @@ export default function FechamentoDetalhes() {
{!loading ? ( {!loading ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap items-center justify-end gap-2"> <div className="flex flex-wrap items-center justify-end gap-2">
{podeAlterarCompetenciaTarefa ? (
modoSelecaoAlterarCompetencia ? (
<>
<Button
type="button"
size="sm"
variant="ghost"
onClick={sairModoSelecaoAlterarCompetencia}
disabled={savingAlteracaoCompetencia}
>
Cancelar
</Button>
<Button
type="button"
size="sm"
variant="outline"
onClick={handleSelecionarTodasVisiveisCompetencia}
disabled={
bloqueioEdicaoTarefa ||
tarefasSelecionaveisCompetencia.length === 0 ||
savingAlteracaoCompetencia
}
>
{todasVisiveisSelecionadas ? "Desmarcar visíveis" : "Selecionar visíveis"}
</Button>
<Button
type="button"
size="sm"
variant="outline"
onClick={handleLimparSelecaoTarefas}
disabled={selectedTarefaCount === 0 || savingAlteracaoCompetencia}
>
Limpar seleção
</Button>
<Button
type="button"
size="sm"
variant="default"
onClick={() => void openAlterarCompetenciaEmLote()}
disabled={
bloqueioEdicaoTarefa || selectedTarefaCount === 0 || savingAlteracaoCompetencia
}
>
<Repeat className="mr-2 h-4 w-4" />
Continuar ({selectedTarefaCount})
</Button>
</>
) : (
<Button
type="button"
size="sm"
variant="outline"
onClick={entrarModoSelecaoAlterarCompetencia}
disabled={bloqueioEdicaoTarefa || savingAlteracaoCompetencia}
>
<Repeat className="mr-2 h-4 w-4" />
Alterar competência
</Button>
)
) : null}
<Button type="button" size="sm" variant="outline" onClick={() => setShowFilters((prev) => !prev)}> <Button type="button" size="sm" variant="outline" onClick={() => setShowFilters((prev) => !prev)}>
{showFilters ? "Ocultar filtros" : "Exibir filtros"} {showFilters ? "Ocultar filtros" : "Exibir filtros"}
</Button> </Button>
@@ -1399,6 +1541,9 @@ export default function FechamentoDetalhes() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{exibirColunaSelecaoCompetencia ? (
<TableHead className="min-w-[88px] whitespace-nowrap px-2">Selecionar</TableHead>
) : null}
<TableHead className="min-w-[120px]">Aprovada</TableHead> <TableHead className="min-w-[120px]">Aprovada</TableHead>
<TableHead className="min-w-[120px]">Ticket</TableHead> <TableHead className="min-w-[120px]">Ticket</TableHead>
<TableHead className="min-w-[320px]">Descrição</TableHead> <TableHead className="min-w-[320px]">Descrição</TableHead>
@@ -1415,25 +1560,47 @@ export default function FechamentoDetalhes() {
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={11} className="py-8 text-center text-muted-foreground"> <TableCell colSpan={tabelaColSpan} className="py-8 text-center text-muted-foreground">
Carregando tarefas... Carregando tarefas...
</TableCell> </TableCell>
</TableRow> </TableRow>
) : tarefasFiltradas.length === 0 ? ( ) : tarefasFiltradas.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={11} className="py-8 text-center text-muted-foreground"> <TableCell colSpan={tabelaColSpan} className="py-8 text-center text-muted-foreground">
Nenhuma tarefa encontrada com os filtros aplicados. Nenhuma tarefa encontrada com os filtros aplicados.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
tarefasFiltradas.map((tarefa) => ( tarefasFiltradas.map((tarefa) => (
<TableRow key={tarefa.id}> <TableRow
key={tarefa.id}
className={cn(
exibirColunaSelecaoCompetencia &&
selectedTarefaIds.has(tarefa.id) &&
"bg-primary/5",
)}
>
{exibirColunaSelecaoCompetencia ? (
<TableCell className="min-w-[88px] px-2">
{tarefa.tipo === "tarefa" ? (
<Checkbox
checked={selectedTarefaIds.has(tarefa.id)}
onCheckedChange={(checked) =>
handleToggleSelecaoTarefa(tarefa.id, Boolean(checked))
}
disabled={bloqueioEdicaoTarefa || savingAlteracaoCompetencia}
aria-label={`Selecionar tarefa ${tarefa.descricao}`}
/>
) : null}
</TableCell>
) : null}
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
checked={tarefa.estaRevisada} checked={tarefa.estaRevisada}
onCheckedChange={(checked) => void handleToggleAprovada(tarefa, Boolean(checked))} onCheckedChange={(checked) => void handleToggleAprovada(tarefa, Boolean(checked))}
disabled={bloqueioEdicaoTarefa || bulkUpdatingRevisao} disabled={bloqueioEdicaoTarefa || bulkUpdatingRevisao}
className="border-emerald-600/70 data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600 data-[state=checked]:text-white focus-visible:ring-emerald-500/40"
/> />
</div> </div>
</TableCell> </TableCell>
@@ -1478,18 +1645,6 @@ export default function FechamentoDetalhes() {
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
<span className="ml-1">Editar</span> <span className="ml-1">Editar</span>
</Button> </Button>
{podeAlterarCompetenciaTarefa && tarefa.tipo === "tarefa" ? (
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => void openAlterarCompetencia(tarefa)}
disabled={bloqueioEdicaoTarefa || savingAlteracaoCompetencia}
>
<Repeat className="h-4 w-4" />
<span className="ml-1">Alterar competência</span>
</Button>
) : null}
{tarefa.tipo === "bonus" || tarefa.tipo === "desconto" ? ( {tarefa.tipo === "bonus" || tarefa.tipo === "desconto" ? (
<Button <Button
type="button" type="button"
@@ -1671,7 +1826,7 @@ export default function FechamentoDetalhes() {
onOpenChange={(open) => { onOpenChange={(open) => {
setIsAlterarCompetenciaOpen(open); setIsAlterarCompetenciaOpen(open);
if (!open) { if (!open) {
setTarefaAlteracaoCompetencia(null); setTarefasAlteracaoCompetencia([]);
setMotivoAlteracaoCompetencia(""); setMotivoAlteracaoCompetencia("");
setCompetenciaDestinoId(""); setCompetenciaDestinoId("");
} }
@@ -1679,12 +1834,35 @@ export default function FechamentoDetalhes() {
> >
<DialogContent className="sm:max-w-lg"> <DialogContent className="sm:max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Alterar competência da tarefa</DialogTitle> <DialogTitle>
{tarefasAlteracaoCompetencia.length <= 1
? "Alterar competência da tarefa"
: `Alterar competência de ${tarefasAlteracaoCompetencia.length} tarefas`}
</DialogTitle>
<DialogDescription> <DialogDescription>
Mova a tarefa para outra competência em aberto. O histórico da alteração ficará registrado. {tarefasAlteracaoCompetencia.length <= 1
? "Mova a tarefa para outra competência em aberto. O histórico da alteração ficará registrado."
: "Mova as tarefas selecionadas para outra competência em aberto. O histórico de cada alteração ficará registrado."}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
{tarefasAlteracaoCompetencia.length > 1 ? (
<div className="space-y-2">
<Label>Tarefas selecionadas</Label>
<ul className="max-h-32 space-y-1 overflow-y-auto rounded-md border bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
{tarefasAlteracaoCompetencia.slice(0, 5).map((t) => (
<li key={t.id} className="truncate">
{t.descricao}
</li>
))}
{tarefasAlteracaoCompetencia.length > 5 ? (
<li className="font-medium text-foreground">
e mais {tarefasAlteracaoCompetencia.length - 5} tarefa(s)
</li>
) : null}
</ul>
</div>
) : null}
<div className="space-y-2"> <div className="space-y-2">
<Label>Competência atual</Label> <Label>Competência atual</Label>
<p className="rounded-md border bg-muted/30 px-3 py-2 text-sm text-muted-foreground"> <p className="rounded-md border bg-muted/30 px-3 py-2 text-sm text-muted-foreground">
@@ -1741,7 +1919,11 @@ export default function FechamentoDetalhes() {
onClick={() => void handleConfirmarAlteracaoCompetencia()} onClick={() => void handleConfirmarAlteracaoCompetencia()}
disabled={savingAlteracaoCompetencia || !competenciaDestinoId} disabled={savingAlteracaoCompetencia || !competenciaDestinoId}
> >
{savingAlteracaoCompetencia ? "Alterando..." : "Confirmar alteração"} {savingAlteracaoCompetencia
? "Alterando..."
: tarefasAlteracaoCompetencia.length > 1
? `Confirmar (${tarefasAlteracaoCompetencia.length})`
: "Confirmar alteração"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
+21
View File
@@ -392,6 +392,27 @@ class FechamentoFechamentosService {
this.handleAxiosError(error, "Erro ao alterar competência da tarefa."); this.handleAxiosError(error, "Erro ao alterar competência da tarefa.");
} }
} }
async alterarCompetenciaTarefasEmLote(
fechamentoId: string,
input: { tarefaIds: string[]; competenciaDestinoId: string; alteradoPorId: string; motivo?: string },
): Promise<{ alteradas: number; tarefas: FechamentoTarefaItem[] }> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<{
data: { alteradas: number; tarefas: FechamentoTarefaItem[] };
}>(`${baseUrl}/fechamentos/${fechamentoId}/tarefas/alterar-competencia`, {
tarefa_ids: input.tarefaIds,
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 das tarefas.");
}
}
} }
export const fechamentoFechamentosService = new FechamentoFechamentosService(); export const fechamentoFechamentosService = new FechamentoFechamentosService();
File diff suppressed because one or more lines are too long