271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { ClipboardList, ExternalLink, Plus } 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 {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { fechamentoCompetenciasService, type CompetenciaItem } from "@/services/fechamento/competencias";
|
|
|
|
const meses = [
|
|
"Janeiro",
|
|
"Fevereiro",
|
|
"Março",
|
|
"Abril",
|
|
"Maio",
|
|
"Junho",
|
|
"Julho",
|
|
"Agosto",
|
|
"Setembro",
|
|
"Outubro",
|
|
"Novembro",
|
|
"Dezembro",
|
|
];
|
|
|
|
function formatCompetenciaMes(mes: number, ano: number): string {
|
|
return `${meses[mes - 1] ?? `Mês ${mes}`} / ${ano}`;
|
|
}
|
|
|
|
export default function Fechamentos() {
|
|
const navigate = useNavigate();
|
|
const currentYear = new Date().getFullYear();
|
|
const [allCompetencias, setAllCompetencias] = useState<CompetenciaItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [anoSelecionado, setAnoSelecionado] = useState<string>("");
|
|
const [modalAberto, setModalAberto] = useState(false);
|
|
const [mesSelecionado, setMesSelecionado] = useState<string>("");
|
|
const [anoNovo, setAnoNovo] = useState<string>("");
|
|
const [criando, setCriando] = useState(false);
|
|
|
|
const anosDisponiveis = useMemo(() => {
|
|
const anosUnicos = [...new Set(allCompetencias.map((c) => c.ano))];
|
|
if (!anosUnicos.includes(currentYear)) {
|
|
anosUnicos.push(currentYear);
|
|
}
|
|
return anosUnicos.sort((a, b) => b - a).map(String);
|
|
}, [allCompetencias, currentYear]);
|
|
|
|
const competenciasFiltradas = useMemo(() => {
|
|
if (!anoSelecionado) return [];
|
|
return allCompetencias.filter((c) => c.ano === Number(anoSelecionado));
|
|
}, [allCompetencias, anoSelecionado]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const loadCompetencias = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await fechamentoCompetenciasService.listarCompetencias({});
|
|
if (!cancelled) {
|
|
setAllCompetencias(data);
|
|
}
|
|
} catch (error) {
|
|
if (!cancelled) {
|
|
const message = error instanceof Error ? error.message : "Erro ao carregar competências.";
|
|
toast.error(message);
|
|
setAllCompetencias([]);
|
|
}
|
|
} finally {
|
|
if (!cancelled) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
void loadCompetencias();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (anosDisponiveis.length > 0 && !anoSelecionado) {
|
|
const defaultAno = anosDisponiveis.includes(String(currentYear))
|
|
? String(currentYear)
|
|
: anosDisponiveis[0];
|
|
setAnoSelecionado(defaultAno);
|
|
}
|
|
}, [anosDisponiveis, anoSelecionado, currentYear]);
|
|
|
|
const handleCriarCompetencia = async () => {
|
|
const mes = Number(mesSelecionado);
|
|
const ano = Number(anoNovo);
|
|
if (!mes || !ano) {
|
|
toast.error("Selecione mês e ano.");
|
|
return;
|
|
}
|
|
setCriando(true);
|
|
try {
|
|
const nova = await fechamentoCompetenciasService.criarCompetencia({ mes, ano });
|
|
setAllCompetencias((prev) => [...prev, nova]);
|
|
setModalAberto(false);
|
|
setMesSelecionado("");
|
|
setAnoNovo("");
|
|
toast.success("Competência criada com sucesso.");
|
|
setAnoSelecionado(String(ano));
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Erro ao criar competência.";
|
|
toast.error(message);
|
|
} finally {
|
|
setCriando(false);
|
|
}
|
|
};
|
|
|
|
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-4 flex flex-col items-start justify-between gap-3 md:flex-row md:items-center">
|
|
<h1 className="flex items-center gap-2 text-xl font-bold text-foreground md:text-3xl">
|
|
<ClipboardList className="h-5 w-5 md:h-6 md:w-6" />
|
|
Fechamentos
|
|
</h1>
|
|
<Dialog open={modalAberto} onOpenChange={setModalAberto}>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm">
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Nova Competência
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Nova Competência</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="grid grid-cols-2 gap-4 py-4">
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-sm font-medium">Mês</label>
|
|
<Select value={mesSelecionado} onValueChange={setMesSelecionado}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Selecione o mês" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{meses.map ((m, i) => (
|
|
<SelectItem key={i + 1} value={String(i + 1)}>
|
|
{m}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<label className="text-sm font-medium">Ano</label>
|
|
<Select value={anoNovo} onValueChange={setAnoNovo}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Selecione o ano" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{anosDisponiveis.map((ano) => (
|
|
<SelectItem key={ano} value={ano}>
|
|
{ano}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setModalAberto(false)} disabled={criando}>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={() => void handleCriarCompetencia()} disabled={criando || !mesSelecionado || !anoNovo}>
|
|
{criando ? "Criando..." : "Criar Competência"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-muted-foreground">Ano:</span>
|
|
<Select value={anoSelecionado} onValueChange={setAnoSelecionado}>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{anosDisponiveis.map((ano) => (
|
|
<SelectItem key={ano} value={ano}>
|
|
{ano}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto p-3 md:p-6">
|
|
{!loading && competenciasFiltradas.length === 0 ? (
|
|
<Card className="mx-auto mt-12 max-w-2xl">
|
|
<CardHeader>
|
|
<CardTitle>Nenhuma competência encontrada</CardTitle>
|
|
<CardDescription>Não há competências cadastradas para o ano selecionado.</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
) : (
|
|
<div className="overflow-x-auto rounded-lg border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="min-w-[220px]">Mês</TableHead>
|
|
<TableHead className="min-w-[180px]">Quantidade de Fechamentos</TableHead>
|
|
<TableHead className="min-w-[120px]">Status</TableHead>
|
|
<TableHead className="text-center">Acessar</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={4} className="py-8 text-center text-muted-foreground">
|
|
Carregando competências...
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
competenciasFiltradas.map((competencia) => (
|
|
<TableRow key={competencia.id}>
|
|
<TableCell className="font-medium">
|
|
{formatCompetenciaMes(competencia.mes, competencia.ano)}
|
|
</TableCell>
|
|
<TableCell>{competencia.quantidadeFechamentos}</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
className={
|
|
competencia.status === "concluido"
|
|
? "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"
|
|
}
|
|
>
|
|
{competencia.status === "concluido" ? "Concluída" : "Em aberto"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex justify-center">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => navigate(`/fechamento-hgtx/competencias/${competencia.id}`)}
|
|
>
|
|
<ExternalLink className="mr-2 h-4 w-4" />
|
|
Acessar
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|