From c5090a29a6b083a58c349cb66e6188af54b33423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alessandro=20Gon=C3=A7aalves?= Date: Tue, 9 Dec 2025 11:29:32 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20corre=C3=A7=C3=B5es=20de=20inicializa?= =?UTF-8?q?=C3=A7=C3=A3o=20da=20rota=20/dre-filial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/analitico-filial-oracle/route.ts | 255 ++++ src/app/api/dre-filial-oracle/route.ts | 102 ++ src/app/dre-filial/analitico.tsx | 997 +++++++++++++++ src/app/dre-filial/page.tsx | 10 + src/app/dre-filial/teste.tsx | 1182 ++++++++++++++++++ 5 files changed, 2546 insertions(+) create mode 100644 src/app/api/analitico-filial-oracle/route.ts create mode 100644 src/app/api/dre-filial-oracle/route.ts create mode 100644 src/app/dre-filial/analitico.tsx create mode 100644 src/app/dre-filial/page.tsx create mode 100644 src/app/dre-filial/teste.tsx 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..9891e47 --- /dev/null +++ b/src/app/api/analitico-filial-oracle/route.ts @@ -0,0 +1,255 @@ +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'); + + // 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 VB_DRE_FILIAL_DESPESA_ANALITICO + let sql = `SELECT * FROM VB_DRE_FILIAL_DESPESA_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 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.FILIAL || "001", // Usar 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-filial/analitico.tsx b/src/app/dre-filial/analitico.tsx new file mode 100644 index 0000000..02c04c3 --- /dev/null +++ b/src/app/dre-filial/analitico.tsx @@ -0,0 +1,997 @@ +"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 atualização de licença +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; + grupo?: string; + filial?: string; + tipo?: string; + // Novos campos + data_lancamento?: string; + data_compensacao?: string; + data_pagto?: string; +} + +interface AnaliticoProps { + filtros: { + dataInicio: string; + dataFim: string; + codigoGrupo?: string; + codigoConta?: string; + linhaSelecionada?: string; + excluirCodigoConta?: string; + codigosContasSelecionadas?: string; + filial?: 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.codigoGrupo) { + params.append('codigoGrupo', filtrosExternos.codigoGrupo); + } + if (filtrosExternos.codigoConta) { + params.append('codigoConta', filtrosExternos.codigoConta); + } + if (filtrosExternos.filial) { + params.append('filial', filtrosExternos.filial); + } + if (filtrosExternos.excluirCodigoConta) { + params.append('excluirCodigoConta', filtrosExternos.excluirCodigoConta); + } + 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, + codigoGrupo: filtrosExternos.codigoGrupo, + codigoConta: filtrosExternos.codigoConta, + filial: filtrosExternos.filial, + 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: "ano_mes_comp", + headerName: "Ano/Mês Comp", + width: 110, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + { + field: "filial", + headerName: "Filial", + width: 90, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + { + field: "codigo_grupo", + headerName: "Cod.Grupo", + width: 100, + sortable: true, + resizable: true, + }, + { + field: "grupo", + headerName: "Grupo", + width: 200, + sortable: true, + resizable: true, + }, + { + 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: "tipo", + headerName: "Tipo", + width: 95, + sortable: true, + resizable: true, + renderCell: (params: any) => params.value || "-", + }, + ]; + + // 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 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 valores monetários + if (column.field === "valor") { + return formatCurrencyValue(value, true); + } + + // Para campos que retornam "-" se vazios + if (column.field === "filial" || column.field === "ano_mes_comp" || + column.field === "tipo") { + return 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.codigoConta) && ( +
+
+ Filtros aplicados pela tabela DRE Filial: +
+ {filtrosExternos.dataInicio && filtrosExternos.dataFim && ( + + Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim} + + )} + {filtrosExternos.codigoGrupo && ( + + Grupo: {filtrosExternos.codigoGrupo} + + )} + {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", + }, + }} + /> +
+
+
+ + ); + }; + + 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.codigoConta) && ( +
+
+ Filtros aplicados pela tabela DRE Filial: +
+ {filtrosExternos.dataInicio && filtrosExternos.dataFim && ( + + Período: {filtrosExternos.dataInicio} a {filtrosExternos.dataFim} + + )} + {filtrosExternos.codigoGrupo && ( + + Grupo: {filtrosExternos.codigoGrupo} + + )} + {filtrosExternos.codigoConta && ( + + Conta: {filtrosExternos.codigoConta} + + )} +
+
+ )} + + {/* 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..dfc2ebc --- /dev/null +++ b/src/app/dre-filial/teste.tsx @@ -0,0 +1,1182 @@ +"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"; + level: number; + grupo?: string; + codigo_grupo?: string; + conta?: string; + codigo_conta?: number; + total?: number; + isExpanded?: boolean; + valoresPorMes?: Record; + percentuaisPorMes?: Record; + percentualTotal?: number; +} + +// 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 ( + + handleRowClick(row)} + > +
+ {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 [expandedGrupos, setExpandedGrupos] = useState>(new Set()); + const [mesesDisponiveis, setMesesDisponiveis] = useState([]); + + // Estados para filtros + const [filtros, setFiltros] = useState({ + periodoDe: "", + periodoAte: "", + grupo: "Todos", + conta: "Todas", + }); + + // Estados para multi-seleção + const [contasSelecionadas, setContasSelecionadas] = useState([]); + const [codigosContas, setCodigosContas] = 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 [opcoesContas, setOpcoesContas] = useState([]); + + // Estados para filtros de busca nos campos de seleção + const [filtroConta, setFiltroConta] = useState(""); + + // Estados para analítico + const [analiticoFiltros, setAnaliticoFiltros] = useState({ + dataInicio: "", + dataFim: "", + centroCusto: "", + codigoGrupo: "", + codigoSubgrupo: "", + codigoConta: "", + 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 contas únicas + const contasUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.conta))].sort() as string[]; + setOpcoesContas(contasUnicas); + + // Criar objeto de códigos das contas + const codigosContasObj: Record = {}; + dadosCompletos.forEach((item: DREItem) => { + if (item.conta && item.codigo_conta) { + codigosContasObj[item.conta] = item.codigo_conta.toString(); + } + }); + setCodigosContas(codigosContasObj); + + // Inicializar com todas as contas selecionadas + setContasSelecionadas(contasUnicas); + + } 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 = (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 + + // 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; + } + + 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() || ""; + } + + // Obter códigos das contas selecionadas no filtro + const codigosContasSelecionadas = contasSelecionadas + .map(conta => { + const item = data.find((d: DREItem) => d.conta === conta); + return item?.codigo_conta?.toString(); + }) + .filter(codigo => codigo) + .join(','); + + const novosFiltros = { + dataInicio: dataInicioFiltro, + dataFim: dataFimFiltro, + centroCusto: "", // Não aplicável na hierarquia filial + codigoGrupo: codigoGrupoFiltro, + codigoSubgrupo: "", // Não aplicável na hierarquia filial + codigoConta: codigoContaFiltro, + linhaSelecionada: row.grupo || row.conta || "", + excluirCentroCusto: "", + excluirCodigoConta: "", + codigosCentrosCustoSelecionados: "", + codigosContasSelecionadas, + }; + + console.log('🎯 Novos filtros para analítico:', novosFiltros); + setAnaliticoFiltros(novosFiltros); + }; + + 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 toggleConta = (conta: string) => { + setContasSelecionadas(prev => { + if (prev.includes(conta)) { + return prev.filter(c => c !== conta); + } else { + return [...prev, conta]; + } + }); + }; + + const selecionarTodasContas = () => { + setContasSelecionadas(opcoesContas); + }; + + const limparContas = () => { + setContasSelecionadas([]); + }; + + // Função auxiliar para calcular valores por mês + const calcularValoresPorMes = (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; + }; + + // Função para calcular percentuais (simplificada - sem faturamento líquido) + const calcularPercentuaisPorMes = ( + valoresPorMes: Record + ): Record => { + const percentuais: Record = {}; + + // Calcular total geral por mês + const totaisPorMes: Record = {}; + mesesDisponiveis.forEach(mes => { + totaisPorMes[mes] = data + .filter(item => item.data_competencia === mes) + .reduce((sum, item) => sum + parseFloat(item.valor), 0); + }); + + Object.keys(valoresPorMes).forEach((mes) => { + const valorAtual = valoresPorMes[mes]; + const totalMes = totaisPorMes[mes] || 0; + + if (totalMes !== 0) { + percentuais[mes] = (valorAtual / totalMes) * 100; + } else { + percentuais[mes] = 0; + } + }); + + return percentuais; + }; + + // Função para calcular percentual do total + const calcularPercentualTotal = (total: number): number => { + const totalGeral = data.reduce((sum, item) => sum + parseFloat(item.valor), 0); + + if (totalGeral !== 0) { + return (total / totalGeral) * 100; + } else { + return 0; + } + }; + + const buildHierarchicalData = (): HierarchicalRow[] => { + const rows: HierarchicalRow[] = []; + + // 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); + + // 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]) => { + // Calcular total do grupo + const totalGrupo = items.reduce( + (sum, item) => sum + parseFloat(item.valor), + 0 + ); + const valoresGrupoPorMes = calcularValoresPorMes(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, + percentuaisPorMes: calcularPercentuaisPorMes(valoresGrupoPorMes), + percentualTotal: calcularPercentualTotal(totalGrupo), + }); + + 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); + + // 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, + percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes), + percentualTotal: calcularPercentualTotal(totalConta), + }); + }); + } + }); + + return rows; + }; + + const getRowStyle = (row: HierarchicalRow) => { + const baseStyle = + "transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30"; + + 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 "conta": + return `${style} bg-white font-normal text-gray-600`; + default: + return style; + } + }; + + const getFixedCellBackground = (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 "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 "grupo": + return ( +
+ + +
+ ); + case "conta": + return ( +
+
+ +
+ +
+ ); + default: + return null; + } + }; + + 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 grupo + if (filtros.grupo !== "Todos") { + dadosFiltrados = dadosFiltrados.filter((item: DREItem) => + item.grupo === filtros.grupo + ); + } + + // Filtro por conta (multi-seleção) + if (contasSelecionadas.length > 0) { + dadosFiltrados = dadosFiltrados.filter((item: DREItem) => { + return contasSelecionadas.includes(item.conta); + }); + } + + setDadosFiltrados(dadosFiltrados); + setData(dadosFiltrados); + setFiltrosAplicados(true); + + // 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, + grupo: "Todos", + conta: "Todas", + }); + + setContasSelecionadas([]); + setData([]); + setDadosFiltrados([]); + setFiltrosAplicados(false); + setMesesDisponiveis([]); + setIsAllExpanded(false); + setIsFilterOpen(false); + + 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': 'Grupo', 'Valor': filtros.grupo }, + { 'Informação': 'Conta', 'Valor': filtros.conta }, + { '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); + }; + + const hierarchicalData = buildHierarchicalData(); + + return ( +
+ {/* Header Section */} +
+
+
+
+

Despesa Filial

+

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

+
+
+ + {/* Controles */} +
+ + + + + + + + + + + Filtros + + Configure os filtros para visualizar os dados do DRE Filial + + + +
+ {/* Período */} +
+ +
+
+ + setFiltros(prev => ({ ...prev, periodoDe: e.target.value }))} + className="h-9" + /> +
+
+ + setFiltros(prev => ({ ...prev, periodoAte: e.target.value }))} + className="h-9" + /> +
+
+
+ + {/* Grupo */} +
+ + +
+ + {/* 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) +
+ )} +
+
+ + + + + + +
+
+
+
+
+ + {/* 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) => ( + + + + + ))} + + + + + + {/* Table Body */} + + {hierarchicalData.map((row, index) => { + const linhaId = `${row.type}-${row.codigo_grupo || ""}-${row.codigo_conta || ""}`; + const isSelected = linhaSelecionada === linhaId; + return ( + + ); + })} + +
+ Descrição + + {mes} + + % + + Total + + % +
+
+
+ )} + + {/* Componente Analítico - Sempre visível */} +
+ +
+
+ ); +} +