Files
OPEN_CODEX_API/src/modules/fechamento-hgtx/pages/Fechamentos.tsx
T

279 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))
.slice()
.sort((a, b) => a.mes - b.mes);
}, [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="flex flex-col 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 className="w-full">
<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 className="w-full">
<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"
className="hover:bg-muted/60 hover:text-foreground"
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 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"
? "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"
}
>
{competencia.status === "concluido" ? "Concluída" : "Em aberto"}
</Badge>
</TableCell>
<TableCell>
<div className="flex justify-center">
<Button
size="sm"
variant="outline"
onClick={() => navigate(`/intelligence-score/competencias/${competencia.id}`)}
>
<ExternalLink className="mr-2 h-4 w-4" />
Acessar
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)}
</div>
</div>
);
}