2025-10-20 20:35:24 +00:00
|
|
|
"use client";
|
2025-10-08 04:19:15 +00:00
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
import { LoaderPinwheel, ChevronDown, ChevronRight, Filter } from "lucide-react";
|
2025-10-20 20:35:24 +00:00
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import AnaliticoComponent from "./analitico";
|
2025-10-21 03:49:20 +00:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
2025-10-21 20:28:50 +00:00
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
2025-10-21 03:49:20 +00:00
|
|
|
import {
|
|
|
|
|
Sheet,
|
|
|
|
|
SheetContent,
|
|
|
|
|
SheetDescription,
|
|
|
|
|
SheetFooter,
|
|
|
|
|
SheetHeader,
|
|
|
|
|
SheetTitle,
|
|
|
|
|
SheetTrigger,
|
|
|
|
|
} from "@/components/ui/sheet";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
2025-10-08 04:19:15 +00:00
|
|
|
|
|
|
|
|
interface DREItem {
|
|
|
|
|
codfilial: string;
|
|
|
|
|
data_competencia: string;
|
|
|
|
|
data_cai: string;
|
|
|
|
|
grupo: string;
|
|
|
|
|
subgrupo: string;
|
|
|
|
|
centro_custo: string;
|
|
|
|
|
codigo_conta: number;
|
|
|
|
|
conta: string;
|
|
|
|
|
valor: string;
|
2025-10-21 03:07:52 +00:00
|
|
|
codgrupo?: string;
|
|
|
|
|
isCalculado?: boolean;
|
2025-10-08 04:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HierarchicalRow {
|
2025-10-20 20:35:24 +00:00
|
|
|
type: "grupo" | "subgrupo" | "centro_custo" | "conta";
|
2025-10-08 04:19:15 +00:00
|
|
|
level: number;
|
|
|
|
|
grupo?: string;
|
|
|
|
|
subgrupo?: string;
|
|
|
|
|
centro_custo?: string;
|
|
|
|
|
conta?: string;
|
|
|
|
|
codigo_conta?: number;
|
|
|
|
|
total?: number;
|
|
|
|
|
isExpanded?: boolean;
|
|
|
|
|
valoresPorMes?: Record<string, number>;
|
2025-10-08 20:32:36 +00:00
|
|
|
percentuaisPorMes?: Record<string, number>;
|
2025-10-21 14:23:00 +00:00
|
|
|
isCalculado?: boolean;
|
2025-10-08 04:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function Teste() {
|
|
|
|
|
const [data, setData] = useState<DREItem[]>([]);
|
2025-10-21 03:49:20 +00:00
|
|
|
const [loading, setLoading] = useState(false);
|
2025-10-08 04:19:15 +00:00
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
|
|
|
|
const [expandedSubgrupos, setExpandedSubgrupos] = useState<Set<string>>(
|
|
|
|
|
new Set()
|
|
|
|
|
);
|
|
|
|
|
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(
|
|
|
|
|
new Set()
|
|
|
|
|
);
|
|
|
|
|
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
|
|
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
// Estados para filtros
|
|
|
|
|
const [filtros, setFiltros] = useState({
|
|
|
|
|
periodoDe: "",
|
|
|
|
|
periodoAte: "",
|
|
|
|
|
grupo: "Todos",
|
|
|
|
|
subgrupo: "Todos",
|
|
|
|
|
centroCusto: "Todos",
|
|
|
|
|
conta: "Todas",
|
|
|
|
|
valorMin: "",
|
|
|
|
|
valorMax: "",
|
|
|
|
|
buscaTextual: ""
|
|
|
|
|
});
|
2025-10-21 20:28:50 +00:00
|
|
|
|
|
|
|
|
// Estados para multi-seleção
|
|
|
|
|
const [centrosCustoSelecionados, setCentrosCustoSelecionados] = useState<string[]>([]);
|
|
|
|
|
const [contasSelecionadas, setContasSelecionadas] = useState<string[]>([]);
|
2025-10-21 03:49:20 +00:00
|
|
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
|
|
|
|
const [dadosFiltrados, setDadosFiltrados] = useState<DREItem[]>([]);
|
|
|
|
|
const [filtrosAplicados, setFiltrosAplicados] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Estados para opções dos filtros
|
|
|
|
|
const [opcoesGrupos, setOpcoesGrupos] = useState<string[]>([]);
|
|
|
|
|
const [opcoesSubgrupos, setOpcoesSubgrupos] = useState<string[]>([]);
|
|
|
|
|
const [opcoesCentrosCusto, setOpcoesCentrosCusto] = useState<string[]>([]);
|
|
|
|
|
const [opcoesContas, setOpcoesContas] = useState<string[]>([]);
|
|
|
|
|
|
2025-10-08 05:08:35 +00:00
|
|
|
// Estados para analítico
|
|
|
|
|
const [analiticoFiltros, setAnaliticoFiltros] = useState({
|
2025-10-20 20:35:24 +00:00
|
|
|
dataInicio: "",
|
|
|
|
|
dataFim: "",
|
|
|
|
|
centroCusto: "",
|
|
|
|
|
codigoGrupo: "",
|
|
|
|
|
codigoSubgrupo: "",
|
|
|
|
|
codigoConta: "",
|
2025-10-21 20:57:58 +00:00
|
|
|
linhaSelecionada: "", // Adicionar informação da linha selecionada
|
2025-10-08 05:08:35 +00:00
|
|
|
});
|
2025-10-08 05:30:37 +00:00
|
|
|
const [linhaSelecionada, setLinhaSelecionada] = useState<string | null>(null);
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
useEffect(() => {
|
2025-10-21 03:49:20 +00:00
|
|
|
// Carregar períodos disponíveis da API
|
|
|
|
|
carregarPeriodosDisponiveis();
|
|
|
|
|
|
|
|
|
|
// Inicializar filtros com período atual
|
|
|
|
|
const agora = new Date();
|
|
|
|
|
const anoAtual = agora.getFullYear();
|
|
|
|
|
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const periodoAtual = `${anoAtual}-${mesAtual}`;
|
|
|
|
|
|
|
|
|
|
setFiltros(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
periodoDe: `${anoAtual}-01`,
|
|
|
|
|
periodoAte: periodoAtual
|
|
|
|
|
}));
|
2025-10-08 04:19:15 +00:00
|
|
|
}, []);
|
|
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
const carregarPeriodosDisponiveis = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/dre-oracle");
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Erro HTTP: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dadosCompletos = await response.json();
|
|
|
|
|
|
|
|
|
|
// Extrair períodos únicos dos dados
|
2025-10-21 15:36:59 +00:00
|
|
|
const periodosUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.data_competencia))].sort() as string[];
|
2025-10-21 03:49:20 +00:00
|
|
|
setMesesDisponiveis(periodosUnicos);
|
|
|
|
|
|
|
|
|
|
// Extrair grupos únicos
|
2025-10-21 15:36:59 +00:00
|
|
|
const gruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.grupo))].sort() as string[];
|
2025-10-21 03:49:20 +00:00
|
|
|
setOpcoesGrupos(gruposUnicos);
|
|
|
|
|
|
|
|
|
|
// Extrair subgrupos únicos
|
2025-10-21 15:36:59 +00:00
|
|
|
const subgruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.subgrupo))].sort() as string[];
|
2025-10-21 03:49:20 +00:00
|
|
|
setOpcoesSubgrupos(subgruposUnicos);
|
|
|
|
|
|
|
|
|
|
// Extrair centros de custo únicos
|
2025-10-21 15:36:59 +00:00
|
|
|
const centrosCustoUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.centro_custo))].sort() as string[];
|
2025-10-21 03:49:20 +00:00
|
|
|
setOpcoesCentrosCusto(centrosCustoUnicos);
|
|
|
|
|
|
|
|
|
|
// Extrair contas únicas
|
2025-10-21 15:36:59 +00:00
|
|
|
const contasUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.conta))].sort() as string[];
|
2025-10-21 03:49:20 +00:00
|
|
|
setOpcoesContas(contasUnicas);
|
|
|
|
|
|
2025-10-21 20:28:50 +00:00
|
|
|
// Inicializar com todos os itens selecionados
|
|
|
|
|
setCentrosCustoSelecionados(centrosCustoUnicos);
|
|
|
|
|
setContasSelecionadas(contasUnicas);
|
|
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Erro ao carregar períodos:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
2025-10-08 05:08:35 +00:00
|
|
|
setError(null);
|
2025-10-21 02:43:13 +00:00
|
|
|
const response = await fetch("/api/dre-oracle");
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
if (!response.ok) {
|
2025-10-08 05:08:35 +00:00
|
|
|
throw new Error(`Erro ao carregar dados: ${response.status}`);
|
2025-10-08 04:19:15 +00:00
|
|
|
}
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
const result = await response.json();
|
|
|
|
|
setData(result);
|
|
|
|
|
|
|
|
|
|
// Extrair meses únicos dos dados
|
|
|
|
|
const meses = [
|
|
|
|
|
...new Set(
|
|
|
|
|
result.map((item: DREItem) => {
|
|
|
|
|
const dataCompetencia = new Date(item.data_competencia);
|
|
|
|
|
return `${dataCompetencia.getFullYear()}-${String(
|
|
|
|
|
dataCompetencia.getMonth() + 1
|
2025-10-20 20:35:24 +00:00
|
|
|
).padStart(2, "0")}`;
|
2025-10-08 04:19:15 +00:00
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
].sort() as string[];
|
|
|
|
|
|
|
|
|
|
setMesesDisponiveis(meses);
|
|
|
|
|
} catch (err) {
|
2025-10-20 20:35:24 +00:00
|
|
|
setError(err instanceof Error ? err.message : "Erro desconhecido");
|
2025-10-08 04:19:15 +00:00
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (value: string | number) => {
|
2025-10-20 20:35:24 +00:00
|
|
|
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
|
|
|
|
return numValue.toLocaleString("pt-BR", {
|
|
|
|
|
style: "currency",
|
|
|
|
|
currency: "BRL",
|
2025-10-08 04:19:15 +00:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 12:28:20 +00:00
|
|
|
const formatCurrencyWithColor = (value: string | number) => {
|
2025-10-20 20:35:24 +00:00
|
|
|
const numValue = typeof value === "string" ? parseFloat(value) : value;
|
2025-10-08 12:28:20 +00:00
|
|
|
const formatted = formatCurrency(value);
|
|
|
|
|
const isNegative = numValue < 0;
|
|
|
|
|
return { formatted, isNegative };
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 05:08:35 +00:00
|
|
|
// Função para extrair códigos dos grupos e subgrupos
|
|
|
|
|
const extractCodes = (grupo: string, subgrupo?: string) => {
|
|
|
|
|
const grupoMatch = grupo.match(/^(\d+)/);
|
2025-10-20 20:35:24 +00:00
|
|
|
const codigoGrupo = grupoMatch ? grupoMatch[1] : "";
|
2025-10-08 05:08:35 +00:00
|
|
|
|
2025-10-20 20:35:24 +00:00
|
|
|
let codigoSubgrupo = "";
|
2025-10-08 05:08:35 +00:00
|
|
|
if (subgrupo) {
|
2025-10-08 05:40:47 +00:00
|
|
|
// Primeiro tenta extrair código numérico (ex: "001.008 - LOJA 8" -> "001.008")
|
2025-10-08 05:08:35 +00:00
|
|
|
const subgrupoMatch = subgrupo.match(/^(\d+(?:\.\d+)+)/);
|
2025-10-08 05:40:47 +00:00
|
|
|
if (subgrupoMatch) {
|
|
|
|
|
codigoSubgrupo = subgrupoMatch[1];
|
|
|
|
|
} else {
|
|
|
|
|
// Se não tem código numérico, usa a descrição completa (ex: "DESPESAS ADMINISTRATIVAS")
|
|
|
|
|
codigoSubgrupo = subgrupo;
|
|
|
|
|
}
|
2025-10-08 05:08:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { codigoGrupo, codigoSubgrupo };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Função para lidar com clique nas linhas
|
2025-10-08 05:30:37 +00:00
|
|
|
const handleRowClick = (row: HierarchicalRow, mesSelecionado?: string) => {
|
2025-10-21 13:16:50 +00:00
|
|
|
console.log('🖱️ Clique na linha:', row);
|
|
|
|
|
console.log('📅 Mês selecionado:', mesSelecionado);
|
|
|
|
|
|
|
|
|
|
if (!data.length) {
|
|
|
|
|
console.log('⚠️ Sem dados disponíveis');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-08 05:08:35 +00:00
|
|
|
|
|
|
|
|
// Pegar todas as datas disponíveis para definir o período
|
|
|
|
|
const datas = data.map((item) => item.data_competencia);
|
|
|
|
|
const dataInicio = Math.min(...datas.map((d) => new Date(d).getTime()));
|
|
|
|
|
const dataFim = Math.max(...datas.map((d) => new Date(d).getTime()));
|
|
|
|
|
|
|
|
|
|
const dataInicioStr = new Date(dataInicio).toISOString().substring(0, 7); // YYYY-MM
|
|
|
|
|
const dataFimStr = new Date(dataFim).toISOString().substring(0, 7); // YYYY-MM
|
|
|
|
|
|
2025-10-21 13:16:50 +00:00
|
|
|
console.log('📅 Datas calculadas:', { dataInicioStr, dataFimStr });
|
|
|
|
|
|
2025-10-08 05:08:35 +00:00
|
|
|
const { codigoGrupo, codigoSubgrupo } = extractCodes(
|
2025-10-20 20:35:24 +00:00
|
|
|
row.grupo || "",
|
2025-10-08 05:08:35 +00:00
|
|
|
row.subgrupo
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-21 13:16:50 +00:00
|
|
|
console.log('🔍 Códigos extraídos:', { codigoGrupo, codigoSubgrupo });
|
|
|
|
|
|
2025-10-08 05:30:37 +00:00
|
|
|
// Criar um identificador único para a linha
|
2025-10-20 20:35:24 +00:00
|
|
|
const linhaId = `${row.type}-${row.grupo || ""}-${row.subgrupo || ""}-${
|
|
|
|
|
row.centro_custo || ""
|
|
|
|
|
}-${row.codigo_conta || ""}`;
|
2025-10-08 05:30:37 +00:00
|
|
|
setLinhaSelecionada(linhaId);
|
|
|
|
|
|
|
|
|
|
// Se um mês específico foi selecionado, usar apenas esse mês
|
|
|
|
|
const dataInicioFiltro = mesSelecionado || dataInicioStr;
|
|
|
|
|
const dataFimFiltro = mesSelecionado || dataFimStr;
|
|
|
|
|
|
2025-10-21 13:16:50 +00:00
|
|
|
const novosFiltros = {
|
2025-10-08 05:30:37 +00:00
|
|
|
dataInicio: dataInicioFiltro,
|
|
|
|
|
dataFim: dataFimFiltro,
|
2025-10-20 20:35:24 +00:00
|
|
|
centroCusto: row.centro_custo || "",
|
2025-10-08 05:08:35 +00:00
|
|
|
codigoGrupo,
|
|
|
|
|
codigoSubgrupo,
|
2025-10-20 20:35:24 +00:00
|
|
|
codigoConta: row.codigo_conta?.toString() || "",
|
2025-10-21 20:57:58 +00:00
|
|
|
linhaSelecionada: row.grupo || row.subgrupo || row.centro_custo || row.conta || "", // Incluir informação da linha selecionada
|
2025-10-21 13:16:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log('🎯 Novos filtros para analítico:', novosFiltros);
|
|
|
|
|
setAnaliticoFiltros(novosFiltros);
|
2025-10-08 05:08:35 +00:00
|
|
|
};
|
|
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
const toggleGroup = (grupo: string) => {
|
|
|
|
|
const newExpanded = new Set(expandedGroups);
|
|
|
|
|
if (newExpanded.has(grupo)) {
|
|
|
|
|
newExpanded.delete(grupo);
|
|
|
|
|
} else {
|
|
|
|
|
newExpanded.add(grupo);
|
|
|
|
|
}
|
|
|
|
|
setExpandedGroups(newExpanded);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleSubgrupo = (subgrupo: string) => {
|
|
|
|
|
const newExpanded = new Set(expandedSubgrupos);
|
|
|
|
|
if (newExpanded.has(subgrupo)) {
|
|
|
|
|
newExpanded.delete(subgrupo);
|
|
|
|
|
} else {
|
|
|
|
|
newExpanded.add(subgrupo);
|
|
|
|
|
}
|
|
|
|
|
setExpandedSubgrupos(newExpanded);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleCentro = (centro: string) => {
|
|
|
|
|
const newExpanded = new Set(expandedCentros);
|
|
|
|
|
if (newExpanded.has(centro)) {
|
|
|
|
|
newExpanded.delete(centro);
|
|
|
|
|
} else {
|
|
|
|
|
newExpanded.add(centro);
|
|
|
|
|
}
|
|
|
|
|
setExpandedCentros(newExpanded);
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
const handleFiltroChange = (campo: string, valor: string) => {
|
|
|
|
|
setFiltros(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[campo]: valor
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-21 20:28:50 +00:00
|
|
|
// Funções para multi-seleção
|
|
|
|
|
const toggleCentroCusto = (centro: string) => {
|
|
|
|
|
setCentrosCustoSelecionados(prev => {
|
|
|
|
|
if (prev.includes(centro)) {
|
|
|
|
|
return prev.filter(c => c !== centro);
|
|
|
|
|
} else {
|
|
|
|
|
return [...prev, centro];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleConta = (conta: string) => {
|
|
|
|
|
setContasSelecionadas(prev => {
|
|
|
|
|
if (prev.includes(conta)) {
|
|
|
|
|
return prev.filter(c => c !== conta);
|
|
|
|
|
} else {
|
|
|
|
|
return [...prev, conta];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selecionarTodosCentros = () => {
|
|
|
|
|
setCentrosCustoSelecionados(opcoesCentrosCusto);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const limparCentros = () => {
|
|
|
|
|
setCentrosCustoSelecionados([]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selecionarTodasContas = () => {
|
|
|
|
|
setContasSelecionadas(opcoesContas);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const limparContas = () => {
|
|
|
|
|
setContasSelecionadas([]);
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
const limparFiltros = () => {
|
|
|
|
|
const agora = new Date();
|
|
|
|
|
const anoAtual = agora.getFullYear();
|
|
|
|
|
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const periodoAtual = `${anoAtual}-${mesAtual}`;
|
|
|
|
|
|
|
|
|
|
setFiltros({
|
|
|
|
|
periodoDe: `${anoAtual}-01`,
|
|
|
|
|
periodoAte: periodoAtual,
|
|
|
|
|
grupo: "Todos",
|
|
|
|
|
subgrupo: "Todos",
|
|
|
|
|
centroCusto: "Todos",
|
|
|
|
|
conta: "Todas",
|
|
|
|
|
valorMin: "",
|
|
|
|
|
valorMax: "",
|
|
|
|
|
buscaTextual: ""
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-21 20:28:50 +00:00
|
|
|
// Limpar multi-seleções
|
|
|
|
|
setCentrosCustoSelecionados([]);
|
|
|
|
|
setContasSelecionadas([]);
|
|
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
// Limpar dados da tabela
|
|
|
|
|
setData([]);
|
|
|
|
|
setDadosFiltrados([]);
|
|
|
|
|
setFiltrosAplicados(false);
|
|
|
|
|
setMesesDisponiveis([]);
|
2025-10-21 20:28:50 +00:00
|
|
|
|
|
|
|
|
// Recarregar opções e selecionar todos novamente
|
|
|
|
|
carregarPeriodosDisponiveis();
|
2025-10-21 03:49:20 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const aplicarFiltros = async () => {
|
|
|
|
|
// Fechar o Sheet primeiro
|
|
|
|
|
setIsFilterOpen(false);
|
|
|
|
|
|
|
|
|
|
// Aguardar um pouco para a animação de fechamento
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
// Carregar dados da API
|
|
|
|
|
const response = await fetch("/api/dre-oracle");
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Erro HTTP: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dadosCompletos = await response.json();
|
|
|
|
|
|
|
|
|
|
// Aplicar filtros nos dados
|
|
|
|
|
let dadosFiltrados = dadosCompletos;
|
|
|
|
|
|
|
|
|
|
// Filtro por período
|
|
|
|
|
if (filtros.periodoDe && filtros.periodoAte) {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
|
|
|
|
|
const dataItem = item.data_competencia;
|
|
|
|
|
return dataItem >= filtros.periodoDe && dataItem <= filtros.periodoAte;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtro por grupo
|
|
|
|
|
if (filtros.grupo !== "Todos") {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
|
|
|
|
item.grupo === filtros.grupo
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtro por subgrupo
|
|
|
|
|
if (filtros.subgrupo !== "Todos") {
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
|
|
|
|
item.subgrupo === filtros.subgrupo
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 20:28:50 +00:00
|
|
|
// Filtro por centro de custo (multi-seleção)
|
|
|
|
|
if (centrosCustoSelecionados.length > 0) {
|
2025-10-21 03:49:20 +00:00
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
2025-10-21 20:28:50 +00:00
|
|
|
centrosCustoSelecionados.includes(item.centro_custo)
|
2025-10-21 03:49:20 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 20:28:50 +00:00
|
|
|
// Filtro por conta (multi-seleção)
|
|
|
|
|
if (contasSelecionadas.length > 0) {
|
2025-10-21 03:49:20 +00:00
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
2025-10-21 20:28:50 +00:00
|
|
|
contasSelecionadas.includes(item.conta)
|
2025-10-21 03:49:20 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtro por valor mínimo
|
|
|
|
|
if (filtros.valorMin) {
|
|
|
|
|
const valorMin = parseFloat(filtros.valorMin.replace(',', '.'));
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
|
|
|
|
parseFloat(item.valor) >= valorMin
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtro por valor máximo
|
|
|
|
|
if (filtros.valorMax) {
|
|
|
|
|
const valorMax = parseFloat(filtros.valorMax.replace(',', '.'));
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
|
|
|
|
parseFloat(item.valor) <= valorMax
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filtro por busca textual
|
|
|
|
|
if (filtros.buscaTextual) {
|
|
|
|
|
const termoBusca = filtros.buscaTextual.toLowerCase();
|
|
|
|
|
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
|
|
|
|
|
item.grupo.toLowerCase().includes(termoBusca) ||
|
|
|
|
|
item.subgrupo.toLowerCase().includes(termoBusca) ||
|
|
|
|
|
item.centro_custo.toLowerCase().includes(termoBusca) ||
|
|
|
|
|
item.conta.toLowerCase().includes(termoBusca)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setData(dadosFiltrados);
|
|
|
|
|
setDadosFiltrados(dadosFiltrados);
|
|
|
|
|
setFiltrosAplicados(true);
|
|
|
|
|
|
|
|
|
|
// Extrair meses únicos dos dados filtrados
|
2025-10-21 15:36:59 +00:00
|
|
|
const mesesUnicos = [...new Set(dadosFiltrados.map((item: DREItem) => item.data_competencia))].sort() as string[];
|
2025-10-21 03:49:20 +00:00
|
|
|
setMesesDisponiveis(mesesUnicos);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Erro ao aplicar filtros:", error);
|
|
|
|
|
setError(error instanceof Error ? error.message : "Erro desconhecido");
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, 300); // Aguardar 300ms para a animação de fechamento
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
const calcularValoresPorMes = (items: DREItem[]): Record<string, number> => {
|
|
|
|
|
const valoresPorMes: Record<string, number> = {};
|
|
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
const dataCompetencia = new Date(item.data_competencia);
|
|
|
|
|
const anoMes = `${dataCompetencia.getFullYear()}-${String(
|
|
|
|
|
dataCompetencia.getMonth() + 1
|
2025-10-20 20:35:24 +00:00
|
|
|
).padStart(2, "0")}`;
|
2025-10-08 04:19:15 +00:00
|
|
|
|
|
|
|
|
if (!valoresPorMes[anoMes]) {
|
|
|
|
|
valoresPorMes[anoMes] = 0;
|
|
|
|
|
}
|
|
|
|
|
valoresPorMes[anoMes] += parseFloat(item.valor);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return valoresPorMes;
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 20:32:36 +00:00
|
|
|
// Função para calcular percentuais baseado no grupo 03 como referência
|
|
|
|
|
const calcularPercentuaisPorMes = (
|
|
|
|
|
valoresPorMes: Record<string, number>,
|
|
|
|
|
grupo: string
|
|
|
|
|
): Record<string, number> => {
|
|
|
|
|
const percentuais: Record<string, number> = {};
|
|
|
|
|
|
|
|
|
|
// Se for o grupo 03, retorna 100% para todos os meses
|
2025-10-20 20:35:24 +00:00
|
|
|
if (grupo.includes("03")) {
|
2025-10-08 20:32:36 +00:00
|
|
|
Object.keys(valoresPorMes).forEach((mes) => {
|
|
|
|
|
percentuais[mes] = 100;
|
|
|
|
|
});
|
|
|
|
|
return percentuais;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Para outros grupos, calcular percentual baseado no grupo 03
|
|
|
|
|
Object.keys(valoresPorMes).forEach((mes) => {
|
|
|
|
|
const valorAtual = valoresPorMes[mes];
|
|
|
|
|
|
|
|
|
|
// Encontrar o valor do grupo 03 para o mesmo mês
|
|
|
|
|
const grupo03Items = data.filter((item) => {
|
|
|
|
|
const dataCompetencia = new Date(item.data_competencia);
|
|
|
|
|
const anoMes = `${dataCompetencia.getFullYear()}-${String(
|
|
|
|
|
dataCompetencia.getMonth() + 1
|
2025-10-20 20:35:24 +00:00
|
|
|
).padStart(2, "0")}`;
|
|
|
|
|
return anoMes === mes && item.grupo.includes("03");
|
2025-10-08 20:32:36 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const valorGrupo03 = grupo03Items.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (valorGrupo03 !== 0) {
|
|
|
|
|
percentuais[mes] = (valorAtual / valorGrupo03) * 100;
|
|
|
|
|
} else {
|
|
|
|
|
percentuais[mes] = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return percentuais;
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
const buildHierarchicalData = (): HierarchicalRow[] => {
|
|
|
|
|
const rows: HierarchicalRow[] = [];
|
|
|
|
|
|
2025-10-21 03:07:52 +00:00
|
|
|
// Agrupar por grupo
|
2025-10-08 04:19:15 +00:00
|
|
|
const grupos = data.reduce((acc, item) => {
|
2025-10-21 20:28:50 +00:00
|
|
|
if (!acc[item.grupo]) {
|
|
|
|
|
acc[item.grupo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[item.grupo].push(item);
|
2025-10-08 04:19:15 +00:00
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
2025-10-21 03:07:52 +00:00
|
|
|
// Ordenar grupos pelo CODGRUPO numérico
|
|
|
|
|
const sortedGrupos = Object.entries(grupos).sort(([grupoA, itemsA], [grupoB, itemsB]) => {
|
|
|
|
|
// Pegar o CODGRUPO do primeiro item de cada grupo
|
|
|
|
|
const codgrupoA = itemsA[0]?.codgrupo || "";
|
|
|
|
|
const codgrupoB = itemsB[0]?.codgrupo || "";
|
|
|
|
|
|
|
|
|
|
// Se ambos têm CODGRUPO, ordenar numericamente
|
|
|
|
|
if (codgrupoA && codgrupoB) {
|
|
|
|
|
return parseInt(codgrupoA) - parseInt(codgrupoB);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Se apenas um tem CODGRUPO, ele vem primeiro
|
|
|
|
|
if (codgrupoA && !codgrupoB) return -1;
|
|
|
|
|
if (!codgrupoA && codgrupoB) return 1;
|
|
|
|
|
|
|
|
|
|
// Se nenhum tem CODGRUPO, ordenar alfabeticamente
|
|
|
|
|
return grupoA.localeCompare(grupoB);
|
|
|
|
|
});
|
2025-10-08 04:19:15 +00:00
|
|
|
|
|
|
|
|
sortedGrupos.forEach(([grupo, items]) => {
|
|
|
|
|
const totalGrupo = items.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Linha do grupo
|
2025-10-08 20:32:36 +00:00
|
|
|
const valoresPorMes = calcularValoresPorMes(items);
|
2025-10-08 04:19:15 +00:00
|
|
|
rows.push({
|
2025-10-20 20:35:24 +00:00
|
|
|
type: "grupo",
|
2025-10-08 04:19:15 +00:00
|
|
|
level: 0,
|
|
|
|
|
grupo,
|
|
|
|
|
total: totalGrupo,
|
|
|
|
|
isExpanded: expandedGroups.has(grupo),
|
2025-10-08 20:32:36 +00:00
|
|
|
valoresPorMes,
|
|
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(valoresPorMes, grupo),
|
2025-10-21 14:23:00 +00:00
|
|
|
isCalculado: items[0]?.isCalculado || false, // Usar a propriedade isCalculado do primeiro item
|
2025-10-08 04:19:15 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedGroups.has(grupo)) {
|
|
|
|
|
// Agrupar por subgrupo dentro do grupo
|
|
|
|
|
const subgrupos = items.reduce((acc, item) => {
|
2025-10-08 20:32:36 +00:00
|
|
|
// Se o item originalmente era do grupo 05, agrupar tudo sob o nome do grupo 05
|
2025-10-20 20:35:24 +00:00
|
|
|
if (item.grupo.includes("05")) {
|
2025-10-08 20:32:36 +00:00
|
|
|
const subgrupoKey = item.grupo; // Usar o nome completo do grupo 05
|
|
|
|
|
if (!acc[subgrupoKey]) {
|
|
|
|
|
acc[subgrupoKey] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[subgrupoKey].push(item);
|
|
|
|
|
} else {
|
|
|
|
|
// Para outros itens, agrupar normalmente por subgrupo
|
|
|
|
|
if (!acc[item.subgrupo]) {
|
|
|
|
|
acc[item.subgrupo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[item.subgrupo].push(item);
|
2025-10-08 04:19:15 +00:00
|
|
|
}
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
|
|
|
|
// Ordenar subgrupos
|
2025-10-20 20:35:24 +00:00
|
|
|
const sortedSubgrupos = Object.entries(subgrupos).sort(([a], [b]) =>
|
|
|
|
|
a.localeCompare(b)
|
|
|
|
|
);
|
2025-10-08 04:19:15 +00:00
|
|
|
|
|
|
|
|
sortedSubgrupos.forEach(([subgrupo, subgrupoItems]) => {
|
|
|
|
|
const totalSubgrupo = subgrupoItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Linha do subgrupo
|
2025-10-08 20:32:36 +00:00
|
|
|
const valoresSubgrupoPorMes = calcularValoresPorMes(subgrupoItems);
|
2025-10-08 04:19:15 +00:00
|
|
|
rows.push({
|
2025-10-20 20:35:24 +00:00
|
|
|
type: "subgrupo",
|
2025-10-08 04:19:15 +00:00
|
|
|
level: 1,
|
|
|
|
|
grupo,
|
|
|
|
|
subgrupo,
|
|
|
|
|
total: totalSubgrupo,
|
|
|
|
|
isExpanded: expandedSubgrupos.has(`${grupo}-${subgrupo}`),
|
2025-10-08 20:32:36 +00:00
|
|
|
valoresPorMes: valoresSubgrupoPorMes,
|
|
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(
|
|
|
|
|
valoresSubgrupoPorMes,
|
|
|
|
|
grupo
|
|
|
|
|
),
|
2025-10-08 04:19:15 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedSubgrupos.has(`${grupo}-${subgrupo}`)) {
|
|
|
|
|
// Agrupar por centro de custo dentro do subgrupo
|
|
|
|
|
const centros = subgrupoItems.reduce((acc, item) => {
|
|
|
|
|
if (!acc[item.centro_custo]) {
|
|
|
|
|
acc[item.centro_custo] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[item.centro_custo].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
|
|
|
|
// Ordenar centros de custo
|
2025-10-20 20:35:24 +00:00
|
|
|
const sortedCentros = Object.entries(centros).sort(([a], [b]) =>
|
|
|
|
|
a.localeCompare(b)
|
|
|
|
|
);
|
2025-10-08 04:19:15 +00:00
|
|
|
|
|
|
|
|
sortedCentros.forEach(([centro, centroItems]) => {
|
|
|
|
|
const totalCentro = centroItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Linha do centro de custo
|
2025-10-08 20:32:36 +00:00
|
|
|
const valoresCentroPorMes = calcularValoresPorMes(centroItems);
|
2025-10-08 04:19:15 +00:00
|
|
|
rows.push({
|
2025-10-20 20:35:24 +00:00
|
|
|
type: "centro_custo",
|
2025-10-08 04:19:15 +00:00
|
|
|
level: 2,
|
|
|
|
|
grupo,
|
|
|
|
|
subgrupo,
|
|
|
|
|
centro_custo: centro,
|
|
|
|
|
total: totalCentro,
|
|
|
|
|
isExpanded: expandedCentros.has(
|
|
|
|
|
`${grupo}-${subgrupo}-${centro}`
|
|
|
|
|
),
|
2025-10-08 20:32:36 +00:00
|
|
|
valoresPorMes: valoresCentroPorMes,
|
|
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(
|
|
|
|
|
valoresCentroPorMes,
|
|
|
|
|
grupo
|
|
|
|
|
),
|
2025-10-08 04:19:15 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (expandedCentros.has(`${grupo}-${subgrupo}-${centro}`)) {
|
|
|
|
|
// Agrupar por conta dentro do centro de custo
|
|
|
|
|
const contas = centroItems.reduce((acc, item) => {
|
|
|
|
|
if (!acc[item.conta]) {
|
|
|
|
|
acc[item.conta] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[item.conta].push(item);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {} as Record<string, DREItem[]>);
|
|
|
|
|
|
|
|
|
|
// Ordenar contas
|
2025-10-20 20:35:24 +00:00
|
|
|
const sortedContas = Object.entries(contas).sort(([a], [b]) =>
|
|
|
|
|
a.localeCompare(b)
|
|
|
|
|
);
|
2025-10-08 04:19:15 +00:00
|
|
|
|
|
|
|
|
sortedContas.forEach(([conta, contaItems]) => {
|
|
|
|
|
const totalConta = contaItems.reduce(
|
|
|
|
|
(sum, item) => sum + parseFloat(item.valor),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Linha da conta (sem ano/mês no nome)
|
2025-10-08 20:32:36 +00:00
|
|
|
const valoresContaPorMes = calcularValoresPorMes(contaItems);
|
2025-10-08 04:19:15 +00:00
|
|
|
rows.push({
|
2025-10-20 20:35:24 +00:00
|
|
|
type: "conta",
|
2025-10-08 04:19:15 +00:00
|
|
|
level: 3,
|
|
|
|
|
grupo,
|
|
|
|
|
subgrupo,
|
|
|
|
|
centro_custo: centro,
|
|
|
|
|
conta,
|
|
|
|
|
codigo_conta: contaItems[0].codigo_conta,
|
|
|
|
|
total: totalConta,
|
2025-10-08 20:32:36 +00:00
|
|
|
valoresPorMes: valoresContaPorMes,
|
|
|
|
|
percentuaisPorMes: calcularPercentuaisPorMes(
|
|
|
|
|
valoresContaPorMes,
|
|
|
|
|
grupo
|
|
|
|
|
),
|
2025-10-08 04:19:15 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getRowStyle = (row: HierarchicalRow) => {
|
2025-10-20 20:35:24 +00:00
|
|
|
const baseStyle =
|
|
|
|
|
"transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30";
|
2025-10-08 04:19:15 +00:00
|
|
|
|
2025-10-08 05:30:37 +00:00
|
|
|
// Criar identificador único para a linha
|
2025-10-20 20:35:24 +00:00
|
|
|
const linhaId = `${row.type}-${row.grupo || ""}-${row.subgrupo || ""}-${
|
|
|
|
|
row.centro_custo || ""
|
|
|
|
|
}-${row.codigo_conta || ""}`;
|
2025-10-08 05:30:37 +00:00
|
|
|
const isSelected = linhaSelecionada === linhaId;
|
|
|
|
|
|
2025-10-21 20:57:58 +00:00
|
|
|
// Verificar se é um grupo calculado
|
|
|
|
|
const isCalculado = row.isCalculado === true;
|
|
|
|
|
|
2025-10-08 05:30:37 +00:00
|
|
|
let style = baseStyle;
|
|
|
|
|
|
|
|
|
|
if (isSelected) {
|
2025-10-20 20:35:24 +00:00
|
|
|
style +=
|
|
|
|
|
" bg-gradient-to-r from-blue-100 to-indigo-100 border-l-4 border-blue-500 shadow-lg";
|
2025-10-08 05:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
switch (row.type) {
|
2025-10-20 20:35:24 +00:00
|
|
|
case "grupo":
|
2025-10-21 20:57:58 +00:00
|
|
|
if (isCalculado) {
|
|
|
|
|
// Destacar grupos calculados com cor mais vibrante
|
|
|
|
|
return `${style} bg-gradient-to-r from-amber-100/80 to-yellow-100/80 font-bold text-gray-900 border-b-2 border-amber-300 shadow-sm`;
|
|
|
|
|
}
|
2025-10-20 20:03:15 +00:00
|
|
|
return `${style} bg-gradient-to-r from-blue-50/20 to-indigo-50/20 font-bold text-gray-900 border-b-2 border-blue-200`;
|
2025-10-20 20:35:24 +00:00
|
|
|
case "subgrupo":
|
2025-10-20 20:03:15 +00:00
|
|
|
return `${style} bg-gradient-to-r from-gray-50/30 to-blue-50/20 font-semibold text-gray-800`;
|
2025-10-20 20:35:24 +00:00
|
|
|
case "centro_custo":
|
2025-10-20 20:03:15 +00:00
|
|
|
return `${style} bg-gradient-to-r from-gray-50/20 to-gray-100/10 font-medium text-gray-700`;
|
2025-10-20 20:35:24 +00:00
|
|
|
case "conta":
|
2025-10-20 20:03:15 +00:00
|
|
|
return `${style} bg-white font-normal text-gray-600`;
|
2025-10-08 04:19:15 +00:00
|
|
|
default:
|
2025-10-08 05:30:37 +00:00
|
|
|
return style;
|
2025-10-08 04:19:15 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getIndentStyle = (level: number) => {
|
|
|
|
|
return { paddingLeft: `${level * 20}px` };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderCellContent = (row: HierarchicalRow) => {
|
2025-10-21 14:23:00 +00:00
|
|
|
// Verificar se é um grupo calculado usando a propriedade isCalculado
|
|
|
|
|
const isCalculado = row.isCalculado === true;
|
2025-10-21 03:07:52 +00:00
|
|
|
|
2025-10-08 04:19:15 +00:00
|
|
|
switch (row.type) {
|
2025-10-20 20:35:24 +00:00
|
|
|
case "grupo":
|
2025-10-08 04:19:15 +00:00
|
|
|
return (
|
2025-10-20 20:03:15 +00:00
|
|
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
2025-10-08 04:19:15 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => toggleGroup(row.grupo!)}
|
2025-10-20 20:03:15 +00:00
|
|
|
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
|
2025-10-08 04:19:15 +00:00
|
|
|
>
|
2025-10-20 21:09:03 +00:00
|
|
|
{row.isExpanded ? (
|
|
|
|
|
<ChevronDown className="w-4 h-4 text-blue-600" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="w-4 h-4 text-blue-600" />
|
|
|
|
|
)}
|
2025-10-08 04:19:15 +00:00
|
|
|
</button>
|
2025-10-08 05:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
2025-10-20 20:03:15 +00:00
|
|
|
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
|
2025-10-08 05:08:35 +00:00
|
|
|
>
|
2025-10-21 03:07:52 +00:00
|
|
|
<div className="flex items-center gap-2">
|
2025-10-21 03:19:21 +00:00
|
|
|
<span className="font-bold text-gray-900">
|
2025-10-21 03:07:52 +00:00
|
|
|
{row.grupo}
|
|
|
|
|
</span>
|
|
|
|
|
{isCalculado && (
|
2025-10-21 20:28:50 +00:00
|
|
|
// <span className="text-blue-600 font-bold text-sm">⚡</span>
|
|
|
|
|
<></>
|
2025-10-21 03:07:52 +00:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-08 05:08:35 +00:00
|
|
|
</button>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
2025-10-20 20:35:24 +00:00
|
|
|
case "subgrupo":
|
2025-10-08 04:19:15 +00:00
|
|
|
return (
|
2025-10-20 20:03:15 +00:00
|
|
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
2025-10-08 04:19:15 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => toggleSubgrupo(`${row.grupo}-${row.subgrupo}`)}
|
2025-10-20 20:03:15 +00:00
|
|
|
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
|
2025-10-08 04:19:15 +00:00
|
|
|
>
|
2025-10-20 21:09:03 +00:00
|
|
|
{row.isExpanded ? (
|
|
|
|
|
<ChevronDown className="w-4 h-4 text-blue-600" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="w-4 h-4 text-blue-600" />
|
|
|
|
|
)}
|
2025-10-08 04:19:15 +00:00
|
|
|
</button>
|
2025-10-08 05:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
2025-10-20 20:03:15 +00:00
|
|
|
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
|
2025-10-08 05:08:35 +00:00
|
|
|
>
|
2025-10-20 20:35:24 +00:00
|
|
|
<span className="font-semibold text-gray-800">
|
|
|
|
|
{row.subgrupo}
|
|
|
|
|
</span>
|
2025-10-08 05:08:35 +00:00
|
|
|
</button>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
2025-10-20 20:35:24 +00:00
|
|
|
case "centro_custo":
|
2025-10-08 04:19:15 +00:00
|
|
|
return (
|
2025-10-20 20:03:15 +00:00
|
|
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
2025-10-08 04:19:15 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() =>
|
|
|
|
|
toggleCentro(`${row.grupo}-${row.subgrupo}-${row.centro_custo}`)
|
|
|
|
|
}
|
2025-10-20 20:03:15 +00:00
|
|
|
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-200 flex items-center justify-center w-8 h-8 flex-shrink-0"
|
2025-10-08 04:19:15 +00:00
|
|
|
>
|
2025-10-20 21:09:03 +00:00
|
|
|
{row.isExpanded ? (
|
|
|
|
|
<ChevronDown className="w-4 h-4 text-blue-600" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="w-4 h-4 text-blue-600" />
|
|
|
|
|
)}
|
2025-10-08 04:19:15 +00:00
|
|
|
</button>
|
2025-10-08 05:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
2025-10-20 20:03:15 +00:00
|
|
|
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
|
2025-10-08 05:08:35 +00:00
|
|
|
>
|
2025-10-20 20:35:24 +00:00
|
|
|
<span className="font-medium text-gray-700">
|
|
|
|
|
{row.centro_custo}
|
|
|
|
|
</span>
|
2025-10-08 05:08:35 +00:00
|
|
|
</button>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
2025-10-20 20:35:24 +00:00
|
|
|
case "conta":
|
2025-10-08 04:19:15 +00:00
|
|
|
return (
|
2025-10-20 20:03:15 +00:00
|
|
|
<div className="flex items-center gap-3 whitespace-nowrap">
|
|
|
|
|
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
<span className="text-gray-400 font-bold text-lg">•</span>
|
|
|
|
|
</div>
|
2025-10-08 05:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleRowClick(row)}
|
2025-10-20 20:03:15 +00:00
|
|
|
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
|
2025-10-08 05:08:35 +00:00
|
|
|
>
|
2025-10-20 20:03:15 +00:00
|
|
|
<span className="font-normal text-gray-600">{row.conta}</span>
|
2025-10-08 05:08:35 +00:00
|
|
|
</button>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
default:
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
// Loading será tratado dentro do componente principal
|
|
|
|
|
|
|
|
|
|
// Error será tratado dentro do componente principal
|
|
|
|
|
|
|
|
|
|
const hierarchicalData = buildHierarchicalData();
|
|
|
|
|
|
2025-10-21 20:28:50 +00:00
|
|
|
return (
|
2025-10-21 03:49:20 +00:00
|
|
|
<div className="w-full max-w-none mx-auto p-6">
|
|
|
|
|
{/* Header Section */}
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
|
|
|
<div className="flex items-center gap-3">
|
2025-10-20 20:03:15 +00:00
|
|
|
<div>
|
2025-10-21 03:49:20 +00:00
|
|
|
<h1 className="text-2xl font-bold text-gray-900">DRE Gerencial</h1>
|
2025-10-20 20:35:24 +00:00
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
Demonstração do Resultado do Exercício
|
|
|
|
|
</p>
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
2025-10-21 20:28:50 +00:00
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
|
|
|
|
|
{/* Botão de Filtro */}
|
|
|
|
|
<Sheet open={isFilterOpen} onOpenChange={setIsFilterOpen}>
|
|
|
|
|
<SheetTrigger asChild>
|
|
|
|
|
<Button variant="outline" className="flex items-center gap-2">
|
|
|
|
|
<Filter className="w-4 h-4" />
|
|
|
|
|
Filtros
|
|
|
|
|
</Button>
|
|
|
|
|
</SheetTrigger>
|
|
|
|
|
<SheetContent className="w-[400px] sm:w-[540px]">
|
|
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>Filtros</SheetTitle>
|
|
|
|
|
<SheetDescription>
|
|
|
|
|
Ajuste os critérios e clique em Pesquisar para atualizar a visão.
|
|
|
|
|
</SheetDescription>
|
|
|
|
|
</SheetHeader>
|
|
|
|
|
|
|
|
|
|
<div className="grid flex-1 auto-rows-min gap-6 py-4">
|
|
|
|
|
{/* Período */}
|
|
|
|
|
<div className="grid gap-3">
|
|
|
|
|
<Label>Período</Label>
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="periodo-de" className="text-xs text-gray-500">DE</Label>
|
|
|
|
|
<Select value={filtros.periodoDe} onValueChange={(value) => handleFiltroChange('periodoDe', value)}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Selecione" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{mesesDisponiveis.map(mes => (
|
|
|
|
|
<SelectItem key={mes} value={mes}>{mes}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-10-21 20:28:50 +00:00
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="periodo-ate" className="text-xs text-gray-500">ATÉ</Label>
|
|
|
|
|
<Select value={filtros.periodoAte} onValueChange={(value) => handleFiltroChange('periodoAte', value)}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Selecione" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{mesesDisponiveis.map(mes => (
|
|
|
|
|
<SelectItem key={mes} value={mes}>{mes}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-10-21 20:28:50 +00:00
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-21 03:52:33 +00:00
|
|
|
{/* Grupo
|
2025-10-21 03:49:20 +00:00
|
|
|
<div className="grid gap-3">
|
|
|
|
|
<Label htmlFor="grupo">GRUPO</Label>
|
|
|
|
|
<Select value={filtros.grupo} onValueChange={(value) => handleFiltroChange('grupo', value)}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Selecione" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="Todos">Todos</SelectItem>
|
|
|
|
|
{opcoesGrupos.map(grupo => (
|
|
|
|
|
<SelectItem key={grupo} value={grupo}>{grupo}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-10-21 03:52:33 +00:00
|
|
|
</div>*/}
|
2025-10-21 03:49:20 +00:00
|
|
|
|
2025-10-21 03:52:33 +00:00
|
|
|
{/* Subgrupo
|
2025-10-21 03:49:20 +00:00
|
|
|
<div className="grid gap-3">
|
|
|
|
|
<Label htmlFor="subgrupo">SUBGRUPO</Label>
|
|
|
|
|
<Select value={filtros.subgrupo} onValueChange={(value) => handleFiltroChange('subgrupo', value)}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Selecione" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="Todos">Todos</SelectItem>
|
|
|
|
|
{opcoesSubgrupos.map(subgrupo => (
|
|
|
|
|
<SelectItem key={subgrupo} value={subgrupo}>{subgrupo}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-10-21 03:52:33 +00:00
|
|
|
</div>*/}
|
2025-10-21 03:49:20 +00:00
|
|
|
|
2025-10-21 20:28:50 +00:00
|
|
|
{/* Centro de Custo */}
|
2025-10-21 03:49:20 +00:00
|
|
|
<div className="grid gap-3">
|
2025-10-21 20:28:50 +00:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<Label htmlFor="centro-custo">CENTRO DE CUSTO</Label>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={selecionarTodosCentros}
|
|
|
|
|
className="text-xs h-6 px-2"
|
|
|
|
|
>
|
|
|
|
|
Todos
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={limparCentros}
|
|
|
|
|
className="text-xs h-6 px-2"
|
|
|
|
|
>
|
|
|
|
|
Limpar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="max-h-32 overflow-y-auto border rounded-md p-2 space-y-2">
|
|
|
|
|
{opcoesCentrosCusto.map(centro => (
|
|
|
|
|
<div key={centro} className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id={`centro-${centro}`}
|
|
|
|
|
checked={centrosCustoSelecionados.includes(centro)}
|
|
|
|
|
onCheckedChange={() => toggleCentroCusto(centro)}
|
|
|
|
|
/>
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor={`centro-${centro}`}
|
|
|
|
|
className="text-sm font-normal cursor-pointer flex-1"
|
|
|
|
|
>
|
|
|
|
|
{centro}
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{centrosCustoSelecionados.length > 0 && (
|
|
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
{centrosCustoSelecionados.length} centro(s) selecionado(s)
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
|
2025-10-21 20:28:50 +00:00
|
|
|
{/* Conta */}
|
2025-10-21 03:49:20 +00:00
|
|
|
<div className="grid gap-3">
|
2025-10-21 20:28:50 +00:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<Label htmlFor="conta">CONTA</Label>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={selecionarTodasContas}
|
|
|
|
|
className="text-xs h-6 px-2"
|
|
|
|
|
>
|
|
|
|
|
Todas
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={limparContas}
|
|
|
|
|
className="text-xs h-6 px-2"
|
|
|
|
|
>
|
|
|
|
|
Limpar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-10-21 20:57:58 +00:00
|
|
|
</div>
|
2025-10-21 20:28:50 +00:00
|
|
|
<div className="max-h-32 overflow-y-auto border rounded-md p-2 space-y-2">
|
|
|
|
|
{opcoesContas.map(conta => (
|
|
|
|
|
<div key={conta} className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id={`conta-${conta}`}
|
|
|
|
|
checked={contasSelecionadas.includes(conta)}
|
|
|
|
|
onCheckedChange={() => toggleConta(conta)}
|
|
|
|
|
/>
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor={`conta-${conta}`}
|
|
|
|
|
className="text-sm font-normal cursor-pointer flex-1"
|
|
|
|
|
>
|
|
|
|
|
{conta}
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{contasSelecionadas.length > 0 && (
|
|
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
{contasSelecionadas.length} conta(s) selecionada(s)
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
|
2025-10-21 03:52:33 +00:00
|
|
|
{/* Valor
|
2025-10-21 03:49:20 +00:00
|
|
|
<div className="grid gap-3">
|
|
|
|
|
<Label>Valor</Label>
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="valor-min" className="text-xs text-gray-500">VALOR MÍN.</Label>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">R$</span>
|
|
|
|
|
<Input
|
|
|
|
|
id="valor-min"
|
|
|
|
|
value={filtros.valorMin}
|
|
|
|
|
onChange={(e) => handleFiltroChange('valorMin', e.target.value)}
|
|
|
|
|
className="pl-8"
|
|
|
|
|
placeholder="0,00"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="valor-max" className="text-xs text-gray-500">VALOR MÁX.</Label>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">R$</span>
|
|
|
|
|
<Input
|
|
|
|
|
id="valor-max"
|
|
|
|
|
value={filtros.valorMax}
|
|
|
|
|
onChange={(e) => handleFiltroChange('valorMax', e.target.value)}
|
|
|
|
|
className="pl-8"
|
|
|
|
|
placeholder="0,00"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-21 03:52:33 +00:00
|
|
|
</div>*/}
|
2025-10-21 03:49:20 +00:00
|
|
|
|
2025-10-21 03:52:33 +00:00
|
|
|
{/* Busca Textual
|
2025-10-21 03:49:20 +00:00
|
|
|
<div className="grid gap-3">
|
|
|
|
|
<Label htmlFor="busca-textual">BUSCA TEXTUAL</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="busca-textual"
|
|
|
|
|
value={filtros.buscaTextual}
|
|
|
|
|
onChange={(e) => handleFiltroChange('buscaTextual', e.target.value)}
|
|
|
|
|
placeholder="Pesquise por grupo, subgrupo, centro ou conta"
|
|
|
|
|
/>
|
2025-10-21 03:52:33 +00:00
|
|
|
</div>*/}
|
2025-10-21 03:49:20 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<SheetFooter className="flex gap-3">
|
2025-10-21 13:08:00 +00:00
|
|
|
{/* <Button variant="outline" onClick={limparFiltros} className="flex-1">
|
2025-10-21 03:49:20 +00:00
|
|
|
Limpar filtros
|
2025-10-21 13:08:00 +00:00
|
|
|
</Button> */}
|
2025-10-21 03:49:20 +00:00
|
|
|
<Button variant="outline" onClick={() => setIsFilterOpen(false)} className="flex-1">
|
|
|
|
|
Cancelar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={aplicarFiltros} className="flex-1">
|
|
|
|
|
Pesquisar
|
|
|
|
|
</Button>
|
|
|
|
|
</SheetFooter>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
|
|
|
|
|
{/* Loading quando aplicando filtros */}
|
|
|
|
|
{loading && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 text-center">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
|
|
|
|
<LoaderPinwheel className="w-8 h-8 text-blue-600 animate-spin" />
|
|
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
<div>
|
2025-10-21 03:49:20 +00:00
|
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
|
|
|
Aplicando filtros...
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-gray-500">
|
|
|
|
|
Aguarde enquanto processamos os dados.
|
2025-10-20 20:35:24 +00:00
|
|
|
</p>
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
)}
|
2025-10-20 20:35:24 +00:00
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
{/* Erro */}
|
|
|
|
|
{error && !loading && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-red-200 p-8 text-center">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
|
|
|
|
|
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
2025-10-20 20:03:15 +00:00
|
|
|
</svg>
|
|
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-red-900 mb-2">
|
|
|
|
|
Erro ao carregar dados
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-red-600 mb-4">{error}</p>
|
|
|
|
|
<Button onClick={() => aplicarFiltros()} className="flex items-center gap-2">
|
|
|
|
|
Tentar novamente
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
)}
|
2025-10-08 04:19:15 +00:00
|
|
|
|
2025-10-21 03:49:20 +00:00
|
|
|
{/* Mensagem quando não há dados */}
|
|
|
|
|
{!filtrosAplicados && !loading && !error && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 text-center">
|
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
|
|
|
|
|
<Filter className="w-8 h-8 text-gray-400" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
|
|
|
Nenhum dado exibido
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-gray-500 mb-4">
|
|
|
|
|
Clique no botão "Filtros" para definir os critérios de busca e visualizar os dados do DRE.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
</div>
|
2025-10-20 20:03:15 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
)}
|
2025-10-08 04:19:15 +00:00
|
|
|
|
2025-10-20 20:03:15 +00:00
|
|
|
{/* Table Container */}
|
2025-10-21 03:49:20 +00:00
|
|
|
{filtrosAplicados && !loading && !error && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
2025-10-20 20:03:15 +00:00
|
|
|
{/* Table Header */}
|
|
|
|
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 sticky top-0 z-20">
|
2025-10-21 20:57:58 +00:00
|
|
|
<div className="flex items-center justify-between px-4 py-2">
|
|
|
|
|
<div className="flex items-center gap-4 text-xs font-semibold text-gray-700 uppercase tracking-wide">
|
|
|
|
|
<div className="flex-1 min-w-[300px] max-w-[400px]">Descrição</div>
|
|
|
|
|
{mesesDisponiveis.map((mes) => (
|
|
|
|
|
<div key={mes} className="flex min-w-[240px] max-w-[300px] gap-2">
|
|
|
|
|
<div className="flex-1 min-w-[120px] text-right">{mes}</div>
|
|
|
|
|
<div className="flex-1 min-w-[100px] text-center text-xs text-gray-500">
|
2025-10-20 20:35:24 +00:00
|
|
|
%
|
|
|
|
|
</div>
|
2025-10-08 04:26:38 +00:00
|
|
|
</div>
|
2025-10-08 04:19:15 +00:00
|
|
|
))}
|
2025-10-21 20:57:58 +00:00
|
|
|
<div className="flex-1 min-w-[120px] text-right">Total</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Botões de controle */}
|
|
|
|
|
<div className="flex gap-2 ml-4">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
// Expandir todos os grupos
|
|
|
|
|
const todosGrupos = hierarchicalData
|
|
|
|
|
.filter(row => row.type === "grupo")
|
|
|
|
|
.map(row => row.grupo!);
|
|
|
|
|
setExpandedGroups(new Set(todosGrupos));
|
|
|
|
|
|
|
|
|
|
// Expandir todos os subgrupos
|
|
|
|
|
const todosSubgrupos = hierarchicalData
|
|
|
|
|
.filter(row => row.type === "subgrupo")
|
|
|
|
|
.map(row => `${row.grupo}-${row.subgrupo}`);
|
|
|
|
|
setExpandedSubgrupos(new Set(todosSubgrupos));
|
|
|
|
|
|
|
|
|
|
// Expandir todos os centros de custo
|
|
|
|
|
const todosCentros = hierarchicalData
|
|
|
|
|
.filter(row => row.type === "centro_custo")
|
|
|
|
|
.map(row => `${row.grupo}-${row.subgrupo}-${row.centro_custo}`);
|
|
|
|
|
setExpandedCentros(new Set(todosCentros));
|
|
|
|
|
}}
|
|
|
|
|
className="text-xs h-6 px-2"
|
|
|
|
|
>
|
|
|
|
|
Expandir Tudo
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
// Recolher tudo
|
|
|
|
|
setExpandedGroups(new Set());
|
|
|
|
|
setExpandedSubgrupos(new Set());
|
|
|
|
|
setExpandedCentros(new Set());
|
|
|
|
|
}}
|
|
|
|
|
className="text-xs h-6 px-2"
|
|
|
|
|
>
|
|
|
|
|
Recolher Tudo
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-10-08 04:26:38 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-20 20:03:15 +00:00
|
|
|
{/* Table Body */}
|
|
|
|
|
<div className="max-h-[500px] overflow-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
|
2025-10-08 04:45:26 +00:00
|
|
|
{hierarchicalData.map((row, index) => (
|
2025-10-20 20:35:24 +00:00
|
|
|
<div
|
|
|
|
|
key={index}
|
2025-10-21 20:57:58 +00:00
|
|
|
className={`flex items-center gap-4 px-4 py-2 text-sm border-b border-gray-100 hover:bg-gray-50 transition-colors ${getRowStyle(
|
2025-10-20 20:35:24 +00:00
|
|
|
row
|
|
|
|
|
)}`}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="flex-1 min-w-[300px] max-w-[400px] whitespace-nowrap overflow-hidden"
|
|
|
|
|
style={getIndentStyle(row.level)}
|
|
|
|
|
>
|
2025-10-08 04:45:26 +00:00
|
|
|
{renderCellContent(row)}
|
|
|
|
|
</div>
|
2025-10-08 04:19:15 +00:00
|
|
|
{mesesDisponiveis.map((mes) => (
|
2025-10-20 20:56:42 +00:00
|
|
|
<div key={mes} className="flex min-w-[240px] max-w-[300px] gap-2">
|
2025-10-08 20:32:36 +00:00
|
|
|
<div
|
2025-10-20 20:56:42 +00:00
|
|
|
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
|
2025-10-08 20:32:36 +00:00
|
|
|
onClick={() => handleRowClick(row, mes)}
|
2025-10-20 20:35:24 +00:00
|
|
|
title={
|
|
|
|
|
row.valoresPorMes && row.valoresPorMes[mes]
|
|
|
|
|
? formatCurrency(row.valoresPorMes[mes])
|
|
|
|
|
: "-"
|
|
|
|
|
}
|
2025-10-08 20:32:36 +00:00
|
|
|
>
|
|
|
|
|
{row.valoresPorMes && row.valoresPorMes[mes]
|
|
|
|
|
? (() => {
|
2025-10-20 20:35:24 +00:00
|
|
|
const { formatted, isNegative } =
|
|
|
|
|
formatCurrencyWithColor(row.valoresPorMes[mes]);
|
2025-10-08 20:32:36 +00:00
|
|
|
return (
|
2025-10-20 20:35:24 +00:00
|
|
|
<span
|
|
|
|
|
className={
|
|
|
|
|
isNegative
|
|
|
|
|
? "text-red-600 font-bold"
|
|
|
|
|
: "text-gray-900"
|
|
|
|
|
}
|
|
|
|
|
>
|
2025-10-08 20:32:36 +00:00
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()
|
2025-10-20 20:35:24 +00:00
|
|
|
: "-"}
|
2025-10-08 20:32:36 +00:00
|
|
|
</div>
|
|
|
|
|
<div
|
2025-10-20 20:56:42 +00:00
|
|
|
className="flex-1 min-w-[100px] text-center font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
|
2025-10-08 20:32:36 +00:00
|
|
|
onClick={() => handleRowClick(row, mes)}
|
2025-10-20 20:35:24 +00:00
|
|
|
title={
|
|
|
|
|
row.percentuaisPorMes &&
|
|
|
|
|
row.percentuaisPorMes[mes] !== undefined
|
|
|
|
|
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
|
|
|
|
|
: "-"
|
|
|
|
|
}
|
2025-10-08 20:32:36 +00:00
|
|
|
>
|
2025-10-20 20:35:24 +00:00
|
|
|
{row.percentuaisPorMes &&
|
|
|
|
|
row.percentuaisPorMes[mes] !== undefined
|
2025-10-08 20:32:36 +00:00
|
|
|
? `${row.percentuaisPorMes[mes].toFixed(1)}%`
|
2025-10-20 20:35:24 +00:00
|
|
|
: "-"}
|
2025-10-08 20:32:36 +00:00
|
|
|
</div>
|
2025-10-08 04:45:26 +00:00
|
|
|
</div>
|
|
|
|
|
))}
|
2025-10-08 05:30:37 +00:00
|
|
|
<div
|
2025-10-20 20:03:15 +00:00
|
|
|
className="flex-1 min-w-[120px] text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden"
|
2025-10-08 05:30:37 +00:00
|
|
|
onClick={() => handleRowClick(row)}
|
2025-10-20 20:35:24 +00:00
|
|
|
title={row.total ? formatCurrency(row.total) : "-"}
|
2025-10-08 05:30:37 +00:00
|
|
|
>
|
2025-10-08 12:28:20 +00:00
|
|
|
{(() => {
|
2025-10-20 20:35:24 +00:00
|
|
|
const { formatted, isNegative } = formatCurrencyWithColor(
|
|
|
|
|
row.total!
|
|
|
|
|
);
|
2025-10-08 12:28:20 +00:00
|
|
|
return (
|
2025-10-20 20:35:24 +00:00
|
|
|
<span
|
|
|
|
|
className={
|
|
|
|
|
isNegative ? "text-red-600 font-bold" : "text-gray-900"
|
|
|
|
|
}
|
|
|
|
|
>
|
2025-10-08 12:28:20 +00:00
|
|
|
{formatted}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
2025-10-08 04:45:26 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-10-08 04:26:38 +00:00
|
|
|
</div>
|
2025-10-21 03:49:20 +00:00
|
|
|
)}
|
2025-10-08 05:08:35 +00:00
|
|
|
|
|
|
|
|
{/* Componente Analítico */}
|
|
|
|
|
{!loading && data.length > 0 && (
|
|
|
|
|
<AnaliticoComponent filtros={analiticoFiltros} />
|
|
|
|
|
)}
|
2025-10-08 04:19:15 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|