"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[]; setOpcoesCentrosCusto(centrosCustoUnicos); // 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[]; setOpcoesContas(contasUnicas); // 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); setOpcoesEntidades(entidadesUnicas); // Inicializar com todos os itens selecionados, exceto o centro de custo 002.003.017 e conta 100050 const centrosCustoIniciaisSelecionados = centrosCustoUnicos.filter(centro => { const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro); return item?.codigo_centro_custo !== "002.003.017"; }); const contasIniciaisSelecionadas = contasUnicas.filter(conta => { const item = dadosCompletos.find((d: DREItem) => d.conta === conta); return item?.codigo_conta?.toString() !== "100050"; }); setCentrosCustoSelecionados(centrosCustoIniciaisSelecionados); setContasSelecionadas(contasIniciaisSelecionadas); // Inicializar com todas as entidades selecionadas 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 entidadeFiltro = ""; let diretoIndiretoFiltro = ""; // Sempre filtrar por entidade se disponível entidadeFiltro = row.entidade || ""; // Filtrar por direto/indireto se disponível diretoIndiretoFiltro = row.direto_indireto || ""; // 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, 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: "", // Não usado na nova hierarquia 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) => { 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([]); }; const toggleEntidadeFiltro = (entidade: string) => { setEntidadesSelecionadas(prev => { if (prev.includes(entidade)) { return prev.filter(e => e !== entidade); } else { return [...prev, entidade]; } }); }; const selecionarTodasEntidades = () => { setEntidadesSelecionadas(opcoesEntidades); }; 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 por entidade const entidades = data.reduce((acc, item) => { const entidade = item.entidades || ""; if (!entidade) return acc; if (!acc[entidade]) { acc[entidade] = []; } acc[entidade].push(item); return acc; }, {} as Record); // Ordenar entidades 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); // Linha da entidade (Level 0) - sempre expandida rows.push({ type: "entidade", level: 0, entidade, total: totalEntidade, isExpanded: true, valoresPorMes: valoresEntidadePorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresEntidadePorMes, ""), percentualTotal: calcularPercentualTotal(totalEntidade, ""), }); // 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); // 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, ""), percentualTotal: calcularPercentualTotal(totalDiretoIndireto, ""), }); // 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); // 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, ""), percentualTotal: calcularPercentualTotal(totalCentro, ""), }); // 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); // 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, ""), percentualTotal: calcularPercentualTotal(totalConta, ""), }); }); }); }); }); 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") if (centrosCustoSelecionados.length > 0) { // Criar conjunto de códigos esperados dos centros selecionados - APENAS CÓDIGOS const codigosEsperados = new Set(); centrosCustoSelecionados.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") if (contasSelecionadas.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 return contasSelecionadas.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") if (entidadesSelecionadas.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 return item.entidades && entidadesSelecionadas.includes(item.entidades); }); console.log('🏢 Filtro de entidades aplicado:', { selecionadas: entidadesSelecionadas, 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 para calcular percentuais (simplificada para nova hierarquia) const calcularPercentuaisPorMes = ( valoresPorMes: Record, grupo: string // Não usado na nova hierarquia, mantido para compatibilidade ): Record => { const percentuais: Record = {}; // Calcular o total geral de todas as entidades para usar como referência const totalGeralPorMes: Record = {}; mesesDisponiveis.forEach(mes => { totalGeralPorMes[mes] = data .filter(item => item.data_competencia === mes) .reduce((sum, item) => sum + parseFloat(item.valor), 0); }); // Calcular percentuais baseado no total geral Object.keys(valoresPorMes).forEach((mes) => { const valorAtual = valoresPorMes[mes]; const totalGeral = totalGeralPorMes[mes] || 0; if (totalGeral !== 0) { percentuais[mes] = (valorAtual / totalGeral) * 100; } else { percentuais[mes] = 0; } }); return percentuais; }; // Função para calcular percentual do total const calcularPercentualTotal = ( total: number, grupo: string // Não usado na nova hierarquia, mantido para compatibilidade ): number => { // Calcular o total geral const totalGeral = data.reduce( (sum, item) => sum + parseFloat(item.valor), 0 ); if (totalGeral !== 0) { return (total / totalGeral) * 100; } else { return 0; } }; const buildHierarchicalData = (): HierarchicalRow[] => { const rows: HierarchicalRow[] = []; // Nova hierarquia: [entidade, direto/indireto, cc, conta] // Agrupar por entidade const entidades = data.reduce((acc, item) => { const entidade = item.entidades || ""; if (!entidade) return acc; if (!acc[entidade]) { acc[entidade] = []; } acc[entidade].push(item); return acc; }, {} as Record); // Ordenar entidades 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); // Linha da entidade (Level 0) rows.push({ type: "entidade", level: 0, entidade, total: totalEntidade, isExpanded: expandedEntidades.has(entidade), valoresPorMes: valoresEntidadePorMes, percentuaisPorMes: calcularPercentuaisPorMes(valoresEntidadePorMes, ""), percentualTotal: calcularPercentualTotal(totalEntidade, ""), }); 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); // 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, ""), percentualTotal: calcularPercentualTotal(totalDiretoIndireto, ""), }); 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); // 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, ""), percentualTotal: calcularPercentualTotal(totalCentro, ""), }); 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); // 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, ""), percentualTotal: calcularPercentualTotal(totalConta, ""), }); }); } }); } }); } }); 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 */}
); }