vendaweb-api/src/app/DRE/teste.tsx

2726 lines
102 KiB
TypeScript

"use client";
import { LoaderPinwheel, ChevronDown, ChevronRight, Filter, Maximize2, Minimize2, Download } from "lucide-react";
import React, { useEffect, useState, useCallback, startTransition, memo } from "react";
import AnaliticoComponent from "./analitico";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import * as XLSX from "xlsx";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface DREItem {
codfilial: string;
data_competencia: string;
data_cai: string;
grupo: string;
subgrupo: string;
centro_custo: string;
codigo_centro_custo: string;
codigo_conta: number;
conta: string;
valor: string;
codgrupo?: string;
isCalculado?: boolean;
}
interface HierarchicalRow {
type: "grupo" | "subgrupo" | "centro_custo" | "conta";
level: number;
grupo?: string;
subgrupo?: string;
centro_custo?: string;
codigo_centro_custo?: string;
conta?: string;
codigo_conta?: number;
total?: number;
isExpanded?: boolean;
valoresPorMes?: Record<string, number>;
percentuaisPorMes?: Record<string, number>;
percentualTotal?: number;
isCalculado?: boolean;
}
// Componente memoizado para linhas da tabela
const TableRow = memo(({
row,
index,
toggleGroup,
toggleCentro,
handleRowClick,
getRowStyle,
getIndentStyle,
renderCellContent,
mesesDisponiveis,
formatCurrency,
formatCurrencyWithColor
}: {
row: HierarchicalRow;
index: number;
toggleGroup: (grupo: string) => void;
toggleCentro: (centro: string) => void;
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 };
}) => {
return (
<tr
className={`text-sm border-b border-gray-100 hover:bg-gray-50 transition-all duration-200 ease-in-out ${getRowStyle(
row
)}`}
>
<td className="px-4 py-1 w-[300px] min-w-[300px] whitespace-nowrap overflow-hidden">
<div style={getIndentStyle(row.level)}>
{renderCellContent(row)}
</div>
</td>
{/* Colunas de valores por mês */}
{mesesDisponiveis.map((mes) => (
<React.Fragment key={mes}>
<td
className="px-2 py-1 text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[120px] min-w-[120px]"
onClick={() => 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 (
<span
className={
isNegative
? "text-red-600 font-bold"
: "text-gray-900"
}
>
{formatted}
</span>
);
})()
: "-"}
</td>
<td
className="px-2 py-1 text-center font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[100px] min-w-[100px]"
onClick={() => 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)}%`
: "-"}
</td>
</React.Fragment>
))}
{/* Coluna Total */}
<td
className="px-4 py-1 text-right font-semibold cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[120px] min-w-[120px]"
onClick={() => handleRowClick(row)}
title={row.total ? formatCurrency(row.total) : "-"}
>
{(() => {
const { formatted, isNegative } = formatCurrencyWithColor(
row.total!
);
return (
<span
className={
isNegative ? "text-red-600 font-bold" : "text-gray-900"
}
>
{formatted}
</span>
);
})()}
</td>
{/* Coluna Percentual Total */}
<td
className="px-2 py-1 text-center font-medium cursor-pointer hover:bg-blue-50/50 transition-colors duration-200 whitespace-nowrap overflow-hidden w-[100px] min-w-[100px]"
onClick={() => handleRowClick(row)}
title={
row.percentualTotal !== undefined
? `${row.percentualTotal.toFixed(1)}%`
: "-"
}
>
{row.percentualTotal !== undefined
? `${row.percentualTotal.toFixed(1)}%`
: "-"}
</td>
</tr>
);
});
TableRow.displayName = 'TableRow';
export default function Teste() {
const [data, setData] = useState<DREItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(
new Set()
);
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
// Estados para filtros
const [filtros, setFiltros] = useState({
periodoDe: "",
periodoAte: "",
grupo: "Todos",
subgrupo: "Todos",
centroCusto: "Todos",
conta: "Todas",
valorMin: "",
valorMax: "",
buscaTextual: ""
});
// Estados para multi-seleção
const [centrosCustoSelecionados, setCentrosCustoSelecionados] = useState<string[]>([]);
// Estado para armazenar os códigos dos centros de custo
const [codigosCentrosCusto, setCodigosCentrosCusto] = useState<Record<string, string>>({});
const [contasSelecionadas, setContasSelecionadas] = useState<string[]>([]);
// Estado para armazenar os códigos das contas
const [codigosContas, setCodigosContas] = useState<Record<string, string>>({});
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [dadosFiltrados, setDadosFiltrados] = useState<DREItem[]>([]);
const [filtrosAplicados, setFiltrosAplicados] = useState(false);
const [ordemHierarquiaContasPrimeiro, setOrdemHierarquiaContasPrimeiro] = useState(true);
// Estados para opções dos filtros
const [opcoesGrupos, setOpcoesGrupos] = useState<string[]>([]);
const [opcoesSubgrupos, setOpcoesSubgrupos] = useState<string[]>([]);
const [opcoesCentrosCusto, setOpcoesCentrosCusto] = useState<string[]>([]);
const [opcoesContas, setOpcoesContas] = useState<string[]>([]);
// Estados para filtros de busca nos campos de seleção
const [filtroCentroCusto, setFiltroCentroCusto] = useState<string>("");
const [filtroConta, setFiltroConta] = useState<string>("");
// Estados para analítico
const [analiticoFiltros, setAnaliticoFiltros] = useState({
dataInicio: "",
dataFim: "",
centroCusto: "",
codigoGrupo: "",
codigoSubgrupo: "",
codigoConta: "",
linhaSelecionada: "", // Adicionar informação da linha selecionada
excluirCentroCusto: "", // Para excluir centro de custo específico quando desmarcado
excluirCodigoConta: "", // Para excluir código de conta específico quando desmarcado
codigosCentrosCustoSelecionados: "", // Códigos dos centros de custo selecionados no filtro
codigosContasSelecionadas: "", // Códigos das contas selecionadas no filtro
});
const [linhaSelecionada, setLinhaSelecionada] = useState<string | null>(null);
const [isAllExpanded, setIsAllExpanded] = useState(false);
useEffect(() => {
// Carregar períodos disponíveis da API
carregarPeriodosDisponiveis();
// Inicializar filtros com período atual
const agora = new Date();
const anoAtual = agora.getFullYear();
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
const periodoAtual = `${anoAtual}-${mesAtual}`;
setFiltros(prev => ({
...prev,
periodoDe: `${anoAtual}-01`,
periodoAte: periodoAtual
}));
}, []);
const carregarPeriodosDisponiveis = async () => {
try {
const response = await fetch("/api/dre-oracle");
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
const dadosCompletos = await response.json();
// Extrair períodos únicos dos dados
const periodosUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.data_competencia))].sort() as string[];
setMesesDisponiveis(periodosUnicos);
// Extrair grupos únicos
const gruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.grupo))].sort() as string[];
setOpcoesGrupos(gruposUnicos);
// Extrair subgrupos únicos
const subgruposUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.subgrupo))].sort() as string[];
setOpcoesSubgrupos(subgruposUnicos);
// Extrair centros de custo únicos com nome e código
const centrosCustoUnicos = [...new Set(dadosCompletos.map((item: DREItem) => item.centro_custo))].sort() as string[];
setOpcoesCentrosCusto(centrosCustoUnicos);
// Criar objeto de códigos dos centros de custo
// Usar um Map para garantir que pegamos o código correto mesmo com duplicatas
const codigos: Record<string, string> = {};
const codigosPorNome = new Map<string, Set<string>>();
dadosCompletos.forEach((item: DREItem) => {
if (item.centro_custo && item.codigo_centro_custo) {
if (!codigosPorNome.has(item.centro_custo)) {
codigosPorNome.set(item.centro_custo, new Set());
}
codigosPorNome.get(item.centro_custo)!.add(item.codigo_centro_custo);
}
});
// Para cada centro de custo, usar o código mais comum ou o primeiro encontrado
codigosPorNome.forEach((codigosSet, nome) => {
const codigosArray = Array.from(codigosSet);
// Se houver apenas um código, usar esse
if (codigosArray.length === 1) {
codigos[nome] = codigosArray[0];
} else {
// Se houver múltiplos códigos, verificar qual é mais frequente nos dados
const frequencia: Record<string, number> = {};
dadosCompletos.forEach((item: DREItem) => {
if (item.centro_custo === nome && item.codigo_centro_custo) {
frequencia[item.codigo_centro_custo] = (frequencia[item.codigo_centro_custo] || 0) + 1;
}
});
// Pegar o código mais frequente
const codigoMaisFrequente = Object.entries(frequencia).sort((a, b) => b[1] - a[1])[0];
codigos[nome] = codigoMaisFrequente ? codigoMaisFrequente[0] : codigosArray[0];
}
});
console.log('🗺️ Mapeamento de códigos de centros de custo criado:', codigos);
setCodigosCentrosCusto(codigos);
// Extrair contas únicas
const contasUnicas = [...new Set(dadosCompletos.map((item: DREItem) => item.conta))].sort() as string[];
setOpcoesContas(contasUnicas);
// Criar objeto de códigos das contas
const codigosContasObj: Record<string, string> = {};
dadosCompletos.forEach((item: DREItem) => {
if (item.conta && item.codigo_conta) {
codigosContasObj[item.conta] = item.codigo_conta.toString();
}
});
setCodigosContas(codigosContasObj);
// Inicializar com todos os itens selecionados, exceto o centro de custo 002.003.017 e conta 100050
const centrosCustoIniciaisSelecionados = centrosCustoUnicos.filter(centro => {
const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro);
return item?.codigo_centro_custo !== "002.003.017";
});
const contasIniciaisSelecionadas = contasUnicas.filter(conta => {
const item = dadosCompletos.find((d: DREItem) => d.conta === conta);
return item?.codigo_conta?.toString() !== "100050";
});
setCentrosCustoSelecionados(centrosCustoIniciaisSelecionados);
setContasSelecionadas(contasIniciaisSelecionadas);
} catch (error) {
console.error("Erro ao carregar períodos:", error);
}
};
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch("/api/dre-oracle");
if (!response.ok) {
throw new Error(`Erro ao carregar dados: ${response.status}`);
}
const result = await response.json();
setData(result);
// Extrair meses únicos dos dados
const meses = [
...new Set(
result.map((item: DREItem) => {
// Usar diretamente o valor de data_competencia que já vem no formato YYYY-MM
return item.data_competencia;
})
),
].sort() as string[];
setMesesDisponiveis(meses);
} catch (err) {
setError(err instanceof Error ? err.message : "Erro desconhecido");
} finally {
setLoading(false);
}
};
const formatCurrency = (value: string | number) => {
const numValue = typeof value === "string" ? parseFloat(value) : value;
return numValue.toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
});
};
const formatCurrencyWithColor = (value: string | number) => {
const numValue = typeof value === "string" ? parseFloat(value) : value;
const formatted = formatCurrency(value);
const isNegative = numValue < 0;
return { formatted, isNegative };
};
// Função para extrair códigos dos grupos e subgrupos
const extractCodes = (grupo: string, subgrupo?: string) => {
const grupoMatch = grupo.match(/^(\d+)/);
const codigoGrupo = grupoMatch ? grupoMatch[1] : "";
let codigoSubgrupo = "";
if (subgrupo) {
// Primeiro tenta extrair código numérico (ex: "001.008 - LOJA 8" -> "001.008")
const subgrupoMatch = subgrupo.match(/^(\d+(?:\.\d+)+)/);
if (subgrupoMatch) {
codigoSubgrupo = subgrupoMatch[1];
} else {
// Se não tem código numérico, usa a descrição completa (ex: "DESPESAS ADMINISTRATIVAS")
codigoSubgrupo = subgrupo;
}
}
return { codigoGrupo, codigoSubgrupo };
};
// Função para lidar com clique nas linhas
const handleRowClick = (row: HierarchicalRow, mesSelecionado?: string) => {
console.log('🖱️ Clique na linha:', row);
console.log('📅 Mês selecionado:', mesSelecionado);
if (!data.length) {
console.log('⚠️ Sem dados disponíveis');
return;
}
// Pegar todas as datas disponíveis para definir o período
const datas = data.map((item) => item.data_competencia);
const dataInicio = Math.min(...datas.map((d) => new Date(d).getTime()));
const dataFim = Math.max(...datas.map((d) => new Date(d).getTime()));
const dataInicioStr = new Date(dataInicio).toISOString().substring(0, 7); // YYYY-MM
const dataFimStr = new Date(dataFim).toISOString().substring(0, 7); // YYYY-MM
console.log('📅 Datas calculadas:', { dataInicioStr, dataFimStr });
const { codigoGrupo, codigoSubgrupo } = extractCodes(
row.grupo || "",
row.subgrupo
);
console.log('🔍 Códigos extraídos:', { codigoGrupo, codigoSubgrupo });
// Criar um identificador único para a linha
const linhaId = `${row.type}-${row.grupo || ""}-${row.subgrupo || ""}-${
row.centro_custo || ""
}-${row.codigo_conta || ""}`;
setLinhaSelecionada(linhaId);
// Se um mês específico foi selecionado, usar apenas esse mês
const dataInicioFiltro = mesSelecionado || dataInicioStr;
const dataFimFiltro = mesSelecionado || dataFimStr;
// Determinar filtros baseado na ordem hierárquica e tipo da linha
let centroCustoFiltro = "";
let codigoContaFiltro = "";
if (ordemHierarquiaContasPrimeiro) {
// Ordem: Contas → Centros de Custo
// SEMPRE filtrar por código da conta, independente do tipo da linha
codigoContaFiltro = row.codigo_conta?.toString() || "";
// Se for centro de custo, também filtrar por código do centro de custo
centroCustoFiltro = row.type === "centro_custo" ? (row.codigo_centro_custo || "") : "";
} else {
// Ordem: Centros de Custo → Contas
// SEMPRE filtrar por código do centro de custo, independente do tipo da linha
centroCustoFiltro = row.codigo_centro_custo || "";
// Se for conta, também filtrar por código da conta
codigoContaFiltro = row.type === "conta" ? (row.codigo_conta?.toString() || "") : "";
}
console.log('🎯 Filtros determinados:', {
centroCustoFiltro,
codigoContaFiltro,
ordemHierarquia: ordemHierarquiaContasPrimeiro ? 'Contas→Centros' : 'Centros→Contas',
tipoLinha: row.type,
rowData: {
codigo_conta: row.codigo_conta,
codigo_centro_custo: row.codigo_centro_custo,
centro_custo: row.centro_custo,
conta: row.conta
}
});
// Determinar exclusões baseado nos filtros aplicados
let excluirCentroCusto = "";
let excluirCodigoConta = "";
// Se o centro de custo "002.003.017" não está selecionado, excluir da consulta
const centroCusto002003017Selecionado = centrosCustoSelecionados.some(centro => {
// Verificar pelo código no mapeamento primeiro
const codigoCentro = codigosCentrosCusto[centro];
if (codigoCentro === "002.003.017") {
return true;
}
// Se não encontrar no mapeamento, buscar nos dados
const item = data.find((d: DREItem) => d.centro_custo === centro);
return item?.codigo_centro_custo === "002.003.017";
});
if (!centroCusto002003017Selecionado) {
excluirCentroCusto = "002.003.017";
}
// Se a conta "100050" não está selecionada, excluir da consulta
const conta100050Selecionada = contasSelecionadas.some(conta => {
// Verificar pelo código no mapeamento primeiro
const codigoConta = codigosContas[conta];
if (codigoConta === "100050") {
return true;
}
// Se não encontrar no mapeamento, buscar nos dados
const item = data.find((d: DREItem) => d.conta === conta);
return item?.codigo_conta?.toString() === "100050";
});
if (!conta100050Selecionada) {
excluirCodigoConta = "100050";
}
// Obter códigos dos centros de custo selecionados no filtro - APENAS CÓDIGOS
const codigosCentrosCustoSelecionados = centrosCustoSelecionados
.map(centro => {
// Primeiro tentar buscar no objeto codigosCentrosCusto (mapeamento)
const codigoDoMapeamento = codigosCentrosCusto[centro];
if (codigoDoMapeamento) {
console.log(`🔍 Código encontrado no mapeamento para "${centro}": ${codigoDoMapeamento}`);
return codigoDoMapeamento;
}
// Se não encontrar no mapeamento, buscar nos dados
const item = data.find((d: DREItem) => d.centro_custo === centro);
const codigoEncontrado = item?.codigo_centro_custo;
if (codigoEncontrado) {
console.log(`🔍 Código encontrado nos dados para "${centro}": ${codigoEncontrado}`);
} else {
console.warn(`⚠️ Código NÃO encontrado para centro de custo "${centro}"`);
}
return codigoEncontrado;
})
.filter(codigo => codigo && codigo.trim() !== '') // Remover undefined e strings vazias
.join(',');
console.log('📋 Códigos de centros de custo selecionados:', codigosCentrosCustoSelecionados);
// 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) // Remover undefined
.join(',');
const novosFiltros = {
dataInicio: dataInicioFiltro,
dataFim: dataFimFiltro,
centroCusto: centroCustoFiltro,
codigoGrupo,
codigoSubgrupo,
codigoConta: codigoContaFiltro,
linhaSelecionada: row.grupo || row.subgrupo || row.centro_custo || row.conta || "", // Incluir informação da linha selecionada
excluirCentroCusto,
excluirCodigoConta,
codigosCentrosCustoSelecionados,
codigosContasSelecionadas,
};
console.log('🎯 Novos filtros para analítico:', novosFiltros);
console.log('🔍 Verificação final dos filtros:', {
centroCusto: novosFiltros.centroCusto,
codigoConta: novosFiltros.codigoConta,
codigoContaType: typeof novosFiltros.codigoConta,
codigoContaLength: novosFiltros.codigoConta?.length,
codigosCentrosCustoSelecionados: novosFiltros.codigosCentrosCustoSelecionados,
codigosContasSelecionadas: novosFiltros.codigosContasSelecionadas
});
setAnaliticoFiltros(novosFiltros);
};
const toggleGroup = useCallback((grupo: string) => {
setExpandedGroups(prev => {
const newExpanded = new Set(prev);
if (newExpanded.has(grupo)) {
newExpanded.delete(grupo);
} else {
newExpanded.add(grupo);
}
return newExpanded;
});
}, []);
const toggleCentro = useCallback((centro: string) => {
setExpandedCentros(prev => {
const newExpanded = new Set(prev);
if (newExpanded.has(centro)) {
newExpanded.delete(centro);
} else {
newExpanded.add(centro);
}
return newExpanded;
});
}, []);
const handleFiltroChange = (campo: string, valor: string) => {
setFiltros(prev => ({
...prev,
[campo]: valor
}));
};
// Funções para multi-seleção
const toggleCentroCusto = (centro: string) => {
setCentrosCustoSelecionados(prev => {
if (prev.includes(centro)) {
return prev.filter(c => c !== centro);
} else {
return [...prev, centro];
}
});
};
const toggleConta = (conta: string) => {
setContasSelecionadas(prev => {
if (prev.includes(conta)) {
return prev.filter(c => c !== conta);
} else {
return [...prev, conta];
}
});
};
const selecionarTodosCentros = () => {
setCentrosCustoSelecionados(opcoesCentrosCusto);
};
const limparCentros = () => {
setCentrosCustoSelecionados([]);
};
const selecionarTodasContas = () => {
setContasSelecionadas(opcoesContas);
};
const limparContas = () => {
setContasSelecionadas([]);
};
// Função auxiliar para obter o código do centro de custo
const obterCodigoCentroCusto = React.useCallback((nomeCentro: string): string => {
if (!data || data.length === 0) {
return '';
}
// Buscar o primeiro item que corresponde ao nome do centro de custo
const item = data.find(item => item.centro_custo === nomeCentro);
if (item && item.codigo_centro_custo) {
return item.codigo_centro_custo;
}
return '';
}, [data]);
const exportarXLSX = () => {
if (!data.length) {
console.log('⚠️ Nenhum dado para exportar');
return;
}
console.log('📊 Exportando TODOS os dados expandidos para XLSX...');
// Criar uma versão completamente expandida dos dados hierárquicos
const dadosCompletosExpandidos = buildHierarchicalDataCompleta();
// Preparar dados para exportação
const dadosExportacao = dadosCompletosExpandidos.map((row, index) => {
const linha: any = {
'Linha': index + 1,
'Tipo': row.type,
'Nível': row.level,
'Grupo': row.grupo || '',
'Centro de Custo': row.centro_custo || '',
'Conta': row.conta || '',
'Código Centro': row.codigo_centro_custo || '',
'Código Conta': row.codigo_conta || '',
'Total': row.total || 0,
};
// Adicionar colunas dos meses
mesesDisponiveis.forEach(mes => {
const valor = row.valoresPorMes?.[mes] || 0;
const percentual = row.percentuaisPorMes?.[mes] || 0;
linha[`Valor ${mes}`] = valor;
linha[`% ${mes}`] = percentual;
});
return linha;
});
// Criar workbook
const wb = XLSX.utils.book_new();
// Criar worksheet principal
const ws = XLSX.utils.json_to_sheet(dadosExportacao);
// Ajustar largura das colunas
const colWidths = [
{ wch: 8 }, // Linha
{ wch: 15 }, // Tipo
{ wch: 8 }, // Nível
{ wch: 30 }, // Grupo
{ wch: 25 }, // Centro de Custo
{ wch: 35 }, // Conta
{ wch: 15 }, // Código Centro
{ wch: 12 }, // Código Conta
{ wch: 15 }, // Total
];
// Adicionar larguras para colunas dos meses
mesesDisponiveis.forEach(() => {
colWidths.push({ wch: 15 }); // Valor
colWidths.push({ wch: 10 }); // %
});
ws['!cols'] = colWidths;
// Adicionar worksheet ao workbook
XLSX.utils.book_append_sheet(wb, ws, 'DRE Gerencial Completo');
// Criar worksheet de resumo
const resumoData = [
{ 'Informação': 'Período', 'Valor': `${filtros.periodoDe} a ${filtros.periodoAte}` },
{ 'Informação': 'Grupo', 'Valor': filtros.grupo },
{ 'Informação': 'Subgrupo', 'Valor': filtros.subgrupo },
{ 'Informação': 'Centro de Custo', 'Valor': filtros.centroCusto },
{ 'Informação': 'Conta', 'Valor': filtros.conta },
{ 'Informação': 'Valor Mínimo', 'Valor': filtros.valorMin || 'N/A' },
{ 'Informação': 'Valor Máximo', 'Valor': filtros.valorMax || 'N/A' },
{ 'Informação': 'Busca Textual', 'Valor': filtros.buscaTextual || 'N/A' },
{ 'Informação': 'Ordem Hierárquica', 'Valor': ordemHierarquiaContasPrimeiro ? 'Contas → Centros' : 'Centros → Contas' },
{ 'Informação': 'Total de Registros', 'Valor': dadosCompletosExpandidos.length },
{ 'Informação': 'Data de Exportação', 'Valor': new Date().toLocaleString('pt-BR') },
];
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
wsResumo['!cols'] = [{ wch: 20 }, { wch: 30 }];
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
// Gerar nome do arquivo
const dataAtual = new Date().toISOString().split('T')[0];
const nomeArquivo = `DRE_Gerencial_Completo_${dataAtual}.xlsx`;
// Exportar arquivo
XLSX.writeFile(wb, nomeArquivo);
console.log('✅ Arquivo XLSX completo exportado:', nomeArquivo);
};
// Função para construir dados hierárquicos completamente expandidos
const buildHierarchicalDataCompleta = (): HierarchicalRow[] => {
const rows: HierarchicalRow[] = [];
// Agrupar dados por grupo
const grupos = data.reduce((acc, item) => {
if (!acc[item.grupo]) {
acc[item.grupo] = [];
}
acc[item.grupo].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar grupos por código
const sortedGrupos = Object.entries(grupos).sort(([grupoA, itemsA], [grupoB, itemsB]) => {
const codigoA = itemsA[0]?.codgrupo || "";
const codigoB = itemsB[0]?.codgrupo || "";
return codigoA.localeCompare(codigoB);
});
// Função para calcular valores de grupos calculados usando códigos dos grupos (mesma lógica da função principal)
const calcularGrupoCalculadoCompleta = (codigoGrupo: string, gruposData: Record<string, DREItem[]>): Record<string, number> => {
const valoresPorMes: Record<string, number> = {};
// Inicializar valores para todos os meses disponíveis
mesesDisponiveis.forEach(mes => {
valoresPorMes[mes] = 0;
});
// Função auxiliar para obter valor de um grupo por código e mês
const obterValorGrupo = (codigoGrupo: string, mes: string): number => {
const grupoEncontrado = Object.values(gruposData).find(items =>
items.length > 0 && items[0].codgrupo === codigoGrupo
);
if (!grupoEncontrado) return 0;
const itemsMes = grupoEncontrado.filter(item => item.data_competencia === mes);
return itemsMes.reduce((sum, item) => sum + parseFloat(item.valor), 0);
};
switch (codigoGrupo) {
case "03": // Faturamento Líquido = Grupo 01 + 02
mesesDisponiveis.forEach(mes => {
const valor01 = obterValorGrupo("01", mes);
const valor02 = obterValorGrupo("02", mes);
valoresPorMes[mes] = valor01 + valor02;
});
break;
case "05": // Lucro Bruto = Grupo 03 + 04
mesesDisponiveis.forEach(mes => {
const valor03 = obterValorGrupo("03", mes);
const valor04 = obterValorGrupo("04", mes);
valoresPorMes[mes] = valor03 + valor04;
});
break;
case "07": // Margem Loja = Grupo 05 + 06
mesesDisponiveis.forEach(mes => {
const valor05 = obterValorGrupo("05", mes);
const valor06 = obterValorGrupo("06", mes);
valoresPorMes[mes] = valor05 + valor06;
});
break;
case "10": // Resultado Operacional = Grupo 07 + 08 + 09
mesesDisponiveis.forEach(mes => {
const valor07 = obterValorGrupo("07", mes);
const valor08 = obterValorGrupo("08", mes);
const valor09 = obterValorGrupo("09", mes);
valoresPorMes[mes] = valor07 + valor08 + valor09;
});
break;
case "13": // Resultado Financeiro = Grupo 11 + 12
mesesDisponiveis.forEach(mes => {
const valor11 = obterValorGrupo("11", mes);
const valor12 = obterValorGrupo("12", mes);
valoresPorMes[mes] = valor11 + valor12;
});
break;
case "19": // Resultado Não Operacional = Grupo 14 + 15 + 16 + 17 + 18
mesesDisponiveis.forEach(mes => {
const valor14 = obterValorGrupo("14", mes);
const valor15 = obterValorGrupo("15", mes);
const valor16 = obterValorGrupo("16", mes);
const valor17 = obterValorGrupo("17", mes);
const valor18 = obterValorGrupo("18", mes);
valoresPorMes[mes] = valor14 + valor15 + valor16 + valor17 + valor18;
});
break;
case "20": // LAIR = Grupo 10 + 13 + 19
mesesDisponiveis.forEach(mes => {
const valor10 = obterValorGrupo("10", mes);
const valor13 = obterValorGrupo("13", mes);
const valor19 = obterValorGrupo("19", mes);
valoresPorMes[mes] = valor10 + valor13 + valor19;
});
break;
case "21": // IR = Se LAIR > 0 calcular 20% e resultado negativo (*-1), se não 0
mesesDisponiveis.forEach(mes => {
const valor20 = obterValorGrupo("20", mes);
if (valor20 > 0) {
valoresPorMes[mes] = (valor20 * 0.20) * -1;
} else {
valoresPorMes[mes] = 0;
}
});
break;
case "22": // CSLL = Se LAIR > 0 calcular 9% e resultado negativo (*-1), se não 0
mesesDisponiveis.forEach(mes => {
const valor20 = obterValorGrupo("20", mes);
if (valor20 > 0) {
valoresPorMes[mes] = (valor20 * 0.09) * -1;
} else {
valoresPorMes[mes] = 0;
}
});
break;
case "23": // Lucro Líquido = Grupo 20 + 21 + 22
mesesDisponiveis.forEach(mes => {
const valor20 = obterValorGrupo("20", mes);
const valor21 = obterValorGrupo("21", mes);
const valor22 = obterValorGrupo("22", mes);
valoresPorMes[mes] = valor20 + valor21 + valor22;
});
break;
case "25": // EBITDA = Grupo 20 - (13 + 19 + 24)
mesesDisponiveis.forEach(mes => {
const valor20 = obterValorGrupo("20", mes);
const valor13 = obterValorGrupo("13", mes);
const valor19 = obterValorGrupo("19", mes);
const valor24 = obterValorGrupo("24", mes);
valoresPorMes[mes] = valor20 - (valor13 + valor19 + valor24);
});
break;
}
return valoresPorMes;
};
sortedGrupos.forEach(([grupo, items]) => {
const totalGrupo = items.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Verificar se é um grupo calculado
const codigoGrupo = items[0]?.codgrupo || "";
const isCalculado = ["03", "05", "07", "10", "13", "19", "20", "21", "22", "23", "25"].includes(codigoGrupo);
let valoresGrupoPorMes;
if (isCalculado) {
// Usar cálculo específico para grupos calculados
valoresGrupoPorMes = calcularGrupoCalculadoCompleta(codigoGrupo, grupos);
} else {
// Usar cálculo normal para grupos não calculados
valoresGrupoPorMes = calcularValoresPorMes(items);
}
// Calcular total do grupo
const totalCalculado = Object.values(valoresGrupoPorMes).reduce((sum, valor) => sum + valor, 0);
const totalFinal = isCalculado ? totalCalculado : totalGrupo;
// Linha do grupo (Level 0)
rows.push({
type: "grupo",
level: 0,
grupo,
total: totalFinal,
valoresPorMes: valoresGrupoPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(valoresGrupoPorMes, grupo),
percentualTotal: calcularPercentualTotal(totalFinal, grupo),
isCalculado: isCalculado,
});
if (ordemHierarquiaContasPrimeiro) {
// ORDEM: Grupos → Contas → Centros de Custo
// Agrupar por conta dentro do grupo
const contas = items.reduce((acc, item) => {
if (!acc[item.conta]) {
acc[item.conta] = [];
}
acc[item.conta].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar contas por CODCONTA
const sortedContas = Object.entries(contas).sort(([contaA, itemsA], [contaB, itemsB]) => {
const codigoA = itemsA[0]?.codigo_conta || 0;
const codigoB = itemsB[0]?.codigo_conta || 0;
return codigoA - codigoB;
});
sortedContas.forEach(([conta, contaItems]) => {
const totalConta = contaItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha da conta (Level 1)
const valoresContaPorMes = calcularValoresPorMes(contaItems);
rows.push({
type: "conta",
level: 1,
grupo,
conta,
codigo_conta: contaItems[0].codigo_conta,
codigo_centro_custo: contaItems[0].codigo_centro_custo,
total: totalConta,
valoresPorMes: valoresContaPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, grupo),
percentualTotal: calcularPercentualTotal(totalConta, grupo),
});
// Agrupar por centro de custo dentro da conta
const centros = contaItems.reduce((acc, item) => {
if (!acc[item.centro_custo]) {
acc[item.centro_custo] = [];
}
acc[item.centro_custo].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar centros de custo por CODIGOCENTROCUSTO
const sortedCentros = Object.entries(centros).sort(([centroA, itemsA], [centroB, itemsB]) => {
const codigoA = itemsA[0]?.codigo_centro_custo || "";
const codigoB = itemsB[0]?.codigo_centro_custo || "";
return codigoA.localeCompare(codigoB);
});
sortedCentros.forEach(([centro, centroItems]) => {
const totalCentro = centroItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha do centro de custo (Level 2)
const valoresCentroPorMes = calcularValoresPorMes(centroItems);
rows.push({
type: "centro_custo",
level: 2,
grupo,
centro_custo: centro,
conta,
codigo_conta: contaItems[0].codigo_conta,
codigo_centro_custo: centroItems[0].codigo_centro_custo,
total: totalCentro,
valoresPorMes: valoresCentroPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(valoresCentroPorMes, grupo),
percentualTotal: calcularPercentualTotal(totalCentro, grupo),
});
});
});
} else {
// ORDEM ORIGINAL: Grupos → Centros de Custo → Contas
// Agrupar por centro de custo dentro do grupo
const centros = items.reduce((acc, item) => {
if (!acc[item.centro_custo]) {
acc[item.centro_custo] = [];
}
acc[item.centro_custo].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar centros de custo por CODIGOCENTROCUSTO
const sortedCentros = Object.entries(centros).sort(([centroA, itemsA], [centroB, itemsB]) => {
const codigoA = itemsA[0]?.codigo_centro_custo || "";
const codigoB = itemsB[0]?.codigo_centro_custo || "";
return codigoA.localeCompare(codigoB);
});
sortedCentros.forEach(([centro, centroItems]) => {
const totalCentro = centroItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha do centro de custo (Level 1)
const valoresCentroPorMes = calcularValoresPorMes(centroItems);
rows.push({
type: "centro_custo",
level: 1,
grupo,
centro_custo: centro,
codigo_conta: centroItems[0].codigo_conta,
codigo_centro_custo: centroItems[0].codigo_centro_custo,
total: totalCentro,
valoresPorMes: valoresCentroPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(valoresCentroPorMes, grupo),
percentualTotal: calcularPercentualTotal(totalCentro, grupo),
});
// Agrupar por conta dentro do centro de custo
const contas = centroItems.reduce((acc, item) => {
if (!acc[item.conta]) {
acc[item.conta] = [];
}
acc[item.conta].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar contas por CODCONTA
const sortedContas = Object.entries(contas).sort(([contaA, itemsA], [contaB, itemsB]) => {
const codigoA = itemsA[0]?.codigo_conta || 0;
const codigoB = itemsB[0]?.codigo_conta || 0;
return codigoA - codigoB;
});
sortedContas.forEach(([conta, contaItems]) => {
const totalConta = contaItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha da conta (Level 2)
const valoresContaPorMes = calcularValoresPorMes(contaItems);
rows.push({
type: "conta",
level: 2,
grupo,
centro_custo: centro,
conta,
codigo_conta: contaItems[0].codigo_conta,
codigo_centro_custo: centroItems[0].codigo_centro_custo,
total: totalConta,
valoresPorMes: valoresContaPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(valoresContaPorMes, grupo),
percentualTotal: calcularPercentualTotal(totalConta, grupo),
});
});
});
}
});
return rows;
};
const toggleExpandAll = useCallback(() => {
if (isAllExpanded) {
// Recolher tudo - usar startTransition para atualizações não urgentes
startTransition(() => {
setExpandedGroups(new Set());
setExpandedCentros(new Set());
setIsAllExpanded(false);
});
} else {
// Expandir todos os grupos e contas usando dados originais - usar startTransition para atualizações não urgentes
startTransition(() => {
const todosGrupos = [...new Set(data.map(item => item.grupo))];
if (ordemHierarquiaContasPrimeiro) {
// Nova ordem: expandir grupos e contas
const todasContas = [...new Set(data.map(item => `${item.grupo}-${item.conta}`))];
setExpandedGroups(new Set(todosGrupos));
setExpandedCentros(new Set(todasContas));
} else {
// Ordem original: expandir grupos e centros de custo
const todosCentros = [...new Set(data.map(item => `${item.grupo}-${item.centro_custo}`))];
setExpandedGroups(new Set(todosGrupos));
setExpandedCentros(new Set(todosCentros));
}
setIsAllExpanded(true);
});
}
}, [isAllExpanded, data, ordemHierarquiaContasPrimeiro]);
// Função para recalcular grupos calculados baseado apenas nos dados filtrados
const recalcularGruposCalculados = (dadosFiltrados: DREItem[]): DREItem[] => {
const gruposCalculados: DREItem[] = [];
// Agrupar dados por mês para cálculos
const dadosPorMes = dadosFiltrados.reduce((acc, item) => {
const mes = item.data_competencia;
if (!acc[mes]) acc[mes] = [];
acc[mes].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Para cada mês, criar os grupos calculados
Object.keys(dadosPorMes).forEach(mes => {
const dadosMes = dadosPorMes[mes];
// Calcular valores por grupo usando código numérico
const valoresPorGrupo = dadosMes.reduce((acc: Record<string, number>, item: DREItem) => {
const codgrupo = item.codgrupo || "";
if (!codgrupo) return acc;
if (!acc[codgrupo]) acc[codgrupo] = 0;
acc[codgrupo] += parseFloat(item.valor);
return acc;
}, {} as Record<string, number>);
// Função auxiliar para obter valor de um grupo (calculado ou não)
const obterValorGrupo = (codigoGrupo: string): number => {
// Primeiro, verificar se já foi calculado nos grupos calculados
const grupoCalculado = gruposCalculados.find(g => g.codgrupo === codigoGrupo && g.data_competencia === mes);
if (grupoCalculado) {
return parseFloat(grupoCalculado.valor);
}
// Se não, buscar nos valores diretos dos grupos
return valoresPorGrupo[codigoGrupo] || 0;
};
// 03 - Faturamento Líquido (01 + 02)
const faturamentoBruto = valoresPorGrupo['01'] || 0;
const devolucao = valoresPorGrupo['02'] || 0;
const faturamentoLiquido = faturamentoBruto + devolucao;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "03 - FATURAMENTO LÍQUIDO",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "FATURAMENTO LÍQUIDO",
valor: faturamentoLiquido.toString(),
codgrupo: "03",
isCalculado: true
});
// 05 - Lucro Bruto (03 + 04) - usar grupo 03 calculado
const cmv = valoresPorGrupo['04'] || 0;
const valor03 = obterValorGrupo("03");
const lucroBruto = valor03 + cmv;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "05 - LUCRO BRUTO",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "LUCRO BRUTO",
valor: lucroBruto.toString(),
codgrupo: "05",
isCalculado: true
});
// 07 - Margem Loja (05 + 06) - usar grupo 05 calculado
const receitasGastosDiretos = valoresPorGrupo['06'] || 0;
const valor05 = obterValorGrupo("05");
const margemLoja = valor05 + receitasGastosDiretos;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "07 - MARGEM LOJA",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "MARGEM LOJA",
valor: margemLoja.toString(),
codgrupo: "07",
isCalculado: true
});
// 10 - Resultado Operacional (07 + 08 + 09) - usar grupo 07 calculado
const verba = valoresPorGrupo['08'] || 0;
const receitasGastosIndiretos = valoresPorGrupo['09'] || 0;
const valor07 = obterValorGrupo("07");
const resultadoOperacional = valor07 + verba + receitasGastosIndiretos;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "10 - RESULTADO OPERACIONAL",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "RESULTADO OPERACIONAL",
valor: resultadoOperacional.toString(),
codgrupo: "10",
isCalculado: true
});
// 13 - Resultado Financeiro (11 + 12)
const receitaFinanceira = valoresPorGrupo['11'] || 0;
const despesaFinanceira = valoresPorGrupo['12'] || 0;
const resultadoFinanceiro = receitaFinanceira + despesaFinanceira;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "13 - RESULTADO FINANCEIRO",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "RESULTADO FINANCEIRO",
valor: resultadoFinanceiro.toString(),
codgrupo: "13",
isCalculado: true
});
// 19 - Resultado Não Operacional (14 + 15 + 16 + 17 + 18)
const prejuizosPerdas = valoresPorGrupo['14'] || 0;
const inativas = valoresPorGrupo['15'] || 0;
const diretoria = valoresPorGrupo['16'] || 0;
const lancamentosSemCC = valoresPorGrupo['17'] || 0;
const receitasDespesasNaoOperacional = valoresPorGrupo['18'] || 0;
const resultadoNaoOperacional = prejuizosPerdas + inativas + diretoria + lancamentosSemCC + receitasDespesasNaoOperacional;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "19 - RESULTADO NÃO OPERACIONAL",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "RESULTADO NÃO OPERACIONAL",
valor: resultadoNaoOperacional.toString(),
codgrupo: "19",
isCalculado: true
});
// 20 - LAIR (10 + 13 + 19) - usar grupos calculados
const valor10 = obterValorGrupo("10");
const valor13 = obterValorGrupo("13");
const valor19 = obterValorGrupo("19");
const lair = valor10 + valor13 + valor19;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "20 - LAIR",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "LUCRO ANTES DO IMPOSTO DE RENDA",
valor: lair.toString(),
codgrupo: "20",
isCalculado: true
});
// 21 - IR = Se LAIR > 0 calcular 20% e resultado negativo (*-1), se não 0
const valor20_ir = obterValorGrupo("20");
const ir = valor20_ir > 0 ? -(valor20_ir * 0.20) : 0;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "21 - IR",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "IMPOSTO DE RENDA",
valor: ir.toString(),
codgrupo: "21",
isCalculado: true
});
// 22 - CSLL = Se LAIR > 0 calcular 9% e resultado negativo (*-1), se não 0
const valor20_csll = obterValorGrupo("20");
const csll = valor20_csll > 0 ? -(valor20_csll * 0.09) : 0;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "22 - CSLL",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "CONTRIBUIÇÃO SOCIAL SOBRE LUCRO LÍQUIDO",
valor: csll.toString(),
codgrupo: "22",
isCalculado: true
});
// 23 - Lucro Líquido (20 + 21 + 22) - usar grupos calculados
const valor20 = obterValorGrupo("20");
const valor21 = obterValorGrupo("21");
const valor22 = obterValorGrupo("22");
const lucroLiquido = valor20 + valor21 + valor22;
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "23 - LUCRO LÍQUIDO",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "LUCRO LÍQUIDO",
valor: lucroLiquido.toString(),
codgrupo: "23",
isCalculado: true
});
// 25 - EBITDA (20 - (13 + 19 + 24)) - usar grupos calculados
const despesaTributaria = valoresPorGrupo['24'] || 0;
const valor20_ebitda = obterValorGrupo("20");
const valor13_ebitda = obterValorGrupo("13");
const valor19_ebitda = obterValorGrupo("19");
const ebitda = valor20_ebitda - (valor13_ebitda + valor19_ebitda + despesaTributaria);
gruposCalculados.push({
codfilial: "001",
data_competencia: mes,
data_cai: mes,
grupo: "25 - EBITDA",
subgrupo: "CALCULADO",
centro_custo: "CALCULADO",
codigo_centro_custo: "",
codigo_conta: 0,
conta: "EBITDA",
valor: ebitda.toString(),
codgrupo: "25",
isCalculado: true
});
});
return gruposCalculados;
};
const limparFiltros = () => {
const agora = new Date();
const anoAtual = agora.getFullYear();
const mesAtual = String(agora.getMonth() + 1).padStart(2, '0');
const periodoAtual = `${anoAtual}-${mesAtual}`;
setFiltros({
periodoDe: `${anoAtual}-01`,
periodoAte: periodoAtual,
grupo: "Todos",
subgrupo: "Todos",
centroCusto: "Todos",
conta: "Todas",
valorMin: "",
valorMax: "",
buscaTextual: ""
});
// Limpar multi-seleções
setCentrosCustoSelecionados([]);
setContasSelecionadas([]);
// Limpar filtros de busca
setFiltroCentroCusto("");
setFiltroConta("");
// Limpar dados da tabela
setData([]);
setDadosFiltrados([]);
setFiltrosAplicados(false);
setMesesDisponiveis([]);
setIsAllExpanded(false);
setOrdemHierarquiaContasPrimeiro(false);
// Fechar o sheet de filtros
setIsFilterOpen(false);
// Recarregar opções e selecionar todos novamente
carregarPeriodosDisponiveis();
};
const aplicarFiltros = async () => {
// Fechar o Sheet primeiro
setIsFilterOpen(false);
// Aguardar um pouco para a animação de fechamento
setTimeout(async () => {
try {
setLoading(true);
setError(null);
// Carregar dados da API
const response = await fetch("/api/dre-oracle");
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
const dadosCompletos = await response.json();
// Aplicar filtros nos dados
let dadosFiltrados = dadosCompletos;
// Filtro por período
if (filtros.periodoDe && filtros.periodoAte) {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
const dataItem = item.data_competencia;
return dataItem >= filtros.periodoDe && dataItem <= filtros.periodoAte;
});
}
// Filtro por grupo
if (filtros.grupo !== "Todos") {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.grupo === filtros.grupo
);
}
// Filtro por subgrupo
if (filtros.subgrupo !== "Todos") {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.subgrupo === filtros.subgrupo
);
}
// Filtro por centro de custo (multi-seleção) - USAR APENAS CÓDIGO
// IMPORTANTE: Preservar grupos calculados (isCalculado ou centro_custo === "CALCULADO")
if (centrosCustoSelecionados.length > 0) {
// Criar conjunto de códigos esperados dos centros selecionados - APENAS CÓDIGOS
const codigosEsperados = new Set<string>();
centrosCustoSelecionados.forEach(centro => {
// Buscar o código no mapeamento primeiro
const codigoCentro = codigosCentrosCusto[centro];
if (codigoCentro) {
codigosEsperados.add(codigoCentro);
} else {
// Se não encontrar no mapeamento, tentar buscar nos dados carregados
const item = dadosCompletos.find((d: DREItem) => d.centro_custo === centro);
if (item?.codigo_centro_custo) {
codigosEsperados.add(item.codigo_centro_custo);
}
}
});
// Filtrar APENAS pelo código do centro de custo, ignorando o nome
// MAS preservar grupos calculados
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
// Preservar grupos calculados (têm centro_custo === "CALCULADO" ou isCalculado === true)
if (item.centro_custo === "CALCULADO" || item.isCalculado === true) {
return true;
}
// Para outros itens, verificar pelo código
if (!item.codigo_centro_custo) {
return false;
}
return codigosEsperados.has(item.codigo_centro_custo);
});
console.log('🏢 Filtro de centros de custo aplicado (APENAS CÓDIGO):', {
selecionados: centrosCustoSelecionados,
codigosEsperados: Array.from(codigosEsperados),
totalFiltrado: dadosFiltrados.length,
centrosEncontrados: [...new Set(dadosFiltrados.map((d: DREItem) => d.centro_custo))],
codigosEncontrados: [...new Set(dadosFiltrados.map((d: DREItem) => d.codigo_centro_custo).filter(Boolean))],
gruposCalculados: dadosFiltrados.filter((d: DREItem) => d.centro_custo === "CALCULADO" || d.isCalculado === true).length
});
}
// Filtro por conta (multi-seleção)
// IMPORTANTE: Preservar grupos calculados (isCalculado ou centro_custo === "CALCULADO")
if (contasSelecionadas.length > 0) {
dadosFiltrados = dadosFiltrados.filter((item: DREItem) => {
// Preservar grupos calculados
if (item.centro_custo === "CALCULADO" || item.isCalculado === true) {
return true;
}
// Para outros itens, verificar se a conta está selecionada
return contasSelecionadas.includes(item.conta);
});
}
// Filtro por valor mínimo
if (filtros.valorMin) {
const valorMin = parseFloat(filtros.valorMin.replace(',', '.'));
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
parseFloat(item.valor) >= valorMin
);
}
// Filtro por valor máximo
if (filtros.valorMax) {
const valorMax = parseFloat(filtros.valorMax.replace(',', '.'));
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
parseFloat(item.valor) <= valorMax
);
}
// Filtro por busca textual
if (filtros.buscaTextual) {
const termoBusca = filtros.buscaTextual.toLowerCase();
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.grupo.toLowerCase().includes(termoBusca) ||
item.subgrupo.toLowerCase().includes(termoBusca) ||
item.centro_custo.toLowerCase().includes(termoBusca) ||
item.conta.toLowerCase().includes(termoBusca)
);
}
// Remover grupos calculados antigos (que foram calculados com todos os dados)
// Eles serão recalculados com base apenas nos dados filtrados
dadosFiltrados = dadosFiltrados.filter((item: DREItem) =>
item.centro_custo !== "CALCULADO" && item.isCalculado !== true
);
// Recalcular grupos calculados com base apenas nos dados filtrados
const gruposCalculadosRecalculados = recalcularGruposCalculados(dadosFiltrados);
// Adicionar os grupos calculados recalculados de volta aos dados filtrados
dadosFiltrados = [...dadosFiltrados, ...gruposCalculadosRecalculados];
setData(dadosFiltrados);
setDadosFiltrados(dadosFiltrados);
setFiltrosAplicados(true);
// Extrair meses únicos dos dados filtrados
const mesesUnicos = [...new Set(dadosFiltrados.map((item: DREItem) => item.data_competencia))].sort() as string[];
setMesesDisponiveis(mesesUnicos);
} catch (error) {
console.error("Erro ao aplicar filtros:", error);
setError(error instanceof Error ? error.message : "Erro desconhecido");
} finally {
setLoading(false);
}
}, 300); // Aguardar 300ms para a animação de fechamento
};
const calcularValoresPorMes = (items: DREItem[]): Record<string, number> => {
const valoresPorMes: Record<string, number> = {};
items.forEach((item) => {
// Usar diretamente o valor de data_competencia que já vem no formato YYYY-MM
const anoMes = item.data_competencia;
if (!valoresPorMes[anoMes]) {
valoresPorMes[anoMes] = 0;
}
valoresPorMes[anoMes] += parseFloat(item.valor);
});
return valoresPorMes;
};
// Função para calcular percentuais baseado no grupo 03 como referência
const calcularPercentuaisPorMes = (
valoresPorMes: Record<string, number>,
grupo: string
): Record<string, number> => {
const percentuais: Record<string, number> = {};
// Se for o grupo 03, retorna 100% para todos os meses
if (grupo.includes("03")) {
Object.keys(valoresPorMes).forEach((mes) => {
percentuais[mes] = 100;
});
return percentuais;
}
// Para outros grupos, calcular percentual baseado no grupo 03
Object.keys(valoresPorMes).forEach((mes) => {
const valorAtual = valoresPorMes[mes];
// Encontrar o valor do grupo 03 para o mesmo mês
const grupo03Items = data.filter((item) => {
// Usar diretamente o valor de data_competencia que já vem no formato YYYY-MM
return item.data_competencia === mes && item.grupo.includes("03");
});
const valorGrupo03 = grupo03Items.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
if (valorGrupo03 !== 0) {
percentuais[mes] = (valorAtual / valorGrupo03) * 100;
} else {
percentuais[mes] = 0;
}
});
return percentuais;
};
// Função para calcular percentual do total baseado no grupo 03 como referência
const calcularPercentualTotal = (
total: number,
grupo: string
): number => {
// Se for o grupo 03, retorna 100%
if (grupo.includes("03")) {
return 100;
}
// Calcular o total do grupo 03
const grupo03Items = data.filter((item) =>
item.grupo.includes("03")
);
const totalGrupo03 = grupo03Items.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
if (totalGrupo03 !== 0) {
return (total / totalGrupo03) * 100;
} else {
return 0;
}
};
const buildHierarchicalData = (): HierarchicalRow[] => {
const rows: HierarchicalRow[] = [];
// Agrupar por grupo
const grupos = data.reduce((acc, item) => {
if (!acc[item.grupo]) {
acc[item.grupo] = [];
}
acc[item.grupo].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar grupos pelo CODGRUPO numérico
const sortedGrupos = Object.entries(grupos).sort(([grupoA, itemsA], [grupoB, itemsB]) => {
// Pegar o CODGRUPO do primeiro item de cada grupo
const codgrupoA = itemsA[0]?.codgrupo || "";
const codgrupoB = itemsB[0]?.codgrupo || "";
// Se ambos têm CODGRUPO, ordenar numericamente
if (codgrupoA && codgrupoB) {
return parseInt(codgrupoA) - parseInt(codgrupoB);
}
// Se apenas um tem CODGRUPO, ele vem primeiro
if (codgrupoA && !codgrupoB) return -1;
if (!codgrupoA && codgrupoB) return 1;
// Se nenhum tem CODGRUPO, ordenar alfabeticamente
return grupoA.localeCompare(grupoB);
});
// Função para calcular valores de grupos calculados usando códigos dos grupos
const calcularGrupoCalculado = (codigoGrupo: string, gruposData: Record<string, DREItem[]>): Record<string, number> => {
const valoresPorMes: Record<string, number> = {};
// Inicializar valores para todos os meses disponíveis
mesesDisponiveis.forEach(mes => {
valoresPorMes[mes] = 0;
});
// Função auxiliar para obter valor de um grupo por código e mês
const obterValorGrupo = (codigoGrupo: string, mes: string): number => {
const grupoEncontrado = Object.values(gruposData).find(items =>
items.length > 0 && items[0].codgrupo === codigoGrupo
);
if (!grupoEncontrado) return 0;
const itemsMes = grupoEncontrado.filter(item => item.data_competencia === mes);
return itemsMes.reduce((sum, item) => sum + parseFloat(item.valor), 0);
};
switch (codigoGrupo) {
case "03": // Faturamento Líquido = Grupo 01 + 02
mesesDisponiveis.forEach(mes => {
const valor01 = obterValorGrupo("01", mes);
const valor02 = obterValorGrupo("02", mes);
valoresPorMes[mes] = valor01 + valor02;
});
break;
case "05": // Lucro Bruto = Grupo 03 + 04
mesesDisponiveis.forEach(mes => {
const valor03 = obterValorGrupo("03", mes);
const valor04 = obterValorGrupo("04", mes);
valoresPorMes[mes] = valor03 + valor04;
});
break;
case "07": // Margem Loja = Grupo 05 + 06
mesesDisponiveis.forEach(mes => {
const valor05 = obterValorGrupo("05", mes);
const valor06 = obterValorGrupo("06", mes);
valoresPorMes[mes] = valor05 + valor06;
});
break;
case "10": // Resultado Operacional = Grupo 07 + 08 + 09
mesesDisponiveis.forEach(mes => {
const valor07 = obterValorGrupo("07", mes);
const valor08 = obterValorGrupo("08", mes);
const valor09 = obterValorGrupo("09", mes);
valoresPorMes[mes] = valor07 + valor08 + valor09;
});
break;
case "13": // Resultado Financeiro = Grupo 11 + 12
mesesDisponiveis.forEach(mes => {
const valor11 = obterValorGrupo("11", mes);
const valor12 = obterValorGrupo("12", mes);
valoresPorMes[mes] = valor11 + valor12;
});
break;
case "19": // Resultado Não Operacional = Grupo 14 + 15 + 16 + 17 + 18
mesesDisponiveis.forEach(mes => {
const valor14 = obterValorGrupo("14", mes);
const valor15 = obterValorGrupo("15", mes);
const valor16 = obterValorGrupo("16", mes);
const valor17 = obterValorGrupo("17", mes);
const valor18 = obterValorGrupo("18", mes);
valoresPorMes[mes] = valor14 + valor15 + valor16 + valor17 + valor18;
});
break;
case "20": // LAIR = Grupo 10 + 13 + 19
mesesDisponiveis.forEach(mes => {
const valor10 = obterValorGrupo("10", mes);
const valor13 = obterValorGrupo("13", mes);
const valor19 = obterValorGrupo("19", mes);
valoresPorMes[mes] = valor10 + valor13 + valor19;
});
break;
case "21": // IR = Se LAIR > 0 calcular 20% e resultado negativo (*-1), se não 0
mesesDisponiveis.forEach(mes => {
const valor20 = obterValorGrupo("20", mes);
if (valor20 > 0) {
valoresPorMes[mes] = (valor20 * 0.20) * -1;
} else {
valoresPorMes[mes] = 0;
}
});
break;
case "22": // CSLL = Se LAIR > 0 calcular 9% e resultado negativo (*-1), se não 0
mesesDisponiveis.forEach(mes => {
const valor20 = obterValorGrupo("20", mes);
if (valor20 > 0) {
valoresPorMes[mes] = (valor20 * 0.09) * -1;
} else {
valoresPorMes[mes] = 0;
}
});
break;
case "23": // Lucro Líquido = Grupo 20 + 21 + 22
mesesDisponiveis.forEach(mes => {
const valor20 = obterValorGrupo("20", mes);
const valor21 = obterValorGrupo("21", mes);
const valor22 = obterValorGrupo("22", mes);
valoresPorMes[mes] = valor20 + valor21 + valor22;
});
break;
case "25": // EBITDA = Grupo 20 - (13 + 19 + 24)
mesesDisponiveis.forEach(mes => {
const valor20 = obterValorGrupo("20", mes);
const valor13 = obterValorGrupo("13", mes);
const valor19 = obterValorGrupo("19", mes);
const valor24 = obterValorGrupo("24", mes);
valoresPorMes[mes] = valor20 - (valor13 + valor19 + valor24);
});
break;
}
return valoresPorMes;
};
sortedGrupos.forEach(([grupo, items]) => {
const totalGrupo = items.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Verificar se é um grupo calculado
const codigoGrupo = items[0]?.codgrupo || "";
const isCalculado = ["03", "05", "07", "10", "13", "19", "20", "21", "22", "23", "25"].includes(codigoGrupo);
let valoresPorMes;
if (isCalculado) {
// Usar cálculo específico para grupos calculados
valoresPorMes = calcularGrupoCalculado(codigoGrupo, grupos);
} else {
// Usar cálculo normal para grupos não calculados
valoresPorMes = calcularValoresPorMes(items);
}
// Calcular total do grupo
const totalCalculado = Object.values(valoresPorMes).reduce((sum, valor) => sum + valor, 0);
const totalFinal = isCalculado ? totalCalculado : totalGrupo;
// Linha do grupo
rows.push({
type: "grupo",
level: 0,
grupo,
total: totalFinal,
isExpanded: expandedGroups.has(grupo),
valoresPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(valoresPorMes, grupo),
percentualTotal: calcularPercentualTotal(totalFinal, grupo),
isCalculado: isCalculado,
});
if (expandedGroups.has(grupo)) {
if (ordemHierarquiaContasPrimeiro) {
// ORDEM: Grupos → Contas → Centros de Custo
// Agrupar por conta dentro do grupo
const contas = items.reduce((acc, item) => {
if (!acc[item.conta]) {
acc[item.conta] = [];
}
acc[item.conta].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar contas por CODCONTA
const sortedContas = Object.entries(contas).sort(([contaA, itemsA], [contaB, itemsB]) => {
const codcontaA = itemsA[0]?.codigo_conta || 0;
const codcontaB = itemsB[0]?.codigo_conta || 0;
if (codcontaA && codcontaB) {
return codcontaA - codcontaB;
}
if (codcontaA && !codcontaB) return -1;
if (!codcontaA && codcontaB) return 1;
return contaA.localeCompare(contaB);
});
sortedContas.forEach(([conta, contaItems]) => {
const totalConta = contaItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha da conta (Level 1)
const valoresContaPorMes = calcularValoresPorMes(contaItems);
rows.push({
type: "conta",
level: 1,
grupo,
conta,
codigo_conta: contaItems[0].codigo_conta,
codigo_centro_custo: contaItems[0].codigo_centro_custo, // ✅ Adicionar código do centro de custo
total: totalConta,
isExpanded: expandedCentros.has(`${grupo}-${conta}`),
valoresPorMes: valoresContaPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresContaPorMes,
grupo
),
percentualTotal: calcularPercentualTotal(totalConta, grupo),
});
if (expandedCentros.has(`${grupo}-${conta}`)) {
// Agrupar por centro de custo dentro da conta
const centros = contaItems.reduce((acc, item) => {
if (!acc[item.centro_custo]) {
acc[item.centro_custo] = [];
}
acc[item.centro_custo].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar centros de custo por CODIGOCENTROCUSTO
const sortedCentros = Object.entries(centros).sort(([centroA, itemsA], [centroB, itemsB]) => {
const codigoA = itemsA[0]?.codigo_centro_custo || "";
const codigoB = itemsB[0]?.codigo_centro_custo || "";
if (codigoA && codigoB) {
return codigoA.localeCompare(codigoB);
}
if (codigoA && !codigoB) return -1;
if (!codigoA && codigoB) return 1;
return centroA.localeCompare(centroB);
});
sortedCentros.forEach(([centro, centroItems]) => {
const totalCentro = centroItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha do centro de custo (Level 2)
const valoresCentroPorMes = calcularValoresPorMes(centroItems);
rows.push({
type: "centro_custo",
level: 2,
grupo,
centro_custo: centro,
conta,
codigo_conta: contaItems[0].codigo_conta, // ✅ Adicionar código da conta
codigo_centro_custo: centroItems[0].codigo_centro_custo,
total: totalCentro,
valoresPorMes: valoresCentroPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresCentroPorMes,
grupo
),
percentualTotal: calcularPercentualTotal(totalCentro, grupo),
});
});
}
});
} else {
// ORDEM ORIGINAL: Grupos → Centros de Custo → Contas
// Agrupar por centro de custo dentro do grupo
const centros = items.reduce((acc, item) => {
if (!acc[item.centro_custo]) {
acc[item.centro_custo] = [];
}
acc[item.centro_custo].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar centros de custo por CODIGOCENTROCUSTO
const sortedCentros = Object.entries(centros).sort(([centroA, itemsA], [centroB, itemsB]) => {
const codigoA = itemsA[0]?.codigo_centro_custo || "";
const codigoB = itemsB[0]?.codigo_centro_custo || "";
if (codigoA && codigoB) {
return codigoA.localeCompare(codigoB);
}
if (codigoA && !codigoB) return -1;
if (!codigoA && codigoB) return 1;
return centroA.localeCompare(centroB);
});
sortedCentros.forEach(([centro, centroItems]) => {
const totalCentro = centroItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha do centro de custo (Level 1)
const valoresCentroPorMes = calcularValoresPorMes(centroItems);
rows.push({
type: "centro_custo",
level: 1,
grupo,
centro_custo: centro,
codigo_conta: centroItems[0].codigo_conta, // ✅ Adicionar código da conta
codigo_centro_custo: centroItems[0].codigo_centro_custo,
total: totalCentro,
isExpanded: expandedCentros.has(`${grupo}-${centro}`),
valoresPorMes: valoresCentroPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresCentroPorMes,
grupo
),
percentualTotal: calcularPercentualTotal(totalCentro, grupo),
});
if (expandedCentros.has(`${grupo}-${centro}`)) {
// Agrupar por conta dentro do centro de custo
const contas = centroItems.reduce((acc, item) => {
if (!acc[item.conta]) {
acc[item.conta] = [];
}
acc[item.conta].push(item);
return acc;
}, {} as Record<string, DREItem[]>);
// Ordenar contas por CODCONTA
const sortedContas = Object.entries(contas).sort(([contaA, itemsA], [contaB, itemsB]) => {
const codcontaA = itemsA[0]?.codigo_conta || 0;
const codcontaB = itemsB[0]?.codigo_conta || 0;
if (codcontaA && codcontaB) {
return codcontaA - codcontaB;
}
if (codcontaA && !codcontaB) return -1;
if (!codcontaA && codcontaB) return 1;
return contaA.localeCompare(contaB);
});
sortedContas.forEach(([conta, contaItems]) => {
const totalConta = contaItems.reduce(
(sum, item) => sum + parseFloat(item.valor),
0
);
// Linha da conta (Level 2)
const valoresContaPorMes = calcularValoresPorMes(contaItems);
rows.push({
type: "conta",
level: 2,
grupo,
centro_custo: centro,
conta,
codigo_conta: contaItems[0].codigo_conta,
codigo_centro_custo: centroItems[0].codigo_centro_custo, // ✅ Adicionar código do centro de custo
total: totalConta,
valoresPorMes: valoresContaPorMes,
percentuaisPorMes: calcularPercentuaisPorMes(
valoresContaPorMes,
grupo
),
percentualTotal: calcularPercentualTotal(totalConta, grupo),
});
});
}
});
}
}
});
return rows;
};
const getRowStyle = (row: HierarchicalRow) => {
const baseStyle =
"transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30";
// Criar identificador único para a linha
const linhaId = `${row.type}-${row.grupo || ""}-${row.subgrupo || ""}-${
row.centro_custo || ""
}-${row.codigo_conta || ""}`;
const isSelected = linhaSelecionada === linhaId;
// Verificar se é um grupo calculado
const isCalculado = row.isCalculado === true;
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":
if (isCalculado) {
// Destacar grupos calculados com cor azul
return `${style} bg-gradient-to-r from-blue-100/80 to-indigo-100/80 font-bold text-gray-900 border-b-2 border-blue-300 shadow-sm`;
}
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 "subgrupo":
return `${style} bg-gradient-to-r from-gray-50/30 to-blue-50/20 font-semibold text-gray-800`;
case "centro_custo":
return `${style} bg-gradient-to-r from-gray-50/20 to-gray-100/10 font-medium text-gray-700`;
case "conta":
return `${style} bg-white font-normal text-gray-600`;
default:
return style;
}
};
const getIndentStyle = (level: number) => {
return { paddingLeft: `${level * 20}px` };
};
const renderCellContent = (row: HierarchicalRow) => {
// Verificar se é um grupo calculado usando a propriedade isCalculado
const isCalculado = row.isCalculado === true;
switch (row.type) {
case "grupo":
return (
<div className="flex items-center gap-2 whitespace-nowrap">
<button
onClick={() => toggleGroup(row.grupo!)}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-150 ease-in-out flex items-center justify-center w-8 h-8 flex-shrink-0 transform hover:scale-105"
>
{row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600 transition-transform duration-150" />
) : (
<ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
)}
</button>
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<div className="flex items-center gap-2">
<span className="font-bold text-gray-900">
{row.grupo}
</span>
{/* {isCalculado && (
<span className="text-blue-600 font-bold text-sm">⚡</span>
)} */}
</div>
</button>
</div>
);
case "conta":
return (
<div className="flex items-center gap-2 whitespace-nowrap">
{ordemHierarquiaContasPrimeiro ? (
// Nova ordem: Conta é level 1 (com botão de toggle)
<button
onClick={() => toggleCentro(`${row.grupo}-${row.conta}`)}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-150 ease-in-out flex items-center justify-center w-8 h-8 flex-shrink-0 transform hover:scale-105"
>
{row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600 transition-transform duration-150" />
) : (
<ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
)}
</button>
) : (
// Ordem original: Conta é level 2 (sem botão de toggle)
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0">
<span className="text-gray-400 font-bold text-lg"></span>
</div>
)}
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<div className="flex items-center gap-2">
<span className="font-semibold text-gray-800">
{row.conta}
</span>
{row.codigo_conta && (
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
{row.codigo_conta}
</span>
)}
</div>
</button>
</div>
);
case "centro_custo":
return (
<div className="flex items-center gap-2 whitespace-nowrap">
{ordemHierarquiaContasPrimeiro ? (
// Nova ordem: Centro de custo é level 2 (sem botão de toggle)
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0">
<span className="text-gray-400 font-bold text-lg"></span>
</div>
) : (
// Ordem original: Centro de custo é level 1 (com botão de toggle)
<button
onClick={() => toggleCentro(`${row.grupo}-${row.centro_custo}`)}
className="p-2 hover:bg-blue-100 rounded-lg transition-all duration-150 ease-in-out flex items-center justify-center w-8 h-8 flex-shrink-0 transform hover:scale-105"
>
{row.isExpanded ? (
<ChevronDown className="w-4 h-4 text-blue-600 transition-transform duration-150" />
) : (
<ChevronRight className="w-4 h-4 text-blue-600 transition-transform duration-150" />
)}
</button>
)}
<button
onClick={() => handleRowClick(row)}
className="flex-1 text-left hover:bg-blue-50/50 p-2 rounded-lg cursor-pointer transition-all duration-200 truncate"
>
<div className="flex items-center gap-2">
<span className="font-medium text-gray-700">
{row.codigo_centro_custo
? `${row.centro_custo} - ${row.codigo_centro_custo}`
: row.centro_custo
}
</span>
</div>
</button>
</div>
);
default:
return null;
}
};
// Loading será tratado dentro do componente principal
// Error será tratado dentro do componente principal
const hierarchicalData = buildHierarchicalData();
return (
<div className="w-full max-w-none mx-auto p-2">
{/* Header Section */}
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<div>
<h1 className="text-2xl font-bold text-gray-900">DRE Gerencial</h1>
<p className="text-sm text-gray-500">
Demonstração do Resultado do Exercício
</p>
</div>
</div>
{/* Controles */}
<div className="flex items-center gap-2">
{/* Botão de Exportar XLSX */}
<Button
variant="outline"
size="sm"
onClick={exportarXLSX}
disabled={!filtrosAplicados || hierarchicalData.length === 0}
className="flex items-center gap-2 text-xs h-8 px-3 transition-all duration-150 ease-in-out hover:scale-105 disabled:hover:scale-100"
>
<Download className="w-4 h-4" />
Exportar XLSX
</Button>
{/* Botão de Expandir/Recolher */}
<Button
variant="outline"
size="sm"
onClick={toggleExpandAll}
disabled={!filtrosAplicados || hierarchicalData.length === 0}
className="flex items-center gap-2 text-xs h-8 px-3 transition-all duration-150 ease-in-out hover:scale-105 disabled:hover:scale-100"
>
{isAllExpanded ? (
<>
<Minimize2 className="w-4 h-4" />
Recolher Tudo
</>
) : (
<>
<Maximize2 className="w-4 h-4" />
Expandir Tudo
</>
)}
</Button>
{/* Botão de Filtro */}
<Sheet open={isFilterOpen} onOpenChange={setIsFilterOpen}>
<SheetTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
<Filter className="w-4 h-4" />
Filtros
</Button>
</SheetTrigger>
<SheetContent className="w-[400px] sm:w-[540px]">
<SheetHeader>
<SheetTitle>Filtros</SheetTitle>
<SheetDescription>
Ajuste os critérios e clique em Pesquisar para atualizar a visão.
</SheetDescription>
</SheetHeader>
<div className="grid flex-1 auto-rows-min gap-3 py-2">
{/* Período */}
<div className="grid gap-2">
<Label>Período</Label>
<div className="grid grid-cols-2 gap-2">
<div>
<Label htmlFor="periodo-de" className="text-xs text-gray-500">DE</Label>
<Select value={filtros.periodoDe} onValueChange={(value) => handleFiltroChange('periodoDe', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{mesesDisponiveis.map(mes => (
<SelectItem key={mes} value={mes}>{mes}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="periodo-ate" className="text-xs text-gray-500">ATÉ</Label>
<Select value={filtros.periodoAte} onValueChange={(value) => handleFiltroChange('periodoAte', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{mesesDisponiveis.map(mes => (
<SelectItem key={mes} value={mes}>{mes}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Grupo
<div className="grid gap-2">
<Label htmlFor="grupo">GRUPO</Label>
<Select value={filtros.grupo} onValueChange={(value) => handleFiltroChange('grupo', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todos">Todos</SelectItem>
{opcoesGrupos.map(grupo => (
<SelectItem key={grupo} value={grupo}>{grupo}</SelectItem>
))}
</SelectContent>
</Select>
</div>*/}
{/* Subgrupo
<div className="grid gap-2">
<Label htmlFor="subgrupo">SUBGRUPO</Label>
<Select value={filtros.subgrupo} onValueChange={(value) => handleFiltroChange('subgrupo', value)}>
<SelectTrigger>
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Todos">Todos</SelectItem>
{opcoesSubgrupos.map(subgrupo => (
<SelectItem key={subgrupo} value={subgrupo}>{subgrupo}</SelectItem>
))}
</SelectContent>
</Select>
</div>*/}
{/* Centro de Custo */}
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="centro-custo">CENTRO DE CUSTO</Label>
<div className="flex gap-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={selecionarTodosCentros}
className="text-xs h-5 px-2"
>
Todos
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={limparCentros}
className="text-xs h-5 px-2"
>
Limpar
</Button>
</div>
</div>
{/* Input de filtro para Centro de Custo */}
<Input
placeholder="Filtrar centros de custo..."
value={filtroCentroCusto}
onChange={(e) => setFiltroCentroCusto(e.target.value)}
className="h-8 text-sm"
/>
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
{opcoesCentrosCusto
.filter(centro => {
if (!filtroCentroCusto) return true;
const termo = filtroCentroCusto.toLowerCase();
const nomeCompleto = `${centro}${codigosCentrosCusto[centro] ? ` - ${codigosCentrosCusto[centro]}` : ''}`;
return nomeCompleto.toLowerCase().includes(termo);
})
.map(centro => (
<div key={centro} className="flex items-center space-x-1">
<Checkbox
id={`centro-${centro}`}
checked={centrosCustoSelecionados.includes(centro)}
onCheckedChange={() => toggleCentroCusto(centro)}
/>
<Label
htmlFor={`centro-${centro}`}
className="text-sm font-normal cursor-pointer flex-1"
>
{centro}{codigosCentrosCusto[centro] ? ` - ${codigosCentrosCusto[centro]}` : ''}
</Label>
</div>
))}
</div>
{centrosCustoSelecionados.length > 0 && (
<div className="text-xs text-gray-500">
{centrosCustoSelecionados.length} centro(s) selecionado(s)
</div>
)}
</div>
{/* Conta */}
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="conta">CONTA</Label>
<div className="flex gap-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={selecionarTodasContas}
className="text-xs h-5 px-2"
>
Todas
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={limparContas}
className="text-xs h-5 px-2"
>
Limpar
</Button>
</div>
</div>
{/* Input de filtro para Conta */}
<Input
placeholder="Filtrar contas..."
value={filtroConta}
onChange={(e) => setFiltroConta(e.target.value)}
className="h-8 text-sm"
/>
<div className="max-h-32 overflow-y-auto border rounded-md p-1 space-y-1">
{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 => (
<div key={conta} className="flex items-center space-x-1">
<Checkbox
id={`conta-${conta}`}
checked={contasSelecionadas.includes(conta)}
onCheckedChange={() => toggleConta(conta)}
/>
<Label
htmlFor={`conta-${conta}`}
className="text-sm font-normal cursor-pointer flex-1"
>
{conta}{codigosContas[conta] ? ` - ${codigosContas[conta]}` : ''}
</Label>
</div>
))}
</div>
{contasSelecionadas.length > 0 && (
<div className="text-xs text-gray-500">
{contasSelecionadas.length} conta(s) selecionada(s)
</div>
)}
</div>
{/* Valor
<div className="grid gap-2">
<Label>Valor</Label>
<div className="grid grid-cols-2 gap-2">
<div>
<Label htmlFor="valor-min" className="text-xs text-gray-500">VALOR MÍN.</Label>
<div className="relative">
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">R$</span>
<Input
id="valor-min"
value={filtros.valorMin}
onChange={(e) => handleFiltroChange('valorMin', e.target.value)}
className="pl-8"
placeholder="0,00"
/>
</div>
</div>
<div>
<Label htmlFor="valor-max" className="text-xs text-gray-500">VALOR MÁX.</Label>
<div className="relative">
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">R$</span>
<Input
id="valor-max"
value={filtros.valorMax}
onChange={(e) => handleFiltroChange('valorMax', e.target.value)}
className="pl-8"
placeholder="0,00"
/>
</div>
</div>
</div>
</div>*/}
{/* Busca Textual
<div className="grid gap-2">
<Label htmlFor="busca-textual">BUSCA TEXTUAL</Label>
<Input
id="busca-textual"
value={filtros.buscaTextual}
onChange={(e) => handleFiltroChange('buscaTextual', e.target.value)}
placeholder="Pesquise por grupo, subgrupo, centro ou conta"
/>
</div>*/}
{/* Ordem da Hierarquia */}
<div className="grid gap-2">
<Label>Ordem da Hierarquia</Label>
<div className="flex items-center space-x-2">
<Checkbox
id="ordem-hierarquia"
checked={ordemHierarquiaContasPrimeiro}
onCheckedChange={(checked: boolean) => setOrdemHierarquiaContasPrimeiro(checked)}
/>
<Label htmlFor="ordem-hierarquia" className="text-sm">
Contas antes de Centros de Custo
</Label>
</div>
<div className="text-xs text-gray-500">
{ordemHierarquiaContasPrimeiro
? "📄 Contas → 🏢 Centros de Custo"
: "🏢 Centros de Custo → 📄 Contas"
}
</div>
</div>
</div>
<SheetFooter className="flex gap-2">
<Button variant="outline" onClick={limparFiltros} className="flex-1">
Limpar Filtros
</Button>
<Button variant="outline" onClick={() => setIsFilterOpen(false)} className="flex-1">
Cancelar
</Button>
<Button onClick={aplicarFiltros} className="flex-1">
Pesquisar
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
</div>
</div>
{/* Loading quando aplicando filtros */}
{loading && (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<LoaderPinwheel className="w-8 h-8 text-blue-600 animate-spin" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-1">
Aplicando filtros...
</h3>
<p className="text-gray-500">
Aguarde enquanto processamos os dados.
</p>
</div>
</div>
</div>
)}
{/* Erro */}
{error && !loading && (
<div className="bg-white rounded-xl shadow-lg border border-red-200 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-red-900 mb-2">
Erro ao carregar dados
</h3>
<p className="text-red-600 mb-4">{error}</p>
<Button onClick={() => aplicarFiltros()} className="flex items-center gap-1">
Tentar novamente
</Button>
</div>
</div>
</div>
)}
{/* Mensagem quando não há dados */}
{!filtrosAplicados && !loading && !error && (
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
<Filter className="w-8 h-8 text-gray-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-1">
Nenhum dado exibido
</h3>
<p className="text-gray-500 mb-2">
Clique no botão "Filtros" para definir os critérios de busca e visualizar os dados do DRE.
</p>
</div>
</div>
</div>
)}
{/* Table Container */}
{filtrosAplicados && !loading && !error && (
<div className="bg-white rounded-xl shadow-lg border border-gray-200">
{/* Scroll Container */}
<div className="overflow-x-auto overflow-y-auto max-h-[500px]">
{/* Table */}
<table className="w-full border-collapse">
{/* Table Header */}
<thead>
<tr className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200">
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase tracking-wide w-[300px] min-w-[300px]">
Descrição
</th>
{mesesDisponiveis.map((mes) => (
<React.Fragment key={mes}>
<th className="px-2 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide w-[120px] min-w-[120px]">
{mes}
</th>
<th className="px-2 py-2 text-center text-xs font-semibold text-gray-500 uppercase tracking-wide w-[100px] min-w-[100px]">
%
</th>
</React.Fragment>
))}
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-700 uppercase tracking-wide w-[120px] min-w-[120px]">
Total
</th>
<th className="px-2 py-2 text-center text-xs font-semibold text-gray-500 uppercase tracking-wide w-[100px] min-w-[100px]">
%
</th>
</tr>
</thead>
{/* Table Body */}
<tbody>
{hierarchicalData.map((row, index) => (
<TableRow
key={index}
row={row}
index={index}
toggleGroup={toggleGroup}
toggleCentro={toggleCentro}
handleRowClick={handleRowClick}
getRowStyle={getRowStyle}
getIndentStyle={getIndentStyle}
renderCellContent={renderCellContent}
mesesDisponiveis={mesesDisponiveis}
formatCurrency={formatCurrency}
formatCurrencyWithColor={formatCurrencyWithColor}
/>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Componente Analítico - Sempre renderizado para evitar violação das Rules of Hooks */}
<AnaliticoComponent filtros={analiticoFiltros} />
</div>
);
}