diff --git a/src/app/api/analitico-entidade-oracle/route.ts b/src/app/api/analitico-entidade-oracle/route.ts new file mode 100644 index 0000000..8b725d9 --- /dev/null +++ b/src/app/api/analitico-entidade-oracle/route.ts @@ -0,0 +1,256 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { executeOracleQuery } from '@/db/oracle'; + +export async function GET(request: NextRequest) { + try { + console.log('🔄 Buscando dados analíticos do Oracle (Entidade)...'); + console.log('📋 URL completa:', request.url); + console.log('🔍 Query params:', request.nextUrl.searchParams.toString()); + console.log('📊 Headers:', Object.fromEntries(request.headers.entries())); + + // Extrair parâmetros de query + const searchParams = request.nextUrl.searchParams; + const dataInicio = searchParams.get('dataInicio'); + const dataFim = searchParams.get('dataFim'); + const centroCusto = searchParams.get('centroCusto'); + const codigoGrupo = searchParams.get('codigoGrupo'); + const codigoSubgrupo = searchParams.get('codigoSubgrupo'); + const codigoConta = searchParams.get('codigoConta'); + + // Parâmetros para exclusão de valores específicos + const excluirCentroCusto = searchParams.get('excluirCentroCusto'); + const excluirCodigoConta = searchParams.get('excluirCodigoConta'); + + // Novos parâmetros para códigos selecionados no filtro + const codigosCentrosCustoSelecionados = searchParams.get('codigosCentrosCustoSelecionados'); + const codigosContasSelecionadas = searchParams.get('codigosContasSelecionadas'); + + console.log('🎯 Filtros recebidos na API:', { + dataInicio, + dataFim, + centroCusto, + codigoGrupo, + codigoSubgrupo, + codigoConta, + excluirCentroCusto, + excluirCodigoConta, + codigosCentrosCustoSelecionados, + codigosContasSelecionadas + }); + console.log('🔍 Verificação específica de centroCusto:', { + centroCusto, + tipo: typeof centroCusto, + vazio: centroCusto === null || centroCusto === undefined || centroCusto === '', + codigosCentrosCustoSelecionados, + codigosCentrosCustoSelecionadosVazio: !codigosCentrosCustoSelecionados || codigosCentrosCustoSelecionados === '' + }); + + // Construir query SQL com filtros usando a view DESPESA_ENTIDADE_ANALITICO + let sql = `SELECT * FROM DESPESA_ENTIDADE_ANALITICO WHERE 1=1`; + const params: any[] = []; + let paramIndex = 1; + + // Filtro por período (usando ANOMESCOMP) + if (dataInicio && dataFim) { + sql += ` AND ANOMESCOMP >= :${paramIndex} AND ANOMESCOMP <= :${paramIndex + 1}`; + params.push(dataInicio, dataFim); + paramIndex += 2; + console.log('📅 Adicionando filtro de período:', dataInicio, 'a', dataFim); + } + + // Filtro por código do grupo + if (codigoGrupo) { + sql += ` AND CODGRUPO = :${paramIndex}`; + params.push(codigoGrupo); + paramIndex++; + console.log('📊 Adicionando filtro de grupo:', codigoGrupo); + } + + // Filtro por subgrupo (DIRETO, INDIRETO, SEM CC, etc.) + if (codigoSubgrupo) { + sql += ` AND SUBGRUPO = :${paramIndex}`; + params.push(codigoSubgrupo); + paramIndex++; + console.log('📊 Adicionando filtro de subgrupo:', codigoSubgrupo); + } + + // Filtro por código da conta + if (codigoConta) { + sql += ` AND CODCONTA = :${paramIndex}`; + params.push(codigoConta); + paramIndex++; + console.log('💰 Adicionando filtro de conta:', codigoConta); + } + + // Filtro por códigos de centros de custo selecionados no filtro + // Se houver codigosCentrosCustoSelecionados E centroCusto individual, incluir ambos + console.log('🔍 Antes de aplicar filtro de centro de custo:', { + codigosCentrosCustoSelecionados, + centroCusto, + codigosCentrosCustoSelecionadosVazio: !codigosCentrosCustoSelecionados || codigosCentrosCustoSelecionados.trim() === '', + centroCustoVazio: !centroCusto || centroCusto.trim() === '', + centroCustoLength: centroCusto?.length, + codigosCentrosCustoSelecionadosLength: codigosCentrosCustoSelecionados?.length + }); + + // IMPORTANTE: Quando centroCusto individual é fornecido (clique na célula), ele tem PRIORIDADE ABSOLUTA + // e deve filtrar APENAS por ele, ignorando codigosCentrosCustoSelecionados do filtro geral + if (centroCusto && centroCusto.trim() !== '') { + // Quando há centroCusto individual (clique na célula), usar APENAS ele + // Ignorar codigosCentrosCustoSelecionados do filtro geral para garantir filtro preciso + sql += ` AND CODIGOCENTROCUSTO = :${paramIndex}`; + params.push(centroCusto); + paramIndex++; + console.log('🏢 PRIORIDADE: Filtrando APENAS por centroCusto individual (clique na célula):', centroCusto); + console.log('⚠️ Ignorando codigosCentrosCustoSelecionados do filtro geral quando há centroCusto individual'); + console.log('📝 SQL após adicionar filtro =:', sql.substring(0, 200) + '...'); + } else if (codigosCentrosCustoSelecionados && codigosCentrosCustoSelecionados.trim() !== '') { + // Se só codigosCentrosCustoSelecionados existe (sem clique na célula), usar ele + const codigosArray = codigosCentrosCustoSelecionados.split(',').filter(c => c.trim() !== ''); + if (codigosArray.length > 0) { + const placeholders = codigosArray.map(() => `:${paramIndex++}`).join(','); + sql += ` AND CODIGOCENTROCUSTO IN (${placeholders})`; + params.push(...codigosArray); + console.log('🏢 Filtrando por códigos de centros de custo selecionados (filtro geral):', codigosArray); + console.log('📝 SQL após adicionar filtro IN:', sql.substring(0, 200) + '...'); + } + } else { + console.log('⚠️ Nenhum filtro de centro de custo aplicado - ambos estão vazios'); + } + + // Exclusão de centro de custo específico (quando desmarcado) + // Só aplicar se não houver codigosCentrosCustoSelecionados, para evitar conflito + if (excluirCentroCusto && !codigosCentrosCustoSelecionados) { + sql += ` AND CODIGOCENTROCUSTO != :${paramIndex}`; + params.push(excluirCentroCusto); + paramIndex++; + console.log('🚫 Excluindo centro de custo:', excluirCentroCusto); + } + + // Exclusão de código de conta específico (quando desmarcado) + if (excluirCodigoConta) { + sql += ` AND CODCONTA != :${paramIndex}`; + params.push(excluirCodigoConta); + paramIndex++; + console.log('🚫 Excluindo código de conta:', excluirCodigoConta); + } + + // Filtro por códigos de contas selecionadas no filtro + if (codigosContasSelecionadas) { + const codigosArray = codigosContasSelecionadas.split(','); + const placeholders = codigosArray.map(() => `:${paramIndex++}`).join(','); + sql += ` AND CODCONTA IN (${placeholders})`; + params.push(...codigosArray); + console.log('💰 Filtrando por códigos de contas:', codigosArray); + } + + sql += ` ORDER BY DTVENC, CODFORNEC, CODCONTA`; + + // Log detalhado da query SQL final + console.log('═══════════════════════════════════════════════════════════════'); + console.log('🗄️ QUERY SQL FINAL:'); + console.log('═══════════════════════════════════════════════════════════════'); + console.log(sql); + console.log('═══════════════════════════════════════════════════════════════'); + console.log('📋 PARÂMETROS FINAIS (na ordem dos placeholders :1, :2, :3, ...):'); + console.log('═══════════════════════════════════════════════════════════════'); + params.forEach((param, index) => { + console.log(` :${index + 1} = ${param} (${typeof param})`); + }); + console.log('═══════════════════════════════════════════════════════════════'); + console.log('📊 RESUMO DOS FILTROS APLICADOS:'); + console.log('═══════════════════════════════════════════════════════════════'); + console.log({ + temPeriodo: dataInicio && dataFim, + periodo: dataInicio && dataFim ? `${dataInicio} a ${dataFim}` : 'N/A', + temCodigoGrupo: !!codigoGrupo, + codigoGrupo: codigoGrupo || 'N/A', + temCodigoSubgrupo: !!codigoSubgrupo, + codigoSubgrupo: codigoSubgrupo || 'N/A', + temCentroCusto: !!centroCusto, + centroCusto: centroCusto || 'N/A', + temCodigosCentrosCustoSelecionados: !!codigosCentrosCustoSelecionados, + codigosCentrosCustoSelecionados: codigosCentrosCustoSelecionados || 'N/A', + temCodigoConta: !!codigoConta, + codigoConta: codigoConta || 'N/A', + temCodigosContasSelecionadas: !!codigosContasSelecionadas, + codigosContasSelecionadas: codigosContasSelecionadas || 'N/A', + temExcluirCentroCusto: !!excluirCentroCusto, + excluirCentroCusto: excluirCentroCusto || 'N/A', + temExcluirCodigoConta: !!excluirCodigoConta, + excluirCodigoConta: excluirCodigoConta || 'N/A' + }); + console.log('═══════════════════════════════════════════════════════════════'); + + // Se há centroCusto individual, destacar especialmente + if (centroCusto && centroCusto.trim() !== '') { + console.log('🎯 FILTRO INDIVIDUAL DE CENTRO DE CUSTO ATIVO (clique na célula)'); + console.log(` Centro de Custo: ${centroCusto}`); + console.log(' ⚠️ Este filtro tem PRIORIDADE sobre codigosCentrosCustoSelecionados'); + } + console.log('═══════════════════════════════════════════════════════════════'); + + const data = await executeOracleQuery(sql, params); + + console.log('✅ Query executada com sucesso:', data.length, 'registros encontrados'); + console.log('📝 Primeiros 3 registros:', data.slice(0, 3)); + + // Transformar os dados do Oracle para o formato esperado pelo componente + // Usando a view DESPESA_ENTIDADE_ANALITICO + const transformedData = data.map((item: any) => { + return { + codigo_grupo: item.CODGRUPO || "", + codigo_subgrupo: item.SUBGRUPO || "", // SUBGRUPO existe na nova view + codigo_fornecedor: item.CODFORNEC || "", + nome_fornecedor: item.FORNECEDOR || "", + id: item.NUMLANC || 0, + codfilial: "001", // Valor padrão + recnum: item.NUMLANC || 0, + data_competencia: item.ANOMESCOMP || "", + data_vencimento: item.DTVENC ? new Date(item.DTVENC).toISOString().split('T')[0] : "", + data_pagamento: item.DTPAGTO ? new Date(item.DTPAGTO).toISOString().split('T')[0] : "", + data_caixa: item.DTCAIXA ? new Date(item.DTCAIXA).toISOString().split('T')[0] : "", + codigo_conta: item.CODCONTA || "", + conta: item.CONTA || "", + codigo_centrocusto: item.CODIGOCENTROCUSTO || "", + centro_custo: item.CENTROCUSTO || "", + valor: item.VLREALIZADO !== null && item.VLREALIZADO !== undefined ? Number(item.VLREALIZADO) : 0, + historico: item.HISTORICO || "", + historico2: item.HISTORICO2 || "", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + // Campos adicionais do Oracle + entidade: item.ENTIDADE || "", + tipo_parceiro: item.TIPOPARCEIRO || "", + valor_previsto: item.VLPREVISTO !== null && item.VLPREVISTO !== undefined ? Number(item.VLPREVISTO) : 0, + valor_confirmado: item.VLCONFIRMADO !== null && item.VLCONFIRMADO !== undefined ? Number(item.VLCONFIRMADO) : 0, + valor_pago: item.VLPAGO !== null && item.VLPAGO !== undefined ? Number(item.VLPAGO) : 0, + numero_lancamento: item.NUMLANC || 0, + ano_mes_comp: item.ANOMESCOMP || "", + codgrupo: item.CODGRUPO || "", + // Novos campos + data_lancamento: item.DTLANC ? new Date(item.DTLANC).toISOString().split('T')[0] : "", + data_compensacao: item.DTCOMPENSACAO ? new Date(item.DTCOMPENSACAO).toISOString().split('T')[0] : "", + data_pagto: item.DTPAGTO ? new Date(item.DTPAGTO).toISOString().split('T')[0] : "" + }; + }); + + console.log('🔄 Dados transformados:', transformedData.length, 'registros'); + console.log('📝 Primeiros 3 transformados:', transformedData.slice(0, 3)); + + return NextResponse.json(transformedData); + + } catch (error) { + console.error('❌ Erro ao buscar dados analíticos do Oracle:', error); + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + details: error instanceof Error ? error.stack : undefined + }, + { status: 500 } + ); + } +} + diff --git a/src/app/api/dre-entidade-oracle/route.ts b/src/app/api/dre-entidade-oracle/route.ts new file mode 100644 index 0000000..e8d9ccc --- /dev/null +++ b/src/app/api/dre-entidade-oracle/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { executeOracleQuery } from '@/db/oracle'; + +export async function GET(request: NextRequest) { + try { + console.log('🔄 Buscando dados DRE do Oracle (Entidade) - View DESPESA_ENTIDADE_SINTETICO...'); + + // Query para buscar dados da view DESPESA_ENTIDADE_SINTETICO + const sql = `SELECT * FROM DESPESA_ENTIDADE_SINTETICO ORDER BY ENTIDADE, SUBGRUPO, CODIGOCENTROCUSTO, CODCONTA`; + + const data = await executeOracleQuery(sql); + + console.log('✅ Query executada com sucesso:', data.length, 'registros encontrados'); + + // Debug: Verificar estrutura dos dados + if (data.length > 0) { + console.log('🔍 Primeiro registro do Oracle:', Object.keys(data[0])); + console.log('🔍 Primeiro registro completo:', data[0]); + console.log('🔍 Valores únicos de ENTIDADE:', [...new Set(data.map((item: any) => item.ENTIDADE).filter(Boolean))]); + console.log('🔍 Valores únicos de SUBGRUPO:', [...new Set(data.map((item: any) => item.SUBGRUPO).filter(Boolean))]); + } + + // Transformar os dados do Oracle para o formato esperado pelo componente + const transformedData = data.map((item: any) => { + // Converter DATA para formato YYYY-MM se necessário + let dataCompetencia = item.DATA; + if (dataCompetencia) { + // Se DATA for uma string no formato YYYY-MM, usar diretamente + if (typeof dataCompetencia === 'string' && dataCompetencia.match(/^\d{4}-\d{2}$/)) { + // Já está no formato correto + } else if (dataCompetencia instanceof Date) { + // Se for Date, converter para YYYY-MM + const year = dataCompetencia.getFullYear(); + const month = String(dataCompetencia.getMonth() + 1).padStart(2, '0'); + dataCompetencia = `${year}-${month}`; + } else { + // Tentar converter string de data para YYYY-MM + try { + const date = new Date(dataCompetencia); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + dataCompetencia = `${year}-${month}`; + } catch (e) { + console.warn('⚠️ Erro ao converter DATA:', dataCompetencia); + dataCompetencia = "2023-03"; // Valor padrão + } + } + } else { + dataCompetencia = "2023-03"; // Valor padrão + } + + return { + codfilial: "001", // Valor padrão + data_competencia: dataCompetencia, + data_cai: dataCompetencia, + grupo: item.CODGRUPO || "", // CODGRUPO + subgrupo: item.SUBGRUPO || "", // SUBGRUPO (DIRETO/INDIRETO) + centro_custo: item.CENTROCUSTO || "", // CENTROCUSTO (nome do centro) + codigo_centro_custo: item.CODIGOCENTROCUSTO || "", // CODIGOCENTROCUSTO (código do centro) + codigo_conta: parseInt(item.CODCONTA) || 0, // Converter CODCONTA para número + conta: item.CONTA || "", // CONTA + valor: item.VALOR?.toString() || "0", // Converter VALOR para string + codgrupo: item.CODGRUPO || "", // CODGRUPO + entidades: item.ENTIDADE || "", // ENTIDADE + }; + }); + + console.log('✅ Dados transformados:', transformedData.length, 'registros'); + console.log('🔍 Exemplo de registro transformado:', transformedData[0]); + + return NextResponse.json(transformedData); + + } catch (error) { + console.error('❌ Erro ao buscar dados DRE do Oracle:', error); + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + details: error instanceof Error ? error.stack : undefined + }, + { status: 500 } + ); + } +} + + diff --git a/src/app/dre-entidade/analitico.tsx b/src/app/dre-entidade/analitico.tsx new file mode 100644 index 0000000..93d3dc0 --- /dev/null +++ b/src/app/dre-entidade/analitico.tsx @@ -0,0 +1,1374 @@ +"use client"; + +import * as React from "react"; +import { DataGridPremium, GridToolbar, GridColDef, GridFilterModel } from "@mui/x-data-grid-premium"; +import { LicenseInfo } from '@mui/x-license-pro'; + +// Garantir que a licença seja aplicada no componente +if (typeof window !== 'undefined') { + try { + const PERPETUAL_LICENSE_KEY = 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y'; + LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY); + console.log('✅ Licença MUI X aplicada no componente Analítico'); + } catch (error) { + console.warn('⚠️ Erro ao aplicar licença no componente:', error); + } +} +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { Download, Filter, X, Search, ArrowUpDown, ArrowUp, ArrowDown, Maximize2, Minimize2 } from "lucide-react"; +import * as XLSX from "xlsx"; + +interface AnaliticoItem { + codigo_grupo: string; + codigo_subgrupo: string; + codigo_fornecedor: string; + nome_fornecedor: string; + id: number; + codfilial: string; + recnum: number; + data_competencia: string; + data_vencimento: string; + data_pagamento: string; + data_caixa: string; + codigo_conta: string; + conta: string; + codigo_centrocusto: string; + centro_custo?: string; + valor: number; + historico: string; + historico2: string; + created_at: string; + updated_at: string; + // Campos adicionais do Oracle + entidade?: string; + tipo_parceiro?: string; + valor_previsto?: number; + valor_confirmado?: number; + valor_pago?: number; + numero_lancamento?: number; + ano_mes_comp?: string; + codgrupo?: string; + // Novos campos + data_lancamento?: string; + data_compensacao?: string; + data_pagto?: string; +} + +interface AnaliticoProps { + filtros: { + dataInicio: string; + dataFim: string; + centroCusto?: string; + codigoGrupo?: string; + codigoSubgrupo?: string; + codigoConta?: string; + linhaSelecionada?: string; + excluirCentroCusto?: string; + excluirCodigoConta?: string; + codigosCentrosCustoSelecionados?: string; + codigosContasSelecionadas?: string; + }; +} + +// Componente de filtro customizado estilo Excel +interface ExcelFilterProps { + column: GridColDef; + data: any[]; + filteredData: any[]; // Dados filtrados para mostrar apenas valores disponíveis + onFilterChange: (field: string, values: string[]) => void; + onSortChange: (field: string, direction: 'asc' | 'desc' | null) => void; + currentFilter?: string[]; + currentSort?: 'asc' | 'desc' | null; +} + +const ExcelFilter: React.FC = ({ + column, + data, + filteredData, + onFilterChange, + onSortChange, + currentFilter = [], + currentSort = null, +}) => { + const [isOpen, setIsOpen] = React.useState(false); + const [searchTerm, setSearchTerm] = React.useState(""); + const [selectedValues, setSelectedValues] = React.useState(currentFilter); + const [selectAll, setSelectAll] = React.useState(false); + + // Sincronizar selectedValues com currentFilter quando ele mudar + React.useEffect(() => { + setSelectedValues(currentFilter); + }, [currentFilter]); + + // Obter valores únicos da coluna baseado nos dados filtrados + const uniqueValues = React.useMemo(() => { + const values = filteredData + .map((row) => { + const value = row[column.field]; + if (value === null || value === undefined) return ""; + return String(value); + }) + .filter((value, index, self) => self.indexOf(value) === index && value !== "") + .sort(); + + return values; + }, [filteredData, column.field]); + + // Filtrar valores baseado na busca + const filteredValues = React.useMemo(() => { + if (!searchTerm) return uniqueValues; + return uniqueValues.filter((value) => + value.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [uniqueValues, searchTerm]); + + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedValues(filteredValues); + setSelectAll(true); + } else { + setSelectedValues([]); + setSelectAll(false); + } + }; + + const handleValueToggle = (value: string, checked: boolean) => { + let newValues: string[]; + if (checked) { + newValues = [...selectedValues, value]; + } else { + newValues = selectedValues.filter((v) => v !== value); + } + setSelectedValues(newValues); + setSelectAll(newValues.length === filteredValues.length && filteredValues.length > 0); + }; + + const handleApply = () => { + onFilterChange(column.field, selectedValues); + setIsOpen(false); + }; + + const handleClear = () => { + setSelectedValues([]); + setSelectAll(false); + onFilterChange(column.field, []); + setIsOpen(false); + }; + + const handleSort = (direction: 'asc' | 'desc') => { + onSortChange(column.field, direction); + setIsOpen(false); + }; + + return ( + + + + + + + + Filtrar por "{column.headerName}" + + + +
+ {/* Opções de ordenação */} +
+
Ordenar
+
+ + +
+
+ +
+ +
+ + {/* Barra de pesquisa */} +
+ + setSearchTerm(e.target.value)} + className="pl-8 h-8 text-sm" + /> +
+ + {/* Lista de valores com checkboxes */} +
+
+
+ + +
+ + {filteredValues.map((value) => ( +
+ handleValueToggle(value, checked)} + /> + +
+ ))} +
+
+ + {/* Botões de ação */} + + + + +
+
+
+ ); +}; + +export default function AnaliticoComponent({ filtros }: AnaliticoProps) { + const [data, setData] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [globalFilter, setGlobalFilter] = React.useState(""); + const [open, setOpen] = React.useState(false); + const [drawerOpen, setDrawerOpen] = React.useState(false); + const [columnFilters, setColumnFilters] = React.useState>({}); + const [columnSorts, setColumnSorts] = React.useState>({}); + const [conditions, setConditions] = React.useState([ + { column: "", operator: "contains", value: "" }, + ]); + + // Estados para o card de agregação customizado (simplificado) + const [aggregationCardRef, setAggregationCardRef] = React.useState(null); + + // Estado para armazenar filtros externos (vindos do teste.tsx) + const [filtrosExternos, setFiltrosExternos] = React.useState(filtros); + + // Funções para gerenciar filtros customizados + const handleColumnFilterChange = React.useCallback((field: string, values: string[]) => { + setColumnFilters(prev => ({ + ...prev, + [field]: values + })); + }, []); + + const handleColumnSortChange = React.useCallback((field: string, direction: 'asc' | 'desc' | null) => { + setColumnSorts(prev => ({ + ...prev, + [field]: direction + })); + }, []); + + // Função para contar filtros aplicados (apenas filtros internos do modal customizado) + const getFilterCount = React.useCallback(() => { + let count = 0; + + // Contar filtros de coluna (filtros do modal customizado) + count += Object.keys(columnFilters).length; + + // Contar filtro global (se aplicável) + if (globalFilter && globalFilter.trim() !== "") { + count += 1; + } + + return count; + }, [columnFilters, globalFilter]); + + // Função para limpar todos os filtros internos (mantém filtros externos) + const clearAllFilters = React.useCallback(() => { + setColumnFilters({}); + setColumnSorts({}); + setGlobalFilter(""); + }, []); + + // Atualizar filtros externos quando os props mudarem + React.useEffect(() => { + console.log('🔄 Analítico - useEffect dos filtros chamado'); + console.log('📋 Filtros recebidos via props:', filtros); + setFiltrosExternos(filtros); + }, [filtros]); + + const fetchData = React.useCallback(async () => { + console.log('🔄 Analítico - fetchData chamado'); + console.log('📋 Filtros externos recebidos:', filtrosExternos); + + if (!filtrosExternos.dataInicio || !filtrosExternos.dataFim) { + console.log('⚠️ Sem dataInicio ou dataFim, limpando dados'); + setData([]); + return; + } + + setLoading(true); + try { + const params = new URLSearchParams(); + + if (filtrosExternos.dataInicio) { + params.append('dataInicio', filtrosExternos.dataInicio); + } + if (filtrosExternos.dataFim) { + params.append('dataFim', filtrosExternos.dataFim); + } + if (filtrosExternos.centroCusto) { + params.append('centroCusto', filtrosExternos.centroCusto); + console.log('🏢 Analítico - Adicionando parâmetro centroCusto:', filtrosExternos.centroCusto); + } else { + console.log('⚠️ Analítico - centroCusto não está presente nos filtros externos'); + } + if (filtrosExternos.codigoGrupo) { + params.append('codigoGrupo', filtrosExternos.codigoGrupo); + } + if (filtrosExternos.codigoSubgrupo) { + params.append('codigoSubgrupo', filtrosExternos.codigoSubgrupo); + } + if (filtrosExternos.codigoConta) { + params.append('codigoConta', filtrosExternos.codigoConta); + } + if (filtrosExternos.excluirCentroCusto) { + params.append('excluirCentroCusto', filtrosExternos.excluirCentroCusto); + } + if (filtrosExternos.excluirCodigoConta) { + params.append('excluirCodigoConta', filtrosExternos.excluirCodigoConta); + } + if (filtrosExternos.codigosCentrosCustoSelecionados) { + params.append('codigosCentrosCustoSelecionados', filtrosExternos.codigosCentrosCustoSelecionados); + } + if (filtrosExternos.codigosContasSelecionadas) { + params.append('codigosContasSelecionadas', filtrosExternos.codigosContasSelecionadas); + } + + const url = `/api/analitico-entidade-oracle?${params.toString()}`; + console.log('🌐 Fazendo requisição para:', url); + console.log('📋 Parâmetros enviados:', { + dataInicio: filtrosExternos.dataInicio, + dataFim: filtrosExternos.dataFim, + centroCusto: filtrosExternos.centroCusto, + codigoGrupo: filtrosExternos.codigoGrupo, + codigoSubgrupo: filtrosExternos.codigoSubgrupo, + codigoConta: filtrosExternos.codigoConta, + codigosCentrosCustoSelecionados: filtrosExternos.codigosCentrosCustoSelecionados, + codigosContasSelecionadas: filtrosExternos.codigosContasSelecionadas + }); + + const response = await fetch(url); + if (response.ok) { + const result = await response.json(); + console.log('✅ Resposta da API recebida:', result.length, 'registros'); + console.log('📝 Primeiros 2 registros:', result.slice(0, 2)); + console.log('🔍 Verificando campos específicos:', { + data_lancamento: result[0]?.data_lancamento, + data_compensacao: result[0]?.data_compensacao, + data_vencimento: result[0]?.data_vencimento, + data_caixa: result[0]?.data_caixa, + data_pagto: result[0]?.data_pagto, + entidade: result[0]?.entidade, + tipo_parceiro: result[0]?.tipo_parceiro, + centro_custo: result[0]?.centro_custo, + valor: result[0]?.valor, + tipo_valor: typeof result[0]?.valor + }); + setData(result as AnaliticoItem[]); + } else { + console.error("❌ Erro ao buscar dados:", await response.text()); + } + } catch (error) { + console.error("❌ Erro ao buscar dados:", error); + } finally { + setLoading(false); + } + }, [filtrosExternos]); + + React.useEffect(() => { + fetchData(); + }, [fetchData]); + + // Filtrar dados baseado nos filtros de coluna + const filteredData = React.useMemo(() => { + if (!data || data.length === 0) return data; + + return data.filter((row) => { + return Object.entries(columnFilters).every(([field, filterValues]) => { + if (!filterValues || filterValues.length === 0) return true; + + const cellValue = (row as any)[field]; + const stringValue = cellValue === null || cellValue === undefined ? "" : String(cellValue); + + return filterValues.includes(stringValue); + }); + }).map((row, index) => ({ + ...row, + id: `filtered-${row.id || row.recnum || index}` // Garantir ID único e estável + })); + }, [data, columnFilters]); + + // Função para renderizar header com filtro Excel + const renderHeaderWithFilter = React.useCallback((column: GridColDef) => { + return (params: any) => ( +
+ {column.headerName} +
+ +
+
+ ); + }, [data, filteredData, columnFilters, columnSorts, handleColumnFilterChange, handleColumnSortChange]); + + // Definir colunas do DataGridPro na ordem solicitada + const columns = React.useMemo(() => { + const dateCellRenderer = (params: any) => { + if (!params.value) return "-"; + try { + return new Date(params.value).toLocaleDateString("pt-BR"); + } catch (error) { + return params.value; + } + }; + + const currencyCellRenderer = (params: any, showZero: boolean = false) => { + const value = params.value; + if (value === null || value === undefined || value === "") return "-"; + if (!showZero && value === 0) return "-"; + const numValue = typeof value === "string" ? parseFloat(value) : Number(value); + if (isNaN(numValue)) return "-"; + const formatted = new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", + }).format(numValue); + return ( + + {formatted} + + ); + }; + + const baseColumns = [ + { + field: "codigo_fornecedor", + headerName: "Cod. Fornec", + width: 100, + sortable: true, + resizable: true, + }, + { + field: "nome_fornecedor", + headerName: "Fornecedor", + width: 200, + sortable: true, + resizable: true, + }, + { + field: "codigo_centrocusto", + headerName: "Cod.CC", + width: 90, + sortable: true, + resizable: true, + }, + { + field: "centro_custo", + headerName: "Centro Custo", + width: 180, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + { + field: "codigo_conta", + headerName: "Cod.Conta", + width: 100, + sortable: true, + resizable: true, + }, + { + field: "conta", + headerName: "Conta", + width: 200, + sortable: true, + resizable: true, + }, + { + field: "valor", + headerName: "VI.Realizado", + type: "number" as const, + width: 120, + sortable: true, + resizable: true, + renderCell: (params: any) => currencyCellRenderer(params, true), + }, + { + field: "valor_previsto", + headerName: "VI.Pr", + type: "number" as const, + width: 85, + sortable: true, + resizable: true, + renderCell: (params: any) => currencyCellRenderer(params, false), + }, + { + field: "valor_confirmado", + headerName: "VI.Confirmado", + type: "number" as const, + width: 125, + sortable: true, + resizable: true, + renderCell: (params: any) => currencyCellRenderer(params, false), + }, + { + field: "historico", + headerName: "Histórico", + width: 250, + sortable: true, + resizable: true, + }, + { + field: "numero_lancamento", + headerName: "ID", + width: 100, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + { + field: "data_lancamento", + headerName: "Dt Lanc", + width: 95, + sortable: true, + resizable: true, + renderCell: dateCellRenderer, + }, + { + field: "data_compensacao", + headerName: "Dt Comp", + width: 95, + sortable: true, + resizable: true, + renderCell: dateCellRenderer, + }, + { + field: "data_vencimento", + headerName: "Dt Venc", + width: 95, + sortable: true, + resizable: true, + renderCell: dateCellRenderer, + }, + { + field: "data_caixa", + headerName: "Dt Caixa", + width: 95, + sortable: true, + resizable: true, + renderCell: dateCellRenderer, + }, + { + field: "data_pagto", + headerName: "Dt Pagto", + width: 95, + sortable: true, + resizable: true, + renderCell: dateCellRenderer, + }, + { + field: "entidade", + headerName: "Entidade", + width: 90, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + { + field: "tipo_parceiro", + headerName: "Tipo Parc", + width: 95, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + { + field: "valor_pago", + headerName: "VI.Pago", + type: "number" as const, + width: 100, + sortable: true, + resizable: true, + renderCell: (params: any) => currencyCellRenderer(params, false), + }, + { + field: "historico2", + headerName: "Histórico 2", + width: 250, + sortable: true, + resizable: true, + }, + ]; + + // Adicionar renderHeader com filtro Excel para todas as colunas + return baseColumns.map((col) => ({ + ...col, + renderHeader: renderHeaderWithFilter(col), + })); + }, [renderHeaderWithFilter]); + + // Ordenar dados baseado na ordenação de coluna + const sortedAndFilteredData = React.useMemo(() => { + if (!filteredData || filteredData.length === 0) return filteredData; + + const sortField = Object.keys(columnSorts).find(field => columnSorts[field] !== null); + if (!sortField || !columnSorts[sortField]) return filteredData; + + return [...filteredData].sort((a, b) => { + const aValue = (a as any)[sortField]; + const bValue = (b as any)[sortField]; + + // Converter para string para comparação + const aString = aValue === null || aValue === undefined ? "" : String(aValue); + const bString = bValue === null || bValue === undefined ? "" : String(bValue); + + if (columnSorts[sortField] === 'asc') { + return aString.localeCompare(bString); + } else { + return bString.localeCompare(aString); + } + }); + }, [filteredData, columnSorts]); + + // Calcular valor total dos dados filtrados + const valorTotal = React.useMemo(() => { + return sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor) || 0), 0); + }, [sortedAndFilteredData]); + + // Calcular totais das colunas de valor para o card de agregação + const columnTotals = React.useMemo(() => { + return { + valor: sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor) || 0), 0), + valor_previsto: sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor_previsto) || 0), 0), + valor_confirmado: sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor_confirmado) || 0), 0), + valor_pago: sortedAndFilteredData.reduce((sum, item) => sum + (Number(item.valor_pago) || 0), 0), + }; + }, [sortedAndFilteredData]); + + // Limpar filtros de colunas que não têm mais valores disponíveis + React.useEffect(() => { + const updatedFilters = { ...columnFilters }; + let hasChanges = false; + + Object.keys(columnFilters).forEach(field => { + const currentFilterValues = columnFilters[field] || []; + if (currentFilterValues.length === 0) return; + + // Obter valores únicos disponíveis para esta coluna nos dados filtrados + const availableValues = filteredData + .map(row => { + const value = (row as any)[field]; + return value === null || value === undefined ? "" : String(value); + }) + .filter((value, index, self) => self.indexOf(value) === index && value !== ""); + + // Filtrar apenas os valores que ainda estão disponíveis + const validFilterValues = currentFilterValues.filter(value => + availableValues.includes(value) + ); + + if (validFilterValues.length !== currentFilterValues.length) { + if (validFilterValues.length === 0) { + delete updatedFilters[field]; + } else { + updatedFilters[field] = validFilterValues; + } + hasChanges = true; + } + }); + + if (hasChanges) { + setColumnFilters(updatedFilters); + } + }, [filteredData, columnFilters]); + + // Exportação XLSX + const exportToExcel = () => { + if (sortedAndFilteredData.length === 0) return; + + const exportData = sortedAndFilteredData.map((item) => ({ + "DTLANC": item.data_lancamento + ? new Date(item.data_lancamento).toLocaleDateString("pt-BR") + : "-", + "DTCOMPENSACAO": item.data_compensacao + ? new Date(item.data_compensacao).toLocaleDateString("pt-BR") + : "-", + "DTVENC": item.data_vencimento + ? new Date(item.data_vencimento).toLocaleDateString("pt-BR") + : "-", + "DTCAIXA": item.data_caixa + ? new Date(item.data_caixa).toLocaleDateString("pt-BR") + : "-", + "DTPAGTO": item.data_pagto + ? new Date(item.data_pagto).toLocaleDateString("pt-BR") + : "-", + "ENTIDADE": item.entidade || "-", + "TIPOPARCEIRO": item.tipo_parceiro || "-", + "CODFORNEC": item.codigo_fornecedor || "-", + "FORNECEDOR": item.nome_fornecedor || "-", + "CODIGOCENTROCUSTO": item.codigo_centrocusto || "-", + "CENTROCUSTO": item.centro_custo || "-", + "CODCONTA": item.codigo_conta || "-", + "CONTA": item.conta || "-", + "VLREALIZADO": typeof item.valor === "string" ? parseFloat(item.valor) : (item.valor || 0), + "VLPREVISTO": typeof item.valor_previsto === "string" ? parseFloat(item.valor_previsto) : (item.valor_previsto || 0), + "VLCONFIRMADO": typeof item.valor_confirmado === "string" ? parseFloat(item.valor_confirmado) : (item.valor_confirmado || 0), + "VLPAGO": typeof item.valor_pago === "string" ? parseFloat(item.valor_pago) : (item.valor_pago || 0), + "HISTORICO": item.historico || "-", + "HISTORICO2": item.historico2 || "-", + "NUMLANC": item.numero_lancamento || "-", + })); + + const wb = XLSX.utils.book_new(); + const ws = XLSX.utils.json_to_sheet(exportData); + + const resumoData = [ + { Métrica: "Total de Registros", Valor: sortedAndFilteredData.length }, + { Métrica: "Valor Total", Valor: valorTotal }, + { Métrica: "Filtros Aplicados", Valor: Object.keys(columnFilters).length > 0 ? "Sim" : "Não" }, + ]; + const wsResumo = XLSX.utils.json_to_sheet(resumoData); + + XLSX.utils.book_append_sheet(wb, ws, "Dados Analíticos"); + XLSX.utils.book_append_sheet(wb, wsResumo, "Resumo"); + + const now = new Date(); + const timestamp = now.toISOString().slice(0, 19).replace(/:/g, "-"); + const fileName = `analitico_${timestamp}.xlsx`; + + XLSX.writeFile(wb, fileName); + }; + + // Aplicar filtros avançados + const applyFilters = () => { + // Implementar lógica de filtros avançados se necessário + setOpen(false); + }; + + const clearFilters = () => { + setConditions([{ column: "", operator: "contains", value: "" }]); + setGlobalFilter(""); + }; + + // Função para renderizar o conteúdo principal do componente (reutilizável) + const renderAnaliticoContent = (isMaximized: boolean = false) => { + return ( + <> + {/* Filtros Externos Ativos - Apenas quando maximizado */} + {isMaximized && (filtrosExternos.dataInicio || filtrosExternos.centroCusto || filtrosExternos.codigoGrupo || filtrosExternos.codigoSubgrupo || filtrosExternos.codigoConta) && ( +
+
+ Filtros aplicados pela tabela DRE Gerencial: +
+ {filtrosExternos.dataInicio && filtrosExternos.dataFim && ( + + Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim} + + )} + {filtrosExternos.centroCusto && ( + + Centro: {filtrosExternos.centroCusto} + + )} + {filtrosExternos.codigoGrupo && ( + + Grupo: {filtrosExternos.codigoGrupo} + + )} + {filtrosExternos.codigoSubgrupo && ( + + Subgrupo: {filtrosExternos.codigoSubgrupo} + + )} + {filtrosExternos.codigoConta && ( + + Conta: {filtrosExternos.codigoConta} + + )} +
+
+ )} + + {/* Controls - Apenas quando maximizado */} + {isMaximized && ( +
+ {data.length > 0 && ( + + )} + +
+ )} + + {/* DataGridPro */} + + +
+
+ Total de Registros:{" "} + {sortedAndFilteredData.length} +
+
+ Valor Total:{" "} + + {new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", + }).format(valorTotal)} + +
+
+ +
+ row.id || `row-${row.recnum || Math.random()}`} + sx={{ + height: "100%", + width: "100%", + "& .MuiDataGrid-root": { + border: "none", + }, + "& .MuiDataGrid-columnHeaders": { + backgroundColor: "#f9fafb", + borderBottom: "1px solid #e5e7eb", + }, + "& .MuiDataGrid-columnHeader": { + backgroundColor: "#f9fafb !important", + fontWeight: 600, + fontSize: "0.875rem", + }, + "& .MuiDataGrid-cell": { + borderBottom: "1px solid #f0f0f0", + fontSize: "0.875rem", + }, + "& .MuiDataGrid-virtualScroller": { + scrollbarWidth: "thin", + "&::-webkit-scrollbar": { + width: "8px", + height: "8px", + }, + "&::-webkit-scrollbar-track": { + background: "#f1f1f1", + }, + "&::-webkit-scrollbar-thumb": { + background: "#888", + borderRadius: "4px", + }, + "&::-webkit-scrollbar-thumb:hover": { + background: "#555", + }, + }, + "& .MuiDataGrid-toolbarContainer": { + backgroundColor: "#f8fafc", + borderBottom: "1px solid #e5e7eb", + padding: "8px 16px", + }, + "& .MuiDataGrid-columnHeaderMenuContainer": { + display: "none !important", + }, + "& .MuiDataGrid-columnHeaderMenuButton": { + display: "none !important", + }, + "& .MuiDataGrid-columnHeaderSortIcon": { + display: "none !important", + }, + "& .MuiDataGrid-footerContainer": { + display: "none !important", + }, + "& .MuiDataGrid-columnHeaderTitleContainer": { + width: "100%", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }, + }} + /> + + {/* Card de Agregação Customizado */} + {sortedAndFilteredData.length > 0 && ( +
+
+
+
+
+ Vl.Realizado: + + {new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", + }).format(columnTotals.valor)} + +
+ +
+ Vl.Previsto: + + {new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", + }).format(columnTotals.valor_previsto)} + +
+ +
+ Vl.Confirmado: + + {new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", + }).format(columnTotals.valor_confirmado)} + +
+ +
+ Vl.Pago: + + {new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", + }).format(columnTotals.valor_pago)} + +
+
+ +
+ Total de Registros: {sortedAndFilteredData.length} +
+
+
+
+ )} +
+
+
+ + ); + }; + + return ( +
+ {/* Header Section */} +
+
+
+

+ Análise Analítica{filtros.linhaSelecionada ? ` - ${filtros.linhaSelecionada}` : ""} +

+

+ Relatório detalhado de transações +

+
+ + {/* Filtros Externos Ativos - Centralizado */} + {(filtrosExternos.dataInicio || filtrosExternos.centroCusto || filtrosExternos.codigoGrupo || filtrosExternos.codigoSubgrupo || filtrosExternos.codigoConta) && ( +
+
+ Filtros aplicados pela tabela DRE Gerencial: +
+ {filtrosExternos.dataInicio && filtrosExternos.dataFim && ( + + Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim} + + )} + {filtrosExternos.centroCusto && ( + + Centro: {filtrosExternos.centroCusto} + + )} + {filtrosExternos.codigoGrupo && ( + + Grupo: {filtrosExternos.codigoGrupo} + + )} + {filtrosExternos.codigoSubgrupo && ( + + Subgrupo: {filtrosExternos.codigoSubgrupo} + + )} + {filtrosExternos.codigoConta && ( + + Conta: {filtrosExternos.codigoConta} + + )} +
+
+ )} + + {/* Controls */} +
+ {/* ) => + setGlobalFilter(e.target.value) + } + className="w-64 bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500" + /> + */} + {globalFilter && ( + + )} +
+ {data.length > 0 && ( + + )} + + + + + + + +
+
+ + Análise Analítica{filtros.linhaSelecionada ? ` - ${filtros.linhaSelecionada}` : ""} + + + Relatório detalhado de transações - Versão Maximizada + +
+ + + +
+
+
+ {renderAnaliticoContent(true)} +
+
+
+
+
+
+ +
+ + {/* Conteúdo Principal - Versão Normal */} + {renderAnaliticoContent(false)} + + {/* Advanced Filters Dialog */} + + + + + Filtros Avançados + +

+ Estes filtros são aplicados sobre os dados já filtrados pela tabela DRE Gerencial. +

+
+ +
+ {conditions.map((cond, idx) => ( +
+
+ + +
+ +
+ + +
+ + {!(cond.operator === "empty" || cond.operator === "notEmpty") && ( +
+ + ) => { + const next = [...conditions]; + next[idx].value = e.target.value; + setConditions(next); + }} + placeholder="Digite o valor" + className="w-full bg-white border-gray-300" + /> +
+ )} + + {conditions.length > 1 && ( +
+ +
+ )} +
+ ))} + +
+ +
+
+ + + + + +
+
+
+ ); +} diff --git a/src/app/dre-entidade/page.tsx b/src/app/dre-entidade/page.tsx new file mode 100644 index 0000000..cdeceb0 --- /dev/null +++ b/src/app/dre-entidade/page.tsx @@ -0,0 +1,10 @@ +import Teste from './teste'; + +export default function DreEntidadePage() { + return ( +
+ +
+ ); +} + diff --git a/src/app/dre-entidade/teste.tsx b/src/app/dre-entidade/teste.tsx new file mode 100644 index 0000000..ac354e9 --- /dev/null +++ b/src/app/dre-entidade/teste.tsx @@ -0,0 +1,2907 @@ +"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] + // IMPORTANTE: Quando clicar em um nível abaixo, manter os filtros dos níveis anteriores + let centroCustoFiltro = ""; + let codigoContaFiltro = ""; + let codigoGrupoFiltro = ""; + let entidadeFiltro = ""; + let diretoIndiretoFiltro = ""; + + // Sempre filtrar por entidade se disponível (mantém em todos os níveis) + // Quando clicar em centro_custo ou conta, manter a entidade do nível anterior + entidadeFiltro = row.entidade || ""; + + // Filtrar por direto/indireto se disponível + // Quando clicar em uma célula de valor de uma linha "direto_indireto" (DIRETO, INDIRETO ou SEM CC), + // adicionar o SUBGRUPO como filtro adicional + // Quando clicar em centro_custo ou conta, manter o direto_indireto do nível anterior + diretoIndiretoFiltro = row.direto_indireto || ""; + + // Verificar se a linha é do tipo "direto_indireto" e se a entidade pai está expandida (drill aberto) + // Determinar codigoSubgrupo baseado na hierarquia + // Quando clicar em qualquer nível abaixo de direto_indireto, deve manter o filtro do nível anterior + let codigoSubgrupoFiltro = ""; + if (row.type === "direto_indireto" && row.direto_indireto) { + // Verificar se a entidade pai está expandida + const entidadePaiExpandida = expandedEntidades.has(row.entidade || ""); + if (entidadePaiExpandida) { + // Usar o direto_indireto (DIRETO, INDIRETO ou SEM CC) como filtro de subgrupo + codigoSubgrupoFiltro = row.direto_indireto; + console.log('📊 Drill aberto - Adicionando filtro de subgrupo:', codigoSubgrupoFiltro); + } + } else if (row.type === "centro_custo" && row.direto_indireto) { + // Quando clicar em centro de custo, manter o filtro do nível anterior (direto_indireto) + codigoSubgrupoFiltro = row.direto_indireto; + console.log('📊 Mantendo filtro de subgrupo do nível anterior (centro_custo):', codigoSubgrupoFiltro); + } else if (row.type === "conta" && row.direto_indireto) { + // Quando clicar em conta, manter o filtro do nível anterior (direto_indireto) + codigoSubgrupoFiltro = row.direto_indireto; + console.log('📊 Mantendo filtro de subgrupo do nível anterior (conta):', codigoSubgrupoFiltro); + } + + // Quando clicar em uma célula de valor de um centro de custo, sempre adicionar o CODIGOCENTROCUSTO ao filtro + // Sempre adicionar o CODIGOCENTROCUSTO quando clicar em uma célula de valor de centro de custo + let codigoCentroCustoFiltroAdicional = ""; + if (row.type === "centro_custo") { + // Sempre adicionar o CODIGOCENTROCUSTO quando clicar em uma célula de valor de centro de custo + // Tentar obter o código do centro de custo de diferentes formas + codigoCentroCustoFiltroAdicional = row.codigo_centro_custo || ""; + + // Se não tiver codigo_centro_custo na row, tentar buscar nos dados originais + if (!codigoCentroCustoFiltroAdicional && row.centro_custo) { + const itemCentro = data.find((item: DREItem) => + item.centro_custo === row.centro_custo && + item.entidades === row.entidade && + item.subgrupo === row.direto_indireto + ); + if (itemCentro?.codigo_centro_custo) { + codigoCentroCustoFiltroAdicional = itemCentro.codigo_centro_custo; + } + } + + console.log('📊 Adicionando filtro de centro de custo (sempre):', { + codigoCentroCustoFiltroAdicional, + tipoLinha: row.type, + codigo_centro_custo: row.codigo_centro_custo, + centro_custo: row.centro_custo, + entidade: row.entidade, + direto_indireto: 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 + // IMPORTANTE: Quando clicar em um nível abaixo, manter os filtros dos níveis anteriores + if (row.type === "centro_custo") { + // Sempre usar o codigo_centro_custo quando clicar em uma célula de valor + // Priorizar o filtro adicional se existir, senão usar o codigo_centro_custo da linha + if (codigoCentroCustoFiltroAdicional) { + centroCustoFiltro = codigoCentroCustoFiltroAdicional; + } else if (row.codigo_centro_custo) { + centroCustoFiltro = row.codigo_centro_custo; + } else { + // Se não tiver na row, buscar nos dados originais + const itemCentro = data.find((item: DREItem) => + item.centro_custo === row.centro_custo && + item.entidades === row.entidade && + item.subgrupo === row.direto_indireto + ); + centroCustoFiltro = itemCentro?.codigo_centro_custo || ""; + } + // Manter filtros dos níveis anteriores (entidade e subgrupo já estão definidos acima) + console.log('📊 Adicionando filtro de centro de custo (mantendo níveis anteriores):', { + centroCustoFiltro, + codigoCentroCustoFiltroAdicional, + codigo_centro_custo: row.codigo_centro_custo, + tipoLinha: row.type, + entidadeFiltro: entidadeFiltro, // Mantido do nível anterior + codigoSubgrupoFiltro: codigoSubgrupoFiltro, // Mantido do nível anterior + rowData: { + centro_custo: row.centro_custo, + entidade: row.entidade, + direto_indireto: row.direto_indireto + } + }); + } else if (row.type === "conta") { + // Para conta, adicionar o centro de custo E manter os filtros dos níveis anteriores + centroCustoFiltro = row.codigo_centro_custo || ""; + // Manter filtros dos níveis anteriores (entidade e subgrupo já estão definidos acima) + console.log('📊 Adicionando filtro de conta (mantendo níveis anteriores):', { + codigoContaFiltro: row.codigo_conta?.toString() || "", + centroCustoFiltro, + entidadeFiltro: entidadeFiltro, // Mantido do nível anterior + codigoSubgrupoFiltro: codigoSubgrupoFiltro, // Mantido do nível anterior + tipoLinha: row.type + }); + } + + // 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(','); + + // Garantir que centroCustoFiltro está definido quando for tipo centro_custo + if (row.type === "centro_custo" && !centroCustoFiltro && row.codigo_centro_custo) { + centroCustoFiltro = row.codigo_centro_custo; + console.log('⚠️ Centro de custo filtro estava vazio, definindo agora:', centroCustoFiltro); + } + + // Garantir que codigoSubgrupo seja mantido quando clicar em níveis abaixo + // Se codigoSubgrupoFiltro estiver vazio mas diretoIndiretoFiltro existir, usar diretoIndiretoFiltro + const codigoSubgrupoFinal = codigoSubgrupoFiltro || diretoIndiretoFiltro || ""; + + const novosFiltros = { + dataInicio: dataInicioFiltro, + dataFim: dataFimFiltro, + centroCusto: centroCustoFiltro, + codigoGrupo: codigoGrupoFiltro, // CODGRUPO da entidade/linha selecionada + codigoSubgrupo: codigoSubgrupoFinal, // SUBGRUPO mantido dos níveis anteriores quando clicar em centro_custo ou conta + codigoConta: codigoContaFiltro, + linhaSelecionada: row.entidade || row.direto_indireto || row.centro_custo || row.conta || "", + excluirCentroCusto, + excluirCodigoConta, + codigosCentrosCustoSelecionados, + codigosContasSelecionadas, + }; + + console.log('🔍 Verificação de filtros mantidos:', { + tipoLinha: row.type, + codigoSubgrupoFiltro, + diretoIndiretoFiltro, + codigoSubgrupoFinal, + entidadeFiltro, + centroCustoFiltro, + codigoGrupoFiltro + }); + + console.log('🎯 Novos filtros para analítico:', novosFiltros); + console.log('🔍 Verificação específica de centro de custo:', { + centroCustoFiltro, + tipoLinha: row.type, + codigo_centro_custo: row.codigo_centro_custo, + codigoCentroCustoFiltroAdicional, + centro_custo: row.centro_custo, + novosFiltrosCentroCusto: novosFiltros.centroCusto + }); + 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 */} + +
+ ); +}