"use client"; import { LoaderPinwheel, ChevronDown, ChevronRight, Filter, Maximize2, Minimize2, Download } from "lucide-react"; import React, { useEffect, useState, useCallback, startTransition, memo } from "react"; import AnaliticoComponent from "./analitico"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import * as XLSX from "xlsx"; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; interface DREItem { codfilial: string; data_competencia: string; data_cai: string; grupo: string; subgrupo: string; centro_custo: string; codigo_centro_custo: string; codigo_conta: number; conta: string; valor: string; codgrupo?: string; isCalculado?: boolean; entidades?: string; } interface HierarchicalRow { type: "entidade" | "direto_indireto" | "centro_custo" | "conta"; level: number; entidade?: string; direto_indireto?: string; // "DIRETO" ou "INDIRETO" (vem do SUBGRUPO) centro_custo?: string; codigo_centro_custo?: string; conta?: string; codigo_conta?: number; total?: number; isExpanded?: boolean; valoresPorMes?: Record; percentuaisPorMes?: Record; percentualTotal?: number; // Campos legados para compatibilidade (não usados na nova hierarquia) grupo?: string; subgrupo?: string; isCalculado?: boolean; } // Componente memoizado para linhas da tabela const TableRow = memo(({ row, index, handleRowClick, getRowStyle, getIndentStyle, renderCellContent, mesesDisponiveis, formatCurrency, formatCurrencyWithColor, getFixedCellBackground }: { row: HierarchicalRow; index: number; handleRowClick: (row: HierarchicalRow, mes?: string) => void; getRowStyle: (row: HierarchicalRow) => string; getIndentStyle: (level: number) => React.CSSProperties; renderCellContent: (row: HierarchicalRow) => React.ReactNode; mesesDisponiveis: string[]; formatCurrency: (value: number) => string; formatCurrencyWithColor: (value: number) => { formatted: string; isNegative: boolean }; getFixedCellBackground: (row: HierarchicalRow) => string; }) => { return (
{renderCellContent(row)}
{/* Colunas de valores por mês */} {mesesDisponiveis.map((mes) => ( handleRowClick(row, mes)} title={ row.valoresPorMes && row.valoresPorMes[mes] ? formatCurrency(row.valoresPorMes[mes]) : "-" } > {row.valoresPorMes && row.valoresPorMes[mes] ? (() => { const { formatted, isNegative } = formatCurrencyWithColor(row.valoresPorMes[mes]); return ( {formatted} ); })() : "-"} handleRowClick(row, mes)} title={ row.percentuaisPorMes && row.percentuaisPorMes[mes] !== undefined ? `${row.percentuaisPorMes[mes].toFixed(1)}%` : "-" } > {row.percentuaisPorMes && row.percentuaisPorMes[mes] !== undefined ? `${row.percentuaisPorMes[mes].toFixed(1)}%` : "-"} ))} {/* Coluna Total */} handleRowClick(row)} title={row.total ? formatCurrency(row.total) : "-"} > {(() => { const { formatted, isNegative } = formatCurrencyWithColor( row.total! ); return ( {formatted} ); })()} {/* Coluna Percentual Total */} handleRowClick(row)} title={ row.percentualTotal !== undefined ? `${row.percentualTotal.toFixed(1)}%` : "-" } > {row.percentualTotal !== undefined ? `${row.percentualTotal.toFixed(1)}%` : "-"} ); }); TableRow.displayName = 'TableRow'; export default function Teste() { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [expandedEntidades, setExpandedEntidades] = useState>(new Set()); const [expandedDiretoIndireto, setExpandedDiretoIndireto] = useState>(new Set()); const [expandedCentros, setExpandedCentros] = useState>(new Set()); const [mesesDisponiveis, setMesesDisponiveis] = useState([]); // Estados para filtros const [filtros, setFiltros] = useState({ periodoDe: "", periodoAte: "", grupo: "Todos", subgrupo: "Todos", centroCusto: "Todos", conta: "Todas", valorMin: "", valorMax: "", buscaTextual: "" }); // Estados para multi-seleção const [centrosCustoSelecionados, setCentrosCustoSelecionados] = useState([]); // Estado para armazenar os códigos dos centros de custo const [codigosCentrosCusto, setCodigosCentrosCusto] = useState>({}); const [contasSelecionadas, setContasSelecionadas] = useState([]); // Estado para armazenar os códigos das contas const [codigosContas, setCodigosContas] = useState>({}); const [entidadesSelecionadas, setEntidadesSelecionadas] = useState([]); const [isFilterOpen, setIsFilterOpen] = useState(false); const [dadosFiltrados, setDadosFiltrados] = useState([]); const [filtrosAplicados, setFiltrosAplicados] = useState(false); const [ordemHierarquiaContasPrimeiro, setOrdemHierarquiaContasPrimeiro] = useState(true); // Estados para opções dos filtros const [opcoesGrupos, setOpcoesGrupos] = useState([]); const [opcoesSubgrupos, setOpcoesSubgrupos] = useState([]); const [opcoesCentrosCusto, setOpcoesCentrosCusto] = useState([]); const [opcoesContas, setOpcoesContas] = useState([]); const [opcoesEntidades, setOpcoesEntidades] = useState([]); // Estados para filtros de busca nos campos de seleção const [filtroCentroCusto, setFiltroCentroCusto] = useState(""); const [filtroConta, setFiltroConta] = useState(""); const [filtroEntidade, setFiltroEntidade] = useState(""); // Estados para analítico const [analiticoFiltros, setAnaliticoFiltros] = useState({ dataInicio: "", dataFim: "", centroCusto: "", codigoGrupo: "", codigoSubgrupo: "", codigoConta: "", linhaSelecionada: "", // Adicionar informação da linha selecionada excluirCentroCusto: "", // Para excluir centro de custo específico quando desmarcado excluirCodigoConta: "", // Para excluir código de conta específico quando desmarcado codigosCentrosCustoSelecionados: "", // Códigos dos centros de custo selecionados no filtro codigosContasSelecionadas: "", // Códigos das contas selecionadas no filtro }); const [linhaSelecionada, setLinhaSelecionada] = useState(null); const [isAllExpanded, setIsAllExpanded] = useState(false); useEffect(() => { // 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 })); }, []); const carregarPeriodosDisponiveis = async () => { try { const response = await fetch("/api/dre-entidade-oracle"); if (!response.ok) { throw new Error(`Erro HTTP: ${response.status}`); } const dadosCompletos = await response.json(); // Extrair períodos únicos dos dados const periodosUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.data_competencia))].sort() as string[]; setMesesDisponiveis(periodosUnicos); // Extrair grupos únicos const gruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.grupo))].sort() as string[]; setOpcoesGrupos(gruposUnicos); // Extrair subgrupos únicos const subgruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.subgrupo))].sort() as string[]; setOpcoesSubgrupos(subgruposUnicos); // Extrair centros de custo únicos com nome e código const centrosCustoUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.centro_custo))].sort() as string[]; // Filtrar centro de custo 999.998 da lista de opções (mas sempre manter selecionado) const centrosCustoParaExibir = centrosCustoUnicos.filter(centro => { const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro); return item?.codigo_centro_custo !== "999.998"; }); setOpcoesCentrosCusto(centrosCustoParaExibir); // Criar objeto de códigos dos centros de custo // Usar um Map para garantir que pegamos o código correto mesmo com duplicatas const codigos: Record = {}; const codigosPorNome = new Map>(); dadosCompletos.forEach((item: DREItem) => { if (item.centro_custo && item.codigo_centro_custo) { if (!codigosPorNome.has(item.centro_custo)) { codigosPorNome.set(item.centro_custo, new Set()); } codigosPorNome.get(item.centro_custo)!.add(item.codigo_centro_custo); } }); // Para cada centro de custo, usar o código mais comum ou o primeiro encontrado codigosPorNome.forEach((codigosSet, nome) => { const codigosArray = Array.from(codigosSet); // Se houver apenas um código, usar esse if (codigosArray.length === 1) { codigos[nome] = codigosArray[0]; } else { // Se houver múltiplos códigos, verificar qual é mais frequente nos dados const frequencia: Record = {}; dadosCompletos.forEach((item: DREItem) => { if (item.centro_custo === nome && item.codigo_centro_custo) { frequencia[item.codigo_centro_custo] = (frequencia[item.codigo_centro_custo] || 0) + 1; } }); // Pegar o código mais frequente const codigoMaisFrequente = Object.entries(frequencia).sort((a, b) => b[1] - a[1])[0]; codigos[nome] = codigoMaisFrequente ? codigoMaisFrequente[0] : codigosArray[0]; } }); console.log('🗺️ Mapeamento de códigos de centros de custo criado:', codigos); setCodigosCentrosCusto(codigos); // Extrair contas únicas const contasUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.conta))].sort() as string[]; // Filtrar conta 199 da lista de opções (mas sempre manter selecionada) const contasParaExibir = contasUnicas.filter(conta => { const item = dadosCompletos.find((d: DREItem) => d.conta === conta); return item?.codigo_conta?.toString() !== "199"; }); setOpcoesContas(contasParaExibir); // Criar objeto de códigos das contas const codigosContasObj: Record = {}; dadosCompletos.forEach((item: DREItem) => { if (item.conta && item.codigo_conta) { codigosContasObj[item.conta] = item.codigo_conta.toString(); } }); setCodigosContas(codigosContasObj); // Extrair entidades únicas const entidadesUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.entidades).filter(Boolean))].sort() as string[]; console.log('🏢 Entidades únicas encontradas:', entidadesUnicas); console.log('📊 Total de entidades:', entidadesUnicas.length); // Filtrar "Faturamento líquido" da lista de opções (mas sempre manter selecionado) const entidadeFaturamentoLiquido = entidadesUnicas.find(ent => ent.toLowerCase().includes('faturamento') && ent.toLowerCase().includes('líquido') ) || entidadesUnicas.find(ent => ent.toLowerCase().includes('faturamento liquido') ); const entidadesParaExibir = entidadesUnicas.filter(ent => ent !== entidadeFaturamentoLiquido); setOpcoesEntidades(entidadesParaExibir); // Inicializar com todos os itens selecionados, exceto o centro de custo 002.003.017 e conta 100050 // Mas sempre incluir centro de custo 999.998 e conta 199 const centrosCustoIniciaisSelecionados = centrosCustoUnicos.filter(centro => { const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro); // Sempre incluir centro de custo com código 999.998 if (item?.codigo_centro_custo === "999.998") { return true; } // Excluir centro de custo 002.003.017 return item?.codigo_centro_custo !== "002.003.017"; }); const contasIniciaisSelecionadas = contasUnicas.filter(conta => { const item = dadosCompletos.find((d: DREItem) => d.conta === conta); // Sempre incluir conta com código 199 if (item?.codigo_conta?.toString() === "199") { return true; } // Excluir conta 100050 return item?.codigo_conta?.toString() !== "100050"; }); setCentrosCustoSelecionados(centrosCustoIniciaisSelecionados); setContasSelecionadas(contasIniciaisSelecionadas); // Inicializar com todas as entidades selecionadas (incluindo Faturamento líquido) setEntidadesSelecionadas(entidadesUnicas); } catch (error) { console.error("Erro ao carregar períodos:", error); } }; const fetchData = async () => { try { setLoading(true); setError(null); const response = await fetch("/api/dre-entidade-oracle"); if (!response.ok) { throw new Error(`Erro ao carregar dados: ${response.status}`); } const result = await response.json(); setData(result); // Extrair meses únicos dos dados const meses = [ ...new Set( result.map((item: DREItem) => { // Usar diretamente o valor de data_competencia que já vem no formato YYYY-MM return item.data_competencia; }) ), ].sort() as string[]; setMesesDisponiveis(meses); } catch (err) { setError(err instanceof Error ? err.message : "Erro desconhecido"); } finally { setLoading(false); } }; const formatCurrency = (value: string | number) => { const numValue = typeof value === "string" ? parseFloat(value) : value; return numValue.toLocaleString("pt-BR", { style: "currency", currency: "BRL", }); }; const formatCurrencyWithColor = (value: string | number) => { const numValue = typeof value === "string" ? parseFloat(value) : value; const formatted = formatCurrency(value); const isNegative = numValue < 0; return { formatted, isNegative }; }; // Função para lidar com clique nas linhas const handleRowClick = (row: HierarchicalRow, mesSelecionado?: string) => { console.log('🖱️ Clique na linha:', row); console.log('📅 Mês selecionado:', mesSelecionado); if (!data.length) { console.log('⚠️ Sem dados disponíveis'); return; } // 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 console.log('📅 Datas calculadas:', { dataInicioStr, dataFimStr }); // Criar um identificador único para a linha const linhaId = `${row.type}-${row.entidade || ""}-${row.direto_indireto || ""}-${ row.centro_custo || "" }-${row.codigo_conta || ""}`; setLinhaSelecionada(linhaId); // Se um mês específico foi selecionado, usar apenas esse mês const dataInicioFiltro = mesSelecionado || dataInicioStr; const dataFimFiltro = mesSelecionado || dataFimStr; // Determinar filtros baseado na nova hierarquia [entidade, direto/indireto, cc, conta] let centroCustoFiltro = ""; let codigoContaFiltro = ""; let codigoGrupoFiltro = ""; let entidadeFiltro = ""; let diretoIndiretoFiltro = ""; // Sempre filtrar por entidade se disponível entidadeFiltro = row.entidade || ""; // Filtrar por direto/indireto se disponível diretoIndiretoFiltro = row.direto_indireto || ""; // Buscar CODGRUPO dos dados originais baseado na hierarquia selecionada // Quando clicar em qualquer nível (entidade, direto/indireto, centro de custo ou conta), deve filtrar por CODGRUPO if (row.type === "entidade" || row.type === "direto_indireto" || row.type === "centro_custo" || row.type === "conta") { // Buscar o CODGRUPO dos dados originais que correspondem a esta linha // Filtrar também pelo período selecionado se houver const itemsCorrespondentes = data.filter((item: DREItem) => { // Filtrar por período se um mês específico foi selecionado if (mesSelecionado && item.data_competencia !== mesSelecionado) { return false; } if (row.type === "entidade") { return item.entidades === row.entidade; } else if (row.type === "direto_indireto") { return item.entidades === row.entidade && item.subgrupo === row.direto_indireto; } else if (row.type === "centro_custo") { return item.entidades === row.entidade && item.subgrupo === row.direto_indireto && item.codigo_centro_custo === row.codigo_centro_custo; } else if (row.type === "conta") { return item.entidades === row.entidade && item.subgrupo === row.direto_indireto && item.codigo_centro_custo === row.codigo_centro_custo && item.codigo_conta === row.codigo_conta; } return false; }); // Pegar o CODGRUPO do primeiro item encontrado (todos devem ter o mesmo CODGRUPO) if (itemsCorrespondentes.length > 0) { const primeiroItem = itemsCorrespondentes[0]; // Priorizar codgrupo direto, depois tentar extrair do campo grupo codigoGrupoFiltro = primeiroItem.codgrupo || (primeiroItem.grupo ? primeiroItem.grupo.split(' ')[0]?.trim() : "") || ""; console.log('📊 CODGRUPO encontrado para a linha:', { codigoGrupoFiltro, codgrupo: primeiroItem.codgrupo, grupo: primeiroItem.grupo, itemsEncontrados: itemsCorrespondentes.length, tipoLinha: row.type, mesSelecionado: mesSelecionado }); } else { console.warn('⚠️ Nenhum item correspondente encontrado para extrair CODGRUPO:', { tipoLinha: row.type, entidade: row.entidade, direto_indireto: row.direto_indireto, centro_custo: row.centro_custo, codigo_centro_custo: row.codigo_centro_custo, codigo_conta: row.codigo_conta, mesSelecionado: mesSelecionado }); } } // Filtrar por centro de custo se for nível centro_custo ou conta if (row.type === "centro_custo" || row.type === "conta") { centroCustoFiltro = row.codigo_centro_custo || ""; } // Filtrar por conta se for nível conta if (row.type === "conta") { codigoContaFiltro = row.codigo_conta?.toString() || ""; } console.log('🎯 Filtros determinados (nova hierarquia):', { entidadeFiltro, diretoIndiretoFiltro, codigoGrupoFiltro, centroCustoFiltro, codigoContaFiltro, tipoLinha: row.type, rowData: { entidade: row.entidade, direto_indireto: row.direto_indireto, codigo_conta: row.codigo_conta, codigo_centro_custo: row.codigo_centro_custo, centro_custo: row.centro_custo, conta: row.conta } }); // Determinar exclusões baseado nos filtros aplicados let excluirCentroCusto = ""; let excluirCodigoConta = ""; // Se o centro de custo "002.003.017" não está selecionado, excluir da consulta const centroCusto002003017Selecionado = centrosCustoSelecionados.some(centro => { const codigoCentro = codigosCentrosCusto[centro]; if (codigoCentro === "002.003.017") { return true; } const item = data.find((d: DREItem) => d.centro_custo === centro); return item?.codigo_centro_custo === "002.003.017"; }); if (!centroCusto002003017Selecionado) { excluirCentroCusto = "002.003.017"; } // Se a conta "100050" não está selecionada, excluir da consulta const conta100050Selecionada = contasSelecionadas.some(conta => { const codigoConta = codigosContas[conta]; if (codigoConta === "100050") { return true; } const item = data.find((d: DREItem) => d.conta === conta); return item?.codigo_conta?.toString() === "100050"; }); if (!conta100050Selecionada) { excluirCodigoConta = "100050"; } // Obter códigos dos centros de custo selecionados no filtro const codigosCentrosCustoSelecionados = centrosCustoSelecionados .map(centro => { const codigoDoMapeamento = codigosCentrosCusto[centro]; if (codigoDoMapeamento) { return codigoDoMapeamento; } const item = data.find((d: DREItem) => d.centro_custo === centro); return item?.codigo_centro_custo; }) .filter(codigo => codigo && codigo.trim() !== '') .join(','); // Obter códigos das contas selecionadas no filtro const codigosContasSelecionadas = contasSelecionadas .map(conta => { const item = data.find((d: DREItem) => d.conta === conta); return item?.codigo_conta?.toString(); }) .filter(codigo => codigo) .join(','); const novosFiltros = { dataInicio: dataInicioFiltro, dataFim: dataFimFiltro, centroCusto: centroCustoFiltro, codigoGrupo: codigoGrupoFiltro, // CODGRUPO da entidade/linha selecionada codigoSubgrupo: diretoIndiretoFiltro, // Usar direto/indireto como subgrupo codigoConta: codigoContaFiltro, linhaSelecionada: row.entidade || row.direto_indireto || row.centro_custo || row.conta || "", excluirCentroCusto, excluirCodigoConta, codigosCentrosCustoSelecionados, codigosContasSelecionadas, }; console.log('🎯 Novos filtros para analítico:', novosFiltros); setAnaliticoFiltros(novosFiltros); }; const toggleEntidade = useCallback((entidade: string) => { setExpandedEntidades(prev => { const newExpanded = new Set(prev); if (newExpanded.has(entidade)) { newExpanded.delete(entidade); } else { newExpanded.add(entidade); } return newExpanded; }); }, []); const toggleDiretoIndireto = useCallback((chave: string) => { setExpandedDiretoIndireto(prev => { const newExpanded = new Set(prev); if (newExpanded.has(chave)) { newExpanded.delete(chave); } else { newExpanded.add(chave); } return newExpanded; }); }, []); const toggleCentro = useCallback((chave: string) => { setExpandedCentros(prev => { const newExpanded = new Set(prev); if (newExpanded.has(chave)) { newExpanded.delete(chave); } else { newExpanded.add(chave); } return newExpanded; }); }, []); const handleFiltroChange = (campo: string, valor: string) => { setFiltros(prev => ({ ...prev, [campo]: valor })); }; // Funções para multi-seleção const toggleCentroCusto = (centro: string) => { // Verificar se é centro de custo 999.998 (não pode ser desmarcado) const item = data.find((d: DREItem) => d.centro_custo === centro); if (item?.codigo_centro_custo === "999.998") { // Não permitir desmarcar centro de custo 999.998 return; } setCentrosCustoSelecionados(prev => { if (prev.includes(centro)) { return prev.filter(c => c !== centro); } else { return [...prev, centro]; } }); }; const toggleConta = (conta: string) => { // Verificar se é conta 199 (não pode ser desmarcada) const item = data.find((d: DREItem) => d.conta === conta); if (item?.codigo_conta?.toString() === "199") { // Não permitir desmarcar conta 199 return; } setContasSelecionadas(prev => { if (prev.includes(conta)) { return prev.filter(c => c !== conta); } else { return [...prev, conta]; } }); }; const selecionarTodosCentros = () => { // Sempre incluir centro de custo 999.998 mesmo que não esteja nas opções const centroCusto999998 = data.find((d: DREItem) => d.codigo_centro_custo === "999.998")?.centro_custo; const todosCentros = centroCusto999998 && !opcoesCentrosCusto.includes(centroCusto999998) ? [...opcoesCentrosCusto, centroCusto999998] : opcoesCentrosCusto; setCentrosCustoSelecionados(todosCentros); }; const limparCentros = () => { setCentrosCustoSelecionados([]); }; const selecionarTodasContas = () => { // Sempre incluir conta 199 mesmo que não esteja nas opções const conta199 = data.find((d: DREItem) => d.codigo_conta?.toString() === "199")?.conta; const todasContas = conta199 && !opcoesContas.includes(conta199) ? [...opcoesContas, conta199] : opcoesContas; setContasSelecionadas(todasContas); }; const limparContas = () => { setContasSelecionadas([]); }; const toggleEntidadeFiltro = (entidade: string) => { // Verificar se é "Faturamento líquido" (não pode ser desmarcado) const isFaturamentoLiquido = entidade.toLowerCase().includes('faturamento') && (entidade.toLowerCase().includes('líquido') || entidade.toLowerCase().includes('liquido')); if (isFaturamentoLiquido) { // Não permitir desmarcar Faturamento líquido return; } setEntidadesSelecionadas(prev => { if (prev.includes(entidade)) { return prev.filter(e => e !== entidade); } else { return [...prev, entidade]; } }); }; const selecionarTodasEntidades = () => { // Sempre incluir "Faturamento líquido" mesmo que não esteja nas opções const entidadeFaturamentoLiquido = data.find((d: DREItem) => d.entidades && ( d.entidades.toLowerCase().includes('faturamento') && (d.entidades.toLowerCase().includes('líquido') || d.entidades.toLowerCase().includes('liquido')) ) )?.entidades; const todasEntidades = entidadeFaturamentoLiquido && !opcoesEntidades.includes(entidadeFaturamentoLiquido) ? [...opcoesEntidades, entidadeFaturamentoLiquido] : opcoesEntidades; setEntidadesSelecionadas(todasEntidades); }; const limparEntidades = () => { setEntidadesSelecionadas([]); }; // Função auxiliar para obter o código do centro de custo const obterCodigoCentroCusto = React.useCallback((nomeCentro: string): string => { if (!data || data.length === 0) { return ''; } // Buscar o primeiro item que corresponde ao nome do centro de custo const item = data.find(item => item.centro_custo === nomeCentro); if (item && item.codigo_centro_custo) { return item.codigo_centro_custo; } return ''; }, [data]); const exportarXLSX = () => { if (!data.length) { console.log('⚠️ Nenhum dado para exportar'); return; } console.log('📊 Exportando TODOS os dados expandidos para XLSX...'); // Criar uma versão completamente expandida dos dados hierárquicos const dadosCompletosExpandidos = buildHierarchicalDataCompleta(); // Preparar dados para exportação const dadosExportacao = dadosCompletosExpandidos.map((row, index) => { const linha: any = { 'Linha': index + 1, 'Tipo': row.type, 'Nível': row.level, 'Entidade': row.entidade || '', 'Direto/Indireto': row.direto_indireto || '', 'Centro de Custo': row.centro_custo || '', 'Código Centro': row.codigo_centro_custo || '', 'Conta': row.conta || '', 'Código Conta': row.codigo_conta || '', 'Total': row.total || 0, }; // Adicionar colunas dos meses mesesDisponiveis.forEach(mes => { const valor = row.valoresPorMes?.[mes] || 0; const percentual = row.percentuaisPorMes?.[mes] || 0; linha[`Valor ${mes}`] = valor; linha[`% ${mes}`] = percentual; }); return linha; }); // Criar workbook const wb = XLSX.utils.book_new(); // Criar worksheet principal const ws = XLSX.utils.json_to_sheet(dadosExportacao); // Ajustar largura das colunas const colWidths = [ { wch: 8 }, // Linha { wch: 15 }, // Tipo { wch: 8 }, // Nível { wch: 25 }, // Entidade { wch: 20 }, // Direto/Indireto { wch: 25 }, // Centro de Custo { wch: 15 }, // Código Centro { wch: 35 }, // Conta { wch: 12 }, // Código Conta { wch: 15 }, // Total ]; // Adicionar larguras para colunas dos meses mesesDisponiveis.forEach(() => { colWidths.push({ wch: 15 }); // Valor colWidths.push({ wch: 10 }); // % }); ws['!cols'] = colWidths; // Adicionar worksheet ao workbook XLSX.utils.book_append_sheet(wb, ws, 'DRE Gerencial Completo'); // Criar worksheet de resumo const resumoData = [ { 'Informação': 'Período', 'Valor': `${filtros.periodoDe} a ${filtros.periodoAte}` }, { 'Informação': 'Grupo', 'Valor': filtros.grupo }, { 'Informação': 'Subgrupo', 'Valor': filtros.subgrupo }, { 'Informação': 'Centro de Custo', 'Valor': filtros.centroCusto }, { 'Informação': 'Conta', 'Valor': filtros.conta }, { 'Informação': 'Valor Mínimo', 'Valor': filtros.valorMin || 'N/A' }, { 'Informação': 'Valor Máximo', 'Valor': filtros.valorMax || 'N/A' }, { 'Informação': 'Busca Textual', 'Valor': filtros.buscaTextual || 'N/A' }, { 'Informação': 'Ordem Hierárquica', 'Valor': ordemHierarquiaContasPrimeiro ? 'Contas → Centros' : 'Centros → Contas' }, { 'Informação': 'Total de Registros', 'Valor': dadosCompletosExpandidos.length }, { 'Informação': 'Data de Exportação', 'Valor': new Date().toLocaleString('pt-BR') }, ]; const wsResumo = XLSX.utils.json_to_sheet(resumoData); wsResumo['!cols'] = [{ wch: 20 }, { wch: 30 }]; XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo'); // Gerar nome do arquivo const dataAtual = new Date().toISOString().split('T')[0]; const nomeArquivo = `DRE_Gerencial_Completo_${dataAtual}.xlsx`; // Exportar arquivo XLSX.writeFile(wb, nomeArquivo); console.log('✅ Arquivo XLSX completo exportado:', nomeArquivo); }; // Função para construir dados hierárquicos completamente expandidos (para exportação XLSX) // Usa a mesma lógica de buildHierarchicalData mas sempre expandido const buildHierarchicalDataCompleta = (): HierarchicalRow[] => { const rows: HierarchicalRow[] = []; // Nova hierarquia: [entidade, direto/indireto, cc, conta] // Agrupar primeiro por CODGRUPO, depois por entidade const gruposPorCodigo = data.reduce((acc, item) => { const codgrupo = item.codgrupo || ""; if (!codgrupo) return acc; if (!acc[codgrupo]) { acc[codgrupo] = {}; } const entidade = item.entidades || ""; if (!entidade) return acc; if (!acc[codgrupo][entidade]) { acc[codgrupo][entidade] = []; } acc[codgrupo][entidade].push(item); return acc; }, {} as Record>); // Ordenar por CODGRUPO (numericamente), depois por entidade (alfabeticamente) const sortedGrupos = Object.entries(gruposPorCodigo).sort(([codA], [codB]) => { // Ordenar numericamente por CODGRUPO const numA = parseInt(codA) || 0; const numB = parseInt(codB) || 0; if (numA !== numB) { return numA - numB; } // Se não for numérico, ordenar alfabeticamente return codA.localeCompare(codB); }); sortedGrupos.forEach(([codgrupo, entidades]) => { // Ordenar entidades dentro do grupo alfabeticamente const sortedEntidades = Object.entries(entidades).sort(([entA], [entB]) => entA.localeCompare(entB) ); sortedEntidades.forEach(([entidade, items]) => { const totalEntidade = items.reduce((sum, item) => sum + parseFloat(item.valor), 0); const valoresEntidadePorMes = calcularValoresPorMes(items); // Verificar se a entidade contém faturamento líquido const temFaturamentoLiquido = items.some(item => item.codgrupo === "00"); const codgrupoParaCalculo = temFaturamentoLiquido ? "00" : ""; // Linha da entidade (Level 0) - sempre expandida rows.push({ type: "entidade", level: 0, entidade, total: totalEntidade, isExpanded: true, valoresPorMes: valoresEntidadePorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresEntidadePorMes, codgrupoParaCalculo), percentualTotal: calcularPercentualTotal(totalEntidade, codgrupoParaCalculo), }); // Agrupar por direto/indireto (SUBGRUPO) dentro da entidade const diretoIndireto = items.reduce((acc, item) => { const subgrupo = item.subgrupo || ""; if (!subgrupo) return acc; if (!acc[subgrupo]) { acc[subgrupo] = []; } acc[subgrupo].push(item); return acc; }, {} as Record); // Ordenar: DIRETO primeiro, depois INDIRETO const sortedDiretoIndireto = Object.entries(diretoIndireto).sort(([a], [b]) => { if (a.toUpperCase() === "DIRETO" && b.toUpperCase() !== "DIRETO") return -1; if (a.toUpperCase() !== "DIRETO" && b.toUpperCase() === "DIRETO") return 1; if (a.toUpperCase() === "INDIRETO" && b.toUpperCase() !== "INDIRETO") return -1; if (a.toUpperCase() !== "INDIRETO" && b.toUpperCase() === "INDIRETO") return 1; return a.localeCompare(b); }); sortedDiretoIndireto.forEach(([diretoIndireto, diretoIndiretoItems]) => { const totalDiretoIndireto = diretoIndiretoItems.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); const valoresDiretoIndiretoPorMes = calcularValoresPorMes(diretoIndiretoItems); // Verificar se direto/indireto contém faturamento líquido const temFaturamentoLiquidoDI = diretoIndiretoItems.some(item => item.codgrupo === "00"); const codgrupoParaCalculoDI = temFaturamentoLiquidoDI ? "00" : ""; // Linha direto/indireto (Level 1) - sempre expandida rows.push({ type: "direto_indireto", level: 1, entidade, direto_indireto: diretoIndireto, total: totalDiretoIndireto, isExpanded: true, valoresPorMes: valoresDiretoIndiretoPorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresDiretoIndiretoPorMes, codgrupoParaCalculoDI), percentualTotal: calcularPercentualTotal(totalDiretoIndireto, codgrupoParaCalculoDI), }); // Agrupar por centro de custo dentro de direto/indireto const centros = diretoIndiretoItems.reduce((acc, item) => { const centro = item.centro_custo || ""; if (!centro) return acc; if (!acc[centro]) { acc[centro] = []; } acc[centro].push(item); return acc; }, {} as Record); // Ordenar centros de custo por CODIGOCENTROCUSTO const sortedCentros = Object.entries(centros).sort(([centroA, itemsA], [centroB, itemsB]) => { const codigoA = itemsA[0]?.codigo_centro_custo || ""; const codigoB = itemsB[0]?.codigo_centro_custo || ""; if (codigoA && codigoB) { return codigoA.localeCompare(codigoB); } if (codigoA && !codigoB) return -1; if (!codigoA && codigoB) return 1; return centroA.localeCompare(centroB); }); sortedCentros.forEach(([centro, centroItems]) => { const totalCentro = centroItems.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); const valoresCentroPorMes = calcularValoresPorMes(centroItems); // Verificar se centro de custo contém faturamento líquido const temFaturamentoLiquidoCC = centroItems.some(item => item.codgrupo === "00"); const codgrupoParaCalculoCC = temFaturamentoLiquidoCC ? "00" : ""; // Linha do centro de custo (Level 2) - sempre expandida rows.push({ type: "centro_custo", level: 2, entidade, direto_indireto: diretoIndireto, centro_custo: centro, codigo_centro_custo: centroItems[0].codigo_centro_custo, total: totalCentro, isExpanded: true, valoresPorMes: valoresCentroPorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresCentroPorMes, codgrupoParaCalculoCC), percentualTotal: calcularPercentualTotal(totalCentro, codgrupoParaCalculoCC), }); // Agrupar por conta dentro do centro de custo const contas = centroItems.reduce((acc, item) => { const conta = item.conta || ""; if (!conta) return acc; if (!acc[conta]) { acc[conta] = []; } acc[conta].push(item); return acc; }, {} as Record); // Ordenar contas por CODCONTA const sortedContas = Object.entries(contas).sort(([contaA, itemsA], [contaB, itemsB]) => { const codcontaA = itemsA[0]?.codigo_conta || 0; const codcontaB = itemsB[0]?.codigo_conta || 0; if (codcontaA && codcontaB) { return codcontaA - codcontaB; } if (codcontaA && !codcontaB) return -1; if (!codcontaA && codcontaB) return 1; return contaA.localeCompare(contaB); }); sortedContas.forEach(([conta, contaItems]) => { const totalConta = contaItems.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); const valoresContaPorMes = calcularValoresPorMes(contaItems); // Verificar se conta contém faturamento líquido const temFaturamentoLiquidoConta = contaItems.some(item => item.codgrupo === "00"); const codgrupoParaCalculoConta = temFaturamentoLiquidoConta ? "00" : ""; // Linha da conta (Level 3) rows.push({ type: "conta", level: 3, entidade, direto_indireto: diretoIndireto, centro_custo: centro, conta, codigo_conta: contaItems[0].codigo_conta, codigo_centro_custo: centroItems[0].codigo_centro_custo, total: totalConta, valoresPorMes: valoresContaPorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, codgrupoParaCalculoConta), percentualTotal: calcularPercentualTotal(totalConta, codgrupoParaCalculoConta), }); }); }); }); }); }); return rows; }; const toggleExpandAll = useCallback(() => { if (isAllExpanded) { // Recolher tudo startTransition(() => { setExpandedEntidades(new Set()); setExpandedDiretoIndireto(new Set()); setExpandedCentros(new Set()); setIsAllExpanded(false); }); } else { // Expandir todos os níveis da nova hierarquia [entidade, direto/indireto, cc, conta] startTransition(() => { const todasEntidades = [...new Set(data.map(item => item.entidades).filter((e): e is string => Boolean(e)))]; const todosDiretoIndireto = [...new Set( data.map(item => `${item.entidades}-${item.subgrupo}`).filter((e): e is string => Boolean(e)) )]; const todosCentros = [...new Set( data.map(item => `${item.entidades}-${item.subgrupo}-${item.centro_custo}`).filter((e): e is string => Boolean(e)) )]; setExpandedEntidades(new Set(todasEntidades)); setExpandedDiretoIndireto(new Set(todosDiretoIndireto)); setExpandedCentros(new Set(todosCentros)); setIsAllExpanded(true); }); } }, [isAllExpanded, data]); // Função para recalcular grupos calculados baseado apenas nos dados filtrados const recalcularGruposCalculados = (dadosFiltrados: DREItem[]): DREItem[] => { const gruposCalculados: DREItem[] = []; // Agrupar dados por mês para cálculos const dadosPorMes = dadosFiltrados.reduce((acc, item) => { const mes = item.data_competencia; if (!acc[mes]) acc[mes] = []; acc[mes].push(item); return acc; }, {} as Record); // Para cada mês, criar os grupos calculados Object.keys(dadosPorMes).forEach(mes => { const dadosMes = dadosPorMes[mes]; // Calcular valores por grupo usando código numérico const valoresPorGrupo = dadosMes.reduce((acc: Record, item: DREItem) => { const codgrupo = item.codgrupo || ""; if (!codgrupo) return acc; if (!acc[codgrupo]) acc[codgrupo] = 0; acc[codgrupo] += parseFloat(item.valor); return acc; }, {} as Record); // Função auxiliar para obter valor de um grupo (calculado ou não) const obterValorGrupo = (codigoGrupo: string): number => { // Primeiro, verificar se já foi calculado nos grupos calculados const grupoCalculado = gruposCalculados.find(g => g.codgrupo === codigoGrupo && g.data_competencia === mes); if (grupoCalculado) { return parseFloat(grupoCalculado.valor); } // Se não, buscar nos valores diretos dos grupos return valoresPorGrupo[codigoGrupo] || 0; }; // 03 - Faturamento Líquido (01 + 02) const faturamentoBruto = valoresPorGrupo['01'] || 0; const devolucao = valoresPorGrupo['02'] || 0; const faturamentoLiquido = faturamentoBruto + devolucao; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "03 - FATURAMENTO LÍQUIDO", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "FATURAMENTO LÍQUIDO", valor: faturamentoLiquido.toString(), codgrupo: "03", isCalculado: true }); // 05 - Lucro Bruto (03 + 04) - usar grupo 03 calculado const cmv = valoresPorGrupo['04'] || 0; const valor03 = obterValorGrupo("03"); const lucroBruto = valor03 + cmv; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "05 - LUCRO BRUTO", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "LUCRO BRUTO", valor: lucroBruto.toString(), codgrupo: "05", isCalculado: true }); // 07 - Margem Loja (05 + 06) - usar grupo 05 calculado const receitasGastosDiretos = valoresPorGrupo['06'] || 0; const valor05 = obterValorGrupo("05"); const margemLoja = valor05 + receitasGastosDiretos; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "07 - MARGEM LOJA", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "MARGEM LOJA", valor: margemLoja.toString(), codgrupo: "07", isCalculado: true }); // 10 - Resultado Operacional (07 + 08 + 09) - usar grupo 07 calculado const verba = valoresPorGrupo['08'] || 0; const receitasGastosIndiretos = valoresPorGrupo['09'] || 0; const valor07 = obterValorGrupo("07"); const resultadoOperacional = valor07 + verba + receitasGastosIndiretos; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "10 - RESULTADO OPERACIONAL", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "RESULTADO OPERACIONAL", valor: resultadoOperacional.toString(), codgrupo: "10", isCalculado: true }); // 13 - Resultado Financeiro (11 + 12) const receitaFinanceira = valoresPorGrupo['11'] || 0; const despesaFinanceira = valoresPorGrupo['12'] || 0; const resultadoFinanceiro = receitaFinanceira + despesaFinanceira; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "13 - RESULTADO FINANCEIRO", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "RESULTADO FINANCEIRO", valor: resultadoFinanceiro.toString(), codgrupo: "13", isCalculado: true }); // 19 - Resultado Não Operacional (14 + 15 + 16 + 17 + 18) const prejuizosPerdas = valoresPorGrupo['14'] || 0; const inativas = valoresPorGrupo['15'] || 0; const diretoria = valoresPorGrupo['16'] || 0; const lancamentosSemCC = valoresPorGrupo['17'] || 0; const receitasDespesasNaoOperacional = valoresPorGrupo['18'] || 0; const resultadoNaoOperacional = prejuizosPerdas + inativas + diretoria + lancamentosSemCC + receitasDespesasNaoOperacional; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "19 - RESULTADO NÃO OPERACIONAL", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "RESULTADO NÃO OPERACIONAL", valor: resultadoNaoOperacional.toString(), codgrupo: "19", isCalculado: true }); // 20 - LAIR (10 + 13 + 19) - usar grupos calculados const valor10 = obterValorGrupo("10"); const valor13 = obterValorGrupo("13"); const valor19 = obterValorGrupo("19"); const lair = valor10 + valor13 + valor19; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "20 - LAIR", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "LUCRO ANTES DO IMPOSTO DE RENDA", valor: lair.toString(), codgrupo: "20", isCalculado: true }); // 21 - IR = Se LAIR > 0 calcular 20% e resultado negativo (*-1), se não 0 const valor20_ir = obterValorGrupo("20"); const ir = valor20_ir > 0 ? -(valor20_ir * 0.20) : 0; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "21 - IR", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "IMPOSTO DE RENDA", valor: ir.toString(), codgrupo: "21", isCalculado: true }); // 22 - CSLL = Se LAIR > 0 calcular 9% e resultado negativo (*-1), se não 0 const valor20_csll = obterValorGrupo("20"); const csll = valor20_csll > 0 ? -(valor20_csll * 0.09) : 0; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "22 - CSLL", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "CONTRIBUIÇÃO SOCIAL SOBRE LUCRO LÍQUIDO", valor: csll.toString(), codgrupo: "22", isCalculado: true }); // 23 - Lucro Líquido (20 + 21 + 22) - usar grupos calculados const valor20 = obterValorGrupo("20"); const valor21 = obterValorGrupo("21"); const valor22 = obterValorGrupo("22"); const lucroLiquido = valor20 + valor21 + valor22; gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "23 - LUCRO LÍQUIDO", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "LUCRO LÍQUIDO", valor: lucroLiquido.toString(), codgrupo: "23", isCalculado: true }); // 25 - EBITDA (20 - (13 + 19 + 24)) - usar grupos calculados const despesaTributaria = valoresPorGrupo['24'] || 0; const valor20_ebitda = obterValorGrupo("20"); const valor13_ebitda = obterValorGrupo("13"); const valor19_ebitda = obterValorGrupo("19"); const ebitda = valor20_ebitda - (valor13_ebitda + valor19_ebitda + despesaTributaria); gruposCalculados.push({ codfilial: "001", data_competencia: mes, data_cai: mes, grupo: "25 - EBITDA", subgrupo: "CALCULADO", centro_custo: "CALCULADO", codigo_centro_custo: "", codigo_conta: 0, conta: "EBITDA", valor: ebitda.toString(), codgrupo: "25", isCalculado: true }); }); return gruposCalculados; }; 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: "" }); // Limpar multi-seleções setCentrosCustoSelecionados([]); setContasSelecionadas([]); setEntidadesSelecionadas([]); // Limpar filtros de busca setFiltroCentroCusto(""); setFiltroConta(""); setFiltroEntidade(""); // Limpar dados da tabela setData([]); setDadosFiltrados([]); setFiltrosAplicados(false); setMesesDisponiveis([]); setIsAllExpanded(false); setOrdemHierarquiaContasPrimeiro(false); // Fechar o sheet de filtros setIsFilterOpen(false); // Recarregar opções e selecionar todos novamente carregarPeriodosDisponiveis(); }; 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-entidade-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 ); } // Filtro por centro de custo (multi-seleção) - USAR APENAS CÓDIGO // IMPORTANTE: Preservar grupos calculados (isCalculado ou centro_custo === "CALCULADO") // Sempre incluir centro de custo 999.998 mesmo que não esteja na lista de selecionados const centroCusto999998 = dadosCompletos.find((d: DREItem) => d.codigo_centro_custo === "999.998")?.centro_custo; const centrosCustoParaFiltrar = centroCusto999998 && !centrosCustoSelecionados.includes(centroCusto999998) ? [...centrosCustoSelecionados, centroCusto999998] : centrosCustoSelecionados; if (centrosCustoParaFiltrar.length > 0) { // Criar conjunto de códigos esperados dos centros selecionados - APENAS CÓDIGOS const codigosEsperados = new Set(); // Sempre incluir código 999.998 codigosEsperados.add("999.998"); centrosCustoParaFiltrar.forEach(centro => { // Buscar o código no mapeamento primeiro const codigoCentro = codigosCentrosCusto[centro]; if (codigoCentro) { codigosEsperados.add(codigoCentro); } else { // Se não encontrar no mapeamento, tentar buscar nos dados carregados const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro); if (item?.codigo_centro_custo) { codigosEsperados.add(item.codigo_centro_custo); } } }); // Filtrar APENAS pelo código do centro de custo, ignorando o nome // MAS preservar grupos calculados dadosFiltrados = dadosFiltrados.filter((item: DREItem) => { // Preservar grupos calculados (têm centro_custo === "CALCULADO" ou isCalculado === true) if (item.centro_custo === "CALCULADO" || item.isCalculado === true) { return true; } // Para outros itens, verificar pelo código if (!item.codigo_centro_custo) { return false; } return codigosEsperados.has(item.codigo_centro_custo); }); console.log('🏢 Filtro de centros de custo aplicado (APENAS CÓDIGO):', { selecionados: centrosCustoSelecionados, codigosEsperados: Array.from(codigosEsperados), totalFiltrado: dadosFiltrados.length, centrosEncontrados: [...new Set(dadosFiltrados.map((d: DREItem) => d.centro_custo))], codigosEncontrados: [...new Set(dadosFiltrados.map((d: DREItem) => d.codigo_centro_custo).filter(Boolean))], gruposCalculados: dadosFiltrados.filter((d: DREItem) => d.centro_custo === "CALCULADO" || d.isCalculado === true).length }); } // Filtro por conta (multi-seleção) // IMPORTANTE: Preservar grupos calculados (isCalculado ou centro_custo === "CALCULADO") // Sempre incluir conta 199 mesmo que não esteja na lista de selecionadas const conta199 = dadosCompletos.find((d: DREItem) => d.codigo_conta?.toString() === "199")?.conta; const contasParaFiltrar = conta199 && !contasSelecionadas.includes(conta199) ? [...contasSelecionadas, conta199] : contasSelecionadas; if (contasParaFiltrar.length > 0) { dadosFiltrados = dadosFiltrados.filter((item: DREItem) => { // Preservar grupos calculados if (item.centro_custo === "CALCULADO" || item.isCalculado === true) { return true; } // Para outros itens, verificar se a conta está selecionada (incluindo conta 199) return contasParaFiltrar.includes(item.conta); }); } // 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) ); } // Filtro por entidades (multi-seleção) // IMPORTANTE: Preservar grupos calculados (isCalculado ou centro_custo === "CALCULADO") // Sempre incluir "Faturamento líquido" mesmo que não esteja na lista de selecionadas const entidadeFaturamentoLiquido = dadosCompletos.find((d: DREItem) => d.entidades && ( d.entidades.toLowerCase().includes('faturamento') && (d.entidades.toLowerCase().includes('líquido') || d.entidades.toLowerCase().includes('liquido')) ) )?.entidades; const entidadesParaFiltrar = entidadeFaturamentoLiquido ? [...entidadesSelecionadas, entidadeFaturamentoLiquido] : entidadesSelecionadas; if (entidadesParaFiltrar.length > 0) { dadosFiltrados = dadosFiltrados.filter((item: DREItem) => { // Preservar grupos calculados if (item.centro_custo === "CALCULADO" || item.isCalculado === true) { return true; } // Para outros itens, verificar se a entidade está selecionada (incluindo Faturamento líquido) return item.entidades && entidadesParaFiltrar.includes(item.entidades); }); console.log('🏢 Filtro de entidades aplicado:', { selecionadas: entidadesSelecionadas, incluindoFaturamentoLiquido: entidadeFaturamentoLiquido, totalFiltrado: dadosFiltrados.length, entidadesEncontradas: [...new Set(dadosFiltrados.map((d: DREItem) => d.entidades).filter(Boolean))], gruposCalculados: dadosFiltrados.filter((d: DREItem) => d.centro_custo === "CALCULADO" || d.isCalculado === true).length }); } // Remover grupos calculados antigos (que foram calculados com todos os dados) // Eles serão recalculados com base apenas nos dados filtrados dadosFiltrados = dadosFiltrados.filter((item: DREItem) => item.centro_custo !== "CALCULADO" && item.isCalculado !== true ); // Recalcular grupos calculados com base apenas nos dados filtrados const gruposCalculadosRecalculados = recalcularGruposCalculados(dadosFiltrados); // Adicionar os grupos calculados recalculados de volta aos dados filtrados dadosFiltrados = [...dadosFiltrados, ...gruposCalculadosRecalculados]; setData(dadosFiltrados); setDadosFiltrados(dadosFiltrados); setFiltrosAplicados(true); // Extrair meses únicos dos dados filtrados const mesesUnicos = [...new Set(dadosFiltrados.map((item: DREItem) => item.data_competencia))].sort() as string[]; 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 }; const calcularValoresPorMes = (items: DREItem[]): Record => { const valoresPorMes: Record = {}; items.forEach((item) => { // Usar diretamente o valor de data_competencia que já vem no formato YYYY-MM const anoMes = item.data_competencia; if (!valoresPorMes[anoMes]) { valoresPorMes[anoMes] = 0; } valoresPorMes[anoMes] += parseFloat(item.valor); }); return valoresPorMes; }; // Função auxiliar para calcular o faturamento líquido por mês const calcularFaturamentoLiquidoPorMes = (): Record => { const faturamentoLiquidoPorMes: Record = {}; mesesDisponiveis.forEach(mes => { // Buscar itens com CODGRUPO = "00" (faturamento líquido) const dadosMes = data.filter(item => item.data_competencia === mes); const faturamentoLiquidoItems = dadosMes.filter(item => item.codgrupo === "00"); if (faturamentoLiquidoItems.length > 0) { // Se encontrou itens com CODGRUPO = "00", somar seus valores faturamentoLiquidoPorMes[mes] = faturamentoLiquidoItems.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); } else { // Se não encontrou, retornar 0 faturamentoLiquidoPorMes[mes] = 0; } }); return faturamentoLiquidoPorMes; }; // Função para calcular percentuais baseado no faturamento líquido const calcularPercentuaisPorMes = ( valoresPorMes: Record, grupo: string // Usado para verificar se é faturamento líquido (CODGRUPO = "00") ): Record => { const percentuais: Record = {}; const faturamentoLiquidoPorMes = calcularFaturamentoLiquidoPorMes(); // Verificar se é faturamento líquido (CODGRUPO = "00") const isFaturamentoLiquido = grupo === "00" || grupo.includes("00") || grupo.includes("FATURAMENTO LÍQUIDO"); Object.keys(valoresPorMes).forEach((mes) => { const valorAtual = valoresPorMes[mes]; const faturamentoLiquido = faturamentoLiquidoPorMes[mes] || 0; if (isFaturamentoLiquido) { // Se for faturamento líquido, sempre retornar 100% percentuais[mes] = 100; } else if (faturamentoLiquido !== 0) { // Calcular percentual baseado no faturamento líquido percentuais[mes] = (valorAtual / faturamentoLiquido) * 100; } else { percentuais[mes] = 0; } }); return percentuais; }; // Função para calcular percentual do total baseado no faturamento líquido const calcularPercentualTotal = ( total: number, grupo: string // Usado para verificar se é faturamento líquido (CODGRUPO = "00") ): number => { // Verificar se é faturamento líquido const isFaturamentoLiquido = grupo === "00" || grupo.includes("00") || grupo.includes("FATURAMENTO LÍQUIDO"); if (isFaturamentoLiquido) { // Se for faturamento líquido, sempre retornar 100% return 100; } // Calcular o total do faturamento líquido const faturamentoLiquidoPorMes = calcularFaturamentoLiquidoPorMes(); const totalFaturamentoLiquido = Object.values(faturamentoLiquidoPorMes).reduce( (sum, valor) => sum + valor, 0 ); if (totalFaturamentoLiquido !== 0) { return (total / totalFaturamentoLiquido) * 100; } else { return 0; } }; const buildHierarchicalData = (): HierarchicalRow[] => { const rows: HierarchicalRow[] = []; // Nova hierarquia: [entidade, direto/indireto, cc, conta] // Agrupar primeiro por CODGRUPO, depois por entidade const gruposPorCodigo = data.reduce((acc, item) => { const codgrupo = item.codgrupo || ""; if (!codgrupo) return acc; if (!acc[codgrupo]) { acc[codgrupo] = {}; } const entidade = item.entidades || ""; if (!entidade) return acc; if (!acc[codgrupo][entidade]) { acc[codgrupo][entidade] = []; } acc[codgrupo][entidade].push(item); return acc; }, {} as Record>); // Ordenar por CODGRUPO (numericamente), depois por entidade (alfabeticamente) const sortedGrupos = Object.entries(gruposPorCodigo).sort(([codA], [codB]) => { // Ordenar numericamente por CODGRUPO const numA = parseInt(codA) || 0; const numB = parseInt(codB) || 0; if (numA !== numB) { return numA - numB; } // Se não for numérico, ordenar alfabeticamente return codA.localeCompare(codB); }); sortedGrupos.forEach(([codgrupo, entidades]) => { // Ordenar entidades dentro do grupo alfabeticamente const sortedEntidades = Object.entries(entidades).sort(([entA], [entB]) => entA.localeCompare(entB) ); sortedEntidades.forEach(([entidade, items]) => { // Calcular total da entidade const totalEntidade = items.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); const valoresEntidadePorMes = calcularValoresPorMes(items); // Verificar se a entidade contém faturamento líquido (CODGRUPO = "00") const temFaturamentoLiquido = items.some(item => item.codgrupo === "00"); const codgrupoParaCalculo = temFaturamentoLiquido ? "00" : ""; // Linha da entidade (Level 0) rows.push({ type: "entidade", level: 0, entidade, total: totalEntidade, isExpanded: expandedEntidades.has(entidade), valoresPorMes: valoresEntidadePorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresEntidadePorMes, codgrupoParaCalculo), percentualTotal: calcularPercentualTotal(totalEntidade, codgrupoParaCalculo), }); if (expandedEntidades.has(entidade)) { // Agrupar por direto/indireto (SUBGRUPO) dentro da entidade const diretoIndireto = items.reduce((acc, item) => { const subgrupo = item.subgrupo || ""; // SUBGRUPO contém "DIRETO" ou "INDIRETO" if (!subgrupo) return acc; if (!acc[subgrupo]) { acc[subgrupo] = []; } acc[subgrupo].push(item); return acc; }, {} as Record); // Ordenar: DIRETO primeiro, depois INDIRETO, depois outros const sortedDiretoIndireto = Object.entries(diretoIndireto).sort(([a], [b]) => { if (a.toUpperCase() === "DIRETO" && b.toUpperCase() !== "DIRETO") return -1; if (a.toUpperCase() !== "DIRETO" && b.toUpperCase() === "DIRETO") return 1; if (a.toUpperCase() === "INDIRETO" && b.toUpperCase() !== "INDIRETO") return -1; if (a.toUpperCase() !== "INDIRETO" && b.toUpperCase() === "INDIRETO") return 1; return a.localeCompare(b); }); sortedDiretoIndireto.forEach(([diretoIndireto, diretoIndiretoItems]) => { const totalDiretoIndireto = diretoIndiretoItems.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); const valoresDiretoIndiretoPorMes = calcularValoresPorMes(diretoIndiretoItems); // Verificar se direto/indireto contém faturamento líquido const temFaturamentoLiquidoDI = diretoIndiretoItems.some(item => item.codgrupo === "00"); const codgrupoParaCalculoDI = temFaturamentoLiquidoDI ? "00" : ""; // Linha direto/indireto (Level 1) const chaveDiretoIndireto = `${entidade}-${diretoIndireto}`; rows.push({ type: "direto_indireto", level: 1, entidade, direto_indireto: diretoIndireto, total: totalDiretoIndireto, isExpanded: expandedDiretoIndireto.has(chaveDiretoIndireto), valoresPorMes: valoresDiretoIndiretoPorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresDiretoIndiretoPorMes, codgrupoParaCalculoDI), percentualTotal: calcularPercentualTotal(totalDiretoIndireto, codgrupoParaCalculoDI), }); if (expandedDiretoIndireto.has(chaveDiretoIndireto)) { // Agrupar por centro de custo dentro de direto/indireto const centros = diretoIndiretoItems.reduce((acc, item) => { const centro = item.centro_custo || ""; if (!centro) return acc; if (!acc[centro]) { acc[centro] = []; } acc[centro].push(item); return acc; }, {} as Record); // Ordenar centros de custo por CODIGOCENTROCUSTO const sortedCentros = Object.entries(centros).sort(([centroA, itemsA], [centroB, itemsB]) => { const codigoA = itemsA[0]?.codigo_centro_custo || ""; const codigoB = itemsB[0]?.codigo_centro_custo || ""; if (codigoA && codigoB) { return codigoA.localeCompare(codigoB); } if (codigoA && !codigoB) return -1; if (!codigoA && codigoB) return 1; return centroA.localeCompare(centroB); }); sortedCentros.forEach(([centro, centroItems]) => { const totalCentro = centroItems.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); const valoresCentroPorMes = calcularValoresPorMes(centroItems); // Verificar se centro de custo contém faturamento líquido const temFaturamentoLiquidoCC = centroItems.some(item => item.codgrupo === "00"); const codgrupoParaCalculoCC = temFaturamentoLiquidoCC ? "00" : ""; // Linha do centro de custo (Level 2) const chaveCentro = `${entidade}-${diretoIndireto}-${centro}`; rows.push({ type: "centro_custo", level: 2, entidade, direto_indireto: diretoIndireto, centro_custo: centro, codigo_centro_custo: centroItems[0].codigo_centro_custo, total: totalCentro, isExpanded: expandedCentros.has(chaveCentro), valoresPorMes: valoresCentroPorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresCentroPorMes, codgrupoParaCalculoCC), percentualTotal: calcularPercentualTotal(totalCentro, codgrupoParaCalculoCC), }); if (expandedCentros.has(chaveCentro)) { // Agrupar por conta dentro do centro de custo const contas = centroItems.reduce((acc, item) => { const conta = item.conta || ""; if (!conta) return acc; if (!acc[conta]) { acc[conta] = []; } acc[conta].push(item); return acc; }, {} as Record); // Ordenar contas por CODCONTA const sortedContas = Object.entries(contas).sort(([contaA, itemsA], [contaB, itemsB]) => { const codcontaA = itemsA[0]?.codigo_conta || 0; const codcontaB = itemsB[0]?.codigo_conta || 0; if (codcontaA && codcontaB) { return codcontaA - codcontaB; } if (codcontaA && !codcontaB) return -1; if (!codcontaA && codcontaB) return 1; return contaA.localeCompare(contaB); }); sortedContas.forEach(([conta, contaItems]) => { const totalConta = contaItems.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); const valoresContaPorMes = calcularValoresPorMes(contaItems); // Verificar se conta contém faturamento líquido const temFaturamentoLiquidoConta = contaItems.some(item => item.codgrupo === "00"); const codgrupoParaCalculoConta = temFaturamentoLiquidoConta ? "00" : ""; // Linha da conta (Level 3) rows.push({ type: "conta", level: 3, entidade, direto_indireto: diretoIndireto, centro_custo: centro, conta, codigo_conta: contaItems[0].codigo_conta, codigo_centro_custo: centroItems[0].codigo_centro_custo, total: totalConta, valoresPorMes: valoresContaPorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, codgrupoParaCalculoConta), percentualTotal: calcularPercentualTotal(totalConta, codgrupoParaCalculoConta), }); }); } }); } }); } }); }); return rows; }; const getRowStyle = (row: HierarchicalRow) => { const baseStyle = "transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30"; // Criar identificador único para a linha const linhaId = `${row.type}-${row.entidade || ""}-${row.direto_indireto || ""}-${ row.centro_custo || "" }-${row.codigo_conta || ""}`; const isSelected = linhaSelecionada === linhaId; let style = baseStyle; if (isSelected) { style += " bg-gradient-to-r from-green-100 to-emerald-100 border-l-4 border-green-500 shadow-lg"; } switch (row.type) { case "entidade": 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`; case "direto_indireto": return `${style} bg-gradient-to-r from-gray-50/30 to-blue-50/20 font-semibold text-gray-800`; case "centro_custo": return `${style} bg-gradient-to-r from-gray-50/20 to-gray-100/10 font-medium text-gray-700`; case "conta": return `${style} bg-white font-normal text-gray-600`; default: return style; } }; // Função para obter o background da célula fixa baseado no tipo de linha const getFixedCellBackground = (row: HierarchicalRow): string => { const linhaId = `${row.type}-${row.entidade || ""}-${row.direto_indireto || ""}-${ row.centro_custo || "" }-${row.codigo_conta || ""}`; const isSelected = linhaSelecionada === linhaId; if (isSelected) { return "bg-gradient-to-r from-green-100 to-emerald-100"; } switch (row.type) { case "entidade": return "bg-gradient-to-r from-blue-50 to-indigo-50"; case "direto_indireto": return "bg-gradient-to-r from-gray-50 to-blue-50"; case "centro_custo": return "bg-gradient-to-r from-gray-50 to-gray-100"; case "conta": return "bg-white"; default: return "bg-white"; } }; const getIndentStyle = (level: number) => { return { paddingLeft: `${level * 20}px` }; }; const renderCellContent = (row: HierarchicalRow) => { switch (row.type) { case "entidade": return (
); case "direto_indireto": return (
); case "centro_custo": return (
); case "conta": return (
); default: return null; } }; // Loading será tratado dentro do componente principal // Error será tratado dentro do componente principal const hierarchicalData = buildHierarchicalData(); return (
{/* Header Section */}

