665 lines
27 KiB
TypeScript
665 lines
27 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import {
|
|
ArrowLeft,
|
|
Download,
|
|
ExternalLink,
|
|
FileSpreadsheet,
|
|
FolderKanban,
|
|
Loader2,
|
|
RefreshCcw,
|
|
} 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 { 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, type FechamentoDaCompetenciaItem } from "@/services/fechamento/competencias";
|
|
import { fechamentoFechamentosService } from "@/services/fechamento/fechamentos";
|
|
|
|
function getDisplayNome(row: FechamentoDaCompetenciaItem): string {
|
|
if (row.parceiroCodinome?.trim()) {
|
|
return `${row.parceiroNome} (${row.parceiroCodinome.trim()})`;
|
|
}
|
|
return row.parceiroNome;
|
|
}
|
|
|
|
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 AsanaImportLoadingCard() {
|
|
return (
|
|
<div
|
|
className="flex flex-col items-center gap-6 py-10 text-center"
|
|
role="status"
|
|
aria-live="polite"
|
|
aria-busy="true"
|
|
>
|
|
<div className="relative flex h-20 w-20 items-center justify-center">
|
|
<span className="absolute inset-0 rounded-full border-4 border-muted" />
|
|
<span className="absolute inset-0 animate-spin rounded-full border-4 border-transparent border-t-primary" />
|
|
<Download className="relative h-8 w-8 text-primary" aria-hidden />
|
|
</div>
|
|
<div className="max-w-sm space-y-2">
|
|
<p className="text-lg font-semibold text-foreground">Sincronizando com o Asana</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Buscando tarefas concluídas no período da competência e montando os fechamentos. Pode levar um minuto —
|
|
não feche esta página.
|
|
</p>
|
|
</div>
|
|
<div className="flex w-full max-w-xs flex-col gap-2">
|
|
<div className="h-2 animate-pulse rounded-full bg-muted" />
|
|
<div className="mx-auto h-2 w-[85%] animate-pulse rounded-full bg-muted" />
|
|
<div className="mx-auto h-2 w-[60%] animate-pulse rounded-full bg-muted" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function reprocessamentoLabel(modo: "reprocessar_tudo" | "reprocessar_alguns" | "buscar_novos_fechamentos"): string {
|
|
if (modo === "reprocessar_tudo") return "Reimportando toda a competência a partir do Asana.";
|
|
if (modo === "reprocessar_alguns") return "Atualizando os parceiros selecionados no Asana.";
|
|
return "Buscando novos fechamentos no Asana.";
|
|
}
|
|
|
|
export default function CompetenciaFechamentos() {
|
|
const { me } = useAuthAccess();
|
|
const { id: competenciaId = "" } = useParams();
|
|
const navigate = useNavigate();
|
|
const [loading, setLoading] = useState(true);
|
|
const [fechamentos, setFechamentos] = useState<FechamentoDaCompetenciaItem[]>([]);
|
|
/** Onde a operação longa do Asana foi disparada (para mensagem e layout de loading). */
|
|
const [importKind, setImportKind] = useState<"sheet" | "modal" | null>(null);
|
|
const importing = importKind !== null;
|
|
const [exportingFechamentoId, setExportingFechamentoId] = useState<string | null>(null);
|
|
const [isReprocessModalOpen, setIsReprocessModalOpen] = useState(false);
|
|
const [isConcluirModalOpen, setIsConcluirModalOpen] = useState(false);
|
|
const [isReabrirModalOpen, setIsReabrirModalOpen] = useState(false);
|
|
const [concluindoCompetencia, setConcluindoCompetencia] = useState(false);
|
|
const [reabrindoCompetencia, setReabrindoCompetencia] = useState(false);
|
|
const [motivoReabertura, setMotivoReabertura] = useState("");
|
|
const [competenciaStatus, setCompetenciaStatus] = useState<"em_aberto" | "concluido">("em_aberto");
|
|
const [reprocessMode, setReprocessMode] = useState<
|
|
"reprocessar_tudo" | "reprocessar_alguns" | "buscar_novos_fechamentos"
|
|
>("reprocessar_tudo");
|
|
const [selectedParceiroIds, setSelectedParceiroIds] = useState<string[]>([]);
|
|
|
|
const loadFechamentos = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [data, competencias] = await Promise.all([
|
|
fechamentoCompetenciasService.listarFechamentosDaCompetencia(competenciaId),
|
|
fechamentoCompetenciasService.listarCompetencias({}),
|
|
]);
|
|
const competenciaAtual = competencias.find((item) => item.id === competenciaId);
|
|
setFechamentos(data);
|
|
setCompetenciaStatus(competenciaAtual?.status ?? "em_aberto");
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Erro ao carregar fechamentos da competência.";
|
|
toast.error(message);
|
|
setFechamentos([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleImportarAsana = async () => {
|
|
setImportKind("sheet");
|
|
try {
|
|
const resultado = await fechamentoCompetenciasService.importarDoAsana(competenciaId);
|
|
toast.success(
|
|
`Importação concluída: ${resultado.tarefasImportadas} tasks, ${resultado.fechamentosCriados} fechamentos criados.`,
|
|
);
|
|
await loadFechamentos();
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Erro ao importar tasks do Asana.";
|
|
toast.error(message);
|
|
} finally {
|
|
setImportKind(null);
|
|
}
|
|
};
|
|
|
|
const handleExecutarReprocessamento = async () => {
|
|
if (reprocessMode === "reprocessar_alguns" && selectedParceiroIds.length === 0) {
|
|
toast.error("Selecione ao menos um fechamento/parceiro para reprocessar.");
|
|
return;
|
|
}
|
|
|
|
setImportKind("modal");
|
|
try {
|
|
const resultado = await fechamentoCompetenciasService.importarDoAsana(competenciaId, {
|
|
modo: reprocessMode,
|
|
parceiroIds: reprocessMode === "reprocessar_alguns" ? selectedParceiroIds : undefined,
|
|
});
|
|
if (reprocessMode === "buscar_novos_fechamentos") {
|
|
toast.success(
|
|
`Busca concluída: ${resultado.fechamentosCriados} novos fechamento(s) criado(s), sem alterar os atuais.`,
|
|
);
|
|
} else {
|
|
toast.success(
|
|
`Reprocessamento concluído: ${resultado.tarefasImportadas} tasks processadas e ${resultado.fechamentosCriados} fechamento(s) criado(s).`,
|
|
);
|
|
}
|
|
setIsReprocessModalOpen(false);
|
|
await loadFechamentos();
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Erro ao executar reprocessamento do Asana.";
|
|
toast.error(message);
|
|
} finally {
|
|
setImportKind(null);
|
|
}
|
|
};
|
|
|
|
const handleConcluirCompetencia = async () => {
|
|
if (!me?.id) {
|
|
toast.error("Não foi possível identificar o usuário para concluir a competência.");
|
|
return;
|
|
}
|
|
try {
|
|
setConcluindoCompetencia(true);
|
|
await fechamentoCompetenciasService.concluirCompetencia(competenciaId, me.id);
|
|
toast.success("Competência concluída com sucesso.");
|
|
setIsConcluirModalOpen(false);
|
|
await loadFechamentos();
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Erro ao concluir competência.";
|
|
toast.error(message);
|
|
} finally {
|
|
setConcluindoCompetencia(false);
|
|
}
|
|
};
|
|
|
|
const handleReabrirCompetencia = async () => {
|
|
if (!me?.id) {
|
|
toast.error("Não foi possível identificar o usuário para reabrir a competência.");
|
|
return;
|
|
}
|
|
try {
|
|
setReabrindoCompetencia(true);
|
|
await fechamentoCompetenciasService.reabrirCompetencia(competenciaId, me.id, motivoReabertura.trim() || undefined);
|
|
toast.success("Competência reaberta com sucesso.");
|
|
setIsReabrirModalOpen(false);
|
|
setMotivoReabertura("");
|
|
await loadFechamentos();
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Erro ao reabrir competência.";
|
|
toast.error(message);
|
|
} finally {
|
|
setReabrindoCompetencia(false);
|
|
}
|
|
};
|
|
|
|
const handleExportar = async (fechamentoId: string) => {
|
|
try {
|
|
setExportingFechamentoId(fechamentoId);
|
|
const { buffer, filename } = await fechamentoFechamentosService.exportarPlanilha(fechamentoId);
|
|
const blob = new Blob([buffer], {
|
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename ?? `fechamento-${fechamentoId}.xlsx`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
toast.success("Planilha exportada com sucesso.");
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Erro ao exportar planilha.";
|
|
toast.error(message);
|
|
} finally {
|
|
setExportingFechamentoId(null);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
if (competenciaId) {
|
|
void loadFechamentos();
|
|
}
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [competenciaId]);
|
|
|
|
useEffect(() => {
|
|
setSelectedParceiroIds(fechamentos.map((f) => f.parceiroId));
|
|
}, [fechamentos]);
|
|
|
|
const temFechamentos = fechamentos.length > 0;
|
|
|
|
return (
|
|
<div className="flex h-full flex-col bg-background pb-16 md:pb-0">
|
|
<div className="border-b border-border p-3 md:p-6">
|
|
<div className="mb-3 flex items-center gap-2">
|
|
<Button variant="ghost" size="sm" onClick={() => navigate("/fechamento")}>
|
|
<ArrowLeft className="mr-1 h-4 w-4" />
|
|
Voltar
|
|
</Button>
|
|
</div>
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<h1 className="flex items-center gap-2 text-xl font-bold text-foreground md:text-3xl">
|
|
<FolderKanban className="h-5 w-5 md:h-6 md:w-6" />
|
|
Fechamentos da Competência
|
|
</h1>
|
|
{!loading && (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<p className="text-sm text-muted-foreground">{fechamentos.length} fechamento(s)</p>
|
|
{competenciaStatus === "concluido" ? (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => setIsReabrirModalOpen(true)}
|
|
disabled={reabrindoCompetencia}
|
|
>
|
|
{reabrindoCompetencia ? "Reabrindo..." : "Reabrir competência"}
|
|
</Button>
|
|
) : temFechamentos ? (
|
|
<>
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
onClick={() => setIsReprocessModalOpen(true)}
|
|
disabled={importing}
|
|
>
|
|
<RefreshCcw className="mr-2 h-4 w-4" />
|
|
Reprocessar Asana
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => setIsConcluirModalOpen(true)}
|
|
disabled={concluindoCompetencia}
|
|
>
|
|
{concluindoCompetencia ? "Concluindo..." : "Concluir competência"}
|
|
</Button>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto p-3 md:p-6">
|
|
{!loading && fechamentos.length === 0 ? (
|
|
<Card className="mx-auto mt-12 max-w-2xl border-border/80 shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle>Nenhum fechamento encontrado</CardTitle>
|
|
<CardDescription>
|
|
Importe as tasks do Asana para criar os fechamentos automaticamente.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<div className="px-6 pb-6">
|
|
{importKind === "sheet" ? (
|
|
<AsanaImportLoadingCard />
|
|
) : (
|
|
<Button onClick={() => void handleImportarAsana()} disabled={importing}>
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Importar tasks do Asana
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<div className="overflow-x-auto rounded-lg border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="min-w-[100px]">Logo</TableHead>
|
|
<TableHead className="min-w-[280px]">Nome</TableHead>
|
|
<TableHead className="min-w-[140px]">Pontuação Total</TableHead>
|
|
<TableHead className="min-w-[120px]">Horas Total</TableHead>
|
|
<TableHead className="min-w-[120px]">Status</TableHead>
|
|
<TableHead className="text-center">Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="py-16">
|
|
<div
|
|
className="flex flex-col items-center justify-center gap-4 text-center"
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
<Loader2 className="h-9 w-9 animate-spin text-primary" aria-hidden />
|
|
<div>
|
|
<p className="font-medium text-foreground">Carregando fechamentos</p>
|
|
<p className="mt-1 text-sm text-muted-foreground">Aguarde um instante.</p>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
fechamentos.map((row) => (
|
|
<TableRow key={row.id}>
|
|
<TableCell>
|
|
{row.parceiroLogoUrl ? (
|
|
<img
|
|
src={row.parceiroLogoUrl}
|
|
alt={`Logo ${row.parceiroNome}`}
|
|
className="h-8 w-8 rounded-md object-cover"
|
|
/>
|
|
) : (
|
|
"—"
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="font-medium">{getDisplayNome(row)}</TableCell>
|
|
<TableCell>{row.pontuacaoTotalEntregue}</TableCell>
|
|
<TableCell>{formatHoras(row.horasTotal * 60)}</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
className={
|
|
row.status === "fechado"
|
|
? "border-emerald-600/30 bg-emerald-50 font-normal text-emerald-800 hover:bg-emerald-50 dark:bg-emerald-950/40 dark:text-emerald-200"
|
|
: "border-slate-500/25 bg-slate-100 font-normal text-slate-700 hover:bg-slate-100 dark:bg-slate-800/60 dark:text-slate-200"
|
|
}
|
|
>
|
|
{row.status === "fechado" ? "Fechado" : "Em aberto"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex justify-center gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="transition-colors hover:bg-muted/50 hover:text-foreground"
|
|
onClick={() => void handleExportar(row.id)}
|
|
disabled={exportingFechamentoId === row.id || row.status !== "fechado"}
|
|
title="Exportar planilha financeira (XLSX)"
|
|
>
|
|
<FileSpreadsheet className="mr-2 h-4 w-4" />
|
|
{exportingFechamentoId === row.id ? "Exportando..." : "Exportar"}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="transition-colors hover:bg-muted/50 hover:text-foreground"
|
|
onClick={() =>
|
|
navigate(`/fechamento/fechamentos/${row.id}`, {
|
|
state: { competenciaId, status: row.status },
|
|
})
|
|
}
|
|
>
|
|
<ExternalLink className="mr-2 h-4 w-4" />
|
|
Acessar
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Dialog
|
|
open={isReprocessModalOpen}
|
|
onOpenChange={(open) => {
|
|
if (!open && importing) return;
|
|
setIsReprocessModalOpen(open);
|
|
}}
|
|
>
|
|
<DialogContent
|
|
className="flex min-h-0 max-h-[85vh] w-[calc(100vw-1.5rem)] max-w-3xl flex-col gap-0 overflow-hidden p-0 sm:w-full"
|
|
hideClose={importing}
|
|
onPointerDownOutside={(e) => {
|
|
if (importing) e.preventDefault();
|
|
}}
|
|
onEscapeKeyDown={(e) => {
|
|
if (importing) e.preventDefault();
|
|
}}
|
|
>
|
|
{importKind === "modal" ? (
|
|
<div
|
|
className="absolute inset-0 z-[60] flex flex-col items-center justify-center gap-4 rounded-b-lg rounded-t-lg bg-background/90 p-6 text-center backdrop-blur-sm sm:rounded-lg"
|
|
role="status"
|
|
aria-live="polite"
|
|
aria-busy="true"
|
|
>
|
|
<div className="relative flex h-16 w-16 items-center justify-center">
|
|
<span className="absolute inset-0 rounded-full border-4 border-muted" />
|
|
<span className="absolute inset-0 animate-spin rounded-full border-4 border-transparent border-t-primary" />
|
|
<RefreshCcw className="relative h-7 w-7 text-primary" aria-hidden />
|
|
</div>
|
|
<div className="max-w-md space-y-2">
|
|
<p className="text-lg font-semibold text-foreground">Processando no Asana</p>
|
|
<p className="text-sm text-muted-foreground">{reprocessamentoLabel(reprocessMode)}</p>
|
|
<p className="text-xs text-muted-foreground">Não feche esta janela até a operação terminar.</p>
|
|
</div>
|
|
<div className="flex w-full max-w-xs flex-col gap-2">
|
|
<div className="h-1.5 animate-pulse rounded-full bg-muted" />
|
|
<div className="mx-auto h-1.5 w-[75%] animate-pulse rounded-full bg-muted" />
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col gap-4 px-6 pb-0 pt-12 sm:pt-14">
|
|
<DialogHeader className="shrink-0 space-y-1.5 pr-8 text-left sm:pr-0">
|
|
<DialogTitle>Reprocessar Asana</DialogTitle>
|
|
<DialogDescription>
|
|
Escolha uma estratégia de reprocessamento para esta competência.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto pb-4 pr-1">
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setReprocessMode("reprocessar_tudo")}
|
|
className={`rounded-lg border p-3 text-left transition ${
|
|
reprocessMode === "reprocessar_tudo"
|
|
? "border-primary bg-primary/5"
|
|
: "border-border hover:border-primary/60"
|
|
}`}
|
|
>
|
|
<p className="text-sm font-semibold">Reprocessar tudo</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
Remove fechamentos/tarefas atuais da competência e importa tudo novamente do zero.
|
|
</p>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setReprocessMode("reprocessar_alguns")}
|
|
className={`rounded-lg border p-3 text-left transition ${
|
|
reprocessMode === "reprocessar_alguns"
|
|
? "border-primary bg-primary/5"
|
|
: "border-border hover:border-primary/60"
|
|
}`}
|
|
>
|
|
<p className="text-sm font-semibold">Reprocessar apenas alguns</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
Reprocessa somente os parceiros selecionados abaixo.
|
|
</p>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setReprocessMode("buscar_novos_fechamentos")}
|
|
className={`rounded-lg border p-3 text-left transition ${
|
|
reprocessMode === "buscar_novos_fechamentos"
|
|
? "border-primary bg-primary/5"
|
|
: "border-border hover:border-primary/60"
|
|
}`}
|
|
>
|
|
<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.
|
|
</p>
|
|
</button>
|
|
</div>
|
|
|
|
{reprocessMode === "reprocessar_alguns" ? (
|
|
<div className="max-h-72 space-y-3 overflow-y-auto rounded-md border p-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<p className="text-sm font-medium">Selecione os fechamentos/parceiros</p>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setSelectedParceiroIds(Array.from(new Set(fechamentos.map((f) => f.parceiroId))))}
|
|
>
|
|
Marcar todos
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setSelectedParceiroIds([])}
|
|
>
|
|
Limpar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{fechamentos.map((f) => {
|
|
const checked = selectedParceiroIds.includes(f.parceiroId);
|
|
return (
|
|
<label
|
|
key={f.id}
|
|
htmlFor={`sel-${f.id}`}
|
|
className={`flex cursor-pointer items-center space-x-3 rounded-md border p-2 transition ${
|
|
checked ? "border-primary bg-primary/5" : "border-border hover:border-primary/50"
|
|
}`}
|
|
>
|
|
<Checkbox
|
|
id={`sel-${f.id}`}
|
|
checked={checked}
|
|
onCheckedChange={(value) => {
|
|
const on = Boolean(value);
|
|
setSelectedParceiroIds((prev) =>
|
|
on ? Array.from(new Set([...prev, f.parceiroId])) : prev.filter((id) => id !== f.parceiroId),
|
|
);
|
|
}}
|
|
/>
|
|
<Label htmlFor={`sel-${f.id}`} className="cursor-pointer text-sm">
|
|
{getDisplayNome(f)}
|
|
<span className="ml-2 text-xs text-muted-foreground">
|
|
{f.status === "fechado" ? "Fechado" : "Em aberto"}
|
|
</span>
|
|
</Label>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="shrink-0 gap-2 border-t bg-background p-4 sm:justify-end">
|
|
<Button
|
|
variant="outline"
|
|
className="hover:bg-muted/60 hover:text-foreground"
|
|
onClick={() => setIsReprocessModalOpen(false)}
|
|
disabled={importing}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={() => void handleExecutarReprocessamento()} disabled={importing}>
|
|
{importing ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
|
Processando...
|
|
</>
|
|
) : (
|
|
"Executar"
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={isReabrirModalOpen} onOpenChange={setIsReabrirModalOpen}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Reabrir competência</DialogTitle>
|
|
<DialogDescription>
|
|
Ao reabrir, o reprocessamento do Asana e as edições dos fechamentos voltam a ficar disponíveis.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="motivo-reabrir-competencia">Motivo (opcional)</Label>
|
|
<Input
|
|
id="motivo-reabrir-competencia"
|
|
value={motivoReabertura}
|
|
onChange={(e) => setMotivoReabertura(e.target.value)}
|
|
placeholder="Ex.: correção de ajustes pós-fechamento"
|
|
/>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
className="hover:bg-muted/60 hover:text-foreground"
|
|
onClick={() => setIsReabrirModalOpen(false)}
|
|
disabled={reabrindoCompetencia}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={() => void handleReabrirCompetencia()} disabled={reabrindoCompetencia}>
|
|
{reabrindoCompetencia ? "Reabrindo..." : "Confirmar reabertura"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={isConcluirModalOpen} onOpenChange={setIsConcluirModalOpen}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Concluir competência</DialogTitle>
|
|
<DialogDescription>
|
|
Deseja concluir esta competência? A operação só será permitida se todos os fechamentos estiverem fechados.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
className="hover:bg-muted/60 hover:text-foreground"
|
|
onClick={() => setIsConcluirModalOpen(false)}
|
|
disabled={concluindoCompetencia}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
onClick={() => void handleConcluirCompetencia()}
|
|
disabled={concluindoCompetencia}
|
|
className="min-w-[172px] shadow-lg hover:shadow-primary/50"
|
|
>
|
|
{concluindoCompetencia ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Concluindo...
|
|
</>
|
|
) : (
|
|
"Confirmar conclusão"
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|