diff --git a/src/app/api/analitico-filial-oracle/route.ts b/src/app/api/analitico-filial-oracle/route.ts new file mode 100644 index 0000000..6dae070 --- /dev/null +++ b/src/app/api/analitico-filial-oracle/route.ts @@ -0,0 +1,293 @@ +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 (Filial)...'); + 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'); + const codFilial = searchParams.get('codFilial'); + + // 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, + codFilial, + 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 VB_DRE_FILIAL_DESPESA_ANALITICO + // Fazer JOIN com FILIAL_CC quando houver filtro por filial + let sql = ''; + const params: any[] = []; + let paramIndex = 1; + + // Verificar se há múltiplas filiais (separadas por vírgula) + const filiaisArray = codFilial ? codFilial.split(',').map(f => f.trim()).filter(f => f !== '') : []; + const temJoin = filiaisArray.length > 0; + const prefixo = temJoin ? 'VB.' : ''; // Usar prefixo apenas quando há JOIN + + // Se houver filtro por filial, fazer JOIN com FILIAL_CC + if (temJoin) { + sql = `SELECT FC.CODFILIAL, VB.* +FROM VB_DRE_FILIAL_DESPESA_ANALITICO VB +INNER JOIN FILIAL_CC FC ON FC.CC = VB.CODIGOCENTROCUSTO +WHERE 1=1`; + } else { + sql = `SELECT * FROM VB_DRE_FILIAL_DESPESA_ANALITICO WHERE 1=1`; + } + + // Filtro por filial (usando JOIN com FILIAL_CC) + // Se houver múltiplas filiais, usar IN; caso contrário, usar = + if (temJoin) { + if (filiaisArray.length === 1) { + // Uma única filial - usar = + sql += ` AND FC.CODFILIAL = :${paramIndex}`; + params.push(filiaisArray[0]); + paramIndex++; + console.log('🏢 Adicionando filtro de filial única (CODFILIAL):', filiaisArray[0]); + } else { + // Múltiplas filiais - usar IN + const placeholders = filiaisArray.map(() => `:${paramIndex++}`).join(','); + sql += ` AND FC.CODFILIAL IN (${placeholders})`; + params.push(...filiaisArray); + console.log('🏢 Adicionando filtro de múltiplas filiais (CODFILIAL IN):', filiaisArray); + } + } + + // Filtro por período (usando ANOMESCOMP) + if (dataInicio && dataFim) { + sql += ` AND ${prefixo}ANOMESCOMP >= :${paramIndex} AND ${prefixo}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 ${prefixo}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 ${prefixo}SUBGRUPO = :${paramIndex}`; + params.push(codigoSubgrupo); + paramIndex++; + console.log('📊 Adicionando filtro de subgrupo:', codigoSubgrupo); + } + + // Filtro por código da conta + if (codigoConta) { + sql += ` AND ${prefixo}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 ${prefixo}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 ${prefixo}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 ${prefixo}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 ${prefixo}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 ${prefixo}CODCONTA IN (${placeholders})`; + params.push(...codigosArray); + console.log('💰 Filtrando por códigos de contas:', codigosArray); + } + + sql += ` ORDER BY ${prefixo}DTVENC, ${prefixo}CODFORNEC, ${prefixo}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', + temCodFilial: !!codFilial, + codFilial: codFilial || '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 VB_DRE_FILIAL_DESPESA_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: item.CODFILIAL || item.FILIAL || "001", // Usar CODFILIAL do JOIN ou FILIAL da view + 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 (Filial):', 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-filial-oracle/route.ts b/src/app/api/dre-filial-oracle/route.ts new file mode 100644 index 0000000..c4ae29a --- /dev/null +++ b/src/app/api/dre-filial-oracle/route.ts @@ -0,0 +1,102 @@ +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 (Filial) - Views VB_DRE_FILIAL_*...'); + + // Query para buscar dados das views VB_DRE_FILIAL_CMV, VB_DRE_FILIAL_DESPESA e VB_DRE_FILIAL_FATLIQ + const sql = `SELECT * +FROM ( + SELECT 'CMV' AS TIPO, + "DATA", + CODGRUPO, GRUPO, FILIAL, CODCONTA, CONTA, VALOR + FROM SEVEN.VB_DRE_FILIAL_CMV + UNION ALL + SELECT 'DESPESA' AS TIPO, + "DATA", + CODGRUPO, GRUPO, FILIAL, CODCONTA, CONTA, VALOR + FROM SEVEN.VB_DRE_FILIAL_DESPESA + UNION ALL + SELECT 'FATLIQ' AS TIPO, + "DATA", + CODGRUPO, GRUPO, FILIAL, CODCONTA, CONTA, VALOR + FROM SEVEN.VB_DRE_FILIAL_FATLIQ +) X +ORDER BY CODGRUPO, 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 CODGRUPO:', [...new Set(data.map((item: any) => item.CODGRUPO).filter(Boolean))]); + console.log('🔍 Valores únicos de FILIAL:', [...new Set(data.map((item: any) => item.FILIAL).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: item.FILIAL || "001", + data_competencia: dataCompetencia, + data_cai: dataCompetencia, + grupo: item.GRUPO || "", // GRUPO (nome) + codigo_grupo: item.CODGRUPO || "", // CODGRUPO (código) + 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 + tipo: item.TIPO || "", // TIPO (CMV, DESPESA, FATLIQ) + filial: item.FILIAL || "", // FILIAL + }; + }); + + 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/teste.tsx b/src/app/dre-entidade/teste.tsx index 1bc9c2b..6310086 100644 --- a/src/app/dre-entidade/teste.tsx +++ b/src/app/dre-entidade/teste.tsx @@ -2368,7 +2368,7 @@ export default function Teste() { ))} - +
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); + } + 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.codFilial) { + params.append('codFilial', filtrosExternos.codFilial); + } + 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-filial-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, + codFilial: filtrosExternos.codFilial, + excluirCentroCusto: filtrosExternos.excluirCentroCusto, + excluirCodigoConta: filtrosExternos.excluirCodigoConta, + 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)); + 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: "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: "ano_mes_comp", + headerName: "Ano/Mês Comp", + width: 110, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + { + field: "entidade", + headerName: "Entidade", + width: 90, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + { + 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: "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]); + + // 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 - Exporta exatamente as colunas e valores da grid + const exportToExcel = () => { + if (sortedAndFilteredData.length === 0) return; + + // Funções auxiliares para formatar valores exatamente como na grid + const formatDateValue = (value: any): string => { + if (!value) return "-"; + try { + return new Date(value).toLocaleDateString("pt-BR"); + } catch (error) { + return value; + } + }; + + const formatCurrencyValue = (value: any, showZero: boolean = false): string | number => { + if (value === null || value === undefined || value === "") return "-"; + const numValue = typeof value === "string" ? parseFloat(value) : Number(value); + if (isNaN(numValue)) return "-"; + if (!showZero && numValue === 0) return "-"; + // Para Excel, retornar o número formatado como string (mantém o formato de moeda) + return new Intl.NumberFormat("pt-BR", { + style: "currency", + currency: "BRL", + }).format(numValue); + }; + + const formatCellValue = (column: GridColDef, item: any): any => { + const value = item[column.field]; + + // Se a coluna tem renderCell, aplicar a mesma lógica + if (column.renderCell) { + // Para datas + if (column.field.includes("data_")) { + return formatDateValue(value); + } + + // Para valores monetários + if (column.field === "valor") { + return formatCurrencyValue(value, true); + } + if (column.field === "valor_previsto" || column.field === "valor_confirmado" || column.field === "valor_pago") { + return formatCurrencyValue(value, false); + } + + // Para campos que retornam "-" se vazios + if (column.field === "centro_custo" || column.field === "numero_lancamento" || + column.field === "entidade" || column.field === "tipo_parceiro" || + column.field === "ano_mes_comp") { + return value || "-"; + } + } + + // Para datas sem renderCell explícito (mas que são datas) + if (column.field.includes("data_")) { + return formatDateValue(value); + } + + // Valor padrão + return value ?? ""; + }; + + // Criar dados de exportação usando as colunas da grid na ordem exata + const exportData = sortedAndFilteredData.map((item) => { + const row: Record = {}; + + // Iterar sobre as colunas na ordem da grid + columns.forEach((column) => { + const headerName = column.headerName || column.field; + row[headerName] = formatCellValue(column, item); + }); + + return row; + }); + + 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_filial_${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.codigoGrupo || filtrosExternos.codigoSubgrupo || filtrosExternos.codigoConta || filtrosExternos.centroCusto || filtrosExternos.codFilial) && ( +
+
+ Filtros aplicados pela tabela DRE Filial: +
+ {filtrosExternos.dataInicio && filtrosExternos.dataFim && ( + + Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim} + + )} + {filtrosExternos.codigoGrupo && ( + + Grupo: {filtrosExternos.codigoGrupo} + + )} + {filtrosExternos.codigoSubgrupo && ( + + Subgrupo: {filtrosExternos.codigoSubgrupo} + + )} + {filtrosExternos.codigoConta && ( + + Conta: {filtrosExternos.codigoConta} + + )} + {filtrosExternos.codFilial && ( + + Filial: {filtrosExternos.codFilial} + + )} + {filtrosExternos.centroCusto && ( + + Centro Custo: {filtrosExternos.centroCusto} + + )} +
+
+ )} + + {/* 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", + }, + }} + /> +
+
+
+ + ); + }; + + return ( +
+ {/* Header Section */} +
+
+
+

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