DRE Gerencial

Demonstração do Resultado do Exercício

{/* Controles */}
{/* Botão de Exportar XLSX */} {/* Botão de Expandir/Recolher */} {/* Botão de Filtro */} Filtros Ajuste os critérios e clique em Pesquisar para atualizar a visão.
{/* Período */}
{/* Grupo
*/} {/* Subgrupo
*/} {/* Centro de Custo */}
{/* Input de filtro para Centro de Custo */} setFiltroCentroCusto(e.target.value)} className="h-8 text-sm" />
{opcoesCentrosCusto .filter(centro => { if (!filtroCentroCusto) return true; const termo = filtroCentroCusto.toLowerCase(); const nomeCompleto = `${centro}${codigosCentrosCusto[centro] ? ` - ${codigosCentrosCusto[centro]}` : ''}`; return nomeCompleto.toLowerCase().includes(termo); }) .map(centro => (
toggleCentroCusto(centro)} />
))}
{centrosCustoSelecionados.length > 0 && (
{centrosCustoSelecionados.length} centro(s) selecionado(s)
)}
{/* Conta */}
{/* Input de filtro para Conta */} setFiltroConta(e.target.value)} className="h-8 text-sm" />
{opcoesContas .filter(conta => { if (!filtroConta) return true; const termo = filtroConta.toLowerCase(); const nomeCompleto = `${conta}${codigosContas[conta] ? ` - ${codigosContas[conta]}` : ''}`; return nomeCompleto.toLowerCase().includes(termo); }) .map(conta => (
toggleConta(conta)} />
))}
{contasSelecionadas.length > 0 && (
{contasSelecionadas.length} conta(s) selecionada(s)
)}
{/* Valor
R$ handleFiltroChange('valorMin', e.target.value)} className="pl-8" placeholder="0,00" />
R$ handleFiltroChange('valorMax', e.target.value)} className="pl-8" placeholder="0,00" />
*/} {/* Busca Textual
handleFiltroChange('buscaTextual', e.target.value)} placeholder="Pesquise por grupo, subgrupo, centro ou conta" />
*/} {/* Entidades */}
{/* Input de filtro para Entidades */} setFiltroEntidade(e.target.value)} className="h-8 text-sm" />
{opcoesEntidades .filter(entidade => { if (!filtroEntidade) return true; const termo = filtroEntidade.toLowerCase(); return entidade.toLowerCase().includes(termo); }) .map(entidade => (
toggleEntidadeFiltro(entidade)} />
))}
{entidadesSelecionadas.length > 0 && (
{entidadesSelecionadas.length} entidade(s) selecionada(s)
)}
{/* Ordem da Hierarquia */}
setOrdemHierarquiaContasPrimeiro(checked)} />
{ordemHierarquiaContasPrimeiro ? "📄 Contas → 🏢 Centros de Custo" : "🏢 Centros de Custo → 📄 Contas" }
{/* Loading quando aplicando filtros */} {loading && (

Aplicando filtros...

Aguarde enquanto processamos os dados.

)} {/* Erro */} {error && !loading && (

Erro ao carregar dados

{error}

)} {/* Mensagem quando não há dados */} {!filtrosAplicados && !loading && !error && (

Nenhum dado exibido

Clique no botão "Filtros" para definir os critérios de busca e visualizar os dados do DRE.

)} {/* Table Container */} {filtrosAplicados && !loading && !error && (
{/* Scroll Container - Apenas um container com scroll */}
{/* Table */} {/* Table Header */} {mesesDisponiveis.map((mes) => ( ))} {/* Table Body */} {hierarchicalData.map((row, index) => { const linhaId = `${row.type}-${row.entidade || ""}-${row.direto_indireto || ""}-${row.centro_custo || ""}-${row.codigo_conta || ""}`; const isSelected = linhaSelecionada === linhaId; return ( {/* Colunas de valores por mês */} {mesesDisponiveis.map((mes) => ( ))} {/* Coluna Total */} {/* Coluna Percentual Total */} ); })}
Descrição {mes} % Total %
handleRowClick(row)} >
{renderCellContent(row)}
handleRowClick(row, mes)} title={ row.valoresPorMes && row.valoresPorMes[mes] ? formatCurrency(row.valoresPorMes[mes]) : "-" } > {row.valoresPorMes && row.valoresPorMes[mes] ? (() => { const { formatted, isNegative } = formatCurrencyWithColor(row.valoresPorMes[mes]); return ( {formatted} ); })() : "-"} handleRowClick(row, mes)} title={ row.percentuaisPorMes && row.percentuaisPorMes[mes] !== undefined ? `${row.percentuaisPorMes[mes].toFixed(1)}%` : "-" } > {row.percentuaisPorMes && row.percentuaisPorMes[mes] !== undefined ? `${row.percentuaisPorMes[mes].toFixed(1)}%` : "-"} handleRowClick(row)} title={row.total ? formatCurrency(row.total) : "-"} > {(() => { const { formatted, isNegative } = formatCurrencyWithColor( row.total! ); return ( {formatted} ); })()} handleRowClick(row)} title={ row.percentualTotal !== undefined ? `${row.percentualTotal.toFixed(1)}%` : "-" } > {row.percentualTotal !== undefined ? `${row.percentualTotal.toFixed(1)}%` : "-"}
)} {/* Componente Analítico - Sempre renderizado para evitar violação das Rules of Hooks */}
); }