novas atualizacoes do sistema de fechamento
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { ArrowLeft, Download, ExternalLink, FileSpreadsheet, FolderKanban, RefreshCcw } from "lucide-react";
|
||||
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";
|
||||
@@ -14,8 +22,10 @@ import {
|
||||
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";
|
||||
|
||||
@@ -35,14 +45,58 @@ function formatHoras(minutos: number | null): string {
|
||||
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[]>([]);
|
||||
const [importing, setImporting] = useState(false);
|
||||
/** 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");
|
||||
@@ -51,8 +105,13 @@ export default function CompetenciaFechamentos() {
|
||||
const loadFechamentos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fechamentoCompetenciasService.listarFechamentosDaCompetencia(competenciaId);
|
||||
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.";
|
||||
@@ -64,7 +123,7 @@ export default function CompetenciaFechamentos() {
|
||||
};
|
||||
|
||||
const handleImportarAsana = async () => {
|
||||
setImporting(true);
|
||||
setImportKind("sheet");
|
||||
try {
|
||||
const resultado = await fechamentoCompetenciasService.importarDoAsana(competenciaId);
|
||||
toast.success(
|
||||
@@ -75,7 +134,7 @@ export default function CompetenciaFechamentos() {
|
||||
const message = error instanceof Error ? error.message : "Erro ao importar tasks do Asana.";
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
setImportKind(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,7 +144,7 @@ export default function CompetenciaFechamentos() {
|
||||
return;
|
||||
}
|
||||
|
||||
setImporting(true);
|
||||
setImportKind("modal");
|
||||
try {
|
||||
const resultado = await fechamentoCompetenciasService.importarDoAsana(competenciaId, {
|
||||
modo: reprocessMode,
|
||||
@@ -106,7 +165,46 @@ export default function CompetenciaFechamentos() {
|
||||
const message = error instanceof Error ? error.message : "Erro ao executar reprocessamento do Asana.";
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,40 +248,63 @@ export default function CompetenciaFechamentos() {
|
||||
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-hgtx")}>
|
||||
<Button variant="ghost" size="sm" onClick={() => navigate("/fechamento")}>
|
||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||
Voltar
|
||||
</Button>
|
||||
</div>
|
||||
<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="mt-2 flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{fechamentos.length} fechamento(s)
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setIsReprocessModalOpen(true)}
|
||||
disabled={importing}
|
||||
>
|
||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||
Reprocessar Asana
|
||||
</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">
|
||||
<Card className="mx-auto mt-12 max-w-2xl border-border/80 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Nenhum fechamento encontrado</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -191,10 +312,14 @@ export default function CompetenciaFechamentos() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<div className="px-6 pb-6">
|
||||
<Button onClick={() => void handleImportarAsana()} disabled={importing}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{importing ? "Importando..." : "Importar tasks do Asana"}
|
||||
</Button>
|
||||
{importKind === "sheet" ? (
|
||||
<AsanaImportLoadingCard />
|
||||
) : (
|
||||
<Button onClick={() => void handleImportarAsana()} disabled={importing}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Importar tasks do Asana
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -213,8 +338,18 @@ export default function CompetenciaFechamentos() {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="py-8 text-center text-muted-foreground">
|
||||
Carregando fechamentos...
|
||||
<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>
|
||||
) : (
|
||||
@@ -238,8 +373,8 @@ export default function CompetenciaFechamentos() {
|
||||
<Badge
|
||||
className={
|
||||
row.status === "fechado"
|
||||
? "bg-green-500 hover:bg-green-500/80 text-white border-green-500"
|
||||
: "bg-yellow-500 hover:bg-yellow-500/80 text-black border-yellow-500"
|
||||
? "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"}
|
||||
@@ -250,6 +385,7 @@ export default function CompetenciaFechamentos() {
|
||||
<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)"
|
||||
@@ -260,8 +396,9 @@ export default function CompetenciaFechamentos() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="transition-colors hover:bg-muted/50 hover:text-foreground"
|
||||
onClick={() =>
|
||||
navigate(`/fechamento-hgtx/fechamentos/${row.id}`, {
|
||||
navigate(`/fechamento/fechamentos/${row.id}`, {
|
||||
state: { competenciaId, status: row.status },
|
||||
})
|
||||
}
|
||||
@@ -280,17 +417,57 @@ export default function CompetenciaFechamentos() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={isReprocessModalOpen} onOpenChange={setIsReprocessModalOpen}>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reprocessar Asana</DialogTitle>
|
||||
<DialogDescription>
|
||||
Escolha uma estratégia de reprocessamento para esta competência.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<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")}
|
||||
@@ -389,14 +566,95 @@ export default function CompetenciaFechamentos() {
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsReprocessModalOpen(false)} disabled={importing}>
|
||||
<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 ? "Processando..." : "Executar"}
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user