+

+ Relatório detalhado de transações +

+
+ + {/* Filtros Externos Ativos - Centralizado */} + {(filtrosExternos.dataInicio || filtrosExternos.codigoGrupo || filtrosExternos.codigoSubgrupo || filtrosExternos.codigoConta || filtrosExternos.centroCusto || filtrosExternos.codFilial) && ( +
+
+ Filtros aplicados pela tabela DRE Filial: +
+ {filtrosExternos.dataInicio && filtrosExternos.dataFim && ( + + Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim} + + )} + {filtrosExternos.codigoGrupo && ( + + Grupo: {filtrosExternos.codigoGrupo} + + )} + {filtrosExternos.codigoSubgrupo && ( + + Subgrupo: {filtrosExternos.codigoSubgrupo} + + )} + {filtrosExternos.codigoConta && ( + + Conta: {filtrosExternos.codigoConta} + + )} + {filtrosExternos.codFilial && ( + + Filial: {filtrosExternos.codFilial} + + )} + {filtrosExternos.centroCusto && ( + + Centro Custo: {filtrosExternos.centroCusto} + + )} +
+
+ )} + + {/* Controls */} +
+
+ {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)} +
+ ); +} + diff --git a/src/app/dre-filial/page.tsx b/src/app/dre-filial/page.tsx new file mode 100644 index 0000000..4d789d4 --- /dev/null +++ b/src/app/dre-filial/page.tsx @@ -0,0 +1,10 @@ +import Teste from './teste'; + +export default function DreFilialPage() { + return ( +
+ +
+ ); +} + diff --git a/src/app/dre-filial/teste.tsx b/src/app/dre-filial/teste.tsx new file mode 100644 index 0000000..c95f67e --- /dev/null +++ b/src/app/dre-filial/teste.tsx @@ -0,0 +1,1596 @@ +"use client"; + +import { LoaderPinwheel, ChevronDown, ChevronRight, Filter, Maximize2, Minimize2, Download } from "lucide-react"; +import React, { useEffect, useState, useCallback, 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; + codigo_grupo: string; + codigo_conta: number; + conta: string; + valor: string; + codgrupo?: string; + tipo?: string; + filial?: string; +} + +interface HierarchicalRow { + type: "grupo" | "conta" | "calculado"; + level: number; + grupo?: string; + codigo_grupo?: string; + conta?: string; + codigo_conta?: number; + total?: number; + isExpanded?: boolean; + valoresPorMes?: Record; + valoresPorMesPorFilial?: Record>; // mes -> filial -> valor + percentuaisPorMes?: Record; + percentuaisPorMesPorFilial?: Record>; // mes -> filial -> percentual + percentualTotal?: number; + isCalculado?: boolean; +} + +// Componente memoizado para linhas da tabela +const TableRow = memo(({ + row, + index, + handleRowClick, + getRowStyle, + getIndentStyle, + renderCellContent, + mesesDisponiveis, + opcoesFiliais, + filiaisSelecionadas, + filtrosAplicados, + formatCurrency, + formatCurrencyWithColor, + getFixedCellBackground +}: { + row: HierarchicalRow; + index: number; + handleRowClick: (row: HierarchicalRow, mes?: string, filial?: string) => void; + getRowStyle: (row: HierarchicalRow) => string; + getIndentStyle: (level: number) => React.CSSProperties; + renderCellContent: (row: HierarchicalRow) => React.ReactNode; + mesesDisponiveis: string[]; + opcoesFiliais: string[]; + filiaisSelecionadas: string[]; + filtrosAplicados: boolean; + formatCurrency: (value: number) => string; + formatCurrencyWithColor: (value: number) => { formatted: string; isNegative: boolean }; + getFixedCellBackground: (row: HierarchicalRow) => string; +}) => { + return ( + + handleRowClick(row)} + > +
+ {renderCellContent(row)} +
+ + + {/* Colunas de valores por mês e por filial - cada filial tem suas próprias colunas */} + {mesesDisponiveis.map((mes) => { + const filiaisParaMes = (filtrosAplicados && filiaisSelecionadas.length > 0) + ? filiaisSelecionadas + : (opcoesFiliais.length > 0 ? opcoesFiliais : ['']); + + return ( + + {/* Colunas de filiais para este mês */} + {filiaisParaMes.map((filial: string) => { + // Só exibir se a filial estiver selecionada ou se não houver filtros aplicados + if (filtrosAplicados && filiaisSelecionadas.length > 0 && !filiaisSelecionadas.includes(filial)) { + return null; + } + return ( + + handleRowClick(row, mes, filial)} + title={ + filial && row.valoresPorMesPorFilial?.[mes]?.[filial] !== undefined + ? formatCurrency(row.valoresPorMesPorFilial[mes][filial]) + : row.valoresPorMes?.[mes] !== undefined + ? formatCurrency(row.valoresPorMes[mes]) + : "-" + } + > + {filial && row.valoresPorMesPorFilial?.[mes]?.[filial] !== undefined && row.valoresPorMesPorFilial[mes][filial] !== 0 ? ( + (() => { + const valor = row.valoresPorMesPorFilial[mes][filial]; + const { formatted, isNegative } = formatCurrencyWithColor(valor); + return ( + + {formatted} + + ); + })() + ) : !filial && row.valoresPorMes?.[mes] !== undefined ? ( + (() => { + const { formatted, isNegative } = formatCurrencyWithColor(row.valoresPorMes[mes]); + return ( + + {formatted} + + ); + })() + ) : ( + - + )} + + handleRowClick(row, mes, filial)} + title={ + filial && row.percentuaisPorMesPorFilial?.[mes]?.[filial] !== undefined + ? `${row.percentuaisPorMesPorFilial[mes][filial].toFixed(1)}%` + : row.percentuaisPorMes?.[mes] !== undefined + ? `${row.percentuaisPorMes[mes].toFixed(1)}%` + : "-" + } + > + {filial && row.percentuaisPorMesPorFilial?.[mes]?.[filial] !== undefined && row.percentuaisPorMesPorFilial[mes][filial] !== 0 ? ( + `${row.percentuaisPorMesPorFilial[mes][filial].toFixed(1)}%` + ) : !filial && row.percentuaisPorMes?.[mes] !== undefined ? ( + `${row.percentuaisPorMes[mes].toFixed(1)}%` + ) : ( + - + )} + + + ); + })} + + {/* Colunas de totalizador para este mês */} + handleRowClick(row, mes)} + title={row.valoresPorMes?.[mes] !== undefined ? formatCurrency(row.valoresPorMes[mes]) : "-"} + > + {row.valoresPorMes?.[mes] !== undefined ? ( + (() => { + const { formatted, isNegative } = formatCurrencyWithColor(row.valoresPorMes[mes]); + return ( + + {formatted} + + ); + })() + ) : ( + - + )} + + handleRowClick(row, mes)} + title={ + row.percentuaisPorMes?.[mes] !== undefined + ? `${row.percentuaisPorMes[mes].toFixed(1)}%` + : "-" + } + > + {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 [expandedGrupos, setExpandedGrupos] = useState>(new Set()); + const [mesesDisponiveis, setMesesDisponiveis] = useState([]); + + // Estados para filtros + const [filtros, setFiltros] = useState({ + periodoDe: "", + periodoAte: "", + filial: "Todas", + }); + + // Estados para multi-seleção + const [filiaisSelecionadas, setFiliaisSelecionadas] = useState([]); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [dadosFiltrados, setDadosFiltrados] = useState([]); + const [filtrosAplicados, setFiltrosAplicados] = useState(false); + + // Estados para opções dos filtros + const [opcoesGrupos, setOpcoesGrupos] = useState([]); + const [opcoesFiliais, setOpcoesFiliais] = useState([]); + + // Estados para filtros de busca nos campos de seleção + const [filtroFilial, setFiltroFilial] = useState(""); + + // Estados para analítico + const [analiticoFiltros, setAnaliticoFiltros] = useState({ + dataInicio: "", + dataFim: "", + centroCusto: "", + codigoGrupo: "", + codigoSubgrupo: "", + codigoConta: "", + codFilial: "", + linhaSelecionada: "", + excluirCentroCusto: "", + excluirCodigoConta: "", + codigosCentrosCustoSelecionados: "", + codigosContasSelecionadas: "", + }); + const [linhaSelecionada, setLinhaSelecionada] = useState(null); + const [isAllExpanded, setIsAllExpanded] = useState(false); + + useEffect(() => { + // Carregar períodos disponíveis da API + carregarPeriodosDisponiveis(); + }, []); + + const carregarPeriodosDisponiveis = async () => { + try { + const response = await fetch("/api/dre-filial-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 filiais únicas + const filiaisUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.filial || item.codfilial).filter(Boolean))].sort() as string[]; + setOpcoesFiliais(filiaisUnicas); + + // Inicializar com todas as filiais selecionadas + setFiliaisSelecionadas(filiaisUnicas); + + // Inicializar filtros de período com o ano corrente + const agora = new Date(); + const anoAtual = agora.getFullYear(); + const mesAtual = String(agora.getMonth() + 1).padStart(2, '0'); + const periodoAtual = `${anoAtual}-${mesAtual}`; + const primeiroMesAno = `${anoAtual}-01`; + + // Verificar se os períodos existem nos dados disponíveis + const periodoDeValido = periodosUnicos.includes(primeiroMesAno) ? primeiroMesAno : (periodosUnicos[0] || primeiroMesAno); + const periodoAteValido = periodosUnicos.includes(periodoAtual) ? periodoAtual : (periodosUnicos[periodosUnicos.length - 1] || periodoAtual); + + setFiltros(prev => ({ + ...prev, + periodoDe: periodoDeValido, + periodoAte: periodoAteValido + })); + + // NÃO inicializar filtros do analítico - só serão definidos após clique em célula + + } catch (error) { + console.error("Erro ao carregar períodos:", error); + } + }; + + const fetchData = async () => { + try { + setLoading(true); + setError(null); + const response = await fetch("/api/dre-filial-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) => { + return item.data_competencia; + }) + ), + ].sort() as string[]; + + setMesesDisponiveis(meses); + } catch (err) { + setError(err instanceof Error ? err.message : "Erro desconhecido"); + } finally { + setLoading(false); + } + }; + + const formatCurrency = React.useCallback((value: string | number) => { + const numValue = typeof value === "string" ? parseFloat(value) : value; + return numValue.toLocaleString("pt-BR", { + style: "currency", + currency: "BRL", + }); + }, []); + + const formatCurrencyWithColor = React.useCallback((value: string | number) => { + const numValue = typeof value === "string" ? parseFloat(value) : value; + const formatted = formatCurrency(value); + const isNegative = numValue < 0; + return { formatted, isNegative }; + }, [formatCurrency]); + + // Função para lidar com clique nas linhas + const handleRowClick = React.useCallback((row: HierarchicalRow, mesSelecionado?: string, filialSelecionada?: string) => { + console.log('🖱️ Clique na linha:', row); + console.log('📅 Mês selecionado:', mesSelecionado); + console.log('🏢 Filial selecionada:', filialSelecionada); + + // Linhas calculadas não devem abrir o componente analítico + if (row.type === "calculado") { + console.log('⚠️ Linha calculada - não abre componente analítico'); + return; + } + + 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 + + // 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 hierarquia [grupo, conta] + let codigoGrupoFiltro = ""; + let codigoContaFiltro = ""; + + if (row.type === "grupo" || row.type === "conta") { + // Buscar o CODGRUPO dos dados originais que correspondem a esta linha + 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; + } + + // Se é um totalizador (mesSelecionado presente mas filialSelecionada não), não filtrar por filial + // Caso contrário, filtrar por filial se especificada + if (filialSelecionada) { + const itemFilial = item.filial || item.codfilial || ""; + if (itemFilial !== filialSelecionada) { + return false; + } + } + + if (row.type === "grupo") { + return item.codigo_grupo === row.codigo_grupo || item.codgrupo === row.codigo_grupo; + } else if (row.type === "conta") { + return (item.codigo_grupo === row.codigo_grupo || item.codgrupo === row.codigo_grupo) && + item.codigo_conta === row.codigo_conta; + } + return false; + }); + + // Pegar o CODGRUPO do primeiro item encontrado + if (itemsCorrespondentes.length > 0) { + const primeiroItem = itemsCorrespondentes[0]; + codigoGrupoFiltro = primeiroItem.codgrupo || primeiroItem.codigo_grupo || ""; + } + } + + // Filtrar por conta se for nível conta + if (row.type === "conta") { + codigoContaFiltro = row.codigo_conta?.toString() || ""; + } + + // Determinar CODFILIAL baseado na filial selecionada + // Se é um totalizador (mesSelecionado presente mas filialSelecionada não), incluir todas as filiais selecionadas + let codFilialFiltro = ""; + if (filialSelecionada) { + // Se a filial selecionada já é um código numérico, usar diretamente + // Caso contrário, buscar o CODFILIAL correspondente ao nome da filial nos dados + if (/^\d+$/.test(filialSelecionada)) { + codFilialFiltro = filialSelecionada; + } else { + // Buscar o CODFILIAL correspondente ao nome da filial nos dados + const itemComFilial = data.find((item: DREItem) => { + const itemFilial = item.filial || item.codfilial || ""; + return itemFilial === filialSelecionada; + }); + if (itemComFilial) { + codFilialFiltro = itemComFilial.codfilial || filialSelecionada; + } else { + // Se não encontrar, tentar usar o próprio valor como código + codFilialFiltro = filialSelecionada; + } + } + } else if (mesSelecionado) { + // Se mesSelecionado está presente mas filialSelecionada não, é um totalizador + // Incluir todas as filiais selecionadas (ou todas disponíveis se nenhuma estiver selecionada) + const filiaisParaTotalizador = filiaisSelecionadas.length > 0 + ? filiaisSelecionadas + : opcoesFiliais; + + // Converter nomes de filiais para códigos CODFILIAL + const codigosFiliais = filiaisParaTotalizador.map(filial => { + if (/^\d+$/.test(filial)) { + return filial; + } else { + const itemComFilial = data.find((item: DREItem) => { + const itemFilial = item.filial || item.codfilial || ""; + return itemFilial === filial; + }); + return itemComFilial?.codfilial || filial; + } + }).filter(Boolean); + + // Passar como string separada por vírgula para a API processar com IN + codFilialFiltro = codigosFiliais.join(','); + console.log('📊 Totalizador: incluindo todas as filiais selecionadas:', codigosFiliais); + } + + const novosFiltros = { + dataInicio: dataInicioFiltro, + dataFim: dataFimFiltro, + centroCusto: "", // Não aplicável na hierarquia filial + codigoGrupo: codigoGrupoFiltro, + codigoSubgrupo: "", // Não aplicável na hierarquia filial + codigoConta: codigoContaFiltro, + codFilial: codFilialFiltro, // Vazio para totalizador = todas as filiais do mês + linhaSelecionada: row.grupo || row.conta || "", + excluirCentroCusto: "", + excluirCodigoConta: "", + codigosCentrosCustoSelecionados: "", + codigosContasSelecionadas: "", + }; + + console.log('🎯 Novos filtros para analítico:', novosFiltros); + console.log('📊 É totalizador?', mesSelecionado && !filialSelecionada ? 'SIM' : 'NÃO'); + setAnaliticoFiltros(novosFiltros); + }, [data, filiaisSelecionadas, opcoesFiliais]); + + const toggleGrupo = useCallback((codigoGrupo: string) => { + setExpandedGrupos(prev => { + const newExpanded = new Set(prev); + if (newExpanded.has(codigoGrupo)) { + newExpanded.delete(codigoGrupo); + } else { + newExpanded.add(codigoGrupo); + } + return newExpanded; + }); + }, []); + + const toggleFilial = (filial: string) => { + setFiliaisSelecionadas(prev => { + if (prev.includes(filial)) { + return prev.filter(f => f !== filial); + } else { + return [...prev, filial]; + } + }); + }; + + const selecionarTodasFiliais = () => { + setFiliaisSelecionadas(opcoesFiliais); + }; + + const limparFiliais = () => { + setFiliaisSelecionadas([]); + }; + + // Função auxiliar para calcular valores por mês + const calcularValoresPorMes = React.useCallback((items: DREItem[]): Record => { + const valoresPorMes: Record = {}; + + mesesDisponiveis.forEach(mes => { + valoresPorMes[mes] = 0; + }); + + items.forEach((item) => { + const anoMes = item.data_competencia; + if (anoMes && valoresPorMes[anoMes] !== undefined) { + valoresPorMes[anoMes] += parseFloat(item.valor); + } + }); + + return valoresPorMes; + }, [mesesDisponiveis]); + + // Função auxiliar para calcular valores por mês e por filial + const calcularValoresPorMesPorFilial = React.useCallback((items: DREItem[]): Record> => { + const valoresPorMesPorFilial: Record> = {}; + + // Usar filiais selecionadas se houver filtros aplicados, senão usar todas as opções disponíveis + const filiaisDisponiveis = (filtrosAplicados && filiaisSelecionadas.length > 0) + ? filiaisSelecionadas + : (opcoesFiliais.length > 0 + ? opcoesFiliais + : [...new Set(items.map(item => item.filial || item.codfilial).filter(Boolean))] as string[]); + + mesesDisponiveis.forEach(mes => { + valoresPorMesPorFilial[mes] = {}; + filiaisDisponiveis.forEach(filial => { + valoresPorMesPorFilial[mes][filial] = 0; + }); + }); + + items.forEach((item) => { + const anoMes = item.data_competencia; + const filial = item.filial || item.codfilial || ""; + if (anoMes && valoresPorMesPorFilial[anoMes] && filial && filiaisDisponiveis.includes(filial)) { + if (!valoresPorMesPorFilial[anoMes][filial]) { + valoresPorMesPorFilial[anoMes][filial] = 0; + } + valoresPorMesPorFilial[anoMes][filial] += parseFloat(item.valor); + } + }); + + return valoresPorMesPorFilial; + }, [mesesDisponiveis, opcoesFiliais, filtrosAplicados, filiaisSelecionadas]); + + // Memoizar valores do grupo 01 por mês para evitar recálculos repetidos + const valoresGrupo01PorMesMemo = React.useMemo(() => { + const valores: Record = {}; + mesesDisponiveis.forEach(mes => { + valores[mes] = data + .filter(item => { + const codgrupo = item.codgrupo || item.codigo_grupo || ""; + return codgrupo === "01" && item.data_competencia === mes; + }) + .reduce((sum, item) => sum + parseFloat(item.valor), 0); + }); + return valores; + }, [data, mesesDisponiveis]); + + // Memoizar valores do grupo 01 por mês e por filial + const valoresGrupo01PorMesPorFilialMemo = React.useMemo(() => { + const valores: Record> = {}; + // Usar filiais selecionadas se houver filtros aplicados, senão usar todas as opções disponíveis + const filiaisDisponiveis = (filtrosAplicados && filiaisSelecionadas.length > 0) + ? filiaisSelecionadas + : (opcoesFiliais.length > 0 + ? opcoesFiliais + : [...new Set(data.map(item => item.filial || item.codfilial).filter(Boolean))] as string[]); + + mesesDisponiveis.forEach(mes => { + valores[mes] = {}; + filiaisDisponiveis.forEach(filial => { + valores[mes][filial] = data + .filter(item => { + const codgrupo = item.codgrupo || item.codigo_grupo || ""; + const itemFilial = item.filial || item.codfilial || ""; + return codgrupo === "01" && item.data_competencia === mes && itemFilial === filial; + }) + .reduce((sum, item) => sum + parseFloat(item.valor), 0); + }); + }); + return valores; + }, [data, mesesDisponiveis, opcoesFiliais, filtrosAplicados, filiaisSelecionadas]); + + // Função para calcular percentuais baseado no CODGRUPO 01 (FATURAMENTO LÍQUIDO) + const calcularPercentuaisPorMes = React.useCallback(( + valoresPorMes: Record, + codigoGrupo?: string + ): Record => { + const percentuais: Record = {}; + + // Se for CODGRUPO 01, sempre retornar 100% + if (codigoGrupo === "01") { + mesesDisponiveis.forEach(mes => { + percentuais[mes] = 100; + }); + return percentuais; + } + + // Usar valores memoizados do grupo 01 + Object.keys(valoresPorMes).forEach((mes) => { + const valorAtual = valoresPorMes[mes]; + const valorGrupo01 = valoresGrupo01PorMesMemo[mes] || 0; + + if (valorGrupo01 !== 0) { + percentuais[mes] = (valorAtual / valorGrupo01) * 100; + } else { + percentuais[mes] = 0; + } + }); + + return percentuais; + }, [mesesDisponiveis, valoresGrupo01PorMesMemo]); + + // Função para calcular percentuais por mês e por filial baseado no CODGRUPO 01 + const calcularPercentuaisPorMesPorFilial = React.useCallback(( + valoresPorMesPorFilial: Record>, + codigoGrupo?: string + ): Record> => { + const percentuaisPorMesPorFilial: Record> = {}; + + // Usar filiais selecionadas se houver filtros aplicados, senão usar todas as opções disponíveis + const filiaisDisponiveis = (filtrosAplicados && filiaisSelecionadas.length > 0) + ? filiaisSelecionadas + : (opcoesFiliais.length > 0 + ? opcoesFiliais + : Object.keys(valoresPorMesPorFilial[mesesDisponiveis[0] || ""] || {})); + + // Se for CODGRUPO 01, sempre retornar 100% para todas as filiais + if (codigoGrupo === "01") { + mesesDisponiveis.forEach(mes => { + percentuaisPorMesPorFilial[mes] = {}; + filiaisDisponiveis.forEach(filial => { + percentuaisPorMesPorFilial[mes][filial] = 100; + }); + }); + return percentuaisPorMesPorFilial; + } + + // Usar valores memoizados do grupo 01 + mesesDisponiveis.forEach(mes => { + percentuaisPorMesPorFilial[mes] = {}; + filiaisDisponiveis.forEach(filial => { + const valorAtual = valoresPorMesPorFilial[mes]?.[filial] || 0; + const valorGrupo01 = valoresGrupo01PorMesPorFilialMemo[mes]?.[filial] || 0; + + if (valorGrupo01 !== 0) { + percentuaisPorMesPorFilial[mes][filial] = (valorAtual / valorGrupo01) * 100; + } else { + percentuaisPorMesPorFilial[mes][filial] = 0; + } + }); + }); + + return percentuaisPorMesPorFilial; + }, [mesesDisponiveis, opcoesFiliais, valoresGrupo01PorMesPorFilialMemo, filtrosAplicados, filiaisSelecionadas]); + + // Memoizar total do grupo 01 + const totalGrupo01Memo = React.useMemo(() => { + return data + .filter(item => { + const codgrupo = item.codgrupo || item.codigo_grupo || ""; + return codgrupo === "01"; + }) + .reduce((sum, item) => sum + parseFloat(item.valor), 0); + }, [data]); + + // Função para calcular percentual do total baseado no CODGRUPO 01 + const calcularPercentualTotal = React.useCallback((total: number, codigoGrupo?: string): number => { + // Se for CODGRUPO 01, sempre retornar 100% + if (codigoGrupo === "01") { + return 100; + } + + // Usar total memoizado do grupo 01 + if (totalGrupo01Memo !== 0) { + return (total / totalGrupo01Memo) * 100; + } else { + return 0; + } + }, [totalGrupo01Memo]); + + const buildHierarchicalData = React.useCallback((): HierarchicalRow[] => { + const rows: HierarchicalRow[] = []; + + // Hierarquia simplificada: [grupo, conta] + // Agrupar por CODGRUPO + const gruposPorCodigo = data.reduce((acc, item) => { + const codgrupo = item.codgrupo || item.codigo_grupo || ""; + if (!codgrupo) return acc; + if (!acc[codgrupo]) { + acc[codgrupo] = []; + } + acc[codgrupo].push(item); + return acc; + }, {} as Record); + + // Calcular valores por grupo para linhas calculadas + const valoresPorGrupo: Record> = {}; + Object.keys(gruposPorCodigo).forEach(codgrupo => { + valoresPorGrupo[codgrupo] = calcularValoresPorMes(gruposPorCodigo[codgrupo]); + }); + + // Ordenar por CODGRUPO (numericamente) + const sortedGrupos = Object.entries(gruposPorCodigo).sort(([codA], [codB]) => { + const numA = parseInt(codA) || 0; + const numB = parseInt(codB) || 0; + if (numA !== numB) { + return numA - numB; + } + return codA.localeCompare(codB); + }); + + sortedGrupos.forEach(([codgrupo, items], index) => { + // Calcular total do grupo + const totalGrupo = items.reduce( + (sum, item) => sum + parseFloat(item.valor), + 0 + ); + const valoresGrupoPorMes = calcularValoresPorMes(items); + const valoresGrupoPorMesPorFilial = calcularValoresPorMesPorFilial(items); + + // Linha do grupo (Level 0) + rows.push({ + type: "grupo", + level: 0, + grupo: items[0]?.grupo || codgrupo, + codigo_grupo: codgrupo, + total: totalGrupo, + isExpanded: expandedGrupos.has(codgrupo), + valoresPorMes: valoresGrupoPorMes, + valoresPorMesPorFilial: valoresGrupoPorMesPorFilial, + percentuaisPorMes: calcularPercentuaisPorMes(valoresGrupoPorMes, codgrupo), + percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresGrupoPorMesPorFilial, codgrupo), + percentualTotal: calcularPercentualTotal(totalGrupo, codgrupo), + }); + + if (expandedGrupos.has(codgrupo)) { + // Agrupar por conta dentro do grupo + const contas = items.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; + } + + return contaA.localeCompare(contaB); + }); + + sortedContas.forEach(([conta, contaItems]) => { + const totalConta = contaItems.reduce( + (sum, item) => sum + parseFloat(item.valor), + 0 + ); + const valoresContaPorMes = calcularValoresPorMes(contaItems); + const valoresContaPorMesPorFilial = calcularValoresPorMesPorFilial(contaItems); + + // Linha da conta (Level 1) + rows.push({ + type: "conta", + level: 1, + grupo: items[0]?.grupo || codgrupo, + codigo_grupo: codgrupo, + conta, + codigo_conta: contaItems[0]?.codigo_conta, + total: totalConta, + isExpanded: false, + valoresPorMes: valoresContaPorMes, + valoresPorMesPorFilial: valoresContaPorMesPorFilial, + percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, codgrupo), + percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresContaPorMesPorFilial, codgrupo), + percentualTotal: calcularPercentualTotal(totalConta, codgrupo), + }); + }); + } + + // Adicionar linha calculada "MARGEM DE LOJA" após o grupo 02 + // Verificar se é o último grupo ou se o próximo grupo é maior que 02 + const proximoCodigo = sortedGrupos[index + 1]?.[0]; + const proximoNumero = proximoCodigo ? parseInt(proximoCodigo) : 999; + + if (codgrupo === "02" || (parseInt(codgrupo) === 2 && proximoNumero > 2)) { + // Calcular MARGEM DE LOJA = CODGRUPO 01 - CODGRUPO 02 + const valoresGrupo01 = valoresPorGrupo["01"] || {}; + const valoresGrupo02 = valoresPorGrupo["02"] || {}; + + // Calcular valores por mês para MARGEM DE LOJA + const valoresMargemPorMes: Record = {}; + mesesDisponiveis.forEach(mes => { + const valor01 = valoresGrupo01[mes] || 0; + const valor02 = valoresGrupo02[mes] || 0; + valoresMargemPorMes[mes] = valor01 - valor02; + }); + + // Calcular valores por mês e por filial para MARGEM DE LOJA + const valoresMargemPorMesPorFilial: Record> = {}; + const valoresGrupo01PorFilial = gruposPorCodigo["01"] ? calcularValoresPorMesPorFilial(gruposPorCodigo["01"]) : {}; + const valoresGrupo02PorFilial = gruposPorCodigo["02"] ? calcularValoresPorMesPorFilial(gruposPorCodigo["02"]) : {}; + + mesesDisponiveis.forEach(mes => { + valoresMargemPorMesPorFilial[mes] = {}; + opcoesFiliais.forEach(filial => { + const valor01 = valoresGrupo01PorFilial[mes]?.[filial] || 0; + const valor02 = valoresGrupo02PorFilial[mes]?.[filial] || 0; + valoresMargemPorMesPorFilial[mes][filial] = valor01 - valor02; + }); + }); + + // Calcular total + const totalMargem = Object.values(valoresMargemPorMes).reduce((sum, val) => sum + val, 0); + + // Adicionar linha calculada + rows.push({ + type: "calculado", + level: 0, + grupo: "MARGEM DE LOJA", + codigo_grupo: "MARGEM", + total: totalMargem, + isExpanded: false, + valoresPorMes: valoresMargemPorMes, + valoresPorMesPorFilial: valoresMargemPorMesPorFilial, + percentuaisPorMes: calcularPercentuaisPorMes(valoresMargemPorMes, "MARGEM"), + percentuaisPorMesPorFilial: calcularPercentuaisPorMesPorFilial(valoresMargemPorMesPorFilial, "MARGEM"), + percentualTotal: calcularPercentualTotal(totalMargem, "MARGEM"), + isCalculado: true, + }); + } + }); + + return rows; + }, [data, mesesDisponiveis, expandedGrupos, opcoesFiliais, filtrosAplicados, filiaisSelecionadas, calcularValoresPorMes, calcularValoresPorMesPorFilial, calcularPercentuaisPorMes, calcularPercentuaisPorMesPorFilial, calcularPercentualTotal]); + + const getRowStyle = React.useCallback((row: HierarchicalRow) => { + const baseStyle = + "transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30"; + + const linhaId = `${row.type}-${row.codigo_grupo || ""}-${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 "grupo": + 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 "calculado": + return `${style} bg-gradient-to-r from-purple-50/30 to-pink-50/30 font-bold text-purple-900 border-b-2 border-purple-300 italic`; + case "conta": + return `${style} bg-white font-normal text-gray-600`; + default: + return style; + } + }, [linhaSelecionada]); + + const getFixedCellBackground = React.useCallback((row: HierarchicalRow): string => { + const linhaId = `${row.type}-${row.codigo_grupo || ""}-${row.codigo_conta || ""}`; + const isSelected = linhaSelecionada === linhaId; + + if (isSelected) { + return "bg-gradient-to-r from-green-100 to-emerald-100"; + } + + switch (row.type) { + case "grupo": + return "bg-gradient-to-r from-blue-50 to-indigo-50"; + case "calculado": + return "bg-gradient-to-r from-purple-50 to-pink-50"; + case "conta": + return "bg-white"; + default: + return "bg-white"; + } + }, [linhaSelecionada]); + + const getIndentStyle = React.useCallback((level: number) => { + return { paddingLeft: `${level * 20}px` }; + }, []); + + const renderCellContent = React.useCallback((row: HierarchicalRow) => { + switch (row.type) { + case "grupo": + return ( +
+ + +
+ ); + case "calculado": + return ( +
+
+ {/* = */} + +
+
+
+ + {row.grupo} + +
+
+
+ ); + case "conta": + return ( +
+
+ +
+ +
+ ); + default: + return null; + } + }, [toggleGrupo, handleRowClick]); + + const toggleExpandAll = () => { + if (isAllExpanded) { + setExpandedGrupos(new Set()); + setIsAllExpanded(false); + } else { + const todosGrupos = [...new Set(data.map(item => item.codgrupo || item.codigo_grupo).filter(Boolean))]; + setExpandedGrupos(new Set(todosGrupos)); + setIsAllExpanded(true); + } + }; + + const aplicarFiltros = async () => { + setIsFilterOpen(false); + + setTimeout(async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch("/api/dre-filial-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 filial (multi-seleção) + if (filiaisSelecionadas.length > 0) { + dadosFiltrados = dadosFiltrados.filter((item: DREItem) => { + const filialItem = item.filial || item.codfilial || ""; + return filiaisSelecionadas.includes(filialItem); + }); + } + + setDadosFiltrados(dadosFiltrados); + setData(dadosFiltrados); + setFiltrosAplicados(true); + + // Limpar filtros do analítico ao aplicar novos filtros na tabela + setAnaliticoFiltros({ + dataInicio: "", + dataFim: "", + centroCusto: "", + codigoGrupo: "", + codigoSubgrupo: "", + codigoConta: "", + codFilial: "", + linhaSelecionada: "", + excluirCentroCusto: "", + excluirCodigoConta: "", + codigosCentrosCustoSelecionados: "", + codigosContasSelecionadas: "", + }); + + // Extrair meses únicos dos dados filtrados + const meses = [ + ...new Set( + dadosFiltrados.map((item: DREItem) => item.data_competencia) + ), + ].sort() as string[]; + setMesesDisponiveis(meses); + + } catch (err) { + setError(err instanceof Error ? err.message : "Erro desconhecido"); + } finally { + setLoading(false); + } + }, 100); + }; + + 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, + filial: "Todas", + }); + + setFiliaisSelecionadas([]); + setData([]); + setDadosFiltrados([]); + setFiltrosAplicados(false); + setMesesDisponiveis([]); + setIsAllExpanded(false); + setIsFilterOpen(false); + + // Limpar filtros do analítico também + setAnaliticoFiltros({ + dataInicio: "", + dataFim: "", + centroCusto: "", + codigoGrupo: "", + codigoSubgrupo: "", + codigoConta: "", + codFilial: "", + linhaSelecionada: "", + excluirCentroCusto: "", + excluirCodigoConta: "", + codigosCentrosCustoSelecionados: "", + codigosContasSelecionadas: "", + }); + + carregarPeriodosDisponiveis(); + }; + + const exportarXLSX = () => { + if (!data.length) { + console.log('⚠️ Nenhum dado para exportar'); + return; + } + + const dadosCompletosExpandidos = buildHierarchicalData(); + + const dadosExportacao = dadosCompletosExpandidos.map((row, index) => { + const linha: any = { + 'Linha': index + 1, + 'Tipo': row.type, + 'Nível': row.level, + 'Grupo': row.grupo || '', + 'Código Grupo': row.codigo_grupo || '', + 'Conta': row.conta || '', + 'Código Conta': row.codigo_conta || '', + 'Total': row.total || 0, + }; + + mesesDisponiveis.forEach(mes => { + const valor = row.valoresPorMes?.[mes] || 0; + const percentual = row.percentuaisPorMes?.[mes] || 0; + linha[`Valor ${mes}`] = valor; + linha[`% ${mes}`] = percentual; + }); + + return linha; + }); + + const wb = XLSX.utils.book_new(); + const ws = XLSX.utils.json_to_sheet(dadosExportacao); + + const colWidths = [ + { wch: 8 }, // Linha + { wch: 15 }, // Tipo + { wch: 8 }, // Nível + { wch: 25 }, // Grupo + { wch: 15 }, // Código Grupo + { wch: 35 }, // Conta + { wch: 12 }, // Código Conta + { wch: 15 }, // Total + ]; + + mesesDisponiveis.forEach(() => { + colWidths.push({ wch: 15 }); // Valor + colWidths.push({ wch: 10 }); // % + }); + + ws['!cols'] = colWidths; + + XLSX.utils.book_append_sheet(wb, ws, 'DRE Filial Completo'); + + const resumoData = [ + { 'Informação': 'Período', 'Valor': `${filtros.periodoDe} a ${filtros.periodoAte}` }, + { 'Informação': 'Filial', 'Valor': filtros.filial }, + { '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'); + + const dataAtual = new Date().toISOString().split('T')[0]; + const nomeArquivo = `DRE_Filial_Completo_${dataAtual}.xlsx`; + + XLSX.writeFile(wb, nomeArquivo); + + console.log('✅ Arquivo XLSX completo exportado:', nomeArquivo); + }; + + // Memoizar dados hierárquicos para evitar recálculos desnecessários + const hierarchicalData = React.useMemo(() => { + if (!data.length || !mesesDisponiveis.length) { + return []; + } + return buildHierarchicalData(); + }, [buildHierarchicalData]); + + return ( +
+ {/* Header Section */} +
+
+
+
+

Despesa Filial

+

+ Demonstração do Resultado do Exercício +

+
+
+ + {/* Controles */} +
+ + + + + + + + + + + Filtros + + Ajuste os critérios e clique em Pesquisar para atualizar a visão. + + + +
+
+ {/* Período */} +
+ +
+
+ + +
+
+ + +
+
+
+ + {/* Filial */} +
+
+ +
+ + +
+
+ setFiltroFilial(e.target.value)} + className="h-8 text-sm" + /> +
+ {opcoesFiliais + .filter(filial => { + if (!filtroFilial) return true; + const termo = filtroFilial.toLowerCase(); + return filial.toLowerCase().includes(termo); + }) + .map(filial => ( +
+ toggleFilial(filial)} + /> + +
+ ))} +
+ {filiaisSelecionadas.length > 0 && ( +
+ {filiaisSelecionadas.length} filial(is) selecionada(s) +
+ )} +
+
+
+ + + + + + +
+
+
+
+
+ + {/* 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 && ( +
+
+ + {/* Table Header */} + + + + {mesesDisponiveis.map((mes) => { + const filiaisParaMes = (filtrosAplicados && filiaisSelecionadas.length > 0) + ? filiaisSelecionadas + : (opcoesFiliais.length > 0 ? opcoesFiliais : ['']); + + return ( + + {/* Cabeçalhos de filiais para este mês */} + {filiaisParaMes.map((filial: string) => ( + + + + + ))} + + {/* Cabeçalhos de totalizador para este mês */} + + + + ); + })} + + + + + + {/* Table Body */} + + {hierarchicalData.map((row, index) => { + const linhaId = `${row.type}-${row.codigo_grupo || ""}-${row.codigo_conta || ""}`; + const isSelected = linhaSelecionada === linhaId; + return ( + + ); + })} + +
+ Descrição + + {mes}{filial && <>
+ Filial - {filial}} +
+ %{filial && <>
+ Filial - {filial}} +
+ {mes}
+ Total +
+ %
+ Total +
+ Total + + % +
+
+
+ )} + + {/* Componente Analítico - Sempre visível, mas só carrega dados após clique */} +
+ +
+
+ ); +} +