implementacao de logs de auditoria e alterar multiplos registros de competencia
This commit is contained in:
@@ -413,7 +413,12 @@ export default function CompetenciaFechamentos() {
|
||||
<TableRow>
|
||||
<TableHead className="min-w-[100px]">Logo</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-[120px]">Status</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="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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -279,7 +279,8 @@ export default function DashboardPontos() {
|
||||
Dashboard
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -302,7 +302,9 @@ export default function FechamentoDetalhes() {
|
||||
const [loadingCompetenciasAbertas, setLoadingCompetenciasAbertas] = useState(false);
|
||||
const [savingAlteracaoCompetencia, setSavingAlteracaoCompetencia] = useState(false);
|
||||
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 [motivoAlteracaoCompetencia, setMotivoAlteracaoCompetencia] = useState("");
|
||||
const [isEditarOpen, setIsEditarOpen] = useState(false);
|
||||
@@ -442,6 +444,16 @@ export default function FechamentoDetalhes() {
|
||||
filtroDataConclusaoFim,
|
||||
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 () => {
|
||||
try {
|
||||
@@ -947,33 +959,91 @@ export default function FechamentoDetalhes() {
|
||||
}
|
||||
};
|
||||
|
||||
const openAlterarCompetencia = async (tarefa: FechamentoTarefaItem) => {
|
||||
const assertPodeAlterarCompetencia = (): boolean => {
|
||||
if (!podeAlterarCompetenciaTarefa) {
|
||||
toast.error("Sem permissão para alterar competência da tarefa.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (isCompetenciaConcluida) {
|
||||
toastBloqueioCompetenciaConcluida();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (isFechado) {
|
||||
toastBloqueioFechado();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
return true;
|
||||
};
|
||||
|
||||
const abrirDialogAlterarCompetencia = (lista: FechamentoTarefaItem[], opcoes: CompetenciaItem[]) => {
|
||||
setTarefasAlteracaoCompetencia(lista);
|
||||
setCompetenciaDestinoId(opcoes[0]?.id ?? "");
|
||||
setMotivoAlteracaoCompetencia("");
|
||||
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 () => {
|
||||
if (!tarefaAlteracaoCompetencia) return;
|
||||
if (tarefasAlteracaoCompetencia.length === 0) return;
|
||||
if (!me?.id) {
|
||||
toast.error("Não foi possível identificar o usuário para registrar a alteração.");
|
||||
return;
|
||||
@@ -982,21 +1052,33 @@ export default function FechamentoDetalhes() {
|
||||
toast.error("Selecione a competência de destino.");
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
competenciaDestinoId,
|
||||
alteradoPorId: me.id,
|
||||
motivo: motivoAlteracaoCompetencia,
|
||||
};
|
||||
try {
|
||||
setSavingAlteracaoCompetencia(true);
|
||||
await fechamentoFechamentosService.alterarCompetenciaTarefa(fechamentoId, tarefaAlteracaoCompetencia.id, {
|
||||
competenciaDestinoId,
|
||||
alteradoPorId: me.id,
|
||||
motivo: motivoAlteracaoCompetencia,
|
||||
});
|
||||
toast.success("Competência da tarefa alterada com sucesso.");
|
||||
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.");
|
||||
} else {
|
||||
const res = await fechamentoFechamentosService.alterarCompetenciaTarefasEmLote(fechamentoId, {
|
||||
tarefaIds: ids,
|
||||
...payload,
|
||||
});
|
||||
toast.success(`Competência alterada em ${res.alteradas} tarefa(s).`);
|
||||
}
|
||||
setIsAlterarCompetenciaOpen(false);
|
||||
setTarefaAlteracaoCompetencia(null);
|
||||
setTarefasAlteracaoCompetencia([]);
|
||||
setCompetenciaDestinoId("");
|
||||
setMotivoAlteracaoCompetencia("");
|
||||
setSelectedTarefaIds(new Set());
|
||||
setModoSelecaoAlterarCompetencia(false);
|
||||
await loadTarefas();
|
||||
} 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);
|
||||
} finally {
|
||||
setSavingAlteracaoCompetencia(false);
|
||||
@@ -1244,6 +1326,66 @@ export default function FechamentoDetalhes() {
|
||||
{!loading ? (
|
||||
<div className="space-y-3">
|
||||
<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)}>
|
||||
{showFilters ? "Ocultar filtros" : "Exibir filtros"}
|
||||
</Button>
|
||||
@@ -1399,6 +1541,9 @@ export default function FechamentoDetalhes() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<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]">Ticket</TableHead>
|
||||
<TableHead className="min-w-[320px]">Descrição</TableHead>
|
||||
@@ -1415,25 +1560,47 @@ export default function FechamentoDetalhes() {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<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...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : tarefasFiltradas.length === 0 ? (
|
||||
<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.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
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>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={tarefa.estaRevisada}
|
||||
onCheckedChange={(checked) => void handleToggleAprovada(tarefa, Boolean(checked))}
|
||||
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>
|
||||
</TableCell>
|
||||
@@ -1478,18 +1645,6 @@ export default function FechamentoDetalhes() {
|
||||
<Pencil className="h-4 w-4" />
|
||||
<span className="ml-1">Editar</span>
|
||||
</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" ? (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -1671,7 +1826,7 @@ export default function FechamentoDetalhes() {
|
||||
onOpenChange={(open) => {
|
||||
setIsAlterarCompetenciaOpen(open);
|
||||
if (!open) {
|
||||
setTarefaAlteracaoCompetencia(null);
|
||||
setTarefasAlteracaoCompetencia([]);
|
||||
setMotivoAlteracaoCompetencia("");
|
||||
setCompetenciaDestinoId("");
|
||||
}
|
||||
@@ -1679,12 +1834,35 @@ export default function FechamentoDetalhes() {
|
||||
>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<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>
|
||||
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>
|
||||
</DialogHeader>
|
||||
<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">
|
||||
<Label>Competência atual</Label>
|
||||
<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()}
|
||||
disabled={savingAlteracaoCompetencia || !competenciaDestinoId}
|
||||
>
|
||||
{savingAlteracaoCompetencia ? "Alterando..." : "Confirmar alteração"}
|
||||
{savingAlteracaoCompetencia
|
||||
? "Alterando..."
|
||||
: tarefasAlteracaoCompetencia.length > 1
|
||||
? `Confirmar (${tarefasAlteracaoCompetencia.length})`
|
||||
: "Confirmar alteração"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -392,6 +392,27 @@ class FechamentoFechamentosService {
|
||||
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();
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user